From c4f3968ec9924aa1ebcade3803a331d1cde327e1 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Fri, 25 Sep 2026 18:28:32 +0800 Subject: [PATCH 01/24] Re-pin the validation corpus to f989e36: special-validation builds, owned_strategies.json re-derived (lane CORPUS3) pineforge-corpus f989e36 (branch release/specials-owned, two commits on eede4a2) closes the corpus's two release-checklist items. Neither touches the 312 validation probes: validation/ and data/ are byte-identical to eede4a2, so no generated.cpp, trade list, tape or tier moves. - 1d3ed90: special-validation/build_specials.sh compiles against the engine's build tree. It lacked -I build/include, so every compile died on the generated ; it now adds it, checks for the header as it checks for libpineforge.a, and takes Eigen from EIGEN3_INCLUDE_DIR, else the build's FetchContent copy (build/_deps/eigen-src), else Homebrew. The six special-validation generated.cpp, last emitted 2026-06-10 and deriving GeneratedStrategy from BacktestEngine (15-20 errors each against this engine), are re-derived from their strategy.pine with codegen f3285d7 through the transpile call of scripts/regen_corpus_cpp.sh, whose corpus/*/*/ glob never reaches special-validation///; all six now compile and link. The two whose feed the corpus ships run (mtf-htf-monthly-ema-cross-01, leverage-margin-call-perp-5x-01); the other four need licensed OHLCV and a TradingView export that are not in the corpus, and no probe has a TV tape to grade against. - f989e36: owned_strategies.json is re-derived from the tree and this engine's verifier. generated_cpp_sha256 matches 60/60 (was 0/60); the other five artifact digests re-check 60/60; tier_distribution is {excellent 311, anomaly 1} (was {excellent 307, strong 4, anomaly 1}); validation_run is this engine's full parity sweep at eede4a2 (91d65ad6, 2026-09-25); native_validation moves on five entries, all still excellent. The pins that name the corpus commit move together: - the corpus gitlink, eede4a2 -> f989e36; - the `# corpus` header of scripts/corpus_parity_baseline.txt, re-recorded with `corpus_trades_identity.py --update` over this engine's 312 trade lists on disk (all 312 re-run at eede4a2 first: baseline match=312, moved=0), after the check at the new gitlink refused on that header, as it must. The baseline diff is that one line; all 312 sha256 rows are unchanged. That header was the only citation of eede4a2 in the tree outside corpus/, and it is a pin statement. The paired-codegen pin (CODEGEN_COMMIT f3285d7 in scripts/regen_corpus_cpp.sh) does not move: `VERIFY=1` at f3285d7 over this corpus still reports 314 verified, 0 drifted. 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/corpus3 (migration 0041) 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 ff8bf5cf5ce9d36bf7b72e5f8ac27ecacc0c7bfb) --- corpus | 2 +- scripts/corpus_parity_baseline.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/corpus b/corpus index eede4a22..f989e362 160000 --- a/corpus +++ b/corpus @@ -1 +1 @@ -Subproject commit eede4a22978051f5f13d791e99b2151fbbbfa073 +Subproject commit f989e36276d6993d862b8d392df89fc49aacee04 diff --git a/scripts/corpus_parity_baseline.txt b/scripts/corpus_parity_baseline.txt index 06c7f2b6..e676c228 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 eede4a22978051f5f13d791e99b2151fbbbfa073 +# corpus f989e36276d6993d862b8d392df89fc49aacee04 a72ee904563a1787ca8371f37f98cec74f6fb44163a5dfac169b77755a05ed1a validation/analyzer-anvil-percent-costs-01/engine_trades.csv df7fbdb2082c8982e8c38d8843fc53262df693d4ce8b32bf05bc0acd1ef41a16 validation/analyzer-cashflow-cash-costs-01/engine_trades.csv From 4552eb8d6862d2f83a577f3adeac42e97c9d87af Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Fri, 25 Sep 2026 20:05:45 +0800 Subject: [PATCH 02/24] tests: harden timing ratio legs against runner noise (cherry picked from commit aac30d6230eb0d51d1f351d366842a495b4044bb) --- tests/test_adapter_live_state_scaling.cpp | 48 ++++++----- tests/test_adapter_lookup_index_scaling.cpp | 33 ++++++-- tests/test_native_bare_host_contracts.cpp | 50 +++++++---- .../test_native_continuation_digest_tail.cpp | 75 ++++++++++------- tests/test_native_handle_stable_replace.cpp | 54 +++++++----- tests/test_native_intrabar_lower_lookup.cpp | 51 ++++++++---- tests/test_native_journal_window.cpp | 38 +++++---- tests/test_native_match_rescan_scaling.cpp | 31 +++++-- tests/test_native_state_continuation.cpp | 82 ++++++++++++------- 9 files changed, 306 insertions(+), 156 deletions(-) diff --git a/tests/test_adapter_live_state_scaling.cpp b/tests/test_adapter_live_state_scaling.cpp index 66728870..1f79f6e5 100644 --- a/tests/test_adapter_live_state_scaling.cpp +++ b/tests/test_adapter_live_state_scaling.cpp @@ -67,14 +67,16 @@ // without recording (ratio 10.3), retaining 7,999 and 31,999 rows, and with // recording 2.32 s at 1,000 bars and 36.75 s at 4,000. // -// R5 INT24 sizes the re-cancel's leg without recording by time. Its bars cost +// R5 INT24 sized the re-cancel's leg without recording by time. Its bars cost // about two microseconds (Mac, Release), so at 4,000 bars the small leg was a // few milliseconds of CPU, and noise on a shared runner decided the ratio: // INT23's CI run 36074490963 measured 5.66 on the hosted macOS Release runner -// at an 18 ms small leg (the Mac itself 3.42-4.07). The small leg now doubles -// from 4,000 bars until one run takes kMinLegSeconds, and 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. +// at an 18 ms small leg (the Mac itself 3.42-4.07). RATIO-HARDEN applies the +// same 100 ms minimum to every workload and recording mode. Each small leg +// 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. // 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 @@ -275,10 +277,11 @@ Leg best_of(Workload workload, const std::vector& bars, bool recording) { constexpr int kBars = 1000; constexpr double kShapeBound = 5.0; -// The re-cancel's calibrated small leg (see the file's comment): at least this -// much CPU, and at most kMaxCalibratedBars bars. +// Every ratio leg is calibrated to at least this much process CPU. The cap +// keeps a pathological mutant from turning a normal ctest row into an +// unbounded run. constexpr double kMinLegSeconds = 0.1; -constexpr int kMaxCalibratedBars = 128000; +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. constexpr std::size_t kRowBound = 64; @@ -307,21 +310,22 @@ void cost_and_rows_are_live(Workload workload, const char* name, bool recording const int scale = short_bars ? 4 : 1; int bars = (gated() ? kBars : kBars / 4) * scale; const std::int64_t step = workload == Workload::Straddle ? kDay : kMinute; - if (gated() && workload == Workload::Recancel && !recording) { - while (bars * 2 <= kMaxCalibratedBars - && replay(workload, tape(bars, step), recording).seconds < kMinLegSeconds) { - bars *= 2; - } - } - const std::vector small_tape = tape(bars, step); - const std::vector large_tape = tape(bars * 4, step); Leg small; Leg large; - 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); + while (true) { + const std::vector small_tape = tape(bars, step); + const std::vector large_tape = tape(bars * 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; } 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), " @@ -352,6 +356,8 @@ void cost_and_rows_are_live(Workload workload, const char* name, bool recording std::printf(" (ratio not gated: non-Release library)\n"); return; } + CHECK(small.seconds >= kMinLegSeconds); + CHECK(large.seconds >= kMinLegSeconds); if (workload != Workload::Churn) CHECK(ratio < kShapeBound); } diff --git a/tests/test_adapter_lookup_index_scaling.cpp b/tests/test_adapter_lookup_index_scaling.cpp index af96aed1..88c683ce 100644 --- a/tests/test_adapter_lookup_index_scaling.cpp +++ b/tests/test_adapter_lookup_index_scaling.cpp @@ -14,13 +14,14 @@ // bracket re-issued every bar adds legs every bar. PERF-D1 measured probe // 044 (two quantity brackets per entry) at 55 % in exit(). // So a run four times longer cost far more than four times as much. This row -// holds the SHAPE of the cost: each workload at 6,000 and at 24,000 bars, -// process CPU time (std::clock), best of five per leg, and the ratio must -// stay under 5 -- four for a linear run, plus room for noise and for the -// per-bar work a shorter run amortizes worse. Measured on this TU (macOS -// arm64, load ~11-19) at the lane's base f71cd820: reversals 9.68, brackets -// 7.68 (per bar 10.2 -> 40.0 and 6.4 -> 22.5 us from 6,000 to 48,000 bars); -// with the lane's indexes 4.07 and 3.94 (8.2 -> 8.6 and 4.9 -> 5.4 us). +// holds the SHAPE of the cost with process CPU time (std::clock), best of five +// per leg, and a ratio under 5 -- four for a linear run, plus room for noise +// and for the per-bar work a shorter run amortizes worse. The small leg is +// calibrated independently for each workload until its best sample has at +// 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). // 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 @@ -154,11 +155,23 @@ Leg best_of_five(Workload workload, const std::vector& bars) { return best; } -constexpr int kBars = 6000; +constexpr int kInitialBars = 6000; +constexpr int kMaxCalibratedBars = 96000; +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; + } +} + void cost_is_linear_in_the_bars(Workload workload, const char* name) { - const int bars = gated() ? kBars : kBars / 4; + 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); @@ -176,6 +189,8 @@ void cost_is_linear_in_the_bars(Workload workload, const char* name) { std::printf(" (ratio not gated: non-Release library)\n"); return; } + CHECK(small.seconds >= kMinLegSeconds); + CHECK(large.seconds >= kMinLegSeconds); CHECK(ratio < kShapeBound); } diff --git a/tests/test_native_bare_host_contracts.cpp b/tests/test_native_bare_host_contracts.cpp index bc532f2f..17020485 100644 --- a/tests/test_native_bare_host_contracts.cpp +++ b/tests/test_native_bare_host_contracts.cpp @@ -535,7 +535,9 @@ void owned_excursion_is_recorded_verbatim() { // 24 ms; lanes PERF-K24 (lower-feed lookup) and PERF-K1 (calendar memo) cut that // run to about 2.2 ms, and the ratio is 3.3-3.5x (1.6x in Debug). A fold that // 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. +// 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. struct LowerPathReader final : NativeStrategyHost { bool read_each_bar = false; std::uint64_t last_read = 0; @@ -549,7 +551,7 @@ struct ReadCost { std::uint64_t digest = 0; }; -ReadCost lower_path_read_cost(int script_bars, bool read_each_bar) { +ReadCost lower_path_read_cost(int script_bars, bool read_each_bar, int repeats) { std::vector bars; IntrabarPath::lower_tf path; path.tf = "1"; @@ -568,29 +570,47 @@ ReadCost lower_path_read_cost(int script_bars, bool read_each_bar) { s.intrabar.value = path; ReadCost best; for (int attempt = 0; attempt < 3; ++attempt) { - LowerPathReader host; - host.read_each_bar = read_each_bar; - CHECK(host.configure_native(s).status == NativeSetupStatus::Applied); const std::clock_t start = std::clock(); - host.run(bars.data(), static_cast(bars.size())); - const double seconds = static_cast(std::clock() - start) / CLOCKS_PER_SEC; - CHECK(host.native_state().kind == NativeLifecycleKind::Completed); - if (attempt == 0 || seconds < best.seconds) best.seconds = seconds; - best.digest = host.native_continuation_hash(); - if (read_each_bar) CHECK(host.last_read != 0); + double seconds = 0.0; + std::uint64_t digest = 0; + for (int repeat = 0; repeat < repeats; ++repeat) { + LowerPathReader host; + host.read_each_bar = read_each_bar; + CHECK(host.configure_native(s).status == NativeSetupStatus::Applied); + host.run(bars.data(), static_cast(bars.size())); + CHECK(host.native_state().kind == NativeLifecycleKind::Completed); + digest = host.native_continuation_hash(); + if (read_each_bar) CHECK(host.last_read != 0); + } + seconds = static_cast(std::clock() - start) / CLOCKS_PER_SEC; + if (attempt == 0 || seconds < best.seconds) { + best.seconds = seconds; + best.digest = digest; + } } return best; } void continuation_read_is_linear_in_the_feed() { - const ReadCost every_bar = lower_path_read_cost(2000, true); - const ReadCost once = lower_path_read_cost(2000, false); + constexpr int kScriptBars = 2000; + constexpr int kMaxRepeats = 256; + 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); const double ratio = every_bar.seconds / std::max(once.seconds, 1e-6); - std::printf(" 2000 bars over a lower-timeframe path: continuation read every bar %.4f s," - " read once %.4f s, ratio %.2f\n", every_bar.seconds, once.seconds, ratio); + 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, + every_bar.seconds, once.seconds, ratio); // expectation corrected: ratio < 3.0 -> ratio < 20.0, because K24 and K1 // made the read-once run about 11x cheaper while each read costs what it // did (see above); the quadratic re-walk this guards against is 440x+. + CHECK(every_bar.seconds >= kMinLegSeconds); + CHECK(once.seconds >= kMinLegSeconds); CHECK(ratio < 20.0); CHECK(every_bar.digest == once.digest); } diff --git a/tests/test_native_continuation_digest_tail.cpp b/tests/test_native_continuation_digest_tail.cpp index 37d446c7..015fb1a8 100644 --- a/tests/test_native_continuation_digest_tail.cpp +++ b/tests/test_native_continuation_digest_tail.cpp @@ -21,7 +21,9 @@ // smaller leg) and about 15.7x while it is quadratic, so 8 separates the two // shapes with more than a factor of two of margin on either side. The sample // 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. +// 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. #include #include @@ -85,52 +87,66 @@ std::vector tape(int count) { return bars; } -std::uint64_t replay(int count, bool read_each_bar, double* cpu_seconds) { - Probe host; - host.read_each_bar = read_each_bar; - const auto setup = host.configure_native(probe_spec()); - CHECK(setup.status == NativeSetupStatus::Applied); +std::uint64_t replay(int count, bool read_each_bar, double* cpu_seconds, int repeats = 1) { const std::vector bars = tape(count); - const std::clock_t started = std::clock(); - host.run(bars.data(), count); - if (cpu_seconds) { - *cpu_seconds = static_cast(std::clock() - started) / CLOCKS_PER_SEC; + double total = 0.0; + std::uint64_t digest = 0; + for (int repeat = 0; repeat < repeats; ++repeat) { + Probe host; + host.read_each_bar = read_each_bar; + const auto setup = host.configure_native(probe_spec()); + CHECK(setup.status == NativeSetupStatus::Applied); + const std::clock_t started = std::clock(); + host.run(bars.data(), count); + total += static_cast(std::clock() - started) / CLOCKS_PER_SEC; + digest = host.native_continuation_hash(); + CHECK(host.last_error().empty()); + CHECK(host.native_state().kind == NativeLifecycleKind::Completed); } - CHECK(host.last_error().empty()); - CHECK(host.native_state().kind == NativeLifecycleKind::Completed); - return host.native_continuation_hash(); + if (cpu_seconds) *cpu_seconds = total; + return digest; } -double best_of_three(int count) { +double best_of_three(int count, int repeats) { double best = 0.0; for (int attempt = 0; attempt < 3; ++attempt) { double sample = 0.0; - replay(count, true, &sample); + replay(count, true, &sample, repeats); if (attempt == 0 || sample < best) best = sample; } return best; } constexpr int kBars = 1500; +constexpr int kMaxRepeats = 256; +constexpr double kMinLegSeconds = 0.1; constexpr double kShapeBound = 8.0; -void the_per_bar_read_is_linear_in_the_bar_count() { - const double small = best_of_three(kBars); - const double large = best_of_three(kBars * 4); - const double ratio = small > 0.0 ? large / small : 0.0; - if (!(small > 0.0) || !(ratio < kShapeBound)) { - std::fprintf(stderr, - "continuation digest cost: %d bars %.4fs, %d bars %.4fs, ratio %.2f " - "(bound %.1f)\n", - kBars, small, kBars * 4, large, ratio, kShapeBound); +int calibrated_repeats() { + int repeats = 1; + while (best_of_three(kBars, repeats) < kMinLegSeconds + && repeats < kMaxRepeats / 2) { + repeats *= 2; } + return 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); + 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, + kShapeBound); CHECK(small > 0.0); + CHECK(small >= kMinLegSeconds); + CHECK(large >= kMinLegSeconds); CHECK(ratio < kShapeBound); } -void the_digest_does_not_depend_on_when_it_is_read() { - const std::uint64_t read_every_bar = replay(kBars, true, nullptr); - const std::uint64_t read_once = replay(kBars, false, nullptr); +void the_digest_does_not_depend_on_when_it_is_read(int repeats) { + const std::uint64_t read_every_bar = replay(kBars, true, nullptr, repeats); + const std::uint64_t read_once = replay(kBars, false, nullptr, repeats); CHECK(read_every_bar == read_once); CHECK(read_once != 0); } @@ -138,8 +154,9 @@ void the_digest_does_not_depend_on_when_it_is_read() { } // namespace int main() { - the_digest_does_not_depend_on_when_it_is_read(); - the_per_bar_read_is_linear_in_the_bar_count(); + const int repeats = calibrated_repeats(); + the_digest_does_not_depend_on_when_it_is_read(repeats); + the_per_bar_read_is_linear_in_the_bar_count(repeats); if (failures == 0) std::printf("test_native_continuation_digest_tail: ok\n"); return failures == 0 ? 0 : 1; } diff --git a/tests/test_native_handle_stable_replace.cpp b/tests/test_native_handle_stable_replace.cpp index 941e2860..a058444a 100644 --- a/tests/test_native_handle_stable_replace.cpp +++ b/tests/test_native_handle_stable_replace.cpp @@ -52,10 +52,10 @@ #include #include -#include #include #include #include +#include #include #include #include @@ -1097,37 +1097,53 @@ class ContinuationHost final : public NativeStrategyHost { std::optional tp_, sl_; }; -double continuation_seconds(int bars) { +double continuation_seconds(int bars, int repeats) { k3_book::BookConfig tape_config; tape_config.seed = 99; tape_config.bars = bars; const auto tape = k3_book::make_tape(tape_config); NativeRunSpec spec = directed_spec(); - ContinuationHost host; - CHECK(host.configure_native(spec).status == NativeSetupStatus::Applied); - const auto start = std::chrono::steady_clock::now(); - host.run(tape.bars.data(), static_cast(tape.bars.size())); - const double seconds = - std::chrono::duration(std::chrono::steady_clock::now() - start).count(); - CHECK(host.last_error().empty()); - CHECK(host.native_state().kind == NativeLifecycleKind::Completed); - // A range per bar: a walk of them at every read would be quadratic. - CHECK(host.issued_ranges() + 10 > static_cast(bars)); + double seconds = 0.0; + for (int repeat = 0; repeat < repeats; ++repeat) { + ContinuationHost host; + CHECK(host.configure_native(spec).status == NativeSetupStatus::Applied); + const std::clock_t start = std::clock(); + host.run(tape.bars.data(), static_cast(tape.bars.size())); + seconds += static_cast(std::clock() - start) / CLOCKS_PER_SEC; + CHECK(host.last_error().empty()); + CHECK(host.native_state().kind == NativeLifecycleKind::Completed); + // A range per bar: a walk of them at every read would be quadratic. + CHECK(host.issued_ranges() + 10 > static_cast(bars)); + } return seconds; } void scaling() { - constexpr int kSmall = 2000; + constexpr int kInitialSmall = 2000; + constexpr int kMaxRepeats = 256; + constexpr double kMinLegSeconds = 0.1; + int repeats = 1; + while (continuation_seconds(kInitialSmall, repeats) < kMinLegSeconds + && repeats < kMaxRepeats / 2) { + repeats *= 2; + } double best_small = 1e9, best_large = 1e9; - for (int round = 0; round < 3; ++round) { - best_small = std::min(best_small, continuation_seconds(kSmall)); - best_large = std::min(best_large, continuation_seconds(4 * kSmall)); + 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)); + } } const double ratio = best_large / best_small; + CHECK(best_small >= kMinLegSeconds); + CHECK(best_large >= kMinLegSeconds); CHECK(ratio < 8.0); - std::printf("continuation read every bar, re-prices keeping handles: %d bars %.4f s, %d bars " - "%.4f s (x%.2f, bound x8)\n", - kSmall, best_small, 4 * kSmall, best_large, ratio); + std::printf("continuation read every bar, re-prices keeping handles: %d bars x%d %.4f s, " + "%d bars x%d %.4f s (x%.2f, bound x8)\n", kInitialSmall, repeats, + best_small, 4 * kInitialSmall, repeats, best_large, ratio); } } // namespace diff --git a/tests/test_native_intrabar_lower_lookup.cpp b/tests/test_native_intrabar_lower_lookup.cpp index c6e17427..129dd4bd 100644 --- a/tests/test_native_intrabar_lower_lookup.cpp +++ b/tests/test_native_intrabar_lower_lookup.cpp @@ -22,7 +22,9 @@ // Linux aarch64 (gcc 13, one pinned core). 6 separates the two shapes with // more than 40 % of room above the linear one and a factor of 2.6 below the // quadratic one. The sample is process CPU time (std::clock), which the -// runtime-budget ruling (A40 rev 7) found load-robust. +// 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. // // 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 @@ -206,38 +208,55 @@ struct Bracket final : NativeStrategyHost { // the run's cost is the driver's and the lookup's alone, and a scan of the // feed stands out against it. (Under canonical labels the per-bar calendar // dominates a run this short and would mask the shape.) -double run_cpu(int count) { - Idle host; - const auto setup = host.configure_native(lower_spec(Feed::Tiling, count, true, "k24-scale")); - CHECK(setup.status == NativeSetupStatus::Applied); +double run_cpu(int count, int repeats) { + const NativeRunSpec spec = lower_spec(Feed::Tiling, count, true, "k24-scale"); const std::vector bars = script_feed(count); const std::clock_t started = std::clock(); - host.run(bars.data(), count); - const double seconds = static_cast(std::clock() - started) / CLOCKS_PER_SEC; - CHECK(host.last_error().empty()); - CHECK(host.native_state().kind == NativeLifecycleKind::Completed); - return seconds; + for (int repeat = 0; repeat < repeats; ++repeat) { + Idle host; + const auto setup = host.configure_native(spec); + CHECK(setup.status == NativeSetupStatus::Applied); + host.run(bars.data(), count); + CHECK(host.last_error().empty()); + CHECK(host.native_state().kind == NativeLifecycleKind::Completed); + } + return static_cast(std::clock() - started) / CLOCKS_PER_SEC; } -double best_of_three(int count) { +double best_of_three(int count, int repeats) { double best = 0.0; for (int attempt = 0; attempt < 3; ++attempt) { - const double sample = run_cpu(count); + const double sample = run_cpu(count, repeats); if (attempt == 0 || sample < best) best = sample; } return best; } constexpr int kBars = 4000; +constexpr int kMaxRepeats = 256; +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; +} + void the_lower_feed_lookup_is_linear_in_the_run() { - const double small = best_of_three(kBars); - const double large = best_of_three(kBars * 4); + const int repeats = calibrated_repeats(); + const double small = best_of_three(kBars, repeats); + const double large = best_of_three(kBars * 4, repeats); const double ratio = small > 0.0 ? large / small : 0.0; - std::printf("lower-feed run: %d bars %.4fs, %d bars %.4fs, ratio %.2f (bound %.1f)\n", - kBars, small, kBars * 4, large, ratio, kShapeBound); + 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, + ratio, kShapeBound); CHECK(small > 0.0); + CHECK(small >= kMinLegSeconds); + CHECK(large >= kMinLegSeconds); CHECK(ratio < kShapeBound); } diff --git a/tests/test_native_journal_window.cpp b/tests/test_native_journal_window.cpp index 92069738..d2b4512e 100644 --- a/tests/test_native_journal_window.cpp +++ b/tests/test_native_journal_window.cpp @@ -14,7 +14,9 @@ // of the unretired journal says the pre-window core did; // 3. a scaling row: canonicalizing and testing the members of a long // replace chain costs O(log) per query, not the chain walk the journal -// scan made quadratic (4x the chain must cost < 5x); +// scan made quadratic (4x the chain must cost < 5x). The query loop is +// calibrated to at least 100 ms of process CPU before the ratio is read; +// the old sub-millisecond escape is therefore unnecessary. // 4. a live deferred group-adjustment chain pins the window at its head, // and the arm that reads it back through collect_pending_chain answers // as it does on a core that retired nothing; @@ -24,9 +26,9 @@ #include #include -#include #include #include +#include #include #include #include @@ -398,7 +400,7 @@ void the_chain_index_answers_what_the_journal_did(uint64_t seed) { // Each query is O(log) in the chain index; the journal scan the pre-window // core made walked the chain back link by link, each link a backward search // of the journal, so the same queries cost the chain length squared. -double chain_queries_seconds(int chain) { +double chain_queries_seconds(int chain, int queries) { const no::RunIdentity run{"journal-scaling", 1}; no::WorkingRequestCore core(run); uint64_t inc = 1; @@ -413,32 +415,40 @@ double chain_queries_seconds(int chain) { } const auto cohort = core.cohort_open(); SplitMix rng{static_cast(chain)}; - const auto start = std::chrono::steady_clock::now(); + const std::clock_t start = std::clock(); core.cohort_add(cohort, tail); std::size_t found = 0; - constexpr int kQueries = 2048; - for (int query = 0; query < kQueries; ++query) { + for (int query = 0; query < queries; ++query) { found += core.cohort_contains(cohort, members[rng.below(members.size())]) ? 1 : 0; } - const double seconds = - std::chrono::duration(std::chrono::steady_clock::now() - start).count(); + const double seconds = static_cast(std::clock() - start) / CLOCKS_PER_SEC; CHECK(core.cohort_receipts().back().status == no::CohortReceiptStatus::Applied); - CHECK(found == static_cast(kQueries)); + CHECK(found == static_cast(queries)); return seconds; } 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; + } // 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)); - large = std::min(large, chain_queries_seconds(1600)); + small = std::min(small, chain_queries_seconds(400, queries)); + large = std::min(large, chain_queries_seconds(1600, queries)); } const double ratio = large / std::max(small, 1e-9); - std::printf(" 2048 chain-membership queries, 400 -> 1600 replaces: %.6f s -> %.6f s " - "(x%.2f for 4x)\n", small, large, ratio); - CHECK(ratio < 5.0 || large < 0.001); + std::printf(" %d chain-membership queries, 400 -> 1600 replaces: %.6f s -> %.6f s " + "(x%.2f for 4x, bound x5)\n", queries, small, large, ratio); + CHECK(small >= kMinLegSeconds); + CHECK(large >= kMinLegSeconds); + CHECK(ratio < 5.0); } // ── 4. a deferred group-adjustment chain pins the window ───────────────── diff --git a/tests/test_native_match_rescan_scaling.cpp b/tests/test_native_match_rescan_scaling.cpp index 5f8b72c8..76242596 100644 --- a/tests/test_native_match_rescan_scaling.cpp +++ b/tests/test_native_match_rescan_scaling.cpp @@ -22,6 +22,10 @@ // (test_native_match_hash_witness) and compared bit for bit against the // 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 +// stay unchanged; sizing the legs, rather than widening the bound, keeps the +// old full-rescan mutant above the gate. #include #include @@ -117,18 +121,35 @@ double best_of_three(int live, const std::vector& bars) { return best; } -constexpr int kBars = 3000; +constexpr int kInitialBars = 3000; +constexpr int kMaxCalibratedBars = 384000; +// 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 +// x8 shape bound. +constexpr double kMinLegSeconds = 0.25; constexpr int kLive = 10; constexpr double kShapeBound = 8.0; void matching_is_linear_in_the_live_requests() { - const std::vector bars = tape(kBars); - const double small = best_of_three(kLive, bars); + int bars_count = kInitialBars; + double small = 0.0; + while (true) { + const std::vector bars = tape(bars_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); const double ratio = small > 0.0 ? large / small : 0.0; - std::printf("resting orders: %d live %.4fs, %d live %.4fs, ratio %.2f (bound %.1f)\n", - kLive, small, kLive * 4, large, ratio, kShapeBound); + 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, + kShapeBound); CHECK(small > 0.0); + CHECK(small >= kMinLegSeconds); + CHECK(large >= kMinLegSeconds); CHECK(ratio < kShapeBound); } diff --git a/tests/test_native_state_continuation.cpp b/tests/test_native_state_continuation.cpp index d790a101..205f36a6 100644 --- a/tests/test_native_state_continuation.cpp +++ b/tests/test_native_state_continuation.cpp @@ -734,27 +734,30 @@ double cpu_seconds() { return static_cast(std::clock()) / CLOCKS_PER_SEC // One bracket round trip every ten bars, a kernel-recorded report and the // per-bar broker hash recorded: the recording's cost, one run. -double recorded_seconds(int bars) { - Host host; - host.bar_script = [](Host& h, int bar) { - if (bar % 10 == 0) h.submit(no::Request{no::Transact{1.0}, "in", ""}); - if (bar % 10 == 5) h.submit(no::Request{no::Flatten{}, "out", ""}); - }; +double recorded_seconds(int bars, int repeats) { auto spec = base_spec(); spec.report_policy = NativeReportPolicy::KernelRecorded; - if (host.configure_native(spec).status != NativeSetupStatus::Applied) return 0.0; - host.set_broker_state_hash_recording(true); const auto input = tape(bars); - const double start = cpu_seconds(); - host.run(input.data(), bars); - const double spent = cpu_seconds() - start; - CHECK(host.native_state().kind == NativeLifecycleKind::Completed); - CHECK(host.rows().size() == static_cast(bars / 10)); + double spent = 0.0; + for (int repeat = 0; repeat < repeats; ++repeat) { + Host host; + host.bar_script = [](Host& h, int bar) { + if (bar % 10 == 0) h.submit(no::Request{no::Transact{1.0}, "in", ""}); + if (bar % 10 == 5) h.submit(no::Request{no::Flatten{}, "out", ""}); + }; + if (host.configure_native(spec).status != NativeSetupStatus::Applied) return 0.0; + host.set_broker_state_hash_recording(true); + const double start = cpu_seconds(); + host.run(input.data(), bars); + spent += cpu_seconds() - start; + CHECK(host.native_state().kind == NativeLifecycleKind::Completed); + CHECK(host.rows().size() == static_cast(bars / 10)); + } return spent; } // A continuation read at a bar, timed over many reads. -double read_seconds(int bars, int read_at) { +double read_seconds(int bars, int read_at, int reads) { Host host; double spent = 0.0; host.bar_script = [&](Host& h, int bar) { @@ -765,7 +768,7 @@ double read_seconds(int bars, int read_at) { std::uint64_t sink = 0; for (int round = 0; round < 5; ++round) { const double start = cpu_seconds(); - for (int i = 0; i < 2000; ++i) sink ^= h.native_continuation_hash(); + for (int i = 0; i < reads; ++i) sink ^= h.native_continuation_hash(); best = std::min(best, cpu_seconds() - start); } spent = best; @@ -787,43 +790,66 @@ double read_seconds(int bars, int read_at) { // Each bound sits between the two shapes it tells apart, a factor of two // from each: linear recording is x4 for 4x the bars and the quadratic walk // 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. +// 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. constexpr int kScalingRounds = 5; +constexpr double kMinLegSeconds = 0.1; +constexpr int kMaxTimingRepeats = 256; void recording_and_reads_scale() { + int recording_repeats = 1; + while (recorded_seconds(5000, recording_repeats) < kMinLegSeconds + && recording_repeats < kMaxTimingRepeats / 2) { + recording_repeats *= 2; + } 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)); - whole = std::min(whole, recorded_seconds(20000)); + 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)); - quarter = std::min(quarter, recorded_seconds(5000)); + whole = std::min(whole, recorded_seconds(20000, recording_repeats)); + quarter = std::min(quarter, recorded_seconds(5000, recording_repeats)); } } 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 %.4f s, 20000 bars %.4f s (x%.2f for 4x the bars, bound x8)\n", - kScalingRounds, quarter, whole, ratio); + "5000 bars x%d %.4f s, 20000 bars x%d %.4f s (x%.2f for 4x the bars, bound x8)\n", + kScalingRounds, recording_repeats, quarter, recording_repeats, whole, ratio); CHECK(ratio > 0.0); + 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)); - late = std::min(late, read_seconds(10000, 9999)); + 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)); - early = std::min(early, read_seconds(10000, 2500)); + late = std::min(late, read_seconds(10000, 9999, read_repeats)); + early = std::min(early, read_seconds(10000, 2500, read_repeats)); } } const double read_ratio = early > 0.0 ? late / early : 0.0; - std::printf(" continuation read, 2000 reads, best of %d interleaved rounds: at bar 2500 " + 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", - kScalingRounds, early, late, read_ratio); + read_repeats, kScalingRounds, early, late, read_ratio); CHECK(read_ratio > 0.0); + CHECK(early >= kMinLegSeconds); + CHECK(late >= kMinLegSeconds); CHECK(read_ratio < 2.0); } From c6e0bb7d181ace3546cf789709cac2032b193ff3 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Fri, 25 Sep 2026 19:20:40 +0800 Subject: [PATCH 03/24] C-SURFACE-1: preserve published C callback layouts Rule 2 ruling: the C callback host mirrors the generic C++ kernel contract by published byte lengths and callback frames. It adds no Pine or TradingView knob and moves no durable state or hash value. Pin the INT23 caller, strict enum width, local refusals, and observed command legality; refresh exact documentation anchors. (cherry picked from commit 7114139f15877b3da555f8c7cae5e763593bccaf) INT25 merge: scripts/ci_verify.py keeps CI-LITE's EXCLUDED_REGISTERED_MIN beside this lane's floors (673/272); five anchors into native_c_api.h, pineforge.h and c_abi.cpp the lane's pages carried unmoved over its own insertions are carried through the exact line map to the content they cited (int24_exact_reanchor). --- CONTRIBUTING.md | 4 +- docs/adr/0001-kernel-adapter-boundary.md | 67 +- docs/design/native-feature-parity.md | 24 +- docs/pages/contributing-llm.md | 8 +- docs/pages/native-engine.md | 33 +- docs/pages/pine-to-native.md | 350 +- include/pineforge/native_c_api.h | 67 +- include/pineforge/pineforge.h | 28 +- scripts/ci_verify.py | 9 +- src/c_abi.cpp | 39 +- src/native_c_host.cpp | 181 +- tests/CMakeLists.txt | 11 + .../int23/pineforge/native_c_api.h | 3102 +++++++++++++++++ .../native_c_api/int23/pineforge/pineforge.h | 1444 ++++++++ tests/test_native_c_api.c | 293 ++ tests/test_native_c_api_int23_header.c | 77 + 16 files changed, 5402 insertions(+), 335 deletions(-) create mode 100644 tests/fixtures/native_c_api/int23/pineforge/native_c_api.h create mode 100644 tests/fixtures/native_c_api/int23/pineforge/pineforge.h create mode 100644 tests/test_native_c_api_int23_header.c diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 91a0113f..834f3673 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:229 and `RELEASE_MIN_TESTS` -ci_verify.py:332. +floor — `KERNEL_MIN_TESTS` ci_verify.py:230 and `RELEASE_MIN_TESTS` +ci_verify.py:333. 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 e43255d6..2ad734f3 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -181,7 +181,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:490`). A host folds its **own** durable state into it through + (`c_abi.cpp:504`). 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. @@ -324,16 +324,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:2598`), runs a batch - (`strategy_native_run_v1` `native_c_api.h:2611`), and **submits, replaces, cancels and - executes** orders — `strategy_native_submit_v1` (`native_c_api.h:2639`), `_replace_v1` - (`native_c_api.h:2661`), `_cancel_v1` (`native_c_api.h:2701`), `_cancel_all_v1` (`native_c_api.h:2705`), `_cancel_where_v1` (`native_c_api.h:2726`), - `_execute_current_v1` (`native_c_api.h:2747`) — under the kernel's own legality rule. Streaming needs no new + callback table (`strategy_native_host_create_v1` `native_c_api.h:2605`), runs a batch + (`strategy_native_run_v1` `native_c_api.h:2618`), and **submits, replaces, cancels and + executes** orders — `strategy_native_submit_v1` (`native_c_api.h:2646`), `_replace_v1` + (`native_c_api.h:2668`), `_cancel_v1` (`native_c_api.h:2708`), `_cancel_all_v1` (`native_c_api.h:2712`), `_cancel_where_v1` (`native_c_api.h:2733`), + `_execute_current_v1` (`native_c_api.h:2754`) — 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:611`), which is why a C host frees its report + codegen-emitted (`include/pineforge/pineforge.h:629`), 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 @@ -577,30 +577,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:2159`), `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:3008`), reading a SIZED request's own sizing block | the sizing-query scenario | -| `resolve_execution_terms`, price half | `on_execution_terms` (`native_c_api.h:2525`): 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:2534`): 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:2541`) | the anchored-level scenario | -| `hash_host_extension` | `on_hash_extension` (`native_c_api.h:2552`): 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:2163`), `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:3015`), reading a SIZED request's own sizing block | the sizing-query scenario | +| `resolve_execution_terms`, price half | `on_execution_terms` (`native_c_api.h:2530`): 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:2539`): 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:2546`) | the anchored-level scenario | +| `hash_host_extension` | `on_hash_extension` (`native_c_api.h:2557`): 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:1232`) / `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:2705`). | +| `cancel_all` (`native_host.hpp:1232`) / `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:2712`). | | `cohort_add` (`native_host.hpp:1250`) / `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:860`, 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:1279`) / `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:2980`). | -| `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:1666`) 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:1279`) / `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:2987`). | +| `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:1668`) 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:63`) | **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:653`) leaves its value unnamed, and the enum guard holds that exclusion. | -| `strategy_set_path_order` (`pineforge.h:947`) 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:891`); 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:1041`) | **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:882`) versus `strategy_configure_native_ext_result_v1` (`native_c_host.cpp:3909`) | **retained legacy base plus typed additive route** | The base ABI's refused specification still returns `-1` and latches `Failed`, preserving its frozen caller contract. 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:564`) | **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. | +| `NativeReportPolicy::KernelRecordedAtHostMarks` (`native_run_spec.hpp:63`) | **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:655`) leaves its value unnamed, and the enum guard holds that exclusion. | +| `strategy_set_path_order` (`pineforge.h:965`) 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:893`); 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:1059`) | **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:3961`) | **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:582`) | **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:866`) and `NativeTimeframeBarContext::interval` (`native_host.hpp:790`) | **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:7474-7486`); 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:1055-1060`) sees the same values, so no hash contract moves. | -| `pf_native_lot_excursion_v1` (`native_c_api.h:1850`) | **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:1854`) | **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) @@ -658,7 +658,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: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` | | `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:8419-8422` (lookahead) and `clear_if_gapped` `:8413-8416` (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:2209`, `::gaps` `include/pineforge/native_c_api.h:2213` | 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:786`, `pf_native_gaps_e` `:797`); 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:2213`, `::gaps` `include/pineforge/native_c_api.h:2217` | 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:788`, `pf_native_gaps_e` `:799`); 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` | @@ -802,3 +802,24 @@ The gates run in different places. `check_doc_anchors.py`, `check_doc_lint.py`, `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. + +## C-SURFACE-1.0 transport ruling (rule 2) + +The C callback host is a generic transport for `NativeStrategyHost`. Its decision +snapshot presents the session tail to every callback table at the published +policy-hook length or later; the later lot-facts marker governs only that other +snapshot. This is the same information a C++ host receives, selected by a +published byte length rather than a venue or Pine rule. The marker must be zero, +the typed FX error word must stay four bytes, and a newly added FX error must +be named by the C translation before the kernel builds. None changes a request, +trade, durable field or hash value. + +C-layer validation refusals clear presentation `last_error` text while leaving +the durable native failure record alone. Configure from a Running callback, or +from a Completed host's hash callback, is refused before it can mutate the +host; an ordinary configure between runs still follows the kernel's reuse +rule. Commands in `on_timeframe_bar` and `on_margin_call` follow the same +`commands_allowed()` guard as C++ commands. The C surface adds no platform +knob and no alternate matching rule. The frozen INT23-header test and the +pure-C refusal, callback and discriminator rows in `tests/test_native_c_api.c` +pin this ruling. diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index dffbb28c..49ff5db7 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -124,7 +124,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:859 fires per accepted realtime print native_execution_consumer.cpp:6992-6996; 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:96 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:284-286. | | 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:7347-7464 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:908 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:2389-2445) | **yes** — `IntrabarPath` native_run_spec.hpp:361-401, delivery native_execution_consumer.cpp:7291-7482; `NativePathOrder` native_run_spec.hpp:334-338 | 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:610 | 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:233-319) are A. S's "neutral tail + trade-window policy" is single-source and unassigned (§4 Q8). | +| 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:233-319) 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:5547), 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:681, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1655-1662 | n/a | A | — | F:E4 O:F5 S:C2 | R5-5: the adapter keeps its path | ### 1.6 Higher timeframes and auxiliary feeds @@ -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:1513-1516, pumped from the scheduler `scheduler_prepare_security_sequence` pine_strategy_host.cpp:1421-1577; 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:1514 **Closed:** `NativeRunSpec::subscriptions` (`NativeTimeframeSubscription` native_run_spec.hpp:484), or `declare_timeframe_subscriptions` native_host.hpp:1101 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:866 and `native_series_bar(index)` native_host.hpp:1084 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:847) | **partial (inert)** — pre-run staging is accepted `refuse_source_mutation` native_execution_consumer.cpp:1407-1423 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, sole caller pine_strategy_host.cpp:1546; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1425-1430 **Closed:** a bare host's subscriptions consume authoritative bars: `NativeTimeframeSubscription::authoritative_bars` native_run_spec.hpp:486 is staged into the feed store by the kernel's own `prepare_native_security_feeds` native_execution_consumer.cpp:8089 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 c_abi.cpp:861) | **partial (inert)** — pre-run staging is accepted `refuse_source_mutation` native_execution_consumer.cpp:1407-1423 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, sole caller pine_strategy_host.cpp:1546; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1425-1430 **Closed:** a bare host's subscriptions consume authoritative bars: `NativeTimeframeSubscription::authoritative_bars` native_run_spec.hpp:486 is staged into the feed store by the kernel's own `prepare_native_security_feeds` native_execution_consumer.cpp:8089 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). | | 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:908 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:513) 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. | @@ -161,9 +161,9 @@ 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:1713) | **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:1438) stay A, measured in §3.7 part 2. | | 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:1192, enabled `stream_observe_actions_` native_execution_consumer.cpp:9084, C ABI `strategy_stream_order_actions_len` c_abi.cpp:643-660 | K | — | F:H5 | (single-source) | +| RP7 | Live order-action stream (runner ledger / webhook) | **yes** — `stream_observe_actions_` engine_execution.cpp:1192, enabled `stream_observe_actions_` native_execution_consumer.cpp:9084, 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:1335, native_execution_consumer.cpp:10470; 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:524; per-bar rows are appended only by the source scheduler `scheduler_record_broker_hash` pine_strategy_host.cpp:1955 **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` in batch and across a stream's warmup and realtime legs. 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. What a recorded row costs — quadratic on the Pine adapter, O(closed rows) per row in the kernel — is measured and ruled in §3.8 | +| 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:1955 **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` in batch and across a stream's warmup and realtime legs. 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. What a recorded row costs — quadratic on the Pine adapter, O(closed rows) per row in the kernel — is measured and ruled in §3.8 | | 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 @@ -174,9 +174,9 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | OT2 | Symbol info, sessions, calendars, timezones (`timezone` pine_adapter.cpp:2195-2196, `chart_timezone` pine_adapter.cpp:2236-2238) | **yes** — spec fields `ticker` native_run_spec.hpp:566-576; `parse_session` native_calendar.hpp:173-419; intervals on every decision context `input_interval` market_driver.hpp:117-118 | 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:2157, `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:882, FX curve c_abi.cpp:882, probe c_abi.cpp:882, stream `strategy_stream_begin` c_abi.cpp:610 **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:529 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:898 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 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:546 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:900 among them (F §2.iii) | | OT6 | C-level order submission + strategy callbacks | **no** — none of the 57 `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 symbols are a second, disjoint inventory beside the 57 codegen-facing ones (`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:154 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:1440 — from the run's own calendar and input (`present_session_day` native_execution_consumer.cpp:7114), 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:1794 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:154 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:1441 — from the run's own calendar and input (`present_session_day` native_execution_consumer.cpp:7114), 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:1794 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:287-289) | 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:739; the source host casts it back `overrides_opaque` pine_strategy_host.cpp:291-294 | 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:323-352; 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:882-884 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1812-1815; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1845, `:8580`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2249-2250. 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:737) | F O S | +| c | `NativeSlotLabelPolicy::LegacyTolerant`, `NativeLegacyTolerance` (now `NativeSlotLabelPolicy::FeedTolerant`, `NativeFeedTolerance`) | `NativeSlotLabelPolicy` native_run_spec.hpp:323-352; 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:882-884 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1812-1815; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1845, `:8580`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2249-2250. 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:739) | 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:445-451) | 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:6268 | 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) | @@ -213,7 +213,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | 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:18857-18886 | 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 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:566) 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:602. 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:1253 | 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:1271 | 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) | @@ -225,7 +225,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa ### 2.iii C-level order submission and callbacks (L13, post-v1 — R5-7) -Today a C host can configure (`strategy_configure_native_v1` c_abi.cpp:847-899), stream (`strategy_stream_*` c_abi.cpp:575-684) 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: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 typedef struct pf_native_decision_v1 { uint64_t ordinal; int32_t interval_index; int64_t effective_time_ms, @@ -267,7 +267,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 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 | -| 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:529 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:898) | 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:546 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:900) | F S | Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the epoch batch. @@ -320,7 +320,7 @@ Union of F §2.vi (9), O §2.vi (5 + 1) and S §2.6 (7), deduplicated. S had no | 5 | "`engine_aux_security.cpp` includes `source/pine_strategy_host.hpp`" (ADR L129-132) | False at `73817c1`: its includes are `engine_internal.hpp`, `ta.hpp` and std headers (*fresh* re-read). 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 | | 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:445-451 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:566; the ledger `close_logical_units_` pine_adapter.hpp:2011-2022; `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. Accurate statement: no C symbol submits / replaces / cancels an order and no C strategy callback exists, so strategy logic is C++-only | `sha256:4c368ea1041915777a9df2782e6ed46c0d7175a5c28adb19a1deeef8940cdb07` c_abi.cpp:275-902, `sha256:4b7e398c78b276b34c4247e8271242b48aa195cea29126f4e809073f2665e1ea` check_c_abi_runtime.py:19-79, c_abi.cpp:848, `sha256:a9d7348a5a060e57c33fe9f537b9e7c21a18417c5d15569074bca299ea72f504` c_abi.cpp:576-695, `sha256:db54076d46a71efb0c7556c97494317e7fca443278b6963c3edf79ce918d72a1` c_abi.cpp:501-553, `sha256:f18d46ac323234f7e3082367d30840b704b8a7548f7ae95ee8cff8714f29fd50` c_abi.cpp:480-497, `sha256:dab6c5091fb32549abf866e5d71ed05f722ab4715defa4f0eca524c8f9238a2f` c_abi.cpp:788-819, c_abi.cpp:902 | F O S | +| 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:4fa627f4c74950d70948186329259be7ebb753c8c50d91c16b838ba01c540a6a` c_abi.cpp:289-929, `sha256:4b7e398c78b276b34c4247e8271242b48aa195cea29126f4e809073f2665e1ea` check_c_abi_runtime.py:19-79, c_abi.cpp:848, `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:902 | 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 | | 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) | @@ -730,7 +730,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:3109-3113) +(`PF_NATIVE_SPEC_EXT_PRICE_GRID`, translated at native_c_host.cpp:3145-3149) 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 diff --git a/docs/pages/contributing-llm.md b/docs/pages/contributing-llm.md index 5da52f71..f1931744 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:229 and - `RELEASE_MIN_TESTS` ci_verify.py:332. Adding rows means raising the floor + *ran* against a floor: `KERNEL_MIN_TESTS` ci_verify.py:230 and + `RELEASE_MIN_TESTS` ci_verify.py:333. Adding rows means raising the floor in the same commit. ## The recipe for a lane @@ -241,7 +241,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:826, or C through the -`strategy_native_*` surface `sha256:1a4d41202e1b32a33ab75c2e42a36285ccb1d061087a8db1c19162b244c5b29f` native_c_api.h:2600. +`strategy_native_*` surface `sha256:1a4d41202e1b32a33ab75c2e42a36285ccb1d061087a8db1c19162b244c5b29f` native_c_api.h:2607. **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 @@ -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:229, `RELEASE_MIN_TESTS` ci_verify.py:332). It +(`KERNEL_MIN_TESTS` ci_verify.py:230, `RELEASE_MIN_TESTS` ci_verify.py:333). 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 6b2256e5..efc30e21 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -482,7 +482,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:2689-2739`), under the same legality rule; see *Driving the +(`native_c_api.h:2696-2746`), under the same legality rule; see *Driving the kernel from C* below. `native_order::Request` values belong to `native_order_v7` @@ -1848,7 +1848,7 @@ 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:1440`). The kernel resolves each session day once through +(`native_c_api.h:1441`). 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 @@ -2090,13 +2090,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:1253`) the C one, with the +`strategy_closed_trade_close_cause` (`pineforge.h:1271`) 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:1176-1206`), which index exactly the +`_exit_id` / `_exit_comment` (`pineforge.h:1194-1224`), 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. @@ -2841,7 +2841,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 (`sha256:2e963d6ab1630db1535bd944dc7406ba649e9d14a589065db25347569fbad150` native_c_api.h:37-39) take a `pf_strategy_t` from `strategy_native_host_create_v1` unchanged — and -`strategy_native_submit_v1` (`native_c_api.h:2639`) obeys the one legality +`strategy_native_submit_v1` (`native_c_api.h:2646`) obeys the one legality rule its C++ spelling does. Rebuild strategy libraries against this engine. An ABI-v4 module without the @@ -3659,8 +3659,8 @@ subscriptions, and the generic risk limits — travel in `pf_native_run_spec_ext_v1`, passed together with the base spec to `strategy_configure_native_ext_v1`. It replaces `strategy_configure_native_v1` rather than following it, because the kernel -configures each run exactly once: a second configure of a Ready handle fails -it. The one field of `NativeRunSpec` it deliberately does not carry is +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. The nine enum-valued words of the two specs are `uint32_t` words holding a @@ -3687,11 +3687,14 @@ marks each report point from inside its own callbacks, and the callback table has no call that marks one, so its integer, 2, is refused like any other. `strategy_configure_native_v1` has no boundary check of its own for the three base-spec words: the kernel's validation judges them, and a bad one answers --1 and leaves the host failed. That call answers every refusal so — Failed -with `PF_NATIVE_FAILURE_INVALID_SPECIFICATION` for an invalid spec, with -`PF_NATIVE_FAILURE_CONTRACT` for a Ready or Running handle or a refused reuse -— and has no out-parameters; a handle an abort failed keeps that failure, -and no configure call accepts a handle a configure failed. +-1 and leaves the host Failed with `PF_NATIVE_FAILURE_INVALID_SPECIFICATION`. +The kernel still fails a Ready handle under its Contract rule. The C boundary +refuses a Running handle without changing its state, including when a callback +calls it. It also refuses a Completed host's +`on_hash_extension` callback during a read-only hash query. A Completed handle or one Failed by a +cooperative abort may be configured for another run; the kernel judges reuse +constraints and can fail a refused reuse with `PF_NATIVE_FAILURE_CONTRACT`. +The call has no typed out-parameters. `strategy_configure_native_ext_result_v1` is the typed spelling of `strategy_configure_native_ext_v1`: the same inputs plus two out-parameters @@ -3804,10 +3807,12 @@ lengths** and the runtime accepts each: the layout this header first shipped field rather than a literal), that plus six hooks (`PF_NATIVE_CALLBACKS_V1_HOOKS_SIZE`), that plus the policy-hook tail (`PF_NATIVE_CALLBACKS_V1_POLICY_SIZE`), and the current one, whose trailing -`reserved1` marker (documented zero, not checked) says the host reads the +`reserved1` marker (required zero, checked at host creation) says the host reads the lot-excursion facts' `entry_commission` tail. A host compiled against an earlier layout keeps working and simply has none of the later hooks -installed, which is exactly the kernel's own default for each. +installed, which is exactly the kernel's own default for each. The policy-hook +layout already receives the decision's session tail; the later marker affects +only the lot-excursion facts tail. Two of the six are ordinary observation callbacks. `on_recalculate` is `on_native_recalculate`: every calculation of the run arrives there, tagged diff --git a/docs/pages/pine-to-native.md b/docs/pages/pine-to-native.md index 2c3c63e3..2dd38622 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -72,7 +72,7 @@ gain: (native_order.hpp:1023) — readable in order from `native_events` (native_host.hpp:1294). - **Any language.** The same kernel drives from C through a callback table - (`pf_native_callbacks_v1` native_c_api.h:2417), so a host in Rust, Go, Python or Zig needs no C++. + (`pf_native_callbacks_v1` native_c_api.h:2422), so a host in Rust, Go, Python or Zig needs no C++. - **One binary, no transpiler.** Link `libpineforge`, ship an executable or a loadable module. @@ -112,33 +112,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:552) applied by `configure_native` (native_host.hpp:1161); a C host sends `pf_native_run_spec_v1` -(pineforge.h:529) plus `pf_native_run_spec_ext_v1` (native_c_api.h:2246) -through `strategy_configure_native_ext_v1` (native_c_api.h:3068). Setup is +(pineforge.h:546) plus `pf_native_run_spec_ext_v1` (native_c_api.h:2250) +through `strategy_configure_native_ext_v1` (native_c_api.h:3075). 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:552 | `pf_native_run_spec_v1` pineforge.h:529 | `hello_kernel.cpp` / `hello_kernel_c.c` | One atomic setup value. `validate_native_run_spec` native_run_spec.hpp:805 names the offending field before a run begins. | -| `initial_capital` | `initial_capital` native_run_spec.hpp:578 | `initial_capital` pineforge.h:520 | `hello_kernel.cpp` | Required, positive. | -| `currency` | `currency` native_run_spec.hpp:569 | `currency` pineforge.h:518 | `hello_kernel.cpp` | With `ticker` native_run_spec.hpp:566, `tickerid` native_run_spec.hpp:567, `type` native_run_spec.hpp:568, `basecurrency` native_run_spec.hpp:570, `description` native_run_spec.hpp:571 and `volumetype` native_run_spec.hpp:572 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:521 | `native_open_lots_strategy.cpp` | `Percent`, `CashPerUnit`, `CashPerExecution` — the same three TradingView offers. | -| `commission_value` | `fee_value` native_run_spec.hpp:588 | `fee_value` pineforge.h:521 | `native_open_lots_strategy.cpp` | Quoted per execution by `quote_execution_commissions` engine_execution.cpp:1487. A cash-per-execution ticket is split by units over every slice of that execution. | -| `slippage` | `slippage_ticks` native_run_spec.hpp:582 | `slippage_ticks` pineforge.h:521 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:3906-3907. TradingView's round-then-slip order is adapter-only. | -| `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:522 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:7353-7354, so a request submitted in bar *k*'s calculation fills at bar *k*'s own close. | -| `pyramiding` | `max_open_lots` native_run_spec.hpp:594 | `max_open_lots` pineforge.h:524 | `native_open_lots_strategy.cpp` | Caps surviving+new physical lots `sha256:87bd0ff848901478840870d95eb356b35d7d368555ce8eadee8571b8e17f86ec` native_execution_consumer.cpp:2096-2099. Pine's per-cycle *entry count* is a TradingView rule, ruled **adapter-policy** in ADR-0001's table row `max_open_lots`. | -| `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:340 with `size_basis` native_c_api.h:2085 | `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:2089 | `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:93 | `calculation` native_c_api.h:2257 | `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:625. 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:93 | `calculation` native_c_api.h:2257 | `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:859 is one call per accepted realtime print whatever the trigger is. | -| `margin_long` | `NativeMarginModel::initial_long` native_run_spec.hpp:240 | `margin_initial_long` native_c_api.h:2267 | `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:241 | `margin_initial_short` native_c_api.h:2271 | `native_margin_strategy.cpp` | Per side, unlike the one-scalar `initial_margin_fraction` native_run_spec.hpp:596 the model replaces. `maintenance_long` native_run_spec.hpp:242 / `maintenance_short` native_run_spec.hpp:243 add the liquidation half Pine has no word for. | +| `strategy()` | `NativeRunSpec` native_run_spec.hpp:552 | `pf_native_run_spec_v1` pineforge.h:546 | `hello_kernel.cpp` / `hello_kernel_c.c` | One atomic setup value. `validate_native_run_spec` native_run_spec.hpp:805 names the offending field before a run begins. | +| `initial_capital` | `initial_capital` native_run_spec.hpp:578 | `initial_capital` pineforge.h:537 | `hello_kernel.cpp` | Required, positive. | +| `currency` | `currency` native_run_spec.hpp:569 | `currency` pineforge.h:535 | `hello_kernel.cpp` | With `ticker` native_run_spec.hpp:566, `tickerid` native_run_spec.hpp:567, `type` native_run_spec.hpp:568, `basecurrency` native_run_spec.hpp:570, `description` native_run_spec.hpp:571 and `volumetype` native_run_spec.hpp:572 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:538 | `native_open_lots_strategy.cpp` | `Percent`, `CashPerUnit`, `CashPerExecution` — the same three TradingView offers. | +| `commission_value` | `fee_value` native_run_spec.hpp:588 | `fee_value` pineforge.h:538 | `native_open_lots_strategy.cpp` | Quoted per execution by `quote_execution_commissions` engine_execution.cpp:1487. A cash-per-execution ticket is split by units over every slice of that execution. | +| `slippage` | `slippage_ticks` native_run_spec.hpp:582 | `slippage_ticks` pineforge.h:538 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:3906-3907. TradingView's round-then-slip order is adapter-only. | +| `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:539 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:7353-7354, so a request submitted in bar *k*'s calculation fills at bar *k*'s own close. | +| `pyramiding` | `max_open_lots` native_run_spec.hpp:594 | `max_open_lots` pineforge.h:541 | `native_open_lots_strategy.cpp` | Caps surviving+new physical lots `sha256:87bd0ff848901478840870d95eb356b35d7d368555ce8eadee8571b8e17f86ec` native_execution_consumer.cpp:2096-2099. Pine's per-cycle *entry count* is a TradingView rule, ruled **adapter-policy** in ADR-0001's table row `max_open_lots`. | +| `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:342 with `size_basis` native_c_api.h:2089 | `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:2093 | `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:93 | `calculation` native_c_api.h:2261 | `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:625. 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:93 | `calculation` native_c_api.h:2261 | `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:859 is one call per accepted realtime print whatever the trigger is. | +| `margin_long` | `NativeMarginModel::initial_long` native_run_spec.hpp:240 | `margin_initial_long` native_c_api.h:2271 | `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:241 | `margin_initial_short` native_c_api.h:2275 | `native_margin_strategy.cpp` | Per side, unlike the one-scalar `initial_margin_fraction` native_run_spec.hpp:596 the model replaces. `maintenance_long` native_run_spec.hpp:242 / `maintenance_short` native_run_spec.hpp:243 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:377 | `intrabar_kind` native_c_api.h:2310 | `tests/test_native_calc_timing.cpp` | The host owns the lower bars, as `lower_tf` native_run_spec.hpp:392 or `synthesized` native_run_spec.hpp:406. The C magnifier arguments of `run_backtest_full` pineforge.h:628 configure a *compiled Pine strategy*, never a native host. | +| `use_bar_magnifier` | `IntrabarPath` native_run_spec.hpp:377 | `intrabar_kind` native_c_api.h:2314 | `tests/test_native_calc_timing.cpp` | The host owns the lower bars, as `lower_tf` native_run_spec.hpp:392 or `synthesized` native_run_spec.hpp:406. The C magnifier arguments of `run_backtest_full` pineforge.h:646 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:303 | `native_sized_report_strategy.cpp` | `pf_metrics_t::equity.sharpe_monthly` and `pf_metrics_t::equity.sortino_monthly` (`sortino_monthly` pineforge.h:315) 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: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`. | ### The feed-shape and presentation policies Pine has no word for @@ -150,7 +150,7 @@ not canonical; `NativePathOrder` native_run_spec.hpp:334 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:737. +native_c_api.h:739. ## Design §1 inventory crosswalk {#pine_to_native_inventory} @@ -256,18 +256,18 @@ 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:331 | `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:364 | `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:365 | `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:358 | `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:359 | `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:670 overrides it for a compiled Pine strategy. | +| `strategy.fixed` | `Transact` native_order.hpp:41 | `PF_NATIVE_INTENT_TRANSACT` native_c_api.h:333 | `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:366 | `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:367 | `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:360 | `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:361 | `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:688 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` (native_host.hpp:927), 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:2509 for a close. +the same question through `on_close_units` native_c_api.h:2514 for a close. ## Orders {#pine_to_native_map_orders} @@ -275,27 +275,27 @@ Everything here is one call: `submit` native_host.hpp:1178, or `submit_market` native_host.hpp:1192 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:2639 over a -`pf_native_request_v1` native_c_api.h:2076. +is `strategy_native_submit_v1` native_c_api.h:2646 over a +`pf_native_request_v1` native_c_api.h:2080. | 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:2639 | `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:2639 | `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:2639 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:346 with `PF_NATIVE_OWNER_BIND_OPENING` native_c_api.h:451 | `native_selected_strategy.cpp` | Closes the lots one opening produced. The cycle comes from `ExecutionAppliedEvent::cycle_after` native_order.hpp:1139. 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. | -| `strategy.close_all()` | `Flatten` native_order.hpp:36 | `PF_NATIVE_INTENT_FLATTEN` native_c_api.h:329 | `hello_kernel.cpp` / `hello_kernel_c.c` | The whole book, in one settlement cycle. | -| `strategy.cancel()` | `cancel_where` native_host.hpp:1243 with `NativeRequestField::Label` native_host.hpp:679 | `strategy_native_cancel_where_v1` native_c_api.h:2726 | `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:1214 is the single-request form for a host that kept the handle. | -| `strategy.cancel_all()` | `cancel_all` native_host.hpp:1232 | `strategy_native_cancel_all_v1` native_c_api.h:2705 | `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:1228 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:470 | `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:471 | `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:464 | `hello_kernel.cpp` | The default: no group, no sibling effect. | -| `strategy.direction.all` | `NativeOpenDirections::Both` native_run_spec.hpp:75 | `allowed_open_directions` pineforge.h:522 | `native_risk_limits_strategy.cpp` | The default. | -| `strategy.direction.long` | `NativeOpenDirections::Long` native_run_spec.hpp:73 | `allowed_open_directions` pineforge.h:522 | `tests/test_native_run_spec.cpp` | An opening the direction forbids is refused with `OpeningDirection` native_execution_consumer.cpp:2903. | -| `strategy.direction.short` | `NativeOpenDirections::Short` native_run_spec.hpp:74 | `allowed_open_directions` pineforge.h:522 | `tests/test_native_run_spec.cpp` | Same, the other way. | -| `strategy.commission.percent` | `NativeFeeKind::Percent` native_run_spec.hpp:20 | `fee_kind` pineforge.h:521 | `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:521 | `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:521 | `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.entry()` | `Request` native_order.hpp:483 carrying `Transact` native_order.hpp:41 | `strategy_native_submit_v1` native_c_api.h:2646 | `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:2646 | `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:2646 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:348 with `PF_NATIVE_OWNER_BIND_OPENING` native_c_api.h:453 | `native_selected_strategy.cpp` | Closes the lots one opening produced. The cycle comes from `ExecutionAppliedEvent::cycle_after` native_order.hpp:1139. 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. | +| `strategy.close_all()` | `Flatten` native_order.hpp:36 | `PF_NATIVE_INTENT_FLATTEN` native_c_api.h:331 | `hello_kernel.cpp` / `hello_kernel_c.c` | The whole book, in one settlement cycle. | +| `strategy.cancel()` | `cancel_where` native_host.hpp:1243 with `NativeRequestField::Label` native_host.hpp:679 | `strategy_native_cancel_where_v1` native_c_api.h:2733 | `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:1214 is the single-request form for a host that kept the handle. | +| `strategy.cancel_all()` | `cancel_all` native_host.hpp:1232 | `strategy_native_cancel_all_v1` native_c_api.h:2712 | `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:1228 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:472 | `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:473 | `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:466 | `hello_kernel.cpp` | The default: no group, no sibling effect. | +| `strategy.direction.all` | `NativeOpenDirections::Both` native_run_spec.hpp:75 | `allowed_open_directions` pineforge.h:539 | `native_risk_limits_strategy.cpp` | The default. | +| `strategy.direction.long` | `NativeOpenDirections::Long` native_run_spec.hpp:73 | `allowed_open_directions` pineforge.h:539 | `tests/test_native_run_spec.cpp` | An opening the direction forbids is refused with `OpeningDirection` native_execution_consumer.cpp:2903. | +| `strategy.direction.short` | `NativeOpenDirections::Short` native_run_spec.hpp:74 | `allowed_open_directions` pineforge.h:539 | `tests/test_native_run_spec.cpp` | Same, the other way. | +| `strategy.commission.percent` | `NativeFeeKind::Percent` native_run_spec.hpp:20 | `fee_kind` pineforge.h:538 | `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:538 | `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:538 | `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 @@ -313,11 +313,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:383 | `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:384 | `native_price_grid_strategy.cpp` | Stop-limit is `StopLimit` native_order.hpp:252. | -| `trail_points` | `FromOwnerFill{+ticks, true}` native_order.hpp:331 | `arm_scope` / `arm_first_match` native_c_api.h:2159-2160 | `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` native_c_api.h:2094 | `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:2099 | `native_trail_risk_strategy.cpp` | The absolute level the trail arms at. Read the live projection with `trail_state` native_host.hpp:1065 (C: `strategy_native_trail_state_v1` native_c_api.h:2947). 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:385 | `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:386 | `native_price_grid_strategy.cpp` | Stop-limit is `StopLimit` native_order.hpp:252. | +| `trail_points` | `FromOwnerFill{+ticks, true}` native_order.hpp:331 | `arm_scope` / `arm_first_match` native_c_api.h:2163-2164 | `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` native_c_api.h:2098 | `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:2103 | `native_trail_risk_strategy.cpp` | The absolute level the trail arms at. Read the live projection with `trail_state` native_host.hpp:1065 (C: `strategy_native_trail_state_v1` native_c_api.h:2954). 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 @@ -372,10 +372,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:2639 with -`PF_NATIVE_OWNER_WAIT_FOR_APPLIED` native_c_api.h:450, and its `first_match` +each leg is its own `strategy_native_submit_v1` native_c_api.h:2646 with +`PF_NATIVE_OWNER_WAIT_FOR_APPLIED` native_c_api.h:452, and its `first_match` and `scope` are the request's `arm_first_match` / `arm_scope` -native_c_api.h:2159-2160, so a C bracket child arms after its owner's print or over +native_c_api.h:2163-2164, 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 @@ -392,16 +392,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:277 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:731 tail. +block is the `PF_NATIVE_SPEC_EXT_RISK` native_c_api.h:733 tail. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `strategy.risk.allow_entry_in()` | `allowed_open_directions` native_run_spec.hpp:595 | `allowed_open_directions` pineforge.h:522 | `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:2903. | -| `strategy.risk.max_position_size()` | `max_abs_units` native_run_spec.hpp:593 | `max_abs_units` pineforge.h:523 | `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:310 | `risk_max_drawdown` native_c_api.h:2293 | `native_risk_limits_strategy.cpp` | `NativeLossLimit` native_run_spec.hpp:258 is account currency, or a percent of the running peak when `percent` native_run_spec.hpp:260 is set. Blocks openings to the end of the run. | -| `strategy.risk.max_intraday_loss()` | `NativeRiskLimits::max_intraday_loss` native_run_spec.hpp:311 | `risk_max_intraday_loss` native_c_api.h:2296 | `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:312 | `risk_max_consecutive_loss_days` native_c_api.h:2298 | `native_risk_limits_strategy.cpp` | Counts *days* that close with a realized loss; TradingView's streak counts trades. `NativeRiskDay` native_run_spec.hpp:269 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:313 | `risk_max_fills_per_day` native_c_api.h:2300 | `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:1288. | +| `strategy.risk.allow_entry_in()` | `allowed_open_directions` native_run_spec.hpp:595 | `allowed_open_directions` pineforge.h:539 | `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:2903. | +| `strategy.risk.max_position_size()` | `max_abs_units` native_run_spec.hpp:593 | `max_abs_units` pineforge.h:540 | `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:310 | `risk_max_drawdown` native_c_api.h:2297 | `native_risk_limits_strategy.cpp` | `NativeLossLimit` native_run_spec.hpp:258 is account currency, or a percent of the running peak when `percent` native_run_spec.hpp:260 is set. Blocks openings to the end of the run. | +| `strategy.risk.max_intraday_loss()` | `NativeRiskLimits::max_intraday_loss` native_run_spec.hpp:311 | `risk_max_intraday_loss` native_c_api.h:2300 | `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:312 | `risk_max_consecutive_loss_days` native_c_api.h:2302 | `native_risk_limits_strategy.cpp` | Counts *days* that close with a realized loss; TradingView's streak counts trades. `NativeRiskDay` native_run_spec.hpp:269 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:313 | `risk_max_fills_per_day` native_c_api.h:2304 | `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:1288. | 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 @@ -420,19 +420,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:240 / `initial_short` native_run_spec.hpp:241 | `margin_initial_long` native_c_api.h:2267 | `native_margin_strategy.cpp` | -| maintenance requirement per side | `maintenance_long` native_run_spec.hpp:242 / `maintenance_short` native_run_spec.hpp:243 | `margin_maintenance_long` native_c_api.h:2272 | `native_margin_strategy.cpp` | -| how much a liquidation closes | `NativeLiquidationSizing` native_run_spec.hpp:139 | `margin_sizing` native_c_api.h:2262 | `native_margin_strategy.cpp` | -| when the requirement is tested | `NativeLiquidationCheck` native_run_spec.hpp:157 | `margin_check` native_c_api.h:2263 | `native_margin_strategy.cpp` | -| which equity it is tested against | `NativeMarginEquityBasis` native_run_spec.hpp:171 | `margin_equity_basis` native_c_api.h:2330 | `tests/test_native_margin_model.cpp` | -| which base the level is solved from | `NativeLiquidationLevelBase` native_run_spec.hpp:181 | `margin_level_base` native_c_api.h:2331 | `tests/test_native_margin_model.cpp` | -| the forced close's ticket | `liquidation_label` native_run_spec.hpp:250 | `margin_liquidation_label` native_c_api.h:2332 | `native_margin_strategy.cpp` | -| the host's own admission verdict | `validate_execution_precommit` native_host.hpp:940 | — C hosts gate through `on_precommit` native_c_api.h:2534 | `native_margin_strategy.cpp` | -| the host's own requirement | `resolve_margin_requirement` native_host.hpp:956 | `on_margin_requirement` native_c_api.h:2471 | `native_margin_strategy.cpp` | -| which check points run at all | `margin_check_allowed` native_host.hpp:968 | `on_margin_check` native_c_api.h:2480 | `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:922 | `native_fx_roll_strategy.cpp` | -| the host's own liquidation size | `resolve_margin_call_units` native_host.hpp:975 | `on_margin_call_units` native_c_api.h:2487 | `native_margin_strategy.cpp` | -| the liquidation event | `MarginCallEvent` native_order.hpp:1216 delivered to `on_native_margin_call` native_host.hpp:981 | `on_margin_call` native_c_api.h:2435 | `native_margin_strategy.cpp` | +| opening requirement per side | `initial_long` native_run_spec.hpp:240 / `initial_short` native_run_spec.hpp:241 | `margin_initial_long` native_c_api.h:2271 | `native_margin_strategy.cpp` | +| maintenance requirement per side | `maintenance_long` native_run_spec.hpp:242 / `maintenance_short` native_run_spec.hpp:243 | `margin_maintenance_long` native_c_api.h:2276 | `native_margin_strategy.cpp` | +| how much a liquidation closes | `NativeLiquidationSizing` native_run_spec.hpp:139 | `margin_sizing` native_c_api.h:2266 | `native_margin_strategy.cpp` | +| when the requirement is tested | `NativeLiquidationCheck` native_run_spec.hpp:157 | `margin_check` native_c_api.h:2267 | `native_margin_strategy.cpp` | +| which equity it is tested against | `NativeMarginEquityBasis` native_run_spec.hpp:171 | `margin_equity_basis` native_c_api.h:2334 | `tests/test_native_margin_model.cpp` | +| which base the level is solved from | `NativeLiquidationLevelBase` native_run_spec.hpp:181 | `margin_level_base` native_c_api.h:2335 | `tests/test_native_margin_model.cpp` | +| the forced close's ticket | `liquidation_label` native_run_spec.hpp:250 | `margin_liquidation_label` native_c_api.h:2336 | `native_margin_strategy.cpp` | +| the host's own admission verdict | `validate_execution_precommit` native_host.hpp:940 | — C hosts gate through `on_precommit` native_c_api.h:2539 | `native_margin_strategy.cpp` | +| the host's own requirement | `resolve_margin_requirement` native_host.hpp:956 | `on_margin_requirement` native_c_api.h:2476 | `native_margin_strategy.cpp` | +| which check points run at all | `margin_check_allowed` native_host.hpp:968 | `on_margin_check` native_c_api.h:2485 | `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:924 | `native_fx_roll_strategy.cpp` | +| the host's own liquidation size | `resolve_margin_call_units` native_host.hpp:975 | `on_margin_call_units` native_c_api.h:2492 | `native_margin_strategy.cpp` | +| the liquidation event | `MarginCallEvent` native_order.hpp:1216 delivered to `on_native_margin_call` native_host.hpp:981 | `on_margin_call` native_c_api.h:2440 | `native_margin_strategy.cpp` | The one-scalar `initial_margin_fraction` native_run_spec.hpp:596 predates the model and remains the simple spelling; the two are mutually exclusive. It is @@ -446,20 +446,20 @@ openings TradingView takes. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| script body at bar close | `on_native_bar` native_host.hpp:874 | `on_bar` native_c_api.h:2425 | `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:893, whose default forwards here. | -| every calculation, tagged with its reason | `on_native_recalculate` native_host.hpp:893 with `NativeCalculationReason` native_host.hpp:806 | `on_recalculate` native_c_api.h:2453 | `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:807 | `on_recalculate` native_c_api.h:2453 | `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:1739 | `native_market_strategy.cpp` | `Batch`, `Warmup`, `Realtime`. | -| bar-open pre-pass | `on_native_bar_open` native_host.hpp:870 | `on_bar_open` native_c_api.h:2424 | `native_calc_on_fills_strategy.cpp` | Receives the whole script bar by default; `NativeOpenBarView::OpenOnly` native_run_spec.hpp:108 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:1046 | `strategy_native_partial_bar_v1` native_c_api.h:2932 | `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:918 | `on_applied` native_c_api.h:2427 | `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:5221. | -| raw input before aggregation | `on_native_input` native_host.hpp:856 | `on_input` native_c_api.h:2422 | `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:908 | `on_sub_bar` native_c_api.h:2463 | `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:114 with `NativeCoordinate` market_driver.hpp:71 | `pf_native_decision_v1` native_c_api.h:1440 | `hello_kernel.cpp` | Copied onto the callback stack; mutating it changes nothing. In C++ `script_interval` market_driver.hpp:118 and `input_interval` market_driver.hpp:117 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 of the current length, and no input interval. The session-day facts are the three rows below. | -| `session.ismarket` | `in_session` market_driver.hpp:154 | `in_session` native_c_api.h:1458 | `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:1457 is 1. | -| `session.isfirstbar` | `opens_session_day` market_driver.hpp:155 | `opens_session_day` native_c_api.h:1459 | `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 slot one script width back; a run's first bar opens its day. | -| `session.islastbar` | `closes_session_day` market_driver.hpp:156, `closes_session_day_open_ended` market_driver.hpp:157 | `closes_session_day` native_c_api.h:1460 | `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:1060 | `price` native_c_api.h:1518 | `native_selected_strategy.cpp` | The readiness preview `inspect_current_execution` native_host.hpp:1072 is an observation, never an apply token; `execute_current` native_host.hpp:1077 is the apply. | +| script body at bar close | `on_native_bar` native_host.hpp:874 | `on_bar` native_c_api.h:2430 | `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:893, whose default forwards here. | +| every calculation, tagged with its reason | `on_native_recalculate` native_host.hpp:893 with `NativeCalculationReason` native_host.hpp:806 | `on_recalculate` native_c_api.h:2458 | `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:807 | `on_recalculate` native_c_api.h:2458 | `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:1741 | `native_market_strategy.cpp` | `Batch`, `Warmup`, `Realtime`. | +| bar-open pre-pass | `on_native_bar_open` native_host.hpp:870 | `on_bar_open` native_c_api.h:2429 | `native_calc_on_fills_strategy.cpp` | Receives the whole script bar by default; `NativeOpenBarView::OpenOnly` native_run_spec.hpp:108 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:1046 | `strategy_native_partial_bar_v1` native_c_api.h:2939 | `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:918 | `on_applied` native_c_api.h:2432 | `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:5221. | +| raw input before aggregation | `on_native_input` native_host.hpp:856 | `on_input` native_c_api.h:2427 | `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:908 | `on_sub_bar` native_c_api.h:2468 | `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:114 with `NativeCoordinate` market_driver.hpp:71 | `pf_native_decision_v1` native_c_api.h:1441 | `hello_kernel.cpp` | Copied onto the callback stack; mutating it changes nothing. In C++ `script_interval` market_driver.hpp:118 and `input_interval` market_driver.hpp:117 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 session-day facts are the three rows below. | +| `session.ismarket` | `in_session` market_driver.hpp:154 | `in_session` native_c_api.h:1459 | `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:1458 is 1. | +| `session.isfirstbar` | `opens_session_day` market_driver.hpp:155 | `opens_session_day` native_c_api.h:1460 | `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 slot one script width back; a run's first bar opens its day. | +| `session.islastbar` | `closes_session_day` market_driver.hpp:156, `closes_session_day_open_ended` market_driver.hpp:157 | `closes_session_day` native_c_api.h:1461 | `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:1060 | `price` native_c_api.h:1520 | `native_selected_strategy.cpp` | The readiness preview `inspect_current_execution` native_host.hpp:1072 is an observation, never an apply token; `execute_current` native_host.hpp:1077 is the apply. | ## Position, account and report {#pine_to_native_map_report} @@ -477,7 +477,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:455, which is what a C host reads. + `pf_report_t` pineforge.h:469, 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 @@ -495,39 +495,39 @@ runs close the same trades for the same money. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `strategy.account_currency` | `currency` native_run_spec.hpp:569 | `currency` pineforge.h:518 | `hello_kernel.cpp` / `native_fx_roll_strategy.cpp` | Declared, never inferred. `account_fx` native_run_spec.hpp:580 is the one positive scalar that converts quote to account; a timestamped curve is `configure_native_fx_curve` native_host.hpp:1167. 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:578 | `initial_capital` pineforge.h:520 | `hello_kernel.cpp` | The value you declared, unchanged by the run. | -| `strategy.equity` | `native_marked_equity` native_host.hpp:1266 | `strategy_native_marked_equity_v1` native_c_api.h:2972 | `native_open_lots_strategy.cpp` | Pine marks at the current `close`; the native call takes the mark explicitly, so the caller says what "now" means. It equals the balance plus the open lots' `unrealized_pnl` native_host.hpp:341, which the example asserts at every calculation. | -| `strategy.netprofit` | `net_profit` engine.hpp:967 | `net_profit` pineforge.h:398 | `native_open_lots_strategy.cpp` | Realized only. | -| `strategy.netprofit_percent` | derive: `net_profit` engine.hpp:967 over `initial_capital` native_run_spec.hpp:578 | `pf_metrics_t` pineforge.h:344 | `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:277 | `native_open_lots_strategy.cpp` | | -| `strategy.grossprofit_percent` | `grossprofit_percent` engine.hpp:975 | `pf_metrics_t` pineforge.h:344 | `native_sized_report_strategy.cpp` | Of initial capital. | -| `strategy.grossloss` | `gross_loss` engine.hpp:969 | `pf_trade_stats_t::gross_loss` pineforge.h:232 | `native_open_lots_strategy.cpp` | The protected C++ accessor is signed (negative for losses), while the C report stores `gross_loss` as a positive magnitude. Convert C to the protected sign with `-gross_loss`; then `netprofit == grossprofit - C.gross_loss`. | -| `strategy.grossloss_percent` | `grossloss_percent` engine.hpp:978 | `pf_metrics_t` pineforge.h:344 | `native_sized_report_strategy.cpp` | | -| `strategy.openprofit` | `open_profit` `sha256:0490e46ca24b81c82c9549ce38af81be1ea888a60277263108cd26bf3f01e0c7` engine.hpp:1024 | `strategy_native_marked_equity_v1` native_c_api.h:2972 less the balance | `native_open_lots_strategy.cpp` | Takes the price to mark at, like every other native mark. | +| `strategy.account_currency` | `currency` native_run_spec.hpp:569 | `currency` pineforge.h:535 | `hello_kernel.cpp` / `native_fx_roll_strategy.cpp` | Declared, never inferred. `account_fx` native_run_spec.hpp:580 is the one positive scalar that converts quote to account; a timestamped curve is `configure_native_fx_curve` native_host.hpp:1167. 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:578 | `initial_capital` pineforge.h:537 | `hello_kernel.cpp` | The value you declared, unchanged by the run. | +| `strategy.equity` | `native_marked_equity` native_host.hpp:1266 | `strategy_native_marked_equity_v1` native_c_api.h:2979 | `native_open_lots_strategy.cpp` | Pine marks at the current `close`; the native call takes the mark explicitly, so the caller says what "now" means. It equals the balance plus the open lots' `unrealized_pnl` native_host.hpp:341, which the example asserts at every calculation. | +| `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:578 | `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` | The protected C++ accessor is signed (negative for losses), while the C report stores `gross_loss` as a positive magnitude. Convert C to the protected sign with `-gross_loss`; then `netprofit == grossprofit - C.gross_loss`. | +| `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:2979 less the balance | `native_open_lots_strategy.cpp` | Takes the price to mark at, like every other native mark. | | `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.max_drawdown` | `max_drawdown_` engine.hpp:581 | `max_equity_drawdown` pineforge.h:282 | `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:62. | -| `strategy.max_drawdown_percent` | `max_drawdown_percent` engine.hpp:1507 | `max_equity_drawdown_pct` pineforge.h:282 | `native_sized_report_strategy.cpp` | Same rule. | -| `strategy.max_runup` | `max_runup_` engine.hpp:582 | `max_equity_runup` pineforge.h:286 | `native_open_lots_strategy.cpp` | Same rule. | -| `strategy.max_runup_percent` | `max_runup_percent` engine.hpp:972 | `max_equity_runup_pct` pineforge.h:286 | `native_sized_report_strategy.cpp` | Same rule. | -| `strategy.avg_trade` | `avg_trade` engine.hpp:981 | `pf_trade_stats_t` pineforge.h:277 | `native_open_lots_strategy.cpp` | | -| `strategy.avg_trade_percent` | `avg_trade_percent` engine.hpp:985 | `pf_trade_stats_t` pineforge.h:277 | `native_sized_report_strategy.cpp` | | -| `strategy.avg_winning_trade` | `avg_winning_trade` engine.hpp:992 | `pf_trade_stats_t` pineforge.h:277 | `native_sized_report_strategy.cpp` | | -| `strategy.avg_winning_trade_percent` | `avg_winning_trade_percent` engine.hpp:998 | `pf_trade_stats_t` pineforge.h:277 | `native_sized_report_strategy.cpp` | | -| `strategy.avg_losing_trade` | `avg_losing_trade` engine.hpp:995 | `pf_trade_stats_t` pineforge.h:277 | `native_sized_report_strategy.cpp` | | -| `strategy.avg_losing_trade_percent` | `avg_losing_trade_percent` engine.hpp:1007 | `pf_trade_stats_t` pineforge.h:277 | `native_sized_report_strategy.cpp` | | -| `strategy.wintrades` | `count_wintrades` engine.hpp:1042 | `pf_trade_stats_t` pineforge.h:277 | `native_open_lots_strategy.cpp` | | -| `strategy.losstrades` | `count_losstrades` engine.hpp:1043 | `pf_trade_stats_t` pineforge.h:277 | `native_open_lots_strategy.cpp` | | -| `strategy.eventrades` | `eventrades` engine.hpp:1841 | `pf_trade_stats_t` pineforge.h:277 | `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: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:62. | +| `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_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:2752 | `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:2752 | `native_selected_strategy.cpp` | Volume-weighted over the open lots. | -| `strategy.position_entry_name` | `native_open_lots` native_host.hpp:1262 then the last row's `entry_label` native_host.hpp:333 | `strategy_native_open_lot_get_v1` native_c_api.h:2793 | `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:1285 | `strategy_native_liquidation_price_v1` native_c_api.h:2980 | `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:564 | `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:580 is one scalar and `configure_native_fx_curve` native_host.hpp:1167 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:564 | `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:2759 | `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:2759 | `native_selected_strategy.cpp` | Volume-weighted over the open lots. | +| `strategy.position_entry_name` | `native_open_lots` native_host.hpp:1262 then the last row's `entry_label` native_host.hpp:333 | `strategy_native_open_lot_get_v1` native_c_api.h:2800 | `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:1285 | `strategy_native_liquidation_price_v1` native_c_api.h:2987 | `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:582 | `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:580 is one scalar and `configure_native_fx_curve` native_host.hpp:1167 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:582 | `tests/test_native_fx_curve.cpp` | Same ruling. | @anchor pine_to_native_map_trades ## Open trades {#pine_to_native_map_open_trades} @@ -535,9 +535,9 @@ runs close the same trades for the same money. `native_open_lots(mark)` native_host.hpp:1262 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:2785 then -`strategy_native_open_lot_get_v1` native_c_api.h:2793 into a -`pf_native_open_lot_v1` native_c_api.h:1715, whose two strings borrow the +`strategy_native_open_lot_count_v1` native_c_api.h:2792 then +`strategy_native_open_lot_get_v1` native_c_api.h:2800 into a +`pf_native_open_lot_v1` native_c_api.h:1717, 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 @@ -550,19 +550,19 @@ native_order.hpp:445 names. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `strategy.opentrades` | `native_open_lots` native_host.hpp:1262 | `strategy_native_open_lot_count_v1` native_c_api.h:2785 | `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:1715 | `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:1715 | `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:1715 | `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:1715 | `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:1715 | `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:1715 | `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:1715 | `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:1715 | `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:1262 | `strategy_native_open_lot_count_v1` native_c_api.h:2792 | `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:1717 | `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:1717 | `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:1717 | `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:1717 | `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:1717 | `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:1717 | `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:1717 | `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:1717 | `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:1715 | `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:1717 | `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:1715 | `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:1717 | `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. | @@ -579,39 +579,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:613 adds. In C the report's -`pf_report_t::trades` pineforge.h:395 carries the numeric fields, and the +`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:1171, `strategy_closed_trade_exit_id` pineforge.h:1184, -`strategy_closed_trade_exit_comment` pineforge.h:1186 and -`strategy_closed_trade_close_cause` pineforge.h:1253, which index exactly the +pineforge.h:1189, `strategy_closed_trade_exit_id` pineforge.h:1202, +`strategy_closed_trade_exit_comment` pineforge.h:1204 and +`strategy_closed_trade_close_cause` pineforge.h:1271, 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:392 | `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: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:1171 | `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:1171 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:1715) before the close. | -| `strategy.closedtrades.entry_bar_index()` | `entry_bar_index` engine.hpp:181 | `pf_trade_t` pineforge.h:211 | `native_open_lots_strategy.cpp` | Copied from the lot. | -| `strategy.closedtrades.entry_time()` | `entry_time` engine.hpp:173 | `pf_trade_t` pineforge.h:211 | `native_open_lots_strategy.cpp` | Unix ms. | -| `strategy.closedtrades.entry_price()` | `entry_price` engine.hpp:175 | `pf_trade_t` pineforge.h:211 | `native_open_lots_strategy.cpp` | | -| `strategy.closedtrades.exit_id()` | `exit_id` engine.hpp:186 | `strategy_closed_trade_exit_id` pineforge.h:1184 | `native_margin_strategy.cpp` | For a kernel liquidation this is the model's own `liquidation_label` native_run_spec.hpp:250, 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:1186 | `native_margin_strategy.cpp` | | -| `strategy.closedtrades.exit_bar_index()` | `exit_bar_index` engine.hpp:182 | `pf_trade_t` pineforge.h:211 | `native_open_lots_strategy.cpp` | | -| `strategy.closedtrades.exit_time()` | `exit_time` engine.hpp:174 | `pf_trade_t` pineforge.h:211 | `native_open_lots_strategy.cpp` | | -| `strategy.closedtrades.exit_price()` | `exit_price` engine.hpp:176 | `pf_trade_t` pineforge.h:211 | `native_open_lots_strategy.cpp` | | -| `strategy.closedtrades.size()` | `qty` engine.hpp:177 with `is_long` engine.hpp:180 | `pf_trade_t` pineforge.h:211 | `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:211 | `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:211 | `native_open_lots_strategy.cpp` | Net return on entry cost. | -| `strategy.closedtrades.commission()` | `commission` engine.hpp:201 | `pf_trade_t` pineforge.h:211 | `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:211 | `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:1189 | `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:1189 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:1717) 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:1202 | `native_margin_strategy.cpp` | For a kernel liquidation this is the model's own `liquidation_label` native_run_spec.hpp:250, 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:1204 | `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.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:211 | `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:225 | `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:1253) distinguishes a script +`strategy_closed_trade_close_cause` pineforge.h:1271) 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 @@ -625,13 +625,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:484 in `subscriptions` native_run_spec.hpp:631, or `declare_timeframe_subscriptions` native_host.hpp:1101 inside `on_native_run_begin` native_host.hpp:853 | `strategy_native_declare_subscriptions_v1` native_c_api.h:2857 | `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:866 and the latest is pulled with `native_series_bar` native_host.hpp:1084. `authoritative_bars` native_run_spec.hpp:486 replace a completed bucket's OHLCV. The row's two delivery words are typed in C: `pf_native_lookahead_e` native_c_api.h:786 and `pf_native_gaps_e` native_c_api.h:797 (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:513 with `NativeSeriesSource::AuxiliaryFeed` native_run_spec.hpp:481 | `strategy_native_append_auxiliary_bars_v1` native_c_api.h:3128 | `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:377 and reads `on_native_sub_bar` native_host.hpp:908 instead. | -| `barmerge.gaps_off` | `NativeTimeframeSubscription::gaps` native_run_spec.hpp:488 set false | `pf_native_subscription_v1::gaps` = `PF_NATIVE_GAPS_HOLD` native_c_api.h:798 | `native_htf_strategy.cpp` | The delivered bucket stands until the next delivery replaces it. | -| `barmerge.gaps_on` | `NativeTimeframeSubscription::gaps` native_run_spec.hpp:488 set true | `pf_native_subscription_v1::gaps` = `PF_NATIVE_GAPS_CLEAR` native_c_api.h:800 | `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:487 set false | `pf_native_subscription_v1::lookahead` = `PF_NATIVE_LOOKAHEAD_AT_COMPLETION` native_c_api.h:787 | `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:487 set true | `pf_native_subscription_v1::lookahead` = `PF_NATIVE_LOOKAHEAD_AT_FIRST_INPUT` native_c_api.h:789 | `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:564 | `native_fx_roll_strategy.cpp` / `tests/test_native_fx_curve.cpp` | The kernel converts with the curve you stage; it fetches nothing. | +| `request.security()` | `NativeTimeframeSubscription` native_run_spec.hpp:484 in `subscriptions` native_run_spec.hpp:631, or `declare_timeframe_subscriptions` native_host.hpp:1101 inside `on_native_run_begin` native_host.hpp:853 | `strategy_native_declare_subscriptions_v1` native_c_api.h:2864 | `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:866 and the latest is pulled with `native_series_bar` native_host.hpp:1084. `authoritative_bars` native_run_spec.hpp:486 replace a completed bucket's OHLCV. The row's two delivery words are typed in C: `pf_native_lookahead_e` native_c_api.h:788 and `pf_native_gaps_e` native_c_api.h:799 (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:513 with `NativeSeriesSource::AuxiliaryFeed` native_run_spec.hpp:481 | `strategy_native_append_auxiliary_bars_v1` native_c_api.h:3137 | `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:377 and reads `on_native_sub_bar` native_host.hpp:908 instead. | +| `barmerge.gaps_off` | `NativeTimeframeSubscription::gaps` native_run_spec.hpp:488 set false | `pf_native_subscription_v1::gaps` = `PF_NATIVE_GAPS_HOLD` native_c_api.h:800 | `native_htf_strategy.cpp` | The delivered bucket stands until the next delivery replaces it. | +| `barmerge.gaps_on` | `NativeTimeframeSubscription::gaps` native_run_spec.hpp:488 set true | `pf_native_subscription_v1::gaps` = `PF_NATIVE_GAPS_CLEAR` native_c_api.h:802 | `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:487 set false | `pf_native_subscription_v1::lookahead` = `PF_NATIVE_LOOKAHEAD_AT_COMPLETION` native_c_api.h:789 | `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:487 set true | `pf_native_subscription_v1::lookahead` = `PF_NATIVE_LOOKAHEAD_AT_FIRST_INPUT` native_c_api.h:791 | `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:582 | `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. | @@ -711,21 +711,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:2417 table handed to -`strategy_native_host_create_v1` native_c_api.h:2598; the run is -`pf_native_run_spec_v1` pineforge.h:529 plus `pf_native_run_spec_ext_v1` -native_c_api.h:2246 through `strategy_configure_native_ext_v1` -native_c_api.h:3068, and `strategy_native_run_v1` native_c_api.h:2611 drives -the bars into a `pf_report_t` pineforge.h:455. +The host is a `pf_native_callbacks_v1` native_c_api.h:2422 table handed to +`strategy_native_host_create_v1` native_c_api.h:2605; the run is +`pf_native_run_spec_v1` pineforge.h:546 plus `pf_native_run_spec_ext_v1` +native_c_api.h:2250 through `strategy_configure_native_ext_v1` +native_c_api.h:3075, and `strategy_native_run_v1` native_c_api.h:2618 drives +the bars into a `pf_report_t` pineforge.h:469. Three rules make the C door behave like the C++ one: 1. **Every struct is tagged and size-prefixed.** Set `struct_size` - native_c_api.h:2077 and `version` native_c_api.h:2078; a mismatch is refused + native_c_api.h:2081 and `version` native_c_api.h:2082; 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:2838. + `Failed`, readable with `strategy_native_state_v1` native_c_api.h:2845. 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. @@ -997,13 +997,13 @@ PINEFORGE_EXPORT_NATIVE_STRATEGY(MyHost); ``` `PINEFORGE_EXPORT_NATIVE_STRATEGY` native_module.hpp:271 defines -`strategy_create` `sha256:bdc1d58cf27e0aaacfa19c793899d70108d6ac472ca4f29373d6d279a2f20f25` pineforge.h:598, `strategy_free`, `strategy_set_input` -pineforge.h:660, `strategy_set_override` pineforge.h:670, -`strategy_set_magnifier_volume_weighted` pineforge.h:678, `run_backtest` -pineforge.h:611, `run_backtest_full` pineforge.h:628 and `report_free` -pineforge.h:642. It adds **no** new C symbol: every remaining runtime export — -`strategy_configure_native_v1` pineforge.h:529, the `strategy_stream_*` family -c_abi.cpp:575-694, `strategy_execution_contract` pineforge.h:484 — already +`strategy_create` `sha256:bdc1d58cf27e0aaacfa19c793899d70108d6ac472ca4f29373d6d279a2f20f25` pineforge.h:616, `strategy_free`, `strategy_set_input` +pineforge.h:678, `strategy_set_override` pineforge.h:688, +`strategy_set_magnifier_volume_weighted` pineforge.h:696, `run_backtest` +pineforge.h:629, `run_backtest_full` pineforge.h:646 and `report_free` +pineforge.h:660. It adds **no** new C symbol: every remaining runtime export — +`strategy_configure_native_v1` pineforge.h:546, the `strategy_stream_*` family +c_abi.cpp:589-708, `strategy_execution_contract` pineforge.h:498 — already lives in the engine, and the generated `strategy_create` references `pf_abi_version()` so a static link keeps that object. diff --git a/include/pineforge/native_c_api.h b/include/pineforge/native_c_api.h index d5a0a97d..187c9a5e 100644 --- a/include/pineforge/native_c_api.h +++ b/include/pineforge/native_c_api.h @@ -284,8 +284,10 @@ extern "C" { /** @defgroup pf_native_c_status Status codes * @brief Every `int`-returning symbol here answers 0 or one of these. * - * Negative values are errors and never mutate run state. Non-negative - * values are outcomes: 0 is success everywhere, and + * Negative values are errors. A C-layer validation refusal does not mutate + * native run state; a kernel-origin refusal can latch Failed, as the + * configure-reuse and reentrant-append contracts below specify. + * Non-negative values are outcomes: 0 is success everywhere, and * #strategy_native_execute_current_v1 additionally answers the positive * #pf_native_execute_outcome_t codes. * @{ */ @@ -299,8 +301,8 @@ extern "C" { #define PF_NATIVE_E_REJECTED -6 /**< The kernel rejected the request (reason written out). */ #define PF_NATIVE_E_UNSUPPORTED -7 /**< A tag this API version cannot represent. */ #define PF_NATIVE_E_EXCEPTION -8 /**< A C++ exception was contained at the boundary. */ -#define PF_NATIVE_E_NOT_WORKING -9 /**< The target handle was issued by this run but is no longer live. */ -#define PF_NATIVE_E_INVALID_TARGET -10 /**< The target handle was never issued by this run (a foreign or unknown incarnation). */ +#define PF_NATIVE_E_NOT_WORKING -9 /**< A nonzero target incarnation is not live, whether it was retired or never issued. */ +#define PF_NATIVE_E_INVALID_TARGET -10 /**< Malformed target: incarnation 0. C commands pair an incarnation with this handle's current run identity, so they cannot pass a foreign run identity. */ #define PF_NATIVE_E_RUN_FAILED -11 /**< The run did not reach Completed; read the state. */ #define PF_NATIVE_E_REFUSED -12 /**< execute_current refused; see pf_native_refusal_e. */ /** Non-negative outcome: the kernel HAS no answer here and the output was @@ -1398,13 +1400,12 @@ typedef enum pf_native_anchored_trigger_e { * `price` is `current_execution_point()`'s price (the bar's own close at its * close calculation) and NaN only where that answers nullopt. * - * It has TWO published layouts, and the runtime PRESENTS the one the - * caller's callback table was published with: a table sent at the current - * length is handed the whole struct, with its session tail; a table sent at - * an earlier published length is handed `struct_size` = - * #PF_NATIVE_DECISION_V1_BASE_SIZE — the sizeof that caller's own header - * compiled — and nothing past it is filled. So an older caller's exact-size - * check keeps holding, and a caller reads a field only below `struct_size`. + * It has TWO published layouts. A callback table sent at + * #PF_NATIVE_CALLBACKS_V1_POLICY_SIZE or the current `sizeof` receives the + * whole struct and its session tail. A table sent at an earlier published + * length receives `struct_size` = #PF_NATIVE_DECISION_V1_BASE_SIZE — the + * sizeof that earlier caller's header compiled — and nothing past it is + * filled. A caller reads a field only below the presented `struct_size`. * * The four session-day bytes after `quote_kind` (R5 lane F5) sit in what * was the base layout's tail padding: its size and every offset before them @@ -1459,8 +1460,8 @@ typedef struct pf_native_decision_v1 { uint8_t opens_session_day; /**< It opens its session day. */ uint8_t closes_session_day; /**< It closes its session day. */ - /* ── The additive session tail (R5 lane F4). Presented only to a callback - * table of the current layout; see the struct note. ── */ + /* ── The additive session tail (R5 lane F4). Presented to a callback + * table with the policy-hook tail or its successor; see above. ── */ int64_t script_interval_open_ms; /**< The script interval's nominal origin. */ int64_t script_interval_eligible_open_ms; /**< Its first in-session instant. */ int64_t script_interval_last_traded_close_ms; /**< Its exclusive end of trading. */ @@ -1487,8 +1488,9 @@ typedef struct pf_native_decision_v1 { /** Byte length of #pf_native_decision_v1 as the L13 lane first published it, * before the session tail was appended — the `struct_size` a callback table - * of an earlier published length is presented. It is the offset of the first - * appended field, which is that layout's sizeof on every target (it ended in + * shorter than #PF_NATIVE_CALLBACKS_V1_POLICY_SIZE is presented. It is the + * offset of the first appended field, which is that layout's sizeof on every + * target (it ended in * padding the tail's first 8-byte field begins after). */ #define PF_NATIVE_DECISION_V1_BASE_SIZE \ ((uint32_t)offsetof(pf_native_decision_v1, script_interval_open_ms)) @@ -1744,8 +1746,10 @@ 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; /**< Reserved beside the code: the kernel records no - * discriminator on this surface, so it reads 0. */ + 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. */ uint64_t failure_ordinal; /**< The point the failure was latched at, 0 when absent. */ uint64_t consumed_high_water; int64_t decision_floor_ms; @@ -2403,10 +2407,11 @@ typedef struct pf_native_run_spec_ext_v1 { * a failure raised there could not be latched without unwinding through * them. A host that must abort does it from an observation callback. * - * Commands are legal inside `on_bar_open`, `on_bar`, `on_tick` and - * `on_applied`. `on_run_begin`, `on_input`, `on_timeframe_bar` and - * `on_margin_call` are observation-only: a command there answers - * PF_NATIVE_E_STATE and changes nothing. + * Commands follow the C++ host's callback guard: they are legal in every + * callback while the kernel is Running, including `on_timeframe_bar` and + * `on_margin_call`. The kernel may still reject a particular request or + * current-execution action for its own reason. The answering hooks must + * only answer their question; they run on paths without the callback guard. * #strategy_native_declare_opened_lot_entry_bar_mask_v1 is legal inside * `on_applied` alone. * @@ -2553,7 +2558,8 @@ typedef struct pf_native_callbacks_v1 { /* The trailing layout marker lets the runtime distinguish the policy-hook * layout published by F4 from this layout, whose lot-excursion facts carry - * the additive entry_commission tail. It is reserved and must be zero. */ + * the additive entry_commission tail. It is reserved and must be zero; + * host creation refuses a nonzero value. */ uint64_t reserved1; } pf_native_callbacks_v1; @@ -2592,8 +2598,9 @@ PF_API int strategy_native_api_version(void); * #strategy_configure_native_v1, the whole `strategy_stream_*` family and the * read-only accessors all take it unchanged. * - * @return The handle, or NULL for a NULL/mis-sized table or on allocation - * failure. Release it with #strategy_native_host_free — never + * @return The handle, or NULL for a NULL/mis-sized table, a nonzero + * `reserved1` in the current layout, or an allocation failure. Release it + * with #strategy_native_host_free — never * #strategy_free, which does not own this allocation. */ PF_API pf_strategy_t strategy_native_host_create_v1(const pf_native_callbacks_v1* callbacks); @@ -3083,7 +3090,8 @@ PF_API int strategy_configure_native_ext_v1(pf_strategy_t s, * #strategy_configure_native_v1 reuses a host. A Ready or Running handle, * and a handle Failed by anything but an abort, answer #PF_NATIVE_E_STATE * with #PF_NATIVE_SPEC_ERROR_WRONG_PHASE / #PF_NATIVE_SPEC_FIELD_NONE, - * without mutation. + * without mutation. A call from a C hook frame is refused the same way, + * including `on_hash_extension` during a hash read of a Completed host. * * On an accepted handle the specification is validated before the kernel * sees it: a refused value answers #PF_NATIVE_E_ARGUMENT with its pair and @@ -3098,10 +3106,11 @@ PF_API int strategy_configure_native_ext_v1(pf_strategy_t s, * again; an aborted handle keeps its abort and may be configured again. * #PF_NATIVE_OK writes NONE / NONE and leaves the handle Ready. * - * A NULL handle (#PF_NATIVE_E_HANDLE), a NULL @p base or @p ext - * (#PF_NATIVE_E_ARGUMENT), a mis-sized struct (#PF_NATIVE_E_STRUCT) and an - * unknown enumerator on an accepted handle (#PF_NATIVE_E_TAG) leave both - * out-parameters untouched. + * C-layer shape refusals leave both out-parameters untouched: a NULL handle + * (#PF_NATIVE_E_HANDLE); NULL @p base, @p ext, or required string member; + * a negative auxiliary count or a NULL subscription timeframe + * (#PF_NATIVE_E_ARGUMENT); a mis-sized struct (#PF_NATIVE_E_STRUCT); or an + * unknown enumerator on an accepted handle (#PF_NATIVE_E_TAG). * * Exercised by `tests/test_native_c_api.c`. */ PF_API int strategy_configure_native_ext_result_v1( diff --git a/include/pineforge/pineforge.h b/include/pineforge/pineforge.h index 5053d153..dc5fb2ad 100644 --- a/include/pineforge/pineforge.h +++ b/include/pineforge/pineforge.h @@ -45,6 +45,20 @@ #include #include +/* Public C words with enum types must retain their published width even when + * a consumer enables -fshort-enums. The C99 fallback keeps the strict-C99 + * header build valid; the two newer languages get a native static assertion. */ +#if defined(__cplusplus) +#define PF_STATIC_ASSERT(condition) static_assert((condition), #condition) +#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L +#define PF_STATIC_ASSERT(condition) _Static_assert((condition), #condition) +#else +#define PF_STATIC_ASSERT_JOIN_(a, b) a##b +#define PF_STATIC_ASSERT_JOIN(a, b) PF_STATIC_ASSERT_JOIN_(a, b) +#define PF_STATIC_ASSERT(condition) \ + typedef char PF_STATIC_ASSERT_JOIN(pf_static_assertion_, __LINE__)[(condition) ? 1 : -1] +#endif + /* ── Version ─────────────────────────────────────────────────────── */ /* Macros (PINEFORGE_VERSION_MAJOR / _MINOR / _PATCH / _STRING / _FULL, * PINEFORGE_GIT_SHA) live in the generated . */ @@ -502,11 +516,14 @@ typedef enum pf_native_spec_optional_e { * #pf_native_close_execution_t and `allowed_open_directions` a * #pf_native_open_directions_t (zero-filled, that word admits no opening at * all). #strategy_configure_native_v1 hands the whole value to the kernel, - * which answers every refusal with -1 and a Failed native handle: - * PF_NATIVE_FAILURE_INVALID_SPECIFICATION for an invalid spec, - * PF_NATIVE_FAILURE_CONTRACT for a Ready or Running handle or a refused reuse - * (a handle an abort already failed keeps that failure). It has no typed - * out-parameters, and no configure call accepts a handle it failed. The + * which answers an invalid spec with -1 and a Failed native handle + * (PF_NATIVE_FAILURE_INVALID_SPECIFICATION). A Ready handle still fails by + * the kernel's Contract rule. A Running handle is refused at the C boundary + * without changing its native state, including from a callback. A Completed + * handle or one Failed by a cooperative abort may be configured for another + * run outside callbacks; the kernel judges reuse constraints. A Completed + * host's `on_hash_extension` callback cannot reconfigure it during a read-only hash query. + * This call has no typed out-parameters. The * extended path validates before configuring: #strategy_configure_native_ext_v1 * keeps the handle Unconfigured on a validation refusal, and * #strategy_configure_native_ext_result_v1 additionally writes the exact @@ -548,6 +565,7 @@ typedef enum pf_native_fx_curve_error_e { PF_NATIVE_FX_CURVE_ERROR_ALLOCATION_FAILURE = 4, PF_NATIVE_FX_CURVE_ERROR_WRONG_PHASE = 5 } pf_native_fx_curve_error_t; +PF_STATIC_ASSERT(sizeof(pf_native_fx_curve_error_t) == 4); /** Stage an immutable account-currency FX curve on a Ready native handle. * diff --git a/scripts/ci_verify.py b/scripts/ci_verify.py index 1ce2a52b..a5e80933 100644 --- a/scripts/ci_verify.py +++ b/scripts/ci_verify.py @@ -226,7 +226,8 @@ # dual-entry witness reach the source layer (see RELEASE_MIN_TESTS), and # B-C-SURFACE and B-DOCS add no row. 272 registered, 271 run: the WebSocket # row still skips on a system libcurl. -KERNEL_MIN_TESTS = 271 +# C-SURFACE-1 adds the INT23 frozen-header decision-tail witness. +KERNEL_MIN_TESTS = 272 # 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 @@ -327,9 +328,9 @@ # test_adapter_margin_revival_cancel # +1 INT24 test_adapter_dual_entry_tie (the ruling on B-ADAPTER's # finding 1) -# B-C-SURFACE's new witnesses are rows inside test_native_c_api. No release row -# skips, so 672 registered is 672 run. -RELEASE_MIN_TESTS = 672 +# B-C-SURFACE's witnesses are rows inside test_native_c_api. C-SURFACE-1 adds +# the INT23 frozen-header decision-tail row. No release row skips: 673 run. +RELEASE_MIN_TESTS = 673 # PR-only registration floors. These are the complete CTest populations at # 91d65ad6 (INT24); the ordinary full-run floors above remain unchanged. # An excluded run must still discover at least this many rows before -LE. diff --git a/src/c_abi.cpp b/src/c_abi.cpp index 36fdcb66..fa0470c1 100644 --- a/src/c_abi.cpp +++ b/src/c_abi.cpp @@ -53,6 +53,11 @@ #include #include +namespace pineforge { +void clear_native_c_host_refusal_error(BacktestEngine* engine) noexcept; +bool native_c_host_in_postrun_hook_frame(BacktestEngine* engine) noexcept; +} + namespace { template @@ -72,6 +77,10 @@ int pf_cabi_int(Fn&& fn) noexcept { } } +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic error "-Wswitch" +#endif pf_native_fx_curve_error_t fx_curve_error_word( pineforge::NativeFxCurveError error) noexcept { switch (error) { @@ -88,20 +97,25 @@ pf_native_fx_curve_error_t fx_curve_error_word( case pineforge::NativeFxCurveError::WrongPhase: return PF_NATIVE_FX_CURVE_ERROR_WRONG_PHASE; } - return static_cast(error); + std::terminate(); } +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic pop +#endif int configure_native_fx_curve_cabi( pf_strategy_t s, const pf_native_fx_curve_v1* curve, pf_native_fx_curve_error_t* error, std::uint64_t* index) noexcept { return pf_cabi_int([&] { - if (!s || !curve) return -1; + if (!s) return -1; auto* engine = static_cast(s); if (!engine->native_bound()) return -1; - if (curve->struct_size != sizeof(pf_native_fx_curve_v1)) return -1; - if (curve->n > 0 && (!curve->effective_from_ms || !curve->account_per_quote)) return -1; auto* host = dynamic_cast(engine); if (!host) return -1; + pineforge::clear_native_c_host_refusal_error(engine); + if (!curve) return -1; + if (curve->struct_size != sizeof(pf_native_fx_curve_v1)) return -1; + if (curve->n > 0 && (!curve->effective_from_ms || !curve->account_per_quote)) return -1; pineforge::NativeFxCurve cpp; if (curve->n > 0) { @@ -881,12 +895,25 @@ PF_API int strategy_execution_contract(pf_strategy_t s) { PF_API int strategy_configure_native_v1(pf_strategy_t s, const pf_native_run_spec_v1* spec) { try { - if (!s || !spec) return -1; + if (!s) return -1; auto* engine = static_cast(s); if (!engine->native_bound()) return -1; - if (spec->struct_size != sizeof(pf_native_run_spec_v1)) return -1; auto* host = dynamic_cast(engine); if (!host) return -1; + pineforge::clear_native_c_host_refusal_error(engine); + if (!spec) return -1; + if (spec->struct_size != sizeof(pf_native_run_spec_v1)) return -1; + if (pineforge::native_c_host_in_postrun_hook_frame(engine)) return -1; + /* A reentrant configure must be refused at the C layer. The C++ + * configure contract would latch a Running host Failed, changing the + * run from inside a hook that only asked for an invalid operation. */ + const auto state = host->native_state(); + /* Ready misuse keeps its established kernel Contract latch. */ + if (state.kind == pineforge::NativeLifecycleKind::Running + || (state.kind == pineforge::NativeLifecycleKind::Failed + && state.failure.code != pineforge::NativeFailureCode::Aborted)) { + return -1; + } const auto require = [](const char* p) -> const char* { return p ? p : ""; }; diff --git a/src/native_c_host.cpp b/src/native_c_host.cpp index 4121a71e..a40e56b5 100644 --- a/src/native_c_host.cpp +++ b/src/native_c_host.cpp @@ -1355,6 +1355,23 @@ class AppliedFrame { bool outer_; }; +/* Running callbacks are covered by the kernel lifecycle check at configure. + * The hash hook can also run during a read of a Completed host, so that + * frame needs its own C-boundary mark. */ +class HookFlagFrame { +public: + explicit HookFlagFrame(bool& inside) noexcept : inside_(inside), outer_(inside) { + inside_ = true; + } + ~HookFlagFrame() { inside_ = outer_; } + HookFlagFrame(const HookFlagFrame&) = delete; + HookFlagFrame& operator=(const HookFlagFrame&) = delete; + +private: + bool& inside_; + bool outer_; +}; + /* The bucket interval is borrowed only for one C timeframe callback. Restore * the outer frame even when the callback raises CallbackFailure. */ class TimeframeIntervalFrame { @@ -1399,6 +1416,12 @@ class CCallbackHost final : public NativeStrategyHost { const pf_native_callbacks_v1& table() const noexcept { return table_; } + /* A C validation refusal is a status, not a new kernel diagnostic. Keep + * presentation text from a previous operation from masquerading as its + * reason; the durable native failure record is left untouched. */ + void clear_c_refusal_error() noexcept { last_error_.clear(); } + bool in_postrun_hook_frame() const noexcept { return in_hash_extension_; } + bool timeframe_interval(pf_native_timeframe_interval_v1& out) const noexcept { if (!timeframe_interval_) return false; const auto& interval = *timeframe_interval_; @@ -1630,6 +1653,7 @@ class CCallbackHost final : public NativeStrategyHost { void hash_host_extension(pineforge::BrokerStateHashSink& sink) const override { BacktestEngine::hash_host_extension(sink); if (!table_.on_hash_extension) return; + const HookFlagFrame frame(in_hash_extension_); std::uint64_t digest = 0; if (table_.on_hash_extension(table_.user, &digest) == PF_NATIVE_ANSWER_DEFAULT) return; sink.s("native-c-host:extension"); @@ -1716,6 +1740,9 @@ class CCallbackHost final : public NativeStrategyHost { /* True while the C `on_applied` runs. Host-side, like the two caches: * never durable engine state, never hashed. */ bool in_applied_ = false; + /* A Completed host may invoke this hook from a read-only hash query. + * Host-side transport state only; never folded into the broker hash. */ + mutable bool in_hash_extension_ = false; }; pf_native_decision_v1 CCallbackHost::decision( @@ -1745,9 +1772,9 @@ pf_native_decision_v1 CCallbackHost::decision( out.price = point->price; out.quote_kind = c_byte(c_word(point->quote_kind)); } - /* A table of an earlier published length is presented the base layout - * its header compiled, and nothing past it is computed. */ - if (caller_table_size_ != sizeof(pf_native_callbacks_v1)) { + /* The policy-hook layout first published the decision's session tail. + * The later lot-facts marker changes only that other presented POD. */ + if (caller_table_size_ < PF_NATIVE_CALLBACKS_V1_POLICY_SIZE) { out.struct_size = PF_NATIVE_DECISION_V1_BASE_SIZE; return out; } @@ -2364,6 +2391,11 @@ CCallbackHost* host_of(pf_strategy_t s) { return dynamic_cast(engine); } +int c_refuse(CCallbackHost* host, int status) noexcept { + host->clear_c_refusal_error(); + return status; +} + /* The staged/running specification's identity, which every RequestHandle a C * caller names belongs to. Absent before the first configure. */ const no::RunIdentity* run_identity(const CCallbackHost& host) { @@ -3269,6 +3301,18 @@ int apply_spec_ext(pineforge::NativeRunSpec& spec, const pf_native_run_spec_ext_ } // namespace +namespace pineforge { +/* Internal bridge for the older configure symbols implemented in c_abi.cpp. + * It reaches only the C callback host and adds no public ABI symbol. */ +void clear_native_c_host_refusal_error(BacktestEngine* engine) noexcept { + if (auto* host = dynamic_cast(engine)) host->clear_c_refusal_error(); +} +bool native_c_host_in_postrun_hook_frame(BacktestEngine* engine) noexcept { + if (auto* host = dynamic_cast(engine)) return host->in_postrun_hook_frame(); + return false; +} +} // namespace pineforge + extern "C" { PF_API int strategy_native_api_version(void) { return PF_NATIVE_API_VERSION; } @@ -3289,6 +3333,10 @@ PF_API pf_strategy_t strategy_native_host_create_v1(const pf_native_callbacks_v1 return nullptr; } if (callbacks->version != PF_NATIVE_API_VERSION) return nullptr; + /* Only the current layout carries this marker. Older callers end + * before it and are not read beyond their published length. */ + if (caller_size == sizeof(pf_native_callbacks_v1) + && callbacks->reserved1 != 0u) return nullptr; pf_native_callbacks_v1 table; std::memset(&table, 0, sizeof(table)); std::memcpy(&table, callbacks, callbacks->struct_size); @@ -3319,7 +3367,7 @@ PF_API int strategy_native_run_v1(pf_strategy_t s, const pf_bar_t* bars, int n, return guarded([&] { auto* host = host_of(s); if (!host) return PF_NATIVE_E_HANDLE; - if (n < 0 || (n > 0 && !bars)) return PF_NATIVE_E_ARGUMENT; + if (n < 0 || (n > 0 && !bars)) return c_refuse(host, PF_NATIVE_E_ARGUMENT); host->run(reinterpret_cast(bars), n); if (out) host->fill_report(reinterpret_cast(out)); return host->native_state().kind == pineforge::NativeLifecycleKind::Completed @@ -3341,11 +3389,13 @@ PF_API int strategy_native_submit_v1(pf_strategy_t s, const pf_native_request_v1 return guarded([&] { auto* host = host_of(s); if (!host) return PF_NATIVE_E_HANDLE; - if (!request) return PF_NATIVE_E_ARGUMENT; + if (!request) return c_refuse(host, PF_NATIVE_E_ARGUMENT); const auto* run = run_identity(*host); - if (!run) return PF_NATIVE_E_STATE; + if (!run) return c_refuse(host, PF_NATIVE_E_STATE); no::Request translated; - if (int rc = translate_request(*request, *run, translated); rc != PF_NATIVE_OK) return rc; + if (int rc = translate_request(*request, *run, translated); rc != PF_NATIVE_OK) { + return c_refuse(host, rc); + } const auto result = host->submit(translated); if (result.status == no::SubmitStatus::Accepted) { if (incarnation && result.handle) *incarnation = result.handle->incarnation; @@ -3362,11 +3412,13 @@ PF_API int strategy_native_replace_ext_v1(pf_strategy_t s, uint64_t incarnation, return guarded([&] { auto* host = host_of(s); if (!host) return PF_NATIVE_E_HANDLE; - if (!request) return PF_NATIVE_E_ARGUMENT; + if (!request) return c_refuse(host, PF_NATIVE_E_ARGUMENT); const auto* run = run_identity(*host); - if (!run) return PF_NATIVE_E_STATE; + if (!run) return c_refuse(host, PF_NATIVE_E_STATE); no::Request translated; - if (int rc = translate_request(*request, *run, translated); rc != PF_NATIVE_OK) return rc; + if (int rc = translate_request(*request, *run, translated); rc != PF_NATIVE_OK) { + return c_refuse(host, rc); + } const auto result = host->replace(no::RequestHandle{*run, incarnation}, translated); switch (result.status) { case no::ReplaceStatus::Replaced: @@ -3398,7 +3450,7 @@ PF_API int strategy_native_cancel_v1(pf_strategy_t s, uint64_t incarnation) { auto* host = host_of(s); if (!host) return PF_NATIVE_E_HANDLE; const auto* run = run_identity(*host); - if (!run) return PF_NATIVE_E_STATE; + if (!run) return c_refuse(host, PF_NATIVE_E_STATE); const auto result = host->cancel(no::RequestHandle{*run, incarnation}); switch (result.status) { case no::CancelStatus::Cancelled: return PF_NATIVE_OK; @@ -3424,7 +3476,7 @@ PF_API int strategy_native_cancel_where_v1(pf_strategy_t s, const char* text, ui // NULL is not the empty string here: "" is the text every request // that carries no comment or label matches, so a caller that meant // one and passed the other would cancel a different set. - if (!text) return PF_NATIVE_E_ARGUMENT; + if (!text) return c_refuse(host, PF_NATIVE_E_ARGUMENT); pineforge::NativeRequestField selector = pineforge::NativeRequestField::Comment; switch (field) { case PF_NATIVE_FIELD_COMMENT: @@ -3434,7 +3486,7 @@ PF_API int strategy_native_cancel_where_v1(pf_strategy_t s, const char* text, ui selector = pineforge::NativeRequestField::Label; break; default: - return PF_NATIVE_E_TAG; + return c_refuse(host, PF_NATIVE_E_TAG); } return static_cast(host->cancel_where(std::string_view(text), selector)); }); @@ -3446,7 +3498,7 @@ PF_API int strategy_native_execute_current_v1(pf_strategy_t s, uint64_t incarnat auto* host = host_of(s); if (!host) return PF_NATIVE_E_HANDLE; const auto* run = run_identity(*host); - if (!run) return PF_NATIVE_E_STATE; + if (!run) return c_refuse(host, PF_NATIVE_E_STATE); pineforge::NativeCurrentExecution selection; selection.target = no::RequestHandle{*run, incarnation}; switch (price_rule) { @@ -3457,7 +3509,7 @@ PF_API int strategy_native_execute_current_v1(pf_strategy_t s, uint64_t incarnat selection.price_rule = pineforge::NativeCurrentPriceRule::NearestTick; break; default: - return PF_NATIVE_E_TAG; + return c_refuse(host, PF_NATIVE_E_TAG); } const auto result = host->execute_current(selection); return std::visit([&](const auto& outcome) -> int { @@ -3505,7 +3557,7 @@ PF_API int strategy_native_working_get_v1(pf_strategy_t s, int index, return guarded([&] { auto* host = host_of(s); if (!host) return PF_NATIVE_E_HANDLE; - if (!out) return PF_NATIVE_E_ARGUMENT; + if (!out) return c_refuse(host, PF_NATIVE_E_ARGUMENT); /* Three published layouts: the base one the L13 lane first shipped, * that plus the arm-presence tail, and the current one with the * relation tail. An earlier caller's struct ends at its own length, @@ -3514,11 +3566,11 @@ PF_API int strategy_native_working_get_v1(pf_strategy_t s, int index, if (struct_size != sizeof(pf_native_working_v1) && struct_size != PF_NATIVE_WORKING_V1_ARM_SIZE && struct_size != PF_NATIVE_WORKING_V1_BASE_SIZE) { - return PF_NATIVE_E_STRUCT; + return c_refuse(host, PF_NATIVE_E_STRUCT); } const auto& cache = host->working_cache(); if (index < 0 || static_cast(index) >= cache.size()) { - return PF_NATIVE_E_ARGUMENT; + return c_refuse(host, PF_NATIVE_E_ARGUMENT); } pf_native_working_v1 row; fill_working(cache[static_cast(index)], row); @@ -3542,11 +3594,11 @@ PF_API int strategy_native_open_lot_get_v1(pf_strategy_t s, int index, return guarded([&] { auto* host = host_of(s); if (!host) return PF_NATIVE_E_HANDLE; - if (!out) return PF_NATIVE_E_ARGUMENT; - if (out->struct_size != sizeof(pf_native_open_lot_v1)) return PF_NATIVE_E_STRUCT; + if (!out) return c_refuse(host, PF_NATIVE_E_ARGUMENT); + if (out->struct_size != sizeof(pf_native_open_lot_v1)) return c_refuse(host, PF_NATIVE_E_STRUCT); const auto& cache = host->open_lot_cache(); if (index < 0 || static_cast(index) >= cache.size()) { - return PF_NATIVE_E_ARGUMENT; + return c_refuse(host, PF_NATIVE_E_ARGUMENT); } fill_open_lot(cache[static_cast(index)], *out); return PF_NATIVE_OK; @@ -3558,7 +3610,7 @@ PF_API int strategy_native_events_v1(pf_strategy_t s, uint64_t after_ordinal, return guarded([&] { auto* host = host_of(s); if (!host) return PF_NATIVE_E_HANDLE; - if (cap < 0 || (cap > 0 && !out)) return PF_NATIVE_E_ARGUMENT; + if (cap < 0 || (cap > 0 && !out)) return c_refuse(host, PF_NATIVE_E_ARGUMENT); const auto events = host->native_events(after_ordinal); int written = 0; std::size_t consumed = 0; @@ -3592,8 +3644,8 @@ PF_API int strategy_native_state_v1(pf_strategy_t s, pf_native_state_v1* out) { return guarded([&] { auto* host = host_of(s); if (!host) return PF_NATIVE_E_HANDLE; - if (!out) return PF_NATIVE_E_ARGUMENT; - if (out->struct_size != sizeof(pf_native_state_v1)) return PF_NATIVE_E_STRUCT; + if (!out) return c_refuse(host, PF_NATIVE_E_ARGUMENT); + if (out->struct_size != sizeof(pf_native_state_v1)) return c_refuse(host, PF_NATIVE_E_STRUCT); const auto state = host->native_state(); const std::uint32_t struct_size = out->struct_size; std::memset(out, 0, sizeof(*out)); @@ -3619,14 +3671,14 @@ PF_API int strategy_native_declare_subscriptions_ext_v1(pf_strategy_t s, return guarded([&] { auto* host = host_of(s); if (!host) return PF_NATIVE_E_HANDLE; - if (n < 0 || (n > 0 && !rows)) return PF_NATIVE_E_ARGUMENT; + if (n < 0 || (n > 0 && !rows)) return c_refuse(host, PF_NATIVE_E_ARGUMENT); std::vector declared; /* NULL sources build every series from the input, exactly as the * run spec's own subscription block does without its source column. */ if (int rc = translate_subscriptions(rows, static_cast(n), sources, declared); rc != PF_NATIVE_OK) { - return rc; + return c_refuse(host, rc); } const auto result = host->declare_timeframe_subscriptions_result(std::move(declared)); write_validation(result.validation, error, field); @@ -3649,7 +3701,7 @@ PF_API int strategy_native_declare_auxiliary_feed_v1(pf_strategy_t s, const char return guarded([&] { auto* host = host_of(s); if (!host) return PF_NATIVE_E_HANDLE; - if (n < 0 || (n > 0 && !bars)) return PF_NATIVE_E_ARGUMENT; + if (n < 0 || (n > 0 && !bars)) return c_refuse(host, PF_NATIVE_E_ARGUMENT); std::optional feed; if (tf) { pineforge::NativeAuxiliaryFeed declared; @@ -3659,7 +3711,7 @@ PF_API int strategy_native_declare_auxiliary_feed_v1(pf_strategy_t s, const char feed = std::move(declared); } else if (n != 0 || bars) { /* A withdrawal names no feed, so it can carry no bars either. */ - return PF_NATIVE_E_ARGUMENT; + return c_refuse(host, PF_NATIVE_E_ARGUMENT); } const auto result = host->declare_auxiliary_feed_result(std::move(feed)); write_validation(result.validation, error, field); @@ -3672,7 +3724,7 @@ PF_API int strategy_native_partial_bar_v1(pf_strategy_t s, pf_bar_t* out) { return guarded([&] { auto* host = host_of(s); if (!host) return PF_NATIVE_E_HANDLE; - if (!out) return PF_NATIVE_E_ARGUMENT; + if (!out) return c_refuse(host, PF_NATIVE_E_ARGUMENT); const auto bar = host->current_partial_bar(); if (!bar) return PF_NATIVE_ABSENT; std::memcpy(out, &*bar, sizeof(pf_bar_t)); @@ -3696,10 +3748,10 @@ PF_API int strategy_native_trail_state_v1(pf_strategy_t s, uint64_t incarnation, return guarded([&] { auto* host = host_of(s); if (!host) return PF_NATIVE_E_HANDLE; - if (!out) return PF_NATIVE_E_ARGUMENT; - if (out->struct_size != sizeof(pf_native_trail_state_v1)) return PF_NATIVE_E_STRUCT; + if (!out) return c_refuse(host, PF_NATIVE_E_ARGUMENT); + if (out->struct_size != sizeof(pf_native_trail_state_v1)) return c_refuse(host, PF_NATIVE_E_STRUCT); const auto* run = run_identity(*host); - if (!run) return PF_NATIVE_E_STATE; + if (!run) return c_refuse(host, PF_NATIVE_E_STATE); const auto state = host->trail_state(no::RequestHandle{*run, incarnation}); if (!state) return PF_NATIVE_ABSENT; const std::uint32_t struct_size = out->struct_size; @@ -3719,7 +3771,7 @@ PF_API int strategy_native_series_bar_v1(pf_strategy_t s, uint32_t subscription, return guarded([&] { auto* host = host_of(s); if (!host) return PF_NATIVE_E_HANDLE; - if (!out) return PF_NATIVE_E_ARGUMENT; + if (!out) return c_refuse(host, PF_NATIVE_E_ARGUMENT); const auto bar = host->native_series_bar(static_cast(subscription)); if (!bar) return PF_NATIVE_ABSENT; std::memcpy(out, &*bar, sizeof(pf_bar_t)); @@ -3732,8 +3784,8 @@ PF_API int strategy_native_timeframe_bar_interval_v1( return guarded([&] { auto* host = host_of(s); if (!host) return PF_NATIVE_E_HANDLE; - if (!out) return PF_NATIVE_E_ARGUMENT; - if (out->struct_size != sizeof(pf_native_timeframe_interval_v1)) return PF_NATIVE_E_STRUCT; + if (!out) return c_refuse(host, PF_NATIVE_E_ARGUMENT); + if (out->struct_size != sizeof(pf_native_timeframe_interval_v1)) return c_refuse(host, PF_NATIVE_E_STRUCT); return host->timeframe_interval(*out) ? PF_NATIVE_OK : PF_NATIVE_E_STATE; }); } @@ -3742,7 +3794,7 @@ PF_API int strategy_native_marked_equity_v1(pf_strategy_t s, double mark, double return guarded([&] { auto* host = host_of(s); if (!host) return PF_NATIVE_E_HANDLE; - if (!out) return PF_NATIVE_E_ARGUMENT; + if (!out) return c_refuse(host, PF_NATIVE_E_ARGUMENT); *out = host->native_marked_equity(mark); return PF_NATIVE_OK; }); @@ -3752,7 +3804,7 @@ PF_API int strategy_native_liquidation_price_v1(pf_strategy_t s, double* out) { return guarded([&] { auto* host = host_of(s); if (!host) return PF_NATIVE_E_HANDLE; - if (!out) return PF_NATIVE_E_ARGUMENT; + if (!out) return c_refuse(host, PF_NATIVE_E_ARGUMENT); const auto price = host->native_liquidation_price(); if (!price) { *out = kNaN; @@ -3769,7 +3821,7 @@ PF_API int strategy_native_sized_units_v1(pf_strategy_t s, const pf_native_reque return guarded([&] { auto* host = host_of(s); if (!host) return PF_NATIVE_E_HANDLE; - if (!sized || !units) return PF_NATIVE_E_ARGUMENT; + if (!sized || !units) return c_refuse(host, PF_NATIVE_E_ARGUMENT); /* The sizing block is read by the one translation a submit uses. A * SIZED request names no handle, so before configure -- when the run * has no identity yet -- an empty one serves, and the kernel's own @@ -3779,10 +3831,10 @@ PF_API int strategy_native_sized_units_v1(pf_strategy_t s, const pf_native_reque no::Request translated; if (int rc = translate_request(*sized, run ? *run : none, translated); rc != PF_NATIVE_OK) { - return rc; + return c_refuse(host, rc); } const auto* basis = std::get_if(&translated.intent); - if (!basis) return PF_NATIVE_E_ARGUMENT; + if (!basis) return c_refuse(host, PF_NATIVE_E_ARGUMENT); const auto answer = host->native_sized_units(*basis, price, equity, fx); if (!answer) { *units = kNaN; @@ -3797,8 +3849,8 @@ PF_API int strategy_native_risk_state_v1(pf_strategy_t s, pf_native_risk_state_v return guarded([&] { auto* host = host_of(s); if (!host) return PF_NATIVE_E_HANDLE; - if (!out) return PF_NATIVE_E_ARGUMENT; - if (out->struct_size != sizeof(pf_native_risk_state_v1)) return PF_NATIVE_E_STRUCT; + if (!out) return c_refuse(host, PF_NATIVE_E_ARGUMENT); + if (out->struct_size != sizeof(pf_native_risk_state_v1)) return c_refuse(host, PF_NATIVE_E_STRUCT); const auto state = host->native_risk_state(); const std::uint32_t struct_size = out->struct_size; std::memset(out, 0, sizeof(*out)); @@ -3824,8 +3876,8 @@ PF_API int strategy_native_margin_call_v1(pf_strategy_t s, uint64_t ordinal, return guarded([&] { auto* host = host_of(s); if (!host) return PF_NATIVE_E_HANDLE; - if (!out) return PF_NATIVE_E_ARGUMENT; - if (out->struct_size != sizeof(pf_native_margin_call_v1)) return PF_NATIVE_E_STRUCT; + if (!out) return c_refuse(host, PF_NATIVE_E_ARGUMENT); + if (out->struct_size != sizeof(pf_native_margin_call_v1)) return c_refuse(host, PF_NATIVE_E_STRUCT); if (ordinal == 0u) return PF_NATIVE_ABSENT; /* The history is ordered by ordinal, so the call is the first row at * or past it, or it is not there at all. */ @@ -3854,7 +3906,7 @@ PF_API int strategy_native_event_window_v1(pf_strategy_t s, uint64_t* out_first_ return guarded([&] { auto* host = host_of(s); if (!host) return PF_NATIVE_E_HANDLE; - if (!out_first_ordinal) return PF_NATIVE_E_ARGUMENT; + if (!out_first_ordinal) return c_refuse(host, PF_NATIVE_E_ARGUMENT); *out_first_ordinal = host->native_event_window_start(); return PF_NATIVE_OK; }); @@ -3864,7 +3916,7 @@ PF_API int strategy_native_continuation_hash_v1(pf_strategy_t s, uint64_t* out) return guarded([&] { auto* host = host_of(s); if (!host) return PF_NATIVE_E_HANDLE; - if (!out) return PF_NATIVE_E_ARGUMENT; + if (!out) return c_refuse(host, PF_NATIVE_E_ARGUMENT); *out = host->native_continuation_hash(); return PF_NATIVE_OK; }); @@ -3874,7 +3926,7 @@ PF_API int strategy_native_cohort_open_v1(pf_strategy_t s, uint64_t* cohort) { return guarded([&] { auto* host = host_of(s); if (!host) return PF_NATIVE_E_HANDLE; - if (!cohort) return PF_NATIVE_E_ARGUMENT; + if (!cohort) return c_refuse(host, PF_NATIVE_E_ARGUMENT); *cohort = host->cohort_open().value; return PF_NATIVE_OK; }); @@ -3886,8 +3938,8 @@ PF_API int strategy_native_cohort_add_v1(pf_strategy_t s, uint64_t cohort, auto* host = host_of(s); if (!host) return PF_NATIVE_E_HANDLE; const auto* run = run_identity(*host); - if (!run) return PF_NATIVE_E_STATE; - if (cohort == 0u) return PF_NATIVE_E_ARGUMENT; + if (!run) return c_refuse(host, PF_NATIVE_E_STATE); + if (cohort == 0u) return c_refuse(host, PF_NATIVE_E_ARGUMENT); host->cohort_add(no::CohortHandle{cohort}, no::RequestHandle{*run, incarnation}); return PF_NATIVE_OK; }); @@ -3899,8 +3951,8 @@ PF_API int strategy_native_cohort_remove_v1(pf_strategy_t s, uint64_t cohort, auto* host = host_of(s); if (!host) return PF_NATIVE_E_HANDLE; const auto* run = run_identity(*host); - if (!run) return PF_NATIVE_E_STATE; - if (cohort == 0u) return PF_NATIVE_E_ARGUMENT; + if (!run) return c_refuse(host, PF_NATIVE_E_STATE); + if (cohort == 0u) return c_refuse(host, PF_NATIVE_E_ARGUMENT); host->cohort_remove(no::CohortHandle{cohort}, no::RequestHandle{*run, incarnation}); return PF_NATIVE_OK; }); @@ -3912,8 +3964,8 @@ PF_API int strategy_configure_native_ext_result_v1( return guarded([&] { auto* host = host_of(s); if (!host) return PF_NATIVE_E_HANDLE; - if (!base || !ext) return PF_NATIVE_E_ARGUMENT; - if (base->struct_size != sizeof(pf_native_run_spec_v1)) return PF_NATIVE_E_STRUCT; + if (!base || !ext) return c_refuse(host, PF_NATIVE_E_ARGUMENT); + if (base->struct_size != sizeof(pf_native_run_spec_v1)) return c_refuse(host, PF_NATIVE_E_STRUCT); /* Five published layouts, and only five: the base one the lane * first shipped, that plus L9's risk tail, that plus N8's intrabar / * policy tail, that plus the auxiliary-feed tail, and the current one @@ -3928,7 +3980,12 @@ PF_API int strategy_configure_native_ext_result_v1( has_policy_tail || ext->struct_size == PF_NATIVE_RUN_SPEC_EXT_V1_RISK_SIZE; if ((!has_risk_tail && ext->struct_size != PF_NATIVE_RUN_SPEC_EXT_V1_BASE_SIZE) || ext->version != PF_NATIVE_API_VERSION) { - return PF_NATIVE_E_STRUCT; + return c_refuse(host, PF_NATIVE_E_STRUCT); + } + if (host->in_postrun_hook_frame()) { + write_validation({pineforge::NativeRunSpecError::WrongPhase, + pineforge::NativeRunSpecField::None}, error, field); + return c_refuse(host, PF_NATIVE_E_STATE); } /* The established spelling configures an Unconfigured host only, so * its pre-kernel readiness check keeps every refusal non-mutating. The @@ -3946,14 +4003,16 @@ PF_API int strategy_configure_native_ext_result_v1( if (!reusable || (!typed && lifecycle.kind != pineforge::NativeLifecycleKind::Unconfigured)) { write_validation({pineforge::NativeRunSpecError::WrongPhase, pineforge::NativeRunSpecField::None}, error, field); - return PF_NATIVE_E_STATE; + return c_refuse(host, PF_NATIVE_E_STATE); } pineforge::NativeRunSpec spec; - if (int rc = translate_base_spec(*base, spec); rc != PF_NATIVE_OK) return rc; + if (int rc = translate_base_spec(*base, spec); rc != PF_NATIVE_OK) { + return c_refuse(host, rc); + } if (int rc = apply_spec_ext(spec, *ext, has_risk_tail, has_policy_tail, has_auxiliary_tail, has_retention_tail); rc != PF_NATIVE_OK) { - return rc; + return c_refuse(host, rc); } /* The kernel FAILS a host whose specification its validation * refuses, so the same validation runs here first: a rejected @@ -3962,7 +4021,7 @@ PF_API int strategy_configure_native_ext_result_v1( * cannot disagree. */ const auto validation = pineforge::validate_native_run_spec(spec); write_validation(validation, error, field); - if (!validation) return PF_NATIVE_E_ARGUMENT; + if (!validation) return c_refuse(host, PF_NATIVE_E_ARGUMENT); const auto result = host->configure_native(spec); write_validation(result.validation, error, field); return result.status == pineforge::NativeSetupStatus::Applied @@ -3983,7 +4042,7 @@ PF_API int strategy_native_append_auxiliary_bars_ext_v1(pf_strategy_t s, const p return guarded([&] { auto* host = host_of(s); if (!host) return PF_NATIVE_E_HANDLE; - if (n < 0 || (n > 0 && !bars)) return PF_NATIVE_E_ARGUMENT; + if (n < 0 || (n > 0 && !bars)) return c_refuse(host, PF_NATIVE_E_ARGUMENT); const auto result = host->append_auxiliary_bars_result( reinterpret_cast(bars), static_cast(n)); if (error) *error = c_word(result.error); @@ -4007,7 +4066,7 @@ PF_API int strategy_native_declare_opened_lot_entry_bar_mask_v1(pf_strategy_t s, return guarded([&] { auto* host = host_of(s); if (!host) return PF_NATIVE_E_HANDLE; - if (!entry_bar) return PF_NATIVE_E_ARGUMENT; + if (!entry_bar) return c_refuse(host, PF_NATIVE_E_ARGUMENT); pineforge::OpenedLotFillPoint point = pineforge::OpenedLotFillPoint::OnPath; switch (fill_point) { case PF_NATIVE_OPENED_LOT_FILL_POINT_ON_PATH: @@ -4017,7 +4076,7 @@ PF_API int strategy_native_declare_opened_lot_entry_bar_mask_v1(pf_strategy_t s, point = pineforge::OpenedLotFillPoint::AfterPath; break; default: - return PF_NATIVE_E_TAG; + return c_refuse(host, PF_NATIVE_E_TAG); } return host->declare_entry_bar_mask(entry_incarnation, *reinterpret_cast(entry_bar), point) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index fd0103a0..ee03ed78 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -2135,6 +2135,17 @@ endif() pineforge_add_twin_test(test_native_c_api test_native_c_api) pineforge_add_twin_test(test_native_c_api_frozen_header test_native_c_api_frozen_header) +# C-SURFACE-1: an INT23 object compiled with the db98990c C header closure +# must receive the decision tail its 168-byte policy-hook table published. +# The fixture include path precedes the live headers for this target only. +add_executable(test_native_c_api_int23_header test_native_c_api_int23_header.c) +target_include_directories(test_native_c_api_int23_header BEFORE PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/fixtures/native_c_api/int23) +set_target_properties(test_native_c_api_int23_header PROPERTIES LINKER_LANGUAGE CXX) +target_link_libraries(test_native_c_api_int23_header PRIVATE pineforge) +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 diff --git a/tests/fixtures/native_c_api/int23/pineforge/native_c_api.h b/tests/fixtures/native_c_api/int23/pineforge/native_c_api.h new file mode 100644 index 00000000..74c6ebf6 --- /dev/null +++ b/tests/fixtures/native_c_api/int23/pineforge/native_c_api.h @@ -0,0 +1,3102 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * native_c_api.h — versioned C-level native host API (R5 lane L13). + * + * 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. + * + * SCOPE + * ───── + * ✓ Create / free a native host backed by a C callback table + * ✓ Run a batch of OHLCV bars and fill a pf_report_t + * ✓ submit / replace / cancel / cancel_all / cancel_where / execute_current + * ✓ Read the physical position, the live working book, the open lots (the + * book lot by lot, marked at a price — strategy.opentrades.* for a C host) + * and the event history + * ✓ Read, at every decision point, the script interval and the session day + * of its open and of the next input — the facts session-day flags are + * derived from + * ✓ Read the run's lifecycle state and its typed failure + * ✓ Read a margin call's whole economics — equity, requirement and the + * position on either side of it — by its event ordinal + * ✓ Read a closed row's own identifiers — its entry and exit ticket, its exit + * comment and its close cause — with the pineforge.h accessors, which take + * any handle this header produces + * ✓ Arm a WAIT_FOR_APPLIED child after its owner's print, or bound to the + * whole book, and size its close with on_close_units + * ✓ Ask the kernel what a SIZED request would resolve to, before submitting + * ✓ Extend the run specification with the fields pf_native_run_spec_v1 predates + * ✓ Declare an auxiliary finer feed — up front, or from `on_run_begin` — + * build a series from it, and append its later bars to a realtime stream, + * each refusal named by the kernel's own typed answer + * ✓ Declare, from `on_applied`, where a lot's opening fill sat on its entry + * bar, for a host that owns its lots' excursions + * + * ✗ Streaming has no new symbols: strategy_stream_begin / _push_bar / + * _push_tick / _advance_time / _end / _fill_report take any handle this + * header produces, unchanged. + * ✓ Answer the kernel's policy hooks: a host-sized close's units + * (on_close_units), a candidate's settlement price and opening shape + * (on_execution_terms), the last gate before a fill (on_precommit), where + * an anchored leg is armed (on_anchored_level), and the host's own state + * folded into the broker-state hash (on_hash_extension). The per-bar hash + * rows need no new symbol: `report_policy` = + * PF_NATIVE_REPORT_KERNEL_RECORDED with + * strategy_set_broker_state_hash_recording fills + * pf_report_t::broker_state_hash, one row per script bar. + * + * COVERAGE + * ──────── + * Every public member of NativeStrategyHost, and either its C spelling or the + * reason it has none. scripts/check_native_c_api_surface.py proves this list is + * exactly that class's public surface: a member added there without a row here, + * or a row here naming a member that no longer exists, fails CI. + * + * [C] on_bar strategy_native_run_v1 / the strategy_stream_* ingress + * [--] prepare_native_begin borrows the codegen ingress (InputsMap, SymInfo, the opaque + * overrides) that no C host supplies; a C run is declared up front + * with strategy_configure_native_ext_v1 + * [C] on_native_run_begin pf_native_callbacks_v1::on_run_begin + * [C] on_native_input pf_native_callbacks_v1::on_input + * [C] on_native_tick pf_native_callbacks_v1::on_tick + * [C] on_native_timeframe_bar pf_native_callbacks_v1::on_timeframe_bar + * [C] on_native_bar_open pf_native_callbacks_v1::on_bar_open + * [C] on_native_bar pf_native_callbacks_v1::on_bar + * [C] on_native_recalculate pf_native_callbacks_v1::on_recalculate + * [C] on_native_sub_bar pf_native_callbacks_v1::on_sub_bar + * [C] on_native_applied pf_native_callbacks_v1::on_applied + * [C] on_native_margin_call pf_native_callbacks_v1::on_margin_call -- the call's whole + * MarginCallEvent is strategy_native_margin_call_v1, by the + * ordinal the hook is handed + * [C] resolve_execution_terms pf_native_callbacks_v1::on_close_units (the units of a host-sized + * close) and pf_native_callbacks_v1::on_execution_terms (the price, + * the opening shape and the grid policy) + * [C] validate_execution_precommit pf_native_callbacks_v1::on_precommit -- the plan, the inspection + * and the projected account flattened into + * pf_native_precommit_view_v1, the closed rows' P&L borrowed + * [C] resolve_anchored_level pf_native_callbacks_v1::on_anchored_level + * [C] resolve_margin_requirement pf_native_callbacks_v1::on_margin_requirement + * [C] margin_check_allowed pf_native_callbacks_v1::on_margin_check + * [C] resolve_margin_call_units pf_native_callbacks_v1::on_margin_call_units + * [C] owns_lot_excursions pf_native_callbacks_v1::on_lot_excursion -- installing it IS + * declaring ownership + * [C] closed_lot_excursion pf_native_callbacks_v1::on_lot_excursion + * [C] declare_native_bar_open_hook pf_native_callbacks_v1::on_bar_open -- a table without it + * declares none, once, when the host is created + * [C] declare_native_precommit_hook pf_native_callbacks_v1::on_precommit -- a table without it + * declares none, once, when the host is created + * [C] current_partial_bar strategy_native_partial_bar_v1 + * [C] native_recalculation_count strategy_native_recalculations_v1 + * [C] native_recalculations_skipped strategy_native_recalculations_v1 + * [C] current_execution_point pf_native_decision_v1::price / ::quote_kind, on every callback; + * 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 + * [C] execute_current strategy_native_execute_current_v1 + * [C] native_series_bar strategy_native_series_bar_v1 + * [C] declare_timeframe_subscriptions strategy_native_declare_subscriptions_v1 -- a row is + * pf_native_subscription_v1, whose `lookahead` / `gaps` bools are + * pf_native_lookahead_e / pf_native_gaps_e words + * [C] declare_timeframe_subscriptions_result strategy_native_declare_subscriptions_ext_v1 -- the + * same call, its NativeRunSpecValidation written to `error` / + * `field` (pf_native_spec_error_e / pf_native_spec_field_e), plus + * a series source per row + * [C] declare_auxiliary_feed strategy_native_declare_auxiliary_feed_v1 -- a NULL `tf` + * withdraws the staged feed + * [C] declare_auxiliary_feed_result strategy_native_declare_auxiliary_feed_v1, whose `error` / + * `field` out-parameters are the typed answer + * [C] append_auxiliary_bars strategy_native_append_auxiliary_bars_v1 + * [C] append_auxiliary_bars_result strategy_native_append_auxiliary_bars_ext_v1 -- + * NativeAuxiliaryAppendError (pf_native_append_error_e) and the + * bar of the call it stopped on + * [C] configure_native strategy_configure_native_v1 / strategy_configure_native_ext_v1 -- + * the two specs' enum-valued words are pf_native_fee_kind_e, + * pf_native_close_execution_e, pf_native_open_directions_e, + * pf_native_report_policy_e, pf_native_price_grid_e, + * pf_native_grid_rounding_e, pf_native_calc_trigger_e, + * pf_native_open_bar_view_e, pf_native_liquidation_sizing_e + * and pf_native_event_retention_e words + * [C] configure_native_fx_curve strategy_configure_native_fx_curve_v1 (pineforge.h) + * [C] native_state strategy_native_state_v1 + * [C] submit strategy_native_submit_v1 -- a WAIT_FOR_APPLIED child's + * first_match / scope are pf_native_request_v1::arm_first_match / + * arm_scope + * [C] replace strategy_native_replace_v1, or strategy_native_replace_ext_v1 + * for the ReplaceResult::reason the first one drops + * [--] submit_market a C++ convenience that REFUSES non-market extras instead of dropping + * them; the same request is strategy_native_submit_v1 with + * PF_NATIVE_TRIGGER_MARKET and a zero-filled struct + * [--] replace_market the same convenience for a replace; see submit_market + * [C] cancel strategy_native_cancel_v1 + * [C] native_working_requests strategy_native_working_len_v1 / strategy_native_working_get_v1; + * a Trail's arm_price presence is + * pf_native_working_v1::trail_has_arm_price, the anchor and + * owner relation are pf_native_working_v1::anchor .. arm_scope + * [C] native_open_lots strategy_native_open_lot_count_v1 / strategy_native_open_lot_get_v1 + * [C] cancel_all strategy_native_cancel_all_v1 + * [C] cancel_where strategy_native_cancel_where_v1 + * [C] cohort_open strategy_native_cohort_open_v1 + * [C] cohort_add strategy_native_cohort_add_v1 + * [C] cohort_remove strategy_native_cohort_remove_v1 + * [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 + * [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 + * [C] native_event_window_start strategy_native_event_window_v1 + * [C] native_decision_floor pf_native_state_v1::decision_floor_ms + * [C] native_consumed_high_water pf_native_state_v1::consumed_high_water + * [C] native_continuation_hash strategy_native_continuation_hash_v1 + * [--] native_closed_rows_amended a C host reads the closed rows and never writes one -- the kernel + * books them -- so it has no amendment to name + * [C] native_sized_units strategy_native_sized_units_v1 -- a PF_NATIVE_INTENT_SIZED + * pf_native_request_v1 is the Sized basis; its sizing block is + * read, nothing is submitted + * + * Three asymmetries this list does not reach, recorded here because a C host + * will look for them. + * + * 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 + * 0.0 -- while a trail now has three numbers: offset, arm price and + * pf_native_request_v1::trail_best_seed. The readback keeps the first two and + * the presence flag; it does NOT carry the seed. The seed is a SUBMISSION + * input -- a floor on where the ride starts, consumed once at the arm -- and + * what a host wants back afterwards is the ride itself, which + * strategy_native_trail_state_v1 already answers as best_price and + * current_level. Another readout tail would cost every caller that sends the + * current sizeof a recompile, for a number the caller wrote. + * Executed by the trail-seed and arm-presence scenarios of + * tests/test_native_c_api.c. + * + * pf_trade_t carries no exit ticket. That POD is the codegen + * ABI's, runtime-allocated and iterated with the caller's own sizeof, so a + * tail costs a PF_ABI_VERSION bump for every existing consumer — and it needs + * none. strategy_closed_trade_entry_id / _exit_id / _exit_comment / + * _close_cause (pineforge.h) index exactly the rows of pf_report_t::trades, + * are implemented in the kernel archive, and take any handle, so a C host + * reads back the ticket its own margin model declared + * (pf_native_run_spec_ext_v1::margin_liquidation_label) without a new symbol. + * Executed by the fx-roll scenario of tests/test_native_c_api.c. + * + * pf_native_decision_v1 carries three of NativeDecisionContext's four + * session-day facts (in_session, opens_session_day, closes_session_day, in + * the base layout's former tail padding, so a table of every published length + * is handed them) and not the fourth, closes_session_day_open_ended. That one + * differs from closes_session_day on a batch's final bar alone, for a host + * that recomputes a batch whose last input is still forming; a C host's live + * edge is the strategy_stream_* ingress, whose bars already read the calendar + * there, and that padding holds exactly the three facts and their presence + * byte. Executed by the session-day scenario of tests/test_native_c_api.c. + * + * BASE-CLASS SEAMS + * ──────────────── + * The rows at the top of this block census NativeStrategyHost's own surface, + * so they cannot see a member of its base. The BacktestEngine members a host + * is documented to call or override from its callbacks are opted in one by + * one: engine.hpp marks each with a `@host-seam` line, and + * scripts/check_native_c_api_surface.py proves this list is exactly the + * marked set, with a C spelling or a reason for each, exactly as it does for + * those rows. A new protected member a host is meant to reach takes the + * marker and a row here. + * + * [C] declare_opened_lot_entry_bar_mask strategy_native_declare_opened_lot_entry_bar_mask_v1 -- legal + * inside on_applied alone; executed by the entry-bar mask + * scenario of tests/test_native_c_api.c + * [C] hash_host_extension pf_native_callbacks_v1::on_hash_extension -- the host folds a + * 64-bit digest of its own state after the kernel's bytes + * [--] hash_source_extension the deprecated spelling of hash_host_extension, kept for C++ + * subclasses written against it; a C host has only the current + * spelling, on_hash_extension + * + * HARDENING RULES + * ─────────────── + * - Every struct is tagged and size-prefixed: `struct_size` is the exact + * sizeof of the version the caller compiled against, `version` is that + * layout's version constant. A mismatch is refused with PF_NATIVE_E_STRUCT + * and mutates nothing. The exception is a deliberately additive tail: a + * struct that grew one publishes every earlier layout's length as a + * `*_SIZE` constant and the runtime accepts each — pf_native_request_v1, + * pf_native_run_spec_ext_v1 and pf_native_callbacks_v1 as inputs, + * pf_native_working_v1 as a readout. A readout is written only as far as + * the length its caller sent. A struct the runtime PRESENTS to a callback + * (pf_native_decision_v1) is presented at the layout the caller's + * callback table was published with, and its `struct_size` says so. + * - Every enum-valued field is translated by an exhaustive switch. A value + * outside its enumeration is refused with PF_NATIVE_E_TAG; a value this + * version deliberately cannot represent is refused with + * PF_NATIVE_E_UNSUPPORTED. No C value is ever cast onto a C++ variant. + * - C callbacks must not unwind. A callback that returns non-zero latches + * NativeFailureCode::CallbackException (PF_NATIVE_FAILURE_CALLBACK) and the + * run ends Failed; the code is readable with strategy_native_state_v1. + * - Commands follow the kernel's existing legality rule: inside a callback, + * or between realtime inputs. A command issued anywhere else returns + * PF_NATIVE_E_STATE and changes nothing. + * + * STABILITY + * ───────── + * Same guarantee as : within a major version the + * layouts below are append-only and the signatures never change. A field is + * appended one of two ways. A deliberately additive tail (HARDENING RULES + * above) keeps the version constant: the struct publishes every earlier + * layout's length as a `*_SIZE` constant and the runtime accepts each, so a + * caller compiled against an earlier layout keeps working unchanged, and a + * readout is written only as far as the length that caller sent. Any other + * layout change is a new revision that raises the version constant; there + * the size prefix keeps an old caller refused rather than silently misread. + */ + +#ifndef PINEFORGE_NATIVE_C_API_H +#define PINEFORGE_NATIVE_C_API_H + +#include +#include + +/* pf_strategy_t, pf_bar_t, pf_report_t, PF_API. pineforge.h includes this + * header back at its end; both guards make either include order work. */ +#include + +/** Feature probe for the C-level native host API. + * When defined, #strategy_native_host_create_v1 is available. */ +#define PINEFORGE_HAS_NATIVE_C_API_V1 1 + +/** Monotonic version of this header's native-C layouts. Every versioned + * struct below carries it in its `version` field. */ +#define PF_NATIVE_API_VERSION 1 + +#ifdef __cplusplus +extern "C" { +#endif + +/** @defgroup pf_native_c_status Status codes + * @brief Every `int`-returning symbol here answers 0 or one of these. + * + * Negative values are errors and never mutate run state. Non-negative + * values are outcomes: 0 is success everywhere, and + * #strategy_native_execute_current_v1 additionally answers the positive + * #pf_native_execute_outcome_t codes. + * @{ */ +#define PF_NATIVE_OK 0 /**< Success. */ +#define PF_NATIVE_E_HANDLE -1 /**< NULL handle, or not a C-callback native host. */ +#define PF_NATIVE_E_STRUCT -2 /**< `struct_size` / `version` mismatch. */ +#define PF_NATIVE_E_TAG -3 /**< An enum field outside its enumeration. */ +#define PF_NATIVE_E_ARGUMENT -4 /**< NULL output, negative count, out-of-range index. */ +#define PF_NATIVE_E_STATE -5 /**< Illegal here: the command legality rule, or the + * kernel's own lifecycle, refused it. */ +#define PF_NATIVE_E_REJECTED -6 /**< The kernel rejected the request (reason written out). */ +#define PF_NATIVE_E_UNSUPPORTED -7 /**< A tag this API version cannot represent. */ +#define PF_NATIVE_E_EXCEPTION -8 /**< A C++ exception was contained at the boundary. */ +#define PF_NATIVE_E_NOT_WORKING -9 /**< The target handle is no longer a live request. */ +#define PF_NATIVE_E_INVALID_TARGET -10 /**< The target handle was never issued by this run. */ +#define PF_NATIVE_E_RUN_FAILED -11 /**< The run did not reach Completed; read the state. */ +#define PF_NATIVE_E_REFUSED -12 /**< execute_current refused; see pf_native_refusal_e. */ +/** Non-negative outcome: the kernel HAS no answer here and the output was + * left at its documented empty. It is not an error — the C++ spelling of + * each accessor that returns it is a `std::optional`, whose empty is a + * legitimate answer (no path walk, an unarmed trail, a series that has not + * delivered, a run with no margin model). Only the accessors whose own + * documentation names it can return it; #strategy_native_execute_current_v1 + * never does, its positive codes being #pf_native_execute_outcome_t. */ +#define PF_NATIVE_ABSENT 1 +/** @} */ + +/** `NativeFailureCode::CallbackException` — the code latched when a C callback + * returns non-zero. The historical spelling of + * #PF_NATIVE_FAILURE_CALLBACK_EXCEPTION, kept for the callers written + * against it; pinned by a static_assert. */ +#define PF_NATIVE_FAILURE_CALLBACK 5 + +/** @defgroup pf_native_c_enums Translated enumerations + * @brief Every value below is the exact integer of the C++ alternative it + * names, pinned by static_asserts in src/native_c_host.cpp. + * @{ */ + +/** Order intent — the alternative index of `native_order::OrderIntent`. */ +typedef enum pf_native_intent_e { + PF_NATIVE_INTENT_FLATTEN = 0, /**< Close the whole book. */ + PF_NATIVE_INTENT_REDUCE = 1, /**< Reduce; see #pf_native_reduction_t. */ + PF_NATIVE_INTENT_TRANSACT = 2, /**< `intent_value` signed units. */ + PF_NATIVE_INTENT_REVERSE_TO = 3, /**< `intent_value` target signed exposure. */ + PF_NATIVE_INTENT_HOST_SIZED = 4, /**< A close sized by on_close_units: the + * cohort close (#PF_NATIVE_OWNER_BIND_COHORT) + * or a WAIT_FOR_APPLIED child that carries + * the arm tail (#pf_native_arm_scope_t; + * the kernel admits it under BOOK only). + * Refused PF_NATIVE_E_UNSUPPORTED under + * any other owner. */ + PF_NATIVE_INTENT_SIZED = 5 /**< Kernel-sized opening (L3). */ +} pf_native_intent_t; + +/** Reduction size — the alternative index of `native_order::ReductionSize`. */ +typedef enum pf_native_reduction_e { + PF_NATIVE_REDUCE_EXPLICIT_UNITS = 0, /**< `intent_value` units. */ + PF_NATIVE_REDUCE_OWNER_OPENED = 1, /**< Exactly what the owner opened. */ + PF_NATIVE_REDUCE_SCOPE_FRACTION = 2 /**< `intent_value` fraction in (0, 1]. */ +} pf_native_reduction_t; + +/** Scope claim of a fractional reduce. */ +typedef enum pf_native_scope_claim_e { + PF_NATIVE_SCOPE_GROSS = 0, + PF_NATIVE_SCOPE_NET_OF_SIBLINGS = 1 +} pf_native_scope_claim_t; + +/** Side of a kernel-sized opening. */ +typedef enum pf_native_side_e { + PF_NATIVE_SIDE_LONG = 0, + PF_NATIVE_SIDE_SHORT = 1 +} pf_native_side_t; + +/** Sizing basis — the alternative index of `native_order::SizeBasis`. */ +typedef enum pf_native_size_basis_e { + PF_NATIVE_SIZE_BASIS_CASH = 0, /**< `intent_value` account-currency cash. */ + PF_NATIVE_SIZE_BASIS_EQUITY_FRACTION = 1 /**< `intent_value` fraction of marked equity. */ +} pf_native_size_basis_t; + +/** When a sizing basis resolves. */ +typedef enum pf_native_size_time_e { + PF_NATIVE_SIZE_AT_MATCH = 0, + PF_NATIVE_SIZE_AT_ACCEPTANCE = 1 +} pf_native_size_time_t; + +/** Whether resolved sizing snaps onto the run's quantity grid. */ +typedef enum pf_native_grid_policy_e { + PF_NATIVE_GRID_SNAP = 0, + PF_NATIVE_GRID_EXPLICIT_UNITS = 1 +} pf_native_grid_policy_t; + +/** Trigger — the alternative index of `native_order::Trigger`. */ +typedef enum pf_native_trigger_e { + PF_NATIVE_TRIGGER_MARKET = 0, /**< p1, p2 ignored. */ + PF_NATIVE_TRIGGER_LIMIT = 1, /**< p1 = price; `fill_through` makes it market-if-touched. */ + PF_NATIVE_TRIGGER_STOP = 2, /**< p1 = price. */ + PF_NATIVE_TRIGGER_STOP_LIMIT = 3, /**< p1 = stop, p2 = limit. */ + PF_NATIVE_TRIGGER_TRAIL = 4 /**< p1 = offset, p2 = arm price when `trail_has_arm_price`; + * `trail_best_seed` when `trail_has_best_seed`. */ +} pf_native_trigger_t; + +/** Where a trigger level comes from — `native_order::TriggerAnchor` (L7). */ +typedef enum pf_native_anchor_e { + PF_NATIVE_ANCHOR_ABSOLUTE = 0, /**< The level written in the trigger. */ + PF_NATIVE_ANCHOR_FROM_OWNER_FILL = 1 /**< owner fill + `anchor_offset` (signed). */ +} pf_native_anchor_t; + +/** How a materialized anchored level snaps onto the run's price tick ladder — + * `native_order::NativeAnchorRounding` (L7b). RAW is the established + * behaviour; the other two need `price_tick > 0` at acceptance. */ +typedef enum pf_native_anchor_rounding_e { + PF_NATIVE_ANCHOR_ROUNDING_RAW = 0, /**< fill + offset exactly. */ + PF_NATIVE_ANCHOR_ROUNDING_HALF_UP = 1, /**< Nearest tick, ties away from zero. */ + PF_NATIVE_ANCHOR_ROUNDING_DIRECTIONAL = 2 /**< Toward the region the leg needs. */ +} pf_native_anchor_rounding_t; + +/** Whether a WAIT_FOR_APPLIED child is a working order before its arm — + * `native_order::NativeArmVisibility` (L7b). PENDING_UNTIL_ARMED keeps it + * out of #strategy_native_working_len_v1 / #strategy_native_working_get_v1 + * until its ArmedEvent; it stays a live request the whole time (replace, + * cancel, cancel_all still address it, and it never matches before the arm + * under either value). */ +typedef enum pf_native_arm_visibility_e { + PF_NATIVE_ARM_VISIBILITY_WORKING = 0, + PF_NATIVE_ARM_VISIBILITY_PENDING_UNTIL_ARMED = 1 +} pf_native_arm_visibility_t; + +/** Whether an armed WAIT_FOR_APPLIED child may trade on its owner's own fill + * print — `native_order::NativeArmFirstMatch`. AT_ARM_PRINT (the default) is + * the established book: the armed child is a candidate at that very cursor, + * so a level the print already satisfies matches there. AFTER_ARM_PRINT + * gives it the birth rule of a request submitted from the owner's own + * `on_applied`: on the point that armed it, it sees only the path after the + * print. It governs a priced trigger's level test; a market trigger has no + * level. Both are broker models. */ +typedef enum pf_native_arm_first_match_e { + PF_NATIVE_ARM_FIRST_MATCH_AT_ARM_PRINT = 0, + PF_NATIVE_ARM_FIRST_MATCH_AFTER_ARM_PRINT = 1 +} pf_native_arm_first_match_t; + +/** What an armed CLOSING child (REDUCE, FLATTEN, a host-sized close) closes — + * `native_order::NativeArmScope`. OWNER_LOT (the default) is the lot its + * owner's fill opened, and nothing a later add brings. BOOK binds it, at the + * arm, to the whole position that fill left, so a protective leg placed with + * its entry covers later adds. A host-sized close may wait for its owner only + * under BOOK (its units are #pf_native_callbacks_v1::on_close_units'); a + * waiting transaction closes nothing and must keep OWNER_LOT. */ +typedef enum pf_native_arm_scope_e { + PF_NATIVE_ARM_SCOPE_OWNER_LOT = 0, + PF_NATIVE_ARM_SCOPE_BOOK = 1 +} pf_native_arm_scope_t; + +/** Capacity — the alternative index of `native_order::Capacity`. */ +typedef enum pf_native_capacity_e { + PF_NATIVE_CAPACITY_IMMEDIATE = 0, /**< Whole remaining at one point. */ + PF_NATIVE_CAPACITY_POINT_BUDGET = 1 /**< `capacity_units` per matching point. */ +} pf_native_capacity_t; + +/** Owner — the alternative index of `native_order::Owner`. */ +typedef enum pf_native_owner_e { + PF_NATIVE_OWNER_INDEPENDENT = 0, /**< No owner. */ + PF_NATIVE_OWNER_WAIT_FOR_APPLIED = 1, /**< One parent in `owner_incarnations`. */ + PF_NATIVE_OWNER_BIND_OPENING = 2, /**< One opening + `owner_cycle`. */ + PF_NATIVE_OWNER_BIND_OPENINGS = 3, /**< `owner_n` openings + `owner_cycle`. */ + /** `cohort` from #strategy_native_cohort_open_v1. The kernel pairs this + * owner with exactly one intent — a host-sized CLOSE — so a cohort close + * is spelled #PF_NATIVE_INTENT_HOST_SIZED with this owner and no + * quantity of its own: the roster's live openings are the target and the + * cohort authority decides the units. That pairing is the only shape in + * which HOST_SIZED is accepted here. */ + PF_NATIVE_OWNER_BIND_COHORT = 4 +} pf_native_owner_t; + +/** Group — the alternative index of `native_order::Group`. */ +typedef enum pf_native_group_e { + PF_NATIVE_GROUP_NONE = 0, + PF_NATIVE_GROUP_MEMBER = 1 /**< `group_id`, `group_cohort`, `group_effect`. */ +} pf_native_group_t; + +/** What a filled group member does to its siblings. */ +typedef enum pf_native_group_effect_e { + PF_NATIVE_GROUP_CANCEL = 0, + PF_NATIVE_GROUP_REDUCE = 1 +} pf_native_group_effect_t; + +/** Which identity text #strategy_native_cancel_where_v1 compares. + * + * Both are the free text the request carried: `comment` is the established + * selector, `label` is where a host that names its orders puts its own id, + * so LABEL is the one call that withdraws every live request issued under + * one such id. Neither is indexed; both walk the live book once. */ +typedef enum pf_native_request_field_e { + PF_NATIVE_FIELD_COMMENT = 0, + PF_NATIVE_FIELD_LABEL = 1 +} pf_native_request_field_t; + +/** Price rule of an immediate execution. */ +typedef enum pf_native_price_rule_e { + PF_NATIVE_PRICE_AS_PRESENTED = 0, + PF_NATIVE_PRICE_NEAREST_TICK = 1 +} pf_native_price_rule_t; + +/** Outcome of #strategy_native_execute_current_v1 (non-negative returns). */ +typedef enum pf_native_execute_outcome_e { + PF_NATIVE_EXECUTED_APPLIED = 0, /**< An execution was applied. */ + PF_NATIVE_EXECUTED_NO_EFFECT = 1, /**< Legal, nothing to execute. */ + PF_NATIVE_EXECUTED_MATCH_REJECTED = 2, /**< Terms/admission rejected it. */ + PF_NATIVE_EXECUTED_CANCELLED = 3 /**< The request was cancelled instead. */ +} pf_native_execute_outcome_t; + +/** Why execute_current refused. Written to `*refusal`, if supplied, when + * #strategy_native_execute_current_v1 answers PF_NATIVE_E_REFUSED. Mirrors + * `NativeCurrentRefusal`. */ +typedef enum pf_native_refusal_e { + PF_NATIVE_REFUSAL_NO_EXECUTION_CONTEXT = 0, + PF_NATIVE_REFUSAL_REENTRANT = 1, + PF_NATIVE_REFUSAL_INVALID_HANDLE = 2, + PF_NATIVE_REFUSAL_NOT_WORKING = 3, + PF_NATIVE_REFUSAL_NOT_ACCEPTED_IN_CALLBACK = 4, + PF_NATIVE_REFUSAL_UNSUPPORTED_REQUEST = 5, + PF_NATIVE_REFUSAL_UNREADY_OWNER = 6, + PF_NATIVE_REFUSAL_INVALID_SELECTION = 7, + PF_NATIVE_REFUSAL_CONFIGURATION_MISMATCH = 8 +} pf_native_refusal_t; + +/** Lifecycle of the native run — mirrors `NativeLifecycleKind`. */ +typedef enum pf_native_lifecycle_e { + PF_NATIVE_LIFECYCLE_UNCONFIGURED = 0, + PF_NATIVE_LIFECYCLE_READY = 1, + PF_NATIVE_LIFECYCLE_RUNNING = 2, + PF_NATIVE_LIFECYCLE_COMPLETED = 3, + PF_NATIVE_LIFECYCLE_FAILED = 4 +} pf_native_lifecycle_t; + +/** Event tag of #pf_native_event_v1. + * + * 1..18 are the first eighteen alternatives of `native_order::CommandEvent`, + * in variant order plus one; 19 and 20 are the driver-point and account + * observations `native_events()` also carries. 21 is L9's `NativeRiskEvent`, + * the nineteenth CommandEvent alternative: it was added after this header + * froze, so it keeps a tag of its own past the two observations rather than + * taking 19 and renumbering them. A reader compiled before it skips it by + * tag, exactly as it must skip any tag it does not know. + * + * One kind named in the design is NOT represented and never appears here: a + * completed higher-timeframe bucket, delivered only through + * `on_timeframe_bar` and never recorded in the event history. */ +typedef enum pf_native_event_kind_e { + PF_NATIVE_EVENT_ACCEPTED = 1, + PF_NATIVE_EVENT_REJECTED = 2, + PF_NATIVE_EVENT_REPLACED = 3, + PF_NATIVE_EVENT_REPLACE_REJECTED = 4, + PF_NATIVE_EVENT_CANCELLED = 5, + PF_NATIVE_EVENT_NOT_WORKING = 6, + PF_NATIVE_EVENT_INVALID_HANDLE = 7, + PF_NATIVE_EVENT_NO_EFFECT = 8, + PF_NATIVE_EVENT_MATCH_REJECTED = 9, + PF_NATIVE_EVENT_APPLIED = 10, + PF_NATIVE_EVENT_CLOSE_BOUND = 11, + PF_NATIVE_EVENT_ACTIVATED = 12, + PF_NATIVE_EVENT_RESERVATION_REDUCED = 13, + PF_NATIVE_EVENT_DEFERRED_GROUP = 14, + PF_NATIVE_EVENT_QUANTITY_BOUND = 15, + PF_NATIVE_EVENT_ARMED = 16, + PF_NATIVE_EVENT_TERMS_RESOLVED = 17, + PF_NATIVE_EVENT_MARGIN_CALL = 18, + PF_NATIVE_EVENT_DRIVER_POINT = 19, + PF_NATIVE_EVENT_ACCOUNT = 20, + PF_NATIVE_EVENT_RISK = 21 +} pf_native_event_kind_t; + +/** Which generic risk limit a #PF_NATIVE_EVENT_RISK event reports — the + * `reason` field of #pf_native_event_v1, mirroring + * `native_order::RiskLimitKind` (L9). */ +typedef enum pf_native_risk_limit_e { + PF_NATIVE_RISK_MAX_DRAWDOWN = 0, + PF_NATIVE_RISK_MAX_INTRADAY_LOSS = 1, + PF_NATIVE_RISK_MAX_CONSECUTIVE_LOSS_DAYS = 2, + PF_NATIVE_RISK_MAX_FILLS_PER_DAY = 3 +} pf_native_risk_limit_t; + +/** Which day a risk limit's "day" is — `NativeRiskDay`. SESSION is the run's + * own session calendar (an overnight session is one day); CALENDAR_TIMEZONE + * is the civil date in the spec's scheduling timezone. */ +typedef enum pf_native_risk_day_e { + PF_NATIVE_RISK_DAY_SESSION = 0, + PF_NATIVE_RISK_DAY_CALENDAR_TIMEZONE = 1 +} pf_native_risk_day_t; + +/** What a breach does — `NativeRiskAction`. BLOCK_OPENINGS refuses every + * opening while the block lasts and leaves the live book alone; + * FLATTEN_AND_BLOCK first closes the book with one kernel-originated + * Flatten. */ +typedef enum pf_native_risk_action_e { + PF_NATIVE_RISK_BLOCK_OPENINGS = 0, + PF_NATIVE_RISK_FLATTEN_AND_BLOCK = 1 +} pf_native_risk_action_t; + +/** Remaining projection of a live request — `native_order::RemainingProjection`. */ +typedef enum pf_native_remaining_e { + PF_NATIVE_REMAINING_UNBOUND = 0, + PF_NATIVE_REMAINING_FLATTEN_ALL = 1, + PF_NATIVE_REMAINING_UNITS = 2, /**< `remaining_units` is meaningful. */ + PF_NATIVE_REMAINING_DEFERRED = 3, + PF_NATIVE_REMAINING_NO_TARGET = 4 +} pf_native_remaining_t; + +/** Trigger state of a live request — `native_order::TriggerState`. */ +typedef enum pf_native_trigger_state_e { + PF_NATIVE_TRIGGER_STATE_MARKET_READY = 0, + PF_NATIVE_TRIGGER_STATE_LIMIT_READY = 1, + PF_NATIVE_TRIGGER_STATE_STOP_IDLE = 2, + PF_NATIVE_TRIGGER_STATE_STOP_ACTIVE = 3, + PF_NATIVE_TRIGGER_STATE_STOP_LIMIT_PENDING = 4, + PF_NATIVE_TRIGGER_STATE_STOP_LIMIT_LIVE = 5, + PF_NATIVE_TRIGGER_STATE_TRAIL_WAIT_ARM = 6, + PF_NATIVE_TRIGGER_STATE_TRAIL_TRACK = 7, + PF_NATIVE_TRIGGER_STATE_TRAIL_ACTIVE = 8 +} pf_native_trigger_state_t; + +/** What one execution costs — `NativeFeeKind`, the `fee_kind` word of + * #pf_native_run_spec_v1, charged at its `fee_value`. */ +typedef enum pf_native_fee_kind_e { + PF_NATIVE_FEE_PERCENT = 0, /**< `fee_value` percent of |units| × price × + * point value × account FX (the default). */ + PF_NATIVE_FEE_CASH_PER_UNIT = 1, /**< `fee_value` account currency per unit. */ + PF_NATIVE_FEE_CASH_PER_EXECUTION = 2 /**< `fee_value` account currency per execution. */ +} pf_native_fee_kind_t; + +/** When a request born at a script calculation may first match — + * `NativeCloseExecution`, the `close_execution` word of + * #pf_native_run_spec_v1. */ +typedef enum pf_native_close_execution_e { + PF_NATIVE_CLOSE_EXECUTION_NEXT_ELIGIBLE_POINT = 0, /**< A later eligible point — the + * next modeled opening, an + * observed print or a carried + * open — never the bar's + * presented prices (the + * default). */ + PF_NATIVE_CLOSE_EXECUTION_AFTER_CALCULATION = 1 /**< Also a modeled close point + * right after that + * calculation. */ +} pf_native_close_execution_t; + +/** Which opening directions the run admits — `NativeOpenDirections`, the + * `allowed_open_directions` word of #pf_native_run_spec_v1; BOTH is + * LONG | SHORT. A refused opening is a match rejection of the whole + * transaction, and a pure reduction stays legal under NONE. Zero is NOT the + * kernel's default here, unlike every other word these enumerations type: a + * zero-filled spec admits no opening at all. */ +typedef enum pf_native_open_directions_e { + PF_NATIVE_OPEN_DIRECTIONS_NONE = 0, + PF_NATIVE_OPEN_DIRECTIONS_LONG = 1, + PF_NATIVE_OPEN_DIRECTIONS_SHORT = 2, + PF_NATIVE_OPEN_DIRECTIONS_BOTH = 3 /**< The kernel's default. */ +} pf_native_open_directions_t; + +/** Who records the per-script-bar report series — `NativeReportPolicy`, the + * `report_policy` word of #pf_native_run_spec_ext_v1. The kernel's third + * policy, `KernelRecordedAtHostMarks`, has no C value: under it the host + * names each report point itself, from inside its own callbacks, and the + * callback table carries no call that marks one — a C host could only + * declare a series nobody records. Its integer, 2, is PF_NATIVE_E_TAG like + * any other word outside this enumeration. */ +typedef enum pf_native_report_policy_e { + PF_NATIVE_REPORT_HOST_RECORDED = 0, /**< The host's; the kernel appends no point + * (the default). */ + PF_NATIVE_REPORT_KERNEL_RECORDED = 1 /**< One equity point per script calculation; + * `report_open_position_at_end` applies. */ +} pf_native_report_policy_t; + +/** A generic instrument price grid — `NativePriceGrid`, the `price_grid` + * word of #pf_native_run_spec_ext_v1. Both quantizing values need + * `price_tick > 0`. */ +typedef enum pf_native_price_grid_e { + PF_NATIVE_PRICE_GRID_NONE = 0, /**< Every price booked as the + * path presents it (the + * default). */ + PF_NATIVE_PRICE_GRID_QUANTIZE_FILLS = 1, /**< Each fill booked on the + * tick ladder. */ + PF_NATIVE_PRICE_GRID_QUANTIZE_FILLS_AND_TRIGGERS = 2 /**< And each resting trigger + * tested against the + * quantized path. */ +} pf_native_price_grid_t; + +/** How a quantizing grid rounds — `NativeGridRounding`, the `grid_rounding` + * word of #pf_native_run_spec_ext_v1, read beside `price_grid`. */ +typedef enum pf_native_grid_rounding_e { + PF_NATIVE_GRID_ROUNDING_HALF_UP = 0, /**< The nearest tick, ties away from zero + * (the default). */ + PF_NATIVE_GRID_ROUNDING_DIRECTIONAL = 1 /**< Toward the region the order needs: a + * buy limit down and a sell limit up, a + * stop the other way, a market fill to + * its adverse side. */ +} pf_native_grid_rounding_t; + +/** When the kernel asks the host to calculate — `NativeCalculationTrigger`, + * the `calculation` word of #pf_native_run_spec_ext_v1. Each value is a + * strict superset of the one before it. */ +typedef enum pf_native_calc_trigger_e { + PF_NATIVE_CALC_TRIGGER_BAR_CLOSE = 0, /**< Once per script bar, at its close + * (the default). */ + PF_NATIVE_CALC_TRIGGER_BAR_CLOSE_AND_FILLS = 1, /**< And at each applied execution's + * cursor, bounded by + * `max_recalculations_per_point`. */ + PF_NATIVE_CALC_TRIGGER_EVERY_MODELED_POINT = 2 /**< And at every modeled point and + * every observed print. */ +} pf_native_calc_trigger_t; + +/** What #pf_native_callbacks_v1::on_bar_open is handed — `NativeOpenBarView`, + * the `open_bar_view` word of #pf_native_run_spec_ext_v1. Neither value + * changes a match, a fill or any other callback. */ +typedef enum pf_native_open_bar_view_e { + PF_NATIVE_OPEN_BAR_VIEW_COMPLETE = 0, /**< The whole script bar (the default). */ + PF_NATIVE_OPEN_BAR_VIEW_OPEN_ONLY = 1 /**< Its lookahead masked: H = L = C = open, + * volume 0. */ +} pf_native_open_bar_view_t; + +/** Which units a kernel-issued liquidation reduces — + * `NativeLiquidationSizing`, the `margin_sizing` word of + * #pf_native_run_spec_ext_v1. Clamped to the position held; + * #pf_native_callbacks_v1::on_margin_call_units has the last word. */ +typedef enum pf_native_liquidation_sizing_e { + PF_NATIVE_LIQUIDATION_SIZING_RESTORE_MINIMUM = 0, /**< The fewest units that restore + * the requirement at the sizing + * mark (the default). */ + PF_NATIVE_LIQUIDATION_SIZING_SHORTFALL_MULTIPLE = 1, /**< That restore times + * `margin_shortfall_multiple`. */ + PF_NATIVE_LIQUIDATION_SIZING_FLATTEN = 2 /**< The whole position. */ +} pf_native_liquidation_sizing_t; + +/** Which extension blocks of #pf_native_run_spec_ext_v1 are meaningful. */ +typedef enum pf_native_spec_ext_mask_e { + PF_NATIVE_SPEC_EXT_REPORT = 1u << 0, + PF_NATIVE_SPEC_EXT_PRICE_GRID = 1u << 1, + PF_NATIVE_SPEC_EXT_CALCULATION = 1u << 2, + PF_NATIVE_SPEC_EXT_OPEN_BAR_VIEW = 1u << 3, + PF_NATIVE_SPEC_EXT_MARGIN = 1u << 4, + PF_NATIVE_SPEC_EXT_SUBSCRIPTIONS = 1u << 5, + /** L9's generic risk limits. Only a caller whose + * pf_native_run_spec_ext_v1 carries the risk tail may set this bit; a + * caller sending the base layout is refused with PF_NATIVE_E_STRUCT. */ + PF_NATIVE_SPEC_EXT_RISK = 1u << 6, + /** The retained intrabar execution path. Needs the N8 tail. */ + PF_NATIVE_SPEC_EXT_INTRABAR = 1u << 7, + /** The four feed-shape and presentation policies: slot labels, feed + * tolerance, the forced path order and abort reporting. Needs the N8 + * tail. */ + PF_NATIVE_SPEC_EXT_FEED_POLICY = 1u << 8, + /** The auxiliary finer feed. Only a caller whose + * pf_native_run_spec_ext_v1 carries the auxiliary tail may set this bit; + * a caller sending any earlier layout is refused with + * PF_NATIVE_E_STRUCT. */ + PF_NATIVE_SPEC_EXT_AUXILIARY_FEED = 1u << 9, + /** What the run keeps of its event record (`event_retention`). Only a + * caller whose pf_native_run_spec_ext_v1 carries the retention tail may + * set this bit; a caller sending any earlier layout is refused with + * PF_NATIVE_E_STRUCT. Without it the run keeps + * #PF_NATIVE_EVENT_RETENTION_FULL. */ + PF_NATIVE_SPEC_EXT_EVENT_RETENTION = 1u << 10 +} pf_native_spec_ext_mask_t; + +/** What a run keeps of its event record — `NativeRunSpec::event_retention`, + * the `event_retention` word of #pf_native_run_spec_ext_v1, read under + * #PF_NATIVE_SPEC_EXT_EVENT_RETENTION. + * + * WINDOW keeps the command journal only until the host has read it: a host + * that polls #strategy_native_events_v1 acknowledges what it has read + * (#strategy_native_acknowledge_events_v1) and the kernel drops the + * acknowledged events at the next script-bar boundary, while a host that + * never acknowledges is served by its callbacks alone and its window closes + * at every script-bar end. No driver point and no account row is kept. + * COMMANDS keeps the whole command journal and every account row, and no + * driver point. FULL keeps everything, O(run) in memory. + * + * A C host that does not set the bit -- every caller of + * #strategy_configure_native_v1, and every caller of an earlier + * pf_native_run_spec_ext_v1 layout -- runs under FULL, the record its + * layout was published with, so reading a whole run's events after it has + * ended keeps working unchanged. The C++ default is WINDOW. */ +typedef enum pf_native_event_retention_e { + PF_NATIVE_EVENT_RETENTION_WINDOW = 0, /**< The unread journal only. */ + PF_NATIVE_EVENT_RETENTION_FULL = 1, /**< Journal, driver points, account rows. */ + PF_NATIVE_EVENT_RETENTION_COMMANDS = 2 /**< Journal and account rows. */ +} pf_native_event_retention_t; + +/** The bars a declared series is built from — `NativeSeriesSource`. */ +typedef enum pf_native_series_source_e { + PF_NATIVE_SERIES_SOURCE_INPUT = 0, /**< The accepted input (the default). */ + PF_NATIVE_SERIES_SOURCE_AUXILIARY_FEED = 1 /**< The run's auxiliary finer feed. */ +} pf_native_series_source_t; + +/** When a declared series delivers a completed bucket — + * `NativeTimeframeSubscription::lookahead`, the `lookahead` word of + * #pf_native_subscription_v1. AT_FIRST_INPUT resolves the historical input + * ahead; a stream's live inputs have nothing ahead to resolve, so under + * either value they deliver each bucket at its completion. */ +typedef enum pf_native_lookahead_e { + PF_NATIVE_LOOKAHEAD_AT_COMPLETION = 0, /**< On the input that completes the + * bucket (the default). */ + PF_NATIVE_LOOKAHEAD_AT_FIRST_INPUT = 1 /**< The bucket's final values, on its + * first contributing input. */ +} pf_native_lookahead_t; + +/** What a declared series holds on an accepted input that delivers no bucket + * of it — `NativeTimeframeSubscription::gaps`, the `gaps` word of + * #pf_native_subscription_v1. Neither value changes which buckets complete, + * when they are delivered or what they contain. */ +typedef enum pf_native_gaps_e { + PF_NATIVE_GAPS_HOLD = 0, /**< The last delivered bucket, until the next + * delivery replaces it (the default). */ + PF_NATIVE_GAPS_CLEAR = 1 /**< Nothing: #strategy_native_series_bar_v1 + * answers #PF_NATIVE_ABSENT on that input. */ +} pf_native_gaps_t; + +/** WHICH price a kernel-sized basis converts at — `native_order::SizePrice` + * (L3b). RESOLVED is the established behaviour: the price the kernel would + * otherwise settle at. SIGNAL is the decision-point price at placement, + * carried to the expected fill by the side's slippage and rounded onto the + * run's fill grid. SIGNAL_ON_TICK is that same rule measured on the + * INSTRUMENT's own tick ladder (`price_tick`) instead of the fill grid, + * rounded before the slippage as well as after. */ +typedef enum pf_native_size_price_e { + PF_NATIVE_SIZE_PRICE_RESOLVED = 0, + PF_NATIVE_SIZE_PRICE_SIGNAL = 1, + PF_NATIVE_SIZE_PRICE_SIGNAL_ON_TICK = 2 +} pf_native_size_price_t; + +/** WHICH measurement of the bound scope a fractional reduce takes its + * fraction of — `native_order::ScopeBasis`. AT_MATCH reads the scope as it + * stands at the matching candidate. AT_ACCEPTANCE freezes the scope SIZE + * when the request is accepted, so two 50 % siblings on one 10-unit lot both + * claim 5 under GROSS even after the first has executed. */ +typedef enum pf_native_scope_basis_e { + PF_NATIVE_SCOPE_BASIS_AT_MATCH = 0, + PF_NATIVE_SCOPE_BASIS_AT_ACCEPTANCE = 1 +} pf_native_scope_basis_t; + +/** When the kernel tests the maintenance requirement — + * `NativeLiquidationCheck`, the `margin_check` field of + * #pf_native_run_spec_ext_v1. */ +typedef enum pf_native_liquidation_check_e { + PF_NATIVE_LIQUIDATION_PATH_ADVERSE_EXTREME = 0, /**< Solve and rest at the level. */ + PF_NATIVE_LIQUIDATION_CALCULATION_ONLY = 1, /**< Test the mark; rest nothing. */ + PF_NATIVE_LIQUIDATION_PATH_ADVERSE_EXTREME_MARK = 2 /**< Rest AT the adverse mark. */ +} pf_native_liquidation_check_t; + +/** Which equity the maintenance requirement is tested against — + * `NativeMarginEquityBasis`. */ +typedef enum pf_native_margin_equity_basis_e { + PF_NATIVE_MARGIN_EQUITY_MARKED = 0, + PF_NATIVE_MARGIN_EQUITY_BEFORE_OPEN_COMMISSION = 1 +} pf_native_margin_equity_basis_t; + +/** Which base the liquidation level is solved from — + * `NativeLiquidationLevelBase`. */ +typedef enum pf_native_margin_level_base_e { + PF_NATIVE_MARGIN_LEVEL_MARKED_EQUITY = 0, + PF_NATIVE_MARGIN_LEVEL_REALIZED_ONLY = 1 +} pf_native_margin_level_base_t; + +/** Which intrabar execution path the run retains — the alternative of + * `IntrabarPath`. NONE is the whole default surface. LOWER_TF retains the + * caller's own finer bars (and is the one mode that delivers + * #pf_native_callbacks_v1::on_sub_bar). SYNTHESIZED samples each script + * bar's own OHLC path through the generic sampler and retains no feed. */ +typedef enum pf_native_intrabar_kind_e { + PF_NATIVE_INTRABAR_NONE = 0, + PF_NATIVE_INTRABAR_LOWER_TF = 1, + PF_NATIVE_INTRABAR_SYNTHESIZED = 2 +} pf_native_intrabar_kind_t; + +/** Whether matching stays continuous between generated samples — + * `IntrabarPath::SampleEligibility`. LOWER_TF only; a synthesized path's + * point-only eligibility is inherent to that mode. */ +typedef enum pf_native_sample_eligibility_e { + PF_NATIVE_SAMPLE_CONTINUOUS_SEGMENTS = 0, + PF_NATIVE_SAMPLE_DISTRIBUTION_SAMPLES = 1 +} pf_native_sample_eligibility_t; + +/** Whether a confirmed bar must name a canonical input slot — + * `NativeSlotLabelPolicy`. A feed-shape policy, not a source-language one; + * the two modes never share a continuation. */ +typedef enum pf_native_slot_label_e { + PF_NATIVE_SLOT_LABEL_CANONICAL = 0, + PF_NATIVE_SLOT_LABEL_FEED_TOLERANT = 1 +} pf_native_slot_label_t; + +/** Opt-in admission exceptions for a tolerated input-feed shape — + * `NativeFeedTolerance`. A BIT MASK, not an enumerator: the values combine, + * and any bit outside this set is PF_NATIVE_E_TAG. */ +typedef enum pf_native_feed_tolerance_e { + PF_NATIVE_FEED_TOLERANCE_NONE = 0, + /** Finite OHLC need not be positive; NaN volume means unavailable. */ + PF_NATIVE_FEED_TOLERANCE_BATCH_STRUCTURAL = 1u << 0, + /** Stream warmups admit finite, non-negative interim OHLC. */ + PF_NATIVE_FEED_TOLERANCE_WARMUP_NONNEGATIVE = 1u << 1 +} pf_native_feed_tolerance_t; + +/** Generic ordering for a modeled OHLC path — `NativePathOrder`. AUTO keeps + * the open-proximity rule; the forced modes make the first excursion + * explicit for replay and live hosts. */ +typedef enum pf_native_path_order_e { + PF_NATIVE_PATH_ORDER_AUTO = 0, + PF_NATIVE_PATH_ORDER_HIGH_FIRST = 1, + PF_NATIVE_PATH_ORDER_LOW_FIRST = 2 +} pf_native_path_order_t; + +/** How a cooperative abort is presented — `NativeAbortReporting`. */ +typedef enum pf_native_abort_reporting_e { + PF_NATIVE_ABORT_ERROR = 0, + PF_NATIVE_ABORT_QUIET = 1 +} pf_native_abort_reporting_t; + +/** Why the kernel is asking the host to calculate — `NativeCalculationReason` + * (L5), the `reason` argument of #pf_native_callbacks_v1::on_recalculate. + * SUB_BAR is reserved and never delivered there: a lower-timeframe sub-bar + * has its own hook, #pf_native_callbacks_v1::on_sub_bar. */ +typedef enum pf_native_calc_reason_e { + PF_NATIVE_CALC_BAR_CLOSE = 0, /**< The script bar's own calculation; `cause` NULL. */ + PF_NATIVE_CALC_ORDER_FILL = 1, /**< At an applied execution's cursor; `cause` is it. */ + PF_NATIVE_CALC_TICK = 2, /**< At a modeled point or print; `cause` NULL. */ + PF_NATIVE_CALC_SUB_BAR = 3 /**< Reserved; see on_sub_bar. */ +} pf_native_calc_reason_t; + +/** Which kernel check point is about to test the maintenance requirement — + * `NativeMarginCheckKind` (L4b). These are the kernel's own points; a broker + * model that checks somewhere else is host policy, expressed by suppressing + * the points it does not share. */ +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_fx_curve_v1: the first point + * the account converts at a new rate, + * offered immediately before that point is + * matched. A run that declares no curve has + * none, and a CalculationOnly model, which + * measures at its calculation alone, is not + * offered it. */ +} pf_native_margin_check_kind_t; + +/** What an ANSWERING callback's return value means. + * + * The four answering hooks of #pf_native_callbacks_v1 do not report success: + * their return value selects WHOSE answer the kernel uses, so every value is + * in contract and an answering hook can never fail the run. A host that + * needs to abort does it from an observation callback, which keeps the + * "non-zero ends the run Failed" rule exactly where it already was. */ +typedef enum pf_native_answer_e { + PF_NATIVE_ANSWER_DEFAULT = 0, /**< Keep the kernel's own; the output is ignored. */ + PF_NATIVE_ANSWER_PROVIDED = 1 /**< Use the output. Any non-zero value means this. */ +} pf_native_answer_t; + +/** Where an opened lot's own fill sits on its entry bar's modeled path — + * `OpenedLotFillPoint`, the one fact + * #strategy_native_declare_opened_lot_entry_bar_mask_v1 carries. ON_PATH is + * a fill at a price the path reaches: the kernel derives which end of the + * bar, if either, the path had already reached before it. AFTER_PATH is a + * fill at the bar's closing point, after the whole path: both ends precede + * it. */ +typedef enum pf_native_opened_lot_fill_point_e { + PF_NATIVE_OPENED_LOT_FILL_POINT_ON_PATH = 0, + PF_NATIVE_OPENED_LOT_FILL_POINT_AFTER_PATH = 1 +} pf_native_opened_lot_fill_point_t; + +/* ── The readout words ───────────────────────────────────────────── + * Every enumeration below names a word the runtime WRITES for a C host: a + * field of a struct it fills or presents, an event's reason, a callback + * argument or an out-parameter. The fields stay the integer words they were + * published as, so no layout moves; each value is written by an exhaustive + * translation of its kernel enumeration, and a kernel value no enumeration + * here names cannot build (src/native_c_host.cpp). */ + +/** Where a point's price came from — `NativePriceProvenance`: the + * `provenance` word of #pf_native_decision_v1 and #pf_native_event_v1 and + * #pf_native_margin_view_v1::cursor_provenance. */ +typedef enum pf_native_price_provenance_e { + PF_NATIVE_PROVENANCE_CONFIRMED = 0, /**< A confirmed input bar's own label, + * or a point on its modeled path. */ + PF_NATIVE_PROVENANCE_OBSERVED_PRINT = 1, /**< A realtime print. */ + PF_NATIVE_PROVENANCE_MODELED_OHLC_OPEN = 2, /**< A modeled path's opening point. */ + PF_NATIVE_PROVENANCE_MODELED_OHLC_CLOSE = 3, /**< A modeled path's closing point. */ + PF_NATIVE_PROVENANCE_CARRIED_OPEN = 4, /**< A quiet tradable interval's carried + * last price. */ + PF_NATIVE_PROVENANCE_AFTER_CALCULATION_CLOSE = 5, /**< The close point right after a + * calculation. */ + PF_NATIVE_PROVENANCE_PARTIAL_FINALIZED = 6, /**< A partially finalized observed slot. */ + PF_NATIVE_PROVENANCE_CALCULATION = 7, /**< The script calculation point itself. */ + PF_NATIVE_PROVENANCE_CURRENT_EXECUTION = 8 /**< A synchronous + * #strategy_native_execute_current_v1. */ +} pf_native_price_provenance_t; + +/** Which leg of a modeled OHLC walk a point sits on — `NativePathPhase`: the + * `path_phase` word of #pf_native_decision_v1 and #pf_native_event_v1 and + * #pf_native_margin_view_v1::cursor_path_phase. NONE is a discrete point (a + * calculation, an observed print); the other four are the waypoints, in the + * order the run's path order resolves. */ +typedef enum pf_native_path_phase_e { + PF_NATIVE_PATH_PHASE_NONE = 0, + PF_NATIVE_PATH_PHASE_OPEN = 1, + PF_NATIVE_PATH_PHASE_HIGH = 2, + PF_NATIVE_PATH_PHASE_LOW = 3, + PF_NATIVE_PATH_PHASE_CLOSE = 4 +} pf_native_path_phase_t; + +/** How a script interval or a higher-timeframe bucket was closed — + * `NativeCompletionKind`: #pf_native_decision_v1::completion and the + * `completion` argument of #pf_native_callbacks_v1::on_timeframe_bar. */ +typedef enum pf_native_completion_kind_e { + PF_NATIVE_COMPLETION_KIND_CONFIRMED = 0, /**< Its own last contributing bar. */ + PF_NATIVE_COMPLETION_KIND_LAZY_COMPLETE = 1, /**< The NEXT interval's first input: a + * session-clipped bar, or a hole over + * the last slot. */ + PF_NATIVE_COMPLETION_KIND_SESSION_SHORTENED = 2, /**< The session close clipped it. */ + PF_NATIVE_COMPLETION_KIND_PARTIAL_FINALIZED = 3 /**< A stream end finalized a forming + * observed slot. */ +} pf_native_completion_kind_t; + +/** Which quote #pf_native_decision_v1::price is — `NativeCurrentQuoteKind`: + * the callback's own market decision point, or the execution it is anchored + * to (inside `on_applied`). */ +typedef enum pf_native_quote_kind_e { + PF_NATIVE_QUOTE_MARKET_DECISION = 0, + PF_NATIVE_QUOTE_EXECUTION_ANCHOR = 1 +} pf_native_quote_kind_t; + +/** Why an applied execution finished its request — + * `native_order::AppliedTerminalReason`: #pf_native_applied_v1::terminal_reason + * and the `reason` of a terminal #PF_NATIVE_EVENT_APPLIED row. */ +typedef enum pf_native_terminal_reason_e { + PF_NATIVE_TERMINAL_WORKING_UNITS_SATISFIED = 0, + PF_NATIVE_TERMINAL_FLATTENED = 1, + PF_NATIVE_TERMINAL_TARGET_EXHAUSTED = 2 +} pf_native_terminal_reason_t; + +/** Why the kernel refused a request — `native_order::RequestRejectReason`: + * the `reject` out-parameter of #strategy_native_submit_v1 and + * #strategy_native_replace_ext_v1, and the `reason` of a + * #PF_NATIVE_EVENT_REJECTED or #PF_NATIVE_EVENT_REPLACE_REJECTED row. */ +typedef enum pf_native_reject_reason_e { + PF_NATIVE_REJECT_INVALID_QUANTITY = 0, + PF_NATIVE_REJECT_OFF_GRID = 1, + PF_NATIVE_REJECT_INVALID_TRIGGER = 2, + PF_NATIVE_REJECT_INVALID_CAPACITY = 3, + PF_NATIVE_REJECT_INVALID_OWNER = 4, + PF_NATIVE_REJECT_INVALID_QUANTITY_BASIS = 5, + PF_NATIVE_REJECT_INVALID_GROUP = 6, + PF_NATIVE_REJECT_PLACEMENT_ADMISSION = 7 /**< A SIZED request resolved at acceptance + * that the run's opening admission + * refuses at the sizing price. */ +} pf_native_reject_reason_t; + +/** Why a live request left the book unfilled — `native_order::CancelReason`: + * the `reason` of a #PF_NATIVE_EVENT_CANCELLED row. */ +typedef enum pf_native_cancel_reason_e { + PF_NATIVE_CANCEL_USER = 0, /**< The host's own cancel. */ + PF_NATIVE_CANCEL_GROUP = 1, /**< A group sibling's fill withdrew it. */ + PF_NATIVE_CANCEL_OWNER_GONE = 2, /**< Its owner left the book first. */ + PF_NATIVE_CANCEL_UNSUPPORTED_RELATION = 3, + PF_NATIVE_CANCEL_SUPERSEDED = 4 /**< A kernel-originated request the kernel + * itself withdrew: its level or units + * moved, or the breach is over. */ +} pf_native_cancel_reason_t; + +/** Why a matching candidate was refused — `native_order::MatchRejectReason`: + * the `reason` of a #PF_NATIVE_EVENT_MATCH_REJECTED row. */ +typedef enum pf_native_match_reject_e { + PF_NATIVE_MATCH_REJECT_NONPOSITIVE_PRICE = 0, + PF_NATIVE_MATCH_REJECT_OPENING_DIRECTION = 1, /**< `allowed_open_directions`. */ + PF_NATIVE_MATCH_REJECT_MAX_ABS_UNITS = 2, + PF_NATIVE_MATCH_REJECT_MAX_OPEN_LOTS = 3, + PF_NATIVE_MATCH_REJECT_INITIAL_MARGIN = 4, + PF_NATIVE_MATCH_REJECT_TERMS_UNRESOLVED = 5, + PF_NATIVE_MATCH_REJECT_INVALID_TERMS = 6, + PF_NATIVE_MATCH_REJECT_NO_OPPOSITE_EXPOSURE = 7, + PF_NATIVE_MATCH_REJECT_HOST_PRECOMMIT = 8, + PF_NATIVE_MATCH_REJECT_RISK_LIMIT = 9 /**< An opening refused while a risk + * limit blocks. */ +} pf_native_match_reject_t; + +/** Which trigger a price reached — `native_order::ActivationKind`: the + * `reason` of a #PF_NATIVE_EVENT_ACTIVATED row. */ +typedef enum pf_native_activation_e { + PF_NATIVE_ACTIVATION_STOP = 0, + PF_NATIVE_ACTIVATION_STOP_LIMIT = 1, + PF_NATIVE_ACTIVATION_TRAIL_ARM = 2, + PF_NATIVE_ACTIVATION_TRAIL_TRIGGER = 3 +} pf_native_activation_t; + +/** Who issued a live request — `native_order::RequestOrigin`: + * #pf_native_working_v1::origin. */ +typedef enum pf_native_origin_e { + PF_NATIVE_ORIGIN_HOST = 0, + PF_NATIVE_ORIGIN_KERNEL_LIQUIDATION = 1, /**< The run's margin model. */ + PF_NATIVE_ORIGIN_KERNEL_RISK = 2 /**< The run's risk limits. */ +} pf_native_origin_t; + +/** A run's durable first failure — `NativeFailureCode`: + * #pf_native_state_v1::failure_code. #PF_NATIVE_FAILURE_CALLBACK is the + * historical spelling of #PF_NATIVE_FAILURE_CALLBACK_EXCEPTION. */ +typedef enum pf_native_failure_code_e { + PF_NATIVE_FAILURE_NONE = 0, + PF_NATIVE_FAILURE_INVALID_SPECIFICATION = 1, /**< A spec refused at configure. */ + PF_NATIVE_FAILURE_CONTRACT = 2, /**< A lifecycle misuse. */ + PF_NATIVE_FAILURE_PREFLIGHT = 3, /**< A bar array the driver refused. */ + PF_NATIVE_FAILURE_UNSUPPORTED_SOURCE = 4, + PF_NATIVE_FAILURE_CALLBACK_EXCEPTION = 5, /**< A callback returned non-zero. */ + PF_NATIVE_FAILURE_SETTLEMENT_FAILURE = 6, + PF_NATIVE_FAILURE_ALLOCATION = 7, + PF_NATIVE_FAILURE_COUNTER_EXHAUSTED = 8, + PF_NATIVE_FAILURE_ABORTED = 9, /**< A cooperative abort. */ + PF_NATIVE_FAILURE_PROJECTION_MISMATCH = 10, + PF_NATIVE_FAILURE_CALENDAR = 11, + PF_NATIVE_FAILURE_UNEXPECTED = 12 +} pf_native_failure_code_t; + +/** Which operation was in flight when the failure latched — + * `NativeFailureOperation`: #pf_native_state_v1::failure_operation. */ +typedef enum pf_native_failure_operation_e { + PF_NATIVE_OPERATION_NONE = 0, + PF_NATIVE_OPERATION_CONFIGURE = 1, + PF_NATIVE_OPERATION_BEGIN = 2, + PF_NATIVE_OPERATION_COMMAND = 3, + PF_NATIVE_OPERATION_INPUT = 4, + PF_NATIVE_OPERATION_CALLBACK = 5, + PF_NATIVE_OPERATION_SETTLEMENT = 6, + PF_NATIVE_OPERATION_MUTATION = 7, + PF_NATIVE_OPERATION_STREAM = 8 +} pf_native_failure_operation_t; + +/** Which driving a Running run is in — `NativeRunPhase`: + * #pf_native_state_v1::phase. */ +typedef enum pf_native_run_phase_e { + PF_NATIVE_PHASE_BATCH = 0, + PF_NATIVE_PHASE_WARMUP = 1, /**< A stream's warmup replay. */ + PF_NATIVE_PHASE_REALTIME = 2 /**< A stream's realtime leg. */ +} pf_native_run_phase_t; + +/** How a Completed run ended — `NativeCompletion`: + * #pf_native_state_v1::completion. */ +typedef enum pf_native_completion_e { + PF_NATIVE_COMPLETION_BATCH_COMPLETE = 0, + PF_NATIVE_COMPLETION_STREAM_ENDED = 1 +} pf_native_completion_t; + +/* ── The typed refusal words of a declaration and an append ──────── */ + +/** Why a declaration was refused — `NativeRunSpecError`, the kernel's own + * validation word: the `error` out-parameter of + * #strategy_native_declare_subscriptions_ext_v1 and + * #strategy_native_declare_auxiliary_feed_v1. Read beside the + * #pf_native_spec_field_t the same call writes. Every value the run spec's + * validation can answer is named, the ones a begin-time declaration cannot + * reach included, so one enumeration reads every setup refusal. */ +typedef enum pf_native_spec_error_e { + PF_NATIVE_SPEC_ERROR_NONE = 0, /**< Applied. */ + PF_NATIVE_SPEC_ERROR_EMPTY_REQUIRED_STRING = 1, + PF_NATIVE_SPEC_ERROR_EMBEDDED_NUL = 2, + PF_NATIVE_SPEC_ERROR_INVALID_UTF8 = 3, + PF_NATIVE_SPEC_ERROR_ZERO_RUN_NUMBER = 4, + PF_NATIVE_SPEC_ERROR_INVALID_TIMEFRAME = 5, + PF_NATIVE_SPEC_ERROR_INCOMPATIBLE_TIMEFRAMES = 6, + PF_NATIVE_SPEC_ERROR_UNRESOLVED_TIMEZONE = 7, + PF_NATIVE_SPEC_ERROR_INVALID_SESSION = 8, + PF_NATIVE_SPEC_ERROR_NOT_FINITE_POSITIVE = 9, + PF_NATIVE_SPEC_ERROR_SLIPPAGE_OUT_OF_RANGE = 10, + PF_NATIVE_SPEC_ERROR_UNKNOWN_FEE_KIND = 11, + PF_NATIVE_SPEC_ERROR_NOT_FINITE_NONNEGATIVE = 12, + PF_NATIVE_SPEC_ERROR_UNKNOWN_CLOSE_EXECUTION = 13, + PF_NATIVE_SPEC_ERROR_UNKNOWN_ABORT_REPORTING = 14, + PF_NATIVE_SPEC_ERROR_UNKNOWN_OPEN_DIRECTIONS = 15, + PF_NATIVE_SPEC_ERROR_ZERO_LOT_LIMIT = 16, + PF_NATIVE_SPEC_ERROR_ALLOCATION_FAILURE = 17, /**< Staging threw. */ + PF_NATIVE_SPEC_ERROR_CALENDAR_FAILURE = 18, /**< No calendar resolved it. */ + PF_NATIVE_SPEC_ERROR_INVALID_INTRABAR_PATH = 19, + PF_NATIVE_SPEC_ERROR_UNKNOWN_INTRABAR_SAMPLE_ELIGIBILITY = 20, + PF_NATIVE_SPEC_ERROR_INVALID_UNDETECTED_TIMEFRAME = 21, + PF_NATIVE_SPEC_ERROR_UNKNOWN_SLOT_LABEL_POLICY = 22, + PF_NATIVE_SPEC_ERROR_UNKNOWN_FEED_TOLERANCE = 23, /**< `feed_tolerance`: the + * kernel's + * `legacy_tolerance`. */ + PF_NATIVE_SPEC_ERROR_UNKNOWN_PATH_ORDER = 24, + PF_NATIVE_SPEC_ERROR_UNKNOWN_REPORT_POLICY = 25, + PF_NATIVE_SPEC_ERROR_UNKNOWN_PRICE_GRID = 26, + PF_NATIVE_SPEC_ERROR_UNKNOWN_GRID_ROUNDING = 27, + PF_NATIVE_SPEC_ERROR_GRID_REQUIRES_PRICE_TICK = 28, + PF_NATIVE_SPEC_ERROR_INVALID_SUBSCRIPTION_TIMEFRAME = 29, + PF_NATIVE_SPEC_ERROR_SUBSCRIPTION_FINER_THAN_INPUT = 30, + PF_NATIVE_SPEC_ERROR_DUPLICATE_SUBSCRIPTION_TIMEFRAME = 31, + PF_NATIVE_SPEC_ERROR_UNORDERED_SUBSCRIPTION_BARS = 32, + PF_NATIVE_SPEC_ERROR_SUBSCRIPTION_WITHOUT_TIMEFRAME = 33, + PF_NATIVE_SPEC_ERROR_MARGIN_MODEL_CONFLICT = 34, + PF_NATIVE_SPEC_ERROR_UNKNOWN_LIQUIDATION_SIZING = 35, + PF_NATIVE_SPEC_ERROR_UNKNOWN_LIQUIDATION_CHECK = 36, + PF_NATIVE_SPEC_ERROR_UNKNOWN_MARGIN_EQUITY_BASIS = 37, + PF_NATIVE_SPEC_ERROR_UNKNOWN_LIQUIDATION_LEVEL_BASE = 38, + PF_NATIVE_SPEC_ERROR_UNKNOWN_CALCULATION_TRIGGER = 39, + PF_NATIVE_SPEC_ERROR_UNKNOWN_OPEN_BAR_VIEW = 40, + PF_NATIVE_SPEC_ERROR_UNKNOWN_RISK_DAY = 41, + PF_NATIVE_SPEC_ERROR_UNKNOWN_RISK_ACTION = 42, + PF_NATIVE_SPEC_ERROR_ZERO_RISK_LIMIT = 43, + PF_NATIVE_SPEC_ERROR_MARGIN_SIDE_UNDECLARED = 44, + PF_NATIVE_SPEC_ERROR_INVALID_AUXILIARY_FEED_TIMEFRAME = 45, + PF_NATIVE_SPEC_ERROR_AUXILIARY_FEED_NOT_FINER_THAN_INPUT = 46, + PF_NATIVE_SPEC_ERROR_UNORDERED_AUXILIARY_FEED_BARS = 47, + PF_NATIVE_SPEC_ERROR_INVALID_AUXILIARY_FEED_BAR = 48, + PF_NATIVE_SPEC_ERROR_AUXILIARY_FEED_WITHOUT_TIMEFRAME = 49, + PF_NATIVE_SPEC_ERROR_UNKNOWN_SERIES_SOURCE = 50, + PF_NATIVE_SPEC_ERROR_SUBSCRIPTION_WITHOUT_AUXILIARY_FEED = 51, + PF_NATIVE_SPEC_ERROR_SUBSCRIPTION_FINER_THAN_AUXILIARY_FEED = 52, + PF_NATIVE_SPEC_ERROR_WRONG_PHASE = 53, /**< The CALL was refused + * before any field was + * judged: not in the + * phase it is legal + * in. Read with + * FIELD_NONE. */ + PF_NATIVE_SPEC_ERROR_UNKNOWN_EVENT_RETENTION = 54 /**< An `event_retention` + * word outside + * #pf_native_event_retention_t. */ +} pf_native_spec_error_t; + +/** Where a declaration was refused — `NativeRunSpecField`, the first field + * the kernel's validation found: the `field` out-parameter of + * #strategy_native_declare_subscriptions_ext_v1 and + * #strategy_native_declare_auxiliary_feed_v1. */ +typedef enum pf_native_spec_field_e { + PF_NATIVE_SPEC_FIELD_NONE = 0, /**< Not about one field. */ + PF_NATIVE_SPEC_FIELD_SESSION_KEY = 1, + PF_NATIVE_SPEC_FIELD_RUN_NUMBER = 2, + PF_NATIVE_SPEC_FIELD_INPUT_TIMEFRAME = 3, + PF_NATIVE_SPEC_FIELD_SCRIPT_TIMEFRAME = 4, + PF_NATIVE_SPEC_FIELD_TICKER = 5, + PF_NATIVE_SPEC_FIELD_TICKER_ID = 6, + PF_NATIVE_SPEC_FIELD_TYPE = 7, + PF_NATIVE_SPEC_FIELD_CURRENCY = 8, + PF_NATIVE_SPEC_FIELD_BASE_CURRENCY = 9, + PF_NATIVE_SPEC_FIELD_DESCRIPTION = 10, + PF_NATIVE_SPEC_FIELD_VOLUME_TYPE = 11, + PF_NATIVE_SPEC_FIELD_TIMEZONE = 12, + PF_NATIVE_SPEC_FIELD_SESSION = 13, + PF_NATIVE_SPEC_FIELD_CHART_TIMEZONE = 14, + PF_NATIVE_SPEC_FIELD_INITIAL_CAPITAL = 15, + PF_NATIVE_SPEC_FIELD_POINT_VALUE = 16, + PF_NATIVE_SPEC_FIELD_ACCOUNT_FX = 17, + PF_NATIVE_SPEC_FIELD_PRICE_TICK = 18, + PF_NATIVE_SPEC_FIELD_SLIPPAGE_TICKS = 19, + PF_NATIVE_SPEC_FIELD_FEE_KIND = 20, + PF_NATIVE_SPEC_FIELD_FEE_VALUE = 21, + PF_NATIVE_SPEC_FIELD_QUANTITY_GRID = 22, + PF_NATIVE_SPEC_FIELD_CLOSE_EXECUTION = 23, + PF_NATIVE_SPEC_FIELD_ABORT_REPORTING = 24, + PF_NATIVE_SPEC_FIELD_MAX_ABS_UNITS = 25, + PF_NATIVE_SPEC_FIELD_MAX_OPEN_LOTS = 26, + PF_NATIVE_SPEC_FIELD_ALLOWED_OPEN_DIRECTIONS = 27, + PF_NATIVE_SPEC_FIELD_INITIAL_MARGIN_FRACTION = 28, + PF_NATIVE_SPEC_FIELD_INTRABAR_TIMEFRAME = 29, + PF_NATIVE_SPEC_FIELD_INTRABAR_SAMPLES = 30, + PF_NATIVE_SPEC_FIELD_INTRABAR_DISTRIBUTION = 31, + PF_NATIVE_SPEC_FIELD_INTRABAR_VOLUME_SAMPLES = 32, + PF_NATIVE_SPEC_FIELD_INTRABAR_SAMPLE_ELIGIBILITY = 33, + PF_NATIVE_SPEC_FIELD_TIMEFRAME_UNDETECTED = 34, + PF_NATIVE_SPEC_FIELD_SLOT_LABEL_POLICY = 35, + PF_NATIVE_SPEC_FIELD_FEED_TOLERANCE = 36, /**< `feed_tolerance`: the kernel's + * `legacy_tolerance`. */ + PF_NATIVE_SPEC_FIELD_PATH_ORDER = 37, + PF_NATIVE_SPEC_FIELD_REPORT_POLICY = 38, + PF_NATIVE_SPEC_FIELD_PRICE_GRID = 39, + PF_NATIVE_SPEC_FIELD_GRID_ROUNDING = 40, + PF_NATIVE_SPEC_FIELD_SUBSCRIPTION_TIMEFRAME = 41, + PF_NATIVE_SPEC_FIELD_SUBSCRIPTION_BARS = 42, + PF_NATIVE_SPEC_FIELD_MARGIN_MODEL = 43, + PF_NATIVE_SPEC_FIELD_MARGIN_INITIAL = 44, + PF_NATIVE_SPEC_FIELD_MARGIN_MAINTENANCE = 45, + PF_NATIVE_SPEC_FIELD_MARGIN_SIZING = 46, + PF_NATIVE_SPEC_FIELD_MARGIN_SHORTFALL_MULTIPLE = 47, + PF_NATIVE_SPEC_FIELD_MARGIN_MIN_UNITS = 48, + PF_NATIVE_SPEC_FIELD_MARGIN_CHECK = 49, + PF_NATIVE_SPEC_FIELD_MARGIN_EQUITY_BASIS = 50, + PF_NATIVE_SPEC_FIELD_MARGIN_LEVEL_BASE = 51, + PF_NATIVE_SPEC_FIELD_CALCULATION = 52, + PF_NATIVE_SPEC_FIELD_OPEN_BAR_VIEW = 53, + PF_NATIVE_SPEC_FIELD_RISK_LIMITS = 54, + PF_NATIVE_SPEC_FIELD_RISK_DRAWDOWN = 55, + PF_NATIVE_SPEC_FIELD_RISK_INTRADAY_LOSS = 56, + PF_NATIVE_SPEC_FIELD_RISK_LOSS_DAYS = 57, + PF_NATIVE_SPEC_FIELD_RISK_FILLS_PER_DAY = 58, + PF_NATIVE_SPEC_FIELD_RISK_DAY_BASIS = 59, + PF_NATIVE_SPEC_FIELD_RISK_ACTION = 60, + PF_NATIVE_SPEC_FIELD_AUXILIARY_FEED_TIMEFRAME = 61, + PF_NATIVE_SPEC_FIELD_AUXILIARY_FEED_BARS = 62, + PF_NATIVE_SPEC_FIELD_SUBSCRIPTION_SOURCE = 63, + PF_NATIVE_SPEC_FIELD_EVENT_RETENTION = 64 +} pf_native_spec_field_t; + +/** Why an append was refused — `NativeAuxiliaryAppendError`: the `error` + * out-parameter of #strategy_native_append_auxiliary_bars_ext_v1. None of + * them fails the host but REENTRANT, which latches the contract failure + * every reentrant stream input is, and ALLOCATION_FAILURE. */ +typedef enum pf_native_append_error_e { + PF_NATIVE_APPEND_ERROR_NONE = 0, /**< Applied. */ + PF_NATIVE_APPEND_ERROR_HOST_FAILED = 1, /**< The host had already + * failed. */ + PF_NATIVE_APPEND_ERROR_REENTRANT = 2, /**< From inside a callback or + * an in-flight input. */ + PF_NATIVE_APPEND_ERROR_NOT_REALTIME = 3, /**< Not on a stream's + * realtime leg. */ + PF_NATIVE_APPEND_ERROR_NO_AUXILIARY_FEED = 4, /**< The run declared no + * feed. */ + PF_NATIVE_APPEND_ERROR_INVALID_BAR_ARRAY = 5, /**< A NULL array with a + * non-zero count. */ + PF_NATIVE_APPEND_ERROR_INVALID_BAR = 6, /**< `index` has invalid + * OHLCV. */ + PF_NATIVE_APPEND_ERROR_UNORDERED_BARS = 7, /**< `index` is not strictly + * after its predecessor + * (the feed's last bar + * for index 0). */ + PF_NATIVE_APPEND_ERROR_INPUT_PERIOD_ALREADY_ACCEPTED = 8, /**< The first bar opened + * inside an input period + * already accepted. */ + PF_NATIVE_APPEND_ERROR_ALLOCATION_FAILURE = 9 /**< Growing the feed threw; + * this fails the host. */ +} pf_native_append_error_t; + +/* ── The policy hooks' words ───────────────────────────────────── */ + +/** How an opening settles against an opposite book — + * `native_order::OpeningShape`, #pf_native_terms_v1::shape. Only an OPENING + * may name a shape other than TRANSACT: a kernel-sized one + * (#PF_NATIVE_INTENT_SIZED) that the answer is about; the kernel refuses any + * other with #PF_NATIVE_MATCH_REJECT_INVALID_TERMS. */ +typedef enum pf_native_opening_shape_e { + PF_NATIVE_OPENING_SHAPE_TRANSACT = 0, /**< Net against the book (the default). */ + PF_NATIVE_OPENING_SHAPE_REVERSE_TO = 1, /**< Close the opposite book, then open the + * units. */ + PF_NATIVE_OPENING_SHAPE_CLOSE_OPPOSITE = 2 /**< Close the opposite book and open the + * remainder. */ +} pf_native_opening_shape_t; + +/** Which price a matching candidate is at — `native_order::NativeCandidatePriceKind`, + * #pf_native_terms_view_v1::price_kind. */ +typedef enum pf_native_candidate_price_e { + PF_NATIVE_CANDIDATE_PRICE_POINT_PRICE = 0, /**< The driver point's own price. */ + PF_NATIVE_CANDIDATE_PRICE_TRIGGER_LEVEL = 1, /**< A resting level the path crossed. */ + PF_NATIVE_CANDIDATE_PRICE_CURRENT_QUOTE = 2 /**< A current execution's quote. */ +} pf_native_candidate_price_t; + +/** Which kind of driver point a candidate matched at — + * `native_order::DriverEligibilityClass`, + * #pf_native_terms_view_v1::driver_class. */ +typedef enum pf_native_driver_class_e { + PF_NATIVE_DRIVER_CLASS_OBSERVED_PRINT = 0, + PF_NATIVE_DRIVER_CLASS_CARRIED_OPEN = 1, + PF_NATIVE_DRIVER_CLASS_TICK_AFTER_CALCULATION = 2, + PF_NATIVE_DRIVER_CLASS_CONFIRMED_OPEN = 3, + PF_NATIVE_DRIVER_CLASS_CONFIRMED_EXCURSION = 4, + PF_NATIVE_DRIVER_CLASS_CONFIRMED_AFTER_CALCULATION_CLOSE = 5, + PF_NATIVE_DRIVER_CLASS_CURRENT_EXECUTION = 6 +} pf_native_driver_class_t; + +/** The settlement shape an execution is about to take — the alternative of + * `native_order::ExecutionPlan`, #pf_native_precommit_view_v1::plan. */ +typedef enum pf_native_plan_e { + PF_NATIVE_PLAN_FLATTEN = 0, /**< Close the whole book; `plan_units` 0. */ + PF_NATIVE_PLAN_REDUCE = 1, /**< Close `plan_units` units. */ + PF_NATIVE_PLAN_TRANSACT = 2, /**< Trade `plan_units` signed units. */ + PF_NATIVE_PLAN_REVERSE_TO = 3 /**< Reach `plan_units` signed exposure. */ +} pf_native_plan_t; + +/** What #pf_native_callbacks_v1::on_precommit answers — + * `NativePrecommitVerdict`. */ +typedef enum pf_native_precommit_verdict_e { + PF_NATIVE_PRECOMMIT_ADMIT = 0, /**< The kernel's own path. */ + PF_NATIVE_PRECOMMIT_REFUSE = 1, /**< A nonfinancial + * #PF_NATIVE_MATCH_REJECT_HOST_PRECOMMIT. */ + PF_NATIVE_PRECOMMIT_ADMIT_WITH_HOST_MARGIN = 2 /**< Admit, and the host owns this one + * opening's margin check: the + * kernel's initial-margin gate is + * skipped for it. */ +} pf_native_precommit_verdict_t; + +/** Which trigger of an anchored leg the owner's fill supplies — + * `NativeAnchoredTrigger`, #pf_native_anchored_level_view_v1::trigger. */ +typedef enum pf_native_anchored_trigger_e { + PF_NATIVE_ANCHORED_TRIGGER_LIMIT = 0, + PF_NATIVE_ANCHORED_TRIGGER_STOP = 1, + PF_NATIVE_ANCHORED_TRIGGER_TRAIL_ARM = 2 +} pf_native_anchored_trigger_t; + +/** @} */ /* end of pf_native_c_enums */ + +/** @defgroup pf_native_c_types Transport types + * @{ */ + +/** Where the kernel is, presented to every callback. + * + * A read-only snapshot of `NativeDecisionContext` plus the current quote: + * `price` is `current_execution_point()`'s price (the bar's own close at its + * close calculation) and NaN only where that answers nullopt. + * + * It has TWO published layouts, and the runtime PRESENTS the one the + * caller's callback table was published with: a table sent at the current + * length is handed the whole struct, with its session tail; a table sent at + * an earlier published length is handed `struct_size` = + * #PF_NATIVE_DECISION_V1_BASE_SIZE — the sizeof that caller's own header + * compiled — and nothing past it is filled. So an older caller's exact-size + * check keeps holding, and a caller reads a field only below `struct_size`. + * + * The four session-day bytes after `quote_kind` (R5 lane F5) sit in what + * was the base layout's tail padding: its size and every offset before them + * are the ones a caller compiled against the first layout reads, so that + * caller keeps working unchanged, and a table of either length is handed + * them. `session_facts` is 1 when the runtime wrote the three after it, and + * 0 from a runtime that predates them. Each is 0 or 1, and every callback of + * one script bar carries the same values: + * - `in_session`: the script bar's label is in session on the run's own + * calendar (`session` / `timezone`); + * - `opens_session_day`: in session, and the bar before it is not, or is + * on another session day; + * - `closes_session_day`: in session, and the bar after it is not, or is on + * another session day. + * The session day is the run calendar's (it rolls at the first window's + * start and is keyed to its trading date, so an overnight session is one day + * across local midnight). The bar before / after is the one the run holds -- + * the batch input or stream warmup -- and otherwise the calendar's slot one + * script width away; a run's first bar opens its day and a batch's final bar + * closes it. A D/W/M bar holds whole days: all three are 1. The C++ context's + * fourth fact, `closes_session_day_open_ended`, has no byte here: it differs + * only on a batch's final bar, for a host that recomputes a batch whose last + * input is still forming, and a C host's live edge is a stream, whose + * `closes_session_day` already reads the calendar. + * + * The session tail is the script interval under delivery and the session + * day of its open and of the NEXT input's open, on the run's own calendar + * (its `session` and `timezone`): the calendar facts under the session-day + * bytes above, for a host that applies a rule of its own. On the calendar + * alone, a script bar is the last of its session day when + * `next_input_session_day_ordinal` differs from `session_day_ordinal`, and + * the first when the bar before it was the last. */ +typedef struct pf_native_decision_v1 { + uint32_t struct_size; /**< sizeof(pf_native_decision_v1). */ + uint32_t version; /**< PF_NATIVE_API_VERSION. */ + uint64_t ordinal; /**< Matching-point ordinal. */ + int32_t interval_index; /**< Script interval index. */ + int32_t sub_index; /**< Sub-bar index inside the script bar. */ + int32_t sub_count; /**< Sub-bars in this script bar (1 when unmagnified). */ + int32_t is_terminal_sub_bar; /**< 1 on the script bar's last sub-bar. */ + int64_t effective_time_ms; /**< The point's effective decision time. */ + int64_t script_bar_open_ms; /**< Open of the script bar under delivery. */ + int64_t sub_bar_open_ms; /**< Open of the sub-bar under delivery. */ + int64_t decision_floor_ms; /**< Lower bound a new request may be matched at. */ + double price; /**< Current quote, NaN outside an execution point. */ + uint8_t provenance; /**< #pf_native_price_provenance_t. */ + uint8_t path_phase; /**< #pf_native_path_phase_t. */ + uint8_t completion; /**< #pf_native_completion_kind_t. */ + uint8_t quote_kind; /**< #pf_native_quote_kind_t; 0 when price is NaN. */ + uint8_t session_facts; /**< 1 when the three bytes below are written. */ + uint8_t in_session; /**< The script bar is in session. */ + uint8_t opens_session_day; /**< It opens its session day. */ + uint8_t closes_session_day; /**< It closes its session day. */ + + /* ── The additive session tail (R5 lane F4). Presented only to a callback + * table of the current layout; see the struct note. ── */ + int64_t script_interval_open_ms; /**< The script interval's nominal origin. */ + int64_t script_interval_eligible_open_ms; /**< Its first in-session instant. */ + int64_t script_interval_last_traded_close_ms; /**< Its exclusive end of trading. */ + int64_t script_interval_next_period_open_ms; /**< The next interval's nominal open, + * a closed slot included. */ + int64_t script_interval_next_input_open_ms; /**< The next actual eligible input + * open at or after that close: + * closed time skipped. */ + int64_t session_day_ordinal; /**< The session day of + * `script_interval_open_ms`: days + * since 1970-01-01 of its trading + * date. */ + int64_t next_input_session_day_ordinal; /**< The session day of + * `script_interval_next_input_open_ms`. */ + uint8_t has_script_interval; /**< 1 when the point has a script + * interval; the seven fields above + * are 0 otherwise. */ + uint8_t has_session_day; /**< 1 when the calendar keyed + * `session_day_ordinal`. */ + uint8_t has_next_input_session_day; /**< 1 when it keyed + * `next_input_session_day_ordinal`. */ + uint8_t reserved0[5]; /**< Always 0. */ +} pf_native_decision_v1; + +/** Byte length of #pf_native_decision_v1 as the L13 lane first published it, + * before the session tail was appended — the `struct_size` a callback table + * of an earlier published length is presented. It is the offset of the first + * appended field, which is that layout's sizeof on every target (it ended in + * padding the tail's first 8-byte field begins after). */ +#define PF_NATIVE_DECISION_V1_BASE_SIZE \ + ((uint32_t)offsetof(pf_native_decision_v1, script_interval_open_ms)) + +/** One applied execution, presented to `on_applied`. */ +typedef struct pf_native_applied_v1 { + uint32_t struct_size; /**< sizeof(pf_native_applied_v1). */ + uint32_t version; /**< PF_NATIVE_API_VERSION. */ + uint64_t ordinal; /**< Event ordinal. */ + uint64_t incarnation; /**< The request that executed. */ + uint64_t opened_lot_incarnation; /**< The lot this fill opened, 0 when none. */ + double raw_price; /**< The path price the match was found at. */ + double resolved_price; /**< The booked price after terms and slippage. */ + double closed_units; /**< Units closed by this fill. */ + double opened_units; /**< Units opened by this fill. */ + double filled_working; /**< Working units this fill consumed. */ + double ticket; /**< `current_ticket` at the fill. */ + int64_t cycle_before; /**< Position cycle before the fill. */ + int64_t cycle_after; /**< Position cycle after the fill. */ + uint8_t terminal; /**< 1 when the request is finished. */ + uint8_t terminal_reason; /**< #pf_native_terminal_reason_t, valid when + * `has_terminal_reason`. */ + uint8_t has_terminal_reason; /**< 1 when `terminal_reason` is meaningful. */ + uint8_t reserved0; +} pf_native_applied_v1; + +/** One recorded event, read back by #strategy_native_events_v1. + * + * `kind` tags the union: every field below is documented per kind and is + * zero where that kind has no such fact. `reason` is read by the kind's own + * enumeration, and is 0 for a kind that names none. + * - ACCEPTED / ARMED: `incarnation`. + * - REJECTED: `reason` is a #pf_native_reject_reason_t. + * - REPLACE_REJECTED: `incarnation` is the target, `reason` a + * #pf_native_reject_reason_t. + * - REPLACED: `incarnation` is the predecessor, `successor` the new handle. + * - CANCELLED: `reason` is a #pf_native_cancel_reason_t. + * - NOT_WORKING / INVALID_HANDLE: `incarnation` is the target. + * - NO_EFFECT: `incarnation`, cursor fields. + * - MATCH_REJECTED: `reason` is a #pf_native_match_reject_t, cursor fields. + * - APPLIED: every price/unit/cycle field, `terminal`, cursor fields; when + * `terminal` is 1, `reason` is the #pf_native_terminal_reason_t that + * finished the request. + * - CLOSE_BOUND: `reason` is the #pf_native_side_t of the book the close + * bound to, `cycle_after` its cycle, cursor fields. + * - RESERVATION_REDUCED / DEFERRED_GROUP: `incarnation` is the recipient, + * `reason` its #pf_native_group_effect_t, `closed_units` the deduction + * (applied, or deferred). + * - QUANTITY_BOUND: `incarnation`, `opened_units` = the source units. + * - TERMS_RESOLVED: `raw_price`, `resolved_price` (= `price`), cursor fields. + * - MARGIN_CALL: `reason` is the #pf_native_side_t of the liquidated + * position, `price` = mark, `closed_units` = liquidated units, + * `opened_units` = the signed position after it, `raw_price` = the + * re-solved liquidation price. The call's equity, requirement and the + * position before it are #strategy_native_margin_call_v1's. + * - ACTIVATED: `reason` is a #pf_native_activation_t, `price` the reached + * price. + * - DRIVER_POINT: cursor fields and `raw_price`. + * - ACCOUNT: `price` = marked equity, `raw_price` = realized balance, + * `opened_units` = signed position units. + * - RISK: `reason` is a #pf_native_risk_limit_t, `price` = the observed + * value that reached the limit, `raw_price` = the limit it was measured + * against (account currency for the two loss limits — a percent limit is + * already resolved against its basis equity — days or fills for the two + * counts), `cycle_before` = the risk day the breach happened on, on the + * spec's own day basis, `successor` = the cursor's matching-point + * ordinal, cursor fields set. The event names no request: it is an + * account fact, so `incarnation` stays 0. */ +typedef struct pf_native_event_v1 { + uint32_t struct_size; /**< sizeof(pf_native_event_v1). */ + uint32_t version; /**< PF_NATIVE_API_VERSION. */ + uint32_t kind; /**< #pf_native_event_kind_t. */ + uint32_t reason; /**< Per-kind reason enumerator, 0 when none. */ + uint64_t ordinal; /**< Event ordinal; strictly increasing. */ + uint64_t incarnation; /**< Subject request, 0 when the kind has none. */ + uint64_t successor; /**< REPLACED: the new handle. RISK: the cursor's + * matching-point ordinal. 0 otherwise. */ + double raw_price; + double resolved_price; + double price; + double closed_units; + double opened_units; + int64_t cycle_before; + int64_t cycle_after; + int64_t effective_time_ms; /**< Cursor effective time, 0 when the kind has no cursor. */ + int32_t interval_index; /**< Cursor interval index. */ + uint8_t provenance; /**< Cursor #pf_native_price_provenance_t. */ + uint8_t path_phase; /**< Cursor #pf_native_path_phase_t. */ + uint8_t terminal; /**< APPLIED only. */ + uint8_t reserved0[3]; +} pf_native_event_v1; + +/** One live request, copied out by #strategy_native_working_get_v1. + * + * `label` and `comment` borrow the snapshot taken by the most recent + * #strategy_native_working_len_v1 call on this handle. They stay valid until + * the next call to that function, or until the host is freed; copy them if + * the host keeps them longer. + * + * This readout has THREE published layouts and the runtime fills any: the + * base layout the L13 lane first shipped (#PF_NATIVE_WORKING_V1_BASE_SIZE), + * that plus `trail_has_arm_price` (#PF_NATIVE_WORKING_V1_ARM_SIZE), and the + * current one, which appends the anchor and the owner relation. A caller + * compiled against an earlier layout keeps working unchanged: its row is + * written up to its own length and never past it. Any other `struct_size` + * is PF_NATIVE_E_STRUCT. + * + * The relation tail is the request's own anchor and owner as the kernel + * holds them now: an anchored leg reads FROM_OWNER_FILL, its rounding and + * its offset (a tick-spelled one already resolved to price units at + * acceptance) until its owner fills, and ABSOLUTE with the installed level + * in `p1` / `p2` after; the owner relation names the owner handle, how many + * there are and the bound cycle, and a WAIT_FOR_APPLIED child's visibility, + * first-match rule and scope. + * + * `trail_has_arm_price` is the request's own flag read back, so a TRAIL with + * no arm price and one armed at 0.0 are two different rows even though both + * read `p2` = 0. An anchored trail (#PF_NATIVE_ANCHOR_FROM_OWNER_FILL) reads + * back the spelling it was submitted with until its owner fills; the arm + * then installs the level, and from there it reads 1 with that level in + * `p2`. A tick-spelled offset needs no such flag: acceptance resolves it + * into `p1` as a price distance. */ +typedef struct pf_native_working_v1 { + uint32_t struct_size; /**< sizeof(pf_native_working_v1). */ + uint32_t version; /**< PF_NATIVE_API_VERSION. */ + uint64_t incarnation; /**< The request's handle. */ + uint32_t intent; /**< #pf_native_intent_t as accepted. */ + uint32_t trigger; /**< #pf_native_trigger_t as accepted. */ + uint32_t owner; /**< #pf_native_owner_t as accepted. */ + uint32_t capacity; /**< #pf_native_capacity_t as accepted. */ + uint32_t group_kind; /**< #pf_native_group_t as accepted. */ + uint32_t group_effect; /**< #pf_native_group_effect_t, 0 when no group. */ + uint32_t remaining_kind; /**< #pf_native_remaining_t. */ + uint32_t trigger_state; /**< #pf_native_trigger_state_t. */ + uint32_t origin; /**< #pf_native_origin_t. */ + uint32_t reserved0; + double intent_value; /**< The intent's own scalar, 0 when it has none. */ + double p1; /**< Trigger level 1 (limit / stop / trail offset). */ + double p2; /**< Trigger level 2 (stop-limit limit / trail arm + * when `trail_has_arm_price`). */ + double capacity_units; /**< Point budget, 0 for immediate capacity. */ + double remaining_units; /**< Valid when `remaining_kind` is UNITS. */ + uint64_t group_id; + int64_t group_cohort; + uint64_t acceptance_ordinal; /**< Birth: the accepting event. */ + int64_t decision_time_lower_bound; /**< Birth: earliest matchable time. */ + const char* label; /**< Borrowed; see the struct note. */ + const char* comment; /**< Borrowed; see the struct note. */ + + /* ── The additive arm-presence tail. Written only when `struct_size` is + * the current sizeof; a caller sending the base layout is filled up to + * `comment` above and never past it. ── */ + uint32_t trail_has_arm_price; /**< TRAIL: 1 when `p2` is the arm price, 0 when + * the trail carries none. 0 for every other + * trigger. */ + uint32_t reserved1; /**< Always 0. */ + + /* ── The additive relation tail (R5 lane F4). Written only when + * `struct_size` is the current sizeof; see the struct note. ── */ + uint32_t anchor; /**< #pf_native_anchor_t as the request stands. */ + uint32_t anchor_rounding; /**< #pf_native_anchor_rounding_t, FROM_OWNER_FILL only. */ + double anchor_offset; /**< FROM_OWNER_FILL: the signed offset, price units. */ + uint64_t owner_handle; /**< WAIT_FOR_APPLIED: the parent; BIND_OPENING: the + * opening; BIND_OPENINGS: the first of them as + * the kernel ordered the list; BIND_COHORT: the + * cohort; 0 for INDEPENDENT. */ + int64_t owner_cycle; /**< BIND_OPENING / BIND_OPENINGS: the bound cycle. */ + uint32_t owner_n; /**< How many owner handles the relation names. */ + uint32_t visibility; /**< #pf_native_arm_visibility_t, WAIT_FOR_APPLIED only. */ + uint32_t arm_first_match; /**< #pf_native_arm_first_match_t, WAIT_FOR_APPLIED only. */ + uint32_t arm_scope; /**< #pf_native_arm_scope_t, WAIT_FOR_APPLIED only. */ +} pf_native_working_v1; + +/** Byte length of #pf_native_working_v1 as the L13 lane first published it, + * before the arm-presence tail was appended. It is the offset of the first + * appended field, so it stays correct on every target this header builds + * for — it is not a literal. #strategy_native_working_get_v1 accepts this + * length as well as the current `sizeof`, and writes no byte past the + * length it was handed, which is what makes a readout's tail additive + * rather than a layout break. */ +#define PF_NATIVE_WORKING_V1_BASE_SIZE \ + ((uint32_t)offsetof(pf_native_working_v1, trail_has_arm_price)) + +/** Byte length of #pf_native_working_v1 with the arm-presence tail but + * without the relation tail — the second of its three published layouts, + * and the `sizeof` every caller compiled before that tail existed sends. + * The offset of the first relation field, for the same reason + * #PF_NATIVE_WORKING_V1_BASE_SIZE is an offset. */ +#define PF_NATIVE_WORKING_V1_ARM_SIZE \ + ((uint32_t)offsetof(pf_native_working_v1, anchor)) + +/** One open physical lot, copied out by #strategy_native_open_lot_get_v1 — + * the C spelling of `NativeOpenLot` (R5 gap lane N18): the book that + * #strategy_native_position_v1 aggregates, lot by lot, marked at the price + * #strategy_native_open_lot_count_v1 was given. Every field is what the + * kernel already holds for the lot; reading it moves nothing. + * + * `unrealized_pnl` is the lot's own term of the marked equity: the move from + * `entry_price` to `mark`, in account currency, less `entry_commission`. + * The two excursions are the largest moves for and against the lot the + * kernel has sampled along the delivered path, in account currency, gross + * of fees, with `mark` folded in. A NaN `mark` keeps every booking fact, + * leaves `unrealized_pnl` NaN and folds nothing into the excursions. + * + * `entry_label` and `entry_comment` borrow the snapshot taken by the most + * recent #strategy_native_open_lot_count_v1 call on this handle. They stay + * valid until the next call to that function, or until the host is freed; + * copy them if the host keeps them longer. */ +typedef struct pf_native_open_lot_v1 { + uint32_t struct_size; /**< sizeof(pf_native_open_lot_v1). */ + uint32_t version; /**< PF_NATIVE_API_VERSION. */ + uint64_t ordinal; /**< Position in the book, oldest first. */ + uint64_t entry_incarnation; /**< The request whose fill opened the lot; never + * reused; 0 only for a legacy synthetic lot. */ + int64_t cycle; /**< The position cycle the lot belongs to — the + * `owner_cycle` a BIND_OPENING(S) request names. */ + uint32_t side; /**< #pf_native_side_t. */ + int32_t entry_bar_index; /**< Script-bar index of the opening fill. */ + int64_t entry_time_ms; /**< Effective time of the opening fill. */ + double entry_price; /**< Booked entry price. */ + double signed_units; /**< Remaining units: > 0 long, < 0 short. */ + double entry_commission; /**< Entry fee still on the lot, account currency; + * a partial realization takes its share with it. */ + double mark; /**< The price the three fields below were marked at. */ + double unrealized_pnl; /**< Fee-net move to `mark`, account currency; NaN for a NaN mark. */ + double favorable_excursion; /**< Largest move for the lot so far, account currency, >= 0. */ + double adverse_excursion; /**< Largest move against the lot so far, account currency, >= 0. */ + const char* entry_label; /**< Borrowed; see the struct note. */ + const char* entry_comment; /**< Borrowed; see the struct note. */ +} pf_native_open_lot_v1; + +/** The run's lifecycle and its typed failure. */ +typedef struct pf_native_state_v1 { + uint32_t struct_size; /**< sizeof(pf_native_state_v1). */ + uint32_t version; /**< PF_NATIVE_API_VERSION. */ + uint32_t lifecycle; /**< #pf_native_lifecycle_t. */ + uint32_t failure_code; /**< #pf_native_failure_code_t; + * 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; /**< Reserved beside the code: the kernel records no + * discriminator on this surface, so it reads 0. */ + uint64_t failure_ordinal; /**< The point the failure was latched at, 0 when absent. */ + uint64_t consumed_high_water; + int64_t decision_floor_ms; + uint32_t phase; /**< #pf_native_run_phase_t while Running. */ + uint32_t completion; /**< #pf_native_completion_t once Completed. */ +} pf_native_state_v1; + +/** The trail projection of #strategy_native_trail_state_v1 — the C spelling + * of `NativeTrailState`. Before the arm is reached `activated` is 0 and + * every other field is 0; once armed, `best_price` and `current_level` are + * the exact raw matcher values and `activation_ordinal` identifies the + * TrailArm event that began tracking. */ +typedef struct pf_native_trail_state_v1 { + uint32_t struct_size; /**< sizeof(pf_native_trail_state_v1). */ + uint32_t version; /**< PF_NATIVE_API_VERSION. */ + uint32_t activated; /**< 0/1: the arm price has been reached. */ + uint32_t reserved0; + double best_price; /**< Running best; 0 before the arm. */ + double current_level; /**< The stop the best is riding; 0 before the arm. */ + uint64_t activation_ordinal; /**< The TrailArm event; 0 before the arm. */ +} pf_native_trail_state_v1; + +/** The facts one margin hook is handed — the C spelling of + * `NativeMarginRequirementView`, `NativeMarginCheckPoint` and + * `NativeMarginCallView`, which differ only in which of these facts they + * carry. Every field is documented per hook and is zero where that hook has + * no such fact, exactly as #pf_native_event_v1's union is: + * + * - on_margin_check: `kind`, `liquidation_resting`, the position, `mark` + * and the cursor. `equity` and `required` are 0 — nothing has been + * evaluated yet at that point. + * - on_margin_requirement: `kind`, the position, `mark`, and the two + * numbers the kernel is ABOUT to compare (`equity`, `required`) — exactly + * what it would compare if the host answered PF_NATIVE_ANSWER_DEFAULT. + * - on_margin_call_units: the position being liquidated, the sizing `mark`, + * the `equity` and `required` measured there, and the cursor. `kind` and + * `liquidation_resting` are 0: a call is not a check point. */ +typedef struct pf_native_margin_view_v1 { + uint32_t struct_size; /**< sizeof(pf_native_margin_view_v1). */ + uint32_t version; /**< PF_NATIVE_API_VERSION. */ + uint32_t kind; /**< #pf_native_margin_check_kind_t. */ + uint32_t liquidation_resting; /**< 0/1: a liquidation rests from an earlier point. */ + double signed_units; /**< The book being measured. */ + double average_price; + uint64_t lot_count; + double mark; /**< The price the breach is measured at. */ + double equity; /**< Marked equity on the model's basis. */ + double required; /**< Maintenance requirement of the whole position at `mark`. */ + uint64_t cursor_ordinal; + int64_t cursor_effective_time_ms; + double cursor_t; + int32_t cursor_interval_index; + uint8_t cursor_provenance; /**< #pf_native_price_provenance_t. */ + uint8_t cursor_path_phase; /**< #pf_native_path_phase_t. */ + uint8_t reserved0[2]; +} pf_native_margin_view_v1; + +/** The host's answer to one requirement view — `NativeMarginDecision`. + * `required` and `equity` replace the kernel's two numbers for that check + * point only: they are a broker's money rule, never a second account. + * `force_breach` makes the kernel proceed past `required > equity` even when + * the answered numbers do not meet it. Read only when the hook answers + * #PF_NATIVE_ANSWER_PROVIDED; a nonfinite `required` or `equity` is refused + * by the kernel and the check point is abandoned, exactly as in C++. */ +typedef struct pf_native_margin_decision_v1 { + uint32_t struct_size; /**< sizeof(pf_native_margin_decision_v1). */ + uint32_t version; /**< PF_NATIVE_API_VERSION. */ + uint32_t force_breach; /**< 0/1. */ + uint32_t reserved0; + double required; + double equity; +} pf_native_margin_decision_v1; + +/** The facts a host-sized CLOSE is resolved from, handed to + * #pf_native_callbacks_v1::on_close_units. + * + * It is the UNITS half of `NativeExecutionTermsFacts`, and only that half: + * `scope_exposure_units` is what the bound scope holds at this candidate — + * for a cohort close, the live units of the roster's own openings and + * nothing else. The price half and the opening shape stay the kernel's. */ +typedef struct pf_native_close_view_v1 { + uint32_t struct_size; /**< sizeof(pf_native_close_view_v1). */ + uint32_t version; /**< PF_NATIVE_API_VERSION. */ + uint64_t incarnation; /**< The request being resolved. */ + double scope_exposure_units; /**< What the bound scope holds here. */ + double default_resolved_price; /**< The price the kernel would settle at. */ + double position_units; /**< The whole physical book, signed. */ + uint64_t cursor_ordinal; + int64_t cursor_effective_time_ms; + uint32_t is_buy; /**< 0/1: the side this candidate would trade on. */ + uint32_t reserved0; +} pf_native_close_view_v1; + +/** One closing lot's booking facts — `ClosedLotExcursionFacts` (RULING A48). + * Handed to #pf_native_callbacks_v1::on_lot_excursion, whose two outputs are + * the favorable and adverse magnitudes the closing row then carries. Facts + * in, magnitudes out: nothing about the host's price model crosses the + * boundary in either direction. The two entry-bar masks are what + * #strategy_native_declare_opened_lot_entry_bar_mask_v1 derived for the lot, + * and 0 for a lot nobody declared. */ +typedef struct pf_native_lot_excursion_v1 { + uint32_t struct_size; /**< sizeof(pf_native_lot_excursion_v1). */ + uint32_t version; /**< PF_NATIVE_API_VERSION. */ + uint64_t entry_incarnation; /**< The lot's opening request. */ + int64_t entry_time_ms; + double entry_price; + double lot_qty; /**< The whole lot. */ + double closed_qty; /**< What this fill closes of it. */ + double fill_price; + double carried_favorable; /**< What the lot carried in, for a partial close. */ + double carried_adverse; + int32_t entry_bar_index; + int32_t exit_bar_index; + uint8_t is_long; + uint8_t entry_bar_high_masked; /**< The entry bar's high is not this trade's. */ + uint8_t entry_bar_low_masked; /**< The entry bar's low is not this trade's. */ + uint8_t reserved0; +} pf_native_lot_excursion_v1; + +/** The facts of one matching candidate, handed to + * #pf_native_callbacks_v1::on_execution_terms — the price half of + * `NativeExecutionTermsFacts`. Every field is what the kernel measured at + * this candidate; `default_resolved_price` is the price it would settle at + * (slippage and the grid applied), i.e. what the host answers DEFAULT to. */ +typedef struct pf_native_terms_view_v1 { + uint32_t struct_size; /**< sizeof(pf_native_terms_view_v1). */ + uint32_t version; /**< PF_NATIVE_API_VERSION. */ + uint64_t incarnation; /**< The request being resolved. */ + uint32_t intent; /**< #pf_native_intent_t of that request. */ + uint32_t trigger; /**< #pf_native_trigger_t of that request. */ + uint32_t trigger_state; /**< #pf_native_trigger_state_t. */ + uint32_t remaining_kind; /**< #pf_native_remaining_t. */ + double remaining_units; /**< Valid when `remaining_kind` is UNITS. */ + uint32_t driver_class; /**< #pf_native_driver_class_t. */ + uint32_t price_kind; /**< #pf_native_candidate_price_t. */ + uint32_t quote_kind; /**< #pf_native_quote_kind_t. */ + uint32_t price_rule; /**< #pf_native_price_rule_t. */ + uint32_t is_buy; /**< 0/1: the side this candidate trades on. */ + uint32_t shared_cursor_collision; /**< 0/1: a rounded quote shared with another level. */ + double raw_price; /**< The path price the match was found at. */ + double trigger_level; /**< The level crossed, when `has_trigger_level`. */ + uint32_t has_trigger_level; /**< 0/1. */ + uint32_t reserved0; /**< Always 0. */ + double default_resolved_price; /**< The kernel's own settlement price. */ + double scope_exposure_units; /**< What the bound scope holds here. */ + double position_units; /**< The whole physical book, signed. */ + double position_average_price; + uint64_t position_lot_count; + double opposite_book_units; + double pending_group_deduction; + int64_t fx_effective_time_ms; /**< Where the account rate is read. */ + double active_fx; /**< The account rate there. */ + uint64_t cursor_ordinal; + int64_t cursor_effective_time_ms; + double cursor_t; + int32_t cursor_interval_index; + uint8_t cursor_provenance; /**< #pf_native_price_provenance_t. */ + uint8_t cursor_path_phase; /**< #pf_native_path_phase_t. */ + uint8_t reserved1[2]; /**< Always 0. */ +} pf_native_terms_view_v1; + +/** The price half of the terms #pf_native_callbacks_v1::on_execution_terms + * answers — `ExecutionTerms` less its units, which + * #pf_native_callbacks_v1::on_close_units answers for a host-sized close. + * The runtime fills it with the kernel's default (`default_resolved_price`, + * TRANSACT, SNAP) before the call. */ +typedef struct pf_native_terms_v1 { + uint32_t struct_size; /**< sizeof(pf_native_terms_v1). */ + uint32_t version; /**< PF_NATIVE_API_VERSION. */ + double resolved_price; /**< The price to settle at. */ + uint32_t shape; /**< #pf_native_opening_shape_t. */ + uint32_t grid_policy; /**< #pf_native_grid_policy_t. */ +} pf_native_terms_v1; + +/** The last facts before one execution's physical effect, handed to + * #pf_native_callbacks_v1::on_precommit — `NativePrecommitView`: the plan, + * the prices, the settlement inspection, and the account the fill would + * leave (the projection's closed rows' P&L, borrowed for the call, in + * roster order). Offered once per ready attempt, never for an inspection, + * and only when the settlement is ready to apply. */ +typedef struct pf_native_precommit_view_v1 { + uint32_t struct_size; /**< sizeof(pf_native_precommit_view_v1). */ + uint32_t version; /**< PF_NATIVE_API_VERSION. */ + uint64_t incarnation; /**< The request about to execute. */ + uint32_t intent; /**< #pf_native_intent_t of that request. */ + uint32_t plan; /**< #pf_native_plan_t. */ + double plan_units; /**< See #pf_native_plan_t. */ + double raw_price; + double resolved_price; + double inspected_closed_units; + double inspected_opened_units; + double inspected_current_ticket; + uint32_t current; /**< 1 for #strategy_native_execute_current_v1. */ + uint32_t would_open; /**< 0/1: the fill opens a lot. */ + uint32_t incoming_short; /**< 0/1: the opened lot is short. */ + uint32_t reserved0; /**< Always 0. */ + double resulting_abs_units; + uint64_t resulting_lot_count; + double resulting_abs_notional; + double realized_balance; /**< After the fill, account currency. */ + double remaining_entry_cost; /**< Paid costs on the resulting book. */ + double marked_equity; /**< After the fill, at its price. */ + int64_t cycle_after; + double signed_units_after; + const double* closed_row_pnl; /**< `closed_row_count` values; borrowed. */ + uint64_t closed_row_count; + uint64_t cursor_ordinal; + int64_t cursor_effective_time_ms; + double cursor_t; + int32_t cursor_interval_index; + uint8_t cursor_provenance; /**< #pf_native_price_provenance_t. */ + uint8_t cursor_path_phase; /**< #pf_native_path_phase_t. */ + uint8_t reserved1[2]; /**< Always 0. */ +} pf_native_precommit_view_v1; + +/** One anchored leg's materialization, handed to + * #pf_native_callbacks_v1::on_anchored_level exactly once, before its armed + * event — `NativeAnchoredLevelView`. `kernel_level` is fill + offset after + * the anchor's own rounding: what the host answers DEFAULT to. */ +typedef struct pf_native_anchored_level_view_v1 { + uint32_t struct_size; /**< sizeof(pf_native_anchored_level_view_v1). */ + uint32_t version; /**< PF_NATIVE_API_VERSION. */ + uint64_t owner; /**< The request whose fill arms the leg. */ + uint64_t owner_applied_ordinal; /**< That fill's applied execution. */ + uint64_t owner_lot_incarnation; /**< The lot it opened. */ + double owner_fill_price; /**< Its resolved price. */ + uint64_t leg; /**< The anchored request. */ + uint32_t leg_side; /**< #pf_native_side_t the leg trades on. */ + uint32_t trigger; /**< #pf_native_anchored_trigger_t. */ + double offset; /**< The anchor's offset, price units. */ + double price_tick; /**< The run's tick, 0 when none. */ + double kernel_level; /**< The level the kernel would install. */ + uint64_t owner_cursor_ordinal; + int64_t owner_cursor_effective_time_ms; + double owner_cursor_t; + int32_t owner_cursor_interval_index; + uint8_t owner_cursor_provenance; /**< #pf_native_price_provenance_t. */ + uint8_t owner_cursor_path_phase; /**< #pf_native_path_phase_t. */ + uint8_t reserved0[2]; /**< Always 0. */ +} pf_native_anchored_level_view_v1; + +/** The run's generic risk ledger — the C spelling of `NativeRiskState` (L9). + * + * Every field is its zero for a run that declares no risk block, exactly as + * the C++ value is. `blocked` is whether openings are refused right now and + * `reason` names the limit that did it (valid only when `has_reason`); + * `day_ordinal` is the risk day the ledger is on, on the spec's own day + * basis, and is meaningful only when `has_day`. Observation only: reading it + * moves nothing. */ +typedef struct pf_native_risk_state_v1 { + uint32_t struct_size; /**< sizeof(pf_native_risk_state_v1). */ + uint32_t version; /**< PF_NATIVE_API_VERSION. */ + uint32_t blocked; /**< 0/1: openings are refused right now. */ + uint32_t has_reason; /**< 0/1: `reason` is meaningful. */ + uint32_t reason; /**< #pf_native_risk_limit_t that blocked. */ + uint32_t has_day; /**< 0/1: the ledger has reached a day. */ + uint32_t consecutive_loss_days; /**< Days that closed with a realized loss. */ + uint32_t reserved0; + int64_t day_ordinal; /**< The risk day, on the spec's day basis. */ + uint64_t fills_today; /**< Applied fills counted in that day. */ + double peak_equity; /**< Running peak of the marked equity. */ + double day_open_equity; /**< Equity the current day opened at. */ +} pf_native_risk_state_v1; + +/** One kernel-issued liquidation that filled, whole — the C spelling of + * `native_order::MarginCallEvent`, copied out by + * #strategy_native_margin_call_v1. + * + * It carries the margin facts of that fill, so a host reconstructs the + * outcome without recomputing the account: `mark` is the booked resolved + * price, `equity` and `required` are the marked equity and the maintenance + * requirement of the SURVIVING book at that price, `liquidation_price` is + * the level re-solved for what is left, and `position_before` / + * `position_after` are the signed book on either side of the reduction. + * `applied_ordinal` names the #PF_NATIVE_EVENT_APPLIED row that booked it; + * the cursor is the point it filled at. The #PF_NATIVE_EVENT_MARGIN_CALL row + * of the same `ordinal` carries a subset of these facts. */ +typedef struct pf_native_margin_call_v1 { + uint32_t struct_size; /**< sizeof(pf_native_margin_call_v1). */ + uint32_t version; /**< PF_NATIVE_API_VERSION. */ + uint64_t ordinal; /**< The call's event ordinal. */ + uint64_t incarnation; /**< The kernel's liquidation request. */ + uint64_t applied_ordinal; /**< The applied execution that booked it. */ + uint32_t side; /**< #pf_native_side_t of the liquidated position. */ + uint32_t reserved0; /**< Always 0. */ + double mark; /**< The booked resolved price. */ + double equity; /**< Marked equity of the surviving book at `mark`. */ + double required; /**< Maintenance requirement of the surviving book at `mark`. */ + double liquidation_price; /**< The level re-solved for what is left. */ + double units; /**< Units liquidated. */ + double position_before; /**< Signed book before the reduction. */ + double position_after; /**< Signed book after it. */ + uint64_t cursor_ordinal; + int64_t cursor_effective_time_ms; + double cursor_t; + int32_t cursor_interval_index; + uint8_t cursor_provenance; /**< #pf_native_price_provenance_t. */ + uint8_t cursor_path_phase; /**< #pf_native_path_phase_t. */ + uint8_t reserved1[2]; /**< Always 0. */ +} pf_native_margin_call_v1; + +/** One order request. Translated field by field into `native_order::Request`; + * it is never cast. Zero-initialise it, set `struct_size` and `version`, then + * set only the fields the chosen `intent` and `trigger` document. + * + * This struct has FIVE published layouts and the runtime accepts any of + * them: the base layout the L13 lane first shipped + * (#PF_NATIVE_REQUEST_V1_BASE_SIZE), that layout plus L7b's anchored-leg + * tail (#PF_NATIVE_REQUEST_V1_ANCHOR_SIZE), that plus L3b's sizing detail + * `size_price`, `reduce_basis` (#PF_NATIVE_REQUEST_V1_SIZING_SIZE), that + * plus E14's trail seed (`trail_best_seed`, `trail_has_best_seed`; + * #PF_NATIVE_REQUEST_V1_SEED_SIZE), and the current one, which appends the + * arm relation (`arm_first_match`, `arm_scope`). A caller compiled against + * an earlier layout keeps working unchanged and simply gets the later tails' + * defaults (RAW, WORKING, RESOLVED, AT_MATCH, no seed, AT_ARM_PRINT, + * OWNER_LOT). Any other `struct_size` is PF_NATIVE_E_STRUCT. Every tail is + * append-only: nothing above it moved. */ +typedef struct pf_native_request_v1 { + uint32_t struct_size; /**< sizeof(pf_native_request_v1). */ + uint32_t version; /**< PF_NATIVE_API_VERSION. */ + + /* Intent */ + uint32_t intent; /**< #pf_native_intent_t. */ + uint32_t reduce_size; /**< #pf_native_reduction_t, REDUCE only. */ + uint32_t reduce_claim; /**< #pf_native_scope_claim_t, SCOPE_FRACTION only. */ + uint32_t side; /**< #pf_native_side_t, SIZED only. */ + uint32_t size_basis; /**< #pf_native_size_basis_t, SIZED only. */ + uint32_t size_time; /**< #pf_native_size_time_t, SIZED only. */ + uint32_t grid_policy; /**< #pf_native_grid_policy_t, SIZED only. */ + uint32_t reserve_percent_fee; /**< 0/1, SIZED only. */ + double intent_value; /**< The intent's own scalar; see #pf_native_intent_t. */ + + /* Trigger */ + uint32_t trigger; /**< #pf_native_trigger_t. */ + uint32_t anchor; /**< #pf_native_anchor_t (L7). */ + double p1; /**< Limit/stop price, or trail offset. */ + double p2; /**< Stop-limit limit, or trail arm price. */ + double anchor_offset; /**< FROM_OWNER_FILL: signed offset. */ + uint8_t fill_through; /**< LIMIT only: market-if-touched. */ + uint8_t trail_offset_in_ticks; /**< TRAIL: p1 is a tick count, not a distance. */ + uint8_t trail_has_arm_price; /**< TRAIL: p2 is the arm price. */ + uint8_t anchor_offset_in_ticks; /**< FROM_OWNER_FILL: the offset is a tick count. */ + + /* Capacity */ + uint32_t capacity; /**< #pf_native_capacity_t. */ + double capacity_units; /**< POINT_BUDGET only. */ + + /* Owner */ + uint32_t owner; /**< #pf_native_owner_t. */ + uint32_t owner_n; /**< Length of `owner_incarnations`. */ + const uint64_t* owner_incarnations; /**< Borrowed for the call only. */ + int64_t owner_cycle; /**< BIND_OPENING / BIND_OPENINGS. */ + uint64_t cohort; /**< BIND_COHORT. */ + + /* Group */ + uint32_t group_kind; /**< #pf_native_group_t. */ + uint32_t group_effect; /**< #pf_native_group_effect_t. */ + uint64_t group_id; /**< MEMBER only. */ + int64_t group_cohort; /**< MEMBER only. */ + + /* Identity. Both borrowed for the call only; the kernel copies them. + * NULL is the empty string. */ + const char* label; + const char* comment; + + /* ── The additive anchored-leg tail (L7b). Read only when `struct_size` + * is the current sizeof; a caller sending the base layout stops at + * `comment` above and gets every default (RAW, WORKING). ── */ + uint32_t anchor_rounding; /**< #pf_native_anchor_rounding_t, FROM_OWNER_FILL only. */ + uint32_t visibility; /**< #pf_native_arm_visibility_t, WAIT_FOR_APPLIED only. */ + + /* ── The additive sizing-detail tail (L3b). Read only when `struct_size` + * is the current sizeof; a caller sending either earlier layout stops + * above and gets both defaults (RESOLVED, AT_MATCH), which is what every + * request accepted before this tail already resolved as. ── */ + uint32_t size_price; /**< #pf_native_size_price_t, SIZED only. */ + uint32_t reduce_basis; /**< #pf_native_scope_basis_t, SCOPE_FRACTION only. */ + + /* ── The additive trail-seed tail (E14). Read only when `struct_size` + * is the current sizeof; a caller sending any earlier layout stops above + * and gets no seed, which is what every trail accepted before this tail + * already rode. ── */ + double trail_best_seed; /**< TRAIL: where the running best starts — + * a floor on it, the favourable one of + * this level and the arm's own print. + * Absolute (an anchor moves the arm + * threshold, not this), finite and + * positive, else PF_NATIVE_E_REJECTED. + * It is a submission input: the working + * readback carries the live best, not the + * seed — see the asymmetry note above. */ + uint8_t trail_has_best_seed; /**< TRAIL: `trail_best_seed` is set. */ + uint8_t reserved2[7]; /**< Must be 0. The seed layout's own padding, + * named so the tail below starts where + * that layout's sizeof ended. */ + + /* ── The additive arm-relation tail (R5 lane F4). Read only when + * `struct_size` is the current sizeof; a caller sending any earlier + * layout stops above and keeps both defaults (AT_ARM_PRINT, OWNER_LOT), + * which is what every child accepted before this tail already armed as. + * A non-default value is legal under WAIT_FOR_APPLIED alone. ── */ + uint32_t arm_first_match; /**< #pf_native_arm_first_match_t. */ + uint32_t arm_scope; /**< #pf_native_arm_scope_t. */ +} pf_native_request_v1; + +/** Byte length of #pf_native_request_v1 as the L13 lane first published it, + * before the anchored-leg tail was appended. It is the offset of the first + * appended field, so it stays correct on every target this header builds + * for — it is not a literal. The runtime accepts this length as well as the + * current `sizeof`, which is what makes the tail additive rather than a + * layout break. */ +#define PF_NATIVE_REQUEST_V1_BASE_SIZE \ + ((uint32_t)offsetof(pf_native_request_v1, anchor_rounding)) + +/** Byte length of #pf_native_request_v1 with L7b's anchored-leg tail but + * without L3b's sizing-detail tail — the second of its five published + * layouts. Defined as the offset of the first field appended after it, for + * the same reason #PF_NATIVE_REQUEST_V1_BASE_SIZE is. */ +#define PF_NATIVE_REQUEST_V1_ANCHOR_SIZE \ + ((uint32_t)offsetof(pf_native_request_v1, size_price)) + +/** Byte length of #pf_native_request_v1 with L3b's sizing-detail tail but + * without E14's trail seed — the third of its five published layouts, and + * the `sizeof` every caller compiled before that seed existed sends. + * Defined as the offset of the first field appended after it, for the same + * reason #PF_NATIVE_REQUEST_V1_BASE_SIZE is. */ +#define PF_NATIVE_REQUEST_V1_SIZING_SIZE \ + ((uint32_t)offsetof(pf_native_request_v1, trail_best_seed)) + +/** Byte length of #pf_native_request_v1 with E14's trail seed but without + * the arm-relation tail — the fourth of its five published layouts, and the + * `sizeof` every caller compiled before that tail existed sends. The seed + * layout ended in padding after `trail_has_best_seed`; `reserved2` names + * exactly that padding, so the offset of the first arm field IS that sizeof + * on every target. */ +#define PF_NATIVE_REQUEST_V1_SEED_SIZE \ + ((uint32_t)offsetof(pf_native_request_v1, arm_first_match)) + +/** One declared higher-timeframe series of #pf_native_run_spec_ext_v1. + * + * A row is a series INSTANCE, not a period: several rows may carry the same + * `tf`, each delivered under its own index (the `subscription` argument of + * #pf_native_callbacks_v1::on_timeframe_bar). Same-period rows may not carry + * DIFFERENT `authoritative_bars`. + * + * `gaps` occupies the word this struct published as `reserved0`, which every + * layout required to be zero — so a caller that zero-fills the struct keeps + * PF_NATIVE_GAPS_HOLD, and the struct's size and field offsets are unchanged. + * A value outside either word's enumeration is PF_NATIVE_E_TAG. */ +typedef struct pf_native_subscription_v1 { + uint32_t struct_size; /**< sizeof(pf_native_subscription_v1). */ + uint32_t lookahead; /**< #pf_native_lookahead_t. */ + const char* tf; /**< Non-NULL timeframe literal. */ + const pf_bar_t* authoritative_bars; /**< Optional exchange bars; copied. */ + int32_t authoritative_n; /**< Length of `authoritative_bars`. */ + uint32_t gaps; /**< #pf_native_gaps_t. */ +} pf_native_subscription_v1; + +/** The run-specification fields #pf_native_run_spec_v1 predates. + * + * It is a companion to #pf_native_run_spec_v1, not a second configure step: + * the kernel configures a host exactly once, so both halves are handed to + * #strategy_configure_native_ext_v1 together and that call replaces + * #strategy_configure_native_v1 for a host that needs these fields. + * `present_mask` selects the blocks that are meaningful; an absent block + * keeps the kernel's own default, so an all-zero mask configures exactly + * 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. + * + * This struct has FIVE published layouts and the runtime accepts any of + * them: the base layout the L13 lane first shipped + * (#PF_NATIVE_RUN_SPEC_EXT_V1_BASE_SIZE); that layout plus L9's `risk_*` + * tail (#PF_NATIVE_RUN_SPEC_EXT_V1_RISK_SIZE); that one plus N8's intrabar + * path, the four feed-shape and presentation policies, and the margin + * model's equity basis, level base and liquidation strings + * (#PF_NATIVE_RUN_SPEC_EXT_V1_POLICY_SIZE); that one plus the + * `auxiliary_*` tail (#PF_NATIVE_RUN_SPEC_EXT_V1_AUXILIARY_SIZE); and the + * current one, which appends the `event_retention` tail after them. A + * caller compiled against an earlier layout keeps working unchanged and + * simply cannot set the mask bits its struct has no fields for + * (#PF_NATIVE_SPEC_EXT_RISK, #PF_NATIVE_SPEC_EXT_INTRABAR, + * #PF_NATIVE_SPEC_EXT_FEED_POLICY, #PF_NATIVE_SPEC_EXT_AUXILIARY_FEED, + * #PF_NATIVE_SPEC_EXT_EVENT_RETENTION): doing so is PF_NATIVE_E_STRUCT. Any + * other `struct_size` is PF_NATIVE_E_STRUCT too. Every tail is append-only: + * nothing above them moved. */ +typedef struct pf_native_run_spec_ext_v1 { + uint32_t struct_size; /**< sizeof(pf_native_run_spec_ext_v1). */ + uint32_t version; /**< PF_NATIVE_API_VERSION. */ + uint32_t present_mask; /**< #pf_native_spec_ext_mask_t bits. */ + + uint32_t report_policy; /**< #pf_native_report_policy_t. */ + uint32_t report_open_position_at_end; /**< 0/1; #PF_NATIVE_REPORT_KERNEL_RECORDED only. */ + + uint32_t price_grid; /**< #pf_native_price_grid_t. */ + uint32_t grid_rounding; /**< #pf_native_grid_rounding_t. */ + + uint32_t calculation; /**< #pf_native_calc_trigger_t. */ + uint32_t max_recalculations_per_point; /**< Fill-cascade bound; 0 is legal. */ + + uint32_t open_bar_view; /**< #pf_native_open_bar_view_t. */ + + uint32_t margin_sizing; /**< #pf_native_liquidation_sizing_t. */ + uint32_t margin_check; /**< #pf_native_liquidation_check_t. */ + uint32_t margin_has_maintenance_long; + uint32_t margin_has_maintenance_short; + uint32_t margin_has_min_units; + double margin_initial_long; /**< 0 = maintenance-only side: no kernel + opening requirement, host owns + admission; legal only with that + side's maintenance set. */ + double margin_initial_short; /**< 0 = maintenance-only; see above. */ + double margin_maintenance_long; + double margin_maintenance_short; + double margin_shortfall_multiple; /**< The C++ default is 1.0, not 0: a + * PRESENT block has every field read, + * so a zero-filled struct must still + * write it — including under a sizing + * policy that never scales. */ + double margin_min_units; + + const pf_native_subscription_v1* subscriptions; /**< Borrowed for the call. */ + uint32_t subscriptions_n; + uint32_t reserved0; + + /* ── The additive risk tail (L9). Read only when `present_mask` carries + * PF_NATIVE_SPEC_EXT_RISK; a caller sending the base layout stops at + * `reserved0` above. Each limit is opt-in through its own `has_` flag, + * and a block whose four flags are all 0 is a declared-but-empty block — + * which is exactly what the C++ `NativeRiskLimits{}` is. ── */ + uint32_t risk_has_max_drawdown; /**< 0/1. */ + uint32_t risk_max_drawdown_percent; /**< 0/1: the value is a percent of the + * running equity peak, out of 100. */ + double risk_max_drawdown; /**< Threshold; account currency unless percent. */ + uint32_t risk_has_max_intraday_loss; /**< 0/1. */ + uint32_t risk_max_intraday_loss_percent; /**< 0/1: percent of the day's opening equity. */ + double risk_max_intraday_loss; /**< Threshold; account currency unless percent. */ + uint32_t risk_has_max_consecutive_loss_days; /**< 0/1. */ + uint32_t risk_max_consecutive_loss_days; /**< Days, when the flag is 1. */ + uint32_t risk_has_max_fills_per_day; /**< 0/1. */ + uint32_t risk_max_fills_per_day; /**< Applied fills, when the flag is 1. */ + uint32_t risk_day_basis; /**< #pf_native_risk_day_t. */ + uint32_t risk_action; /**< #pf_native_risk_action_t. */ + + /* ── The additive intrabar / policy tail (N8). Read only when + * `struct_size` is the current sizeof; a caller sending either earlier + * layout stops at `risk_action` above and keeps every kernel default. + * The two blocks below have mask bits of their own; the four margin + * fields at the end extend the EXISTING PF_NATIVE_SPEC_EXT_MARGIN block + * and are read only when that bit is set AND this tail is present. ── */ + uint32_t intrabar_kind; /**< #pf_native_intrabar_kind_t. */ + int32_t intrabar_samples; /**< Samples per script bar; LOWER_TF and SYNTHESIZED. */ + uint32_t intrabar_distribution; /**< #pf_magnifier_distribution_t. */ + uint32_t intrabar_volume_weighted; /**< 0/1. */ + int32_t intrabar_volume_weighted_min_samples; + int32_t intrabar_volume_weighted_max_samples; + uint32_t intrabar_sample_eligibility; /**< #pf_native_sample_eligibility_t, LOWER_TF only. */ + int32_t intrabar_n; /**< Length of `intrabar_bars`; LOWER_TF only. */ + const char* intrabar_tf; /**< The finer timeframe; LOWER_TF only, non-NULL. */ + const pf_bar_t* intrabar_bars; /**< The finer feed; borrowed for the call, copied. */ + + uint32_t slot_label_policy; /**< #pf_native_slot_label_t. FEED_TOLERANT keeps + * the caller's own labels, and a + * #PF_NATIVE_INTRABAR_LOWER_TF path then + * delivers no sub-bar: its bars are not + * keyed to canonical input slots. */ + uint32_t feed_tolerance; /**< #pf_native_feed_tolerance_t bits. */ + uint32_t path_order; /**< #pf_native_path_order_t. */ + uint32_t abort_reporting; /**< #pf_native_abort_reporting_t. */ + + uint32_t margin_equity_basis; /**< #pf_native_margin_equity_basis_t. */ + uint32_t margin_level_base; /**< #pf_native_margin_level_base_t. */ + const char* margin_liquidation_label; /**< Ticket of a kernel liquidation; NULL is "". */ + const char* margin_liquidation_comment; /**< Comment of the same; NULL is "". */ + + /* ── The additive auxiliary-feed tail. Read only when `present_mask` + * carries PF_NATIVE_SPEC_EXT_AUXILIARY_FEED; a caller sending an earlier + * layout stops at `margin_liquidation_comment`, `risk_action` or + * `reserved0` above. The feed is the + * run's own symbol at a timeframe strictly finer than the input, routed + * by time into the series that name it (`NativeAuxiliaryFeed`). ── */ + const char* auxiliary_tf; /**< Non-NULL feed timeframe literal. */ + const pf_bar_t* auxiliary_bars; /**< Strictly increasing bars; copied. May be + * NULL when `auxiliary_n` is 0. */ + int32_t auxiliary_n; /**< Length of `auxiliary_bars`. */ + uint32_t reserved1; /**< Must be 0. */ + /** Optional, borrowed for the call: `subscriptions_n` entries of + * #pf_native_series_source_t, one per row of `subscriptions`. NULL means + * every series is built from the input. Meaningful only together with + * PF_NATIVE_SPEC_EXT_SUBSCRIPTIONS. */ + const uint32_t* subscription_sources; + + /* ── The additive event-retention tail (V19-B). Read only when + * `present_mask` carries PF_NATIVE_SPEC_EXT_EVENT_RETENTION; a caller + * sending an earlier layout stops at `subscription_sources` or above and + * runs under PF_NATIVE_EVENT_RETENTION_FULL. ── */ + uint32_t event_retention; /**< #pf_native_event_retention_t. */ + uint32_t reserved2; /**< Must be 0. */ +} pf_native_run_spec_ext_v1; + +/** Byte length of #pf_native_run_spec_ext_v1 as the L13 lane first published + * it, before the `risk_*` tail was appended. It is the offset of the first + * appended field, so it stays correct on every target this header builds for + * — it is not a literal. The runtime accepts this length as well as the + * current `sizeof`, which is what makes the tail additive rather than a + * layout break. */ +#define PF_NATIVE_RUN_SPEC_EXT_V1_BASE_SIZE \ + ((uint32_t)offsetof(pf_native_run_spec_ext_v1, risk_has_max_drawdown)) + +/** Byte length of #pf_native_run_spec_ext_v1 with L9's risk tail but without + * N8's intrabar / policy tail — the second of its four published layouts. */ +#define PF_NATIVE_RUN_SPEC_EXT_V1_RISK_SIZE \ + ((uint32_t)offsetof(pf_native_run_spec_ext_v1, intrabar_kind)) + +/** Byte length of #pf_native_run_spec_ext_v1 with N8's intrabar / policy tail + * but before the `auxiliary_*` tail was appended — the third of its four + * published layouts. It is the offset of the first auxiliary field, for the + * same reason #PF_NATIVE_RUN_SPEC_EXT_V1_BASE_SIZE is an offset. */ +#define PF_NATIVE_RUN_SPEC_EXT_V1_POLICY_SIZE \ + ((uint32_t)offsetof(pf_native_run_spec_ext_v1, auxiliary_tf)) + +/** Byte length of #pf_native_run_spec_ext_v1 with the `auxiliary_*` tail but + * before the `event_retention` tail was appended — the fourth of its five + * published layouts, an offset for the same reason + * #PF_NATIVE_RUN_SPEC_EXT_V1_BASE_SIZE is. */ +#define PF_NATIVE_RUN_SPEC_EXT_V1_AUXILIARY_SIZE \ + ((uint32_t)offsetof(pf_native_run_spec_ext_v1, event_retention)) + +/** The C host's strategy logic. + * + * Every entry may be NULL, which is exactly the C++ default: the kernel does + * nothing for that hook. `user` is handed back unchanged to every callback. + * A callback must never let an exception, a longjmp or any other non-local + * exit escape. + * + * There are two classes of entry, and they read their return value + * differently. An OBSERVATION callback — everything down to and including + * `on_sub_bar` — returns 0 to continue; any other value ends the run Failed + * with PF_NATIVE_FAILURE_CALLBACK. An ANSWERING callback — every hook from + * `on_margin_requirement` on — returns a #pf_native_answer_t selecting + * WHOSE answer the kernel uses; every value is in contract, so an answering + * hook can never fail the run. That split is deliberate: the answering hooks + * are consulted from kernel paths that are not inside the callback guard, so + * a failure raised there could not be latched without unwinding through + * them. A host that must abort does it from an observation callback. + * + * Commands are legal inside `on_bar_open`, `on_bar`, `on_tick` and + * `on_applied`. `on_run_begin`, `on_input`, `on_timeframe_bar` and + * `on_margin_call` are observation-only: a command there answers + * PF_NATIVE_E_STATE and changes nothing. + * #strategy_native_declare_opened_lot_entry_bar_mask_v1 is legal inside + * `on_applied` alone. + * + * `on_bar` is also the recalculation hook: with a calculation trigger above + * #PF_NATIVE_CALC_TRIGGER_BAR_CLOSE the kernel calls it again at each fill + * cursor or modeled point, which is exactly what the C++ + * `on_native_recalculate` default does. */ +typedef struct pf_native_callbacks_v1 { + uint32_t struct_size; /**< sizeof(pf_native_callbacks_v1). */ + uint32_t version; /**< PF_NATIVE_API_VERSION. */ + void* user; /**< Opaque; handed back unchanged. */ + int (*on_run_begin)(void* user); + int (*on_input)(void* user, const pf_bar_t* bar, int32_t input_index, + int32_t completes_script_interval); + int (*on_bar_open)(void* user, const pf_bar_t* bar, const pf_native_decision_v1* at); + int (*on_bar)(void* user, const pf_bar_t* bar, const pf_native_decision_v1* at); + int (*on_tick)(void* user, const pf_bar_t* bar, const pf_native_decision_v1* at); + int (*on_applied)(void* user, const pf_native_applied_v1* applied, + const pf_native_decision_v1* at); + /** One completed bucket of a declared series. `completion` is a + * #pf_native_completion_kind_t: CONFIRMED when the bucket's own last + * contributing bar closed it, LAZY_COMPLETE when the next period's first + * input did. */ + int (*on_timeframe_bar)(void* user, const pf_bar_t* bar, uint32_t subscription, + uint32_t completion, int64_t delivered_at_ms); + int (*on_margin_call)(void* user, const pf_native_event_v1* margin_call); + + /* ── The additive hook tail. Read only when `struct_size` is the current + * sizeof; a caller sending the base layout stops at `on_margin_call` + * above and gets exactly the kernel's own defaults for all six, which is + * what every host compiled before this tail already had. ── */ + /** EVERY calculation of the run, including the script bar's own close — + * `on_native_recalculate`. `reason` is a #pf_native_calc_reason_t and + * `cause` is the applied execution of an ORDER_FILL recalculation, valid + * only for that call and NULL otherwise. `bar` is the COMPLETE script + * bar even mid-path; #strategy_native_partial_bar_v1 is the + * lookahead-free bar so far. Commands are legal here. + * + * Installing it REPLACES `on_bar` for every calculation, exactly as + * overriding `on_native_recalculate` replaces the C++ default forwarding: + * a host that wants both calls `on_bar` itself from here. Leaving it + * NULL keeps the established contract, where the kernel forwards every + * calculation to `on_bar`. Observation callback: non-zero ends the run. */ + int (*on_recalculate)(void* user, const pf_bar_t* bar, const pf_native_decision_v1* at, + uint32_t reason, const pf_native_applied_v1* cause); + + /** One completed lower-timeframe sub-bar of a run that retains a lower + * feed — `on_native_sub_bar`. Delivered + * after that sub-bar's whole matching path and before the next one's; + * never called for a run with no retained lower feed. The decision point + * is the sub-bar's last modeled point, so commands and + * #strategy_native_execute_current_v1 are legal. + * Observation callback: non-zero ends the run. */ + int (*on_sub_bar)(void* user, const pf_bar_t* sub, const pf_native_decision_v1* at); + + /** The two numbers one check point is about to compare — + * `resolve_margin_requirement`. ANSWERING callback: return + * #PF_NATIVE_ANSWER_DEFAULT to keep the kernel's own, any other value to + * use @p out. The kernel keeps the whole mechanism — the level solve, + * the check points, its own request, the receipt, `on_margin_call`; this + * supplies only the money rule brokers legitimately differ on. */ + int (*on_margin_requirement)(void* user, const pf_native_margin_view_v1* view, + pf_native_margin_decision_v1* out); + + /** Whether this kernel check point is one the host's broker model shares + * — `margin_check_allowed`. ANSWERING callback: return + * #PF_NATIVE_ANSWER_DEFAULT to admit the point (the kernel's own + * answer), any other value to use @p allowed (0 suppresses it). A + * suppressed point is not evaluated, re-armed or withdrawn: the margin + * state is left exactly as the last admitted point left it. */ + int (*on_margin_check)(void* user, const pf_native_margin_view_v1* at, int32_t* allowed); + + /** The size of a kernel-issued liquidation, before it rests — + * `resolve_margin_call_units`. ANSWERING callback: return + * #PF_NATIVE_ANSWER_DEFAULT to keep the run spec's sizing policy, any + * other value to use @p units, which the kernel clamps into (0, held]. + * It has the last word on units, including over a forced breach. */ + int (*on_margin_call_units)(void* user, const pf_native_margin_view_v1* view, + double* units); + + /** The favorable and adverse magnitudes of one closing lot — + * `closed_lot_excursion`. Installing it at all is + * `owns_lot_excursions() == true`: the consumer then stops sampling + * excursion at matched trigger prices for the WHOLE run and every + * closing row takes both magnitudes from here. ANSWERING callback: + * return #PF_NATIVE_ANSWER_DEFAULT to answer the kernel's own zero + * magnitudes — which, ownership having been declared, is what a declined + * lot gets — or any other value to use @p favorable and @p adverse. */ + int (*on_lot_excursion)(void* user, const pf_native_lot_excursion_v1* facts, + 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. + * 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 + * callback: return #PF_NATIVE_ANSWER_DEFAULT to keep that default, any + * other value to close @p units of + * #pf_native_close_view_v1::scope_exposure_units. */ + int (*on_close_units)(void* user, const pf_native_close_view_v1* view, double* units); + + /* ── The additive policy-hook tail (R5 lane F4). Read only when + * `struct_size` is the current sizeof; a caller sending an earlier layout + * stops at `on_close_units` (or at `on_margin_call`) and gets the + * kernel's own answers for all four, exactly the C++ defaults. All four + * are ANSWERING callbacks. ── */ + + /** The price half of `resolve_execution_terms`, consulted at EVERY + * matching candidate once installed. ANSWERING callback: return + * #PF_NATIVE_ANSWER_DEFAULT to keep the kernel's terms, any other value + * to settle at @p out's price, shape and grid policy. @p out arrives + * holding the kernel's own. A shape or grid word outside its enumeration + * is the kernel's #PF_NATIVE_MATCH_REJECT_INVALID_TERMS, as is a shape + * other than TRANSACT on anything but an opening. The units half stays + * `on_close_units`'. */ + int (*on_execution_terms)(void* user, const pf_native_terms_view_v1* view, + pf_native_terms_v1* out); + + /** The last gate before one execution's physical effect — + * `validate_execution_precommit`. ANSWERING callback: return + * #PF_NATIVE_ANSWER_DEFAULT to admit it on the kernel's own path, any + * other value to use @p verdict (#pf_native_precommit_verdict_t). A + * verdict word outside the enumeration refuses, the conservative + * reading of an answer the kernel cannot act on. */ + int (*on_precommit)(void* user, const pf_native_precommit_view_v1* view, + uint32_t* verdict); + + /** Where an anchored leg is armed — `resolve_anchored_level`. ANSWERING + * callback: return #PF_NATIVE_ANSWER_DEFAULT to install the kernel's + * level, any other value to install @p level; the kernel's + * representability check still applies. */ + int (*on_anchored_level)(void* user, const pf_native_anchored_level_view_v1* view, + double* level); + + /** The host's own durable state, folded into the broker-state hash — + * `hash_host_extension`. Called once per hash (every per-bar row, the + * final and the stream hash) after the kernel's own bytes. ANSWERING + * callback: return #PF_NATIVE_ANSWER_DEFAULT to fold nothing more, any + * other value to fold a domain tag and @p digest, a 64-bit digest of + * whatever the host's next decision depends on. It must be a pure + * function of that state. The continuation hash is the kernel's alone + * and does not move. */ + int (*on_hash_extension)(void* user, uint64_t* digest); +} pf_native_callbacks_v1; + +/** Byte length of #pf_native_callbacks_v1 as the L13 lane first published it, + * before the six-hook tail was appended. It is the offset of the first + * appended field, so it stays correct on every target this header builds for + * — it is not a literal. #strategy_native_host_create_v1 accepts this length + * as well as #PF_NATIVE_CALLBACKS_V1_HOOKS_SIZE and the current `sizeof`, + * which is what makes each tail additive rather than a layout break. */ +#define PF_NATIVE_CALLBACKS_V1_BASE_SIZE \ + ((uint32_t)offsetof(pf_native_callbacks_v1, on_recalculate)) + +/** Byte length of #pf_native_callbacks_v1 with the six-hook tail but without + * the policy-hook tail — the second of its three published layouts, and the + * `sizeof` every caller compiled before that tail existed sends. */ +#define PF_NATIVE_CALLBACKS_V1_HOOKS_SIZE \ + ((uint32_t)offsetof(pf_native_callbacks_v1, on_execution_terms)) + +/** @} */ /* end of pf_native_c_types */ + +/** @defgroup pf_native_c_api Entry points + * @{ */ + +/** This header's layout version. Mirrors #strategy_stream_api_version. */ +PF_API int strategy_native_api_version(void); + +/** Allocate a native host that forwards every kernel callback to @p callbacks. + * + * The table is copied; the caller's struct need not outlive the call. The + * returned handle is a `pf_strategy_t` the existing runtime symbols accept: + * #strategy_configure_native_v1, the whole `strategy_stream_*` family and the + * read-only accessors all take it unchanged. + * + * @return The handle, or NULL for a NULL/mis-sized table or on allocation + * failure. Release it with #strategy_native_host_free — never + * #strategy_free, which does not own this allocation. */ +PF_API pf_strategy_t strategy_native_host_create_v1(const pf_native_callbacks_v1* callbacks); + +/** Release a handle from #strategy_native_host_create_v1. NULL is a no-op; + * a handle this API did not create is refused without freeing anything. */ +PF_API void strategy_native_host_free(pf_strategy_t s); + +/** Run @p n bars as one batch and fill @p out. + * + * The specification must already be Ready (#strategy_configure_native_v1). + * @p out may be NULL to skip reporting; otherwise its arrays are + * heap-allocated and released by #strategy_native_report_free_v1. + * @return PF_NATIVE_OK when the run reached Completed, PF_NATIVE_E_RUN_FAILED + * when it did not (read #strategy_native_state_v1 for the code). */ +PF_API int strategy_native_run_v1(pf_strategy_t s, const pf_bar_t* bars, int n, + pf_report_t* out); + +/** Free the heap arrays inside a report filled by #strategy_native_run_v1. + * + * The unprefixed #report_free is a per-strategy export the transpiler emits, + * so it is absent from a runtime a C host links on its own: this is that + * host's release path. Idempotent; NULL is a no-op. The `pf_report_t` struct + * itself stays caller-owned. */ +PF_API void strategy_native_report_free_v1(pf_report_t* report); + +/** Submit @p request. + * + * Legal from inside a native callback; the request joins the working book + * and is matched by the consumer from the next execution point on. The + * handle it answers with is the request's identity for the rest of the run: + * replace, cancel and every event carry it. + * + * @param s The host this run is driving, from #strategy_create. + * @param request Borrowed for the call only; the kernel copies what it + * keeps. + * @param incarnation Optional; receives the accepted request's handle. + * @param reject Optional; receives a #pf_native_reject_reason_t on + * rejection. + * @return PF_NATIVE_OK when accepted, PF_NATIVE_E_REJECTED when the kernel + * rejected it, PF_NATIVE_E_STATE when commands are not legal here. + * + * Exercised by `tests/test_native_c_api.c`. */ +PF_API int strategy_native_submit_v1(pf_strategy_t s, const pf_native_request_v1* request, + uint64_t* incarnation, uint32_t* reject); + +/** Replace the live request @p incarnation with @p request. + * + * Amendment, not cancel-and-resubmit: the predecessor leaves the book and + * the successor takes its place in one step, and the REPLACED event names + * both. A target that is no longer working is PF_NATIVE_E_NOT_WORKING. + * + * @param s The host this run is driving, from #strategy_create. + * @param incarnation The live request to amend, as #strategy_native_submit_v1 + * handed it back. + * @param request Borrowed for the call only. + * @param successor Optional; receives the successor's handle. + * @return PF_NATIVE_OK, PF_NATIVE_E_REJECTED, PF_NATIVE_E_NOT_WORKING, + * PF_NATIVE_E_INVALID_TARGET, or PF_NATIVE_E_STATE. + * + * PF_NATIVE_E_REJECTED is where this signature stops: it has nowhere to put + * the RequestRejectReason that #strategy_native_submit_v1 writes to its + * `reject`, so use #strategy_native_replace_ext_v1 when the reason matters. + * + * Exercised by `tests/test_native_c_api.c`. */ +PF_API int strategy_native_replace_v1(pf_strategy_t s, uint64_t incarnation, + const pf_native_request_v1* request, + uint64_t* successor); + +/** Replace the live request @p incarnation, reporting a rejection's reason. + * + * The same call as #strategy_native_replace_v1 — same legality, same + * amendment, same verdicts, same successor — with the `reject` + * out-parameter #strategy_native_submit_v1 has always had. That asymmetry + * was the whole gap: a C++ replace answers ReplaceResult::reason and the C + * submit writes it out, while the C replace dropped it, so a C host learned + * "rejected" and no more. (#strategy_native_cancel_v1 needs no such + * spelling: its status IS its reason.) + * + * It is an additional symbol rather than a wider signature because this + * header's STABILITY rule is that a signature never changes within a major + * version — the same reason #strategy_configure_native_ext_v1 exists beside + * #strategy_configure_native_v1. Use either; #strategy_native_replace_v1 + * stays exactly what it was and is this call with @p reject NULL. + * + * @param s The host this run is driving, from #strategy_create. + * @param incarnation The live request to amend. + * @param request Borrowed for the call only. + * @param successor Optional; receives the successor's handle. Untouched + * unless the replace is accepted. + * @param reject Optional; receives a #pf_native_reject_reason_t, and only + * on PF_NATIVE_E_REJECTED. Untouched otherwise — including + * for NOT_WORKING and INVALID_TARGET, which are the + * target's verdict and not the request's. + * @return PF_NATIVE_OK, PF_NATIVE_E_REJECTED, PF_NATIVE_E_NOT_WORKING, + * PF_NATIVE_E_INVALID_TARGET, or PF_NATIVE_E_STATE. + * + * Exercised by `tests/test_native_c_api.c`. */ +PF_API int strategy_native_replace_ext_v1(pf_strategy_t s, uint64_t incarnation, + const pf_native_request_v1* request, + uint64_t* successor, uint32_t* reject); + +/** Cancel one live request. + * @return PF_NATIVE_OK, PF_NATIVE_E_NOT_WORKING, PF_NATIVE_E_INVALID_TARGET + * or PF_NATIVE_E_STATE. */ +PF_API int strategy_native_cancel_v1(pf_strategy_t s, uint64_t incarnation); + +/** Cancel every live request, dependants included. + * @return The number cancelled (>= 0), or PF_NATIVE_E_STATE. */ +PF_API int strategy_native_cancel_all_v1(pf_strategy_t s); + +/** Cancel exactly the live requests whose @p field equals @p text. + * + * The C spelling of `NativeStrategyHost::cancel_where`. With + * #PF_NATIVE_FIELD_LABEL it is the one call that withdraws every live + * request a host issued under one of its own order ids; with + * #PF_NATIVE_FIELD_COMMENT it is the established comment predicate. A + * dependant of a cancelled owner still leaves the book, but it is counted + * only when its own field matched. Text that matches nothing is not a + * command. + * + * @param s The host this run is driving, from #strategy_create. + * @param text Borrowed for the call only; "" matches the requests that + * carry no such text. NULL is PF_NATIVE_E_ARGUMENT, not "". + * @param field #pf_native_request_field_t. + * @return The number cancelled (>= 0), PF_NATIVE_E_TAG for a field outside + * the enumeration, PF_NATIVE_E_ARGUMENT for a NULL @p text, or another + * negative status. + * + * Exercised by `tests/test_native_c_api.c`. */ +PF_API int strategy_native_cancel_where_v1(pf_strategy_t s, const char* text, + uint32_t field); + +/** Execute one live request at the current execution point. + * + * Legal only inside a callback the kernel has opened an execution point for + * (`on_bar`, `on_bar_open`, `on_tick`, `on_applied`, `on_recalculate`); + * anywhere else the answer is PF_NATIVE_E_STATE. The request is filled at + * this cursor rather than waiting for the consumer's own matching pass. + * + * @param s The host this run is driving, from #strategy_create. + * @param incarnation The live request to execute, as + * #strategy_native_submit_v1 handed it back. A handle + * that is not in the working book is + * PF_NATIVE_E_INVALID_TARGET. + * @param price_rule #pf_native_price_rule_t. + * @param refusal Optional; receives a #pf_native_refusal_t when the + * return is PF_NATIVE_E_REFUSED. + * @return A non-negative #pf_native_execute_outcome_t, or a negative status. + * + * Exercised by `tests/test_native_c_api.c`. */ +PF_API int strategy_native_execute_current_v1(pf_strategy_t s, uint64_t incarnation, + uint32_t price_rule, uint32_t* refusal); + +/** Read the physical position. Every output is optional. + * @return PF_NATIVE_OK, or a negative status. */ +PF_API int strategy_native_position_v1(pf_strategy_t s, double* signed_units, + double* average_price, uint64_t* lots); + +/** Snapshot the live working book and return its length. + * + * The snapshot is retained on the handle: #strategy_native_working_get_v1 + * reads from it, so a row already copied out is not invalidated by a later + * command. The next call to this function replaces the snapshot. + * @return The row count (>= 0), or a negative status. */ +PF_API int strategy_native_working_len_v1(pf_strategy_t s); + +/** Copy row @p index of the most recent working snapshot into @p out. + * + * @p out is an in/out size prefix: set `out->struct_size` to + * `sizeof(pf_native_working_v1)` before the call. Everything else is filled. + * A caller built against an earlier layout sends + * #PF_NATIVE_WORKING_V1_BASE_SIZE or #PF_NATIVE_WORKING_V1_ARM_SIZE and is + * filled exactly that far. + * @return PF_NATIVE_OK, PF_NATIVE_E_ARGUMENT for an out-of-range index, + * PF_NATIVE_E_STRUCT for a mis-sized row, or another negative status. */ +PF_API int strategy_native_working_get_v1(pf_strategy_t s, int index, + pf_native_working_v1* out); + +/** Snapshot the open lots marked at @p mark and return their count. + * + * The C spelling of `NativeStrategyHost::native_open_lots(mark)` (R5 gap + * lane N18). The snapshot is retained on the handle: + * #strategy_native_open_lot_get_v1 reads from it, so a row already copied + * out is not invalidated by a later command; the next call to this function + * replaces it. Observation only — it moves no fill, no hash and no row — and + * legal wherever #strategy_native_position_v1 is. A NaN @p mark keeps every + * booking fact and leaves `unrealized_pnl` NaN. + * @return The row count (>= 0), or a negative status. */ +PF_API int strategy_native_open_lot_count_v1(pf_strategy_t s, double mark); + +/** Copy row @p index of the most recent open-lot snapshot into @p out. + * + * @p out is an in/out size prefix: set `out->struct_size` to + * `sizeof(pf_native_open_lot_v1)` before the call. Everything else is filled. + * @return PF_NATIVE_OK, PF_NATIVE_E_ARGUMENT for an out-of-range index, + * PF_NATIVE_E_STRUCT for a mis-sized row, or another negative status. */ +PF_API int strategy_native_open_lot_get_v1(pf_strategy_t s, int index, + pf_native_open_lot_v1* out); + +/** Copy up to @p cap events with an ordinal strictly greater than + * @p after_ordinal into @p out, in the kernel's own recording order. + * + * Ordinals are non-decreasing rather than strictly increasing: an applied + * execution and the account observation it produced carry the same one. A + * page therefore never ends in the middle of such a group (for @p cap >= 2), + * so a poller advances by the last returned `ordinal` without losing or + * repeating a row. The rows are what the run's event retention keeps + * (#pf_native_event_retention_t): under FULL and COMMANDS the history is + * append-only, so the same @p after_ordinal always yields the same rows; + * under WINDOW a read answers what the window still holds, and one from + * below its start (#strategy_native_event_window_v1) starts there. + * @return The number written (>= 0), or a negative status. */ +PF_API int strategy_native_events_v1(pf_strategy_t s, uint64_t after_ordinal, + pf_native_event_v1* out, int cap); + +/** The host has read every event through @p through_ordinal — + * `native_acknowledge_events()`. + * + * Under the Window event retention the kernel drops the acknowledged command + * events at the next script-bar boundary and keeps every event above the + * acknowledgement; the first call also marks the host as one that polls, so + * a polling host calls it from `on_run_begin` (0 is legal). A later, smaller + * acknowledgement never lowers an earlier one, one above the event high + * water acknowledges the high water, and outside a running run the call + * records nothing. The Full and Commands retentions keep the whole journal + * whatever is acknowledged. + * @return PF_NATIVE_OK, or a negative status for a bad handle. */ +PF_API int strategy_native_acknowledge_events_v1(pf_strategy_t s, uint64_t through_ordinal); + +/** The oldest ordinal a read can still return — `native_event_window_start()` + * — written to @p out_first_ordinal: every command event at or above it is + * retained, and #strategy_native_events_v1 with an `after_ordinal` below it + * starts there. 1 while nothing was dropped. + * @return PF_NATIVE_OK, PF_NATIVE_E_ARGUMENT for a NULL @p out_first_ordinal, + * or another negative status. */ +PF_API int strategy_native_event_window_v1(pf_strategy_t s, uint64_t* out_first_ordinal); + +/** Read the run's lifecycle and typed failure into @p out. + * + * @p out is an in/out size prefix: set `out->struct_size` to + * `sizeof(pf_native_state_v1)` before the call. */ +PF_API int strategy_native_state_v1(pf_strategy_t s, pf_native_state_v1* out); + +/** Declare this run's higher-timeframe series from inside `on_run_begin` — + * `declare_timeframe_subscriptions()`. + * + * The list REPLACES the `subscriptions` staged by + * #strategy_configure_native_ext_v1; the kernel registers from the staged + * spec after the callback returns, so the run's continuation identity folds + * what actually ran. @p n may be 0 (with @p rows NULL), which declares no + * series at all. + * @return PF_NATIVE_OK when the list was staged. Each row is read first: + * PF_NATIVE_E_STRUCT for its `struct_size`, PF_NATIVE_E_ARGUMENT for a NULL + * `tf` or bad authoritative bars (and for a negative @p n or NULL @p rows), + * PF_NATIVE_E_TAG for a `lookahead` / `gaps` word outside its enumeration; + * then PF_NATIVE_E_STATE anywhere but inside `on_run_begin` and for a list this + * run's input timeframe would refuse — the same validation + * #strategy_configure_native_ext_v1 applies. A refusal stages nothing. + * #strategy_native_declare_subscriptions_ext_v1 also names which validation + * refused the list. */ +PF_API int strategy_native_declare_subscriptions_v1(pf_strategy_t s, + const pf_native_subscription_v1* rows, + int n); + +/** The same declaration, answered by name — `declare_timeframe_subscriptions_result()`. + * + * The same call as #strategy_native_declare_subscriptions_v1 — same + * legality, same staging, same statuses — with the refusal's words written + * out, and with a series source per row, so a series declared here may be + * built from the auxiliary feed #strategy_native_declare_auxiliary_feed_v1 + * staged. #strategy_native_declare_subscriptions_v1 stays exactly what it + * was: this call with @p sources, @p error and @p field NULL. + * + * @param s The host this run is driving. + * @param rows @p n rows, borrowed for the call; NULL when @p n is 0. + * @param n Row count (0 declares no series at all). + * @param sources Optional: @p n #pf_native_series_source_t words, one per + * row. NULL builds every series from the input. + * @param error Optional; receives a #pf_native_spec_error_t whenever the + * kernel judged the call: #PF_NATIVE_SPEC_ERROR_NONE when + * staged, #PF_NATIVE_SPEC_ERROR_WRONG_PHASE outside + * `on_run_begin`, else the first error the list's + * validation found — the validation + * #strategy_configure_native_ext_v1 applies. + * @param field Optional; receives the #pf_native_spec_field_t beside it. + * @return PF_NATIVE_OK when the list was staged; PF_NATIVE_E_STATE for every + * refusal the kernel names in @p error, in which case nothing is staged; + * PF_NATIVE_E_ARGUMENT / E_STRUCT / E_TAG for a row the C layer refuses + * before the kernel sees the list, leaving @p error and @p field untouched. + * + * Exercised by `tests/test_native_c_api.c`. */ +PF_API int strategy_native_declare_subscriptions_ext_v1(pf_strategy_t s, + const pf_native_subscription_v1* rows, + int n, const uint32_t* sources, + uint32_t* error, uint32_t* field); + +/** Declare, replace or withdraw this run's auxiliary finer feed from inside + * `on_run_begin` — `declare_auxiliary_feed_result()`. + * + * The feed REPLACES the one staged by #strategy_configure_native_ext_v1's + * auxiliary tail, and the kernel registers from the staged spec after the + * callback returns. It is judged together with the series staged at that + * moment, so a host that declares both here declares the feed first and its + * feed-built series second (#strategy_native_declare_subscriptions_ext_v1). + * + * @param s The host this run is driving. + * @param tf The feed's timeframe literal, or NULL to withdraw the staged + * feed (with @p n 0 and @p bars NULL). + * @param bars @p n strictly increasing bars, copied; NULL when @p n is 0. + * @param n Bar count. + * @param error Optional; receives a #pf_native_spec_error_t whenever the + * kernel judged the call (#PF_NATIVE_SPEC_ERROR_NONE when + * staged, #PF_NATIVE_SPEC_ERROR_WRONG_PHASE outside + * `on_run_begin`). + * @param field Optional; receives the #pf_native_spec_field_t beside it. + * @return PF_NATIVE_OK when the feed was staged or withdrawn; + * PF_NATIVE_E_STATE for every refusal the kernel names in @p error — a feed + * the input timeframe refuses, or a withdrawal that would strand a staged + * feed-built series — changing nothing; PF_NATIVE_E_ARGUMENT for a negative + * @p n, NULL @p bars with @p n > 0, or a withdrawal carrying bars, leaving + * @p error and @p field untouched. + * + * Exercised by `tests/test_native_c_api.c`. */ +PF_API int strategy_native_declare_auxiliary_feed_v1(pf_strategy_t s, const char* tf, + const pf_bar_t* bars, int32_t n, + uint32_t* error, uint32_t* field); + +/** The bar so far at the current cursor — `current_partial_bar()`. + * + * Open of the script bar's first modeled point, running high/low, close at + * the cursor; volume is the activity actually consumed so far. Valid in the + * bar-open, applied, tick, sub-bar and recalculation callbacks. + * @return PF_NATIVE_OK when @p out was written, #PF_NATIVE_ABSENT outside a + * path walk — including in the bar's own close calculation, where the + * callback already holds the complete bar — leaving @p out untouched. */ +PF_API int strategy_native_partial_bar_v1(pf_strategy_t s, pf_bar_t* out); + +/** How many recalculations the kernel drove, and how many it suppressed + * because a point had spent its `max_recalculations_per_point` budget — + * `native_recalculation_count()` / `native_recalculations_skipped()`. + * Either pointer may be NULL. Observation only. */ +PF_API int strategy_native_recalculations_v1(pf_strategy_t s, uint64_t* driven, + uint64_t* skipped); + +/** The trail projection of one live request — `trail_state()`. + * + * @p incarnation names a request this run issued. + * @return PF_NATIVE_OK when @p out was written, #PF_NATIVE_ABSENT when the + * handle is not a live Trail request (unknown, finished, or another + * trigger), leaving @p out untouched. */ +PF_API int strategy_native_trail_state_v1(pf_strategy_t s, uint64_t incarnation, + pf_native_trail_state_v1* out); + +/** The latest completed bucket of a declared subscription — + * `native_series_bar()`. @p subscription is the row's index in the + * `subscriptions` array #strategy_configure_native_ext_v1 was given (or the + * list #strategy_native_declare_subscriptions_v1 installed). Legal inside + * every callback, `on_timeframe_bar` included. + * @return PF_NATIVE_OK when @p out was written, #PF_NATIVE_ABSENT before the + * series' first delivery, for an unknown index, and on every input bar a + * #PF_NATIVE_GAPS_CLEAR series publishes nothing on — the empty that stands + * for na. */ +PF_API int strategy_native_series_bar_v1(pf_strategy_t s, uint32_t subscription, + pf_bar_t* out); + +/** The account's marked equity at @p mark — `native_marked_equity()`. */ +PF_API int strategy_native_marked_equity_v1(pf_strategy_t s, double mark, double* out); + +/** The price at which the marked equity falls below the run's maintenance + * requirement for the live position's side — `native_liquidation_price()`. + * @return PF_NATIVE_OK when @p out was written, #PF_NATIVE_ABSENT when the + * run declares 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); @p out is then written NaN. */ +PF_API int strategy_native_liquidation_price_v1(pf_strategy_t s, double* out); + +/** The units a kernel-sized request resolves to under this run's spec, as a + * pure query — `native_sized_units()`. + * + * @p sized is a #PF_NATIVE_INTENT_SIZED request, read for its sizing block + * alone (side, basis and its value, reserve, grid policy): units = + * cash / (price * point_value * fx), the cash being the basis value or the + * fraction of @p equity, net of the percent fee reserve when the request + * asks for it, then the grid policy. It is the very function the kernel + * sizes with, at acceptance and at the candidate, so a host that gates a + * command on its quantity reads the number here before it submits. + * Observation only: it moves and freezes nothing. + * + * @param s The host whose run spec sizes the request. + * @param sized The #PF_NATIVE_INTENT_SIZED request to size, borrowed for + * the call. + * @param price The sizing price. + * @param equity The marked equity an EQUITY_FRACTION basis is a share of. + * @param fx The account rate the conversion divides by. + * @param units Receives the units; NaN with #PF_NATIVE_ABSENT. + * @return PF_NATIVE_OK, #PF_NATIVE_ABSENT when the run is not configured or + * the basis is unresolvable there (non-positive money or denominator, a + * below-one-step quotient under a snapping grid), PF_NATIVE_E_ARGUMENT for a + * request that is not SIZED or a NULL pointer, or the request's own + * PF_NATIVE_E_STRUCT / PF_NATIVE_E_TAG. + * + * Exercised by `tests/test_native_c_api.c`. */ +PF_API int strategy_native_sized_units_v1(pf_strategy_t s, const pf_native_request_v1* sized, + double price, double equity, double fx, + double* units); + +/** The run's generic risk ledger — `native_risk_state()`. Every field is its + * zero for a run that declares no risk block. */ +PF_API int strategy_native_risk_state_v1(pf_strategy_t s, pf_native_risk_state_v1* out); + +/** The whole economics of one margin call — what a C++ host's + * `on_native_margin_call` receives as `MarginCallEvent`. + * + * @p ordinal is the call's event ordinal: the `ordinal` of the + * #pf_native_event_v1 #pf_native_callbacks_v1::on_margin_call is handed, or + * of a #PF_NATIVE_EVENT_MARGIN_CALL row #strategy_native_events_v1 returns. + * Legal inside `on_margin_call` and anywhere the event history is; + * observation only. + * + * @p out is an in/out size prefix: set `out->struct_size` to + * `sizeof(pf_native_margin_call_v1)` before the call. + * @return PF_NATIVE_OK when @p out was written, #PF_NATIVE_ABSENT when no + * margin call of this run has that ordinal (@p out untouched), + * PF_NATIVE_E_STRUCT for a mis-sized row, or another negative status. + * + * Exercised by the fx-roll scenario of `tests/test_native_c_api.c`. */ +PF_API int strategy_native_margin_call_v1(pf_strategy_t s, uint64_t ordinal, + pf_native_margin_call_v1* out); + +/** The run's continuation identity — `native_continuation_hash()`. Two runs + * driven the same way that folded the same declarations and the same inputs + * answer the same value; a batch and a stream over identical bars booking + * identical trades do NOT, because the driving mode is part of a + * continuation. It is the C spelling of the hash a stream resumes against, + * and the same reason the per-bar broker-state hash is per driving mode + * (`pineforge.h`, `strategy_set_broker_state_hash_recording`). */ +PF_API int strategy_native_continuation_hash_v1(pf_strategy_t s, uint64_t* out); + +/** Open a cohort roster for PF_NATIVE_OWNER_BIND_COHORT. */ +PF_API int strategy_native_cohort_open_v1(pf_strategy_t s, uint64_t* cohort); + +/** Enrol a live request into a cohort roster. */ +PF_API int strategy_native_cohort_add_v1(pf_strategy_t s, uint64_t cohort, + uint64_t incarnation); + +/** Remove a request from a cohort roster. */ +PF_API int strategy_native_cohort_remove_v1(pf_strategy_t s, uint64_t cohort, + uint64_t incarnation); + +/** Configure a native run from the v1 specification plus the extension. + * + * Use this INSTEAD of #strategy_configure_native_v1, not after it: the + * kernel configures a host exactly once and refuses (and fails) a second + * attempt, so this call takes both halves and applies them together. + * @p base is the same #pf_native_run_spec_v1 the other entry point takes. + * + * Refuses without mutation — the handle stays usable — for an + * already-configured handle, a mis-sized struct, an unknown enumerator, or a + * specification the kernel's own validation rejects, which answers + * PF_NATIVE_E_ARGUMENT: the call validates the whole specification before it + * configures, so a rejected one leaves the handle Unconfigured. + * @return PF_NATIVE_OK, or a negative status. */ +PF_API int strategy_configure_native_ext_v1(pf_strategy_t s, + const pf_native_run_spec_v1* base, + const pf_native_run_spec_ext_v1* ext); + +/** Append later bars to the run's declared auxiliary feed on a realtime + * stream (`NativeStrategyHost::append_auxiliary_bars`). + * + * Legal between stream inputs, after #strategy_stream_begin, on a host whose + * specification declared PF_NATIVE_SPEC_EXT_AUXILIARY_FEED. The bars are + * copied, join the feed behind every bar it holds, and ride on the next + * pushed bar whose period they opened before — the routing a batch of the + * same bars applies. @p n of 0 is accepted and appends nothing. + * + * Refused without mutation, the handle staying usable, for bars that are out + * of order or not after the feed's last bar, a bar with invalid OHLCV, a bar + * that opened inside an input period already accepted, a host that declared + * no feed, and a run that is not realtime: PF_NATIVE_E_STATE, the reason + * readable with #strategy_get_last_error and, by name, from + * #strategy_native_append_auxiliary_bars_ext_v1. A call from inside a callback fails the + * run, as every reentrant stream input does. + * @return PF_NATIVE_OK, or a negative status. */ +PF_API int strategy_native_append_auxiliary_bars_v1(pf_strategy_t s, const pf_bar_t* bars, + int32_t n); + +/** The same append, answered by name — `append_auxiliary_bars_result()`. + * + * The same call as #strategy_native_append_auxiliary_bars_v1 — same + * legality, same appended bars, same statuses, the same latched contract + * failure for a call from inside a callback — with the refusal written out. + * #strategy_native_append_auxiliary_bars_v1 stays this call with both + * out-parameters NULL. + * + * @param s The host this run is driving. + * @param bars @p n bars, strictly increasing and after the feed's last + * bar, copied; NULL when @p n is 0. + * @param n Bar count; 0 appends nothing. + * @param error Optional; receives a #pf_native_append_error_t whenever the + * kernel judged the call (#PF_NATIVE_APPEND_ERROR_NONE when + * appended). + * @param index Optional; receives the bar OF THIS CALL the refusal stopped + * on, 0 for a refusal that is not about one bar and for an + * applied append. + * @return PF_NATIVE_OK, PF_NATIVE_E_STATE for every refusal the kernel names + * in @p error, or PF_NATIVE_E_ARGUMENT for a negative @p n or NULL @p bars + * with @p n > 0, which the C layer refuses itself, leaving both + * out-parameters untouched. + * + * Exercised by `tests/test_native_c_api.c`. */ +PF_API int strategy_native_append_auxiliary_bars_ext_v1(pf_strategy_t s, const pf_bar_t* bars, + int32_t n, uint32_t* error, + int32_t* index); + +/** Declare where the fill that opened a lot sits on its entry bar — + * `declare_opened_lot_entry_bar_mask()`, the entry-side half of the + * excursion capability #pf_native_callbacks_v1::on_lot_excursion owns + * (RULING A48). + * + * The host says only WHERE its fill sat. The kernel walks @p entry_bar's + * modeled path in the run's own leg order, the one its matcher walks — + * #pf_native_run_spec_ext_v1::path_order when HIGH_FIRST or LOW_FIRST, and + * under AUTO the high first when |high - open| < |open - low|, the low first + * otherwise — finds where a price is first touched on it, and sets the + * two entry-bar masks of every open lot booked under @p entry_incarnation: + * the ends of the bar the path had already reached before that lot's own + * price. They come back on the lot's closing facts, + * #pf_native_lot_excursion_v1::entry_bar_high_masked and + * `entry_bar_low_masked`. A fill the path never reaches leaves the lot as + * it was, and so does an incarnation that booked no open lot. The masks are + * durable lot state, folded into the broker-state hash. + * + * Legal inside `on_applied` alone — the frame in which a host learns that a + * fill opened a lot (#pf_native_applied_v1::opened_lot_incarnation) — and + * refused with PF_NATIVE_E_STATE everywhere else, changing nothing. + * + * @param s The host this run is driving, from #strategy_create. + * @param entry_incarnation The request whose fill opened the lot. + * @param entry_bar The whole bar the fill sat on; borrowed. + * @param fill_point #pf_native_opened_lot_fill_point_t. + * @return PF_NATIVE_OK, PF_NATIVE_E_ARGUMENT for a NULL @p entry_bar, + * PF_NATIVE_E_TAG for a fill point outside the enumeration, + * PF_NATIVE_E_STATE outside `on_applied`, or another negative status. */ +PF_API int strategy_native_declare_opened_lot_entry_bar_mask_v1(pf_strategy_t s, + uint64_t entry_incarnation, + const pf_bar_t* entry_bar, + uint32_t fill_point); + +/** @} */ /* end of pf_native_c_api */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* PINEFORGE_NATIVE_C_API_H */ diff --git a/tests/fixtures/native_c_api/int23/pineforge/pineforge.h b/tests/fixtures/native_c_api/int23/pineforge/pineforge.h new file mode 100644 index 00000000..4efab281 --- /dev/null +++ b/tests/fixtures/native_c_api/int23/pineforge/pineforge.h @@ -0,0 +1,1444 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * pineforge.h — public C ABI for the PineForge runtime. + * + * This header is the single source of truth for the harness ↔ compiled- + * strategy boundary. Every PineForge-generated .so exports a fixed set + * of C symbols declared below; the Python harness (validate_detailed_ + * report.py) and any C/C++/FFI consumer of compiled strategies links + * against this contract. + * + * STABILITY GUARANTEE + * ─────────────────── + * Within the same PINEFORGE_VERSION_MAJOR, this header's POD struct + * layouts and `extern "C"` symbol signatures are append-only. Fields + * are never reordered, removed, or retyped; new fields may only be + * appended at the end of structs. New functions may be added; existing + * functions are not removed or signature-changed. + * + * Across major versions all bets are off. Bump + * PINEFORGE_VERSION_MAJOR when breaking the ABI. + * + * SCOPE — WHAT THIS HEADER COVERS + * ─────────────────────────────── + * ✓ Lifecycle of a compiled strategy (create / destroy) + * ✓ Running a backtest (auto-detect or fully configured) + * ✓ Per-strategy configuration (inputs, overrides, magnifier, trace) + * ✓ The shape of the report returned to the harness + * + * SCOPE — WHAT THIS HEADER DOES NOT COVER (BY DESIGN) + * ─────────────────────────────────────────────────── + * ✗ The contract between codegen-emitted strategy code and the runtime + * internals (TA classes, math, series, strategy commands). That + * contract stays C++ — codegen and runtime ship together and are + * versioned in lockstep within the closed transpiler. + * ✗ Source-compiling strategies. Use the closed transpiler binary. + * + * The C++ headers under `` etc. are *internal* + * implementation surface — not part of this stability guarantee. + */ + +#ifndef PINEFORGE_H +#define PINEFORGE_H + +#include +#include + +/* ── Version ─────────────────────────────────────────────────────── */ +/* Macros (PINEFORGE_VERSION_MAJOR / _MINOR / _PATCH / _STRING / _FULL, + * PINEFORGE_GIT_SHA) live in the generated . */ +#include + +/* pf_pending_order_v1_t / pf_field_desc_t -- the generated, C-compatible POD + * mirror of the engine's resting-order record (ABI v4, task 7). Regenerate + * with scripts/gen_pending_order_mirror.py; never edit by hand. */ +#include + +/* ── Visibility ──────────────────────────────────────────────────── */ + +#if defined(_WIN32) || defined(__CYGWIN__) + #if defined(PINEFORGE_BUILD_SHARED) + #define PF_API __declspec(dllexport) + #else + #define PF_API __declspec(dllimport) + #endif +#elif defined(__GNUC__) || defined(__clang__) + #define PF_API __attribute__((visibility("default"))) +#else + #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. + * pf_report_t is caller-allocated: growth causes silent stack corruption + * in old callers that under-size the struct. pf_trade_t is runtime- + * allocated: growth causes array-stride misindexing in old readers that + * iterate the trades array with the stale sizeof. Value 2 = first + * versioned layout (metrics + equity curve); .so files predating this + * macro have no pf_abi_version symbol — treat dlsym failure as + * version 1. Value 3 appends pf_trade_t::open_at_end (the range-end + * close flag); a v2 reader iterating trades with the v2 stride would + * misindex every row after the first. Value 4 appends the live-runtime + * accessors and the per-bar broker-state hash array to pf_report_t. */ +#define PF_ABI_VERSION 4 + +/** Feature probe for the opt-in split chart/request.security feed boundary. + * When defined, #strategy_set_aux_security_feed is available. */ +#define PINEFORGE_HAS_AUX_SECURITY_FEED_V1 1 + +/** Feature probe for native higher-timeframe request.security feeds. + * When defined, #strategy_set_native_security_feed is available. */ +#define PINEFORGE_HAS_NATIVE_SECURITY_FEED_V1 1 + +/** Feature probe for immutable native-run account-currency FX curves. + * When defined, #strategy_configure_native_fx_curve_v1 is available. */ +#define PINEFORGE_HAS_NATIVE_FX_CURVE_V1 1 + +#ifdef __cplusplus +extern "C" { +#endif + +/** @defgroup pf_types Types + * @brief POD types and enums passed across the C ABI. + * @{ + */ + +/** Bar-magnifier sub-bar sampling distribution. + * + * Selects how intra-bar synthetic ticks are placed when the bar + * magnifier is enabled in #run_backtest_full. Layout-compatible with the + * internal C++ `pineforge::MagnifierDistribution` enum class — a + * `static_assert` in `c_abi.cpp` guarantees the integer values match. */ +typedef enum pf_magnifier_distribution_e { + PF_MAGNIFIER_UNIFORM = 0, /**< Uniform spacing across the parent bar. */ + PF_MAGNIFIER_COSINE = 1, /**< Cosine-tapered density. */ + PF_MAGNIFIER_TRIANGLE = 2, /**< Triangle-tapered density. */ + PF_MAGNIFIER_ENDPOINTS = 3, /**< Default — exact O,H,L,C points plus uniform fill between. */ + PF_MAGNIFIER_FRONT_LOADED = 4, /**< Sample density biased toward bar open. */ + PF_MAGNIFIER_BACK_LOADED = 5 /**< Sample density biased toward bar close. */ +} pf_magnifier_distribution_t; + +/** Single OHLCV bar pushed into the engine. + * + * Layout-compatible with the internal C++ `pineforge::Bar` struct. */ +typedef struct pf_bar_s { + double open; /**< Open price. */ + double high; /**< High price. */ + double low; /**< Low price. */ + double close; /**< Close price. */ + double volume; /**< Bar volume. */ + int64_t timestamp; /**< Bar open time, Unix milliseconds. */ +} pf_bar_t; + +/** One provider-neutral realtime executed-trade update. + * + * `sequence` is optional: pass 0 when the normalized source has no stable + * ordering key. Non-zero values must increase strictly within a stream. + * `quantity` is expressed in the configured symbol's volume units and is + * accumulated into the input bar's volume. The source adapter owns all + * provider-specific fields and normalization. */ +typedef struct pf_trade_tick_s { + int64_t timestamp; /**< Source event time, Unix milliseconds. */ + uint64_t sequence; /**< Normalized per-stream sequence, or 0. */ + double price; /**< Executed trade price (> 0). */ + double quantity; /**< Traded quantity in symbol volume units (>= 0). */ +} pf_trade_tick_t; + +/** Closed-trade record returned in pf_report_t::trades. + * + * Layout-compatible with internal `pineforge::TradeC`. */ +typedef struct pf_trade_s { + int64_t entry_time; /**< Entry fill time (Unix ms). */ + int64_t exit_time; /**< Exit fill time (Unix ms). */ + double entry_price; /**< Entry fill price (incl. slippage). */ + double exit_price; /**< Exit fill price (incl. slippage). */ + double pnl; /**< Net realized PnL in account currency (commission-inclusive). */ + double pnl_pct; /**< Net return-on-cost in percent: pnl (NET of commission) / + * entry cost (entry_price * qty * pointvalue) * 100. This is + * TradingView's "Net P&L %" convention, arbitrated 2026-06-12 + * against a real TV export (trade #258 short: 102.44 USD on a + * 2276.66 entry => 4.50%). Degenerates to the old gross + * (exit/entry-1)*100 form for longs with zero commission; + * the previous short form (entry/exit-1)*100 was wrong on + * large moves. Sign always matches pnl. */ + int is_long; /**< 1 if long, 0 if short. */ + double max_runup; /**< Peak favorable price travel during the trade ($/unit qty). */ + double max_drawdown; /**< Peak adverse price travel during the trade ($/unit qty). */ + double qty; /**< Filled quantity. */ + double commission; /**< Entry+exit commission actually deducted from pnl + * (account currency). pnl is already net of this. */ + int32_t entry_bar_index;/**< Script-bar index of the entry fill (0-based). */ + int32_t exit_bar_index; /**< Script-bar index of the exit fill (0-based). */ + int32_t open_at_end; /**< 1 when this row is the RANGE-END close of a position + * that was still open after the final bar; 0 for an + * exit the script or a bracket produced. TradingView's + * deep-backtest report does not leave the last position + * open: it reports it as a closed trade whose exit leg is + * the range's last bar at that bar's CLOSE, with an + * empty exit Signal, and counts it in closedTrades + * (orb-lite on NYSE:F 1D: Entry short 2026-03-16 @ 11.82, + * Exit 2026-04-30 @ 12.08 = the last close, + * closedTrades:1). The engine emulates that row + * (operator decision 2026-09-02): exit_time is the last + * script bar's label, exit_price its mintick-rounded + * close with no slippage, commission per the strategy's + * rules, pnl/pnl_pct/excursions as for any close. + * Appended in ABI v3. */ +} pf_trade_t; + +/** Trade-level statistics block — computed once each for all / long / short. + * + * Loss-side fields (`gross_loss`, `avg_loss`, `largest_loss`) are + * **positive magnitudes** (absolute values of the underlying negative PnL). */ +typedef struct pf_trade_stats_s { + int32_t num_trades; /**< Closed trades in this block (all / long-only / short-only). */ + int32_t num_wins; /**< Trades with pnl > 0. */ + int32_t num_losses; /**< Trades with pnl < 0. */ + int32_t num_even; /**< Trades with pnl == 0.0 exactly; breaks both win and loss + * streaks; excluded from win/loss averages. + * Invariant: num_trades == num_wins + num_losses + num_even. */ + double percent_profitable; /**< 100 * num_wins / num_trades, in PERCENT (0-100). + * NaN when num_trades == 0. */ + double net_profit; /**< Sum of pnl (account currency, net of commission). */ + double net_profit_pct; /**< net_profit as a percent of initial capital (0-100 scale). + * NaN when initial capital <= 0. */ + double gross_profit; /**< Sum of winning pnl. */ + double gross_profit_pct; /**< gross_profit as a percent of initial capital (0-100 scale). + * NaN when initial capital <= 0. */ + double gross_loss; /**< Sum of |losing pnl| — POSITIVE magnitude (TV display convention). */ + double gross_loss_pct; /**< gross_loss as a percent of initial capital (0-100 scale). + * NaN when initial capital <= 0. */ + double profit_factor; /**< gross_profit / gross_loss. NaN when gross_loss == 0. */ + double avg_trade; /**< net_profit / num_trades. NaN when num_trades == 0. */ + double avg_trade_pct; /**< Mean of per-trade pnl_pct over all trades. + * NaN when num_trades == 0. */ + double avg_win; /**< gross_profit / num_wins. NaN when num_wins == 0. */ + double avg_win_pct; /**< Mean of per-trade pnl_pct over winning trades. + * NaN when num_wins == 0. */ + double avg_loss; /**< gross_loss / num_losses (positive magnitude). + * NaN when num_losses == 0. */ + double avg_loss_pct; /**< Mean of the NEGATED pnl_pct of the losing trades. Since + * pnl_pct is net return-on-cost (sign matches pnl), this is + * a genuinely POSITIVE magnitude. Basis = pf_trade_t::pnl_pct. + * NaN when num_losses == 0. */ + double ratio_avg_win_avg_loss; /**< avg_win / avg_loss. NaN unless both sides non-empty. */ + double largest_win; /**< Single largest pnl among winning trades. + * NaN when num_wins == 0. */ + double largest_win_pct; /**< Maximum pnl_pct over winning trades — an INDEPENDENT + * maximum, not the pct of the largest-USD win (TV convention, + * validated 2026-06-12 vs TV export). + * NaN when num_wins == 0. */ + double largest_loss; /**< Single largest |pnl| among losing trades (positive magnitude). + * NaN when num_losses == 0. */ + double largest_loss_pct; /**< Maximum of -pnl_pct over losing trades (positive magnitude) — + * an INDEPENDENT maximum, not the pct of the largest-USD loss + * (TV convention, validated 2026-06-12 vs TV export: All + * "Largest loss %" came from a different trade than the + * largest USD loss). NaN when num_losses == 0. */ + double commission_paid; /**< Sum of pf_trade_t::commission in the block. */ + double expectancy; /**< (num_wins/num_trades)*avg_win - (num_losses/num_trades)*avg_loss, + * account currency per trade. NaN when num_trades == 0. */ + int32_t max_consecutive_wins; /**< Longest winning run; even trades reset both streaks. */ + int32_t max_consecutive_losses; /**< Longest losing run; even trades reset both streaks. */ + double avg_bars_in_trade; /**< Mean of (exit_bar_index - entry_bar_index + 1) in SCRIPT + * bars, over all trades — inclusive of the entry bar (TV + * convention, validated 2026-06-12). + * NaN when num_trades == 0. */ + double avg_bars_in_wins; /**< Mean bar duration of winning trades, inclusive of the entry + * bar (TV convention, validated 2026-06-12). + * NaN when num_wins == 0. */ + double avg_bars_in_losses; /**< Mean bar duration of losing trades, inclusive of the entry + * bar (TV convention, validated 2026-06-12). + * NaN when num_losses == 0. */ +} pf_trade_stats_t; + +/** Equity-curve-derived statistics (all-trades only, like TV). */ +typedef struct pf_equity_stats_s { + double max_equity_drawdown; /**< Peak-to-trough equity drop, positive currency magnitude. */ + double max_equity_drawdown_pct; /**< max_equity_drawdown relative to the peak in effect + * (PERCENT 0-100). */ + double max_equity_runup; /**< Trough-to-peak rise where the trough resets on each new + * equity peak (mirrors the engine's intra-run extremes). */ + double max_equity_runup_pct; /**< max_equity_runup relative to that trough (PERCENT 0-100). */ + double buy_hold_return; /**< initial_capital * (last_close/first_open - 1), currency. + * NaN when first chart open is non-finite or <= 0. */ + double buy_hold_return_pct; /**< buy_hold_return as PERCENT. + * NaN when first chart open is non-finite or <= 0. */ + /** Month-end-resampled equity simple returns (chart timezone, open-time + * 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. */ + }; + /** 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. */ + }; + 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. + * NaN with <2 returns or zero deviation. */ + double sortino_bar; /**< Same construction as sharpe_bar over per-bar returns; + * uses population downside deviation. + * NaN with <2 returns or zero deviation. */ + double cagr; /**< PERCENT per year: 100*((final_equity/initial_capital)^(1/years)-1). + * NaN when span <= 0 or either side <= 0. */ + double calmar; /**< cagr / max_equity_drawdown_pct — BOTH IN PERCENT, so the + * ratio is dimensionless. NaN when drawdown is 0. */ + double recovery_factor; /**< net_profit / max_equity_drawdown (currency / currency). + * NaN when drawdown is 0. */ + double time_in_market_pct; /**< PERCENT (0-100) of script bars with an open position + * at bar close. */ + double open_pl; /**< Mark-to-market open profit at the final bar. */ +} pf_equity_stats_t; + +/** Composite metrics container: trade stats (all / long / short) + + * equity-curve stats. */ +typedef struct pf_metrics_s { + pf_trade_stats_t all, longs, shorts; + pf_equity_stats_t equity; +} pf_metrics_t; + +/** Single per-script-bar equity point. + * + * `time_ms` is the script-bar **open** timestamp (Unix ms). + * `equity` = `initial_capital` + `net_profit` + `open_profit` at bar close. */ +typedef struct pf_equity_point_s { + int64_t time_ms; /**< Script-bar OPEN timestamp (Unix ms). */ + double equity; /**< initial_capital + net_profit + open_profit. */ + double open_profit; /**< Mark-to-market open P&L at bar close. */ +} pf_equity_point_t; + +/** Per-`request.security()` site diagnostic counters. + * + * Layout-compatible with internal `pineforge::SecurityDiagC`. */ +typedef struct pf_security_diag_s { + int sec_id; /**< Stable id for the request.security site. */ + int64_t feed_count; /**< Higher-TF feed bars consumed. */ + int64_t complete_count; /**< Evaluations on completed parent bars. */ + int64_t partial_count; /**< Evaluations on still-forming parent bars. */ +} pf_security_diag_t; + +/** Single per-bar trace entry. + * + * Emitted when the source script contains `// @pf-trace name=expr` + * pragmas and tracing is enabled via #strategy_set_trace_enabled. + * Layout-compatible with internal `pineforge::TraceEntryC`. */ +typedef struct pf_trace_entry_s { + int64_t timestamp; /**< Bar timestamp (Unix ms). */ + int32_t bar_index; /**< Zero-based bar index. */ + int32_t name_id; /**< Index into pf_report_t::trace_names. */ + double value; /**< Traced expression value on this bar. */ +} pf_trace_entry_t; + +/** Backtest report filled by #run_backtest / #run_backtest_full. + * + * Layout-compatible with internal `pineforge::ReportC`. + * + * ### Ownership and lifetime + * The struct itself is caller-owned (typically stack). The embedded + * arrays (`trades`, `security_diag`, `trace`, `trace_names`, + * `equity_curve`, `broker_state_hash`) are heap-allocated by the + * runtime; the caller frees each filled report exactly once, with #report_free + * or, on the kernel archive, #strategy_native_report_free_v1. `trace_names` + * strings stay owned by the strategy handle until #strategy_free. */ + +typedef struct pf_report_s { + /* Trades */ + int total_trades; /**< Closed-trade count (== trades_len), including + * the range-end close of a position still open + * after the final bar (pf_trade_t::open_at_end). */ + pf_trade_t* trades; /**< Heap array of closed trades; script-driven exits + * first, then the range-end rows (open_at_end=1). */ + int trades_len; /**< Length of #trades. */ + double net_profit; /**< Sum of all closed-trade PnL. */ + + /* Bar processing counts */ + int64_t input_bars_processed; /**< Source-feed bars consumed. */ + int64_t script_bars_processed; /**< Script-timeframe bars evaluated. */ + + /* Security diagnostics */ + int64_t security_feeds_total; /**< Total higher-TF feed bars across all security sites. */ + int64_t security_complete_total; /**< Total complete-bar evals across all security sites. */ + int64_t security_partial_total; /**< Total partial-bar evals across all security sites. */ + + /* Bar magnifier diagnostics */ + int64_t magnifier_sub_bars_total; /**< Sub-bars synthesized by the magnifier. */ + int64_t magnifier_sample_ticks_total; /**< Sample ticks visited by the magnifier. */ + + /* Timeframe metadata */ + int input_tf_seconds; /**< Detected/configured input timeframe (seconds). */ + int script_tf_seconds; /**< Script timeframe (seconds). */ + int script_tf_ratio; /**< script_tf_seconds / input_tf_seconds. */ + int needs_aggregation; /**< 1 if input → script TF aggregation was performed. */ + int bar_magnifier_enabled; /**< 1 if magnifier was active for this run. */ + + /* Per-security feed/eval counters */ + pf_security_diag_t* security_diag; /**< One entry per request.security site. */ + int security_diag_len; /**< Length of #security_diag. */ + + /* Per-bar trace records */ + pf_trace_entry_t* trace; /**< Per-bar trace records (empty unless tracing enabled). */ + int trace_len; /**< Length of #trace. */ + const char** trace_names; /**< Names indexed by pf_trace_entry_t::name_id. */ + int trace_names_len; /**< Length of #trace_names. */ + + /* Computed trading metrics. Trade-based blocks reported for all / + * long-only / short-only; equity-based stats are all-trades only. + * Loss-side fields are positive magnitudes. Undefined values are NaN + * (see per-field docs). */ + pf_metrics_t metrics; + /* Per-script-bar equity curve. time_ms is the script-bar OPEN + * timestamp; equity = initial_capital + net_profit + open_profit at + * bar close. Heap-allocated; freed by report_free. len == + * script_bars_processed, EXCEPT after a mid-run error (check + * strategy_get_last_error): an exception can truncate the curve, and + * metrics then describe the truncated prefix. NOTE int64_t length + * (ctypes: c_int64). */ + pf_equity_point_t* equity_curve; + int64_t equity_curve_len; + /* Per-script-bar broker-state hash, filled when + * #strategy_set_broker_state_hash_recording is on; freed by + * #report_free. NULL / 0-length when recording was off (default) or no + * script bars were dispatched. When populated, len == + * script_bars_processed, and the last element equals + * #strategy_broker_state_hash's value at the end of the run for a + * compiled Pine strategy and a bare native host alike: the recorder + * latches the continuation at each report point, and the scalar read + * after the run folds the last latch. ABI v4. */ + uint64_t* broker_state_hash; + int64_t broker_state_hash_len; +} pf_report_t; + +/** @} */ /* end of pf_types */ + +/** Opaque handle to a compiled strategy instance. */ +typedef void* pf_strategy_t; + +/** @defgroup pf_lifecycle Strategy lifecycle + * @brief Create, run, and destroy a compiled strategy instance. + * @{ + * + * NOTE: Per-strategy symbols (strategy_create, run_backtest, etc.) are + * emitted by the codegen with internal C++ types (ReportC, Bar) that are + * layout-compatible but type-distinct from the public C PODs below. + * Guard with PINEFORGE_NO_STRATEGY_DECLS so engine.hpp can include this + * header for its POD types without conflicting with per-strategy TU + * definitions. + */ + +/** What #strategy_execution_contract answers for a valid handle. */ +typedef enum pf_execution_contract_e { + PF_EXECUTION_CONTRACT_LEGACY = 1, /**< The retired legacy execution path. */ + PF_EXECUTION_CONTRACT_NATIVE_MARKET_V1 = 2 /**< The native market kernel; every + * handle this runtime creates. */ +} pf_execution_contract_t; + +/** Native execution contract query: a #pf_execution_contract_t, or -1 for a + * NULL handle. Absence of this symbol on a known ABI4/stream-v1 library + * means legacy. */ +PF_API int strategy_execution_contract(pf_strategy_t s); + +/** The bits of #pf_native_run_spec_v1::optional_mask: each names the one + * optional field it makes meaningful. A bit outside this set is refused. */ +typedef enum pf_native_spec_optional_e { + PF_NATIVE_SPEC_OPTIONAL_QUANTITY_GRID = 1u << 0, /**< `quantity_grid`. */ + PF_NATIVE_SPEC_OPTIONAL_MAX_ABS_UNITS = 1u << 1, /**< `max_abs_units`. */ + PF_NATIVE_SPEC_OPTIONAL_INITIAL_MARGIN_FRACTION = 1u << 2, /**< `initial_margin_fraction`. */ + PF_NATIVE_SPEC_OPTIONAL_MAX_OPEN_LOTS = 1u << 3 /**< `max_open_lots`. */ +} pf_native_spec_optional_t; + +/** Versioned native run specification. All string pointers are non-null. + * session may be empty (all-day literal). session_key, timeframes, timezone + * and tickerid are nonempty. `optional_mask` carries + * #pf_native_spec_optional_t bits. + * + * Its three enum-valued words are named in : + * `fee_kind` is a #pf_native_fee_kind_t, `close_execution` a + * #pf_native_close_execution_t and `allowed_open_directions` a + * #pf_native_open_directions_t (zero-filled, that word admits no opening at + * all). #strategy_configure_native_v1 hands a word outside its enumeration + * to the kernel's own validation, which refuses the spec: it answers -1 and + * the host is Failed. #strategy_configure_native_ext_v1 refuses the same + * word with PF_NATIVE_E_TAG before the kernel sees it, and the host stays + * usable. */ +typedef struct pf_native_run_spec_v1 { + uint32_t struct_size; + const char *session_key; uint64_t run_number; + const char *input_tf, *script_tf; + const char *ticker, *tickerid, *type, *currency, *basecurrency, *description, *volumetype; + const char *timezone, *session, *chart_timezone; + double initial_capital, point_value, account_fx, price_tick; + uint32_t slippage_ticks, fee_kind; double fee_value; + uint32_t optional_mask, close_execution, allowed_open_directions; + double quantity_grid, max_abs_units, initial_margin_fraction; + uint64_t max_open_lots; +} pf_native_run_spec_v1; + +/** Apply a native v1 specification. Returns 0 on success, -1 on failure. + * Legacy handles refuse without native state mutation. */ +PF_API int strategy_configure_native_v1(pf_strategy_t s, const pf_native_run_spec_v1* spec); + +/** Immutable timestamped account-currency FX curve for a native run. + * The two arrays have exactly @c n elements and are copied by the runtime. */ +typedef struct pf_native_fx_curve_v1 { + uint32_t struct_size; + uint32_t n; + const int64_t* effective_from_ms; + const double* account_per_quote; +} pf_native_fx_curve_v1; + +/** Stage an immutable account-currency FX curve on a Ready native handle. + * + * Timestamps must be strictly increasing and rates finite and positive. + * Pass @c n == 0 to clear the staged curve; the scalar @c account_fx remains + * the pre-first fallback. Curves are applied when the native run begins. + * Legacy handles and non-Ready native handles refuse without mutation. + * + * @return 0 only when staging is applied; -1 for invalid input, an invalid + * handle, a non-native or non-host native handle, or a non-Ready host. */ +PF_API int strategy_configure_native_fx_curve_v1( + pf_strategy_t s, const pf_native_fx_curve_v1* curve); + +/* ─────────────────────────────────────────────────────────────────── + * STRATEGY .SO EXPORTS — implemented per compiled strategy + * ─────────────────────────────────────────────────────────────────── + * + * Each .so emitted by the codegen exports the following symbols. The + * runtime library itself does NOT define them — they are per-strategy + * implementations generated by the transpiler. + * + * Note on naming: these are the legacy unprefixed names retained for + * backward compatibility with the existing harness. Future major + * versions may introduce `pf_`-prefixed equivalents and deprecate the + * unprefixed forms. + */ +#ifndef PINEFORGE_NO_STRATEGY_DECLS + +/** Allocate a new strategy instance. + * + * @param params_json Currently ignored; pass `NULL`. + * @return Strategy handle, or `NULL` on allocation failure. + * + * Caller owns the returned handle and must release it via #strategy_free. */ +PF_API pf_strategy_t strategy_create(const char* params_json); + +/** Release a strategy handle previously returned by #strategy_create. + * + * Safe to call with `NULL`. Invalidates any `pf_report_t::trace_names` + * pointers obtained from this strategy. */ +PF_API void strategy_free(pf_strategy_t s); + +/** Run a backtest with auto-detected timeframe and no bar magnifier. + * + * @param s Strategy handle from #strategy_create. + * @param bars Non-NULL pointer to OHLCV bars (length @p n). + * @param n Bar count (>= 0). + * @param out Non-NULL output report. Fields are populated with heap + * allocations the caller must release via #report_free. */ +PF_API void run_backtest(pf_strategy_t s, + pf_bar_t* bars, + int n, + pf_report_t* out); + +/** Run a backtest with explicit timeframe and magnifier configuration. + * + * @param s Strategy handle. + * @param bars Bar feed. + * @param n Bar count. + * @param input_tf Input timeframe ("1", "5", "15", "60", "1D", ...). + * Empty string → auto-detect from bar timestamps. + * @param script_tf Script timeframe. Empty string → defaults to @p input_tf. + * @param bar_magnifier Boolean (0 / non-zero) — enable bar magnifier. + * @param magnifier_samples Sub-bar samples per parent bar (typical: 4). + * @param magnifier_dist Sampling distribution (see #pf_magnifier_distribution_t). + * @param out Output report. Free with #report_free. */ +PF_API void run_backtest_full(pf_strategy_t s, + pf_bar_t* bars, + int n, + const char* input_tf, + const char* script_tf, + int bar_magnifier, + int magnifier_samples, + pf_magnifier_distribution_t magnifier_dist, + pf_report_t* out); + +/** Free heap arrays attached to a filled report. + * + * Idempotent. Safe to call with `NULL` or an already-freed report. + * The `pf_report_t` struct itself is caller-owned. */ +PF_API void report_free(pf_report_t* report); + +/** @} */ /* end of pf_lifecycle */ + +/** @defgroup pf_config Per-strategy configuration + * @brief Override @c input.*() values, `strategy(...)` params, and runtime knobs. + * @{ + */ + +/** Override a Pine @c input.*() value before the next run. + * + * @param s Strategy handle. + * @param key The input's title (or fallback identifier). + * @param value Serialized value — numbers as decimal strings, + * booleans as `"true"` / `"false"`. + * + * Calls after #run_backtest are accepted but only take effect on + * subsequent runs. */ +PF_API void strategy_set_input(pf_strategy_t s, + const char* key, + const char* value); + +/** Override a `strategy(...)` declaration parameter. + * + * Recognised @p key values: `initial_capital`, `commission_value`, + * `default_qty_value`, `pyramiding`, `slippage`, + * `process_orders_on_close`, `close_entries_rule`, `default_qty_type`, + * `commission_type`. */ +PF_API void strategy_set_override(pf_strategy_t s, + const char* key, + const char* value); + +/** Toggle volume-weighted bar-magnifier sampling. + * + * Has no effect unless the bar magnifier is enabled in + * #run_backtest_full. */ +PF_API void strategy_set_magnifier_volume_weighted(pf_strategy_t s, + int on); + +#endif /* PINEFORGE_NO_STRATEGY_DECLS */ + +/* ─────────────────────────────────────────────────────────────────── + * RUNTIME LIBRARY EXPORTS — implemented in libpineforge + * ─────────────────────────────────────────────────────────────────── */ +/** Toggle per-bar trace recording. Default off (zero-cost when off). + * + * Enables capture for `// @pf-trace name=expr` pragmas already compiled + * into the strategy `.so`. Trace records appear in pf_report_t::trace. */ +PF_API void strategy_set_trace_enabled(pf_strategy_t s, int on); + +/** Set the earliest Unix-ms timestamp at which strategy order commands + * may fire. + * + * Earlier bars still execute user code and warm TA/series state, but + * `strategy.entry/order/exit/close` commands are ignored. */ +PF_API void strategy_set_trade_start_time(pf_strategy_t s, int64_t timestamp_ms); + +/** @} */ /* end of pf_config */ + +/** @addtogroup pf_lifecycle + * @{ + */ + +/** Return the physical entry incarnation for one closed-trade row. + * + * Partial-close/FIFO fragments emitted from the same physical entry share + * this value. Distinct broker entry objects receive distinct monotonically + * increasing values even when Pine reuses the same user-visible entry ID. + * The value is scoped to one strategy run and is intended as report + * provenance, not as a stable cross-run identifier. + * + * @param s Strategy handle whose most recent run filled a report. + * @param trade_index Zero-based row index into that report's `trades` + * array — the script's closed trades followed by the + * range-end rows (`open_at_end`, ABI v3), which carry + * the incarnation of the lot they mark like any other + * close. + * @return Non-zero physical-entry identity, or 0 for an invalid index or a + * legacy/synthetic trade without request record provenance. */ +PF_API uint64_t strategy_closed_trade_entry_incarnation( + pf_strategy_t s, int trade_index); + +/** @} */ /* end of pf_lifecycle */ + +/** @defgroup pf_streaming Historical to realtime streaming + * @brief Warm on confirmed OHLCV and continue the same strategy instance on + * normalized ordered trades from any data source. + * @{ + */ + +/** Warm a strategy with confirmed OHLCV, then switch the same instance to a + * realtime trade stream without resetting position, equity, pending orders, + * Pine variables, TA state, request.security state, or timeframe aggregation. + * + * The warmup must contain at least one complete fixed-duration input bar. + * Normalized ticks start at or after the next input bar's open. This + * lifecycle uses close-only strategy calculation (the Pine strategy default) + * while resting broker orders are evaluated on every normalized trade. + * + * calc_on_order_fills, historical probe/tail overrides, timestamped FX, + * auxiliary and native security feeds are rejected. No every-tick strategy + * callback is provided; hand-written strategies follow the same lifecycle. + * @return 0 on success, -1 on failure. Inspect #strategy_get_last_error. */ +PF_API int strategy_stream_begin(pf_strategy_t s, + const pf_bar_t* warmup_bars, + int n_warmup, + const char* input_tf, + const char* script_tf); + +/** Native live extension version (1). Additive to ABI v4; callers must probe + * this symbol before using the confirmed-bar/action API in older modules. */ +PF_API int strategy_stream_api_version(void); + +/** One physical emulator lot action. is_long describes the position side: + * buy = is_entry == is_long. A reversal is exits followed by an entry. + * price/time are emulator reference values, not external broker fills. + * Strings are borrowed until the next stream mutation or queue clear. */ +typedef struct pf_stream_order_action { + uint64_t sequence; + int64_t timestamp_ms; + int32_t bar_index; + int32_t is_entry; + int32_t is_long; + double quantity; + double price; + const char* order_id; + const char* comment; + uint64_t entry_incarnation; +} pf_stream_order_action_t; + +/** Consume one confirmed input-timeframe bar. Its script-timeframe aggregate + * uses the existing batch OHLC fill kernel when complete. Close-only strategy + * calculation; no bar magnifier or synthetic trade ticks. The first tick/bar + * locks the feed mode. Off-grid/out-of-order data, invalid OHLCV and missing + * in-session bars fail. Calendar-closed intervals may be skipped. No padding. + * Returns 0/-1. On ANY input-processing failure discard/recover the instance: + * the operation is not transactional and can have advanced native state. */ +PF_API int strategy_stream_push_bar(pf_strategy_t s, const pf_bar_t* bar); + +/** Queued physical fills since the last clear, in execution order. Historical + * warmup and report-only range-end rows never enqueue. Sequence starts at 1 + * after warmup and is not reset by clear. Returns -1 on a null handle. */ +PF_API int strategy_stream_order_actions_len(pf_strategy_t s); +/** Copy one action; return 0 on success, -1 on invalid handle/index/output. */ +PF_API int strategy_stream_order_action_get(pf_strategy_t s, int index, + pf_stream_order_action_t* out); +/** Clear observed events after the caller durably journals them. */ +PF_API void strategy_stream_order_actions_clear(pf_strategy_t s); +/** Versioned deterministic fingerprint of observable broker/stream state. + * 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. */ +PF_API uint64_t strategy_stream_state_hash(pf_strategy_t s); + +/** Push one normalized realtime trade. Returns 0 on success, -1 on failure. */ +PF_API int strategy_stream_push_tick(pf_strategy_t s, + const pf_trade_tick_t* tick); + +/** Push an ordered batch of realtime trades. Semantically identical to + * repeated #strategy_stream_push_tick calls, with lower FFI overhead. */ +PF_API int strategy_stream_push_ticks(pf_strategy_t s, + const pf_trade_tick_t* ticks, + int n); + +/** Advance the stream clock and close every input bar whose end is <= the + * supplied time. Quiet in-session intervals become zero-volume carry-forward + * bars; intervals outside the configured syminfo session are skipped. */ +PF_API int strategy_stream_advance_time(pf_strategy_t s, int64_t timestamp_ms); + +/** End a realtime stream. When @p finalize_partial_input_bar is non-zero, the + * currently forming input bar is dispatched before ending; normally callers + * should first advance to a confirmed boundary and pass zero here. */ +PF_API int strategy_stream_end(pf_strategy_t s, int finalize_partial_input_bar); + +/** Snapshot the cumulative warmup + realtime report. The embedded arrays are + * caller-owned after return and must be released with #report_free, or with + * #strategy_native_report_free_v1 by a host that links the kernel archive, + * which has no #report_free. */ +PF_API int strategy_stream_fill_report(pf_strategy_t s, pf_report_t* out); + +/** @} */ /* end of pf_streaming */ + +/** @defgroup pf_live Live-runtime surface (ABI v4) + * Default-off flags and read-only accessors used by pineforge-live. None of + * them changes a historical run unless enabled. @{ */ +/** Request cooperative abort of the run in progress (see c_abi.cpp). */ +PF_API void strategy_request_abort(pf_strategy_t s); +/** 0 = completed, 1 = NOT_COMPLETED (aborted), -1 = @p s is NULL. */ +PF_API int strategy_last_run_status(pf_strategy_t s); +/** Live-runtime tail semantics (spec §3.1): the LAST bar of the array fed to + * every subsequent run() is a still-forming bar, not the chart's rightmost + * historical bar. This is persistent configuration, not a one-shot flag -- + * it stays in effect until a caller passes @p on == 0, so a handle reused + * for a later plain historical replay must be explicitly turned back off. + * Effects when @p on is non-zero: + * 1. `barstate.islast` is false for that bar. + * 2. `session.islastbar` is computed from the bucket calendar (no next + * bar to peek at, so it evaluates whether the next bucket -- this + * bar's timestamp plus one script-TF step -- falls out of session). + * 3. `bar_index` stays put; `last_bar_index` is frozen at the horizon + * bar (`horizon_bars - 1`), when @p horizon_bars > 0. `last_bar_time` + * is exact when the horizon bar is in the script-bar array, one + * script-TF step per missing bar past the array's last bar + * otherwise; under aggregation (input_tf < script_tf) it is + * extrapolated from the first bar instead, and the aggregation-path + * caveat below applies. + * 4. The range-end synthetic close row/trade is skipped (no + * `open_at_end` row); the final equity point keeps `open_profit`. + * 5. Interior bars (every bar before the last) are unaffected, except + * effect 2 under aggregation (below). + * Scope: the Pine source host's scheduler applies these where it publishes + * a script bar to the generated script. Effects 1, 3 and 4 hold on every + * path. Effect 2 is its session-predicate step, which runs for every script + * bar of a run whose timeframe is known -- a timeframe-aware overload, or + * `run(bars, n)` with at least two bars, whose timeframe it detects from + * them; with fewer, `session.ismarket`/`session.islastbar` keep their + * reset-state `false` whatever this flag says. The bucket-calendar + * lookahead stands in for the next bar wherever the scheduler holds none: + * with input_tf == script_tf that is the array's last bar alone, but under + * aggregation (input_tf < script_tf) the scheduler holds no next script + * bar at all, so the lookahead decides `session.islastbar` on every bar of + * the run. Callers must feed an input_tf == script_tf array, matching + * #strategy_set_probe_suppress_tail_logic's scope note. + * `strategy_stream_begin` refuses a handle with this flag set. + * Default off (@p on == 0): every historical run stays byte-identical to + * before this flag existed. The mode belongs to the Pine source host; on + * a host that models no still-forming tail bar (a bare kernel host) the + * call is accepted and inert, as it always was there. */ +PF_API void strategy_set_realtime_tail(pf_strategy_t s, int on, int horizon_bars); +/** Live probe tail suppression (spec §3.2): the LAST script bar of the array + * fed to every subsequent run() is matched but not calculated. The kernel + * matches that bar like any other, so resting stop/limit orders settle + * against it; the Pine source host's scheduler then publishes it through + * its suppressed-tail step instead of the generated script -- source + * history and `bar_index` advance, the fills already matched on the bar + * are settled, the report point is marked, `on_bar` is not invoked and no + * range-end row is written. The host's own bar-close policy is not the + * script and still runs on that bar -- its risk-state update and its + * TradingView margin-call checkpoint -- so a margin call due against the + * forming bar is booked against it. + * Apart from what that policy books, the run's last-bar fills are exactly + * the settled book's fills against the forming bar, and the post-run + * pending-order book is the book in force during that bar. This is + * persistent configuration, like #strategy_set_realtime_tail, and + * independent of it -- do not assume the two flags are coupled; set each + * explicitly. + * Scope (ABI v4 / live v1): honoured on every batch path -- the + * single-timeframe and the timeframe-aware overloads, with or without the + * bar magnifier. Under aggregation (input_tf < script_tf) the suppressed + * bar is the run's last COMPLETED script bar, and a trailing partial + * bucket is never calculated at all. Under `calc_on_order_fills` only the + * bar's close calculation is suppressed: a fill on the forming bar still + * triggers the fill recalculation, which runs the script on it. + * `calc_on_order_fills` and the bar magnifier stay gated in live v1 and a + * probe must not enable them; callers must feed an input_tf == script_tf + * array. + * `strategy_stream_begin` refuses a handle with this flag set: clear it + * (on = 0) first. + * Default off (@p on == 0): every historical run stays byte-identical to + * before this flag existed. Like #strategy_set_realtime_tail the mode + * belongs to the Pine source host; on a host that models no still-forming + * tail bar the call is accepted and inert, as it always was there. */ +PF_API void strategy_set_probe_suppress_tail_logic(pf_strategy_t s, int on); +/** Force this run's intrabar path order (ABI v4 live-runtime surface): the + * leg order every modeled OHLC path the engine walks uses -- stop/limit + * fill priority, exit trail walking, dual-entry-stop arbitration, + * bar-magnifier sub-bar sampling, and the excursion columns a source host + * reports on a closed row. Values: + * - `0` AUTO (default): the unchanged TV-emulator rule -- the leg nearer + * `open` (by `|high-open|` vs `|open-low|`) goes first. + * - `1` HIGH_FIRST: force `O -> H -> L -> C` regardless of the bar's own + * shape. + * - `2` LOW_FIRST: force `O -> L -> H -> C` regardless of the bar's own + * shape. + * The values are #pf_native_path_order_t's (): + * #PF_NATIVE_PATH_ORDER_AUTO, #PF_NATIVE_PATH_ORDER_HIGH_FIRST and + * #PF_NATIVE_PATH_ORDER_LOW_FIRST. Any other @p mode is clamped to AUTO: + * this setter answers nothing, so it cannot refuse one. + * A live probe runs the SAME forming bar under BOTH forced orders and emits + * only the fills that agree between the two -- a fill that depends on which + * leg TradingView's own (unobservable, still-forming) bar will resolve to + * is path-dependent and must be suppressed rather than guessed. + * This is persistent configuration, like #strategy_set_realtime_tail -- it + * stays in effect until a caller passes @p mode == 0, so a handle reused + * for a later plain historical replay must be explicitly set back to AUTO. + * Scope: the mode is carried by the RUN, in `NativeRunSpec::path_order`. A + * native-bound source host projects this setting into that field at begin + * -- at run() and at #strategy_stream_begin alike -- and a bare native host + * declares the field itself; every modeled path the native driver walks + * then resolves its leg order from that field. So the setting takes effect + * at the next begin rather than mid-run, and a stream is NOT exempt: a + * confirmed bar pushed with #strategy_stream_push_bar is sealed on the same + * forced waypoint sequence a run() walks (measured on a 99/101 bracket and + * an AUTO-low-first touch bar: both routes exit at 99.00 under AUTO and at + * 101.00 under HIGH_FIRST). What a forced order cannot reach is an OBSERVED + * TICK (#strategy_stream_push_tick): one price is not a modeled path and + * has no legs to order, so the same tape fills identically under all three + * modes; the forming bar those ticks build is ordered only when it seals, + * on the waypoint sequence above. + * Default AUTO (@p mode == 0): every historical run stays byte-identical to + * before this flag existed. */ +PF_API void strategy_set_path_order(pf_strategy_t s, int mode); +/** The dual-entry-stop arbitration decided on the LAST bar the most recent + * run() dispatched: a flat position resting exactly one long stop-only + * ENTRY and one short stop-only ENTRY, both touched on that bar + * (`dual_entry_stop_path_winner`, internal). Values mirror + * `internal::DualEntryStopPathWinner`'s enumerator order: + * - `0` None -- no such pair was arbitrated on that bar (not flat, no + * matching pair, or neither/only one side touched). + * - `1` LongFirst -- the long stop's first-touch position on the intrabar + * path came first (or the two tied, which the engine always resolves + * in the long leg's favour). + * - `2` ShortFirst -- the short stop's first-touch position came first. + * - `-1` -- @p s is NULL. + * This is a per-BAR snapshot, not a live read of the engine's per-pass + * working state: it is written once, at the arbitration itself, and then + * holds for the rest of that bar even though the working state goes back + * to None the moment the winning side fills (position no longer flat) or + * its stop-entry admission is declined -- neither of which undoes the fact + * that TradingView's broker emulator arbitrated a real pair that bar. A + * caller therefore gets the right answer whether it reads this after a + * `strategy_set_probe_suppress_tail_logic` forming-bar probe (a single + * native request-matching pass) or after an ordinary + * `process_orders_on_close` run with no tail suppression (two passes, the + * winner already filled by the second). + * A live probe reads this after a forming-bar run to see which side the + * engine's own broker-emulator tie-break picked, without having to re-run + * and infer it from which of the two possible fills came back. + * Only the standard (non-`calc_on_order_fills`) dispatch path updates this + * value; it is a silent no-op under the COOF scheduler, mirroring + * #strategy_set_probe_suppress_tail_logic's dispatch-path-scope caveat. */ +PF_API int strategy_last_bar_dual_entry_path(pf_strategy_t s); +/** Toggle per-script-bar broker-state hash recording (spec §3.4, ABI v4). + * + * When @p on is non-zero, every subsequent run() appends + * #strategy_broker_state_hash's value to pf_report_t::broker_state_hash + * immediately after each script bar is dispatched, so the array's length + * matches pf_report_t::script_bars_processed. Cleared (recorded array + * emptied, not the flag itself) at the start of every run(); the flag is + * persistent configuration, like #strategy_set_realtime_tail, and stays + * set until a caller passes @p on == 0. + * Also covers #strategy_stream_begin's warmup run() and every script bar + * dispatched afterward by the realtime tick stream, so + * #strategy_stream_fill_report's cumulative report satisfies the same + * len == script_bars_processed invariant. Set this BEFORE + * #strategy_stream_begin to also record the warmup leg -- reset_run_state() + * (which stream_begin's internal run() invokes) empties the recorded + * array, not the flag, but a flag flipped on only after stream_begin + * returns misses the warmup bars already dispatched. + * Default off (@p on == 0): pf_report_t::broker_state_hash is NULL / + * 0-length and every historical run stays byte-identical to before this + * flag existed. + * + * What a row is, and what it is for. A row is the run's CONTINUATION + * IDENTITY at that bar, not its trade outcome: it folds the broker state and, + * ahead of it, the state a resume would continue from -- the driving mode + * (batch run, stream warmup, stream realtime) included, on purpose. So the + * array is a replay check WITHIN one driving mode and deliberately not across + * modes: two runs driven the same way over the same bars record the same + * rows, and a run driven the same way that ended at bar k recorded, as its + * last row, the row the longer run recorded after bar k; but a run(), a + * #strategy_stream_begin with one warmup bar and one with every bar as warmup + * record DIFFERENT rows from index 0 over the same bars booking the same + * trades. Only the len == script_bars_processed identity above holds across + * drivings. What pins that a stream books what a batch books is the outcome + * itself -- the trades, the position and the equity -- and not this array. */ +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. */ +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 + * book in force for the NEXT bar. 0 when @p s is NULL. Read-only; a + * historical run is byte-identical whether or not a caller reads it. */ +PF_API int strategy_pending_orders_len(pf_strategy_t s); +/** Copy the @p index-th resting order (0-based, the engine's own book + * order -- insertion order; broker fill priority is decided at fill time + * from `created_seq`, not from this index) into @p out as a + * pf_pending_order_v1_t value snapshot. Copies + * min(@p size_in, sizeof(pf_pending_order_v1_t)) bytes: an older reader + * with a smaller struct receives a prefix (struct_version and size first), + * a newer reader with a larger one receives the whole v1 and must not + * read past pf_pending_order_v1_t::size. Strings are NUL-terminated + * char[64] copies with a `_truncated` flag and a `_hash64` (FNV-1a 64 of + * the full string); enums are int32 values; NaN sentinels are copied + * verbatim. Returns 0 on success, -1 -- with nothing written -- when + * @p s or @p out is NULL, @p index is out of range, or @p size_in < 8 + * (too small to hold even the `struct_version` + `size` header; every + * larger @p size_in is honoured as a prefix copy). The layout is + * self-described by #strategy_pending_order_layout. */ +PF_API int strategy_pending_order_get(pf_strategy_t s, int index, void* out, size_t size_in); +/** The field table of pf_pending_order_v1_t as THIS runtime compiled it -- + * one pf_field_desc_t {name, type, offset, size} per field, in struct + * order, starting with `struct_version` and `size`. Static storage: the + * pointer stays valid for the life of the process and needs no handle. + * @p count (may be NULL) receives the row count. An FFI consumer builds + * its struct from this table rather than from a hand-typed copy, so the + * mirror can grow (append-only) without breaking it. */ +PF_API const pf_field_desc_t* strategy_pending_order_layout(int* count); +/** Which sizing rule #strategy_pending_order_fill_qty's quantity came from, + * written to its `partition` out-parameter; each value is documented there. + * The value set is the Pine source host's pending-order sizing, the only + * host that answers a probe. */ +typedef enum pf_fill_qty_partition_e { + PF_FILL_QTY_PARTITION_EXIT = -1, /**< An exit: no partition. */ + PF_FILL_QTY_PARTITION_EXPLICIT = 0, + PF_FILL_QTY_PARTITION_FROZEN_PLACEMENT = 1, + PF_FILL_QTY_PARTITION_DEFAULT_STOP_PLACEMENT = 2, + PF_FILL_QTY_PARTITION_AT_FILL = 3 +} pf_fill_qty_partition_t; + +/** Engine-computed fill quantity of the @p index-th resting order (ABI v4 + * live-runtime surface, task 8, spec 3.6): the contracts the entry kernel + * would OPEN if that order filled at @p fill_price, sized by the engine's + * own rules so a live runtime never re-implements them. @p fill_price is + * slipped the way the kernel slips it (`native_matching::apply_slippage` + * then the directional grid snap; an entry with a + * limit leg takes the unslipped limit-or-better route). @p partition + * receives which sizing rule produced the value, a #pf_fill_qty_partition_t: + * - `0` EXPLICIT -- a script-supplied qty: for `strategy.entry` the + * lot-floored contracts (`apply_qty_step`) of a fixed qty, or the + * explicit percent/cash budget sized at the fill for a per-call + * qty_type override; a `strategy.order` explicit qty is dispatched + * verbatim (no lot step). + * - `1` FROZEN_PLACEMENT -- a quantity fixed before the fill, never + * re-derived from the fill price: the default percent_of_equity / + * cash MARKET (or strategy.order) size frozen at the signal close + * (`frozen_default_qty`); a MARKET's frozen broker transaction (a + * finalized flat pair's `paired_flat_market_transaction_qty`; a + * same-bar-market member's `sbmt_tx_qty` from flat or as a kept + * over-cap add); or what one of the two MARKET reversal kernels + * opens -- a same-bar-market member against an opposite live position + * opens the remainder `sbmt_tx_qty - min(sbmt_tx_qty, live qty)` + * (`apply_same_bar_market_tx_reversal`), and the exact SHORT-seed + * collision's final short re-opens the residual + * `pyramid_entries[0].qty - pyramid_entries[1].qty` after closing both + * lots (`short_seed_collision_final_short_is_live`). 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 `use_default_stop_placement_qty` says the + * fill consumes it: created flat, filling from flat, positive fill. + * - `3` AT_FILL -- default sizing at the slipped fill (`calc_qty`). + * @p close_only receives 1 when the kernel's close-only predicate fires + * -- the fill closes against the live opposite position and that + * predicate opens no leg of its own: the order's + * `affordability_close_only` (entry leg declined at placement), the + * priced-entry `prior_cycle_close_only` rule (opposite live position whose + * cycle the order was not placed in -- `created_position_side !=` the + * live side -- and not a KI-65 `reverses_same_bar_market_from_flat`), the + * same-cycle frozen explicit-FIXED transaction the close consumes exactly, + * a finalized flat MARKET pair against an opposite position, or one of the + * two reversal kernels above opening nothing. Where the order was created + * FLAT the engine's close-only branch is `close_opposite_then_enter`: a + * transaction larger than the live position still opens the remainder, so + * a consumer compares @p qty with the live position. A replaced + * default-percent short (`replaced_percent_short_market_is_live`) is + * dispatched `close_opposite_then_enter` with its `frozen_default_qty`: + * @p qty is that transaction, @p close_only 0. The probe answers for the + * order filling against the CURRENT book and position; fills that an + * earlier order in the same pass would make first are not simulated. + * NOT folded into @p qty: the deferred-flip + * carry (`tv_carry_qty`, added by `enter_market_from_flat` for a priced + * entry firing from FLAT whose placement side is the opposite of the + * requested side) -- read `tv_carry_qty` / `created_position_side` from + * the mirror. Returns 0 on success; 1 -- with @p qty NaN, @p close_only 0, + * @p partition -1 (#PF_FILL_QTY_PARTITION_EXIT) -- when the order is an EXIT (its fill quantity is + * decided against the live position at the fill, not by a partition); -1 + * with nothing written when @p s is NULL, @p index is out of range, or any + * out-pointer is NULL. Read-only: no historical run changes because a + * caller probed it. */ +PF_API int strategy_pending_order_fill_qty(pf_strategy_t s, int index, double fill_price, + double* qty, int* close_only, int* partition); +/** 1 when the @p index-th resting order's entry-relative offsets + * (`profit_ticks` / `loss_ticks` / `trail_points`) resolve now (ABI v4, + * task 8): entries, plain orders and exits with an empty `from_entry` + * always; an exit bound to a `from_entry` only once that id has filled in + * the CURRENT position cycle -- the gate the engine's own + * `materialize_relative_exit_prices_for_live_position` and eligibility + * pass share. 0 otherwise; -1 when @p s is NULL or @p index is out of + * range. */ +PF_API int strategy_pending_order_level_resolved(pf_strategy_t s, int index); +/** The price levels the @p index-th resting order would fire at, as the + * engine's fill path resolves them (ABI v4, task 8). A leg the order + * carries as a price (`stop_price`, `limit_price`, `trail_price`) is + * reported verbatim -- it is already on the price grid. A leg carried as + * a tick offset is resolved against the live position's average entry + * price only when #strategy_pending_order_level_resolved is 1 AND a + * position is live, with the POSITION side's sign exactly as the fill + * path: `limit = entry + dir * profit_ticks * mintick`, `stop = entry - + * dir * loss_ticks * mintick` (dir = +1 long, -1 short; both + * `level_on_price_grid`), and `trail_activation = entry +/- ceil( + * trail_points - 5e-5) * mintick` snapped to the tick grid + * (`trail_points` wins over `trail_price` when both are set, as in + * the trail activation rule). NaN for a leg that is unset or not yet + * resolvable. Returns 0; -1 with nothing written when @p s is NULL, + * @p index is out of range, or any out-pointer is NULL. */ +PF_API int strategy_pending_order_effective_levels(pf_strategy_t s, int index, double* stop, + double* limit, double* trail_activation); +/** The trail extreme the exit trail legs ride (`trail_best_price_`: the + * running high of a long / low of a short since the position filled, + * bar extremes folded in as the fill path folds them). NaN when @p s is + * NULL and NaN until a position has filled. */ +PF_API double strategy_trail_best_price(pf_strategy_t s); +/** The live position's volume-weighted average entry price + * (`position_entry_price_`). NaN when @p s is NULL and NaN when the + * position is flat (the engine keeps 0 there; a live reader must not + * mistake it for a price). */ +PF_API double strategy_position_avg_price(pf_strategy_t s); +/** The live position's cycle id (`position_cycle_seq_`): 0 when flat, a + * fresh nonzero id per open or reversal, unchanged across same-direction + * adds -- the id `created_position_cycle_seq` on a mirrored order refers + * to. -1 when @p s is NULL. */ +PF_API int64_t strategy_position_cycle_seq(pf_strategy_t s); +/** Task 9: closed-trade id / exit-comment string accessors, indexing the + * same REPORT row space as #strategy_closed_trade_entry_incarnation + * (`trades_` then the range-end rows, `open_at_end`). The returned pointer + * is valid until the next run() (or stream call) on this handle, like + * #strategy_get_last_error. NULL on a NULL @p s or an out-of-range + * @p trade_index. */ +PF_API const char* strategy_closed_trade_entry_id(pf_strategy_t s, int trade_index); +/** See #strategy_closed_trade_entry_id for the row-space/lifetime/NULL + * contract. The returned string is the engine's own INTERNAL exit id, not + * always the script's `strategy.exit`/`strategy.close` id verbatim: + * - a real `strategy.exit` bracket leg -- the user's own id, unchanged. + * - a `strategy.close(id, ...)` close -- `"__close__" + id`. + * - `strategy.close_all()` / a bare-id `strategy.close()` -- the literal + * `"__close__"` (empty target id appended). + * - a margin-call forced liquidation -- the sentinel `"__margin_call__"`. + * - an intraday-cap close (`risk.max_intraday_loss`, or the + * max-filled-orders cap) -- empty (`""`). + * A caller matching exit ids back to its own `strategy.close` calls should + * strip the `"__close__"` prefix rather than compare verbatim. */ +PF_API const char* strategy_closed_trade_exit_id(pf_strategy_t s, int trade_index); +/** See #strategy_closed_trade_entry_id. */ +PF_API const char* strategy_closed_trade_exit_comment(pf_strategy_t s, int trade_index); +/** Why a closed report row exited — `execution::CloseCause`, the value + * #strategy_closed_trade_close_cause answers for a valid row. The names are + * the kernel's; the descriptive labels in that function's documentation are + * the same numbers. */ +typedef enum pf_close_cause_e { + PF_CLOSE_CAUSE_UNSPECIFIED = 0, /**< No cause recorded (UNKNOWN). */ + PF_CLOSE_CAUSE_SCRIPT = 1, /**< A market close or a reversal. */ + PF_CLOSE_CAUSE_BRACKET = 2, /**< A bracket leg. */ + PF_CLOSE_CAUSE_LIQUIDATION = 3, /**< A margin-call liquidation slice (MARGIN_CALL). */ + PF_CLOSE_CAUSE_RISK_LIMIT = 4, /**< A risk-limit flatten (INTRADAY_LOSS_CAP). */ + PF_CLOSE_CAUSE_FILL_CAP = 5, /**< A filled-order cap (INTRADAY_FILL_CAP). */ + PF_CLOSE_CAUSE_RANGE_END = 6 /**< The range-end close of a position still open. */ +} pf_close_cause_t; + +/** Task 9: why the @p trade_index-th REPORT-row closed trade exited, a + * #pf_close_cause_t. + * Values: + * - `0` UNKNOWN -- reserved for the documented "no cause" value on a + * VALID trade. Every in-range row currently falls through the + * derivation below to at worst `1` SCRIPT, so no live derivation + * today actually returns `0`; it is not used for a bad index (see + * `-1` below, final review F7). + * - `1` SCRIPT -- a `strategy.close` / `strategy.close_all` market close, + * or a reversal-driven close. + * - `2` BRACKET -- a `strategy.exit` stop/limit/trail/profit/loss leg, + * or a native host's close its owner's fill armed. + * - `3` MARGIN_CALL -- a forced liquidation slice. + * - `4` INTRADAY_LOSS_CAP -- `risk.max_intraday_loss`. + * - `5` INTRADAY_FILL_CAP -- the max-filled-orders intraday cap. + * - `6` RANGE_END -- the still-open position closed at the end of a + * flag-off run (`open_at_end`, ABI v3) -- this always wins over every + * other cause below it. + * Derivation order (see `BacktestEngine::closed_trade_close_cause`, + * engine_trade_accessors.cpp): `open_at_end` -> 6; then the cause the + * CLOSER recorded on the row (`execution::CloseCause`, whose numbers are + * exactly these values) -- a kernel-originated liquidation or risk flatten + * states it through the settling fill, and so does a host request its + * owner's fill armed (a `WaitForApplied` owner relation, as + * `native_toolkit::submit_bracket` builds, with an intent that only closes: + * `Reduce`, `Flatten` or a host-sized close), which records `2` BRACKET; a + * host running its own forced-close policy states it on the row it + * produced, which is where the Pine adapter's margin-call (3), + * max-intraday-loss (4) and filled-order-cap (5) rows get their value (the + * adapter classifies its own bracket rows by order family, so it clears the + * kernel's `2` on them first); then the row's `exit_from_bracket` flag -- true + * only for a REAL `strategy.exit` leg, either an `OrderType::EXIT` fill + * whose id does NOT carry the internal `"__close__"` prefix that a deferred + * `strategy.close`/`close_all` order is also given (that path reuses the + * same `OrderType::EXIT` fill machinery), or a whole-position bracket + * revived and fired at the margin-call event price + * (`revive_position_brackets_after_margin_call_partial`) -- -> 2; + * otherwise 1. + * A Pine/source run's values are unchanged. A bare native host that declares + * a kernel margin model now reads `3` for its own liquidation rows (and `4` + * for a kernel risk flatten) where the retired string derivation, which only + * recognised the adapter's sentinels, answered `1`, and it reads `2` for a + * bracket leg its owner armed, where it read `1` before R5 lane F3. + * `-1` when @p s is NULL, or when @p trade_index is out of range (final + * review F7: matches every sibling indexed live accessor's -1-on-bad-index + * convention -- #strategy_pending_order_fill_qty, + * #strategy_pending_order_level_resolved, + * #strategy_pending_order_effective_levels). `0` therefore never + * ambiguously means "bad index"; a caller can tell "row exists but reads + * UNKNOWN" apart from "bad index" without a separate + * #strategy_closed_trade_entry_incarnation `report_trade_count` bounds + * check first, though doing that check is still good practice. */ +PF_API int strategy_closed_trade_close_cause(pf_strategy_t s, int trade_index); +/** Task 9: the script-facing signed position size (`strategy.position_size`; + * KI-64 freeze-aware -- while a same-bar `process_orders_on_close` close is + * frozen for the current bar, this reads the PRE-close position, matching + * what the script itself observes). NaN when @p s is NULL. */ +PF_API double strategy_position_size(pf_strategy_t s); +/** Task 9: initial capital plus realized net profit + * (`strategy.initial_capital + strategy.netprofit`). NOT Pine's + * `strategy.equity`, which adds open profit on top of this (unlike the + * last point of `pf_report_t::equity_curve`, which does). NaN when @p s is + * NULL. */ +PF_API double strategy_current_equity(pf_strategy_t s); +/** Task 9: total SCRIPT bars dispatched by the most recent run() (mirrors + * `pf_report_t::script_bars_processed`, engine_report.cpp) -- includes a + * stream's warmup leg and every realtime tick-driven bar dispatched + * afterward by #strategy_stream_push_tick / #strategy_stream_push_ticks. + * `-1` when @p s is NULL. */ +PF_API int64_t strategy_script_bars_processed(pf_strategy_t s); +/** @} */ + +/** @addtogroup pf_config + * @{ + */ + +/** Set the strategy's chart timezone (IANA / POSIX TZ string). + * + * Pine builtins ``hour``, ``minute``, ``second``, ``dayofmonth``, + * ``dayofweek``, ``month``, ``year`` and ``weekofyear`` return the + * wall-clock for the chart's timezone — TV exports trade rows in chart + * TZ too. Engine bars are stored as Unix-ms (UTC), so without this + * override these builtins return UTC and silently diverge from TV by N + * hours when the chart is on a non-UTC zone (Asia/Taipei = UTC+8 is the + * validator default). + * + * Pass `NULL`, `""`, `"UTC"` or `"Etc/UTC"` for the legacy UTC + * behaviour (cheap, mutex-free). Any other value names a TZ resolved by + * the system tzdata; the per-bar decomposition then runs under a + * process-global mutex so multi-threaded harnesses don't corrupt each + * other's wall time. + * + * Should be called before #run_backtest / #run_backtest_full. Persists + * across runs on the same strategy handle until overridden. */ +PF_API void strategy_set_chart_timezone(pf_strategy_t s, const char* tz); + +/** Plumb the symbol's exchange timezone (IANA string) into syminfo. Feeds + * ``session.ismarket`` / ``time(session)`` predicates. Defaults to "UTC" + * (crypto). Distinct from #strategy_set_chart_timezone — the chart TZ + * drives wall-clock builtins and intraday-cap day rollover; this drives + * session membership. `NULL` is ignored. Call before #run_backtest / #run_backtest_full. */ +PF_API void strategy_set_syminfo_timezone(pf_strategy_t s, const char* tz); + +/** Set the symbol's session string (e.g. "0930-1600:23456", default + * "24x7"). Feeds ``session.ismarket`` / ``time(session)``. `NULL` + * ignored. Call before #run_backtest / #run_backtest_full. */ +PF_API void strategy_set_syminfo_session(pf_strategy_t s, const char* session); + +/** Set the instrument class (``syminfo.type``: "forex", "stock", "crypto", + * "futures", "index", "fund", "cfd", ...; default "crypto"). Scripts branch + * on it for instrument conventions (e.g. the forex pip size). `NULL` / + * empty ignored. Call before #run_backtest / #run_backtest_full. */ +PF_API void strategy_set_syminfo_type(pf_strategy_t s, const char* type); + +/** Set one of the remaining string members of ``syminfo`` by Pine member + * name: "ticker", "tickerid", "currency", "basecurrency", "description", + * "volumetype" (and "type"). Returns 0 when set, -1 for an unknown key, + * empty value or NULL. Call before #run_backtest / #run_backtest_full. */ +PF_API int strategy_set_syminfo_string(pf_strategy_t s, const char* key, + const char* value); + +/** Set the instrument tick size (``syminfo.mintick``, default 0.01). Drives the + * directional stop-entry snap and ``slippage = N*mintick`` economics. Set + * per-instrument (e.g. 0.25 for ES, 0.00001 for FX). Non-positive ignored. + * Call before #run_backtest / #run_backtest_full. */ +PF_API void strategy_set_syminfo_mintick(pf_strategy_t s, double mintick); + +/** Set the instrument point value (``syminfo.pointvalue``, default 1.0) — the + * $-per-point-per-contract multiplier applied to every money path: realized + * PnL and MFE/MAE, open profit / mark-to-market equity (and the drawdown / + * runup extremes), percent-of-equity and cash position sizing, percent + * commission notionals, and the margin admission check. Set per-instrument + * (e.g. 50 for ES). Non-positive ignored. Call before #run_backtest / #run_backtest_full. */ +PF_API void strategy_set_syminfo_pointvalue(pf_strategy_t s, double pointvalue); + +/** Inject a fundamental/exchange metadata value by Pine member name + * (e.g. "shares_outstanding_total", "target_price_average"). These have + * no OHLCV source; reads of un-injected members return na. Call before + * #run_backtest / #run_backtest_full. */ +PF_API void strategy_set_syminfo_metadata(pf_strategy_t s, const char* key, + double value); + +/** Install a timestamped quote-to-account currency conversion curve. + * + * Each value is account-currency units per one unit of the symbol's quote + * currency and becomes active, inclusively, at the corresponding Unix-ms + * timestamp. The latest active value carries forward; broker events before + * the first point use the scalar `account_currency_fx` metadata fallback. + * Installing a curve also selects the converted account-currency broker + * ledger, including during that pre-first fallback interval; it is not + * equivalent to a same-currency run merely because a rate happens to be 1. + * Arrays are copied. Timestamps must be strictly increasing and rates + * positive and finite. Pass `n == 0` to clear the curve and restore scalar + * behavior. Timestamped curves currently support ordinary historical runs. + * Broker-open rate changes: carried 1× longs and 1× shorts are supported; + * leveraged positions fail closed at the crossing. Streaming, + * calc-on-order-fills, and bar-magnifier runs + * also fail closed. + * + * @return 0 on success, -1 for a null strategy or invalid arrays. */ +PF_API int strategy_set_account_currency_fx_series( + pf_strategy_t s, const int64_t* effective_from_ms, + const double* account_per_quote, int n); + +#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1 +/** Copy a finer feed used exclusively by same-symbol request.security calls. + * + * The next ordinary #run_backtest_full call must receive native chart bars + * with @c input_tf equal to @c script_tf and bar magnifier disabled. Chart + * OHLCV, broker fills, and @c bar_index continue to advance only from that + * native chart feed; @p bars advance only request.security evaluators. + * Every auxiliary bar must map to exactly one native chart bar and every + * native chart bar must have at least one auxiliary bar, otherwise the run + * fails closed via #strategy_get_last_error. Arrays are copied. Pass + * @p n == 0 to clear the auxiliary feed. + * + * @return 0 on success, -1 for a null strategy or invalid input. */ +PF_API int strategy_set_aux_security_feed(pf_strategy_t s, + const pf_bar_t* bars, + int n, + const char* input_tf); +#endif + +#ifdef PINEFORGE_HAS_NATIVE_SECURITY_FEED_V1 +/** Copy the exchange's OWN bars of one higher timeframe for same-symbol + * request.security calls that request exactly that timeframe. + * + * On intraday charts of CME futures and US/Indian equities TradingView's + * request.security(syminfo.tickerid, "D", close) returns the exchange's + * daily bar -- the 15:00 CT settlement on ES1!/NQ1!, the official closing + * print on NASDAQ/NYSE/NSE -- which no aggregation of the intraday feed can + * reproduce. With a native feed installed for @p timeframe ("D", "1D", "W", + * ...), every completed request.security bucket of that timeframe carries + * the native bar's OHLCV (matched by the bucket's own label: the session-day + * stamp for D/W/M, the grid open for intraday) instead of the aggregate; + * bucket TIMING -- when the bar completes relative to the chart -- and every + * other timeframe's request are unchanged, and so are chart OHLCV, broker + * fills and bar_index. A "W" / "M" request with no feed of its own reads + * the daily feed's bars aggregated per week / month (TradingView builds its + * W/M bars from the native daily bars, never from intraday prints), keyed by + * the period's first session-day. A completed bucket with no native bar + * keeps its aggregate (counted, not fatal). Bars must be strictly + * increasing; arrays are copied; pass @p n == 0 to clear the feed for + * @p timeframe. Historical runs only: stream_begin() fails closed while a + * native feed is installed. + * + * @return 0 on success, -1 for a null strategy or invalid input. */ +PF_API int strategy_set_native_security_feed(pf_strategy_t s, + const char* timeframe, + const pf_bar_t* bars, + int n); +#endif + +/** Returns the error message captured by the most recent #run_backtest / + * #run_backtest_full call on this strategy. + * + * Returns an empty string when the run completed normally, or `NULL` + * only when `s` itself is `NULL`. The pointer is owned by the engine + * and remains valid until the next #run_backtest / #run_backtest_full call (which clears + * the captured error before it begins). + * + * The runtime catches every `std::exception` derivative inside the + * engine's run loop so the C ABI never unwinds a C++ exception across + * the `extern "C"` boundary. Consumers must check this after every + * run to surface engine-rejected configurations such as a script + * timeframe finer than the input timeframe, a `request.security` + * timeframe below the chart timeframe without a supported lower-TF + * emulation, or a missing input timeframe when securities are + * registered. */ +PF_API const char* strategy_get_last_error(pf_strategy_t s); + +/** @} */ /* end of pf_config */ + +/** @defgroup pf_version Version query + * @brief Runtime version metadata. + * @{ + */ + +/** Runtime version descriptor returned by #pf_version_get. */ +typedef struct pf_version_s { + int major; /**< Major version. */ + int minor; /**< Minor version. */ + int patch; /**< Patch version. */ + const char* commit_sha; /**< Short git commit SHA, or `""` if unknown. */ +} pf_version_t; + +/** @return Linked runtime version. */ +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. + * + * 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. */ +PF_API const char* pf_version_string(void); + +/** @} */ /* end of pf_version */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +/* The C-level native host API (R5 lane L13). Included last, after the PODs + * it builds on (pf_bar_t, pf_report_t, pf_strategy_t) and outside this + * header's `extern "C"` block, which it opens for itself. Both headers guard + * their own include, so either include order resolves. Its PF_API symbols are + * pinned separately by scripts/check_c_abi_runtime.py, which is why they do + * not move this header's own declaration count. */ +#include + +#endif /* PINEFORGE_H */ diff --git a/tests/test_native_c_api.c b/tests/test_native_c_api.c index ae5a6727..ea2bf122 100644 --- a/tests/test_native_c_api.c +++ b/tests/test_native_c_api.c @@ -1046,6 +1046,36 @@ static void check_callback_failure_latch(void) { CHECK_EQ_INT(state.failure_code, PF_NATIVE_FAILURE_CALLBACK, "failure is not CallbackException"); CHECK_EQ_INT(calls, 3, "the run continued past the refusing callback"); + CHECK(strategy_get_last_error(host) && strategy_get_last_error(host)[0] != '\0', + "the failed run has no presentation error to clear"); + CHECK_EQ_INT(strategy_native_state_v1(host, NULL), PF_NATIVE_E_ARGUMENT, + "a NULL state output was not refused"); + CHECK(strategy_get_last_error(host) && strategy_get_last_error(host)[0] == '\0', + "a C-layer state refusal kept the previous run's last_error"); + strategy_native_host_free(host); + + /* The older configure symbol is implemented in c_abi.cpp rather than + * native_c_host.cpp. Its own argument refusal must clear that same stale + * presentation text without moving the durable Failed state. */ + calls = 0; + host = strategy_native_host_create_v1(&table); + CHECK(host != NULL, "second refusing host create failed"); + if (!host) return; + CHECK_EQ_INT(strategy_configure_native_v1(host, &spec), 0, "second refusing configure"); + CHECK_EQ_INT(strategy_native_run_v1(host, bars, n, NULL), PF_NATIVE_E_RUN_FAILED, + "second refusing callback did not fail the run"); + CHECK(strategy_get_last_error(host) && strategy_get_last_error(host)[0] != '\0', + "the second failed run has no presentation error to clear"); + CHECK_EQ_INT(strategy_configure_native_v1(host, NULL), -1, + "a NULL native spec was not refused"); + CHECK(strategy_get_last_error(host) && strategy_get_last_error(host)[0] == '\0', + "a C-layer configure refusal kept the previous run's last_error"); + memset(&state, 0, sizeof(state)); + state.struct_size = (uint32_t)sizeof(state); + CHECK_EQ_INT(strategy_native_state_v1(host, &state), PF_NATIVE_OK, + "durable failure was not readable after C-layer refusal"); + CHECK_EQ_INT(state.lifecycle, PF_NATIVE_LIFECYCLE_FAILED, + "the C-layer refusal changed durable Failed state"); strategy_native_host_free(host); } @@ -3642,12 +3672,18 @@ typedef struct aux_state { int interval_checks; int interval_wrong; int interval_elsewhere; /* on_bar asked and was refused with E_STATE */ + int timeframe_command_checked; + int timeframe_command_rc; } aux_state; static int aux_on_timeframe_bar(void* user, const pf_bar_t* bar, uint32_t subscription, uint32_t completion, int64_t delivered_at_ms) { aux_state* state = (aux_state*)user; pf_native_timeframe_interval_v1 interval; + if (!state->timeframe_command_checked) { + state->timeframe_command_rc = strategy_native_cancel_all_v1(state->host); + state->timeframe_command_checked = 1; + } memset(&interval, 0, sizeof(interval)); interval.struct_size = (uint32_t)sizeof(interval); interval.version = PF_NATIVE_API_VERSION; @@ -3827,6 +3863,8 @@ static void check_auxiliary_feed(void) { CHECK(state.interval_checks > 0 && state.interval_wrong == 0, "on_timeframe_bar did not read the C++ bucket interval"); CHECK(state.interval_elsewhere > 0, "on_bar never asked for a bucket interval"); + CHECK(state.timeframe_command_checked && state.timeframe_command_rc >= 0, + "on_timeframe_bar refused a command the C++ callback guard allows"); /* After the run there is no bucket either; a missing or mis-sized output, * or a NULL handle, is refused before the phase is judged. */ { @@ -3987,6 +4025,7 @@ typedef struct fx_roll_state { int economics_rc; pf_native_margin_call_v1 economics; double position_in_call; + int margin_command_rc; } fx_roll_state; static int fx_roll_on_bar(void* user, const pf_bar_t* bar, const pf_native_decision_v1* at) { @@ -4054,6 +4093,7 @@ static int fx_roll_on_margin_call(void* user, const pf_native_event_v1* call) { state->economics_rc = strategy_native_margin_call_v1(state->host, call->ordinal, &state->economics); strategy_native_position_v1(state->host, &state->position_in_call, NULL, NULL); + state->margin_command_rc = strategy_native_cancel_all_v1(state->host); return 0; } @@ -4071,6 +4111,7 @@ static void check_fx_roll_margin_point(void) { fx_roll_fill(); memset(&state, 0, sizeof(state)); + state.margin_command_rc = PF_NATIVE_E_STATE; memset(&report, 0, sizeof(report)); table = blank_callbacks(&state); table.on_bar = fx_roll_on_bar; @@ -4144,6 +4185,8 @@ static void check_fx_roll_margin_point(void) { CHECK_EQ_INT(state.roll_numbers, 1, "the roll's two numbers are not the new rate at the unchanged price"); CHECK_EQ_INT(state.margin_calls, 1, "the roll's breach called no margin"); + CHECK(state.margin_command_rc >= 0, + "on_margin_call refused a command the C++ callback guard allows"); CHECK(fabs(state.called_units - FX_ROLL_SLICE) < 1e-9, "the liquidation sliced another quantity"); /* R5 lane E3: the call belongs to the ROLL's instant. The entry fill's own @@ -8400,8 +8443,258 @@ static void check_session_day_tail(void) { } } +typedef struct csurface_configure_state { + pf_strategy_t host; + pf_native_run_spec_v1 spec; + int base_rc; + int ext_rc; + int fx_rc; + int state_rc; + int begin_command_rc; + int input_command_rc; + int input_checked; + uint32_t ext_error; + uint32_t ext_field; + pf_native_fx_curve_error_t fx_error; + uint32_t lifecycle_during_hook; +} csurface_configure_state; + +typedef struct csurface_postrun_state { + pf_strategy_t host; + pf_native_run_spec_v1 next_spec; + int saw_completed_hook; + int typed_rc; + int base_rc; + uint32_t typed_error; + uint32_t typed_field; +} csurface_postrun_state; + +typedef struct csurface_failure_state { + pf_strategy_t host; + int submit_rc; +} csurface_failure_state; + +static int csurface_nonrepresentable_on_begin(void* user) { + csurface_failure_state* state = (csurface_failure_state*)user; + pf_native_request_v1 request = blank_request(); + request.intent = PF_NATIVE_INTENT_TRANSACT; + request.intent_value = 0x1p60; + request.capacity = PF_NATIVE_CAPACITY_POINT_BUDGET; + request.capacity_units = 1.0; + state->submit_rc = strategy_native_submit_v1(state->host, &request, NULL, NULL); + return 0; +} + +static void check_csurface_failure_discriminator(void) { + csurface_failure_state state; + pf_native_callbacks_v1 table; + pf_native_run_spec_v1 spec = twin_spec(); + pf_native_state_v1 result; + const pf_bar_t* bars; + int n = 0; + memset(&state, 0, sizeof(state)); + table = blank_callbacks(&state); + table.on_run_begin = csurface_nonrepresentable_on_begin; + state.host = strategy_native_host_create_v1(&table); + CHECK(state.host != NULL, "discriminator host create failed"); + if (!state.host) return; + spec.session_key = "csurface-discriminator"; + CHECK_EQ_INT(strategy_configure_native_v1(state.host, &spec), 0, + "discriminator host configure failed"); + bars = pf_twin_bars(&n); + CHECK_EQ_INT(strategy_native_run_v1(state.host, bars, n, NULL), PF_NATIVE_E_RUN_FAILED, + "nonrepresentable request did not fail settlement"); + CHECK_EQ_INT(state.submit_rc, PF_NATIVE_OK, + "nonrepresentable request was rejected at submission"); + memset(&result, 0, sizeof(result)); + result.struct_size = (uint32_t)sizeof(result); + CHECK_EQ_INT(strategy_native_state_v1(state.host, &result), PF_NATIVE_OK, + "discriminator state read failed"); + CHECK_EQ_INT(result.failure_code, PF_NATIVE_FAILURE_SETTLEMENT_FAILURE, + "the failure was not a settlement failure"); + CHECK_EQ_INT(result.failure_operation, PF_NATIVE_OPERATION_SETTLEMENT, + "the failure operation was not settlement"); + CHECK(result.failure_discriminator != 0u, + "the C state dropped the kernel's nonzero failure discriminator"); + strategy_native_host_free(state.host); +} + +static int csurface_postrun_hash_hook(void* user, uint64_t* digest) { + csurface_postrun_state* state = (csurface_postrun_state*)user; + pf_native_state_v1 lifecycle; + pf_native_run_spec_ext_v1 ext; + (void)digest; + memset(&lifecycle, 0, sizeof(lifecycle)); + lifecycle.struct_size = (uint32_t)sizeof(lifecycle); + if (strategy_native_state_v1(state->host, &lifecycle) != PF_NATIVE_OK + || lifecycle.lifecycle != PF_NATIVE_LIFECYCLE_COMPLETED + || state->saw_completed_hook) { + return PF_NATIVE_ANSWER_DEFAULT; + } + state->saw_completed_hook = 1; + memset(&ext, 0, sizeof(ext)); + ext.struct_size = (uint32_t)sizeof(ext); + ext.version = PF_NATIVE_API_VERSION; + state->typed_rc = strategy_configure_native_ext_result_v1( + state->host, &state->next_spec, &ext, &state->typed_error, &state->typed_field); + state->base_rc = strategy_configure_native_v1(state->host, &state->next_spec); + return PF_NATIVE_ANSWER_DEFAULT; +} + +static int csurface_configure_in_begin(void* user) { + csurface_configure_state* state = (csurface_configure_state*)user; + pf_native_run_spec_ext_v1 ext; + pf_native_fx_curve_v1 curve; + pf_native_state_v1 lifecycle; + memset(&ext, 0, sizeof(ext)); + ext.struct_size = (uint32_t)sizeof(ext); + ext.version = PF_NATIVE_API_VERSION; + memset(&curve, 0, sizeof(curve)); + curve.struct_size = (uint32_t)sizeof(curve); + state->base_rc = strategy_configure_native_v1(state->host, &state->spec); + state->ext_rc = strategy_configure_native_ext_result_v1( + state->host, &state->spec, &ext, &state->ext_error, &state->ext_field); + state->fx_rc = strategy_configure_native_fx_curve_ext_v1( + state->host, &curve, &state->fx_error, NULL); + memset(&lifecycle, 0, sizeof(lifecycle)); + lifecycle.struct_size = (uint32_t)sizeof(lifecycle); + state->state_rc = strategy_native_state_v1(state->host, &lifecycle); + state->lifecycle_during_hook = lifecycle.lifecycle; + state->begin_command_rc = strategy_native_cancel_all_v1(state->host); + return 0; +} + +static int csurface_command_in_input(void* user, const pf_bar_t* bar, int32_t input_index, + int32_t completes_script_interval) { + csurface_configure_state* state = (csurface_configure_state*)user; + (void)bar; + (void)input_index; + (void)completes_script_interval; + if (!state->input_checked) { + state->input_command_rc = strategy_native_cancel_all_v1(state->host); + state->input_checked = 1; + } + return 0; +} + +static void check_csurface_create_and_configure_refusals(void) { + pf_native_callbacks_v1 table = blank_callbacks(NULL); + pf_strategy_t host; + csurface_configure_state state; + pf_native_state_v1 lifecycle; + const pf_bar_t* bars; + int n = 0; + + table.reserved1 = 1u; + host = strategy_native_host_create_v1(&table); + CHECK(host == NULL, "a nonzero callback-table reserved1 marker was accepted"); + if (host) strategy_native_host_free(host); + + /* The pre-existing base-ABI Ready misuse still goes to the kernel and + * latches Contract. Running and Completed hash-hook calls use the new + * early refusal. */ + table = blank_callbacks(NULL); + host = strategy_native_host_create_v1(&table); + CHECK(host != NULL, "Ready-contract host create failed"); + if (host) { + pf_native_run_spec_v1 ready_spec = twin_spec(); + ready_spec.session_key = "csurface-ready-contract"; + CHECK_EQ_INT(strategy_configure_native_v1(host, &ready_spec), 0, + "Ready-contract initial setup failed"); + CHECK_EQ_INT(strategy_configure_native_v1(host, &ready_spec), -1, + "base configure accepted a Ready host"); + memset(&lifecycle, 0, sizeof(lifecycle)); + lifecycle.struct_size = (uint32_t)sizeof(lifecycle); + CHECK_EQ_INT(strategy_native_state_v1(host, &lifecycle), PF_NATIVE_OK, + "Ready-contract state read failed"); + CHECK_EQ_INT(lifecycle.lifecycle, PF_NATIVE_LIFECYCLE_FAILED, + "base Ready misuse lost its kernel Contract latch"); + CHECK_EQ_INT(lifecycle.failure_code, PF_NATIVE_FAILURE_CONTRACT, + "base Ready misuse latched another failure"); + strategy_native_host_free(host); + } + + memset(&state, 0, sizeof(state)); + state.spec = twin_spec(); + state.spec.session_key = "csurface-configure-hook"; + table = blank_callbacks(&state); + table.on_run_begin = csurface_configure_in_begin; + table.on_input = csurface_command_in_input; + state.host = strategy_native_host_create_v1(&table); + CHECK(state.host != NULL, "configure-hook host create failed"); + if (!state.host) return; + CHECK_EQ_INT(strategy_configure_native_v1(state.host, &state.spec), 0, + "configure-hook initial setup failed"); + bars = pf_twin_bars(&n); + CHECK_EQ_INT(strategy_native_run_v1(state.host, bars, n, NULL), PF_NATIVE_OK, + "a refused configure inside on_run_begin failed the run"); + CHECK_EQ_INT(state.base_rc, -1, "base configure was accepted inside a hook frame"); + CHECK_EQ_INT(state.ext_rc, PF_NATIVE_E_STATE, + "extended configure was accepted inside a hook frame"); + CHECK_EQ_INT(state.ext_error, PF_NATIVE_SPEC_ERROR_WRONG_PHASE, + "extended configure did not name WrongPhase"); + CHECK_EQ_INT(state.ext_field, PF_NATIVE_SPEC_FIELD_NONE, + "extended configure named a field inside a hook frame"); + CHECK_EQ_INT(state.fx_rc, -1, "FX curve configure was accepted inside a hook frame"); + CHECK_EQ_INT(state.fx_error, PF_NATIVE_FX_CURVE_ERROR_WRONG_PHASE, + "FX curve configure did not name WrongPhase"); + CHECK_EQ_INT(state.state_rc, PF_NATIVE_OK, "hook could not read its native state"); + CHECK_EQ_INT(state.lifecycle_during_hook, PF_NATIVE_LIFECYCLE_RUNNING, + "a refused configure changed the hook's Running state"); + CHECK(state.begin_command_rc >= 0, + "on_run_begin refused a command the C++ callback guard allows"); + CHECK(state.input_checked && state.input_command_rc >= 0, + "on_input refused a command the C++ callback guard allows"); + memset(&lifecycle, 0, sizeof(lifecycle)); + lifecycle.struct_size = (uint32_t)sizeof(lifecycle); + CHECK_EQ_INT(strategy_native_state_v1(state.host, &lifecycle), PF_NATIVE_OK, + "configure-hook final state read failed"); + CHECK_EQ_INT(lifecycle.lifecycle, PF_NATIVE_LIFECYCLE_COMPLETED, + "a hook-frame configure refusal failed the host"); + strategy_native_host_free(state.host); + + /* A hash read can call the C hook after the host reached Completed. This + * frame must also refuse both configure routes and leave the read pure. */ + { + csurface_postrun_state postrun; + pf_native_run_spec_v1 first = twin_spec(); + memset(&postrun, 0, sizeof(postrun)); + first.session_key = "csurface-postrun-hook"; + postrun.next_spec = first; + postrun.next_spec.run_number = 2; + table = blank_callbacks(&postrun); + table.on_hash_extension = csurface_postrun_hash_hook; + postrun.host = strategy_native_host_create_v1(&table); + CHECK(postrun.host != NULL, "postrun-hook host create failed"); + if (!postrun.host) return; + CHECK_EQ_INT(strategy_configure_native_v1(postrun.host, &first), 0, + "postrun-hook initial setup failed"); + CHECK_EQ_INT(strategy_native_run_v1(postrun.host, bars, n, NULL), PF_NATIVE_OK, + "postrun-hook initial run failed"); + (void)strategy_broker_state_hash(postrun.host); + CHECK(postrun.saw_completed_hook, "the Completed hash read did not call its C hook"); + CHECK_EQ_INT(postrun.typed_rc, PF_NATIVE_E_STATE, + "typed configure was accepted from a Completed hash hook"); + CHECK_EQ_INT(postrun.typed_error, PF_NATIVE_SPEC_ERROR_WRONG_PHASE, + "Completed hash hook did not name WrongPhase"); + CHECK_EQ_INT(postrun.typed_field, PF_NATIVE_SPEC_FIELD_NONE, + "Completed hash hook named a field"); + CHECK_EQ_INT(postrun.base_rc, -1, + "base configure was accepted from a Completed hash hook"); + memset(&lifecycle, 0, sizeof(lifecycle)); + lifecycle.struct_size = (uint32_t)sizeof(lifecycle); + CHECK_EQ_INT(strategy_native_state_v1(postrun.host, &lifecycle), PF_NATIVE_OK, + "postrun-hook state read failed"); + CHECK_EQ_INT(lifecycle.lifecycle, PF_NATIVE_LIFECYCLE_COMPLETED, + "a hash read mutated the Completed native state"); + strategy_native_host_free(postrun.host); + } +} + int pf_native_c_api_checks(void) { failures = 0; + check_csurface_create_and_configure_refusals(); + check_csurface_failure_discriminator(); check_struct_and_tag_refusals(); check_spec_extension(); check_lifecycle_round_trips(); diff --git a/tests/test_native_c_api_int23_header.c b/tests/test_native_c_api_int23_header.c new file mode 100644 index 00000000..fb283ac4 --- /dev/null +++ b/tests/test_native_c_api_int23_header.c @@ -0,0 +1,77 @@ +/* C-SURFACE-1: compile this entire caller against db98990c's frozen public + * header closure, then link it to the current kernel. Its policy-hook table + * was 168 bytes and already entitled to the decision's session tail. */ +#include + +#include +#include + +struct caller_state { int saw_tail; int saw_callback; }; + +_Static_assert(sizeof(pf_native_callbacks_v1) == 168, + "the INT23 callback table fixture changed"); + +static int on_bar(void* user, const pf_bar_t* bar, const pf_native_decision_v1* at) { + struct caller_state* state = (struct caller_state*)user; + (void)bar; + ++state->saw_callback; + if (at->struct_size == sizeof(pf_native_decision_v1) + && at->has_script_interval == 1u) state->saw_tail = 1; + return 0; +} + +int main(void) { + struct caller_state state = {0, 0}; + pf_native_callbacks_v1 callbacks; + pf_native_run_spec_v1 spec; + pf_bar_t bars[3] = { + {100, 102, 99, 101, 1, 0}, + {101, 103, 100, 102, 1, 300000}, + {102, 104, 101, 103, 1, 600000} + }; + pf_report_t report; + pf_strategy_t host; + int rc; + memset(&callbacks, 0, sizeof(callbacks)); + callbacks.struct_size = (uint32_t)sizeof(callbacks); + callbacks.version = PF_NATIVE_API_VERSION; + callbacks.user = &state; + callbacks.on_bar = on_bar; + printf("CALLBACK TABLE %zu\n", sizeof(callbacks)); + host = strategy_native_host_create_v1(&callbacks); + if (!host) { puts("HOST REFUSED"); return 2; } + memset(&spec, 0, sizeof(spec)); + spec.struct_size = (uint32_t)sizeof(spec); + spec.session_key = "oldcaller"; + spec.run_number = 1; + spec.input_tf = "5"; + spec.script_tf = "5"; + spec.ticker = "MOCK"; + spec.tickerid = "TEST:MOCK"; + spec.type = "crypto"; + spec.currency = "USDT"; + spec.basecurrency = "ETH"; + spec.description = ""; + spec.volumetype = ""; + spec.timezone = "UTC"; + spec.session = "24x7"; + spec.chart_timezone = ""; + spec.initial_capital = 10000; + spec.point_value = 1; + spec.account_fx = 1; + spec.price_tick = 0.01; + spec.allowed_open_directions = 3; + if (strategy_configure_native_v1(host, &spec) != 0) { + puts("CONFIGURE REFUSED"); strategy_native_host_free(host); return 2; + } + memset(&report, 0, sizeof(report)); + rc = strategy_native_run_v1(host, bars, 3, &report); + if (rc != PF_NATIVE_OK || state.saw_callback == 0) { + printf("RUN REFUSED %d CALLBACKS %d\n", rc, state.saw_callback); + strategy_native_host_free(host); return 2; + } + puts(state.saw_tail ? "TAIL PRESENTED" : "TAIL WITHHELD"); + strategy_native_report_free_v1(&report); + strategy_native_host_free(host); + return state.saw_tail ? 0 : 1; +} From 409ebad15513dcf2a2f91447b80a420a9898ec53 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Fri, 25 Sep 2026 19:15:59 +0800 Subject: [PATCH 04/24] fix(v19 KERNEL-EDGE): repair FX and calendar edges, expose host marks Keep synchronous current-execution points out of the walked FX predecessor marks, and derive stream session-day neighbours from eligible calendar slots. Calculate a final pending script bucket at a complete batch end or stream_end(true) only when its contributing input reached the session close; warmup and stream_end(false) carry it. Expose KernelRecordedAtHostMarks through a C++ host call, document FeedTolerant warmup, and pin the four failures in a source-free test. Unreleased v19 value move, once in this commit: re-pin tests/test_native_calendar_hash_witness.cpp ny_daily_over_hourly (digest 0x12d290fd048a6c7c -> 0xfa85a7f9a92aea39; calculated bars 12 -> 13; driver rows 48 -> 52; trades 1 -> 2) because the final calendar-closed daily bucket now calculates. Update the final-batch sequence witness in test_native_htf_subscriptions_stream; the stream_end(false) and warmup sequences stay pending. Full remote profiles and corpus identity judge the final tree. (cherry picked from commit 6cded2c420b6e16ca13da16674d507871cccad34) INT25 merge: the floors add this lane's row to C-SURFACE-1's (kernel 273, release 674; CI-LITE's EXCLUDED_REGISTERED_MIN kept). The six pages merge on digit-normalised text (int16_digit_merge, a citation both sides moved mapped from this lane's tree); 80 anchors the lane's pages kept unmoved over its own native_execution_consumer.cpp / native_c_api.h / native_run_spec.hpp edits are carried through the exact line map (int24_exact_reanchor). Every anchor cites the text one parent cited (verify_carry: 1672 exact, 6 = the floor lines). --- CONTRIBUTING.md | 4 +- docs/adr/0001-kernel-adapter-boundary.md | 122 ++++--- docs/design/native-feature-parity.md | 148 ++++----- docs/pages/contributing-llm.md | 10 +- docs/pages/live-surface.md | 2 +- docs/pages/native-engine.md | 83 ++--- docs/pages/pine-to-native.md | 314 +++++++++--------- include/pineforge/market_driver.hpp | 11 +- include/pineforge/native_c_api.h | 2 + include/pineforge/native_host.hpp | 8 + include/pineforge/native_run_spec.hpp | 1 + scripts/ci_verify.py | 8 +- src/native_execution_consumer.cpp | 147 +++++--- src/native_execution_consumer.hpp | 10 +- tests/CMakeLists.txt | 3 + tests/test_native_calendar_hash_witness.cpp | 4 +- .../test_native_htf_subscriptions_stream.cpp | 39 ++- tests/test_native_kernel_edge.cpp | 268 +++++++++++++++ tests/test_native_session_day_facts.cpp | 3 +- 19 files changed, 786 insertions(+), 401 deletions(-) create mode 100644 tests/test_native_kernel_edge.cpp diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 834f3673..04da3d8a 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:230 and `RELEASE_MIN_TESTS` -ci_verify.py:333. +floor — `KERNEL_MIN_TESTS` ci_verify.py:231 and `RELEASE_MIN_TESTS` +ci_verify.py:335. 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 2ad734f3..054c429e 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -72,18 +72,18 @@ 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:1161`), then feed. + continues live. Configure with `configure_native(spec)` (`native_host.hpp:1169`), then feed. - **Callbacks are not close-only.** `on_native_bar` is the pure-virtual script-bar calculation (`native_host.hpp:874`); also `on_native_input` (`native_host.hpp:856`), `on_native_tick` (`native_host.hpp:859`), `on_native_timeframe_bar` (`native_host.hpp:866`), `on_native_bar_open` — at the modeled opening, before its matching pass - (`native_host.hpp:870`, `on_native_bar_open` `native_execution_consumer.cpp:6910-6912`) — + (`native_host.hpp:870`, `on_native_bar_open` `native_execution_consumer.cpp:6913-6915`) — `on_native_recalculate` for every calculation of the run (`native_host.hpp:893`), `on_native_sub_bar` (`native_host.hpp:908`) and post-fill `on_native_applied` (`native_host.hpp:918`). `on_bar` is `final` (`native_host.hpp:838`). - **Requests.** `submit` / `replace` / `cancel` / `submit_market` / `replace_market` - (`native_host.hpp:1192-1197`), plus `cancel_all` (`native_host.hpp:1232`) and `cancel_where` - (`native_host.hpp:1237`). A request carries one of five triggers — Market, Limit (with a + (`native_host.hpp:1202-1207`), plus `cancel_all` (`native_host.hpp:1240`) and `cancel_where` + (`native_host.hpp:1245`). 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` @@ -98,10 +98,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:927`) returns no units is a `TermsUnresolved` rejection - (`TermsUnresolved` `native_execution_consumer.cpp:4845`). + (`TermsUnresolved` `native_execution_consumer.cpp:4849`). - **Observing executions.** Implement `on_native_applied` (`native_host.hpp:918`) — notifications drain FIFO after the outer callback returns — and/or poll `native_events(after_ordinal)` - (`native_host.hpp:1294`). Do *not* implement + (`native_host.hpp:1302`). 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`: @@ -109,26 +109,26 @@ 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:4818`) | - | Cohorts | `cohort_open` (`native_host.hpp:1247`) / `cohort_add` (`:1250`) / `cohort_remove` (`:1253`) — the handle a `BindCohort` owner names | + | Failure model | `NativeFailureCode` (`native_host.hpp:63-77`); `Failed` is latched, nothing resumes. A throwing callback latches `CallbackException` (`CallbackException` `native_execution_consumer.cpp:4827`) | + | Cohorts | `cohort_open` (`native_host.hpp:1255`) / `cohort_add` (`:1258`) / `cohort_remove` (`:1261`) — the handle a `BindCohort` owner names | | Mid-callback execution | `current_execution_point` (`native_host.hpp:1060`), `inspect_current_execution` (`native_host.hpp:1072`) / `execute_current` (`native_host.hpp:1077`) | - | Margin seams | four, not one: the run spec's own `margin` model (`NativeMarginModel` `native_run_spec.hpp:239`) with its three hooks — `resolve_margin_requirement` (`native_host.hpp:956`), `margin_check_allowed` (`native_host.hpp:968`), `resolve_margin_call_units` (`native_host.hpp:975`) — and `validate_execution_precommit` (`native_host.hpp:940`), whose `AdmitWithHostMargin` verdict hands the opening check to the host | - | Risk seam | `NativeRiskLimits` (`native_run_spec.hpp:309`) with `native_risk_state()` (`native_host.hpp:1288`); the breach appends a `NativeRiskEvent` to the command history | - | State reads | `physical_position()` (`native_host.hpp:1257`) / `native_marked_equity(mark)` (`native_host.hpp:1266`) / `native_open_lots(mark)` (`native_host.hpp:1262`), `trail_state(handle)` (`native_host.hpp:1065`), 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:1046`) is the lookahead-free bar so far, and `NativeOpenBarView::OpenOnly` (`native_run_spec.hpp:108`) masks that one callback | + | 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:956`), `margin_check_allowed` (`native_host.hpp:968`), `resolve_margin_call_units` (`native_host.hpp:975`) — and `validate_execution_precommit` (`native_host.hpp:940`), 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:1296`); the breach appends a `NativeRiskEvent` to the command history | + | State reads | `physical_position()` (`native_host.hpp:1265`) / `native_marked_equity(mark)` (`native_host.hpp:1274`) / `native_open_lots(mark)` (`native_host.hpp:1270`), `trail_state(handle)` (`native_host.hpp:1065`), 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:1046`) 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:552`) owns instrument and clock facts, fees + (`native_run_spec.hpp:553`) 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` - `native_run_spec.hpp:117`), the per-side broker margin model with its liquidation policy - (`NativeMarginModel` `native_run_spec.hpp:239`), the account risk block (`NativeRiskLimits` - `native_run_spec.hpp:309`), the report policy (`NativeReportPolicy` - `native_run_spec.hpp:60`), calculation timing (`NativeCalculationTrigger` - `native_run_spec.hpp:93`) and the open-bar view, declared higher-timeframe series - (`NativeTimeframeSubscription` `native_run_spec.hpp:484`) and the auxiliary finer feed - (`NativeAuxiliaryFeed` `native_run_spec.hpp:513`). `initial_margin_fraction` - (`native_run_spec.hpp:596`) remains the one-scalar admission-only spelling and is mutually + `native_run_spec.hpp:118`), the per-side broker margin model with its liquidation policy + (`NativeMarginModel` `native_run_spec.hpp:240`), the account risk block (`NativeRiskLimits` + `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 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 @@ -142,9 +142,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:1101`) from inside `on_native_run_begin`, with + (`native_host.hpp:1109`) from inside `on_native_run_begin`, with `on_native_timeframe_bar` (`native_host.hpp:866`) and `native_series_bar` - (`native_host.hpp:1084`) reading them back, and `NativeAuxiliaryFeed` + + (`native_host.hpp:1092`) 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 @@ -154,7 +154,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:1167`). + `configure_native_fx_curve` (`native_host.hpp:1175`). - **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 @@ -167,7 +167,7 @@ summary of it. `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. - **Reproducibility.** Two digests, one wrapping the other. `native_continuation_hash()` - (`native_host.hpp:1335`) returns the consumer's event/continuation hash. **A digest names + (`native_host.hpp:1343`) 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 @@ -324,11 +324,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:2605`), runs a batch - (`strategy_native_run_v1` `native_c_api.h:2618`), and **submits, replaces, cancels and - executes** orders — `strategy_native_submit_v1` (`native_c_api.h:2646`), `_replace_v1` - (`native_c_api.h:2668`), `_cancel_v1` (`native_c_api.h:2708`), `_cancel_all_v1` (`native_c_api.h:2712`), `_cancel_where_v1` (`native_c_api.h:2733`), - `_execute_current_v1` (`native_c_api.h:2754`) — under the kernel's own legality rule. Streaming needs no new + callback table (`strategy_native_host_create_v1` `native_c_api.h:2607`), runs a batch + (`strategy_native_run_v1` `native_c_api.h:2620`), and **submits, replaces, cancels and + executes** orders — `strategy_native_submit_v1` (`native_c_api.h:2648`), `_replace_v1` + (`native_c_api.h:2670`), `_cancel_v1` (`native_c_api.h:2710`), `_cancel_all_v1` (`native_c_api.h:2714`), `_cancel_where_v1` (`native_c_api.h:2735`), + `_execute_current_v1` (`native_c_api.h:2756`) — 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 @@ -449,7 +449,7 @@ see a mechanism, so these rows are held by reading, not by the checker): | 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`. | | the closed rows as the broker-state hash folds them — `BacktestEngine::trades_` under `pineforge-broker-state/v19`, through the consumer's running digest (`NativeExecutionConsumer::closed_rows_digest`, `src/native_execution_consumer.cpp`), and `NativeStrategyHost::native_closed_rows_amended` (`include/pineforge/native_host.hpp`) | the kernel books every row, at the end of the rows; the Pine host amends a booked row inside the applied notification of the execution that booked it (`PineStrategyHost::on_native_applied`: an aggregated chart dates the exit at its chart bar), and after those notifications have returned it reorders a bar's trailing same-bar bracket exits by command sequence (`sort_same_bar_exit_trades`, `src/source/pine_strategy_host.cpp`), naming the first row it moved | **a row is final once the applied notification of the execution that booked it has returned** (R5 lane V19-A). Rule 2 (a), mechanism: the finality point is the kernel's own notification boundary, the same for every host, and what a host that changes a final row supplies is data -- the first row it changed -- so a host of any venue drives the same surface and nothing in it knows Pine. Rule 2 (b), knob: none -- the call reports a change the host has already made and chooses nothing. A final row changed without a name keeps the value the digest took, and the Debug re-fold at each run's end (`verify_closed_rows`) aborts on it; rows removed from the end need no name, because the kernel only appends, so a read over fewer rows or a booking below the final mark forgets them. Every read answers the fold of the six fields v18 walked (entry and exit time and price, quantity, P&L) over the rows as they are, in their order, folded once per row per run instead of once per row per read. Pinned by part 7 of `tests/test_native_state_continuation.cpp` (kernel-only) and by every Debug ctest run, each of whose runs ends in the re-fold. | | the event record a run keeps — `NativeRunSpec::event_retention` (`include/pineforge/native_run_spec.hpp`), the journal window (`WorkingRequestCore::retire_history`, `include/pineforge/native_order.hpp`; `NativeExecutionConsumer::retire_journal`, `src/native_execution_consumer.cpp`) and the reader's acknowledgement (`NativeStrategyHost::native_acknowledge_events`, `native_event_window_start`, `include/pineforge/native_host.hpp`) | the host declares a retention in its spec and acknowledges what it has read; the Pine adapter declares `Window` in `PineExecutionAdapter::project()` and acknowledges its receipt cursor in `observe_terminal_receipts` (and "nothing read yet" at its run begin); the kernel retires at every script-bar boundary | **a reporting policy with a reader acknowledgement** (R5 lane V19-B). Rule 2 (a), mechanism: what a run keeps of its own event record, and a reader telling the kernel what it has consumed, the same for every host; the kernel never retires an event its own live state reads (a queued applied notification, a live deferred group-adjustment chain), and what the journal used to answer beyond that is state (next row). Rule 2 (b), knob: a spec field with no TradingView meaning -- `Window`, `Commands`, `Full` -- folded into the spec digest only off the default; a C caller that does not send the word keeps `Full`, the record its layout was published with. No trade, and no Pine value, depends on it: the adapter reads above its own cursor only. Pinned by `tests/test_native_event_retention.cpp` (kernel-only: `Full` row for row against the pre-window record, the window and the acknowledgement's edges, and the same books booking the same trades with the window closing at every driver point) and `check_event_retention` of `tests/test_native_c_api.c`. | -| 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 driver point | **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). | +| 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). | 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 (→ @@ -577,30 +577,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:2163`), `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:3015`), reading a SIZED request's own sizing block | the sizing-query scenario | -| `resolve_execution_terms`, price half | `on_execution_terms` (`native_c_api.h:2530`): 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:2539`): 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:2546`) | the anchored-level scenario | -| `hash_host_extension` | `on_hash_extension` (`native_c_api.h:2557`): 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:2165`), `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:3017`), reading a SIZED request's own sizing block | the sizing-query scenario | +| `resolve_execution_terms`, price half | `on_execution_terms` (`native_c_api.h:2532`): 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:2541`): 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:2548`) | the anchored-level scenario | +| `hash_host_extension` | `on_hash_extension` (`native_c_api.h:2559`): 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:1232`) / `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:2712`). | -| `cohort_add` (`native_host.hpp:1250`) / `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:860`, 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:1279`) / `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:2987`). | -| `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:1668`) 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:1240`) / `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:2714`). | +| `cohort_add` (`native_host.hpp:1258`) / `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:860`, 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:1287`) / `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:2989`). | +| `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:1670`) 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:63`) | **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:655`) leaves its value unnamed, and the enum guard holds that exclusion. | -| `strategy_set_path_order` (`pineforge.h:965`) 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:893`); 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: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:965`) 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:895`); 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:1059`) | **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:3961`) | **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:582`) | **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:866`) and `NativeTimeframeBarContext::interval` (`native_host.hpp:790`) | **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:7474-7486`); 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:1055-1060`) sees the same values, so no hash contract moves. | -| `pf_native_lot_excursion_v1` (`native_c_api.h:1854`) | **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:7493-7505`); 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:1055-1060`) sees the same values, so no hash contract moves. | +| `pf_native_lot_excursion_v1` (`native_c_api.h:1857`) | **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) @@ -639,7 +639,7 @@ The table records, at the tree the doc wave read, what each block described and | `src/engine_orders.cpp`, lines 350-355 | TradingView same-tick multi-entry sequential-fill semantics | the consumer's same-point fill order, by acceptance and incarnation ordinal | | `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:157`), whose three values are three broker models | +| `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:445-451`) | ### B. Live kernel mechanisms with a calibrated number — each passes the amended rule 2 @@ -652,13 +652,13 @@ The table records, at the tree the doc wave read, what each block described and | 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 holiday session walk `set_native_periods` `src/timeframe.cpp:1621` (the `TradingView` comment `sha256:01f0fe296ca8c2a1e93cad96b9a6cc25331fb326a6a63a2cbd894bb11710fc7e` :1470) | 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:334`) 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` | +| `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` | | `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:8419-8422` (lookahead) and `clear_if_gapped` `:8413-8416` (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:2213`, `::gaps` `include/pineforge/native_c_api.h:2217` | 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:788`, `pf_native_gaps_e` `:799`); 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:8452-8455` (lookahead) and `clear_if_gapped` `:8469-8472` (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:2215`, `::gaps` `include/pineforge/native_c_api.h:2219` | 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:790`, `pf_native_gaps_e` `:801`); 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` | @@ -671,10 +671,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:7114`, presenting `NativeDecisionContext::in_session` `include/pineforge/market_driver.hpp:154` 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 slot one script width away; 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:7704-7720` | 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:7681-7702` | 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:6449` | 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:7116`, presenting `NativeDecisionContext::in_session` `include/pineforge/market_driver.hpp:155` 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:7725-7741` | 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:7702-7723` | 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:6453` | 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 @@ -705,7 +705,7 @@ Two session-flag questions lane E26 left open were decided by R5 lane F5: - **Session-day facts at a bar with nothing held after it. RULED retained (E26-6).** A stream's bars, the stream warmup's last bar and a live probe's forming bar have no bar held after them, so `closes_session_day` (and a live probe's open-ended reading) steps the calendar one - script width on. An early close the session string does not declare is invisible there: + eligible input slot on. An early close the session string does not declare is invisible there: streamed, the NYSE half day's 12:45 does not close its day, while a batch of the same bars closes it (the bar held after it is the next session day's) and TradingView's tape flags it (`test_stream_cannot_see_an_undeclared_early_close` in @@ -722,6 +722,20 @@ Two session-flag questions lane E26 left open were decided by R5 lane F5: a layout change inside the unshipped `engine_script_run_v18`, as R5 N14's move of the live-tail flags was, and moves no hash (none of them was folded). +### Wave G KERNEL-EDGE: rule 2 rulings + +| Kernel capability | Generic mechanism | Why no platform knob is added | Executed witness | +|---|---|---|---| +| FX roll predecessor | A curve step is measured between successive **walked** driver points. A synchronous `execute_current` fill keeps its own execution cursor and readback, but cannot replace the path predecessor, whose time may be earlier than the fill's decision floor. | The immutable FX curve and the path coordinates already determine each rate change for every host. A switch would only permit duplicate or missed checks. | `tests/test_native_kernel_edge.cpp` N1; audit F6a and `fxroll_recalc.cpp` | +| Session-day neighbours | Batch uses its held input neighbours. A stream or open-ended edge uses the calendar's previous or next eligible input slot, which skips a declared closed window without rolling the session day. | Session and timezone are run data. Neither a venue name nor an adapter callback chooses the neighbour. | `tests/test_native_kernel_edge.cpp` N2; `tests/test_native_session_day_facts.cpp` tapes; audit F10a/S7/F10c | +| Final script bucket | Batch end and `stream_end(true)` calculate a pending bucket if its final contributing input reached the calendar's last-traded close. `stream_end(false)` and a feed stopped inside its session leave it pending. | Completion follows the declared session and input intervals, not a source-language end-of-history flag. There is no completion policy field. | `tests/test_native_kernel_edge.cpp` B1; `final_interval.cpp` and `daily_facts.cpp` | +| Host report mark | `NativeStrategyHost::mark_native_report_point` exposes the existing kernel report producer to a C++ host that chose `KernelRecordedAtHostMarks`, and answers whether a mark was accepted. C still has no value for this policy. | A host supplies only the timestamp and cadence; the kernel defines the equity point for every venue. The existing report policy is the choice, so no new knob is needed. | `tests/test_native_kernel_edge.cpp` G1; `kc_athostmarks` | + +`FeedTolerant` warmup may omit in-session slots while realtime confirmed-bar +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. + ## 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 49ff5db7..76bc162d 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -47,18 +47,18 @@ 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:2678-2689; `entry()` pine_adapter.cpp:5082, `order()` pine_adapter.cpp:10270) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5532-5579; `submit` native_host.hpp:1178 | K | — | F:A1 O:A1 S:O2 | — | +| OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:2678-2689; `entry()` pine_adapter.cpp:5082, `order()` pine_adapter.cpp:10270) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5536-5583; `submit` native_host.hpp:1186 | 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:8133 → `Flatten{}` pine_adapter.cpp:8162-8163; `Reduce{ExplicitUnits}` pine_adapter.cpp:7280-7289; `ReverseTo` pine_adapter.cpp:6114-6115) | **yes** — `OrderIntent` native_order.hpp:229, reduce sizes native_order.hpp:229-236, `ReverseTo` native_order.hpp:60-62, `OpeningShape` native_order.hpp:894-898, `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:2648-3707 → `host.replace` pine_adapter.cpp:2664-2744) | **yes** — `replace` native_host.hpp:1183-1185, `cancel` native_host.hpp:1214; `ReplacedEvent` native_order.hpp:997-1011; result and event types `SubmitStatus` native_order.hpp:835-861, `ReplacedEvent` native_order.hpp:981-1047 | 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:4515-4561, `cancel_all` pine_adapter.cpp:17806-17821; `live_by_source_key_` pine_adapter.hpp:1983; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:3857-3922) | **partial** — single-handle `cancel` native_host.hpp:1214; no enumeration (`WorkingRequestCore::live()` is consumer-internal native_order.hpp:1645); labels exist but are unindexed (`Request::label` native_order.hpp:485) **Closed:** `cancel_all` native_host.hpp:1232, `cancel_where(comment)` native_host.hpp:1237 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1237 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1228 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:2648-3707 → `host.replace` pine_adapter.cpp:2664-2744) | **yes** — `replace` native_host.hpp:1192-1194, `cancel` native_host.hpp:1222; `ReplacedEvent` native_order.hpp:997-1011; result and event types `SubmitStatus` native_order.hpp:835-861, `ReplacedEvent` native_order.hpp:981-1047 | 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:4515-4561, `cancel_all` pine_adapter.cpp:17806-17821; `live_by_source_key_` pine_adapter.hpp:1983; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:3857-3922) | **partial** — single-handle `cancel` native_host.hpp:1222; no enumeration (`WorkingRequestCore::live()` is consumer-internal native_order.hpp:1645); labels exist but are unindexed (`Request::label` native_order.hpp:485) **Closed:** `cancel_all` native_host.hpp:1240, `cancel_where(comment)` native_host.hpp:1245 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1245 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1236 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:3982; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:6965-7316) | **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 | — | -| 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:5303-5338, `cohort_for` pine_adapter.cpp:2442; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7263-7280) | **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:1247, `CancelReason::OwnerGone` native_order.hpp:933 | 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:5303-5338, `cohort_for` pine_adapter.cpp:2442; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7263-7280) | **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:1255, `CancelReason::OwnerGone` native_order.hpp:933 | 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:2687-2693) | **yes** — `Group` / `GroupEffect` native_order.hpp:466-473, `CancelReason::Group` native_order.hpp:932-937, `ReservationReducedEvent` native_order.hpp:1156-1166 (deferred form native_order.hpp:1156-1166) | 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:1077-1078 | 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:12779; lowering `submit_one` pine_adapter.cpp:8830-8842, `BindCohort` pine_adapter.cpp:9331-9337, `submit_leg` pine_adapter.cpp:9355-9379; 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:1824); 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:8435-8438; `materialize_relative_exits` pine_adapter.cpp:17096-17137, pine_adapter.hpp:1812-1824) | **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:5458-5492. `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:992 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:18857-18886) | **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:1228 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:8435-8438; `materialize_relative_exits` pine_adapter.cpp:17096-17137, pine_adapter.hpp:1812-1824) | **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:5462-5496. `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:992 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:18857-18886) | **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:1236 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:1985-2003); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1116-1195); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3261-3428, `materialize_snapshot` pine_adapter.cpp:10670-10694), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1580-1603, `CloseCallsiteState` pine_adapter.hpp:1674-1689; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15406, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15640) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:649-651 | 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: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:855-887) | n/a | A | — | S:R4 O:A10 | TV half of OL9. **V19D-P1 (R5 lane B-ADAPTER):** a margin-call revival never restores an exit the script withdrew: `strategy.cancel` / `cancel_all` retire every leg of a dormant exit's lifecycle (`retire_cancelled_dormant_exits` pine_adapter.cpp:1795) and the revival skips it (`revive_brackets_after_margin` pine_adapter.cpp:1856), as the lab tv tapes badapter-v19dp1-control / -cancel (NYSE:F 15m) decide it — replayed by `tests/test_adapter_margin_revival_cancel.cpp` | @@ -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:5544-5566; `NativeTrailState` native_host.hpp:657-662, `trail_state` native_host.hpp:1065 | 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:5548-5570; `NativeTrailState` native_host.hpp:657-662, `trail_state` native_host.hpp:1065 | 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:11554-11566) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4399-4404) **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:11467-11604) stays A. | +| TR3 | Zero-distance trail, "ride the best, exit on any adverse tick" (adapter substitutes `tick * 0.5` pine_adapter.cpp:11554-11566) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4403-4408) **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:11467-11604) stays A. | | TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:3814-3860) | **partial** — the read exists `trail_state` native_host.hpp:1065; `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:9381-9386, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11467-11604, retained best `retained_trail_best` pine_adapter.hpp:287-289, operand / restart rules `source_trail_points` pine_adapter.cpp:8405-8631, `trail_one_shot` pine_adapter.cpp:9470-9599 | n/a | A | — | F:B3 S:T2 | R5-1 | @@ -77,12 +77,12 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | SZ1 | Explicit units (`Transact{signed}` after a grid floor pine_adapter.cpp:5692-5696, pine_adapter.cpp:5692) | **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:10705-11882) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:925-929, invoked native_execution_consumer.cpp:4804-4822, guarded native_execution_consumer.cpp:4804-4838 | 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:2536-2591) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4845-4847 **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:2488, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:453 (→ SZ10) | +| SZ2 | Host override of the resolved price / units (`resolve_terms` pine_adapter.cpp:10705-11882) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:925-929, invoked native_execution_consumer.cpp:4808-4826, guarded native_execution_consumer.cpp:4808-4842 | 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:2536-2591) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4848-4850 **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:2488, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:453 (→ SZ10) | | SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:2497-2499) | **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:2549, `typed_entry_units` pine_adapter.cpp:2567; `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:2528; `percent_commission_live_equity` pine_adapter.cpp:4121-4161) | **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:2454-2469, `PineSizingSnapshot` pine_adapter.hpp:147-154; fill-time exceptions `default_sized` pine_adapter.cpp:6435-6461) | **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:590, native_order.hpp:1375-1392; host-sized terms snap (`ExecutionGridPolicy` native_order.hpp:71-85); exact grid validation `execution_terms_grid_representable` native_execution_consumer.cpp:4859-4862 | 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:1375-1392; host-sized terms snap (`ExecutionGridPolicy` native_order.hpp:71-85); exact grid validation `execution_terms_grid_representable` native_execution_consumer.cpp:4863-4866 | 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:12253-12281, `compute_exit_reservation` pine_adapter.cpp:4204-4325) | **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:4256-4579, `reserved_exit_qty` pine_adapter.cpp:8757-9067) | **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:444-451, `source_money_floor_lot` pine_adapter.cpp:453; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:4236-4251) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1537-1556 | A | — | O:C8 F:C3 S:M4 S:S5 | R5-1 | @@ -94,20 +94,20 @@ 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:2302) | **yes** — `allowed_open_directions` native_run_spec.hpp:595 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2896-2904 | 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:593 tests the resulting book native_execution_consumer.cpp:2910-2913 | 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:13001-13004) is TV. | -| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:594 → `MaxOpenLots` native_execution_consumer.cpp:2915-2918 | 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:5970-6016, cap left unset `max_open_lots` pine_adapter.cpp:2301-2303) is TV. | -| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13055-13120 and leaves the kernel gate unset pine_adapter.cpp:13054-13056) | **yes** — `initial_margin_fraction` native_run_spec.hpp:596; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2933 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:2792 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. | -| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13710-13711, `margin_pct` pine_adapter.cpp:13118) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:596 **Closed:** `NativeMarginModel::initial_long` / `initial_short` native_run_spec.hpp:241, 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:13810-13988, `submit_margin_call_units` pine_adapter.cpp:13949-13995, `schedule_margin_call_path` pine_adapter.cpp:17959-18036) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:596-597; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2892 **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 | +| MG1 | Allowed opening directions (`allowed_open_directions` pine_adapter.cpp:2302) | **yes** — `allowed_open_directions` native_run_spec.hpp:596 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2899-2907 | 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:2913-2916 | 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:13001-13004) is TV. | +| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:595 → `MaxOpenLots` native_execution_consumer.cpp:2918-2921 | 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:5970-6016, cap left unset `max_open_lots` pine_adapter.cpp:2301-2303) is TV. | +| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13055-13120 and leaves the kernel gate unset pine_adapter.cpp:13054-13056) | **yes** — `initial_margin_fraction` native_run_spec.hpp:597; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2936 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:2795 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. | +| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13710-13711, `margin_pct` pine_adapter.cpp:13118) | **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:13810-13988, `submit_margin_call_units` pine_adapter.cpp:13949-13995, `schedule_margin_call_path` pine_adapter.cpp:17959-18036) | **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:2895 **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:13741-13777) | **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:975 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:179-200) | **no** **Closed:** `native_liquidation_price()` native_host.hpp:1285 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:16057-16073, `apply_fx_opening_margin_slice` pine_adapter.cpp:18037-18065) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1167 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:16548-16587) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1266 **Closed:** `NativeRiskLimits::max_drawdown` native_run_spec.hpp:310, 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 | +| MG8 | Liquidation price query (`compute_liquidation_price` pine_strategy_host.cpp:179-200) | **no** **Closed:** `native_liquidation_price()` native_host.hpp:1293 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:16057-16073, `apply_fx_opening_margin_slice` pine_adapter.cpp:18037-18065) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1175 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:16548-16587) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1274 **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:13090-13093) | **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:13671-13685, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14697-14727) | **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:14740-14974) | **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:13481-13560) | **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:1288 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 | +| MG14 | Risk-day boundary (`chart_day_key` pine_adapter.cpp:13481-13560) | **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:1296 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:13699, 4× shortfall `4.0 * minimum` pine_adapter.cpp:13758, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14466-14506, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:13990-14117, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:13990) | n/a | A | — | F:D2 F:G4 O:D4 S:M4 | R5-1 | | 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:3610-3677, `record_market_review` pine_adapter.cpp:14978-15036) | n/a | A | — | S:R2 | (single-source) | @@ -115,15 +115,15 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| CT1 | Calculate at script-bar close | **yes** — `on_native_bar` native_host.hpp:874, dispatched native_execution_consumer.cpp:7048-7069 (call site `sha256:fc29ccd415732193f24f2370c6aca8d6f8910bca5b95315e63e88f2befe8200b` native_execution_consumer.cpp:7059) | K | — | F:E1 O:F1 | — | -| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2259-2260) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7353-7357 | K | — | F:E2 O:F6 S:C1 | — | -| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:15893) | **yes** — `on_native_bar_open` native_host.hpp:870 runs before the open match native_execution_consumer.cpp:6910-6914; `execute_current` legal native_host.hpp:1077 | 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:6853 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:6904 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1046 is the lookahead-free bar so far at the cursor, and `NativeOpenBarView::OpenOnly` native_run_spec.hpp:108 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:874, dispatched native_execution_consumer.cpp:7052-7073 (call site `sha256:fc29ccd415732193f24f2370c6aca8d6f8910bca5b95315e63e88f2befe8200b` native_execution_consumer.cpp:7063) | K | — | F:E1 O:F1 | — | +| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2259-2260) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7372-7376 | K | — | F:E2 O:F6 S:C1 | — | +| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:15893) | **yes** — `on_native_bar_open` native_host.hpp:870 runs before the open match native_execution_consumer.cpp:6914-6918; `execute_current` legal native_host.hpp:1077 | 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:6857 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:6908 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1046 is the lookahead-free bar so far at the cursor, 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:856, `NativeInputContext` native_host.hpp:754-759 | 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:681; `begin_coof_recalc` pine_adapter.cpp:5400, `flush_coof_tail` pine_adapter.cpp:16035, `end_coof_recalc` pine_adapter.cpp:5421) | **partial** — `on_native_applied` native_host.hpp:918 fires mid-path after each fill native_execution_consumer.cpp:6764, native_execution_consumer.cpp:6743-6773; commands are legal there `commands_allowed` native_execution_consumer.cpp:1376-1384; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5458-5492, `remaining_path_delivery` native_order.cpp:2037-2045. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:95 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:859 fires per accepted realtime print native_execution_consumer.cpp:6992-6996; 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:96 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:284-286. | -| 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:7347-7464 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:908 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:2389-2445) | **yes** — `IntrabarPath` native_run_spec.hpp:361-401, delivery native_execution_consumer.cpp:7291-7482; `NativePathOrder` native_run_spec.hpp:334-338 | K+A | — | F:E7 F:G5 O:F7 S:C4 | **Disagree:** S partial (it wants callback cadence → CT8). TV sampler choice is A. | +| 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:681; `begin_coof_recalc` pine_adapter.cpp:5400, `flush_coof_tail` pine_adapter.cpp:16035, `end_coof_recalc` pine_adapter.cpp:5421) | **partial** — `on_native_applied` native_host.hpp:918 fires mid-path after each fill native_execution_consumer.cpp:6768, native_execution_consumer.cpp:6747-6777; commands are legal there `commands_allowed` native_execution_consumer.cpp:1376-1384; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5462-5496, `remaining_path_delivery` native_order.cpp:2037-2045. 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:859 fires per accepted realtime print native_execution_consumer.cpp:6996-7000; 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:284-286. | +| 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:7366-7483 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:908 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:2389-2445) | **yes** — `IntrabarPath` native_run_spec.hpp:362-402, delivery native_execution_consumer.cpp:7310-7501; `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:233-319) 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:5547), 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:681, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1655-1662 | n/a | A | — | F:E4 O:F5 S:C2 | R5-5: the adapter keeps its path | @@ -131,38 +131,38 @@ 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:1513-1516, pumped from the scheduler `scheduler_prepare_security_sequence` pine_strategy_host.cpp:1421-1577; 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:1514 **Closed:** `NativeRunSpec::subscriptions` (`NativeTimeframeSubscription` native_run_spec.hpp:484), or `declare_timeframe_subscriptions` native_host.hpp:1101 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:866 and `native_series_bar(index)` native_host.hpp:1084 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:1407-1423 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, sole caller pine_strategy_host.cpp:1546; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1425-1430 **Closed:** a bare host's subscriptions consume authoritative bars: `NativeTimeframeSubscription::authoritative_bars` native_run_spec.hpp:486 is staged into the feed store by the kernel's own `prepare_native_security_feeds` native_execution_consumer.cpp:8089 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). | +| HT1 | HTF series of the same symbol (`request.security`): evaluators registered `scheduler_configure_security_evaluators` pine_strategy_host.cpp:1513-1516, pumped from the scheduler `scheduler_prepare_security_sequence` pine_strategy_host.cpp:1421-1577; 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:1514 **Closed:** `NativeRunSpec::subscriptions` (`NativeTimeframeSubscription` native_run_spec.hpp:485), or `declare_timeframe_subscriptions` native_host.hpp:1109 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:866 and `native_series_bar(index)` native_host.hpp:1092 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:1407-1423 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, sole caller pine_strategy_host.cpp:1546; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1425-1430 **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:8120 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). | | 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:908 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:513) 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. | +| 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:856; the Pine scheduler asks the kernel's `script_bucket_completions` native_execution_consumer.cpp:7704-7721, which feeds the same class `TimeframeAggregator` native_execution_consumer.cpp:7711, 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:856; the Pine scheduler asks the kernel's `script_bucket_completions` native_execution_consumer.cpp:7725-7742, which feeds the same class `TimeframeAggregator` native_execution_consumer.cpp:7732, 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:5532-5579, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:6115-6123; `Limit::fill_through` native_order.hpp:239-244 enforced native_execution_consumer.cpp:4949-4962, re-checked after host terms native_execution_consumer.cpp:4949-4969 | K | — | F:G3 O:H1 O:H2 O:H3 | — | -| FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:582 applied once native_execution_consumer.cpp:4546-4547, clamp native_execution_consumer.cpp:4546 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:11721-11730) 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:5536-5583, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:6119-6127; `Limit::fill_through` native_order.hpp:239-244 enforced native_execution_consumer.cpp:4956-4969, re-checked after host terms native_execution_consumer.cpp:4953-4973 | 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:4549-4550, clamp native_execution_consumer.cpp:4550 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:11721-11730) is A | | FP3 | Commission: percent / cash per unit / cash per execution (`fee_kind_for` pine_adapter.cpp:463-470) | **yes** — `NativeFeeKind` native_run_spec.hpp:19-23; `quote_execution_commissions` engine_execution.cpp:1474-1510 | K | — | F:G2 O:H8 S:F1 | — | -| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:256-259, `source_bar_fill` pine_adapter.cpp:11737-11746, `directional_tick` pine_adapter.cpp:301-305) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4008; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6197-6198; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:117) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:127) 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:256-259, `source_bar_fill` pine_adapter.cpp:11737-11746, `directional_tick` pine_adapter.cpp:301-305) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4012; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6200-6201; 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:332-380) 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:1167 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:8519). **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:1175 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:8550). **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:339-387 (used `source_trigger_threshold` pine_adapter.cpp:8822-8827, `exit_limit_trigger` pine_adapter.cpp:9356-9377), `source_bar_fill_tick` pine_adapter.cpp:277-290, raw-vs-booked spelling pine_adapter.cpp:11220-11326, margin-call fill pricing pine_adapter.cpp:13786-13795 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:11446) | 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:2575-2588, closing-row fold `max_runup` engine_orders.cpp:157-171; `owns_lot_excursions` native_host.hpp:1001-1005 | 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:2578-2591, closing-row fold `max_runup` engine_orders.cpp:157-171; `owns_lot_excursions` native_host.hpp:1001-1005 | 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). | | 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:62 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 | +| 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:1713) | **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:1438) stay A, measured in §3.7 part 2. | | 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:1192, enabled `stream_observe_actions_` native_execution_consumer.cpp:9084, 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:1335, native_execution_consumer.cpp:10470; 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:1192, enabled `stream_observe_actions_` native_execution_consumer.cpp:9124, 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:1343, native_execution_consumer.cpp:10523; 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:1955 **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` in batch and across a stream's warmup and realtime legs. 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. What a recorded row costs — quadratic on the Pine adapter, O(closed rows) per row in the kernel — is measured and ruled in §3.8 | | 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:2195-2196, `chart_timezone` pine_adapter.cpp:2236-2238) | **yes** — spec fields `ticker` native_run_spec.hpp:566-576; `parse_session` native_calendar.hpp:173-419; intervals on every decision context `input_interval` market_driver.hpp:117-118 | K | — | F:J2 O:G-5 | — | +| OT2 | Symbol info, sessions, calendars, timezones (`timezone` pine_adapter.cpp:2195-2196, `chart_timezone` pine_adapter.cpp:2236-2238) | **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:117-118 | 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:2157, `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:546 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:900 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 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:546 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:902 among them (F §2.iii) | | OT6 | C-level order submission + strategy callbacks | **no** — none of the 57 `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 symbols are a second, disjoint inventory beside the 57 codegen-facing ones (`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:154 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:1441 — from the run's own calendar and input (`present_session_day` native_execution_consumer.cpp:7114), 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:1794 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:155 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:1444 — from the run's own calendar and input (`present_session_day` native_execution_consumer.cpp:7116), 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:1794 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:287-289) | 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,15 +204,15 @@ 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:739; the source host casts it back `overrides_opaque` pine_strategy_host.cpp:291-294 | 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:323-352; 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:882-884 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1812-1815; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1845, `:8580`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2249-2250. 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:739) | 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:882-884 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1812-1815; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1845, `:8670`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2249-2250. 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:445-451) | 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:6268 | 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 native_execution_consumer.cpp:6272 | 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:18857-18886 | 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 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:566) 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:602. 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: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:1271 | 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 | @@ -262,12 +262,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 `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:2910-2920 | F O | +| 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:2913-2923 | F O | | Legality | The existing rule (`commands_allowed` native_execution_consumer.cpp:1376-1383): 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 | -| 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:546 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:900) | 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:546 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:902) | F S | Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the epoch batch. @@ -276,8 +276,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:638-652, `contribute_input` native_execution_consumer.cpp:7828, `seal_script` native_execution_consumer.cpp:7805) 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:6942), 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:638-652, `contribute_input` native_execution_consumer.cpp:7859, `seal_script` native_execution_consumer.cpp:7826) 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:6946), 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 | @@ -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:2896-2963) 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:2905-2972) 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). @@ -314,7 +314,7 @@ 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:2989-2991, `sha256:b609622f89aaeafb07764c51e9529fee6c29e12c28fa77db7d728131e4583bcc` native_run_spec.hpp:19-176, `sha256:f4041a4df37f679d9055e856e1cc247a9ce5e1e80f01a49f9cdd822631f352c6` native_run_spec.hpp:173-174 | O S | +| 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:2992-2994, `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:1600-1601 | 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 | @@ -336,7 +336,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:8954-8994 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:8994-9034 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:1046; and `set_native_security_feed` documented as the pre-run door for a series' authoritative bars, refused in-run | --- @@ -403,10 +403,10 @@ 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:63), `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:2298, 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:2298, 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:5463-5467) and `drain_applied_notifications` (native_execution_consumer.cpp:6124-6137) 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:5467-5471) and `drain_applied_notifications` (native_execution_consumer.cpp:6127-6140) 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:1334-1418 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) | @@ -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:4845-4847). 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 (`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:4848-4850). 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 @@ projected the spec (pine_strategy_host.cpp:244) 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:607-614,504`, folded at +begin-time declaration (`native_run_spec.hpp:608-615,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 @@ -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:238-301 and `sha256:efad6b9c7bcd3f7d0e459d434ef088a61b18c546d53cfb107f481a227fbe532b` native_execution_consumer.cpp:3250-3439 +`sha256:8c97d3e1d9d6f036a9be0b50a3a43188f1e2eee23da0384fbed242829108c3c3` native_run_spec.hpp:239-302 and `sha256:efad6b9c7bcd3f7d0e459d434ef088a61b18c546d53cfb107f481a227fbe532b` native_execution_consumer.cpp:3253-3442 (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 | @@ -1161,7 +1161,7 @@ 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 `sha256:620926caaa868e64ede32f547ac1dd0bfe81c9758267593a9a924051208df0a4` native_execution_consumer.hpp:937-941); 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:12916-12983, pine_adapter.cpp:13030); its chronology lives at `sha256:bb89768c9a7b2dc9f6de5943c04de8103ed107c21bb6e184446de0e6a84cf4c2` pine_adapter.cpp:13589-13666 and its excursion sample differs by chronology (`sha256:764a1a69f2940863b4f77cbf9d20267a19ec38e86720796c7081a59ac7245427` pine_strategy_host.cpp:729-759) | 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:2296-2336) | 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:5547) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `sha256:86617ca43bb2847380f1845b07cc9ecd27059814ee80fecf80649a9393dfe8c8` native_execution_consumer.cpp:3427-3436 and `drain_applied_notifications` (native_execution_consumer.cpp:6124-6137); new behaviour only behind new opt-ins | F | +| E4 | **Calc on fill.** Kernel suffix eligibility is continuous along the segment; TV refills only at O/H/L/C waypoints (`sha256:ddad8dc42e078838c0d53fd49e50c31060221e3b82e7d5bbaa0420769daa7d31` pine_scheduler_native.cpp:585-637, `coof_next_waypoint` pine_adapter.cpp:5547) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `sha256:86617ca43bb2847380f1845b07cc9ecd27059814ee80fecf80649a9393dfe8c8` native_execution_consumer.cpp:3430-3439 and `drain_applied_notifications` (native_execution_consumer.cpp:6127-6140); 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:15893-15921) | 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:1600-1619) | 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:332-380) 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 | @@ -1169,7 +1169,7 @@ unchanged after it. | 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 | | 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:323, `NativeFeedTolerance` native_run_spec.hpp:344; `NativeLegacyTolerance` native_run_spec.hpp:356 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 | +| 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 | ### 4.2 Process risks @@ -1191,7 +1191,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:1272); between realtime inputs there is none — reject or fall back to `AtMatch`? | F: reject with `InvalidQuantityBasis` | +| Q4 | `SizeTime::AtAcceptance` needs a current execution point (`sha256:00d2682bf92be87d76eea9d03004abad74aee8b5ad03e3f611dbb0d6539a99c4` native_host.hpp:1280); 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 | @@ -1229,11 +1229,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:7036, native_execution_consumer.cpp:7036, native_execution_consumer.cpp:7036) 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): the same recording, at the points the host marks (`mark_script_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. It is what the Pine adapter declares (`KernelRecordedAtHostMarks` pine_adapter.cpp:2298), 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:7040, native_execution_consumer.cpp:7040, native_execution_consumer.cpp:7040) 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:2298), 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:1003). 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:4940-4982), `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:4944-4986), `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) @@ -1256,7 +1256,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:663; terms resolution `sha256:b06b928e9f12217d7d3036f6adf77250c691c2ab1ad69d48e87417dbdfe1adc7` native_execution_consumer.cpp:2902-2948). A host override of `resolve_execution_terms` keeps the last word. Non-representable → `MatchRejectReason::TermsUnresolved` (native_order.hpp:875); 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:663; terms resolution `sha256:b06b928e9f12217d7d3036f6adf77250c691c2ab1ad69d48e87417dbdfe1adc7` native_execution_consumer.cpp:2905-2951). A host override of `resolve_execution_terms` keeps the last word. Non-representable → `MatchRejectReason::TermsUnresolved` (native_order.hpp:875); 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. @@ -1285,7 +1285,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:12912-12933). New hashed `RequestDefinition::origin { Host, KernelLiquidation, KernelRisk }`, new `CancelReason::Superseded` on re-price, `MarginCallEvent` joins `CommandEvent` (17 → 18, native_order.hpp:1262). O's alternative, a checkpoint on the excursion walk (`sha256:ed6f4ff668195caf3c953c8ef5dd448c6010949353f7b9ef44d50b764f7488db` native_execution_consumer.cpp:1874-1887), 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:4614-4649 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:4618-4653 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) @@ -1305,9 +1305,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:1603-1604) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6826-6838) 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:3440-3449). `born_on_remaining_path` (native_execution_consumer.cpp:5463-5467) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1655-1662) 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:1053-1060, whose body is native_execution_consumer.cpp:6676-6683). `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:1603-1604) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6830-6842) 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:3443-3452). `born_on_remaining_path` (native_execution_consumer.cpp:5467-5471) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1655-1662) 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:1053-1060, whose body is native_execution_consumer.cpp:6680-6687). `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:2522); 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). @@ -1346,7 +1346,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 (`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:3495-3500) +// 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:3499-3504) // 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; @@ -1374,7 +1374,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:3897-3919, `execute_current` path native_execution_consumer.cpp:3717-3727), `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:3901-3923, `execute_current` path native_execution_consumer.cpp:3721-3731), `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). ### A.8 L9 — risk limits (F P3 + O G7 + S P5) @@ -1391,10 +1391,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:1294) (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:1302) (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. - 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:13465-13563); 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:4614-4649 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:4618-4653 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 f1931744..7e13f15f 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:230 and - `RELEASE_MIN_TESTS` ci_verify.py:333. Adding rows means raising the floor + *ran* against a floor: `KERNEL_MIN_TESTS` ci_verify.py:231 and + `RELEASE_MIN_TESTS` ci_verify.py:335. Adding rows means raising the floor in the same commit. ## The recipe for a lane @@ -227,7 +227,7 @@ decision, and removing one is a regression: | `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` | -| `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:310 and native_run_spec.hpp:352 | +| `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 | ## Glossary @@ -241,7 +241,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:826, or C through the -`strategy_native_*` surface `sha256:1a4d41202e1b32a33ab75c2e42a36285ccb1d061087a8db1c19162b244c5b29f` native_c_api.h:2607. +`strategy_native_*` surface `sha256:1a4d41202e1b32a33ab75c2e42a36285ccb1d061087a8db1c19162b244c5b29f` native_c_api.h:2609. **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 @@ -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:230, `RELEASE_MIN_TESTS` ci_verify.py:333). It +(`KERNEL_MIN_TESTS` ci_verify.py:231, `RELEASE_MIN_TESTS` ci_verify.py:335). 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/live-surface.md b/docs/pages/live-surface.md index 9f832fa5..49a301d7 100644 --- a/docs/pages/live-surface.md +++ b/docs/pages/live-surface.md @@ -68,7 +68,7 @@ configuration**, not one-shot: it stays in effect until a caller passes day), pinned to agree with the `i+1` lookahead on every interior bar of the 24x7 lanes. Since R5 lane F5 that is the kernel's answer, not the host's: the tail bar reads `NativeDecisionContext::closes_session_day_open_ended` - (`market_driver.hpp:157`), the same fact as `closes_session_day` on every + (`market_driver.hpp:158`), the same fact as `closes_session_day` on every bar except a batch's final one, which it reads off the calendar instead of the run end. Historical bars read early closes correctly from the session day alone, with no holiday calendar; a *forming* tail bar diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index efc30e21..c78f286f 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -226,13 +226,13 @@ refusal. Each is generic, and all four are folded into the continuation identity unconditionally, including their defaults; changing any of them changes the identity. -- `slot_label_policy` (`NativeSlotLabelPolicy` `native_run_spec.hpp:323`) — +- `slot_label_policy` (`NativeSlotLabelPolicy` `native_run_spec.hpp:324`) — `Canonical` (default) requires every confirmed input to carry its calendar slot label (`interval.open_ms`, or the scheduled clipped `eligible_open_ms`). `FeedTolerant` accepts a provider's own strictly increasing labels instead. `LegacyTolerant` is the deprecated spelling of the same value and hashes identically. -- `legacy_tolerance` (`NativeFeedTolerance` `native_run_spec.hpp:344`) — a bit +- `legacy_tolerance` (`NativeFeedTolerance` `native_run_spec.hpp:345`) — a bit mask of admission exceptions for a tolerated feed shape, separate from slot labels because a host may want the tolerant price admission and canonical labels. `BatchStructuralBars` admits finite but non-positive OHLC and reads @@ -240,7 +240,7 @@ identity. finite non-negative interim warmup values, while the final warmup close stays strictly positive. `native_feed_tolerance_enabled` tests one bit. `NativeLegacyTolerance` is the deprecated spelling of the type. -- `path_order` (`NativePathOrder` `native_run_spec.hpp:334`) — `Auto` +- `path_order` (`NativePathOrder` `native_run_spec.hpp:335`) — `Auto` (default) keeps the open-proximity rule that decides whether a modeled bar walks its high or its low first. `HighFirst` / `LowFirst` state it, so a replay or a live host does not depend on that inference. @@ -257,7 +257,7 @@ explicitly instead of inventing a clock literal. It is incompatible with ### The intrabar path -`NativeRunSpec::intrabar` (`IntrabarPath` `native_run_spec.hpp:377`) is a +`NativeRunSpec::intrabar` (`IntrabarPath` `native_run_spec.hpp:378`) is a `std::variant` the spec owns — deliberately a spec value, not a caller borrow, because matching may need the finer bars later, while sealing an aggregated script bar: @@ -284,12 +284,12 @@ the sub-bar sections of `tests/test_native_calc_timing.cpp`. ### Validating a spec -`validate_native_run_spec(spec)` (`native_run_spec.hpp:805`) answers a -`NativeRunSpecValidation` (`native_run_spec.hpp:788`): a -`NativeRunSpecError` (`native_run_spec.hpp:677`) and the -`NativeRunSpecField` (`native_run_spec.hpp:645`) it first failed on, with +`validate_native_run_spec(spec)` (`native_run_spec.hpp:806`) answers a +`NativeRunSpecValidation` (`native_run_spec.hpp:789`): a +`NativeRunSpecError` (`native_run_spec.hpp:678`) and the +`NativeRunSpecField` (`native_run_spec.hpp:646`) it first failed on, with `ok()` and an explicit `operator bool`. `normalize_native_run_spec(spec)` -(`native_run_spec.hpp:815`) validates and rewrites the one admitted literal — +(`native_run_spec.hpp:816`) 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 @@ -307,7 +307,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:891`) is the portable +`native_run_spec_digest(spec)` (`native_run_spec.hpp:892`) 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 @@ -454,7 +454,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:8954-8994`), which admits the begin, checks the +(`native_execution_consumer.cpp:8994-9034`), 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 @@ -482,7 +482,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:2696-2746`), under the same legality rule; see *Driving the +(`native_c_api.h:2698-2748`), under the same legality rule; see *Driving the kernel from C* below. `native_order::Request` values belong to `native_order_v7` @@ -618,13 +618,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:5463-5467`). Requests accepted before the +`native_execution_consumer.cpp:5467-5471`). 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:6910-6912`). **Lookahead warning:** the +pass (`native_execution_consumer.cpp:6913-6915`). **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:6802`), the +`engine.current_bar_ = open_view` (`native_execution_consumer.cpp:6806`), 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 @@ -1300,8 +1300,8 @@ liquidated immediately as a current execution. Nothing fills mid-path. **The FX roll (`NativeMarginCheckKind::FxRoll`).** `required(P)` carries the account rate, so under a declared `NativeFxCurve` an account can cross its maintenance line while every price stands still. A step of the curve is -therefore a check point of its own: the first driver point the account -converts at a different rate than the point before it, offered immediately +therefore a check point of its own: the first walked driver point the account +converts at a different rate than the previous walked point, offered immediately before that point is matched. The walk has not moved yet — a discrete point is measured at its own price, a segment at its origin with its destination among the waypoints that remain — so the mark is the unchanged price and only the @@ -1808,8 +1808,8 @@ says otherwise, and the C spellings are in | `current_partial_bar()` | the lookahead-free bar so far at this cursor | `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:1320`) | 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:1325`) | 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:1328`) | 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:1333`) | 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 | @@ -1835,20 +1835,21 @@ rather than relabelled. Failure copy and move do not allocate. `NativeCoordinate` (`market_driver.hpp:71` — ordinal, the interval stamps, the effective time, the `NativePathPhase` and the `NativeCompletionKind`), the decision floor, both calendar intervals, and the script bar's -session-day facts (R5 lane F5): `in_session` (`market_driver.hpp:154`), +session-day facts (R5 lane F5): `in_session` (`market_driver.hpp:155`), `opens_session_day`, `closes_session_day` and `closes_session_day_open_ended`. They read the run's own calendar and session day — the cycle that rolls at the session's first window start, keyed to its trading date, so an overnight session is one day across local midnight — and the bar before or after the script bar: the one the run -holds (its batch input or stream warmup), otherwise the calendar's slot one -script width away. A run's first bar opens its session day and a batch's +holds (its batch input or stream warmup), otherwise the calendar's previous +or next eligible input slot, across declared breaks. A run's first bar opens +its session day and a batch's final bar closes it; the open-ended close reads the calendar at that final bar instead, for a host whose last input is still forming; a D/W/M bar holds whole days, so all four are true. Every callback of one script bar carries the same four, fills and fill recalculations included, and a C host reads the first three from `pf_native_decision_v1`'s session bytes -(`native_c_api.h:1441`). The kernel resolves each session day once through +(`native_c_api.h:1444`). 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 @@ -1863,8 +1864,8 @@ decision context plus the print's sequence, where zero keeps the public **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:1250`) enrolls one accepted opening's handle, -`cohort_remove` (`native_host.hpp:1253`) takes it back off, and a request with +(`native_host.hpp:1258`) enrolls one accepted opening's handle, +`cohort_remove` (`native_host.hpp:1261`) 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 @@ -1969,7 +1970,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:7631`), so +(`record_script_report_point`, `native_execution_consumer.cpp:7650`), so ```text broker_state_hash_len == equity_curve_len == script_bars_processed @@ -2024,9 +2025,11 @@ appends its own rows. `NativeReportPolicy::KernelRecordedAtHostMarks` records the very same series at the points the host marks, for a host whose report cadence is not one point per calculation. The consumer never records on its own initiative under it: -the host calls the kernel's report mark from inside its own callback, naming -the label the point carries, and the kernel performs the extremes fold and the -curve append. The Pine adapter is that host — its report has one point per +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. +The Pine adapter is one marking host — its report has one point per published *source* slot, which is not the same series of instants: a `calc_on_order_fills` re-entry marks the slot it opened at the fill and the bar's ordinary close calculation then marks nothing, and a suppressed probe @@ -2237,8 +2240,9 @@ binding a ledger). Stream D/W uses civil/session next-open stepping. Each confirmed input is attributed **wholly** to the script period that contains its input open (OHLC/volume are not split across a calendar boundary). Script calculation time is not earlier than the latest included -contributor close. An unfinished trailing coarser script bucket does not -become complete merely because batch input ended. +contributor close. A trailing coarser script bucket is calculated at batch +end when its final contributor reaches the calendar's last-traded close. +Input that stops inside the interval leaves its bucket unfinished. Session strings: empty or `"24x7"`; comma-separated `HHMM-HHMM` windows; `2400` as a legal end; overnight wrap; equal start/end as a full day from @@ -2250,8 +2254,12 @@ IANA aliases. **Batch** may be sparse (missing in-session slots are allowed; no synthetic prices). **Stream warmup** requires at least one input and every **provided** -input slot to be a complete confirmed interval; missing in-session bars are -refused. An unfinished coarser **script** bucket is retained through +input slot to be a complete confirmed interval. Under `Canonical`, missing +in-session bars are refused; under `FeedTolerant`, warmup may omit them, +including an entire trading day. Realtime `stream_push_bar` still refuses an +in-session gap under either policy. A calendar day mask must exclude days a +stream is expected to skip, such as weekends. An unfinished coarser **script** +bucket is retained through Warmup → Realtime. The handoff does not invent a script callback, a future exclusive close, or a raised floor to a future seal. Empty batch is a valid completed run with no events. @@ -2756,7 +2764,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:7711`). What it does +(`TimeframeAggregator` `native_execution_consumer.cpp:7732`). 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 @@ -2803,8 +2811,9 @@ synthesize neither bars nor matching points. `stream_advance_time(ms)` finishes elapsed intervals and raises the accepted time floor; it is not an extra print. `stream_end(false)` completes without inventing a final callback or fill. `stream_end(true)` may partial-finalize -an existing forming observed input; it does not synthesize a missing input -or force an incomplete coarser script bucket complete. +an existing forming observed input and calculates a pending script bucket +when its final contributor reached the calendar's last-traded close. It does +not synthesize a missing input or complete a bucket whose session is still open. The decision floor is monotonic (delivered event time, accepted input completion, tick time, time-advance). A refused preflight does not raise it. @@ -2841,7 +2850,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 (`sha256:2e963d6ab1630db1535bd944dc7406ba649e9d14a589065db25347569fbad150` native_c_api.h:37-39) take a `pf_strategy_t` from `strategy_native_host_create_v1` unchanged — and -`strategy_native_submit_v1` (`native_c_api.h:2646`) obeys the one legality +`strategy_native_submit_v1` (`native_c_api.h:2648`) 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 2dd38622..f820e566 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -62,7 +62,7 @@ gain: - **Your own decision model.** `on_native_bar` is an ordinary C++ member function (native_host.hpp:874). Any data structure, any library, any precomputation — no Pine type system, no series-of-everything. -- **Explicit setup.** One `NativeRunSpec` (native_run_spec.hpp:552) names the +- **Explicit setup.** One `NativeRunSpec` (native_run_spec.hpp:553) 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:981), `ExecutionAppliedEvent` (native_order.hpp:1120), `MatchRejectedEvent` (native_order.hpp:1082), `CancelledEvent` (native_order.hpp:1023) — readable in order from `native_events` - (native_host.hpp:1294). + (native_host.hpp:1302). - **Any language.** The same kernel drives from C through a callback table - (`pf_native_callbacks_v1` native_c_api.h:2422), so a host in Rust, Go, Python or Zig needs no C++. + (`pf_native_callbacks_v1` native_c_api.h:2425), so a host in Rust, Go, Python or Zig needs no C++. - **One binary, no transpiler.** Link `libpineforge`, ship an executable or a loadable module. @@ -110,32 +110,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:552) applied by `configure_native` -(native_host.hpp:1161); a C host sends `pf_native_run_spec_v1` -(pineforge.h:546) plus `pf_native_run_spec_ext_v1` (native_c_api.h:2250) -through `strategy_configure_native_ext_v1` (native_c_api.h:3075). Setup is +`NativeRunSpec` (native_run_spec.hpp:553) applied by `configure_native` +(native_host.hpp:1169); a C host sends `pf_native_run_spec_v1` +(pineforge.h:546) plus `pf_native_run_spec_ext_v1` (native_c_api.h:2253) +through `strategy_configure_native_ext_v1` (native_c_api.h:3077). 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:552 | `pf_native_run_spec_v1` pineforge.h:546 | `hello_kernel.cpp` / `hello_kernel_c.c` | One atomic setup value. `validate_native_run_spec` native_run_spec.hpp:805 names the offending field before a run begins. | -| `initial_capital` | `initial_capital` native_run_spec.hpp:578 | `initial_capital` pineforge.h:537 | `hello_kernel.cpp` | Required, positive. | -| `currency` | `currency` native_run_spec.hpp:569 | `currency` pineforge.h:535 | `hello_kernel.cpp` | With `ticker` native_run_spec.hpp:566, `tickerid` native_run_spec.hpp:567, `type` native_run_spec.hpp:568, `basecurrency` native_run_spec.hpp:570, `description` native_run_spec.hpp:571 and `volumetype` native_run_spec.hpp:572 this is Pine's whole `syminfo` block, named rather than inherited from a chart. | +| `strategy()` | `NativeRunSpec` native_run_spec.hpp:553 | `pf_native_run_spec_v1` pineforge.h:546 | `hello_kernel.cpp` / `hello_kernel_c.c` | One atomic setup value. `validate_native_run_spec` native_run_spec.hpp:806 names the offending field before a run begins. | +| `initial_capital` | `initial_capital` native_run_spec.hpp:579 | `initial_capital` pineforge.h:537 | `hello_kernel.cpp` | Required, positive. | +| `currency` | `currency` native_run_spec.hpp:570 | `currency` pineforge.h:535 | `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:538 | `native_open_lots_strategy.cpp` | `Percent`, `CashPerUnit`, `CashPerExecution` — the same three TradingView offers. | -| `commission_value` | `fee_value` native_run_spec.hpp:588 | `fee_value` pineforge.h:538 | `native_open_lots_strategy.cpp` | Quoted per execution by `quote_execution_commissions` engine_execution.cpp:1487. A cash-per-execution ticket is split by units over every slice of that execution. | -| `slippage` | `slippage_ticks` native_run_spec.hpp:582 | `slippage_ticks` pineforge.h:538 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:3906-3907. TradingView's round-then-slip order is adapter-only. | -| `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:539 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:7353-7354, so a request submitted in bar *k*'s calculation fills at bar *k*'s own close. | -| `pyramiding` | `max_open_lots` native_run_spec.hpp:594 | `max_open_lots` pineforge.h:541 | `native_open_lots_strategy.cpp` | Caps surviving+new physical lots `sha256:87bd0ff848901478840870d95eb356b35d7d368555ce8eadee8571b8e17f86ec` native_execution_consumer.cpp:2096-2099. Pine's per-cycle *entry count* is a TradingView rule, ruled **adapter-policy** in ADR-0001's table row `max_open_lots`. | -| `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:342 with `size_basis` native_c_api.h:2089 | `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:2093 | `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:93 | `calculation` native_c_api.h:2261 | `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:625. 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:93 | `calculation` native_c_api.h:2261 | `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:859 is one call per accepted realtime print whatever the trigger is. | -| `margin_long` | `NativeMarginModel::initial_long` native_run_spec.hpp:240 | `margin_initial_long` native_c_api.h:2271 | `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:241 | `margin_initial_short` native_c_api.h:2275 | `native_margin_strategy.cpp` | Per side, unlike the one-scalar `initial_margin_fraction` native_run_spec.hpp:596 the model replaces. `maintenance_long` native_run_spec.hpp:242 / `maintenance_short` native_run_spec.hpp:243 add the liquidation half Pine has no word for. | +| `commission_value` | `fee_value` native_run_spec.hpp:589 | `fee_value` pineforge.h:538 | `native_open_lots_strategy.cpp` | Quoted per execution by `quote_execution_commissions` engine_execution.cpp:1487. 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:538 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:3910-3911. TradingView's round-then-slip order is adapter-only. | +| `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:539 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:7372-7373, 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:541 | `native_open_lots_strategy.cpp` | Caps surviving+new physical lots `sha256:87bd0ff848901478840870d95eb356b35d7d368555ce8eadee8571b8e17f86ec` native_execution_consumer.cpp:2096-2099. Pine's per-cycle *entry count* is a TradingView rule, ruled **adapter-policy** in ADR-0001's table row `max_open_lots`. | +| `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:2091 | `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:2095 | `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:2263 | `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:2263 | `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:859 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:2273 | `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:2277 | `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:377 | `intrabar_kind` native_c_api.h:2314 | `tests/test_native_calc_timing.cpp` | The host owns the lower bars, as `lower_tf` native_run_spec.hpp:392 or `synthesized` native_run_spec.hpp:406. The C magnifier arguments of `run_backtest_full` pineforge.h:646 configure a *compiled Pine strategy*, never a native host. | +| `use_bar_magnifier` | `IntrabarPath` native_run_spec.hpp:378 | `intrabar_kind` native_c_api.h:2316 | `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:646 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`. | @@ -144,13 +144,13 @@ rather than defaulted. These have no Pine spelling at all, so they carry no row above; they are here because a host that ports a Pine strategy will meet them. -`NativeSlotLabelPolicy` native_run_spec.hpp:323 and `NativeFeedTolerance` -native_run_spec.hpp:344 admit a provider feed whose bar labels or values are -not canonical; `NativePathOrder` native_run_spec.hpp:334 forces the intrabar +`NativeSlotLabelPolicy` native_run_spec.hpp:324 and `NativeFeedTolerance` +native_run_spec.hpp:345 admit a provider feed whose bar labels or values are +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:739. +native_c_api.h:741. ## Design §1 inventory crosswalk {#pine_to_native_inventory} @@ -256,43 +256,43 @@ 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:333 | `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:366 | `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:367 | `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:360 | `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:361 | `native_selected_strategy.cpp` | Same. | +| `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.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:688 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` (native_host.hpp:927), 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:2514 for a close. +the same question through `on_close_units` native_c_api.h:2516 for a close. ## Orders {#pine_to_native_map_orders} -Everything here is one call: `submit` native_host.hpp:1178, or `submit_market` -native_host.hpp:1192 for the market-only shorthand, which *refuses* non-market +Everything here is one call: `submit` native_host.hpp:1186, or `submit_market` +native_host.hpp:1200 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:2646 over a -`pf_native_request_v1` native_c_api.h:2080. +is `strategy_native_submit_v1` native_c_api.h:2648 over a +`pf_native_request_v1` native_c_api.h:2083. | 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:2646 | `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:2646 | `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:2646 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:348 with `PF_NATIVE_OWNER_BIND_OPENING` native_c_api.h:453 | `native_selected_strategy.cpp` | Closes the lots one opening produced. The cycle comes from `ExecutionAppliedEvent::cycle_after` native_order.hpp:1139. 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. | -| `strategy.close_all()` | `Flatten` native_order.hpp:36 | `PF_NATIVE_INTENT_FLATTEN` native_c_api.h:331 | `hello_kernel.cpp` / `hello_kernel_c.c` | The whole book, in one settlement cycle. | -| `strategy.cancel()` | `cancel_where` native_host.hpp:1243 with `NativeRequestField::Label` native_host.hpp:679 | `strategy_native_cancel_where_v1` native_c_api.h:2733 | `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:1214 is the single-request form for a host that kept the handle. | -| `strategy.cancel_all()` | `cancel_all` native_host.hpp:1232 | `strategy_native_cancel_all_v1` native_c_api.h:2712 | `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:1228 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:472 | `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:473 | `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:466 | `hello_kernel.cpp` | The default: no group, no sibling effect. | -| `strategy.direction.all` | `NativeOpenDirections::Both` native_run_spec.hpp:75 | `allowed_open_directions` pineforge.h:539 | `native_risk_limits_strategy.cpp` | The default. | -| `strategy.direction.long` | `NativeOpenDirections::Long` native_run_spec.hpp:73 | `allowed_open_directions` pineforge.h:539 | `tests/test_native_run_spec.cpp` | An opening the direction forbids is refused with `OpeningDirection` native_execution_consumer.cpp:2903. | -| `strategy.direction.short` | `NativeOpenDirections::Short` native_run_spec.hpp:74 | `allowed_open_directions` pineforge.h:539 | `tests/test_native_run_spec.cpp` | Same, the other way. | +| `strategy.entry()` | `Request` native_order.hpp:483 carrying `Transact` native_order.hpp:41 | `strategy_native_submit_v1` native_c_api.h:2648 | `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:2648 | `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:2648 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:1139. 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. | +| `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:1251 with `NativeRequestField::Label` native_host.hpp:679 | `strategy_native_cancel_where_v1` native_c_api.h:2735 | `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:1222 is the single-request form for a host that kept the handle. | +| `strategy.cancel_all()` | `cancel_all` native_host.hpp:1240 | `strategy_native_cancel_all_v1` native_c_api.h:2714 | `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:1236 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.direction.all` | `NativeOpenDirections::Both` native_run_spec.hpp:76 | `allowed_open_directions` pineforge.h:539 | `native_risk_limits_strategy.cpp` | The default. | +| `strategy.direction.long` | `NativeOpenDirections::Long` native_run_spec.hpp:74 | `allowed_open_directions` pineforge.h:539 | `tests/test_native_run_spec.cpp` | An opening the direction forbids is refused with `OpeningDirection` native_execution_consumer.cpp:2906. | +| `strategy.direction.short` | `NativeOpenDirections::Short` native_run_spec.hpp:75 | `allowed_open_directions` pineforge.h:539 | `tests/test_native_run_spec.cpp` | Same, the other way. | | `strategy.commission.percent` | `NativeFeeKind::Percent` native_run_spec.hpp:20 | `fee_kind` pineforge.h:538 | `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:538 | `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:538 | `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. | @@ -302,7 +302,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:1185, which emits +quantity. Amending a live order is `replace` native_host.hpp:1193, which emits `ReplacedEvent` native_order.hpp:997 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 @@ -313,11 +313,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:385 | `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:386 | `native_price_grid_strategy.cpp` | Stop-limit is `StopLimit` native_order.hpp:252. | -| `trail_points` | `FromOwnerFill{+ticks, true}` native_order.hpp:331 | `arm_scope` / `arm_first_match` native_c_api.h:2163-2164 | `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` native_c_api.h:2098 | `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:2103 | `native_trail_risk_strategy.cpp` | The absolute level the trail arms at. Read the live projection with `trail_state` native_host.hpp:1065 (C: `strategy_native_trail_state_v1` native_c_api.h:2954). 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: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 | `arm_scope` / `arm_first_match` native_c_api.h:2165-2166 | `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` native_c_api.h:2100 | `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:2105 | `native_trail_risk_strategy.cpp` | The absolute level the trail arms at. Read the live projection with `trail_state` native_host.hpp:1065 (C: `strategy_native_trail_state_v1` native_c_api.h:2956). 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 @@ -343,7 +343,7 @@ once. Natively they are separate and each is nameable: in `ArmedEvent` native_order.hpp:1200. - **Invisible until armed.** `NativeArmVisibility::PendingUntilArmed` native_order.hpp:394 keeps the legs out of `native_working_requests` - native_host.hpp:1228 until the fill, like Pine's pending exit and like a + native_host.hpp:1236 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: @@ -372,10 +372,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:2646 with -`PF_NATIVE_OWNER_WAIT_FOR_APPLIED` native_c_api.h:452, and its `first_match` +each leg is its own `strategy_native_submit_v1` native_c_api.h:2648 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:2163-2164, so a C bracket child arms after its owner's print or over +native_c_api.h:2166-2167, 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 @@ -387,21 +387,21 @@ runs keep the adapter's fill-point submission. ## Risk limits {#pine_to_native_map_risk} Pine's `strategy.risk.*` are per-bar statements. Natively they are one opt-in -block, `NativeRiskLimits` native_run_spec.hpp:309, declared before the run and +block, `NativeRiskLimits` native_run_spec.hpp:310, declared before the run and 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:277 says — refuse openings, or first +`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:733 tail. +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:595 | `allowed_open_directions` pineforge.h:539 | `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:2903. | -| `strategy.risk.max_position_size()` | `max_abs_units` native_run_spec.hpp:593 | `max_abs_units` pineforge.h:540 | `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:310 | `risk_max_drawdown` native_c_api.h:2297 | `native_risk_limits_strategy.cpp` | `NativeLossLimit` native_run_spec.hpp:258 is account currency, or a percent of the running peak when `percent` native_run_spec.hpp:260 is set. Blocks openings to the end of the run. | -| `strategy.risk.max_intraday_loss()` | `NativeRiskLimits::max_intraday_loss` native_run_spec.hpp:311 | `risk_max_intraday_loss` native_c_api.h:2300 | `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:312 | `risk_max_consecutive_loss_days` native_c_api.h:2302 | `native_risk_limits_strategy.cpp` | Counts *days* that close with a realized loss; TradingView's streak counts trades. `NativeRiskDay` native_run_spec.hpp:269 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:313 | `risk_max_fills_per_day` native_c_api.h:2304 | `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:1288. | +| `strategy.risk.allow_entry_in()` | `allowed_open_directions` native_run_spec.hpp:596 | `allowed_open_directions` pineforge.h:539 | `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:2906. | +| `strategy.risk.max_position_size()` | `max_abs_units` native_run_spec.hpp:594 | `max_abs_units` pineforge.h:540 | `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:2299 | `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:2302 | `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:2304 | `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:2306 | `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:1296. | 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 @@ -414,27 +414,27 @@ is `tests/test_adapter_risk_relower.cpp`. ## Margin and liquidation {#pine_to_native_map_margin} Pine has `margin_long` / `margin_short` and a margin call it does not let you -describe. `NativeMarginModel` native_run_spec.hpp:239 is the broker model: +describe. `NativeMarginModel` native_run_spec.hpp:240 is the broker model: per-side opening requirements, per-side maintenance requirements, a solved 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:240 / `initial_short` native_run_spec.hpp:241 | `margin_initial_long` native_c_api.h:2271 | `native_margin_strategy.cpp` | -| maintenance requirement per side | `maintenance_long` native_run_spec.hpp:242 / `maintenance_short` native_run_spec.hpp:243 | `margin_maintenance_long` native_c_api.h:2276 | `native_margin_strategy.cpp` | -| how much a liquidation closes | `NativeLiquidationSizing` native_run_spec.hpp:139 | `margin_sizing` native_c_api.h:2266 | `native_margin_strategy.cpp` | -| when the requirement is tested | `NativeLiquidationCheck` native_run_spec.hpp:157 | `margin_check` native_c_api.h:2267 | `native_margin_strategy.cpp` | -| which equity it is tested against | `NativeMarginEquityBasis` native_run_spec.hpp:171 | `margin_equity_basis` native_c_api.h:2334 | `tests/test_native_margin_model.cpp` | -| which base the level is solved from | `NativeLiquidationLevelBase` native_run_spec.hpp:181 | `margin_level_base` native_c_api.h:2335 | `tests/test_native_margin_model.cpp` | -| the forced close's ticket | `liquidation_label` native_run_spec.hpp:250 | `margin_liquidation_label` native_c_api.h:2336 | `native_margin_strategy.cpp` | -| the host's own admission verdict | `validate_execution_precommit` native_host.hpp:940 | — C hosts gate through `on_precommit` native_c_api.h:2539 | `native_margin_strategy.cpp` | -| the host's own requirement | `resolve_margin_requirement` native_host.hpp:956 | `on_margin_requirement` native_c_api.h:2476 | `native_margin_strategy.cpp` | -| which check points run at all | `margin_check_allowed` native_host.hpp:968 | `on_margin_check` native_c_api.h:2485 | `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:924 | `native_fx_roll_strategy.cpp` | -| the host's own liquidation size | `resolve_margin_call_units` native_host.hpp:975 | `on_margin_call_units` native_c_api.h:2492 | `native_margin_strategy.cpp` | -| the liquidation event | `MarginCallEvent` native_order.hpp:1216 delivered to `on_native_margin_call` native_host.hpp:981 | `on_margin_call` native_c_api.h:2440 | `native_margin_strategy.cpp` | - -The one-scalar `initial_margin_fraction` native_run_spec.hpp:596 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:2273 | `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:2278 | `native_margin_strategy.cpp` | +| how much a liquidation closes | `NativeLiquidationSizing` native_run_spec.hpp:140 | `margin_sizing` native_c_api.h:2268 | `native_margin_strategy.cpp` | +| when the requirement is tested | `NativeLiquidationCheck` native_run_spec.hpp:158 | `margin_check` native_c_api.h:2269 | `native_margin_strategy.cpp` | +| which equity it is tested against | `NativeMarginEquityBasis` native_run_spec.hpp:172 | `margin_equity_basis` native_c_api.h:2336 | `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:2337 | `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:2338 | `native_margin_strategy.cpp` | +| the host's own admission verdict | `validate_execution_precommit` native_host.hpp:940 | — C hosts gate through `on_precommit` native_c_api.h:2541 | `native_margin_strategy.cpp` | +| the host's own requirement | `resolve_margin_requirement` native_host.hpp:956 | `on_margin_requirement` native_c_api.h:2478 | `native_margin_strategy.cpp` | +| which check points run at all | `margin_check_allowed` native_host.hpp:968 | `on_margin_check` native_c_api.h:2487 | `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:926 | `native_fx_roll_strategy.cpp` | +| the host's own liquidation size | `resolve_margin_call_units` native_host.hpp:975 | `on_margin_call_units` native_c_api.h:2494 | `native_margin_strategy.cpp` | +| the liquidation event | `MarginCallEvent` native_order.hpp:1216 delivered to `on_native_margin_call` native_host.hpp:981 | `on_margin_call` native_c_api.h:2442 | `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 ruled **adapter-policy** (ADR-0001's `initial_margin_fraction` row): the adapter answers TradingView's ten-significant-digit money admission itself with @@ -446,29 +446,29 @@ openings TradingView takes. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| script body at bar close | `on_native_bar` native_host.hpp:874 | `on_bar` native_c_api.h:2430 | `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:893, whose default forwards here. | -| every calculation, tagged with its reason | `on_native_recalculate` native_host.hpp:893 with `NativeCalculationReason` native_host.hpp:806 | `on_recalculate` native_c_api.h:2458 | `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:807 | `on_recalculate` native_c_api.h:2458 | `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:1741 | `native_market_strategy.cpp` | `Batch`, `Warmup`, `Realtime`. | -| bar-open pre-pass | `on_native_bar_open` native_host.hpp:870 | `on_bar_open` native_c_api.h:2429 | `native_calc_on_fills_strategy.cpp` | Receives the whole script bar by default; `NativeOpenBarView::OpenOnly` native_run_spec.hpp:108 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:1046 | `strategy_native_partial_bar_v1` native_c_api.h:2939 | `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:918 | `on_applied` native_c_api.h:2432 | `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:5221. | -| raw input before aggregation | `on_native_input` native_host.hpp:856 | `on_input` native_c_api.h:2427 | `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:908 | `on_sub_bar` native_c_api.h:2468 | `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:114 with `NativeCoordinate` market_driver.hpp:71 | `pf_native_decision_v1` native_c_api.h:1441 | `hello_kernel.cpp` | Copied onto the callback stack; mutating it changes nothing. In C++ `script_interval` market_driver.hpp:118 and `input_interval` market_driver.hpp:117 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 session-day facts are the three rows below. | -| `session.ismarket` | `in_session` market_driver.hpp:154 | `in_session` native_c_api.h:1459 | `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:1458 is 1. | -| `session.isfirstbar` | `opens_session_day` market_driver.hpp:155 | `opens_session_day` native_c_api.h:1460 | `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 slot one script width back; a run's first bar opens its day. | -| `session.islastbar` | `closes_session_day` market_driver.hpp:156, `closes_session_day_open_ended` market_driver.hpp:157 | `closes_session_day` native_c_api.h:1461 | `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:1060 | `price` native_c_api.h:1520 | `native_selected_strategy.cpp` | The readiness preview `inspect_current_execution` native_host.hpp:1072 is an observation, never an apply token; `execute_current` native_host.hpp:1077 is the apply. | +| script body at bar close | `on_native_bar` native_host.hpp:874 | `on_bar` native_c_api.h:2432 | `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:893, whose default forwards here. | +| every calculation, tagged with its reason | `on_native_recalculate` native_host.hpp:893 with `NativeCalculationReason` native_host.hpp:806 | `on_recalculate` native_c_api.h:2460 | `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:807 | `on_recalculate` native_c_api.h:2460 | `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:1744 | `native_market_strategy.cpp` | `Batch`, `Warmup`, `Realtime`. | +| bar-open pre-pass | `on_native_bar_open` native_host.hpp:870 | `on_bar_open` native_c_api.h:2431 | `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:1046 | `strategy_native_partial_bar_v1` native_c_api.h:2941 | `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:918 | `on_applied` native_c_api.h:2434 | `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:5225. | +| raw input before aggregation | `on_native_input` native_host.hpp:856 | `on_input` native_c_api.h:2429 | `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:908 | `on_sub_bar` native_c_api.h:2470 | `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:114 with `NativeCoordinate` market_driver.hpp:71 | `pf_native_decision_v1` native_c_api.h:1443 | `hello_kernel.cpp` | Copied onto the callback stack; mutating it changes nothing. In C++ `script_interval` market_driver.hpp:118 and `input_interval` market_driver.hpp:117 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 session-day facts are the three rows below. | +| `session.ismarket` | `in_session` market_driver.hpp:155 | `in_session` native_c_api.h:1461 | `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:1460 is 1. | +| `session.isfirstbar` | `opens_session_day` market_driver.hpp:156 | `opens_session_day` native_c_api.h:1462 | `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:157, `closes_session_day_open_ended` market_driver.hpp:158 | `closes_session_day` native_c_api.h:1463 | `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:1060 | `price` native_c_api.h:1523 | `native_selected_strategy.cpp` | The readiness preview `inspect_current_execution` native_host.hpp:1072 is an observation, never an apply token; `execute_current` native_host.hpp:1077 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:1257, - `native_marked_equity` native_host.hpp:1266, `native_open_lots` - native_host.hpp:1262, `native_liquidation_price` native_host.hpp:1285. +1. **Live, inside a callback** — `physical_position` native_host.hpp:1265, + `native_marked_equity` native_host.hpp:1274, `native_open_lots` + native_host.hpp:1270, `native_liquidation_price` native_host.hpp:1293. 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` @@ -481,7 +481,7 @@ migration decision: 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 -`NativeReportPolicy::KernelRecorded` native_run_spec.hpp:62 (or +`NativeReportPolicy::KernelRecorded` native_run_spec.hpp:63 (or `KernelRecordedAtHostMarks`); the default `HostRecorded` leaves that series, and every `pf_equity_stats_t` figure walked out of it, to the host. The equity **extremes** and the **position-size peaks** are not scoped that way: they are @@ -495,18 +495,18 @@ runs close the same trades for the same money. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `strategy.account_currency` | `currency` native_run_spec.hpp:569 | `currency` pineforge.h:535 | `hello_kernel.cpp` / `native_fx_roll_strategy.cpp` | Declared, never inferred. `account_fx` native_run_spec.hpp:580 is the one positive scalar that converts quote to account; a timestamped curve is `configure_native_fx_curve` native_host.hpp:1167. 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:578 | `initial_capital` pineforge.h:537 | `hello_kernel.cpp` | The value you declared, unchanged by the run. | -| `strategy.equity` | `native_marked_equity` native_host.hpp:1266 | `strategy_native_marked_equity_v1` native_c_api.h:2979 | `native_open_lots_strategy.cpp` | Pine marks at the current `close`; the native call takes the mark explicitly, so the caller says what "now" means. It equals the balance plus the open lots' `unrealized_pnl` native_host.hpp:341, which the example asserts at every calculation. | +| `strategy.account_currency` | `currency` native_run_spec.hpp:570 | `currency` pineforge.h:535 | `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:1175. 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:537 | `hello_kernel.cpp` | The value you declared, unchanged by the run. | +| `strategy.equity` | `native_marked_equity` native_host.hpp:1274 | `strategy_native_marked_equity_v1` native_c_api.h:2981 | `native_open_lots_strategy.cpp` | Pine marks at the current `close`; the native call takes the mark explicitly, so the caller says what "now" means. It equals the balance plus the open lots' `unrealized_pnl` native_host.hpp:341, which the example asserts at every calculation. | | `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:578 | `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:579 | `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` | The protected C++ accessor is signed (negative for losses), while the C report stores `gross_loss` as a positive magnitude. Convert C to the protected sign with `-gross_loss`; then `netprofit == grossprofit - C.gross_loss`. | | `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:2979 less the balance | `native_open_lots_strategy.cpp` | Takes the price to mark at, like every other native mark. | +| `strategy.openprofit` | `open_profit` `sha256:0490e46ca24b81c82c9549ce38af81be1ea888a60277263108cd26bf3f01e0c7` engine.hpp:1024 | `strategy_native_marked_equity_v1` native_c_api.h:2981 less the balance | `native_open_lots_strategy.cpp` | Takes the price to mark at, like every other native mark. | | `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.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:62. | +| `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. | @@ -522,22 +522,22 @@ runs close the same trades for the same money. | `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:2759 | `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:2759 | `native_selected_strategy.cpp` | Volume-weighted over the open lots. | -| `strategy.position_entry_name` | `native_open_lots` native_host.hpp:1262 then the last row's `entry_label` native_host.hpp:333 | `strategy_native_open_lot_get_v1` native_c_api.h:2800 | `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:1285 | `strategy_native_liquidation_price_v1` native_c_api.h:2987 | `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:582 | `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:580 is one scalar and `configure_native_fx_curve` native_host.hpp:1167 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:2761 | `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:2761 | `native_selected_strategy.cpp` | Volume-weighted over the open lots. | +| `strategy.position_entry_name` | `native_open_lots` native_host.hpp:1270 then the last row's `entry_label` native_host.hpp:333 | `strategy_native_open_lot_get_v1` native_c_api.h:2802 | `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:1293 | `strategy_native_liquidation_price_v1` native_c_api.h:2989 | `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:582 | `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:1175 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:582 | `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:1262 answers one `NativeOpenLot` +`native_open_lots(mark)` native_host.hpp:1270 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:2792 then -`strategy_native_open_lot_get_v1` native_c_api.h:2800 into a -`pf_native_open_lot_v1` native_c_api.h:1717, whose two strings borrow the +`strategy_native_open_lot_count_v1` native_c_api.h:2794 then +`strategy_native_open_lot_get_v1` native_c_api.h:2802 into a +`pf_native_open_lot_v1` native_c_api.h:1720, 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 @@ -550,19 +550,19 @@ native_order.hpp:445 names. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `strategy.opentrades` | `native_open_lots` native_host.hpp:1262 | `strategy_native_open_lot_count_v1` native_c_api.h:2792 | `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:1717 | `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:1717 | `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:1717 | `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:1717 | `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:1717 | `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:1717 | `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:1717 | `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:1717 | `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:1270 | `strategy_native_open_lot_count_v1` native_c_api.h:2794 | `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:1720 | `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:1720 | `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:1720 | `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:1720 | `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:1720 | `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:1720 | `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:1720 | `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:1720 | `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:1717 | `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:1720 | `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:1717 | `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:1720 | `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. | @@ -578,7 +578,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:613 adds. In C the report's +`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:1189, `strategy_closed_trade_exit_id` pineforge.h:1202, @@ -591,11 +591,11 @@ 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:1189 | `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:1189 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:1717) 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:1189 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:1720) 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:1202 | `native_margin_strategy.cpp` | For a kernel liquidation this is the model's own `liquidation_label` native_run_spec.hpp:250, which is how a reporting layer classifies the row. | +| `strategy.closedtrades.exit_id()` | `exit_id` engine.hpp:186 | `strategy_closed_trade_exit_id` pineforge.h:1202 | `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:1204 | `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` | | @@ -625,12 +625,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:484 in `subscriptions` native_run_spec.hpp:631, or `declare_timeframe_subscriptions` native_host.hpp:1101 inside `on_native_run_begin` native_host.hpp:853 | `strategy_native_declare_subscriptions_v1` native_c_api.h:2864 | `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:866 and the latest is pulled with `native_series_bar` native_host.hpp:1084. `authoritative_bars` native_run_spec.hpp:486 replace a completed bucket's OHLCV. The row's two delivery words are typed in C: `pf_native_lookahead_e` native_c_api.h:788 and `pf_native_gaps_e` native_c_api.h:799 (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:513 with `NativeSeriesSource::AuxiliaryFeed` native_run_spec.hpp:481 | `strategy_native_append_auxiliary_bars_v1` native_c_api.h:3137 | `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:377 and reads `on_native_sub_bar` native_host.hpp:908 instead. | -| `barmerge.gaps_off` | `NativeTimeframeSubscription::gaps` native_run_spec.hpp:488 set false | `pf_native_subscription_v1::gaps` = `PF_NATIVE_GAPS_HOLD` native_c_api.h:800 | `native_htf_strategy.cpp` | The delivered bucket stands until the next delivery replaces it. | -| `barmerge.gaps_on` | `NativeTimeframeSubscription::gaps` native_run_spec.hpp:488 set true | `pf_native_subscription_v1::gaps` = `PF_NATIVE_GAPS_CLEAR` native_c_api.h:802 | `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:487 set false | `pf_native_subscription_v1::lookahead` = `PF_NATIVE_LOOKAHEAD_AT_COMPLETION` native_c_api.h:789 | `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:487 set true | `pf_native_subscription_v1::lookahead` = `PF_NATIVE_LOOKAHEAD_AT_FIRST_INPUT` native_c_api.h:791 | `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:485 in `subscriptions` native_run_spec.hpp:632, or `declare_timeframe_subscriptions` native_host.hpp:1109 inside `on_native_run_begin` native_host.hpp:853 | `strategy_native_declare_subscriptions_v1` native_c_api.h:2866 | `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:866 and the latest is pulled with `native_series_bar` native_host.hpp:1092. `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:790 and `pf_native_gaps_e` native_c_api.h:801 (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:3139 | `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:908 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:802 | `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:804 | `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:791 | `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:793 | `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:582 | `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. | @@ -691,9 +691,9 @@ class HelloKernel : public pineforge::NativeStrategyHost { }; ``` -The host is a class; the run is one `NativeRunSpec` native_run_spec.hpp:552 +The host is a class; the run is one `NativeRunSpec` native_run_spec.hpp:553 naming the clock, instrument, account and fees; `configure_native` -native_host.hpp:1161 applies it and `run` engine.hpp:1709 drives the bars. +native_host.hpp:1169 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: @@ -711,21 +711,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:2422 table handed to -`strategy_native_host_create_v1` native_c_api.h:2605; the run is +The host is a `pf_native_callbacks_v1` native_c_api.h:2425 table handed to +`strategy_native_host_create_v1` native_c_api.h:2607; the run is `pf_native_run_spec_v1` pineforge.h:546 plus `pf_native_run_spec_ext_v1` -native_c_api.h:2250 through `strategy_configure_native_ext_v1` -native_c_api.h:3075, and `strategy_native_run_v1` native_c_api.h:2618 drives +native_c_api.h:2253 through `strategy_configure_native_ext_v1` +native_c_api.h:3077, and `strategy_native_run_v1` native_c_api.h:2620 drives the bars into a `pf_report_t` pineforge.h:469. Three rules make the C door behave like the C++ one: 1. **Every struct is tagged and size-prefixed.** Set `struct_size` - native_c_api.h:2081 and `version` native_c_api.h:2082; a mismatch is refused + native_c_api.h:2083 and `version` native_c_api.h:2084; 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:2845. + `Failed`, readable with `strategy_native_state_v1` native_c_api.h:2847. 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. @@ -740,7 +740,7 @@ has a C route: `validate_execution_precommit` native_host.hpp:940 is `on_precommit`, `resolve_anchored_level` native_host.hpp:992 `on_anchored_level`, the price and opening-shape halves of `resolve_execution_terms` native_host.hpp:927 `on_execution_terms`, `native_sized_units` -native_host.hpp:1279 `strategy_native_sized_units_v1` and the +native_host.hpp:1287 `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 lists every public member with either its C spelling or that reason, and @@ -786,9 +786,9 @@ spec.fee_value = 0.1; // commission_value spec.max_open_lots = 1; // pyramiding = 1 ``` -Nothing is inferred. `price_tick` native_run_spec.hpp:581 is the ladder the +Nothing is inferred. `price_tick` native_run_spec.hpp:582 is the ladder the next three blocks all measure against, and `fee_value` -native_run_spec.hpp:588 is a **percent**, spelled exactly as Pine's +native_run_spec.hpp:589 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. @@ -819,7 +819,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:746. +`MarginSideUndeclared` native_run_spec.hpp:747. 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. @@ -834,10 +834,10 @@ void on_native_run_begin() override { } ``` -`native_series_bar(0)` native_host.hpp:1084 then answers the latest completed +`native_series_bar(0)` native_host.hpp:1092 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:488 changes. +version, including what `gaps` native_run_spec.hpp:489 changes. ### 4. The entry: cash, with the fee reserved @@ -933,28 +933,28 @@ configuring, or stops charging what the Pine declares, fails it. 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:1161 refuses an incomplete value + them, and `configure_native` native_host.hpp:1169 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:1294. + `native_events` native_host.hpp:1302. 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:588 + The two exceptions keep Pine's own unit: `fee_value` native_run_spec.hpp:589 under `NativeFeeKind::Percent`, so `commission_value = 0.1` is `fee_value = 0.1`, and a `NativeLossLimit` whose `percent` - native_run_spec.hpp:260 is set, so `20.0` is twenty percent. + native_run_spec.hpp:261 is set, so `20.0` is twenty percent. 5. **The equity curve is opt-in; the rest of the report is not.** `trade_count` engine.hpp:1798 and `get_trade` engine.hpp:1799 are always complete, and so are the equity extremes and the position-size peaks, which every report policy folds. Only the recorded equity **series**, and the `pf_equity_stats_t` figures walked out of it, arrive with - `NativeReportPolicy::KernelRecorded` native_run_spec.hpp:62 alone — the + `NativeReportPolicy::KernelRecorded` native_run_spec.hpp:63 alone — the rule of [Position, account and report](@ref pine_to_native_map_report). ## Building and exporting {#pine_to_native_building} @@ -1029,8 +1029,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:1294, ownership (which opening a close consumed), and the - continuation hash `native_continuation_hash` native_host.hpp:1335. + native_host.hpp:1302, ownership (which opening a close consumed), and the + continuation hash `native_continuation_hash` native_host.hpp:1343. 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/market_driver.hpp b/include/pineforge/market_driver.hpp index 5edd678a..c2bf07f0 100644 --- a/include/pineforge/market_driver.hpp +++ b/include/pineforge/market_driver.hpp @@ -129,21 +129,22 @@ struct NativeDecisionContext { // day across local midnight). Every callback of the bar carries the same // four: its open, its sub-bars and ticks, its calculation, and every fill // and recalculation on it. - // in_session the script bar's label is in session. + // in_session the script bar has an eligible instant in session. // opens_session_day in session, and the bar before it is not, or is on // another session day. // closes_session_day in session, and the bar after it is not, or is on // another session day. // "The bar before / after" is the one the run holds — the batch input or - // stream warmup being consumed — and otherwise the calendar's slot one - // script width away. At the run's own edges nothing is held: its first bar + // stream warmup being consumed — and otherwise the calendar's previous + // or next eligible input slot, across declared breaks. At the run's own + // edges nothing is held: its first bar // opens its session day, and a batch's final bar closes it, because a // batch is complete input; a stream reads on, because it continues. // closes_session_day_open_ended // closes_session_day for a run whose input goes on // past it: a batch's final bar, which nothing held - // follows, is judged by the calendar one script width - // on, exactly as a stream's bar is, instead of by the + // follows, is judged by the calendar's next eligible + // input slot, exactly as a stream's bar is, instead of by the // run's end. Every other bar reads closes_session_day. // It is what a host that recomputes a batch whose // last input is still forming reads for that bar. diff --git a/include/pineforge/native_c_api.h b/include/pineforge/native_c_api.h index 187c9a5e..860c27b5 100644 --- a/include/pineforge/native_c_api.h +++ b/include/pineforge/native_c_api.h @@ -103,6 +103,8 @@ * strategy_native_execute_current_v1 answers the same verdicts as * pf_native_execute_outcome_e and pf_native_refusal_e * [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 * [C] native_series_bar strategy_native_series_bar_v1 * [C] declare_timeframe_subscriptions strategy_native_declare_subscriptions_v1 -- a row is * pf_native_subscription_v1, whose `lookahead` / `gaps` bools are diff --git a/include/pineforge/native_host.hpp b/include/pineforge/native_host.hpp index e1dc3b81..0d51111b 100644 --- a/include/pineforge/native_host.hpp +++ b/include/pineforge/native_host.hpp @@ -1076,6 +1076,14 @@ class NativeStrategyHost : public BacktestEngine { /// C spelling: strategy_native_execute_current_v1. NativeCurrentExecutionResult execute_current(const NativeCurrentExecution&); + /// Append one kernel-owned equity point at this host's report mark. + /// Returns true only in a running KernelRecordedAtHostMarks run; false + /// changes nothing. Call at the desired mark inside a native callback. + /// This is reporting only: it does not book cash or place an order. A + /// host that records per-point broker hashes still owns those hash rows. + /// No C spelling: the C spec cannot select KernelRecordedAtHostMarks. + bool mark_native_report_point(int64_t report_ts); + /// The latest completed bucket delivered for a declared subscription, or /// nullopt before its first delivery / for an unknown index. Legal inside /// every native callback, including on_native_timeframe_bar itself. A diff --git a/include/pineforge/native_run_spec.hpp b/include/pineforge/native_run_spec.hpp index da7d5e34..c8c3e96e 100644 --- a/include/pineforge/native_run_spec.hpp +++ b/include/pineforge/native_run_spec.hpp @@ -57,6 +57,7 @@ enum class NativeAbortReporting : std::uint32_t { /// what a report point is. The consumer never records on its own initiative /// under it, so it leaves the continuation identity exactly where /// HostRecorded leaves it (see hash_spec in native_execution_consumer.cpp). +/// A C++ host marks through NativeStrategyHost::mark_native_report_point. enum class NativeReportPolicy : std::uint32_t { HostRecorded = 0, KernelRecorded = 1, diff --git a/scripts/ci_verify.py b/scripts/ci_verify.py index a5e80933..5ca61146 100644 --- a/scripts/ci_verify.py +++ b/scripts/ci_verify.py @@ -227,7 +227,8 @@ # B-C-SURFACE and B-DOCS add no row. 272 registered, 271 run: the WebSocket # row still skips on a system libcurl. # C-SURFACE-1 adds the INT23 frozen-header decision-tail witness. -KERNEL_MIN_TESTS = 272 +# Wave G KERNEL-EDGE adds the source-free test_native_kernel_edge row. +KERNEL_MIN_TESTS = 273 # 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 @@ -329,8 +330,9 @@ # +1 INT24 test_adapter_dual_entry_tie (the ruling on B-ADAPTER's # finding 1) # B-C-SURFACE's witnesses are rows inside test_native_c_api. C-SURFACE-1 adds -# the INT23 frozen-header decision-tail row. No release row skips: 673 run. -RELEASE_MIN_TESTS = 673 +# the INT23 frozen-header decision-tail row. Wave G KERNEL-EDGE adds the same +# test_native_kernel_edge row in release. No release row skips: 674 run. +RELEASE_MIN_TESTS = 674 # PR-only registration floors. These are the complete CTest populations at # 91d65ad6 (INT24); the ordinary full-run floors above remain unchanged. # An excluded run must still discover at least this many rows before -LE. diff --git a/src/native_execution_consumer.cpp b/src/native_execution_consumer.cpp index 617b10dd..855bd551 100644 --- a/src/native_execution_consumer.cpp +++ b/src/native_execution_consumer.cpp @@ -1740,7 +1740,7 @@ uint64_t NativeExecutionConsumer::continuation_hash() const noexcept { } f.u(margin_point_ordinal_); f.u(margin_point_calls_); - // v19-B: the last two driver points' instants, which the FX-roll + // v19-B: the last two walked driver points' instants, which the FX-roll // check reads for its previous point now that no driver log is // kept. Only a staged curve reads them, so only it folds them. if (staged_fx_curve_) { @@ -2524,11 +2524,14 @@ void NativeExecutionConsumer::record_driver(const NativeDriverPoint& point) { // stress switch (set_retire_every_point); by default it closes at // script-bar boundaries only. if (retire_every_point_) retire_journal(); - // The FX-roll check's previous point, and the stream's high water, are - // kept whatever the retention. - driver_marks_[0] = driver_marks_[1]; - driver_marks_[1] = DriverMark{point.coordinate.ordinal, point.coordinate.effective_time_ms}; - if (driver_mark_count_ < 2) ++driver_mark_count_; + // FX rolls compare successive walked path points. A synchronous current + // execution has its own cursor (sometimes at the decision floor ahead of + // this path point), so it cannot become the path's FX predecessor. + if (point.coordinate.provenance != NativePriceProvenance::CurrentExecution) { + driver_marks_[0] = driver_marks_[1]; + driver_marks_[1] = DriverMark{point.coordinate.ordinal, point.coordinate.effective_time_ms}; + if (driver_mark_count_ < 2) ++driver_mark_count_; + } last_driver_ordinal_ = point.coordinate.ordinal; // The driver log is an owning readback surface (native_events), kept only // under NativeEventRetention::Full; since v19 no hash folds it. @@ -3449,9 +3452,10 @@ void NativeExecutionConsumer::calculation_margin_check_at( // // The account converts at account_currency_fx_at(effective time), so the rate // a driver point is walked under is a function of the immutable curve and of -// that point alone. A roll is a point whose rate is not its predecessor's in -// the driver log -- both already durable, digested state, so the detection -// adds none of its own and no continuation identity moves. The point is +// that point alone. A roll is a walked point whose rate is not its walked +// predecessor's -- both already durable, digested state, so the detection +// adds no new state. Its existing folded predecessor marks change in v19 +// only when a current execution occurred under a staged curve. The point is // offered immediately before it is matched, where the walk still stands: a // discrete point at its own price, a segment at its origin with its // destination among the waypoints that remain, so a level the new rate moved @@ -7105,12 +7109,10 @@ void NativeExecutionConsumer::invoke_callback(BacktestEngine& engine, const Bar& // HOLDS on that side when it holds one -- the pumped batch input or stream // warmup -- because the input, not the schedule, says where a trading day // actually stopped (an early close the session string does not declare, a -// holiday). With nothing held, the neighbour is the calendar's slot one script -// width away, except at the run's own edges: its first bar opens its session -// day, and a batch's final bar closes it, because a batch is complete input. -// A stream's bars read on, because the stream continues. "Opens" reads the bar -// before exactly as "closes" reads the bar after, so every path that holds a -// bar agrees on the pair by construction. +// holiday). With nothing held, the neighbour is the calendar's previous or +// next eligible input slot, except at the run's own edges: its first bar opens +// its session day, and a batch's final bar closes it because batch input is +// complete. A stream reads on. A declared break does not split a session day. void NativeExecutionConsumer::present_session_day(NativeDecisionContext& context, int64_t label) const { context.in_session = false; @@ -7126,22 +7128,25 @@ void NativeExecutionConsumer::present_session_day(NativeDecisionContext& context context.closes_session_day_open_ended = true; return; } - const SessionPoint here = session_point(label); + // The nominal script label may itself be in a declared break (an hourly + // 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); 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 SessionPoint there = session_point(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); return there.in_session && there.ordinal && here.ordinal && *there.ordinal == *here.ordinal; }; - const int64_t width = script_width_ms(); - std::optional step_before; - std::optional step_after; - if (width > 0) { - if (label >= std::numeric_limits::min() + width) step_before = label - width; - if (label <= std::numeric_limits::max() - width) step_after = label + width; - } // The bucket this bar's inputs went into, when any have: its first and // last input indices place it in the run. A stream whose warmup ended // inside a script bar seals that bar in realtime, and it is still the @@ -7156,7 +7161,31 @@ void NativeExecutionConsumer::present_session_day(NativeDecisionContext& context ? std::optional(pumped_last_label_) : pumped_script_label(script_.first_index - 1); } - if (!run_start && !before) before = step_before; + 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. + if (!session_day_memo_ || !session_day_memo_->holds(here_ms)) { + session_day_memo_ = native_calendar::session_day_at( + calendar_, here_ms, calendar_memo_); + } + if (session_day_memo_) { + for (auto it = session_day_memo_->spans.rbegin(); + it != session_day_memo_->spans.rend(); ++it) { + if (it->first >= here_ms) continue; + const int64_t end = std::min(here_ms, it->second); + if (end <= it->first) continue; + const int64_t instant = end - 1; + const auto previous = native_calendar::interval_containing( + calendar_, input_tf_, instant, calendar_memo_); + if (previous && previous->eligible_open_ms < here_ms) + before = previous->eligible_open_ms; + break; + } + } + } context.opens_session_day = run_start || !before || !same_day(*before); std::optional after; @@ -7175,9 +7204,9 @@ void NativeExecutionConsumer::present_session_day(NativeDecisionContext& context context.closes_session_day = !same_day(*after); context.closes_session_day_open_ended = context.closes_session_day; } else { - // Nothing held after the bar: the calendar's next slot, which is also - // the open-ended reading of a batch's final bar. - const bool scheduled = step_after && !same_day(*step_after); + // Nothing held after the bar: the calendar's next eligible input + // slot, which jumps across a declared break without ending the day. + 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; } @@ -7227,16 +7256,6 @@ std::optional NativeExecutionConsumer::pumped_script_label(int index) c return script->open_ms; } -// One script bar's width on a fixed (second / minute) timeframe, 0 otherwise. -int64_t NativeExecutionConsumer::script_width_ms() const noexcept { - if (!script_tf_.valid() || !script_tf_.is_fixed()) return 0; - const int64_t unit = script_tf_.unit() == native_calendar::TimeframeUnit::Second - ? 1000 : 60'000; - const int64_t count = script_tf_.count(); - if (count <= 0 || count > std::numeric_limits::max() / unit) return 0; - return count * unit; -} - void NativeExecutionConsumer::deliver_confirmed_script(BacktestEngine& engine, const Bar& bar, const NativeCoordinate& base) { const auto* spec = spec_ptr(); @@ -7652,11 +7671,13 @@ void NativeExecutionConsumer::record_script_report_point( // read the curve. Recording is still the kernel's: the host names when, not // what. Inert under every other policy, so a host that records its own // report, or one that asked for the per-calculation cadence, is unaffected. -void NativeExecutionConsumer::mark_script_report_point( +bool NativeExecutionConsumer::mark_script_report_point( BacktestEngine& engine, int64_t script_bar_ts) const { const auto* spec = spec_ptr(); - if (!spec || spec->report_policy != NativeReportPolicy::KernelRecordedAtHostMarks) return; + if (!std::holds_alternative(state_) || !spec + || spec->report_policy != NativeReportPolicy::KernelRecordedAtHostMarks) return false; record_report_point(engine, script_bar_ts); + return true; } // The fold and the append together, at one instant: the shape a policy whose @@ -7825,6 +7846,16 @@ void NativeExecutionConsumer::seal_script(BacktestEngine& engine, NativeCompleti script_.has_data = false; } +bool NativeExecutionConsumer::final_script_session_closed() const noexcept { + // A nominally unfinished bucket can be complete at the calendar's final + // traded input. Its last contributor reaches the script interval's + // last-traded close; a feed that merely stops mid-interval does not. + return script_.has_data && !script_.sealed && last_accepted_input_ + && script_.interval.last_traded_close_ms > script_.interval.eligible_open_ms + && last_accepted_input_->last_traded_close_ms + >= script_.interval.last_traded_close_ms; +} + bool NativeExecutionConsumer::contribute_input( BacktestEngine& engine, const Bar& bar, const native_calendar::NativeInterval& interval, @@ -8873,6 +8904,15 @@ void NativeExecutionConsumer::pump_batch(BacktestEngine& engine, const Bar* bars if (!check_abort(engine, NativeFailureOperation::Input)) return; presize_logs(static_cast(i) + 1); } + // stream_begin uses this pump for Warmup too. Its pending bucket must + // carry into Realtime; only a complete batch ends its input here. + const auto* running = std::get_if(&state_); + if (running && running->phase == NativeRunPhase::Batch + && final_script_session_closed()) { + seal_script(engine, NativeCompletionKind::Confirmed); + if (failed()) return; + script_ = ScriptBucket{}; + } } (void)check_abort_or_projection(engine, NativeFailureOperation::Input); } @@ -9503,16 +9543,25 @@ bool NativeExecutionConsumer::stream_end(BacktestEngine& engine, bool finalize_p return false; } if (!check_abort_or_projection(engine, NativeFailureOperation::Stream)) return false; - if (finalize_partial_input_bar && has_forming_) { + if (finalize_partial_input_bar) { PumpScope pump(*this); - auto forming_interval = native_calendar::interval_containing( - calendar_, input_tf_, forming_.timestamp, calendar_memo_); - if (forming_interval) { - if (!finalize_observed_tick_slot(engine, *forming_interval, - NativeCompletionKind::PartialFinalized)) { - return false; + if (has_forming_) { + auto forming_interval = native_calendar::interval_containing( + calendar_, input_tf_, forming_.timestamp, calendar_memo_); + if (forming_interval) { + if (!finalize_observed_tick_slot(engine, *forming_interval, + NativeCompletionKind::PartialFinalized)) { + return false; + } } } + if (final_script_session_closed()) { + processing_input_ = true; + seal_script(engine, NativeCompletionKind::Confirmed); + processing_input_ = false; + if (failed()) return false; + script_ = ScriptBucket{}; + } } if (failed()) return false; if (!check_abort_or_projection(engine, NativeFailureOperation::Stream)) return false; @@ -10339,6 +10388,10 @@ NativeCurrentExecutionResult NativeStrategyHost::execute_current(const NativeCur return NativeExecutionConsumer::bound(*this).execute_current(*this, command); } +bool NativeStrategyHost::mark_native_report_point(int64_t report_ts) { + return NativeExecutionConsumer::bound(*this).mark_script_report_point(*this, report_ts); +} + NativePhysicalPosition NativeStrategyHost::physical_position() const { return NativeExecutionConsumer::bound(*this).position(*this); } diff --git a/src/native_execution_consumer.hpp b/src/native_execution_consumer.hpp index 623630fd..4c24983a 100644 --- a/src/native_execution_consumer.hpp +++ b/src/native_execution_consumer.hpp @@ -321,8 +321,8 @@ class NativeExecutionConsumer final : public IExecutionConsumer { // Report truth at the host's own cadence // (NativeReportPolicy::KernelRecordedAtHostMarks): the host marks the // script bar it has just published and the kernel records the point. - // Reporting only, and inert under every other policy. - void mark_script_report_point(BacktestEngine& engine, int64_t script_bar_ts) const; + // Reporting only; false outside a running HostMarks run. + bool mark_script_report_point(BacktestEngine& engine, int64_t script_bar_ts) const; std::optional series_bar(std::size_t subscription) const; // NativeStrategyHost::declare_timeframe_subscriptions_result: replace the // staged list from inside on_native_run_begin, before the kernel @@ -969,6 +969,7 @@ class NativeExecutionConsumer final : public IExecutionConsumer { // was to seal or the sealed calculation succeeded. bool seal_stale_script(BacktestEngine& engine, std::int64_t script_key); void seal_script(BacktestEngine& engine, NativeCompletionKind kind); + bool final_script_session_closed() const noexcept; void deliver_confirmed_script(BacktestEngine& engine, const Bar& bar, const NativeCoordinate& base); void deliver_intrabar_script(BacktestEngine& engine, const Bar& bar, const NativeCoordinate& base); @@ -978,7 +979,7 @@ class NativeExecutionConsumer final : public IExecutionConsumer { // opening at `label`. When the script_ bucket is that bar, its input // indices place it in the run, and inside pump_batch its held neighbours // are the pumped inputs around it; everything else reads its neighbours - // off the calendar, one script width away. + // off the calendar's previous or next eligible input slot. void present_session_day(NativeDecisionContext& context, int64_t label) const; struct SessionPoint { bool in_session = false; @@ -1000,7 +1001,6 @@ class NativeExecutionConsumer final : public IExecutionConsumer { SessionPoint point; }; std::optional pumped_script_label(int index) const; - int64_t script_width_ms() const noexcept; int64_t calculation_time(const NativeCoordinate& base) const noexcept; // Report truth at the kernel's own cadence, one per script calculation. // Reporting-only throughout: these mark equity and synthesize report @@ -1532,7 +1532,7 @@ class NativeExecutionConsumer final : public IExecutionConsumer { // V19-B: the newest driver point and account observation the run // produced, retained or not -- the stream's high water, which // event_high_water() answers under every retention -- and the instants - // of the last two driver points, the only driver facts a decision reads + // of the last two walked driver points, the only driver facts a decision reads // (fx_roll_margin_check's previous point). The high waters are readback // state and fold nowhere; the two instants fold with the margin state // they serve. diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ee03ed78..7911c081 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -854,6 +854,9 @@ list(APPEND TEST_SOURCES test_native_bare_host_contracts) # overnight Tokyo session, streams and a fill recalculation. Source-free, so # it also runs in the kernel-only profile. 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 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_calendar_hash_witness.cpp b/tests/test_native_calendar_hash_witness.cpp index f79c40ef..9f90d93d 100644 --- a/tests/test_native_calendar_hash_witness.cpp +++ b/tests/test_native_calendar_hash_witness.cpp @@ -571,7 +571,9 @@ const Pinned kPinned[] = { {"london_dst", 0x36decf92d3c95856ULL, 400, 400, 400, 0, 0, 1600, 80, 11}, {"posix_overnight", 0x82af75049101b94aULL, 140, 140, 140, 0, 0, 560, 67, 9}, {"fixed_offset", 0x7be69391f0425aadULL, 900, 900, 900, 0, 0, 3600, 136, 19}, - {"ny_daily_over_hourly", 0x12d290fd048a6c7cULL, 12, 12, 300, 0, 0, 48, 11, 1}, + // expectation corrected (KERNEL-EDGE, v19): the calendar-closed final + // daily bucket is calculated at batch end, including its last trade. + {"ny_daily_over_hourly", 0xfa85a7f9a92aea39ULL, 13, 13, 300, 0, 0, 52, 14, 2}, {"ny_hourly_series", 0x9237cf98641532f4ULL, 234, 234, 234, 21, 0, 936, 42, 6}, {"tolerant_aggregating", 0x0110dff138377266ULL, 200, 200, 600, 0, 0, 800, 63, 9}, {"tolerant_stream_night", 0x7de1da848e8cb622ULL, 34, 34, 34, 0, 0, 136, 21, 3}, diff --git a/tests/test_native_htf_subscriptions_stream.cpp b/tests/test_native_htf_subscriptions_stream.cpp index 96721bef..b92fc8b4 100644 --- a/tests/test_native_htf_subscriptions_stream.cpp +++ b/tests/test_native_htf_subscriptions_stream.cpp @@ -8,7 +8,7 @@ // produces, compared element by element (both publication modes); // 2. live pushes continue the very bucket the warmup left open and deliver // it before the calculation of the bar that completed it; -// 3. a bucket still open when the stream ends is never delivered; +// 3. a bucket still open when stream_end(false) runs is never delivered; // 4. native_series_bar answers with the latest delivered bucket on both // sides of the warmup -> live boundary; // 5. a stream whose spec declares no series keeps this tree's event @@ -32,7 +32,8 @@ // nominal end, so the NEXT session's first bar seals it lazily -- once // inside the warmup, once live -- and that calculation reads the series // as its own session left them, before the new session's first bar -// reaches any of them (seal(k) -> deliver(i+1) -> calc(i+1)). +// reaches any of them (seal(k) -> deliver(i+1) -> calc(i+1)). A complete +// batch calculates its final clipped bar at its calendar close. #include @@ -886,9 +887,13 @@ void test_lazy_seal_precedes_the_pump() { want_completions.push_back(NativeCompletionKind::Confirmed); } } - // Seven script bars a day; the last session's clipped one has no - // successor to seal it, in a batch and on a stream alike. + // Seven script bars a day. A complete batch calculates its final clipped + // bar at the calendar close; stream_end(false) leaves that bar pending. CHECK(want_completions.size() == 20); + std::vector want_batch_log = want_log; + want_batch_log.push_back("bar@" + stamp(2 * per_session + 24)); + std::vector want_batch_completions = want_completions; + want_batch_completions.push_back(NativeCompletionKind::Confirmed); SeriesHost batch; batch.probes = 3; @@ -921,19 +926,21 @@ void test_lazy_seal_precedes_the_pump() { } } - // The stream is the batch, and both are the hand-built sequence. - check_logs_equal(batch.log, want_log); + // The two modes share every input and series delivery; batch alone + // calculates the calendar-closed final script bucket at run end. + check_logs_equal(batch.log, want_batch_log); check_logs_equal(stream.log, want_log); check_deliveries_equal(stream.deliveries, batch.deliveries); - CHECK(batch.completions == want_completions); + CHECK(batch.completions == want_batch_completions); CHECK(stream.completions == want_completions); // What the two lazily sealed calculations read, and how many script bars // had been calculated when the sealing input's own bucket arrived: the // clipped bar's calculation is already among them. for (const SeriesHost* host : {&batch, &stream}) { - CHECK(host->series_at_bar.size() == 20); - if (host->series_at_bar.size() != 20) continue; + const std::size_t expected = host == &batch ? 21 : 20; + CHECK(host->series_at_bar.size() == expected); + if (host->series_at_bar.size() != expected) continue; for (int day = 0; day < 2; ++day) { const std::size_t calc = static_cast(day) * 7 + 6; const int last = day * per_session + 25; @@ -960,6 +967,20 @@ void test_lazy_seal_precedes_the_pump() { } } + const int last = 3 * per_session - 1; + const Bar final_clipped_hour = hand_aggregate( + bars, last - 1, 2, bars[static_cast(last - 1)].timestamp); + const auto& final_series = batch.series_at_bar.back(); + CHECK(final_series.size() == 3); + if (final_series.size() == 3) { + check_series(final_series[0], bars[static_cast(last)], + "batch final clipped bar reads 15"); + check_series(final_series[1], final_clipped_hour, + "batch final clipped bar reads 60 gaps"); + check_series(final_series[2], final_clipped_hour, + "batch final clipped bar reads 60"); + } + // stream_end seals nothing and delivers nothing. const std::size_t deliveries_before_end = stream.deliveries.size(); CHECK(stream.stream_end(false)); diff --git a/tests/test_native_kernel_edge.cpp b/tests/test_native_kernel_edge.cpp new file mode 100644 index 00000000..c873bf08 --- /dev/null +++ b/tests/test_native_kernel_edge.cpp @@ -0,0 +1,268 @@ +// KERNEL-EDGE: four audit defects, each driven by a bare NativeStrategyHost. +// The copied audit probes and their before/after output live outside the tree. +#include + +#include +#include +#include +#include +#include + +using namespace pineforge; +namespace no = pineforge::native_order; + +namespace { + +int failures = 0; + +void check(bool ok, const char* row) { + std::printf("%s %s\n", ok ? "PASS" : "FAIL", row); + if (!ok) ++failures; +} + +NativeRunSpec base_spec(const char* id, const char* input, const char* script, + const char* session = "24x7", const char* zone = "UTC") { + NativeRunSpec spec; + spec.identity = {id, 1}; + spec.input_tf = input; + spec.script_tf = script; + spec.ticker = "EDGE"; + spec.tickerid = "TEST:EDGE"; + spec.type = "stock"; + spec.currency = "USD"; + spec.timezone = zone; + spec.session = session; + spec.initial_capital = 10000; + spec.point_value = 1; + spec.account_fx = 1; + spec.price_tick = 0.01; + return spec; +} + +struct RollHost final : NativeStrategyHost { + bool sync = false; + int bars = 0; + mutable int rolls = 0; + void on_native_bar(const Bar&, const NativeDecisionContext&) override { + if (bars++ == 0) { + no::Request limit{no::Transact{1.0}, "limit", ""}; + limit.trigger = no::Limit{99.5}; + (void)submit(limit); + } + } + void on_native_recalculate(const Bar& bar, const NativeDecisionContext& context, + NativeCalculationReason reason, + const no::ExecutionAppliedEvent* cause) override { + if (reason == NativeCalculationReason::BarClose) { + on_native_bar(bar, context); + } else if (sync && cause && cause->request().label == "limit") { + const auto accepted = submit({no::Transact{1.0}, "sync", ""}); + if (accepted.handle) (void)execute_current({*accepted.handle}); + } + } + bool margin_check_allowed(const NativeMarginCheckPoint& point) const override { + if (point.kind == NativeMarginCheckKind::FxRoll) ++rolls; + return true; + } +}; + +void fx_roll_witness() { + constexpr std::int64_t start = 1704067200000LL; + constexpr std::int64_t minute = 60'000; + const std::vector bars = { + {100, 100.2, 99.9, 100, 1, start}, + {100, 100.2, 99.9, 100, 1, start + minute}, + {100, 100.2, 99, 99.8, 1, start + 2 * minute}, + {99.8, 100, 99.7, 99.9, 1, start + 3 * minute}, + {99.9, 100, 99.8, 99.9, 1, start + 4 * minute}, + }; + auto spec = base_spec("edge-fx", "1", "1"); + spec.calculation = NativeCalculationTrigger::BarCloseAndFills; + NativeMarginModel margin; + margin.initial_long = 0; + margin.maintenance_long = 0.1; + margin.initial_short = 0.5; + spec.margin = margin; + for (bool sync : {false, true}) { + RollHost host; + host.sync = sync; + check(host.configure_native(spec).status == NativeSetupStatus::Applied, + "N1 configure"); + check(host.configure_native_fx_curve( + NativeFxCurve{{start + 3 * minute}, {2.0}}).status + == NativeSetupStatus::Applied, + "N1 curve"); + host.run(bars.data(), static_cast(bars.size())); + check(host.native_state().kind == NativeLifecycleKind::Completed, + "N1 completed"); + check(host.rolls == 1, sync ? "N1 one roll with execute_current" + : "N1 one roll without execute_current"); + } +} + +struct SessionFacts { + bool seen = false; + bool opens = false; + bool closes = false; +}; + +struct SessionHost final : NativeStrategyHost { + std::int64_t pre_label = 1704111300000LL; + std::int64_t reopen_label = 1704115800000LL; + SessionFacts pre_break; + SessionFacts reopen; + void on_native_bar(const Bar&, const NativeDecisionContext& context) override { + auto* row = context.script_bar_open_ms == pre_label ? &pre_break + : context.script_bar_open_ms == reopen_label ? &reopen : nullptr; + if (row) *row = {true, context.opens_session_day, context.closes_session_day}; + } +}; + +void session_witness() { + constexpr std::int64_t day = 1704067200000LL; // 2024-01-01 00:00 UTC. + std::vector bars; + for (int minute = 8 * 60; minute < 22 * 60; minute += 15) { + if (minute >= 12 * 60 + 30 && minute < 13 * 60 + 30) continue; + bars.push_back({100, 101, 99, 100, 1, day + minute * 60'000LL}); + } + auto spec = base_spec("edge-session", "15", "15", "0800-1230,1330-2200"); + SessionHost batch; + check(batch.configure_native(spec).status == NativeSetupStatus::Applied, + "N2 batch configure"); + batch.run(bars.data(), static_cast(bars.size())); + check(batch.pre_break.seen && !batch.pre_break.closes + && batch.reopen.seen && !batch.reopen.opens, + "N2 batch lunch is one session day"); + SessionHost stream; + check(stream.configure_native(spec).status == NativeSetupStatus::Applied, + "N2 stream configure"); + constexpr int warmup = (12 * 60 + 30 - 8 * 60) / 15; + bool ok = stream.stream_begin(bars.data(), warmup, "15", "15"); + for (std::size_t i = warmup; ok && i < bars.size(); ++i) ok = stream.stream_push_bar(bars[i]); + if (ok) ok = stream.stream_end(false); + check(ok, "N2 stream completed"); + check(stream.pre_break.seen && !stream.pre_break.closes + && stream.reopen.seen && !stream.reopen.opens, + "N2 stream lunch is one session day"); + + // The reopened hourly bucket is labelled 13:00 although its first + // eligible input is 13:30; both driving modes must read that same day. + spec.identity = {"edge-session-hour", 1}; + spec.script_tf = "60"; + SessionHost hourly_batch; + hourly_batch.pre_label = day + 12 * 60 * 60'000LL; + hourly_batch.reopen_label = day + 13 * 60 * 60'000LL; + check(hourly_batch.configure_native(spec).status == NativeSetupStatus::Applied, + "N2 hourly batch configure"); + hourly_batch.run(bars.data(), static_cast(bars.size())); + check(hourly_batch.pre_break.seen && !hourly_batch.pre_break.closes + && hourly_batch.reopen.seen && !hourly_batch.reopen.opens, + "N2 hourly batch lunch is one session day"); + SessionHost hourly_stream; + hourly_stream.pre_label = hourly_batch.pre_label; + hourly_stream.reopen_label = hourly_batch.reopen_label; + check(hourly_stream.configure_native(spec).status == NativeSetupStatus::Applied, + "N2 hourly stream configure"); + ok = hourly_stream.stream_begin(bars.data(), warmup, "15", "60"); + for (std::size_t i = warmup; ok && i < bars.size(); ++i) + ok = hourly_stream.stream_push_bar(bars[i]); + if (ok) ok = hourly_stream.stream_end(false); + check(ok, "N2 hourly stream completed"); + check(hourly_stream.pre_break.seen && !hourly_stream.pre_break.closes + && hourly_stream.reopen.seen && !hourly_stream.reopen.opens, + "N2 hourly stream lunch is one session day"); +} + +struct CountHost final : NativeStrategyHost { + int calculations = 0; + void on_native_bar(const Bar&, const NativeDecisionContext&) override { + ++calculations; + } +}; + +void final_bucket_witness() { + constexpr std::int64_t first = 1751376600000LL; // NY 2025-07-01 09:30. + constexpr std::int64_t quarter = 15 * 60'000; + std::vector one_day; + for (int i = 0; i < 26; ++i) + one_day.push_back({100, 101, 99, 100, 1, first + i * quarter}); + auto spec = base_spec("edge-final", "15", "60", "0930-1600", "America/New_York"); + CountHost batch; + check(batch.configure_native(spec).status == NativeSetupStatus::Applied, + "B1 batch configure"); + batch.run(one_day.data(), static_cast(one_day.size())); + check(batch.calculations == 7, "B1 batch calculates final clipped hour"); + + spec.identity = {"edge-final-stream", 1}; + spec.script_tf = "D"; + std::vector three_days; + for (int d = 0; d < 3; ++d) + for (const auto& bar : one_day) { + Bar next = bar; + next.timestamp += d * 86'400'000LL; + three_days.push_back(next); + } + for (bool finalize : {false, true}) { + CountHost stream; + check(stream.configure_native(spec).status == NativeSetupStatus::Applied, + "B1 stream configure"); + bool ok = stream.stream_begin(three_days.data(), 52, "15", "D"); + for (int i = 52; ok && i < 78; ++i) ok = stream.stream_push_bar(three_days[i]); + if (ok) ok = stream.stream_end(finalize); + check(ok, "B1 stream completed"); + check(stream.calculations == (finalize ? 3 : 2), + finalize ? "B1 stream_end(true) calculates final day" + : "B1 stream_end(false) leaves final day pending"); + } +} + +template +struct HasReportMark : std::false_type {}; + +template +struct HasReportMark().mark_native_report_point(std::int64_t{}))>> : std::true_type {}; + +template +bool mark_if_available(Host& host, std::int64_t timestamp) { + if constexpr (HasReportMark::value) + return host.mark_native_report_point(timestamp); + return false; +} + +struct MarkHost final : NativeStrategyHost { + int accepted_marks = 0; + void on_native_bar(const Bar&, const NativeDecisionContext& context) override { + accepted_marks += mark_if_available(*this, context.script_bar_open_ms); + } +}; + +void report_marks_witness() { + constexpr std::int64_t first = 1718668800000LL; + std::vector bars; + for (int i = 0; i < 6; ++i) + bars.push_back({10.0 + i, 11.0 + i, 9.0 + i, 10.5 + i, 1, + first + i * 60'000LL}); + auto spec = base_spec("edge-marks", "1", "1"); + spec.report_policy = NativeReportPolicy::KernelRecordedAtHostMarks; + MarkHost host; + check(host.configure_native(spec).status == NativeSetupStatus::Applied, + "G1 host marks configure"); + host.run(bars.data(), static_cast(bars.size())); + ReportC report{}; + host.fill_report(&report); + check(host.accepted_marks == 6 && report.equity_curve_len == 6, + "G1 public host marks record six points"); + BacktestEngine::free_report(&report); +} + +} // namespace + +int main() { + fx_roll_witness(); + session_witness(); + final_bucket_witness(); + report_marks_witness(); + std::printf("native_kernel_edge: %d failures\n", failures); + return failures == 0 ? 0 : 1; +} diff --git a/tests/test_native_session_day_facts.cpp b/tests/test_native_session_day_facts.cpp index 829e45c3..8f850b4a 100644 --- a/tests/test_native_session_day_facts.cpp +++ b/tests/test_native_session_day_facts.cpp @@ -19,7 +19,8 @@ * rolls at the session's first window start, keyed to its trading date, so an * overnight session is ONE day across local midnight. The bar before / after * is the one the run holds when it holds one — a batch's input, a stream's - * warmup — and otherwise the calendar's, one script width away. The run's + * warmup — and otherwise the calendar's previous or next eligible input + * slot, across declared breaks. The run's * own edges keep the convention a bar with nothing held beyond it has: the * first bar of a run opens its session day, and the final bar of a batch * closes it (a batch is complete input); a stream's bars read on, because From 86780e43463b015442b899c29bdcb0308de23ae7 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Fri, 25 Sep 2026 19:58:32 +0800 Subject: [PATCH 05/24] Kernel: no ordinary quantity request stops a run -- the typed UnrepresentableQuantity refusal, the book's own quantities on the grid, and an opt-in quantity tolerance (R5 lane K-ULP4) Audit X1 (AUDIT4-opus kulp NF1-NF4): a bare C++ or C host lost its whole run on an ordinary decimal close. Open 1.5, Reduce 1.3, open 2.9: the book is {0.19999999999999996, 2.9}, and Reduce 0.2 needs 2^-54 of the 2.9 lot, below half its ulp; next_close_split answered UnrepresentableQuantity and the consumer turned every non-Applied inspection into a durable SettlementFailure (lifecycle Failed, code 6, discriminator 5). A dust lot at the head of the book did the same to the next Reduce (stuck rest) and to the next opening on its side (absorbed aggregate). On a quantity grid, Reduce{ScopeFraction{1}} was floored onto the grid (a step short, a dust lot, or nothing to close) and a close of a lot's own off-grid size was OffGrid. (a) Mandatory, generic -- a request the settlement cannot represent exactly is a TERMINAL, TYPED refusal of that request: - native_order.hpp: MatchRejectReason::UnrepresentableQuantity = 10 (appended); C PF_NATIVE_MATCH_REJECT_UNREPRESENTABLE_QUANTITY = 10 (appended, pinned in native_c_host.cpp, translated by c_word). - native_execution_consumer.cpp consume_matched_request: an inspection of UnrepresentableQuantity -- next_close_split's refusals, order_action::plan's absorbed reduction/transaction, the opening a surviving book absorbs -- is terminal(MatchRejectReason::UnrepresentableQuantity); nothing moves, the run goes on. Every other non-Applied inspection still fails the run. execute_current answers the MatchRejectedEvent without throwing; inspect_current_execution already showed the readiness. - The grid admits the book's own quantities: a ScopeFraction whose product is its scope (fraction == 1) resolves to the scope unfloored and the terms grid check admits it; a close (Reduce ExplicitUnits, or a Transact against the book's side) of exactly one lot's binary64 size is on the grid at submit (CommandContext::units_are_lot_quantity, appended) and as a host-sized close's answered units. (b) Opt-in, default off, generic -- NativeRunSpec::quantity_tolerance (appended; validated NotFinitePositive on NativeRunSpecField:: QuantityTolerance; folded into the spec digest only when present; C: the sixth pf_native_run_spec_ext_v1 layout, PF_NATIVE_SPEC_EXT_QUANTITY_TOLERANCE = 1u << 11, PF_NATIVE_RUN_SPEC_EXT_V1_RETENTION_SIZE for the fifth, PF_NATIVE_SPEC_FIELD_QUANTITY_TOLERANCE = 65). engine_execution.cpp tolerant_close_split: a close within t of a FIFO boundary ends there, charged its request (the lot it ends in keeping at most t closes whole; a rest of at most t is not taken from a lot larger than t; a trailing dust lot is taken; the scope's end is a boundary, so an overshoot opens nothing), and a lot of at most t that binary64 cannot take off the rest or the running sum closes whole; a dust survivor book beside an opening stays. The engine reads the value from a private BacktestEngine member the consumer projects at begin. Absent, the exact walk runs, bit for bit; the Pine adapter declares no tolerance and keeps its own 1e-10 policy. ADR-0001 rule 2 (recorded as the mechanism row and the quantity_tolerance ruling row, docs/adr/0001-kernel-adapter-boundary.md): (a) mechanism -- binary64 bookkeeping is venue-neutral; any host of any venue meets the same absorbed quantities and gets the same typed outcome for the one request; the grid admits the book's own sizes for every venue; the tolerance is data, a spec value with no platform branch. (b) knob -- the refusal is none: it replaces a whole-run failure no host could want, and an exact request settles as before; the tolerance is a knob, but absent it is the exact walk and the pre-field spec digest (pinned from 91d65ad6), so no existing run faces a hash-visible choice. Ruled adapter-policy in the feature table: the adapter's prefix Flatten is charged the lots it holds and its sweep books no row, so moving it onto the tolerance is a later lane's measurement. What moves: only runs that stopped (they complete with the typed refusal) and, on a quantity grid, whole-scope fractions and one-lot closes that were floored or OffGrid. Within the unreleased v19 epoch; no witness re-pinned (the K-ULP2/K-ULP3 witnesses' genuine-* cases now assert the typed refusal, their other checks unchanged). Tests (source-free, kernel profile too): test_native_unrepresentable_refusal (333 checks; on 91d65ad6 it does not compile -- no enumerator -- and, spelled 10, fails 84 of 292), test_native_quantity_tolerance (368 checks; staged and direct, fused and staged-only), check_unrepresentable_quantity in test_native_c_api.c (C-language, the ext layout pinned at 320). Floors: KERNEL_MIN_TESTS 271 -> 273, RELEASE_MIN_TESTS 672 -> 674. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit f961f73b69dc1edd32f008112d5ab6fee88b4a4e) INT25 merge: the floors add this lane's two rows to C-SURFACE-1's and KERNEL-EDGE's (kernel 275, release 676); strategy_configure_native_ext_result_v1 keeps C-SURFACE-1's c_refuse() on its refusals beside the sixth layout; ADR-0001 keeps KERNEL-EDGE's walked-driver-point wording on the journal-window row and adds this lane's row after it. The pages are re-anchored once, at the lane's last pick. --- docs/adr/0001-kernel-adapter-boundary.md | 108 +-- include/pineforge/engine.hpp | 9 + include/pineforge/native_c_api.h | 58 +- include/pineforge/native_host.hpp | 6 +- include/pineforge/native_order.hpp | 21 +- include/pineforge/native_run_spec.hpp | 13 + scripts/ci_verify.py | 15 +- scripts/test_native_feature_rulings.py | 4 +- src/engine_execution.cpp | 111 ++- src/native_c_host.cpp | 42 +- src/native_execution_consumer.cpp | 84 +- src/native_execution_consumer.hpp | 7 +- src/native_order.cpp | 6 +- src/native_run_spec.cpp | 2 + tests/CMakeLists.txt | 17 + .../relocation-manifest-v18-v19.json | 6 +- tests/test_native_c_api.c | 183 ++++- tests/test_native_exact_sum_close.cpp | 30 +- tests/test_native_partial_close_split.cpp | 32 +- tests/test_native_quantity_tolerance.cpp | 616 +++++++++++++++ tests/test_native_unrepresentable_refusal.cpp | 733 ++++++++++++++++++ 21 files changed, 1984 insertions(+), 119 deletions(-) create mode 100644 tests/test_native_quantity_tolerance.cpp create mode 100644 tests/test_native_unrepresentable_refusal.cpp diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index 054c429e..bfce512b 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:826`), an abstract subclass of + `pineforge::NativeStrategyHost` (`native_host.hpp:830`), 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:826`) carrying the +Subclass `NativeStrategyHost`: a **zero-argument** host (`native_host.hpp:830`) 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,18 +72,18 @@ 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:1169`), then feed. + continues live. Configure with `configure_native(spec)` (`native_host.hpp:1173`), then feed. - **Callbacks are not close-only.** `on_native_bar` is the pure-virtual script-bar calculation - (`native_host.hpp:874`); also `on_native_input` (`native_host.hpp:856`), `on_native_tick` - (`native_host.hpp:859`), `on_native_timeframe_bar` (`native_host.hpp:866`), + (`native_host.hpp:878`); also `on_native_input` (`native_host.hpp:860`), `on_native_tick` + (`native_host.hpp:863`), `on_native_timeframe_bar` (`native_host.hpp:870`), `on_native_bar_open` — at the modeled opening, before its matching pass - (`native_host.hpp:870`, `on_native_bar_open` `native_execution_consumer.cpp:6913-6915`) — - `on_native_recalculate` for every calculation of the run (`native_host.hpp:893`), - `on_native_sub_bar` (`native_host.hpp:908`) and post-fill - `on_native_applied` (`native_host.hpp:918`). `on_bar` is `final` (`native_host.hpp:838`). + (`native_host.hpp:874`, `on_native_bar_open` `native_execution_consumer.cpp:6980-6982`) — + `on_native_recalculate` for every calculation of the run (`native_host.hpp:897`), + `on_native_sub_bar` (`native_host.hpp:912`) and post-fill + `on_native_applied` (`native_host.hpp:922`). `on_bar` is `final` (`native_host.hpp:842`). - **Requests.** `submit` / `replace` / `cancel` / `submit_market` / `replace_market` - (`native_host.hpp:1202-1207`), plus `cancel_all` (`native_host.hpp:1240`) and `cancel_where` - (`native_host.hpp:1245`). A request carries one of five triggers — Market, Limit (with a + (`native_host.hpp:1204-1209`), plus `cancel_all` (`native_host.hpp:1244`) and `cancel_where` + (`native_host.hpp:1249`). 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` @@ -96,12 +96,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:927`) returns no + unresolved `HostSized` whose `resolve_execution_terms` (`native_host.hpp:931`) returns no units is a `TermsUnresolved` rejection - (`TermsUnresolved` `native_execution_consumer.cpp:4849`). -- **Observing executions.** Implement `on_native_applied` (`native_host.hpp:918`) — notifications + (`TermsUnresolved` `native_execution_consumer.cpp:4894`). +- **Observing executions.** Implement `on_native_applied` (`native_host.hpp:922`) — notifications drain FIFO after the outer callback returns — and/or poll `native_events(after_ordinal)` - (`native_host.hpp:1302`). Do *not* implement + (`native_host.hpp:1306`). 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`: @@ -109,13 +109,13 @@ 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:4827`) | - | Cohorts | `cohort_open` (`native_host.hpp:1255`) / `cohort_add` (`:1258`) / `cohort_remove` (`:1261`) — the handle a `BindCohort` owner names | - | Mid-callback execution | `current_execution_point` (`native_host.hpp:1060`), `inspect_current_execution` (`native_host.hpp:1072`) / `execute_current` (`native_host.hpp:1077`) | - | 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:956`), `margin_check_allowed` (`native_host.hpp:968`), `resolve_margin_call_units` (`native_host.hpp:975`) — and `validate_execution_precommit` (`native_host.hpp:940`), 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:1296`); the breach appends a `NativeRiskEvent` to the command history | - | State reads | `physical_position()` (`native_host.hpp:1265`) / `native_marked_equity(mark)` (`native_host.hpp:1274`) / `native_open_lots(mark)` (`native_host.hpp:1270`), `trail_state(handle)` (`native_host.hpp:1065`), 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:1046`) 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:4867`) | + | Cohorts | `cohort_open` (`native_host.hpp:1259`) / `cohort_add` (`:1262`) / `cohort_remove` (`:1265`) — the handle a `BindCohort` owner names | + | Mid-callback execution | `current_execution_point` (`native_host.hpp:1064`), `inspect_current_execution` (`native_host.hpp:1076`) / `execute_current` (`native_host.hpp:1081`) | + | 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:960`), `margin_check_allowed` (`native_host.hpp:972`), `resolve_margin_call_units` (`native_host.hpp:979`) — and `validate_execution_precommit` (`native_host.hpp:944`), 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:1300`); the breach appends a `NativeRiskEvent` to the command history | + | State reads | `physical_position()` (`native_host.hpp:1269`) / `native_marked_equity(mark)` (`native_host.hpp:1278`) / `native_open_lots(mark)` (`native_host.hpp:1274`), `trail_state(handle)` (`native_host.hpp:1069`), 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:1050`) 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 (`NativeFeeKind` `native_run_spec.hpp:19`), a scalar account FX, close timing, the quantity @@ -142,9 +142,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:1109`) from inside `on_native_run_begin`, with - `on_native_timeframe_bar` (`native_host.hpp:866`) and `native_series_bar` - (`native_host.hpp:1092`) reading them back, and `NativeAuxiliaryFeed` + + (`native_host.hpp:1113`) from inside `on_native_run_begin`, with + `on_native_timeframe_bar` (`native_host.hpp:870`) and `native_series_bar` + (`native_host.hpp:1096`) 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 @@ -154,7 +154,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:1175`). + `configure_native_fx_curve` (`native_host.hpp:1179`). - **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 @@ -167,7 +167,7 @@ summary of it. `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. - **Reproducibility.** Two digests, one wrapping the other. `native_continuation_hash()` - (`native_host.hpp:1343`) returns the consumer's event/continuation hash. **A digest names + (`native_host.hpp:1347`) 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 @@ -284,7 +284,7 @@ citations, 264 are in `src/source/` and its headers; 6 sit in kernel files `sha256:e3312d2dd7ad7067ad41b26dfb3b502293dcdc5b98ed91563bd89f9482bb2b64` engine_aux_security.cpp:5-12, 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:739`), which the kernel never dereferences; the + and forwards it as `overrides_opaque` (`native_host.hpp:743`), 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 @@ -324,11 +324,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:2607`), runs a batch - (`strategy_native_run_v1` `native_c_api.h:2620`), and **submits, replaces, cancels and - executes** orders — `strategy_native_submit_v1` (`native_c_api.h:2648`), `_replace_v1` - (`native_c_api.h:2670`), `_cancel_v1` (`native_c_api.h:2710`), `_cancel_all_v1` (`native_c_api.h:2714`), `_cancel_where_v1` (`native_c_api.h:2735`), - `_execute_current_v1` (`native_c_api.h:2756`) — under the kernel's own legality rule. Streaming needs no new + callback table (`strategy_native_host_create_v1` `native_c_api.h:2637`), runs a batch + (`strategy_native_run_v1` `native_c_api.h:2650`), and **submits, replaces, cancels and + executes** orders — `strategy_native_submit_v1` (`native_c_api.h:2678`), `_replace_v1` + (`native_c_api.h:2700`), `_cancel_v1` (`native_c_api.h:2740`), `_cancel_all_v1` (`native_c_api.h:2744`), `_cancel_where_v1` (`native_c_api.h:2765`), + `_execute_current_v1` (`native_c_api.h:2786`) — 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 @@ -450,6 +450,7 @@ see a mechanism, so these rows are held by reading, not by the checker): | the closed rows as the broker-state hash folds them — `BacktestEngine::trades_` under `pineforge-broker-state/v19`, through the consumer's running digest (`NativeExecutionConsumer::closed_rows_digest`, `src/native_execution_consumer.cpp`), and `NativeStrategyHost::native_closed_rows_amended` (`include/pineforge/native_host.hpp`) | the kernel books every row, at the end of the rows; the Pine host amends a booked row inside the applied notification of the execution that booked it (`PineStrategyHost::on_native_applied`: an aggregated chart dates the exit at its chart bar), and after those notifications have returned it reorders a bar's trailing same-bar bracket exits by command sequence (`sort_same_bar_exit_trades`, `src/source/pine_strategy_host.cpp`), naming the first row it moved | **a row is final once the applied notification of the execution that booked it has returned** (R5 lane V19-A). Rule 2 (a), mechanism: the finality point is the kernel's own notification boundary, the same for every host, and what a host that changes a final row supplies is data -- the first row it changed -- so a host of any venue drives the same surface and nothing in it knows Pine. Rule 2 (b), knob: none -- the call reports a change the host has already made and chooses nothing. A final row changed without a name keeps the value the digest took, and the Debug re-fold at each run's end (`verify_closed_rows`) aborts on it; rows removed from the end need no name, because the kernel only appends, so a read over fewer rows or a booking below the final mark forgets them. Every read answers the fold of the six fields v18 walked (entry and exit time and price, quantity, P&L) over the rows as they are, in their order, folded once per row per run instead of once per row per read. Pinned by part 7 of `tests/test_native_state_continuation.cpp` (kernel-only) and by every Debug ctest run, each of whose runs ends in the re-fold. | | the event record a run keeps — `NativeRunSpec::event_retention` (`include/pineforge/native_run_spec.hpp`), the journal window (`WorkingRequestCore::retire_history`, `include/pineforge/native_order.hpp`; `NativeExecutionConsumer::retire_journal`, `src/native_execution_consumer.cpp`) and the reader's acknowledgement (`NativeStrategyHost::native_acknowledge_events`, `native_event_window_start`, `include/pineforge/native_host.hpp`) | the host declares a retention in its spec and acknowledges what it has read; the Pine adapter declares `Window` in `PineExecutionAdapter::project()` and acknowledges its receipt cursor in `observe_terminal_receipts` (and "nothing read yet" at its run begin); the kernel retires at every script-bar boundary | **a reporting policy with a reader acknowledgement** (R5 lane V19-B). Rule 2 (a), mechanism: what a run keeps of its own event record, and a reader telling the kernel what it has consumed, the same for every host; the kernel never retires an event its own live state reads (a queued applied notification, a live deferred group-adjustment chain), and what the journal used to answer beyond that is state (next row). Rule 2 (b), knob: a spec field with no TradingView meaning -- `Window`, `Commands`, `Full` -- folded into the spec digest only off the default; a C caller that does not send the word keeps `Full`, the record its layout was published with. No trade, and no Pine value, depends on it: the adapter reads above its own cursor only. Pinned by `tests/test_native_event_retention.cpp` (kernel-only: `Full` row for row against the pre-window record, the window and the acknowledgement's edges, and the same books booking the same trades with the window closing at every driver point) and `check_event_retention` of `tests/test_native_c_api.c`. | | 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`); the quantity grid's own quantities (`CommandContext::units_are_lot_quantity`, and a `ScopeFraction` whose product is its scope, unfloored); 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; on a quantity grid a whole-scope fraction and a one-lot close that were floored or `OffGrid` now settle -- 54 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`. | 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 (→ @@ -556,6 +557,7 @@ Every line number in this section names its symbol on this tree, which | `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:2266-2268`) | 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:1613`): 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` | | `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` | What the table does not cover, on purpose: request kinds and host members the adapter never emits or @@ -577,30 +579,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:2165`), `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:3017`), reading a SIZED request's own sizing block | the sizing-query scenario | -| `resolve_execution_terms`, price half | `on_execution_terms` (`native_c_api.h:2532`): 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:2541`): 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:2548`) | the anchored-level scenario | -| `hash_host_extension` | `on_hash_extension` (`native_c_api.h:2559`): 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:2176`), `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:3047`), reading a SIZED request's own sizing block | the sizing-query scenario | +| `resolve_execution_terms`, price half | `on_execution_terms` (`native_c_api.h:2562`): 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:2571`): 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:2578`) | the anchored-level scenario | +| `hash_host_extension` | `on_hash_extension` (`native_c_api.h:2589`): 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:1240`) / `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:2714`). | -| `cohort_add` (`native_host.hpp:1258`) / `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:860`, 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:1287`) / `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:2989`). | -| `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:1670`) 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:1244`) / `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:2744`). | +| `cohort_add` (`native_host.hpp:1262`) / `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:870`, 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:1291`) / `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:3019`). | +| `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:1681`) 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:965`) 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:895`); 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`. | +| `strategy_set_path_order` (`pineforge.h:965`) 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:1059`) | **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:3961`) | **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:582`) | **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:866`) and `NativeTimeframeBarContext::interval` (`native_host.hpp:790`) | **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:7493-7505`); 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:1055-1060`) sees the same values, so no hash contract moves. | -| `pf_native_lot_excursion_v1` (`native_c_api.h:1857`) | **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:870`) and `NativeTimeframeBarContext::interval` (`native_host.hpp:794`) | **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:7559-7571`); 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:1065-1070`) sees the same values, so no hash contract moves. | +| `pf_native_lot_excursion_v1` (`native_c_api.h:1867`) | **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) @@ -657,8 +659,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: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` | | `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:8452-8455` (lookahead) and `clear_if_gapped` `:8469-8472` (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:2215`, `::gaps` `include/pineforge/native_c_api.h:2219` | 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:790`, `pf_native_gaps_e` `:801`); 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:8516-8519` (lookahead) and `clear_if_gapped` `:8510-8513` (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:2226`, `::gaps` `include/pineforge/native_c_api.h:2230` | 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` | @@ -672,9 +674,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:7116`, presenting `NativeDecisionContext::in_session` `include/pineforge/market_driver.hpp:155` 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:7725-7741` | 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:7702-7723` | 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:6453` | 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:7791-7807` | 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:7768-7789` | 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:6519` | 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 @@ -741,7 +743,7 @@ no new kernel policy. 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. 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:1003`; the market-if-touched + excursion ownership, `owns_lot_excursions` `native_host.hpp:1007`; 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/include/pineforge/engine.hpp b/include/pineforge/engine.hpp index 2d4210a8..e4c98e10 100644 --- a/include/pineforge/engine.hpp +++ b/include/pineforge/engine.hpp @@ -2167,6 +2167,15 @@ class BacktestEngine { void trace(const std::string& name, double value); void trace(const std::string& name, bool value) { trace(name, value ? 1.0 : 0.0); } void trace(const std::string& name, int value) { trace(name, static_cast(value)); } + +private: + // The run spec's quantity tolerance (NativeRunSpec::quantity_tolerance), + // 0 when the run declares none, which keeps the settlement exact. The FIFO + // close walk and the opening beside a surviving book read it + // (src/engine_execution.cpp); the execution consumer, a friend, projects + // it from the spec at every begin, and the spec digest folds the spec's + // value. Private, so no host writes it (R5 lane K-ULP4). + double native_quantity_tolerance_ = 0.0; }; } // inline namespace engine_script_run_v19 diff --git a/include/pineforge/native_c_api.h b/include/pineforge/native_c_api.h index 860c27b5..586cca39 100644 --- a/include/pineforge/native_c_api.h +++ b/include/pineforge/native_c_api.h @@ -749,7 +749,12 @@ typedef enum pf_native_spec_ext_mask_e { * set this bit; a caller sending any earlier layout is refused with * PF_NATIVE_E_STRUCT. Without it the run keeps * #PF_NATIVE_EVENT_RETENTION_FULL. */ - PF_NATIVE_SPEC_EXT_EVENT_RETENTION = 1u << 10 + PF_NATIVE_SPEC_EXT_EVENT_RETENTION = 1u << 10, + /** The settlement's quantity tolerance (`quantity_tolerance`). Only a + * caller whose pf_native_run_spec_ext_v1 carries the tolerance tail may + * set this bit; a caller sending any earlier layout is refused with + * PF_NATIVE_E_STRUCT. Without it the settlement stays exact. */ + PF_NATIVE_SPEC_EXT_QUANTITY_TOLERANCE = 1u << 11 } pf_native_spec_ext_mask_t; /** What a run keeps of its event record — `NativeRunSpec::event_retention`, @@ -1066,8 +1071,13 @@ typedef enum pf_native_match_reject_e { PF_NATIVE_MATCH_REJECT_INVALID_TERMS = 6, PF_NATIVE_MATCH_REJECT_NO_OPPOSITE_EXPOSURE = 7, PF_NATIVE_MATCH_REJECT_HOST_PRECOMMIT = 8, - PF_NATIVE_MATCH_REJECT_RISK_LIMIT = 9 /**< An opening refused while a risk + PF_NATIVE_MATCH_REJECT_RISK_LIMIT = 9, /**< An opening refused while a risk * limit blocks. */ + PF_NATIVE_MATCH_REJECT_UNREPRESENTABLE_QUANTITY = 10 /**< A quantity the settlement + * cannot book exactly on + * this book: the request + * ends, nothing moves and + * the run goes on. */ } pf_native_match_reject_t; /** Which trigger a price reached — `native_order::ActivationKind`: the @@ -1296,7 +1306,8 @@ typedef enum pf_native_spec_field_e { PF_NATIVE_SPEC_FIELD_AUXILIARY_FEED_TIMEFRAME = 61, PF_NATIVE_SPEC_FIELD_AUXILIARY_FEED_BARS = 62, PF_NATIVE_SPEC_FIELD_SUBSCRIPTION_SOURCE = 63, - PF_NATIVE_SPEC_FIELD_EVENT_RETENTION = 64 + PF_NATIVE_SPEC_FIELD_EVENT_RETENTION = 64, + PF_NATIVE_SPEC_FIELD_QUANTITY_TOLERANCE = 65 } pf_native_spec_field_t; /** Why an append was refused — `NativeAuxiliaryAppendError`: the `error` @@ -2233,22 +2244,24 @@ typedef struct pf_native_subscription_v1 { * #pf_native_run_spec_v1 now travels here. The one deliberate omission is * `identity`, which the base spec owns. * - * This struct has FIVE published layouts and the runtime accepts any of + * This struct has SIX published layouts and the runtime accepts any of * them: the base layout the L13 lane first shipped * (#PF_NATIVE_RUN_SPEC_EXT_V1_BASE_SIZE); that layout plus L9's `risk_*` * tail (#PF_NATIVE_RUN_SPEC_EXT_V1_RISK_SIZE); that one plus N8's intrabar * path, the four feed-shape and presentation policies, and the margin * model's equity basis, level base and liquidation strings * (#PF_NATIVE_RUN_SPEC_EXT_V1_POLICY_SIZE); that one plus the - * `auxiliary_*` tail (#PF_NATIVE_RUN_SPEC_EXT_V1_AUXILIARY_SIZE); and the - * current one, which appends the `event_retention` tail after them. A - * caller compiled against an earlier layout keeps working unchanged and - * simply cannot set the mask bits its struct has no fields for - * (#PF_NATIVE_SPEC_EXT_RISK, #PF_NATIVE_SPEC_EXT_INTRABAR, - * #PF_NATIVE_SPEC_EXT_FEED_POLICY, #PF_NATIVE_SPEC_EXT_AUXILIARY_FEED, - * #PF_NATIVE_SPEC_EXT_EVENT_RETENTION): doing so is PF_NATIVE_E_STRUCT. Any - * other `struct_size` is PF_NATIVE_E_STRUCT too. Every tail is append-only: - * nothing above them moved. */ + * `auxiliary_*` tail (#PF_NATIVE_RUN_SPEC_EXT_V1_AUXILIARY_SIZE); that one + * plus the `event_retention` tail + * (#PF_NATIVE_RUN_SPEC_EXT_V1_RETENTION_SIZE); and the current one, which + * appends the `quantity_tolerance` tail after them. A caller compiled + * against an earlier layout keeps working unchanged and simply cannot set + * the mask bits its struct has no fields for (#PF_NATIVE_SPEC_EXT_RISK, + * #PF_NATIVE_SPEC_EXT_INTRABAR, #PF_NATIVE_SPEC_EXT_FEED_POLICY, + * #PF_NATIVE_SPEC_EXT_AUXILIARY_FEED, #PF_NATIVE_SPEC_EXT_EVENT_RETENTION, + * #PF_NATIVE_SPEC_EXT_QUANTITY_TOLERANCE): doing so is PF_NATIVE_E_STRUCT. + * Any other `struct_size` is PF_NATIVE_E_STRUCT too. Every tail is + * append-only: nothing above them moved. */ typedef struct pf_native_run_spec_ext_v1 { uint32_t struct_size; /**< sizeof(pf_native_run_spec_ext_v1). */ uint32_t version; /**< PF_NATIVE_API_VERSION. */ @@ -2361,6 +2374,16 @@ typedef struct pf_native_run_spec_ext_v1 { * runs under PF_NATIVE_EVENT_RETENTION_FULL. ── */ uint32_t event_retention; /**< #pf_native_event_retention_t. */ uint32_t reserved2; /**< Must be 0. */ + + /* ── The additive quantity-tolerance tail (K-ULP4). Read only when + * `present_mask` carries PF_NATIVE_SPEC_EXT_QUANTITY_TOLERANCE; a caller + * sending an earlier layout stops at `reserved2` or above and keeps the + * exact settlement, in which a quantity it cannot book exactly is that + * request's #PF_NATIVE_MATCH_REJECT_UNREPRESENTABLE_QUANTITY. ── */ + double quantity_tolerance; /**< Units, finite and positive: + * `NativeRunSpec::quantity_tolerance`, two + * quantities within it of each other are + * one quantity to the settlement. */ } pf_native_run_spec_ext_v1; /** Byte length of #pf_native_run_spec_ext_v1 as the L13 lane first published @@ -2385,12 +2408,19 @@ typedef struct pf_native_run_spec_ext_v1 { ((uint32_t)offsetof(pf_native_run_spec_ext_v1, auxiliary_tf)) /** Byte length of #pf_native_run_spec_ext_v1 with the `auxiliary_*` tail but - * before the `event_retention` tail was appended — the fourth of its five + * before the `event_retention` tail was appended — the fourth of its six * published layouts, an offset for the same reason * #PF_NATIVE_RUN_SPEC_EXT_V1_BASE_SIZE is. */ #define PF_NATIVE_RUN_SPEC_EXT_V1_AUXILIARY_SIZE \ ((uint32_t)offsetof(pf_native_run_spec_ext_v1, event_retention)) +/** Byte length of #pf_native_run_spec_ext_v1 with the `event_retention` tail + * but before the `quantity_tolerance` tail was appended — the fifth of its + * six published layouts, an offset for the same reason + * #PF_NATIVE_RUN_SPEC_EXT_V1_BASE_SIZE is. */ +#define PF_NATIVE_RUN_SPEC_EXT_V1_RETENTION_SIZE \ + ((uint32_t)offsetof(pf_native_run_spec_ext_v1, quantity_tolerance)) + /** The C host's strategy logic. * * Every entry may be NULL, which is exactly the C++ default: the kernel does diff --git a/include/pineforge/native_host.hpp b/include/pineforge/native_host.hpp index 0d51111b..968a6c68 100644 --- a/include/pineforge/native_host.hpp +++ b/include/pineforge/native_host.hpp @@ -700,7 +700,11 @@ struct NativeCurrentExecution { /// Recomputed observations, never an apply token. Readiness is the financial /// pre-source preparation boundary, independent of account projection validity -/// and excluding opening admission and late counter/lifecycle checks. +/// and excluding opening admission and late counter/lifecycle checks. A +/// readiness of execution::Status::UnrepresentableQuantity is a quantity the +/// settlement cannot book exactly on this book, which execute_current answers +/// as a MatchRejectedEvent with MatchRejectReason::UnrepresentableQuantity; +/// the run goes on. struct NativeCurrentExecutionPreview { std::optional refusal; std::optional settlement_readiness; diff --git a/include/pineforge/native_order.hpp b/include/pineforge/native_order.hpp index c3196413..8d52a56f 100644 --- a/include/pineforge/native_order.hpp +++ b/include/pineforge/native_order.hpp @@ -865,7 +865,9 @@ struct CancelResult { /// submits again. OpeningDirection, MaxAbsUnits, MaxOpenLots, InitialMargin and /// RiskLimit are the run's admission gates; TermsUnresolved is a host-sized or /// basis-sized quantity that could not be resolved; NoOppositeExposure and -/// InvalidTerms are shapes the answered terms cannot take. +/// InvalidTerms are shapes the answered terms cannot take; +/// UnrepresentableQuantity is a quantity the settlement cannot book exactly on +/// the book it met. enum class MatchRejectReason : std::uint8_t { NonpositivePrice = 0, OpeningDirection = 1, @@ -879,6 +881,14 @@ enum class MatchRejectReason : std::uint8_t { /// An opening refused while the run spec's generic risk limits are /// blocking (L9). Reduces never reach this gate. RiskLimit = 9, + /// The settlement's inspection answered execution::Status:: + /// UnrepresentableQuantity: a close split binary64 cannot hold (a rest + /// below half an ulp of the lot it ends in, a whole lot that cannot + /// decrement the rest or move the running sum), a reduction or transaction + /// the position absorbs, or an opening the surviving book absorbs. The + /// request ends here, nothing moves, and the run goes on; the same request + /// stopped the whole run before R5 lane K-ULP4. + UnrepresentableQuantity = 10, }; /// Which price a candidate is being offered at: the point's own price, or the @@ -1405,6 +1415,15 @@ struct CommandContext { std::optional sizing_scope; std::optional sizing_price; bool sizing_admissible = true; + /// The request's explicit close quantity -- a Reduce's ExplicitUnits, or + /// the units of a Transact against the book's side -- is the binary64 + /// quantity of one of the book's own lots. Such a quantity is on the + /// quantity grid whatever the grid predicate answers: the grid admits the + /// quantities a host chooses, and a lot's size is the book's own, which + /// settlement arithmetic can move off any decimal grid (R5 lane K-ULP4). + /// The execution consumer measures it; appended last, like the members + /// above. + bool units_are_lot_quantity = false; }; struct EvaluationContext { diff --git a/include/pineforge/native_run_spec.hpp b/include/pineforge/native_run_spec.hpp index c8c3e96e..b6d8a483 100644 --- a/include/pineforge/native_run_spec.hpp +++ b/include/pineforge/native_run_spec.hpp @@ -638,6 +638,18 @@ struct NativeRunSpec { /// What native_events() can still return; see NativeEventRetention. /// Folded into the spec digest only when it is not Window. NativeEventRetention event_retention = NativeEventRetention::Window; + /// Opt-in quantity tolerance, in units (finite, positive): two quantities + /// within it of each other are one quantity to the settlement. A close + /// that comes within it of a FIFO boundary -- the binary64 sum of the + /// lots through one of them -- ends at that boundary, charged its request, + /// and a lot of at most this size that binary64 cannot take off a close's + /// rest closes whole with the close that reaches it (docs/native- + /// settlement.md, "Quantity tolerance"). Absent is the whole default + /// surface: the settlement stays exact, a quantity it cannot book exactly + /// is that request's MatchRejectReason::UnrepresentableQuantity, and the + /// continuation digest is the pre-tolerance one; the value folds only + /// when present (R5 lane K-ULP4). + std::optional quantity_tolerance; }; /// Which field a validation refused, in deterministic first-error order. It is @@ -667,6 +679,7 @@ enum class NativeRunSpecField : std::uint8_t { RiskDayBasis, RiskAction, AuxiliaryFeedTimeframe, AuxiliaryFeedBars, SubscriptionSource, EventRetention, + QuantityTolerance, }; /// Why a field was refused. Read it beside NativeRunSpecValidation::field: the diff --git a/scripts/ci_verify.py b/scripts/ci_verify.py index 5ca61146..001a221d 100644 --- a/scripts/ci_verify.py +++ b/scripts/ci_verify.py @@ -228,7 +228,12 @@ # row still skips on a system libcurl. # C-SURFACE-1 adds the INT23 frozen-header decision-tail witness. # Wave G KERNEL-EDGE adds the source-free test_native_kernel_edge row. -KERNEL_MIN_TESTS = 273 +# 275 run = those 271, C-SURFACE-1's and KERNEL-EDGE's rows, and the two +# source-free TUs of R5 lane K-ULP4: +# +2 K-ULP4 test_native_unrepresentable_refusal, test_native_quantity_tolerance +# All four register in release too. 276 registered, 275 run: the WebSocket row +# still skips on a system libcurl. +KERNEL_MIN_TESTS = 275 # 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 @@ -331,8 +336,12 @@ # finding 1) # B-C-SURFACE's witnesses are rows inside test_native_c_api. C-SURFACE-1 adds # the INT23 frozen-header decision-tail row. Wave G KERNEL-EDGE adds the same -# test_native_kernel_edge row in release. No release row skips: 674 run. -RELEASE_MIN_TESTS = 674 +# test_native_kernel_edge row in release: 674. +# 676 = those 674 plus the two R5 lane K-ULP4 rows KERNEL_MIN_TESTS lists +# above, which register here too (ctest -N on the lane's tree); its C-language +# checks are rows inside test_native_c_api. No release row skips, so 676 +# registered is 676 run. +RELEASE_MIN_TESTS = 676 # PR-only registration floors. These are the complete CTest populations at # 91d65ad6 (INT24); the ordinary full-run floors above remain unchanged. # An excluded run must still discover at least this many rows before -LE. diff --git a/scripts/test_native_feature_rulings.py b/scripts/test_native_feature_rulings.py index f9c6a61e..e59d7a1b 100644 --- a/scripts/test_native_feature_rulings.py +++ b/scripts/test_native_feature_rulings.py @@ -49,7 +49,7 @@ def test_inventory_is_the_ruled_one(self): self.assertEqual(undeclared, [ "price_grid", "grid_rounding", "max_abs_units", "max_open_lots", "initial_margin_fraction", "risk", "report_open_position_at_end", - "open_bar_view", "subscriptions", "auxiliary_feed"]) + "open_bar_view", "subscriptions", "auxiliary_feed", "quantity_tolerance"]) kinds = {field: row.kind for row in rulings.parse_rulings(ADR) for field in row.fields} self.assertEqual({field for field, kind in kinds.items() if kind == "native-only"}, {"price_grid", "grid_rounding", "risk"}) @@ -69,7 +69,7 @@ def test_an_emptied_table_reports_every_undeclared_field(self): begin = ADR.index(rulings.BEGIN_MARKER) + len(rulings.BEGIN_MARKER) end = ADR.index(rulings.END_MARKER) found = findings(adr=ADR[:begin] + "\n" + ADR[end:]) - self.assertEqual(len(found), 10) + self.assertEqual(len(found), 11) self.assertTrue(all("no ruling row covers it" in finding for finding in found)) def test_missing_markers_are_a_shape_error(self): diff --git a/src/engine_execution.cpp b/src/engine_execution.cpp index 6feac5ef..80f227af 100644 --- a/src/engine_execution.cpp +++ b/src/engine_execution.cpp @@ -172,6 +172,95 @@ CloseSplit next_close_split(const PyramidEntry& lot, bool closes, bool flatten, return {execution::Status::Applied, amount, kept}; } +// The same walk under a declared quantity tolerance t > 0 +// (NativeRunSpec::quantity_tolerance): two quantities within t of each other +// are one quantity. A close that comes within t of a FIFO boundary -- the +// binary64 sum of the scope's lots through one of them -- ends at that +// boundary, and a lot of at most t that binary64 cannot take off the rest or +// the running sum closes whole with the close that reaches it. Each such end +// is charged the request, as a split its rows cannot add up to is (above): +// - the lot the request ends in would keep at most t: it closes whole; +// - a rest of at most t is left after a whole lot, and the next member lot +// is larger than t: the walk ends before it, which is not touched; a next +// lot of at most t closes whole instead (the rule above), so a dust lot +// behind the boundary is taken, not left; after the scope's last lot the +// caller ends the walk the same way (finish_tolerant_walk); +// - a whole lot of at most t that cannot move the rest or the running sum, +// or a running sum of at most t that this lot's close absorbs -- the dust +// lot a close meets at the head of a book -- closes whole, and the walk +// goes on. +// What stays refused is what no tolerance turns into a quantity: a rest below +// half an ulp of the first lot it meets (a request of at most t, or a lot far +// larger than the rest), and a rest or running sum above t that a lot absorbs. +// Zero is not a boundary to snap to: a request of at most t closes what it +// asks of a larger lot, or is refused (R5 lane K-ULP4). +CloseSplit tolerant_close_split(const PyramidEntry& lot, double requested, + double tolerance, double& closed, double& remaining) { + if (remaining == 0.0) return {execution::Status::Applied, 0.0, lot.qty}; + if (closed != 0.0 && remaining <= tolerance && lot.qty > tolerance) { + closed = requested; + remaining = 0.0; + return {execution::Status::Applied, 0.0, lot.qty}; + } + double amount = std::min(lot.qty, remaining); + double kept = lot.qty - amount; + const bool snaps_whole = kept != 0.0 && kept <= tolerance; + if (snaps_whole) { + amount = lot.qty; + kept = 0.0; + } + const bool dust_lot = amount == lot.qty && lot.qty <= tolerance; + const double next_closed = closed + amount; + if (!std::isfinite(next_closed) || kept == lot.qty + || (closed != 0.0 && next_closed == closed && !dust_lot) + || (closed != 0.0 && next_closed == amount && closed > tolerance)) + return {execution::Status::UnrepresentableQuantity}; + if (snaps_whole) { + closed = requested; + remaining = 0.0; + return {execution::Status::Applied, amount, kept}; + } + if (next_closed > requested || (next_closed < requested && kept != 0.0)) { + closed = requested; + remaining = 0.0; + return {execution::Status::Applied, amount, kept}; + } + if (kept != 0.0 && closed + lot.qty == requested) { + closed = requested; + remaining = 0.0; + return {execution::Status::Applied, lot.qty, 0.0}; + } + const double next_remaining = requested - next_closed; + if (next_remaining == remaining && !dust_lot) + return {execution::Status::UnrepresentableQuantity}; + remaining = next_remaining; + closed = next_closed; + return {execution::Status::Applied, amount, kept}; +} + +// The walk's one close-level step: the exact walk, or the tolerant one when +// the run declared a tolerance and this lot is a member of a close that is not +// a Flatten. +CloseSplit close_split(const PyramidEntry& lot, bool closes, bool flatten, + double requested, double tolerance, double& closed, + double& remaining) { + if (tolerance > 0.0 && closes && !flatten) + return tolerant_close_split(lot, requested, tolerance, closed, remaining); + return next_close_split(lot, closes, flatten, requested, closed, remaining); +} + +// After the scope's last lot: a tolerant walk that closed every member whole +// with a rest of at most t left ends at the scope's end, charged the request, +// and a transaction opens nothing for that rest (tolerant_close_split). +void finish_tolerant_walk(double tolerance, bool closes, bool flatten, double requested, + double& closed, double& remaining) { + if (tolerance > 0.0 && closes && !flatten && closed != 0.0 && remaining > 0.0 + && remaining <= tolerance) { + closed = requested; + remaining = 0.0; + } +} + execution::AccountEffectProjection invalid_projection(execution::Status status) { execution::AccountEffectProjection out; out.status = status; @@ -298,12 +387,15 @@ bool BacktestEngine::NativeSettlementStage::OneLot::stage( if (!lots.empty()) { const bool closes_member = (flatten || reduce || opposite) && selected_for_close(book_or_opening, lots.front()); - const auto split = next_close_split(lots.front(), closes_member, flatten, - allocation_requested, closed, remaining); + const double tolerance = engine.native_quantity_tolerance_; + const auto split = close_split(lots.front(), closes_member, flatten, + allocation_requested, tolerance, closed, remaining); // An addition keeps the lot whole and a partial close keeps a part of // it: both leave a survivor, which is the chain's. if (split.status != Status::Applied || split.amount == 0.0 || split.kept != 0.0) return false; + finish_tolerant_walk(tolerance, closes_member, flatten, allocation_requested, + closed, remaining); closes = true; closing = split.amount; } @@ -636,10 +728,10 @@ execution::Status BacktestEngine::allocate_native_settlement_closes( const bool member = stage.use_selected ? stage.selected_ids.count(lot.entry_incarnation) != 0 : selected_for_close(book_or_opening, lot); - const auto split = next_close_split( + const auto split = close_split( lot, stage.closes && member, stage.flatten || consume_selected_exactly, - stage.allocation_requested, closed, remaining); + stage.allocation_requested, native_quantity_tolerance_, closed, remaining); if (split.status != Status::Applied) return split.status; if (split.amount == 0.0) { stage.survivors.push_back(lot); @@ -658,6 +750,9 @@ execution::Status BacktestEngine::allocate_native_settlement_closes( stage.survivors.push_back(std::move(survivor)); } } + finish_tolerant_walk(native_quantity_tolerance_, stage.closes, + stage.flatten || consume_selected_exactly, + stage.allocation_requested, closed, remaining); stage.closed = closed; return Status::Applied; } @@ -856,8 +951,14 @@ void BacktestEngine::finish_native_settlement_stage( } if (stage.opening > 0.0) { const double next = after_qty + stage.opening; + // Under a quantity tolerance t, a surviving book of at most t that the + // opening absorbs stays as it is -- dust beside the new lot, which + // the next close takes whole (tolerant_close_split) -- and is not a + // refusal (R5 lane K-ULP4). An opening the book absorbs still is. + const bool dust_book = native_quantity_tolerance_ > 0.0 + && after_qty <= native_quantity_tolerance_; if (!std::isfinite(next) || (after_qty > 0.0 - && (next == after_qty || next == stage.opening))) { + && (next == after_qty || (next == stage.opening && !dust_book)))) { fail(Status::UnrepresentableQuantity); return; } diff --git a/src/native_c_host.cpp b/src/native_c_host.cpp index a40e56b5..623a7689 100644 --- a/src/native_c_host.cpp +++ b/src/native_c_host.cpp @@ -101,10 +101,14 @@ static_assert(PF_NATIVE_RUN_SPEC_EXT_V1_AUXILIARY_SIZE == PF_NATIVE_RUN_SPEC_EXT_V1_POLICY_SIZE + 3u * sizeof(void*) + 2u * sizeof(std::uint32_t), "the pf_native_run_spec_ext_v1 auxiliary tail moved"); -/* And the event-retention tail, last of the four: two words past it. */ -static_assert(sizeof(pf_native_run_spec_ext_v1) +/* And the event-retention tail: two words past it. */ +static_assert(PF_NATIVE_RUN_SPEC_EXT_V1_RETENTION_SIZE == PF_NATIVE_RUN_SPEC_EXT_V1_AUXILIARY_SIZE + 2u * sizeof(std::uint32_t), "the pf_native_run_spec_ext_v1 event-retention tail moved"); +/* And the quantity-tolerance tail, last of the five: one double past it. */ +static_assert(sizeof(pf_native_run_spec_ext_v1) + == PF_NATIVE_RUN_SPEC_EXT_V1_RETENTION_SIZE + sizeof(double), + "the pf_native_run_spec_ext_v1 quantity-tolerance tail moved"); static_assert(static_cast(pineforge::NativeEventRetention::Window) == PF_NATIVE_EVENT_RETENTION_WINDOW && static_cast(pineforge::NativeEventRetention::Full) @@ -473,6 +477,8 @@ PF_PIN_WORD(no::MatchRejectReason::NoOppositeExposure, PF_NATIVE_MATCH_REJECT_NO_OPPOSITE_EXPOSURE); PF_PIN_WORD(no::MatchRejectReason::HostPrecommit, PF_NATIVE_MATCH_REJECT_HOST_PRECOMMIT); PF_PIN_WORD(no::MatchRejectReason::RiskLimit, PF_NATIVE_MATCH_REJECT_RISK_LIMIT); +PF_PIN_WORD(no::MatchRejectReason::UnrepresentableQuantity, + PF_NATIVE_MATCH_REJECT_UNREPRESENTABLE_QUANTITY); PF_PIN_WORD(no::ActivationKind::Stop, PF_NATIVE_ACTIVATION_STOP); PF_PIN_WORD(no::ActivationKind::StopLimit, PF_NATIVE_ACTIVATION_STOP_LIMIT); PF_PIN_WORD(no::ActivationKind::TrailArm, PF_NATIVE_ACTIVATION_TRAIL_ARM); @@ -780,6 +786,8 @@ PF_PIN_WORD(pineforge::NativeRunSpecField::AuxiliaryFeedBars, PF_PIN_WORD(pineforge::NativeRunSpecField::SubscriptionSource, PF_NATIVE_SPEC_FIELD_SUBSCRIPTION_SOURCE); PF_PIN_WORD(pineforge::NativeRunSpecField::EventRetention, PF_NATIVE_SPEC_FIELD_EVENT_RETENTION); +PF_PIN_WORD(pineforge::NativeRunSpecField::QuantityTolerance, + PF_NATIVE_SPEC_FIELD_QUANTITY_TOLERANCE); PF_PIN_WORD(pineforge::NativeFxCurveError::None, PF_NATIVE_FX_CURVE_ERROR_NONE); PF_PIN_WORD(pineforge::NativeFxCurveError::LengthMismatch, PF_NATIVE_FX_CURVE_ERROR_LENGTH_MISMATCH); @@ -916,6 +924,7 @@ constexpr std::uint32_t c_word(no::MatchRejectReason value) noexcept { case V::NoOppositeExposure: return PF_NATIVE_MATCH_REJECT_NO_OPPOSITE_EXPOSURE; case V::HostPrecommit: return PF_NATIVE_MATCH_REJECT_HOST_PRECOMMIT; case V::RiskLimit: return PF_NATIVE_MATCH_REJECT_RISK_LIMIT; + case V::UnrepresentableQuantity: return PF_NATIVE_MATCH_REJECT_UNREPRESENTABLE_QUANTITY; } return static_cast(value); } @@ -1217,6 +1226,7 @@ constexpr std::uint32_t c_word(pineforge::NativeRunSpecField value) noexcept { case V::AuxiliaryFeedBars: return PF_NATIVE_SPEC_FIELD_AUXILIARY_FEED_BARS; case V::SubscriptionSource: return PF_NATIVE_SPEC_FIELD_SUBSCRIPTION_SOURCE; case V::EventRetention: return PF_NATIVE_SPEC_FIELD_EVENT_RETENTION; + case V::QuantityTolerance: return PF_NATIVE_SPEC_FIELD_QUANTITY_TOLERANCE; } return static_cast(value); } @@ -3108,8 +3118,8 @@ int translate_subscriptions(const pf_native_subscription_v1* rows, std::uint32_t int apply_spec_ext(pineforge::NativeRunSpec& spec, const pf_native_run_spec_ext_v1& ext, bool has_risk_tail, bool has_policy_tail, bool has_auxiliary_tail, - bool has_retention_tail) { - if (ext.present_mask & ~0x7ffu) return PF_NATIVE_E_TAG; + bool has_retention_tail, bool has_tolerance_tail) { + if (ext.present_mask & ~0xfffu) return PF_NATIVE_E_TAG; if ((ext.present_mask & PF_NATIVE_SPEC_EXT_RISK) && !has_risk_tail) { return PF_NATIVE_E_STRUCT; } @@ -3119,6 +3129,14 @@ int apply_spec_ext(pineforge::NativeRunSpec& spec, const pf_native_run_spec_ext_ if ((ext.present_mask & PF_NATIVE_SPEC_EXT_EVENT_RETENTION) && !has_retention_tail) { return PF_NATIVE_E_STRUCT; } + if ((ext.present_mask & PF_NATIVE_SPEC_EXT_QUANTITY_TOLERANCE) && !has_tolerance_tail) { + return PF_NATIVE_E_STRUCT; + } + /* K-ULP4: the tolerance is set only under its bit; the value itself is + * judged by validate_native_run_spec (QuantityTolerance). */ + if (ext.present_mask & PF_NATIVE_SPEC_EXT_QUANTITY_TOLERANCE) { + spec.quantity_tolerance = ext.quantity_tolerance; + } /* V19-B: without the bit the base translation's FULL stands. */ if (ext.present_mask & PF_NATIVE_SPEC_EXT_EVENT_RETENTION) { if (ext.reserved2 != 0u) return PF_NATIVE_E_TAG; @@ -3966,12 +3984,15 @@ PF_API int strategy_configure_native_ext_result_v1( if (!host) return PF_NATIVE_E_HANDLE; if (!base || !ext) return c_refuse(host, PF_NATIVE_E_ARGUMENT); if (base->struct_size != sizeof(pf_native_run_spec_v1)) return c_refuse(host, PF_NATIVE_E_STRUCT); - /* Five published layouts, and only five: the base one the lane + /* Six published layouts, and only six: the base one the lane * first shipped, that plus L9's risk tail, that plus N8's intrabar / - * policy tail, that plus the auxiliary-feed tail, and the current one - * with V19-B's event-retention tail behind it. Anything else is a - * caller this runtime cannot read. */ - const bool has_retention_tail = ext->struct_size == sizeof(pf_native_run_spec_ext_v1); + * policy tail, that plus the auxiliary-feed tail, that plus V19-B's + * event-retention tail, and the current one with K-ULP4's + * quantity-tolerance tail behind it. Anything else is a caller this + * runtime cannot read. */ + const bool has_tolerance_tail = ext->struct_size == sizeof(pf_native_run_spec_ext_v1); + const bool has_retention_tail = + has_tolerance_tail || ext->struct_size == PF_NATIVE_RUN_SPEC_EXT_V1_RETENTION_SIZE; const bool has_auxiliary_tail = has_retention_tail || ext->struct_size == PF_NATIVE_RUN_SPEC_EXT_V1_AUXILIARY_SIZE; const bool has_policy_tail = @@ -4010,7 +4031,8 @@ PF_API int strategy_configure_native_ext_result_v1( return c_refuse(host, rc); } if (int rc = apply_spec_ext(spec, *ext, has_risk_tail, has_policy_tail, - has_auxiliary_tail, has_retention_tail); + has_auxiliary_tail, has_retention_tail, + has_tolerance_tail); rc != PF_NATIVE_OK) { return c_refuse(host, rc); } diff --git a/src/native_execution_consumer.cpp b/src/native_execution_consumer.cpp index 855bd551..be699096 100644 --- a/src/native_execution_consumer.cpp +++ b/src/native_execution_consumer.cpp @@ -329,6 +329,11 @@ void hash_spec(F& f, const NativeRunSpec& spec, if (spec.event_retention != NativeEventRetention::Window) { f.u(static_cast(spec.event_retention)); } + // K-ULP4: the quantity tolerance folds only where a host declared one, so + // every spec that declares none keeps its digest. + if (spec.quantity_tolerance) { + f.d(*spec.quantity_tolerance); + } } template @@ -455,10 +460,14 @@ bool explicit_reduction_units_representable(double units, double exposure) noexc return order_action::plan(exposure, order_action::Reduce{units}).has_value(); } +// `own_quantity`: the answered units are the book's own -- a closing size equal +// to one lot's binary64 quantity, or a fraction's whole scope -- which the +// quantity grid admits as they stand (R5 lane K-ULP4). bool execution_terms_grid_representable( const native_order::ExecutionTerms& terms, const native_order::HostSized* host_sized, bool unresolved, - double scope_exposure_units, const NativeRunSpec* spec) noexcept { + double scope_exposure_units, const NativeRunSpec* spec, + bool own_quantity) noexcept { if (!valid_execution_grid_policy(terms.grid_policy)) return false; if (terms.grid_policy == native_order::ExecutionGridPolicy::ExplicitUnits) { return unresolved && host_sized @@ -470,7 +479,8 @@ bool execution_terms_grid_representable( } if (!terms.units || !(*terms.units > 0.0)) return true; return spec && (!spec->quantity_grid - || native_order::quantity_on_grid(*terms.units, *spec->quantity_grid)); + || native_order::quantity_on_grid(*terms.units, *spec->quantity_grid) + || own_quantity); } bool path_uses_high_first(const Bar& bar, NativePathOrder order) noexcept { @@ -1901,6 +1911,7 @@ bool NativeExecutionConsumer::apply_spec(BacktestEngine& engine, const NativeRun engine.syminfo_mintick_ = spec.price_tick; engine.commission_type_ = fee_to_commission(spec.fee_kind); engine.commission_value_ = spec.fee_value; + engine.native_quantity_tolerance_ = spec.quantity_tolerance ? *spec.quantity_tolerance : 0.0; engine.syminfo_.ticker = spec.ticker; engine.syminfo_.tickerid = spec.tickerid; engine.syminfo_.type = spec.type; @@ -2809,6 +2820,21 @@ native_order::CommandContext NativeExecutionConsumer::make_command_context( ? current_frame_->point.decision.coordinate.effective_time_ms : decision_floor(); if (const auto* spec = spec_ptr()) { ctx.quantity_grid = spec->quantity_grid; + // A close of exactly one of the book's own lots is on the grid as it + // stands (CommandContext::units_are_lot_quantity; R5 lane K-ULP4). + if (spec->quantity_grid && engine.position_side_ != PositionSide::FLAT) { + if (const auto* reduce = std::get_if(&request.intent)) { + if (const auto* units = std::get_if(&reduce->size)) { + ctx.units_are_lot_quantity = lot_quantity(engine, units->units); + } + } else if (const auto* transact = std::get_if( + &request.intent)) { + const bool closes = (engine.position_side_ == PositionSide::LONG) + == (transact->signed_units < 0.0); + ctx.units_are_lot_quantity = closes + && lot_quantity(engine, std::abs(transact->signed_units)); + } + } // A Sized request freezes its sizing price here when it asked for the // signal rule, and a Sized{AtAcceptance} additionally freezes its // units against that price, the marked equity there and the activated @@ -4626,9 +4652,18 @@ bool NativeExecutionConsumer::admit_placement_units( nullptr); } +bool NativeExecutionConsumer::lot_quantity(const BacktestEngine& engine, + double units) noexcept { + for (const auto& lot : engine.pyramid_entries_) { + if (lot.qty == units) return true; + } + return false; +} + std::optional NativeExecutionConsumer::resolve_sized_units( const BacktestEngine& engine, const native_order::LiveRequest& live, - const NativeExecutionTermsFacts& facts) const { + const NativeExecutionTermsFacts& facts, bool* whole_scope) const { + if (whole_scope) *whole_scope = false; const auto* spec = spec_ptr(); if (!spec) return std::nullopt; if (const auto* native_sized = sized_intent(live)) { @@ -4666,8 +4701,17 @@ std::optional NativeExecutionConsumer::resolve_sized_units( // units = scope * fraction, one binary64 multiplication. A percent-spelled // caller converts percent -> fraction itself, so no second rounding step // enters here. - return representable_units(scope * fraction->fraction, - native_order::ExecutionGridPolicy::SnapToGrid, + const double units = scope * fraction->fraction; + // A fraction whose product is the scope itself -- fraction 1, "close it + // all" -- resolves to the scope, which the grid does not floor. The scope + // is the binary64 fold of the book's own lots, which 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 (R5 lane K-ULP4). + if (units == scope) { + if (whole_scope) *whole_scope = true; + return scope; + } + return representable_units(units, native_order::ExecutionGridPolicy::SnapToGrid, spec->quantity_grid); } @@ -4807,8 +4851,9 @@ std::optional NativeExecutionConsumer::consume_mat // and publishes the result as the facts' remaining units, so a host // override of resolve_execution_terms still has the last word. std::optional kernel_units; + bool whole_scope = false; if (unresolved && (native_sized || scope_fraction)) { - kernel_units = resolve_sized_units(engine, *live, terms_facts); + kernel_units = resolve_sized_units(engine, *live, terms_facts, &whole_scope); if (kernel_units) { terms_facts.remaining = native_order::RemainingUnits{*kernel_units}; } @@ -4860,9 +4905,14 @@ std::optional NativeExecutionConsumer::consume_mat if (terms.units && (!std::isfinite(*terms.units) || *terms.units < 0.0)) { return terminal(native_order::MatchRejectReason::InvalidTerms, terms); } + const auto* grid_spec = spec_ptr(); + const bool own_quantity = closing_size && terms.units && grid_spec + && grid_spec->quantity_grid + && ((whole_scope && kernel_units && *terms.units == *kernel_units) + || lot_quantity(engine, *terms.units)); if (!execution_terms_grid_representable( terms, host_sized, unresolved, terms_facts.scope_exposure_units, - spec_ptr())) { + spec_ptr(), own_quantity)) { return terminal(native_order::MatchRejectReason::InvalidTerms, terms); } @@ -5027,6 +5077,15 @@ std::optional NativeExecutionConsumer::consume_mat execution_fx, plan ? &*plan : nullptr); const auto& inspect = candidate.inspect; if (inspect.status == execution::Status::NoEffect) return terminal(std::nullopt); + // A quantity the settlement cannot book exactly on this book is that + // request's refusal, not the run's failure: the request ends with a + // typed MatchRejected and nothing moves (R5 lane K-ULP4). Every other + // non-Applied inspection is a broken book, price or accounting, and + // still stops the run. + if (inspect.status == execution::Status::UnrepresentableQuantity) { + return terminal(native_order::MatchRejectReason::UnrepresentableQuantity, + nonidentity_attempt); + } if (inspect.status != execution::Status::Applied) { fail(engine, NativeFailure{NativeFailureCode::SettlementFailure, NativeFailureOperation::Settlement, P, static_cast(inspect.status)}); @@ -6324,8 +6383,9 @@ NativeCurrentExecutionPreview NativeExecutionConsumer::inspect_current_execution native_order::NativeCandidatePriceKind::CurrentQuote, command.price_rule, raw_price, default_resolved); std::optional kernel_units; + bool whole_scope = false; if (unresolved && (native_sized || scope_fraction)) { - kernel_units = resolve_sized_units(engine, *live, facts); + kernel_units = resolve_sized_units(engine, *live, facts, &whole_scope); if (kernel_units) facts.remaining = native_order::RemainingUnits{*kernel_units}; } native_order::ExecutionTerms terms; @@ -6373,8 +6433,14 @@ NativeCurrentExecutionPreview NativeExecutionConsumer::inspect_current_execution out.terms_rejection = native_order::MatchRejectReason::InvalidTerms; return out; } + const auto* grid_spec = spec_ptr(); + const bool own_quantity = closing_size && terms.units && grid_spec + && grid_spec->quantity_grid + && ((whole_scope && kernel_units && *terms.units == *kernel_units) + || lot_quantity(engine, *terms.units)); if (!execution_terms_grid_representable( - terms, host_sized, unresolved, facts.scope_exposure_units, spec_ptr())) { + terms, host_sized, unresolved, facts.scope_exposure_units, spec_ptr(), + own_quantity)) { out.terms_rejection = native_order::MatchRejectReason::InvalidTerms; return out; } diff --git a/src/native_execution_consumer.hpp b/src/native_execution_consumer.hpp index 4c24983a..6a626d3c 100644 --- a/src/native_execution_consumer.hpp +++ b/src/native_execution_consumer.hpp @@ -593,9 +593,14 @@ class NativeExecutionConsumer final : public IExecutionConsumer { // L3 kernel sizing bases. resolve_sized_units answers the units a Sized // opening or a ScopeFraction reduce claims at this candidate; nullopt is // the nonrepresentable basis the matching path reports as TermsUnresolved. + // `whole_scope`, when given, says whether the answer is a fraction's + // whole scope, which the quantity grid does not floor (R5 lane K-ULP4). std::optional resolve_sized_units( const BacktestEngine& engine, const native_order::LiveRequest& live, - const NativeExecutionTermsFacts& facts) const; + const NativeExecutionTermsFacts& facts, bool* whole_scope = nullptr) const; + // Whether `units` is the binary64 quantity of one of the book's own lots, + // which the quantity grid admits as it stands (R5 lane K-ULP4). + static bool lot_quantity(const BacktestEngine& engine, double units) noexcept; double sibling_claimed_units(const native_order::LiveRequest& live) const noexcept; // L3b placement-time sizing. sizing_point_price answers the price a Sized // request's basis converts at when it is accepted; placement_scope_units diff --git a/src/native_order.cpp b/src/native_order.cpp index 765cbe4f..90cffc75 100644 --- a/src/native_order.cpp +++ b/src/native_order.cpp @@ -1552,7 +1552,8 @@ std::optional WorkingRequestCore::validate_request( } else if (const auto* reduce = as_reduce(request.intent)) { if (const auto* units = explicit_size(*reduce)) { if (!finite_positive(units->units)) return RequestRejectReason::InvalidQuantity; - if (!on_optional_grid(units->units, context.quantity_grid)) { + if (!on_optional_grid(units->units, context.quantity_grid) + && !context.units_are_lot_quantity) { return RequestRejectReason::OffGrid; } } else if (const auto* fraction = fraction_size(*reduce)) { @@ -1590,7 +1591,8 @@ std::optional WorkingRequestCore::validate_request( } } else if (const auto* transact = as_transact(request.intent)) { if (!finite_nonzero(transact->signed_units)) return RequestRejectReason::InvalidQuantity; - if (!on_optional_grid(transact->signed_units, context.quantity_grid)) { + if (!on_optional_grid(transact->signed_units, context.quantity_grid) + && !context.units_are_lot_quantity) { return RequestRejectReason::OffGrid; } } else if (reverse_to) { diff --git a/src/native_run_spec.cpp b/src/native_run_spec.cpp index 21198be2..8eea61aa 100644 --- a/src/native_run_spec.cpp +++ b/src/native_run_spec.cpp @@ -566,6 +566,8 @@ Result validate_values(const NativeRunSpec& spec) noexcept { return {Error::NotFiniteNonnegative, Field::FeeValue}; if (spec.quantity_grid && !positive(*spec.quantity_grid)) return {Error::NotFinitePositive, Field::QuantityGrid}; + if (spec.quantity_tolerance && !positive(*spec.quantity_tolerance)) + return {Error::NotFinitePositive, Field::QuantityTolerance}; switch (spec.close_execution) { case NativeCloseExecution::NextEligiblePoint: case NativeCloseExecution::AfterCalculation: diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7911c081..153c37eb 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1167,6 +1167,23 @@ list(APPEND TEST_SOURCES test_native_partial_close_split) # and the refusals of unrepresentable splits are controls. Source-free, so it # also runs in the kernel-only profile. list(APPEND TEST_SOURCES test_native_exact_sum_close) +# R5 lane K-ULP4: a request the settlement cannot book exactly on its book -- +# a close split binary64 cannot hold, a reduction the position absorbs, an +# opening beside a dust lot -- ends with a typed +# MatchRejected(UnrepresentableQuantity) and the run goes on (it stopped with +# code 6 / discriminator 5 on 91d65ad6), staged and direct, execute_current +# and its preview, a C host; on a quantity grid a whole-scope fraction and a +# close of one lot's own size are the book's own quantities and settle. +# Source-free, so it also runs in the kernel-only profile. +list(APPEND TEST_SOURCES test_native_unrepresentable_refusal) +# R5 lane K-ULP4 (ruling part b): NativeRunSpec::quantity_tolerance -- a close +# within the tolerance of a FIFO boundary ends at it, charged its request; a +# dust lot a close meets at the head of the book closes whole; a dust book +# beside an opening stays; an overshoot opens nothing -- staged and direct, +# fused and staged-only, the spec's validation and digest (unchanged when +# absent, pinned from 91d65ad6), and the C extension's tail. Source-free, so it +# also runs in the kernel-only profile. +list(APPEND TEST_SOURCES test_native_quantity_tolerance) # 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/fixtures/native_cpp_abi/host-fc7aad6/relocation-manifest-v18-v19.json b/tests/fixtures/native_cpp_abi/host-fc7aad6/relocation-manifest-v18-v19.json index 4ec167a1..7dafe02c 100644 --- a/tests/fixtures/native_cpp_abi/host-fc7aad6/relocation-manifest-v18-v19.json +++ b/tests/fixtures/native_cpp_abi/host-fc7aad6/relocation-manifest-v18-v19.json @@ -25,7 +25,11 @@ "RequestDefinition::priority", "RequestDefinition::kept_binding", "ReplaceOptions::keep_handle", - "ReplaceOptions::keep_binding" + "ReplaceOptions::keep_binding", + "MatchRejectReason::UnrepresentableQuantity", + "CommandContext::units_are_lot_quantity", + "NativeRunSpec::quantity_tolerance", + "NativeRunSpecField::QuantityTolerance" ], "addedVirtuals": [], "removedVirtuals": [], diff --git a/tests/test_native_c_api.c b/tests/test_native_c_api.c index ea2bf122..5698b00e 100644 --- a/tests/test_native_c_api.c +++ b/tests/test_native_c_api.c @@ -1229,7 +1229,7 @@ static void check_event_retention(void) { const uint32_t current = (uint32_t)sizeof(pf_native_run_spec_ext_v1); const uint32_t bit = PF_NATIVE_SPEC_EXT_EVENT_RETENTION; retention_run v1, clear, older, full, commands, window, polled, bad_word, bad_reserved, - older_bit; + older_bit, fifth; v1 = retention_case(0u, 0u, 0u, 0u, 0); clear = retention_case(current, 0u, PF_NATIVE_EVENT_RETENTION_WINDOW, 0u, 0); @@ -1242,6 +1242,9 @@ static void check_event_retention(void) { bad_reserved = retention_case(current, bit, PF_NATIVE_EVENT_RETENTION_FULL, 1u, 0); older_bit = retention_case(PF_NATIVE_RUN_SPEC_EXT_V1_AUXILIARY_SIZE, bit, PF_NATIVE_EVENT_RETENTION_WINDOW, 0u, 0); + /* K-ULP4 appended a sixth layout; the fifth still carries the word. */ + fifth = retention_case(PF_NATIVE_RUN_SPEC_EXT_V1_RETENTION_SIZE, bit, + PF_NATIVE_EVENT_RETENTION_COMMANDS, 0u, 0); /* A caller that does not send the word keeps everything. */ CHECK_EQ_INT(v1.configure, PF_NATIVE_OK, "the v1 spec was refused"); @@ -1280,6 +1283,9 @@ static void check_event_retention(void) { CHECK_EQ_INT(bad_reserved.configure, PF_NATIVE_E_TAG, "a nonzero reserved2 was accepted"); CHECK_EQ_INT(older_bit.configure, PF_NATIVE_E_STRUCT, "the retention bit was accepted from the fourth layout"); + CHECK_EQ_INT(fifth.configure, PF_NATIVE_OK, "the fifth layout's retention word was refused"); + CHECK(fifth.drivers == 0 && fifth.commands == v1.commands, + "the fifth layout did not read COMMANDS"); /* The two symbols refuse what they cannot read. */ CHECK_EQ_INT(strategy_native_acknowledge_events_v1(NULL, 0u), PF_NATIVE_E_HANDLE, "a NULL handle's acknowledgement was accepted"); @@ -1287,6 +1293,171 @@ static void check_event_retention(void) { "a NULL handle's window read was accepted"); } +/* ── K-ULP4: the typed quantity refusal and the tolerance tail, from C ─ + * + * Open 1.5, Reduce 1.3, open 2.9: the book is {fl(1.5 - 1.3), 2.9}, and + * Reduce 0.2 needs 2^-54 of the 2.9 lot, below half its ulp. The exact + * settlement refuses THAT request with a MATCH_REJECTED row whose reason is + * PF_NATIVE_MATCH_REJECT_UNREPRESENTABLE_QUANTITY and the run completes (it + * failed with code 6 before the lane); under the extension's + * quantity-tolerance tail the close ends at the first lot. */ +typedef struct kulp4_state { + pf_strategy_t host; + int bar; + int error; + uint64_t last; +} kulp4_state; + +static int kulp4_on_bar(void* user, const pf_bar_t* bar, const pf_native_decision_v1* at) { + static const double units[4] = {1.5, 1.3, 2.9, 0.2}; + static const int reduces[4] = {0, 1, 0, 1}; + kulp4_state* state = (kulp4_state*)user; + const int b = state->bar++; + pf_native_request_v1 request; + uint64_t incarnation = 0; + uint32_t reject = 0; + (void)bar; + (void)at; + if (b % 2 != 0 || b / 2 >= 4) return 0; + request = blank_request(); + request.trigger = PF_NATIVE_TRIGGER_MARKET; + request.label = "k-ulp4"; + request.comment = ""; + if (reduces[b / 2]) { + request.intent = PF_NATIVE_INTENT_REDUCE; + request.reduce_size = PF_NATIVE_REDUCE_EXPLICIT_UNITS; + } else { + request.intent = PF_NATIVE_INTENT_TRANSACT; + } + request.intent_value = units[b / 2]; + if (strategy_native_submit_v1(state->host, &request, &incarnation, &reject) != PF_NATIVE_OK + && state->error == 0) { + state->error = 1; + } + state->last = incarnation; + return 0; +} + +/* size 0 = strategy_configure_native_v1 alone; otherwise the extension at + * that size, carrying the tolerance bit when `tolerance` is not 0. Answers the + * configure status, the last request's MATCH_REJECTED reason (-1 when it has + * none), the book's lot count and the lifecycle. */ +static int kulp4_case(uint32_t size, double tolerance, uint32_t* error, uint32_t* field, + int* reason, int* lots, uint32_t* lifecycle) { + static pf_native_event_v1 rows[256]; + kulp4_state state; + pf_native_callbacks_v1 table; + pf_native_run_spec_v1 spec = twin_spec(); + pf_native_run_spec_ext_v1 ext; + pf_native_state_v1 run_state; + pf_bar_t bars[12]; + int configure; + int got; + int i; + + *reason = -1; + *lots = -1; + *lifecycle = 0; + memset(&state, 0, sizeof(state)); + table = blank_callbacks(&state); + table.on_bar = kulp4_on_bar; + state.host = strategy_native_host_create_v1(&table); + CHECK(state.host != NULL, "K-ULP4 host create failed"); + if (!state.host) return -100; + spec.session_key = "native-c-api-k-ulp4"; + spec.initial_capital = 1e9; + spec.fee_kind = PF_NATIVE_FEE_CASH_PER_EXECUTION; + if (size == 0u) { + configure = strategy_configure_native_v1(state.host, &spec); + } else { + memset(&ext, 0, sizeof(ext)); + ext.struct_size = size; + ext.version = PF_NATIVE_API_VERSION; + ext.present_mask = PF_NATIVE_SPEC_EXT_EVENT_RETENTION; + ext.event_retention = PF_NATIVE_EVENT_RETENTION_FULL; + if (tolerance != 0.0) { + ext.present_mask |= PF_NATIVE_SPEC_EXT_QUANTITY_TOLERANCE; + ext.quantity_tolerance = tolerance; + } + configure = strategy_configure_native_ext_result_v1(state.host, &spec, &ext, error, + field); + } + if (configure == PF_NATIVE_OK) { + for (i = 0; i < 12; ++i) { + bars[i].open = bars[i].high = bars[i].low = bars[i].close = 100.0; + bars[i].volume = 5.0; + bars[i].timestamp = (int64_t)i * 300000; + } + CHECK_EQ_INT(strategy_native_run_v1(state.host, bars, 12, NULL), PF_NATIVE_OK, + "K-ULP4 run"); + CHECK_EQ_INT(state.error, 0, "a K-ULP4 submit was refused"); + memset(&run_state, 0, sizeof(run_state)); + run_state.struct_size = (uint32_t)sizeof(run_state); + run_state.version = PF_NATIVE_API_VERSION; + CHECK_EQ_INT(strategy_native_state_v1(state.host, &run_state), PF_NATIVE_OK, + "K-ULP4 state read"); + *lifecycle = run_state.lifecycle; + memset(rows, 0, sizeof(rows)); + for (i = 0; i < 256; ++i) { + rows[i].struct_size = (uint32_t)sizeof(rows[i]); + rows[i].version = PF_NATIVE_API_VERSION; + } + got = strategy_native_events_v1(state.host, 0, rows, 256); + for (i = 0; i < got; ++i) { + if (rows[i].kind == PF_NATIVE_EVENT_MATCH_REJECTED + && rows[i].incarnation == state.last) { + *reason = (int)rows[i].reason; + } + } + *lots = strategy_native_open_lot_count_v1(state.host, NAN); + } + strategy_native_host_free(state.host); + return configure; +} + +static void check_unrepresentable_quantity(void) { + uint32_t error = 0; + uint32_t field = 0; + uint32_t lifecycle = 0; + int reason = 0; + int lots = 0; + const uint32_t current = (uint32_t)sizeof(pf_native_run_spec_ext_v1); + + CHECK_EQ_INT(PF_NATIVE_MATCH_REJECT_UNREPRESENTABLE_QUANTITY, 10, + "the typed quantity refusal's word moved"); + CHECK_EQ_INT(PF_NATIVE_SPEC_FIELD_QUANTITY_TOLERANCE, 65, + "the tolerance's field word moved"); + CHECK_EQ_INT(PF_NATIVE_SPEC_EXT_QUANTITY_TOLERANCE, 1u << 11, + "the tolerance's mask bit moved"); + /* Exact: the request is refused, typed; the book is untouched; the run + * completes. */ + CHECK_EQ_INT(kulp4_case(0u, 0.0, &error, &field, &reason, &lots, &lifecycle), PF_NATIVE_OK, + "the K-ULP4 v1 spec was refused"); + CHECK_EQ_INT(reason, PF_NATIVE_MATCH_REJECT_UNREPRESENTABLE_QUANTITY, + "Reduce 0.2 was not the typed quantity refusal"); + CHECK_EQ_INT(lifecycle, PF_NATIVE_LIFECYCLE_COMPLETED, "the refusal stopped the run"); + CHECK_EQ_INT(lots, 2, "the refused close moved the book"); + /* The tolerance tail: the close ends at the first lot, whole. */ + CHECK_EQ_INT(kulp4_case(current, 1e-10, &error, &field, &reason, &lots, &lifecycle), + PF_NATIVE_OK, "the tolerance tail was refused"); + CHECK_EQ_INT(reason, -1, "a close within the tolerance was refused"); + CHECK_EQ_INT(lifecycle, PF_NATIVE_LIFECYCLE_COMPLETED, "the tolerant run did not complete"); + CHECK_EQ_INT(lots, 1, "the close within the tolerance did not end at the lot"); + /* The bit from the fifth layout, which has no field for it, and a bad + * value, named by its field. */ + CHECK_EQ_INT(kulp4_case(PF_NATIVE_RUN_SPEC_EXT_V1_RETENTION_SIZE, 1e-10, &error, &field, + &reason, &lots, &lifecycle), + PF_NATIVE_E_STRUCT, "the tolerance bit was accepted from the fifth layout"); + error = 0; + field = 0; + CHECK(kulp4_case(current, -1.0, &error, &field, &reason, &lots, &lifecycle) != PF_NATIVE_OK, + "a negative tolerance was accepted"); + CHECK_EQ_INT(error, PF_NATIVE_SPEC_ERROR_NOT_FINITE_POSITIVE, + "a negative tolerance named another error"); + CHECK_EQ_INT(field, PF_NATIVE_SPEC_FIELD_QUANTITY_TOLERANCE, + "a negative tolerance named another field"); +} + /* ── L9's risk limits, read back through the C event history ────── */ /* One opening per calculation, with a limit of one applied fill per day: the @@ -5571,7 +5742,13 @@ static void check_spec_word_layout(void) { "the event-retention tail does not start where the auxiliary layout ended"); CHECK_EQ_INT(PF_NATIVE_RUN_SPEC_EXT_V1_AUXILIARY_SIZE, 304, "the auxiliary layout's length moved"); - CHECK_EQ_INT(sizeof(pf_native_run_spec_ext_v1), 312, "pf_native_run_spec_ext_v1 resized"); + /* K-ULP4 appends the quantity-tolerance tail: one double where the + * retention layout ended, 312 -> 320. */ + CHECK_EQ_INT(offsetof(pf_native_run_spec_ext_v1, quantity_tolerance), 312, + "the quantity-tolerance tail does not start where the retention layout ended"); + CHECK_EQ_INT(PF_NATIVE_RUN_SPEC_EXT_V1_RETENTION_SIZE, 312, + "the retention layout's length moved"); + CHECK_EQ_INT(sizeof(pf_native_run_spec_ext_v1), 320, "pf_native_run_spec_ext_v1 resized"); } } @@ -5698,6 +5875,7 @@ static int match_reject_named(uint32_t w) { case PF_NATIVE_MATCH_REJECT_NO_OPPOSITE_EXPOSURE: case PF_NATIVE_MATCH_REJECT_HOST_PRECOMMIT: case PF_NATIVE_MATCH_REJECT_RISK_LIMIT: + case PF_NATIVE_MATCH_REJECT_UNREPRESENTABLE_QUANTITY: return 1; default: return 0; @@ -8701,6 +8879,7 @@ int pf_native_c_api_checks(void) { check_callback_failure_latch(); check_event_polling(); check_event_retention(); + check_unrepresentable_quantity(); check_risk_event(); check_absent_accessors(); check_live_accessors(); diff --git a/tests/test_native_exact_sum_close.cpp b/tests/test_native_exact_sum_close.cpp index d42e63a6..70cda936 100644 --- a/tests/test_native_exact_sum_close.cpp +++ b/tests/test_native_exact_sum_close.cpp @@ -55,7 +55,10 @@ // closed by its whole sum (consumed exactly), a // one-lot book (the fused settlement) // genuine-* the refusals of splits binary64 cannot hold: -// still code 6, discriminator 5, nothing moved +// a typed MatchRejected(UnrepresentableQuantity), +// nothing moved, the run completed (code 6, +// discriminator 5 and a stopped run until R5 +// lane K-ULP4) // // Fail-before: on 07249e3b every dust case closes r and keeps fl(q - r) -- a // dust lot, and a dust position when the request is the whole book -- while the @@ -218,12 +221,15 @@ struct Outcome { std::vector rows; std::size_t working = 0; std::vector fills; + // The close's MatchRejected reasons, in event order (R5 lane K-ULP4). + std::vector rejections; std::uint64_t continuation = 0, broker = 0; bool operator==(const Outcome& o) const { return completed == o.completed && failure_code == o.failure_code && discriminator == o.discriminator && signed_units == o.signed_units && lots == o.lots && rows == o.rows && working == o.working - && fills == o.fills && continuation == o.continuation && broker == o.broker; + && fills == o.fills && rejections == o.rejections + && continuation == o.continuation && broker == o.broker; } }; @@ -335,6 +341,12 @@ Outcome run_cpp(const Case& c, bool direct, Host** keep = nullptr) { out.working = host->native_working_requests().size(); for (const auto& row : host->native_events(0)) { if (!row.command) continue; + if (const auto* r = std::get_if(&*row.command)) { + if (r->request().label == "close") { + out.rejections.push_back(static_cast(r->reason)); + } + continue; + } const auto* e = std::get_if(&*row.command); if (!e || e->request().label != "close") continue; Fill f; @@ -840,15 +852,19 @@ void run_genuine_case(const Case& c) { std::printf("%s: U=%a\n", c.name, c.units); print_outcome("staged", staged); print_outcome("direct", direct); + // The request is refused, typed, and the run goes on (R5 lane K-ULP4). for (const Outcome* o : {&staged, &direct}) { - CHECK(!o->completed); - CHECK(o->failure_code == static_cast(NativeFailureCode::SettlementFailure)); - CHECK(o->discriminator - == static_cast(ex::Status::UnrepresentableQuantity)); + CHECK(o->completed); + CHECK(o->failure_code == 0); + CHECK(o->discriminator == 0); CHECK(o->lots == c.lots); CHECK(o->rows.empty()); CHECK(o->fills.empty()); - CHECK(o->working == 1); + CHECK(o->working == 0); + CHECK(o->rejections.size() == 1); + CHECK(!o->rejections.empty() + && o->rejections.front() + == static_cast(no::MatchRejectReason::UnrepresentableQuantity)); } CHECK(staged == direct); } diff --git a/tests/test_native_partial_close_split.cpp b/tests/test_native_partial_close_split.cpp index 25c71477..f6a42629 100644 --- a/tests/test_native_partial_close_split.cpp +++ b/tests/test_native_partial_close_split.cpp @@ -54,8 +54,11 @@ // genuine-* requests no binary64 split can carry: a // reduction that cannot move the position, a // rest below half an ulp of its lot, a rest that -// a whole lot cannot decrement. They still fail -// with code 6, discriminator 5, and move nothing. +// a whole lot cannot decrement. Each ends with a +// typed MatchRejected(UnrepresentableQuantity), +// moves nothing, and the run completes; until R5 +// lane K-ULP4 they stopped it with code 6, +// discriminator 5. // // Fail-before: on db98990c every up case stops with code 6 / discriminator 5, // every down Reduce books a second 2^-51 fill, every down Transact stops, and @@ -210,12 +213,15 @@ struct Outcome { std::vector rows; std::size_t working = 0; std::vector fills; + // The close's MatchRejected reasons, in event order (R5 lane K-ULP4). + std::vector rejections; std::uint64_t continuation = 0, broker = 0; bool operator==(const Outcome& o) const { return completed == o.completed && failure_code == o.failure_code && discriminator == o.discriminator && signed_units == o.signed_units && lots == o.lots && rows == o.rows && working == o.working - && fills == o.fills && continuation == o.continuation && broker == o.broker; + && fills == o.fills && rejections == o.rejections + && continuation == o.continuation && broker == o.broker; } }; @@ -325,6 +331,12 @@ Outcome run_cpp(const Case& c, bool direct, Host** keep = nullptr) { out.working = host->native_working_requests().size(); for (const auto& row : host->native_events(0)) { if (!row.command) continue; + if (const auto* r = std::get_if(&*row.command)) { + if (r->request().label == "close") { + out.rejections.push_back(static_cast(r->reason)); + } + continue; + } const auto* e = std::get_if(&*row.command); if (!e || e->request().label != "close") continue; Fill f; @@ -736,15 +748,19 @@ void run_genuine_case(const Case& c) { std::printf("%s: U=%a\n", c.name, c.units); print_outcome("staged", staged); print_outcome("direct", direct); + // The request is refused, typed, and the run goes on (R5 lane K-ULP4). for (const Outcome* o : {&staged, &direct}) { - CHECK(!o->completed); - CHECK(o->failure_code == static_cast(NativeFailureCode::SettlementFailure)); - CHECK(o->discriminator - == static_cast(ex::Status::UnrepresentableQuantity)); + CHECK(o->completed); + CHECK(o->failure_code == 0); + CHECK(o->discriminator == 0); CHECK(o->lots == c.lots); CHECK(o->rows.empty()); CHECK(o->fills.empty()); - CHECK(o->working == 1); + CHECK(o->working == 0); + CHECK(o->rejections.size() == 1); + CHECK(!o->rejections.empty() + && o->rejections.front() + == static_cast(no::MatchRejectReason::UnrepresentableQuantity)); } CHECK(staged == direct); } diff --git a/tests/test_native_quantity_tolerance.cpp b/tests/test_native_quantity_tolerance.cpp new file mode 100644 index 00000000..51696a89 --- /dev/null +++ b/tests/test_native_quantity_tolerance.cpp @@ -0,0 +1,616 @@ +// R5 lane K-ULP4 (audit X1, ruling part b): the opt-in quantity tolerance. +// +// NativeRunSpec::quantity_tolerance = t declares that two quantities within t +// of each other are one quantity to the settlement (tolerant_close_split, +// src/engine_execution.cpp). A close that comes within t of a FIFO boundary +// -- the binary64 sum of the lots through one of them -- ends at that +// boundary, charged its request; a lot of at most t that binary64 cannot take +// off a close's rest, or its running sum, closes whole with the close that +// reaches it; a surviving book of at most t beside an opening stays. Absent, +// the settlement is exact and the spec digest is the one it had before the +// field (pinned below from 91d65ad6). +// +// snap-down {fl(1.5 - 1.3), 2.9}, Reduce 0.2: the lot closes whole +// and the 2^-54 rest is not taken from the 2.9 lot +// snap-down-transact the same, Transact{-0.2} +// snap-up Reduce prev(fl(1.5 - 1.3)): the lot closes whole, no +// 2^-55 lot is kept +// dust-head-reduce {2^-55, 2.9}, Reduce 1.0: the dust lot closes whole +// and the rest 1.0 comes from the 2.9 lot +// dust-head-open {2^-55}, open 200 on its side: the dust stays beside it +// dust-head-cross {2^-55, 2.9}, Transact{-10}: both close, 7.1 opens +// trailing-dust {1, 2^-40}, Reduce 1 + 5e-13: the boundary after the +// first lot is within t, and the dust lot behind it is +// taken rather than left +// overshoot {1}, Transact{-(1 + 1e-12)}: the book closes, no +// 1e-12 opposite lot opens; the same spelled Reduce is +// charged its request +// one-lot {1}, Reduce 1 - 1e-12: the fused one-lot settlement +// closes it whole +// exact-control a close exactly at a boundary, and one far from any +// still-refused a request of at most t on a larger lot (zero is not a +// boundary to snap to), and a rest above t a huge lot +// absorbs: typed MatchRejected(UnrepresentableQuantity) +// spec validation (NotFinitePositive on QuantityTolerance), +// the digest pin, and the value folded when present +// c-host the extension's quantity-tolerance tail, its mask +// bit refused from the fifth layout, a bad value refused +// with its field word +// Every C++ case runs staged and direct on the request core, and with the +// fused one-lot settlement on and off, and all four must agree bit for bit. +// +// Fail-before: compiled against 91d65ad6 the TU stops at its first use of +// MatchRejectReason::UnrepresentableQuantity (no such enumerator), then at +// every use of NativeRunSpec::quantity_tolerance (no such member). +// Source-free: the kernel-only profile registers the row. +#include "../src/engine_internal.hpp" +#include "../src/native_execution_consumer.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace pineforge; +namespace no = pineforge::native_order; + +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) + +constexpr double kPrice = 100.0; +constexpr double kTolerance = 1e-10; +constexpr unsigned kUnrepresentable = + static_cast(no::MatchRejectReason::UnrepresentableQuantity); +const double kSurvivor = 1.5 - 1.3; // 0x1.9999999999998p-3 + +// native_run_spec_digest of base_spec() below, with and without a 0.1 +// quantity grid, computed on 91d65ad6, before the field existed. +constexpr std::uint64_t kDigestBefore = 6393930142210106639ULL; +constexpr std::uint64_t kDigestGridBefore = 15026561762967368001ULL; + +struct Step { + enum Kind { Transact, Reduce, Flatten } kind = Transact; + double value = 0.0; +}; + +struct Case { + const char* name; + std::vector steps; + std::optional tolerance = kTolerance; +}; + +struct StepOutcome { + int submit = -1; + int terminal = 0; // 0 none, 1 applied, 2 MatchRejected, 3 NoEffect + unsigned reason = 0; + std::vector closed, opened, filled; + std::vector rows; + std::vector book; + bool operator==(const StepOutcome& o) const { + return submit == o.submit && terminal == o.terminal && reason == o.reason + && closed == o.closed && opened == o.opened && filled == o.filled + && rows == o.rows && book == o.book; + } +}; + +struct Outcome { + bool completed = false; + unsigned code = 0; + std::vector steps; + std::uint64_t continuation = 0, broker = 0; + bool operator==(const Outcome& o) const { + return completed == o.completed && code == o.code && steps == o.steps + && continuation == o.continuation && broker == o.broker; + } +}; + +NativeRunSpec base_spec(const char* key) { + NativeRunSpec s; + s.event_retention = NativeEventRetention::Full; + s.identity = {key, 1}; + s.input_tf = "1"; + s.script_tf = "1"; + s.tickerid = "K-ULP4:TOLERANCE"; + 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; +} + +class Host final : public NativeStrategyHost { +public: + Host(const Case& c, bool direct) : case_(c) { + 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_++; + if (b % 2 != 0) return; + const std::size_t k = static_cast(b / 2); + if (k > 0 && k - 1 < steps.size()) steps[k - 1].book = book(); + if (k >= case_.steps.size()) return; + const Step& s = case_.steps[k]; + no::Request r; + switch (s.kind) { + case Step::Transact: r.intent = no::Transact{s.value}; break; + case Step::Reduce: r.intent = no::Reduce{no::ExplicitUnits{s.value}}; break; + case Step::Flatten: r.intent = no::Flatten{}; break; + } + r.label = "step"; + StepOutcome out; + const auto res = submit(r); + if (res.status != no::SubmitStatus::Accepted || !res.handle) { + out.submit = res.reason ? static_cast(*res.reason) : -2; + incarnations.push_back(0); + } else { + incarnations.push_back(res.handle->incarnation); + } + steps.push_back(out); + } + std::vector book() const { + std::vector out; + for (const auto& lot : native_open_lots(std::numeric_limits::quiet_NaN())) { + out.push_back(lot.signed_units); + } + return out; + } + std::vector row_units(std::size_t first, std::size_t n) const { + std::vector out; + for (std::size_t i = first; i < first + n && i < trades_.size(); ++i) { + out.push_back(trades_[i].qty); + } + return out; + } + std::vector steps; + std::vector incarnations; + +private: + Case case_; + int bar_ = 0; +}; + +Outcome run_cpp(const Case& c, bool direct, bool fused) { + internal::set_fused_settlement(fused); + auto host = std::make_unique(c, direct); + Outcome out; + auto spec = base_spec((std::string("k-ulp4-tol-") + c.name).c_str()); + spec.quantity_tolerance = c.tolerance; + CHECK(host->configure_native(spec).status == NativeSetupStatus::Applied); + std::vector bars; + const int n = static_cast(2 * c.steps.size() + 3); + for (int i = 0; i < n; ++i) { + bars.push_back(Bar{kPrice, kPrice, kPrice, kPrice, 10.0, 60'000LL * (i + 1)}); + } + host->run(bars.data(), n); + internal::set_fused_settlement(true); + const auto state = host->native_state(); + out.completed = state.kind == NativeLifecycleKind::Completed; + out.code = static_cast(state.failure.code); + out.steps = host->steps; + for (std::size_t k = 0; k < out.steps.size(); ++k) { + auto& step = out.steps[k]; + const std::uint64_t inc = host->incarnations[k]; + if (inc == 0) continue; + for (const auto& row : host->native_events(0)) { + if (!row.command) continue; + if (const auto* a = std::get_if(&*row.command)) { + if (a->handle().incarnation != inc) continue; + step.closed.push_back(a->closed_units); + step.opened.push_back(a->opened_units); + step.filled.push_back(a->filled_working); + const auto rows = host->row_units(a->first_trade_index, a->closed_trade_count); + step.rows.insert(step.rows.end(), rows.begin(), rows.end()); + if (a->terminal) step.terminal = 1; + } else if (const auto* r = std::get_if(&*row.command)) { + if (r->handle().incarnation != inc) continue; + step.terminal = 2; + step.reason = static_cast(r->reason); + } else if (const auto* e = std::get_if(&*row.command)) { + if (e->handle().incarnation == inc) step.terminal = 3; + } + } + } + out.continuation = host->native_continuation_hash(); + out.broker = host->broker_state_hash(); + return out; +} + +void print_outcome(const Outcome& o) { + std::printf(" completed=%d code=%u\n", o.completed ? 1 : 0, o.code); + for (std::size_t k = 0; k < o.steps.size(); ++k) { + const auto& s = o.steps[k]; + std::printf(" step %zu submit=%d terminal=%d reason=%u", k, s.submit, s.terminal, + s.reason); + for (std::size_t i = 0; i < s.filled.size(); ++i) { + std::printf(" [closed=%a opened=%a filled=%a]", s.closed[i], s.opened[i], s.filled[i]); + } + std::printf(" rows:"); + for (double r : s.rows) std::printf(" %a", r); + std::printf(" book:"); + for (double lot : s.book) std::printf(" %a", lot); + std::printf("\n"); + } +} + +// All four twins agree and the run completes. +Outcome run_case(const Case& c) { + scenario = c.name; + const Outcome a = run_cpp(c, false, true); + const Outcome b = run_cpp(c, true, true); + const Outcome d = run_cpp(c, false, false); + const Outcome e = run_cpp(c, true, false); + std::printf("%s (quantity_tolerance=%g)\n", c.name, c.tolerance ? *c.tolerance : 0.0); + print_outcome(a); + CHECK(a == b); + CHECK(a == d); + CHECK(a == e); + CHECK(a.completed); + CHECK(a.code == 0); + CHECK(a.steps.size() == c.steps.size()); + return a; +} + +void filled(const Outcome& o, std::size_t k, double closed, double opened, double units, + const std::vector& rows, const std::vector& book) { + CHECK(k < o.steps.size()); + if (k >= o.steps.size()) return; + const auto& s = o.steps[k]; + CHECK(s.submit == -1); + CHECK(s.terminal == 1); + CHECK(s.filled.size() == 1); + if (s.filled.size() == 1) { + CHECK(s.closed[0] == closed); + CHECK(s.opened[0] == opened); + CHECK(s.filled[0] == units); + } + CHECK(s.rows == rows); + CHECK(s.book == book); +} + +void refused(const Outcome& o, std::size_t k, const std::vector& book) { + CHECK(k < o.steps.size()); + if (k >= o.steps.size()) return; + const auto& s = o.steps[k]; + CHECK(s.submit == -1); + CHECK(s.terminal == 2); + CHECK(s.reason == kUnrepresentable); + CHECK(s.filled.empty() && s.rows.empty()); + CHECK(s.book == book); +} + +std::vector with(std::vector steps, const std::vector& more) { + steps.insert(steps.end(), more.begin(), more.end()); + return steps; +} + +void settlement_cases() { + const double s02 = kSurvivor; + const std::vector survivor_book = { + {Step::Transact, 1.5}, {Step::Reduce, 1.3}, {Step::Transact, 2.9}}; + { + const auto o = run_case({"snap-down", with(survivor_book, {{Step::Reduce, 0.2}})}); + filled(o, 3, 0.2, 0.0, 0.2, {s02}, {2.9}); + } + { + const auto o = run_case({"snap-down-transact", with(survivor_book, {{Step::Transact, -0.2}})}); + filled(o, 3, 0.2, -0.0, 0.2, {s02}, {2.9}); + } + // Exact, the same request is refused (test_native_unrepresentable_refusal). + { + Case c{"snap-down-exact-control", with(survivor_book, {{Step::Reduce, 0.2}}), std::nullopt}; + const auto o = run_case(c); + refused(o, 3, {s02, 2.9}); + } + { + const double u = std::nextafter(s02, 0.0); + const auto o = run_case({"snap-up", with(survivor_book, {{Step::Reduce, u}, {Step::Reduce, 1.0}})}); + filled(o, 3, u, 0.0, u, {s02}, {2.9}); + filled(o, 4, 1.0, 0.0, 1.0, {1.0}, {2.9 - 1.0}); + } + { + const auto o = run_case({"dust-head-reduce", + {{Step::Transact, 0x1p-55}, {Step::Transact, 2.9}, {Step::Reduce, 1.0}}}); + filled(o, 1, 0.0, 2.9, 2.9, {}, {0x1p-55, 2.9}); + filled(o, 2, 1.0, 0.0, 1.0, {0x1p-55, 1.0}, {2.9 - 1.0}); + } + { + const auto o = run_case({"dust-head-open", {{Step::Transact, 0x1p-55}, {Step::Transact, 200.0}}}); + filled(o, 1, 0.0, 200.0, 200.0, {}, {0x1p-55, 200.0}); + } + { + const auto o = run_case({"dust-head-cross", + {{Step::Transact, 0x1p-55}, {Step::Transact, 2.9}, {Step::Transact, -10.0}}}); + filled(o, 2, 0x1p-55 + 2.9, -(10.0 - (0x1p-55 + 2.9)), 10.0, {0x1p-55, 2.9}, + {-(10.0 - (0x1p-55 + 2.9))}); + } + { + const double u = 1.0 + 5e-13; + const auto o = run_case({"trailing-dust", + {{Step::Transact, 1.0}, {Step::Transact, 0x1p-40}, {Step::Reduce, u}}}); + filled(o, 2, u, 0.0, u, {1.0, 0x1p-40}, {}); + } + // A rest of at most t before a lot larger than t: that lot is not touched. + { + const double u = 1.0 + 5e-13; + const auto o = run_case({"boundary-before-large-lot", + {{Step::Transact, 1.0}, {Step::Transact, 3.0}, {Step::Reduce, u}}}); + filled(o, 2, u, 0.0, u, {1.0}, {3.0}); + } + { + const double u = 1.0 + 1e-12; + const auto o = run_case({"overshoot", {{Step::Transact, 1.0}, {Step::Transact, -u}}}); + filled(o, 1, u, -0.0, u, {1.0}, {}); + } + // The same overshoot spelled Reduce: charged its request, not the held + // total an exact oversized close is charged. + { + const double u = 1.0 + 1e-12; + const auto o = run_case({"overshoot-reduce", {{Step::Transact, 1.0}, {Step::Reduce, u}}}); + filled(o, 1, u, 0.0, u, {1.0}, {}); + } + // Exact: the overshoot opens its 1e-12 on the other side. + { + const double u = 1.0 + 1e-12; + Case c{"overshoot-exact-control", {{Step::Transact, 1.0}, {Step::Transact, -u}}, std::nullopt}; + const auto o = run_case(c); + filled(o, 1, 1.0, -(u - 1.0), u, {1.0}, {-(u - 1.0)}); + } + { + const double u = 1.0 - 1e-12; + const auto o = run_case({"one-lot", {{Step::Transact, 1.0}, {Step::Reduce, u}}}); + filled(o, 1, u, 0.0, u, {1.0}, {}); + } + // Far from any boundary and exactly at one: what the exact walk books. + { + const auto o = run_case({"exact-control-partial", + {{Step::Transact, 1.0}, {Step::Transact, 3.0}, {Step::Reduce, 2.0}}}); + filled(o, 2, 2.0, 0.0, 2.0, {1.0, 1.0}, {2.0}); + } + { + const auto o = run_case({"exact-control-boundary", + {{Step::Transact, 1.0}, {Step::Transact, 3.0}, {Step::Reduce, 1.0}}}); + filled(o, 2, 1.0, 0.0, 1.0, {1.0}, {3.0}); + } + // Zero is not a boundary: 2^-54 of a 2.9 lot cannot move the position. + { + const auto o = run_case({"still-refused-dust-request", + {{Step::Transact, 2.9}, {Step::Reduce, 0x1p-54}}}); + refused(o, 1, {2.9}); + } + // A rest of 1 is far above t, and the 2^60 lot absorbs it. + { + const double big = std::ldexp(1.0, 60); + const auto o = run_case({"still-refused-huge-lot", + {{Step::Transact, 1024.0}, {Step::Transact, big}, {Step::Reduce, 1025.0}}}); + refused(o, 2, {1024.0, big}); + } +} + +void spec_cases() { + scenario = "spec"; + auto spec = base_spec("k-ulp4-digest"); + CHECK(native_run_spec_digest(spec) == kDigestBefore); + spec.quantity_grid = 0.1; + CHECK(native_run_spec_digest(spec) == kDigestGridBefore); + spec.quantity_grid.reset(); + spec.quantity_tolerance = 1e-10; + const auto with_t = native_run_spec_digest(spec); + CHECK(with_t != kDigestBefore); + spec.quantity_tolerance = 1e-9; + CHECK(native_run_spec_digest(spec) != with_t); + CHECK(native_run_spec_digest(spec) != kDigestBefore); + for (double bad : {0.0, -0.0, -1e-10, std::numeric_limits::quiet_NaN(), + std::numeric_limits::infinity()}) { + spec.quantity_tolerance = bad; + const auto v = validate_native_run_spec(spec); + CHECK(v.error == NativeRunSpecError::NotFinitePositive); + CHECK(v.field == NativeRunSpecField::QuantityTolerance); + } + spec.quantity_tolerance = 1e-10; + CHECK(validate_native_run_spec(spec).ok()); +} + +// ---- The C spelling ----------------------------------------------------------- + +struct CHost { + const Case* c = nullptr; + pf_strategy_t host = nullptr; + int bar = 0; + std::vector incarnations; +}; + +int c_on_run_begin(void* user) { + static_cast(user)->bar = 0; + return 0; +} + +int c_on_bar(void* user, const pf_bar_t*, const pf_native_decision_v1*) { + auto& s = *static_cast(user); + const int b = s.bar++; + if (b % 2 != 0) return 0; + const std::size_t k = static_cast(b / 2); + if (k >= s.c->steps.size()) return 0; + const Step& step = s.c->steps[k]; + pf_native_request_v1 r; + std::memset(&r, 0, sizeof(r)); + r.struct_size = static_cast(sizeof(r)); + r.version = PF_NATIVE_API_VERSION; + r.trigger = PF_NATIVE_TRIGGER_MARKET; + r.label = "step"; + r.comment = ""; + switch (step.kind) { + case Step::Transact: r.intent = PF_NATIVE_INTENT_TRANSACT; r.intent_value = step.value; break; + case Step::Reduce: + r.intent = PF_NATIVE_INTENT_REDUCE; + r.reduce_size = PF_NATIVE_REDUCE_EXPLICIT_UNITS; + r.intent_value = step.value; + break; + case Step::Flatten: r.intent = PF_NATIVE_INTENT_FLATTEN; break; + } + std::uint64_t inc = 0; + std::uint32_t reject = 0; + s.incarnations.push_back( + strategy_native_submit_v1(s.host, &r, &inc, &reject) == PF_NATIVE_OK ? inc : 0); + return 0; +} + +pf_native_run_spec_v1 c_spec(const char* key) { + pf_native_run_spec_v1 spec; + std::memset(&spec, 0, sizeof(spec)); + spec.struct_size = static_cast(sizeof(spec)); + spec.session_key = key; + spec.run_number = 1; + spec.input_tf = "1"; + spec.script_tf = "1"; + spec.ticker = "TOLERANCE"; + spec.tickerid = "K-ULP4:TOLERANCE"; + spec.type = "crypto"; + spec.currency = "USD"; + spec.basecurrency = ""; + spec.description = ""; + spec.volumetype = ""; + spec.timezone = "UTC"; + spec.session = "24x7"; + spec.chart_timezone = ""; + spec.initial_capital = 1e9; + spec.point_value = 1.0; + spec.account_fx = 1.0; + spec.price_tick = 0.01; + spec.fee_kind = PF_NATIVE_FEE_CASH_PER_EXECUTION; + spec.fee_value = 0.0; + spec.allowed_open_directions = 3; + return spec; +} + +pf_native_run_spec_ext_v1 c_ext(std::uint32_t size, bool with_tolerance, double tolerance) { + pf_native_run_spec_ext_v1 ext; + std::memset(&ext, 0, sizeof(ext)); + ext.struct_size = size; + ext.version = PF_NATIVE_API_VERSION; + ext.present_mask = PF_NATIVE_SPEC_EXT_EVENT_RETENTION; + ext.event_retention = PF_NATIVE_EVENT_RETENTION_FULL; + if (with_tolerance) { + ext.present_mask |= PF_NATIVE_SPEC_EXT_QUANTITY_TOLERANCE; + ext.quantity_tolerance = tolerance; + } + return ext; +} + +void c_cases() { + scenario = "c-host"; + const double s02 = kSurvivor; + const Case c{"c-host-snap-down", + {{Step::Transact, 1.5}, {Step::Reduce, 1.3}, {Step::Transact, 2.9}, + {Step::Reduce, 0.2}}}; + CHost h; + h.c = &c; + pf_native_callbacks_v1 table; + std::memset(&table, 0, sizeof(table)); + table.struct_size = static_cast(sizeof(table)); + table.version = PF_NATIVE_API_VERSION; + table.user = &h; + table.on_run_begin = c_on_run_begin; + table.on_bar = c_on_bar; + h.host = strategy_native_host_create_v1(&table); + CHECK(h.host != nullptr); + if (!h.host) return; + const auto spec = c_spec("k-ulp4-tol-c"); + // The layout, then the value. + CHECK(PF_NATIVE_RUN_SPEC_EXT_V1_RETENTION_SIZE + == PF_NATIVE_RUN_SPEC_EXT_V1_AUXILIARY_SIZE + 2u * sizeof(std::uint32_t)); + CHECK(sizeof(pf_native_run_spec_ext_v1) == PF_NATIVE_RUN_SPEC_EXT_V1_RETENTION_SIZE + sizeof(double)); + CHECK(offsetof(pf_native_run_spec_ext_v1, quantity_tolerance) + == PF_NATIVE_RUN_SPEC_EXT_V1_RETENTION_SIZE); + auto older = c_ext(PF_NATIVE_RUN_SPEC_EXT_V1_RETENTION_SIZE, true, kTolerance); + CHECK(strategy_configure_native_ext_v1(h.host, &spec, &older) == PF_NATIVE_E_STRUCT); + std::uint32_t error = 0, field = 0; + auto bad = c_ext(static_cast(sizeof(pf_native_run_spec_ext_v1)), true, -1.0); + CHECK(strategy_configure_native_ext_result_v1(h.host, &spec, &bad, &error, &field) + != PF_NATIVE_OK); + CHECK(error == PF_NATIVE_SPEC_ERROR_NOT_FINITE_POSITIVE); + CHECK(field == PF_NATIVE_SPEC_FIELD_QUANTITY_TOLERANCE); + auto good = c_ext(static_cast(sizeof(pf_native_run_spec_ext_v1)), true, kTolerance); + CHECK(strategy_configure_native_ext_v1(h.host, &spec, &good) == PF_NATIVE_OK); + const int n = static_cast(2 * c.steps.size() + 3); + std::vector bars(static_cast(n)); + for (int i = 0; i < n; ++i) { + auto& bar = bars[static_cast(i)]; + bar.open = bar.high = bar.low = bar.close = kPrice; + bar.volume = 10.0; + bar.timestamp = 60'000LL * (i + 1); + } + CHECK(strategy_native_run_v1(h.host, bars.data(), n, nullptr) == PF_NATIVE_OK); + pf_native_state_v1 state; + std::memset(&state, 0, sizeof(state)); + state.struct_size = static_cast(sizeof(state)); + state.version = PF_NATIVE_API_VERSION; + CHECK(strategy_native_state_v1(h.host, &state) == PF_NATIVE_OK); + CHECK(state.lifecycle == PF_NATIVE_LIFECYCLE_COMPLETED); + std::vector rows(4096); + for (auto& row : rows) { + std::memset(&row, 0, sizeof(row)); + row.struct_size = static_cast(sizeof(row)); + row.version = PF_NATIVE_API_VERSION; + } + const int got = strategy_native_events_v1(h.host, 0, rows.data(), static_cast(rows.size())); + int applied = 0; + double closed = 0.0; + for (int i = 0; i < got; ++i) { + const auto& row = rows[static_cast(i)]; + if (h.incarnations.size() == 4 && row.incarnation == h.incarnations[3]) { + if (row.kind == PF_NATIVE_EVENT_APPLIED) { + ++applied; + closed = row.closed_units; + } + CHECK(row.kind != PF_NATIVE_EVENT_MATCH_REJECTED); + } + } + CHECK(applied == 1); + CHECK(closed == 0.2); + CHECK(strategy_native_open_lot_count_v1(h.host, std::numeric_limits::quiet_NaN()) == 1); + pf_native_open_lot_v1 lot; + std::memset(&lot, 0, sizeof(lot)); + lot.struct_size = static_cast(sizeof(lot)); + lot.version = PF_NATIVE_API_VERSION; + CHECK(strategy_native_open_lot_get_v1(h.host, 0, &lot) == PF_NATIVE_OK); + CHECK(lot.signed_units == 2.9); + std::printf("c-host-snap-down: closed=%a, the book holds %a (s02 = %a closed whole)\n", closed, + lot.signed_units, s02); + strategy_native_host_free(h.host); +} + +} // namespace + +int main() { + settlement_cases(); + spec_cases(); + c_cases(); + std::printf("test_native_quantity_tolerance: %d checks, %d failures\n", checks, failures); + return failures == 0 ? 0 : 1; +} diff --git a/tests/test_native_unrepresentable_refusal.cpp b/tests/test_native_unrepresentable_refusal.cpp new file mode 100644 index 00000000..97854eb4 --- /dev/null +++ b/tests/test_native_unrepresentable_refusal.cpp @@ -0,0 +1,733 @@ +// R5 lane K-ULP4 (audit X1): no ordinary quantity request stops a run. +// +// A request the settlement cannot book exactly on the book it meets -- the +// execution::Status::UnrepresentableQuantity of next_close_split, of +// order_action::plan and of an opening the surviving book absorbs +// (src/engine_execution.cpp) -- ends THAT request with a typed +// MatchRejected(MatchRejectReason::UnrepresentableQuantity), C word +// PF_NATIVE_MATCH_REJECT_UNREPRESENTABLE_QUANTITY; nothing moves and the run +// goes on (NativeExecutionConsumer::consume_matched_request). Until this lane +// the consumer turned the same inspection into a durable SettlementFailure: +// lifecycle Failed, code 6, discriminator 5, "native settlement inspection +// failed", on ordinary decimal quantities (AUDIT4-opus kulp NF1 / NF2). +// +// On a quantity grid, the book's own quantities are on the grid: a fraction +// whose product is its scope resolves to the scope, unfloored, and a close of +// exactly one lot's binary64 size is admitted (NF3 / NF4). +// +// survivor open 1.5, Reduce 1.3, open 2.9: the book is +// {fl(1.5 - 1.3), 2.9}; Reduce 0.2 needs 2^-54 of the +// 2.9 lot, below half its ulp +// survivor-transact the same close spelled Transact{-0.2} +// plan Reduce 2^-54 of a 2.9 lot: the position cannot move +// stuck a host-made 2^-55 lot at the head; Reduce 1.0 cannot +// take the dust lot off its rest +// absorbed-opening a 2^-55 lot, then open 200 on its side +// absorbed-cross a 2^-55 lot and 2.9, then Transact{-10} +// current the survivor close through execute_current: a +// MatchRejectedEvent, no throw; the preview answers +// UnrepresentableQuantity first +// c-host-* survivor and absorbed-opening through +// strategy_native_submit_v1, read back as MATCH_REJECTED +// rows of strategy_native_events_v1 +// grid-fraction-* Reduce{ScopeFraction{1}} of books that settlement +// moved off a 0.1 grid: 0.09999999999999859 (found +// nothing to close), 0.19999999999999857 (closed one +// step), {0.10000000000000142, 0.2} (left 2^-50 dust); +// each now closes the book whole +// grid-own-lot Reduce and Transact of a lot's own off-grid size: they +// were OffGrid at submit; an off-grid quantity that is no +// lot's size is still OffGrid +// grid-after-dust the AUDIT4 F5b/c/d continuations: open, an oversized +// Reduce and a reversal after the whole-scope close +// controls the lot's own size and the host's total, which close +// Every C++ case runs on the request core's staged and direct paths, which +// must agree bit for bit, hashes included, and every run completes. +// +// Fail-before: compiled against 91d65ad6 the TU stops at its first use of +// MatchRejectReason::UnrepresentableQuantity (no such enumerator). With that +// name spelled 10 it runs there and fails 84 of 292 checks: every refusal case +// stops its run with code 6, discriminator 5, and every grid case keeps its +// residual, its dust lot or its OffGrid refusal; only the controls pass. +// Source-free: the kernel-only profile registers the row. +#include "../src/engine_internal.hpp" +#include "../src/native_execution_consumer.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace pineforge; +namespace no = pineforge::native_order; +namespace ex = pineforge::execution; + +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) + +constexpr double kPrice = 100.0; +constexpr unsigned kUnrepresentable = + static_cast(no::MatchRejectReason::UnrepresentableQuantity); + +const double kSurvivor = 1.5 - 1.3; // 0x1.9999999999998p-3 + +struct Step { + enum Kind { Transact, Reduce, Fraction, Flatten } kind = Transact; + double value = 0.0; +}; + +struct Case { + const char* name; + std::vector steps; + std::optional grid; + bool current = false; // the last step through execute_current +}; + +// What one step came to: its submit answer, its terminal outcome, its fills +// and rows, and the book two bars later. +struct StepOutcome { + int submit = -1; // -1 accepted, else the RequestRejectReason + int terminal = 0; // 0 none, 1 applied, 2 MatchRejected, 3 NoEffect + unsigned reason = 0; // the MatchRejectReason + std::vector closed, opened, filled; + std::vector rows; + std::vector book; // signed lot units after the step + bool operator==(const StepOutcome& o) const { + return submit == o.submit && terminal == o.terminal && reason == o.reason + && closed == o.closed && opened == o.opened && filled == o.filled + && rows == o.rows && book == o.book; + } +}; + +struct Outcome { + bool completed = false; + unsigned code = 0, discriminator = 0; + std::vector steps; + std::uint64_t continuation = 0, broker = 0; + // execute_current's answer for a `current` case, and the preview before it. + int current_kind = -1; // index of the NativeCurrentExecutionResult alternative + unsigned current_reason = 0; + std::optional preview_readiness; + bool current_threw = false; + bool operator==(const Outcome& o) const { + return completed == o.completed && code == o.code && discriminator == o.discriminator + && steps == o.steps && continuation == o.continuation && broker == o.broker + && current_kind == o.current_kind && current_reason == o.current_reason + && preview_readiness == o.preview_readiness && current_threw == o.current_threw; + } +}; + +NativeRunSpec spec_for(const Case& c) { + NativeRunSpec s; + s.event_retention = NativeEventRetention::Full; + s.identity = {std::string("k-ulp4-") + c.name, 1}; + s.input_tf = "1"; + s.script_tf = "1"; + s.tickerid = "K-ULP4:REFUSAL"; + 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; + if (c.grid) s.quantity_grid = *c.grid; + return s; +} + +no::Request request_of(const Step& s) { + no::Request r; + switch (s.kind) { + case Step::Transact: r.intent = no::Transact{s.value}; break; + case Step::Reduce: r.intent = no::Reduce{no::ExplicitUnits{s.value}}; break; + case Step::Fraction: r.intent = no::Reduce{no::ScopeFraction{s.value}}; break; + case Step::Flatten: r.intent = no::Flatten{}; break; + } + r.label = "step"; + return r; +} + +// Places step k at bar 2k and reads the book at bar 2k + 2. +class Host final : public NativeStrategyHost { +public: + Host(const Case& c, bool direct) : case_(c) { + 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_++; + if (b % 2 != 0) return; + const std::size_t k = static_cast(b / 2); + if (k > 0 && k - 1 < steps.size()) steps[k - 1].book = book(); + if (k >= case_.steps.size()) return; + StepOutcome out; + const auto res = submit(request_of(case_.steps[k])); + if (res.status != no::SubmitStatus::Accepted || !res.handle) { + out.submit = res.reason ? static_cast(*res.reason) : -2; + incarnations.push_back(0); + } else { + incarnations.push_back(res.handle->incarnation); + if (case_.current && k + 1 == case_.steps.size()) { + const NativeCurrentExecution command{*res.handle}; + preview = inspect_current_execution(command).settlement_readiness; + try { + const auto result = execute_current(command); + current_kind = static_cast(result.index()); + if (const auto* r = std::get_if(&result)) { + current_reason = static_cast(r->reason); + } + } catch (const std::exception&) { + current_threw = true; + } + } + } + steps.push_back(out); + } + std::vector book() const { + std::vector out; + for (const auto& lot : native_open_lots(std::numeric_limits::quiet_NaN())) { + out.push_back(lot.signed_units); + } + return out; + } + std::vector row_units(std::size_t first, std::size_t n) const { + std::vector out; + for (std::size_t i = first; i < first + n && i < trades_.size(); ++i) { + out.push_back(trades_[i].qty); + } + return out; + } + + std::vector steps; + std::vector incarnations; + std::optional preview; + int current_kind = -1; + unsigned current_reason = 0; + bool current_threw = false; + +private: + Case case_; + int bar_ = 0; +}; + +Outcome run_cpp(const Case& c, bool direct) { + auto host = std::make_unique(c, direct); + Outcome out; + CHECK(host->configure_native(spec_for(c)).status == NativeSetupStatus::Applied); + std::vector bars; + const int n = static_cast(2 * c.steps.size() + 3); + for (int i = 0; i < n; ++i) { + bars.push_back(Bar{kPrice, kPrice, kPrice, kPrice, 10.0, 60'000LL * (i + 1)}); + } + host->run(bars.data(), n); + const auto state = host->native_state(); + out.completed = state.kind == NativeLifecycleKind::Completed; + out.code = static_cast(state.failure.code); + out.discriminator = static_cast(state.failure.discriminator); + out.steps = host->steps; + for (std::size_t k = 0; k < out.steps.size(); ++k) { + auto& step = out.steps[k]; + const std::uint64_t inc = host->incarnations[k]; + if (inc == 0) continue; + for (const auto& row : host->native_events(0)) { + if (!row.command) continue; + if (const auto* a = std::get_if(&*row.command)) { + if (a->handle().incarnation != inc) continue; + step.closed.push_back(a->closed_units); + step.opened.push_back(a->opened_units); + step.filled.push_back(a->filled_working); + const auto rows = host->row_units(a->first_trade_index, a->closed_trade_count); + step.rows.insert(step.rows.end(), rows.begin(), rows.end()); + if (a->terminal) step.terminal = 1; + } else if (const auto* r = std::get_if(&*row.command)) { + if (r->handle().incarnation != inc) continue; + step.terminal = 2; + step.reason = static_cast(r->reason); + } else if (const auto* e = std::get_if(&*row.command)) { + if (e->handle().incarnation == inc) step.terminal = 3; + } + } + } + if (!out.steps.empty() && out.steps.back().book.empty() && !host->book().empty()) { + out.steps.back().book = host->book(); + } + out.continuation = host->native_continuation_hash(); + out.broker = host->broker_state_hash(); + out.preview_readiness = host->preview; + out.current_kind = host->current_kind; + out.current_reason = host->current_reason; + out.current_threw = host->current_threw; + return out; +} + +void print_outcome(const char* path, const Outcome& o) { + std::printf(" %-6s completed=%d code=%u discriminator=%u\n", path, o.completed ? 1 : 0, + o.code, o.discriminator); + for (std::size_t k = 0; k < o.steps.size(); ++k) { + const auto& s = o.steps[k]; + std::printf(" step %zu submit=%d terminal=%d reason=%u fills=%zu", k, s.submit, + s.terminal, s.reason, s.filled.size()); + for (std::size_t i = 0; i < s.filled.size(); ++i) { + std::printf(" [closed=%a opened=%a filled=%a]", s.closed[i], s.opened[i], s.filled[i]); + } + std::printf(" book:"); + for (double lot : s.book) std::printf(" %a", lot); + std::printf("\n"); + } +} + +// Runs both twins, prints them, requires them equal and complete. +Outcome run_case(const Case& c) { + scenario = c.name; + const Outcome staged = run_cpp(c, false); + const Outcome direct = run_cpp(c, true); + std::printf("%s\n", c.name); + print_outcome("staged", staged); + print_outcome("direct", direct); + CHECK(staged == direct); + CHECK(staged.completed); + CHECK(staged.code == 0 && staged.discriminator == 0); + CHECK(staged.steps.size() == c.steps.size()); + return staged; +} + +// Step k was refused with the typed reason, moved nothing, booked no fill. +void refused(const Outcome& o, std::size_t k, const std::vector& book) { + CHECK(k < o.steps.size()); + if (k >= o.steps.size()) return; + const auto& s = o.steps[k]; + CHECK(s.submit == -1); + CHECK(s.terminal == 2); + CHECK(s.reason == kUnrepresentable); + CHECK(s.filled.empty() && s.rows.empty()); + CHECK(s.book == book); +} + +// Step k filled once, terminally, and left `book`. +void filled(const Outcome& o, std::size_t k, double closed, double opened, double units, + const std::vector& book) { + CHECK(k < o.steps.size()); + if (k >= o.steps.size()) return; + const auto& s = o.steps[k]; + CHECK(s.submit == -1); + CHECK(s.terminal == 1); + CHECK(s.filled.size() == 1); + if (s.filled.size() == 1) { + CHECK(s.closed[0] == closed); + CHECK(s.opened[0] == opened); + CHECK(s.filled[0] == units); + } + CHECK(s.book == book); +} + +// ---- A C host ------------------------------------------------------------------ + +struct CHost { + const Case* c = nullptr; + pf_strategy_t host = nullptr; + int bar = 0; + std::vector incarnations; + std::vector submits; + std::vector> books; +}; + +std::vector c_book(pf_strategy_t h) { + std::vector out; + const int n = strategy_native_open_lot_count_v1(h, std::numeric_limits::quiet_NaN()); + for (int i = 0; i < n; ++i) { + pf_native_open_lot_v1 row; + std::memset(&row, 0, sizeof(row)); + row.struct_size = static_cast(sizeof(row)); + row.version = PF_NATIVE_API_VERSION; + if (strategy_native_open_lot_get_v1(h, i, &row) == PF_NATIVE_OK) { + out.push_back(row.signed_units); + } + } + return out; +} + +int c_on_run_begin(void* user) { + static_cast(user)->bar = 0; + return 0; +} + +int c_on_bar(void* user, const pf_bar_t*, const pf_native_decision_v1*) { + auto& s = *static_cast(user); + const int b = s.bar++; + if (b % 2 != 0) return 0; + const std::size_t k = static_cast(b / 2); + if (k > 0 && s.books.size() == k - 1) s.books.push_back(c_book(s.host)); + if (k >= s.c->steps.size()) return 0; + const Step& step = s.c->steps[k]; + pf_native_request_v1 r; + std::memset(&r, 0, sizeof(r)); + r.struct_size = static_cast(sizeof(r)); + r.version = PF_NATIVE_API_VERSION; + r.trigger = PF_NATIVE_TRIGGER_MARKET; + r.label = "step"; + r.comment = ""; + switch (step.kind) { + case Step::Transact: r.intent = PF_NATIVE_INTENT_TRANSACT; r.intent_value = step.value; break; + case Step::Reduce: + r.intent = PF_NATIVE_INTENT_REDUCE; + r.reduce_size = PF_NATIVE_REDUCE_EXPLICIT_UNITS; + r.intent_value = step.value; + break; + case Step::Fraction: + r.intent = PF_NATIVE_INTENT_REDUCE; + r.reduce_size = PF_NATIVE_REDUCE_SCOPE_FRACTION; + r.intent_value = step.value; + break; + case Step::Flatten: r.intent = PF_NATIVE_INTENT_FLATTEN; break; + } + std::uint64_t inc = 0; + std::uint32_t reject = 0; + const int rc = strategy_native_submit_v1(s.host, &r, &inc, &reject); + s.incarnations.push_back(rc == PF_NATIVE_OK ? inc : 0); + s.submits.push_back(rc == PF_NATIVE_OK ? -1 : static_cast(reject)); + return 0; +} + +// The C host's terminal outcome per step, read back from the event record. +struct CStep { + int submit = -1; + int terminal = 0; + unsigned reason = 0; + std::vector filled; + std::vector book; +}; + +struct COutcome { + int rc = 0; + std::uint32_t lifecycle = 0, code = 0; + std::vector steps; +}; + +COutcome run_c(const Case& c) { + CHost h; + h.c = &c; + pf_native_callbacks_v1 table; + std::memset(&table, 0, sizeof(table)); + table.struct_size = static_cast(sizeof(table)); + table.version = PF_NATIVE_API_VERSION; + table.user = &h; + table.on_run_begin = c_on_run_begin; + table.on_bar = c_on_bar; + h.host = strategy_native_host_create_v1(&table); + CHECK(h.host != nullptr); + COutcome out; + if (!h.host) return out; + const std::string key = std::string("k-ulp4-c-") + c.name; + pf_native_run_spec_v1 spec; + std::memset(&spec, 0, sizeof(spec)); + spec.struct_size = static_cast(sizeof(spec)); + spec.session_key = key.c_str(); + spec.run_number = 1; + spec.input_tf = "1"; + spec.script_tf = "1"; + spec.ticker = "REFUSAL"; + spec.tickerid = "K-ULP4:REFUSAL"; + spec.type = "crypto"; + spec.currency = "USD"; + spec.basecurrency = ""; + spec.description = ""; + spec.volumetype = ""; + spec.timezone = "UTC"; + spec.session = "24x7"; + spec.chart_timezone = ""; + spec.initial_capital = 1e9; + spec.point_value = 1.0; + spec.account_fx = 1.0; + spec.price_tick = 0.01; + spec.fee_kind = PF_NATIVE_FEE_CASH_PER_EXECUTION; + spec.fee_value = 0.0; + spec.allowed_open_directions = 3; + if (c.grid) { + spec.optional_mask |= PF_NATIVE_SPEC_OPTIONAL_QUANTITY_GRID; + spec.quantity_grid = *c.grid; + } + CHECK(strategy_configure_native_v1(h.host, &spec) == 0); + const int n = static_cast(2 * c.steps.size() + 3); + std::vector bars(static_cast(n)); + for (int i = 0; i < n; ++i) { + auto& bar = bars[static_cast(i)]; + bar.open = bar.high = bar.low = bar.close = kPrice; + bar.volume = 10.0; + bar.timestamp = 60'000LL * (i + 1); + } + out.rc = strategy_native_run_v1(h.host, bars.data(), n, nullptr); + pf_native_state_v1 state; + std::memset(&state, 0, sizeof(state)); + state.struct_size = static_cast(sizeof(state)); + state.version = PF_NATIVE_API_VERSION; + CHECK(strategy_native_state_v1(h.host, &state) == PF_NATIVE_OK); + out.lifecycle = state.lifecycle; + out.code = state.failure_code; + std::vector rows(4096); + for (auto& row : rows) { + std::memset(&row, 0, sizeof(row)); + row.struct_size = static_cast(sizeof(row)); + row.version = PF_NATIVE_API_VERSION; + } + const int got = strategy_native_events_v1(h.host, 0, rows.data(), static_cast(rows.size())); + CHECK(got >= 0); + for (std::size_t k = 0; k < h.incarnations.size(); ++k) { + CStep step; + step.submit = h.submits[k]; + if (k < h.books.size()) step.book = h.books[k]; + for (int i = 0; i < got; ++i) { + const auto& row = rows[static_cast(i)]; + if (h.incarnations[k] == 0 || row.incarnation != h.incarnations[k]) continue; + if (row.kind == PF_NATIVE_EVENT_APPLIED) { + step.filled.push_back(row.closed_units); + if (row.terminal) step.terminal = 1; + } else if (row.kind == PF_NATIVE_EVENT_MATCH_REJECTED) { + step.terminal = 2; + step.reason = row.reason; + } else if (row.kind == PF_NATIVE_EVENT_NO_EFFECT) { + step.terminal = 3; + } + } + out.steps.push_back(step); + } + if (!out.steps.empty() && out.steps.back().book.empty()) out.steps.back().book = c_book(h.host); + strategy_native_host_free(h.host); + return out; +} + +// The C host's run completes and its step k is the typed MATCH_REJECTED row. +void c_refused(const Case& c, std::size_t k, const std::vector& book) { + scenario = c.name; + const COutcome o = run_c(c); + std::printf("%s: rc=%d lifecycle=%u code=%u\n", c.name, o.rc, o.lifecycle, o.code); + CHECK(o.rc == PF_NATIVE_OK); + CHECK(o.lifecycle == PF_NATIVE_LIFECYCLE_COMPLETED); + CHECK(o.code == 0); + CHECK(k < o.steps.size()); + if (k >= o.steps.size()) return; + const auto& s = o.steps[k]; + std::printf(" step %zu submit=%d terminal=%d reason=%u fills=%zu\n", k, s.submit, s.terminal, + s.reason, s.filled.size()); + CHECK(s.submit == -1); + CHECK(s.terminal == 2); + CHECK(s.reason == PF_NATIVE_MATCH_REJECT_UNREPRESENTABLE_QUANTITY); + CHECK(s.reason == kUnrepresentable); + CHECK(s.filled.empty()); + CHECK(s.book == book); +} + +// ---- Cases ----------------------------------------------------------------------- + +void refusal_cases() { + const double s02 = kSurvivor; + const std::vector survivor_book = { + {Step::Transact, 1.5}, {Step::Reduce, 1.3}, {Step::Transact, 2.9}}; + auto with = [&](std::vector steps, std::vector more) { + steps.insert(steps.end(), more.begin(), more.end()); + return steps; + }; + + // Reduce 0.2: the rest 2^-54 is below half an ulp of the 2.9 lot. + { + const auto o = run_case({"survivor", with(survivor_book, + {{Step::Reduce, 0.2}, {Step::Flatten, 0.0}})}); + CHECK(0.2 - s02 == 0x1p-54); + refused(o, 3, {s02, 2.9}); + // The run went on: the next command settles. + filled(o, 4, s02 + 2.9, 0.0, s02 + 2.9, {}); + } + { + const auto o = run_case({"survivor-transact", with(survivor_book, {{Step::Transact, -0.2}})}); + refused(o, 3, {s02, 2.9}); + } + // The position cannot move by 2^-54. + { + const auto o = run_case({"plan", {{Step::Transact, 2.9}, {Step::Reduce, 0x1p-54}}}); + refused(o, 1, {2.9}); + } + // A host-made dust lot at the head: the whole-lot close of 2^-55 cannot + // decrement the rest 1.0. + { + const auto o = run_case({"stuck", with(survivor_book, + {{Step::Reduce, std::nextafter(s02, 0.0)}, {Step::Reduce, 1.0}, {Step::Flatten, 0.0}})}); + filled(o, 3, std::nextafter(s02, 0.0), 0.0, std::nextafter(s02, 0.0), {0x1p-55, 2.9}); + refused(o, 4, {0x1p-55, 2.9}); + filled(o, 5, 0x1p-55 + 2.9, 0.0, 0x1p-55 + 2.9, {}); + } + // The dust lot's side absorbs 200 into the book's total: fl(2^-55 + 200) is + // 200, so the survivor would vanish from it. + { + const auto o = run_case({"absorbed-opening", + {{Step::Transact, 0x1p-55}, {Step::Transact, 200.0}, {Step::Transact, 2.0}}}); + refused(o, 1, {0x1p-55}); + // fl(2^-55 + 2) is 2 too: every opening beside the dust lot is refused; + // the run is not. + refused(o, 2, {0x1p-55}); + } + { + const auto o = run_case({"absorbed-cross", + {{Step::Transact, 0x1p-55}, {Step::Transact, -10.0}, {Step::Flatten, 0.0}}}); + // A crossing whose closing leg is the dust lot and whose opening the + // rest absorbs -- fl(10 - 2^-55) is 10, the lot does not move it. + refused(o, 1, {0x1p-55}); + filled(o, 2, 0x1p-55, 0.0, 0x1p-55, {}); + } + // execute_current answers the refusal as a MatchRejectedEvent (alternative 3 + // of NativeCurrentExecutionResult) and does not throw; the preview shows the + // readiness first. + { + Case c{"current", with(survivor_book, {{Step::Reduce, 0.2}}), std::nullopt, true}; + const auto o = run_case(c); + CHECK(!o.current_threw); + CHECK(o.current_kind == 3); + CHECK(o.current_reason == kUnrepresentable); + CHECK(o.preview_readiness && *o.preview_readiness == ex::Status::UnrepresentableQuantity); + refused(o, 3, {s02, 2.9}); + } + // Controls: the lot's own size and the host's total close. + { + const auto o = run_case({"control-lot", with(survivor_book, {{Step::Reduce, s02}})}); + filled(o, 3, s02, 0.0, s02, {2.9}); + } + // 3.1 is one ulp above the book's held total: an oversized close takes + // the book whole and is charged what it held. + { + const auto o = run_case({"control-total", with(survivor_book, {{Step::Reduce, 3.1}})}); + CHECK(s02 + 2.9 == std::nextafter(3.1, 0.0)); + filled(o, 3, s02 + 2.9, 0.0, s02 + 2.9, {}); + } + c_refused({"c-host-survivor", with(survivor_book, {{Step::Reduce, 0.2}})}, 3, {s02, 2.9}); + c_refused({"c-host-absorbed-opening", {{Step::Transact, 0x1p-55}, {Step::Transact, 200.0}}}, 1, + {0x1p-55}); +} + +void grid_cases() { + const double left_a = 0.2 - (39.1 - 39.0); // 0x1.9999999999934p-4 + const double left_b = 0.3 - (39.1 - 39.0); // 0x1.9999999999966p-3 + const double left_c = 39.0 - 38.9; // 0x1.9999999999ap-4 + CHECK(!no::quantity_on_grid(left_a, 0.1)); + CHECK(!no::quantity_on_grid(left_b, 0.1)); + CHECK(!no::quantity_on_grid(left_c, 0.1)); + // Reduce{ScopeFraction{1}} of a book the settlement moved off its grid + // resolves to the book's own held total and closes it whole. + { + const auto o = run_case({"grid-fraction-no-fill", + {{Step::Transact, 39.0}, {Step::Transact, 0.2}, {Step::Transact, -39.1}, + {Step::Fraction, 1.0}}, 0.1}); + filled(o, 2, 39.1, -0.0, 39.1, {left_a}); + filled(o, 3, left_a, 0.0, left_a, {}); + } + { + const auto o = run_case({"grid-fraction-step", + {{Step::Transact, 39.0}, {Step::Transact, 0.3}, {Step::Transact, -39.1}, + {Step::Fraction, 1.0}}, 0.1}); + filled(o, 3, left_b, 0.0, left_b, {}); + } + { + const auto o = run_case({"grid-fraction-dust", + {{Step::Transact, 39.0}, {Step::Transact, 0.2}, {Step::Reduce, 38.9}, + {Step::Fraction, 1.0}}, 0.1}); + filled(o, 3, left_c + 0.2, 0.0, left_c + 0.2, {}); + CHECK(o.steps.size() == 4 && o.steps[3].rows == std::vector({left_c, 0.2})); + } + // A fraction below one is still floored onto the grid. + { + const auto o = run_case({"grid-fraction-part", + {{Step::Transact, 39.0}, {Step::Transact, 0.3}, {Step::Transact, -39.1}, + {Step::Fraction, 0.75}}, 0.1}); + filled(o, 3, 0.1, 0.0, 0.1, {left_b - 0.1}); + } + // A close of exactly one lot's own size is admitted on the grid, spelled + // Reduce or Transact; any other off-grid quantity is still OffGrid. + { + const auto o = run_case({"grid-own-lot", + {{Step::Transact, 39.0}, {Step::Transact, 0.2}, {Step::Transact, -39.1}, + {Step::Reduce, left_a}}, 0.1}); + filled(o, 3, left_a, 0.0, left_a, {}); + } + { + const auto o = run_case({"grid-own-lot-transact", + {{Step::Transact, 39.0}, {Step::Transact, 0.2}, {Step::Transact, -39.1}, + {Step::Transact, -left_a}}, 0.1}); + filled(o, 3, left_a, -0.0, left_a, {}); + } + { + const auto o = run_case({"grid-off-grid-still-refused", + {{Step::Transact, 39.0}, {Step::Transact, 0.2}, {Step::Transact, -39.1}, + {Step::Reduce, std::nextafter(left_a, 0.0)}, {Step::Transact, left_a}}, 0.1}); + CHECK(o.steps.size() == 5); + if (o.steps.size() == 5) { + CHECK(o.steps[3].submit == static_cast(no::RequestRejectReason::OffGrid)); + // An opening on the lot's own side is not a close of it. + CHECK(o.steps[4].submit == static_cast(no::RequestRejectReason::OffGrid)); + } + } + // AUDIT4 F5b/c/d: after the whole-scope close nothing is left to trip on. + const std::vector dust_book = { + {Step::Transact, 39.0}, {Step::Transact, 0.2}, {Step::Reduce, 38.9}, {Step::Fraction, 1.0}}; + { + auto steps = dust_book; + steps.push_back({Step::Transact, 200.0}); + const auto o = run_case({"grid-after-dust-open", steps, 0.1}); + filled(o, 4, 0.0, 200.0, 200.0, {200.0}); + } + { + auto steps = dust_book; + steps.push_back({Step::Reduce, 100.0}); + const auto o = run_case({"grid-after-dust-reduce", steps, 0.1}); + CHECK(o.steps.size() == 5 && o.steps[4].terminal == 3); + } + { + auto steps = dust_book; + steps.push_back({Step::Transact, -100.0}); + const auto o = run_case({"grid-after-dust-cross", steps, 0.1}); + filled(o, 4, 0.0, -100.0, 100.0, {-100.0}); + } + // The C host resolves the same whole scope. + { + scenario = "c-host-grid-fraction"; + const Case c{"c-host-grid-fraction", + {{Step::Transact, 39.0}, {Step::Transact, 0.3}, {Step::Transact, -39.1}, + {Step::Fraction, 1.0}}, 0.1}; + const COutcome o = run_c(c); + CHECK(o.rc == PF_NATIVE_OK && o.lifecycle == PF_NATIVE_LIFECYCLE_COMPLETED); + CHECK(o.steps.size() == 4); + if (o.steps.size() == 4) { + CHECK(o.steps[3].terminal == 1); + CHECK(o.steps[3].filled == std::vector({left_b})); + CHECK(o.steps[3].book.empty()); + } + } +} + +} // namespace + +int main() { + refusal_cases(); + grid_cases(); + std::printf("test_native_unrepresentable_refusal: %d checks, %d failures\n", checks, failures); + return failures == 0 ? 0 : 1; +} From 2d4affc0595aa417d3e8f030b13185d7e8a75e29 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Fri, 25 Sep 2026 19:59:11 +0800 Subject: [PATCH 06/24] Docs: what a host gets from a quantity -- the typed refusal, the grid's own quantities, the quantity tolerance; the citations K-ULP4 moved re-anchored (R5 lane K-ULP4) - docs/native-settlement.md: the "refused before settlement" sentence and the K-ULP2 paragraph's refusal list now say what a host gets -- the request's terminal MatchRejected(UnrepresentableQuantity), nothing moved, the run going on (it was code 6 / discriminator 5 and a stopped run) -- with the ordinary decimal example (Reduce 0.2 after 1.5 - 1.3 then 2.9) and the dust head that refuses the next close and opening; a "what a native host gets" paragraph (K-ULP1/2/3/4, the grid's own quantities); a new "Quantity tolerance" section with the rules and what stays refused; the Pine paragraph records why the adapter keeps its own 1e-10 rule. - docs/pages/native-engine.md: quantity_tolerance in the optional spec list; the grid admission of a lot's own size; UnrepresentableQuantity among the MatchRejectedEvent reasons; a "What a host gets from a quantity" section beside K-ULP1's, linking native-settlement.md; ScopeFraction{1} not floored; the sixth pf_native_run_spec_ext_v1 length. - README.md, docs/pages/pine-to-native.md, docs/design/native-feature-parity.md (SZ7): the six ext lengths, qty_percent=100 on a grid, the grid's own quantities. - Every published citation carried from 91d65ad6 by an exact difflib line map (K-ULP4-scratch/tools/kulp4_exact_reanchor.py): 464 + 60 anchors moved, 0 failed their re-judgement; the four spans that grew contain the lines this lane inserted and still name their symbol; the ci_verify floor lines (their numbers changed) set by hand. check_doc_anchors 1678/1678, check_doc_lint 0. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 87218e628163776435c6ce45d2407934da8bfad2) --- CONTRIBUTING.md | 4 +- README.md | 5 +- docs/design/native-feature-parity.md | 184 +++++++++---------- docs/native-settlement.md | 94 +++++++++- docs/pages/contributing-llm.md | 10 +- docs/pages/native-engine.md | 163 +++++++++++------ docs/pages/pine-to-native.md | 252 +++++++++++++-------------- 7 files changed, 425 insertions(+), 287 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 04da3d8a..65bfb597 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:231 and `RELEASE_MIN_TESTS` -ci_verify.py:335. +floor — `KERNEL_MIN_TESTS` ci_verify.py:235 and `RELEASE_MIN_TESTS` +ci_verify.py:343. 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 a436b44a..e9a207ad 100644 --- a/README.md +++ b/README.md @@ -502,12 +502,13 @@ declare all fail CI. Every struct is tagged and size-prefixed (`struct_size`, `version`); an unknown size, version or enumerator is refused with a documented negative status and -mutates nothing. `pf_native_run_spec_ext_v1` has five published lengths — the +mutates nothing. `pf_native_run_spec_ext_v1` has six published lengths — the layout the lane first shipped (`PF_NATIVE_RUN_SPEC_EXT_V1_BASE_SIZE`), the same struct with L9's appended risk tail (`PF_NATIVE_RUN_SPEC_EXT_V1_RISK_SIZE`), that plus N8's intrabar / policy tail (`PF_NATIVE_RUN_SPEC_EXT_V1_POLICY_SIZE`), the auxiliary-feed tail (`PF_NATIVE_RUN_SPEC_EXT_V1_AUXILIARY_SIZE`), the event-retention -tail, and the current layout; `pf_native_callbacks_v1` +tail (`PF_NATIVE_RUN_SPEC_EXT_V1_RETENTION_SIZE`), and the current layout with +K-ULP4's quantity-tolerance tail; `pf_native_callbacks_v1` has four — the layout the lane first shipped (`PF_NATIVE_CALLBACKS_V1_BASE_SIZE`), that plus its six-hook tail (`PF_NATIVE_CALLBACKS_V1_HOOKS_SIZE`), that plus the policy-hook tail (`PF_NATIVE_CALLBACKS_V1_POLICY_SIZE`), and the current one, diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 76bc162d..c938b939 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:1824, *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:1843, *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). | @@ -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:2678-2689; `entry()` pine_adapter.cpp:5082, `order()` pine_adapter.cpp:10270) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5536-5583; `submit` native_host.hpp:1186 | 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:8133 → `Flatten{}` pine_adapter.cpp:8162-8163; `Reduce{ExplicitUnits}` pine_adapter.cpp:7280-7289; `ReverseTo` pine_adapter.cpp:6114-6115) | **yes** — `OrderIntent` native_order.hpp:229, reduce sizes native_order.hpp:229-236, `ReverseTo` native_order.hpp:60-62, `OpeningShape` native_order.hpp:894-898, `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:2648-3707 → `host.replace` pine_adapter.cpp:2664-2744) | **yes** — `replace` native_host.hpp:1192-1194, `cancel` native_host.hpp:1222; `ReplacedEvent` native_order.hpp:997-1011; result and event types `SubmitStatus` native_order.hpp:835-861, `ReplacedEvent` native_order.hpp:981-1047 | 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:4515-4561, `cancel_all` pine_adapter.cpp:17806-17821; `live_by_source_key_` pine_adapter.hpp:1983; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:3857-3922) | **partial** — single-handle `cancel` native_host.hpp:1222; no enumeration (`WorkingRequestCore::live()` is consumer-internal native_order.hpp:1645); labels exist but are unindexed (`Request::label` native_order.hpp:485) **Closed:** `cancel_all` native_host.hpp:1240, `cancel_where(comment)` native_host.hpp:1245 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1245 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1236 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. | +| OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:2678-2689; `entry()` pine_adapter.cpp:5082, `order()` pine_adapter.cpp:10270) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5595-5642; `submit` native_host.hpp:1190 | 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:8133 → `Flatten{}` pine_adapter.cpp:8162-8163; `Reduce{ExplicitUnits}` pine_adapter.cpp:7280-7289; `ReverseTo` pine_adapter.cpp:6114-6115) | **yes** — `OrderIntent` native_order.hpp:229, reduce sizes native_order.hpp:229-236, `ReverseTo` native_order.hpp:60-62, `OpeningShape` native_order.hpp:904-908, `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:2648-3707 → `host.replace` pine_adapter.cpp:2664-2744) | **yes** — `replace` native_host.hpp:1195-1197, `cancel` native_host.hpp:1226; `ReplacedEvent` native_order.hpp:1007-1021; result and event types `SubmitStatus` native_order.hpp:835-861, `ReplacedEvent` native_order.hpp:991-1057 | 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:4515-4561, `cancel_all` pine_adapter.cpp:17806-17821; `live_by_source_key_` pine_adapter.hpp:1983; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:3857-3922) | **partial** — single-handle `cancel` native_host.hpp:1226; no enumeration (`WorkingRequestCore::live()` is consumer-internal native_order.hpp:1664); labels exist but are unindexed (`Request::label` native_order.hpp:485) **Closed:** `cancel_all` native_host.hpp:1244, `cancel_where(comment)` native_host.hpp:1249 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1249 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1240 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:3982; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:6965-7316) | **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 | — | -| 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:5303-5338, `cohort_for` pine_adapter.cpp:2442; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7263-7280) | **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:1255, `CancelReason::OwnerGone` native_order.hpp:933 | 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:2687-2693) | **yes** — `Group` / `GroupEffect` native_order.hpp:466-473, `CancelReason::Group` native_order.hpp:932-937, `ReservationReducedEvent` native_order.hpp:1156-1166 (deferred form native_order.hpp:1156-1166) | 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:1077-1078 | K | — | F:A9 | (single-source row; O:F2 cites the same call) | +| 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:5303-5338, `cohort_for` pine_adapter.cpp:2442; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7263-7280) | **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:1259, `CancelReason::OwnerGone` native_order.hpp:943 | 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:2687-2693) | **yes** — `Group` / `GroupEffect` native_order.hpp:466-473, `CancelReason::Group` native_order.hpp:942-947, `ReservationReducedEvent` native_order.hpp:1166-1176 (deferred form native_order.hpp:1166-1176) | 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:1081-1082 | 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:12779; lowering `submit_one` pine_adapter.cpp:8830-8842, `BindCohort` pine_adapter.cpp:9331-9337, `submit_leg` pine_adapter.cpp:9355-9379; 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:1824); 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:8435-8438; `materialize_relative_exits` pine_adapter.cpp:17096-17137, pine_adapter.hpp:1812-1824) | **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:5462-5496. `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:992 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:18857-18886) | **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:1236 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:1985-2003); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1116-1195); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3261-3428, `materialize_snapshot` pine_adapter.cpp:10670-10694), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1580-1603, `CloseCallsiteState` pine_adapter.hpp:1674-1689; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15406, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15640) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:649-651 | 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:12779; lowering `submit_one` pine_adapter.cpp:8830-8842, `BindCohort` pine_adapter.cpp:9331-9337, `submit_leg` pine_adapter.cpp:9355-9379; 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:1843); 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:8435-8438; `materialize_relative_exits` pine_adapter.cpp:17096-17137, pine_adapter.hpp:1812-1824) | **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:5521-5555. `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:996 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:18857-18886) | **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:1240 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:1985-2003); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1116-1195); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3261-3428, `materialize_snapshot` pine_adapter.cpp:10670-10694), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1580-1603, `CloseCallsiteState` pine_adapter.hpp:1674-1689; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15406, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15640) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:656-658 | 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: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:855-887) | n/a | A | — | S:R4 O:A10 | TV half of OL9. **V19D-P1 (R5 lane B-ADAPTER):** a margin-call revival never restores an exit the script withdrew: `strategy.cancel` / `cancel_all` retire every leg of a dormant exit's lifecycle (`retire_cancelled_dormant_exits` pine_adapter.cpp:1795) and the revival skips it (`revive_brackets_after_margin` pine_adapter.cpp:1856), as the lab tv tapes badapter-v19dp1-control / -cancel (NYSE:F 15m) decide it — replayed by `tests/test_adapter_margin_revival_cancel.cpp` | ### 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:5548-5570; `NativeTrailState` native_host.hpp:657-662, `trail_state` native_host.hpp:1065 | 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:5607-5629; `NativeTrailState` native_host.hpp:657-662, `trail_state` native_host.hpp:1069 | 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:11554-11566) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4403-4408) **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:11467-11604) stays A. | -| TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:3814-3860) | **partial** — the read exists `trail_state` native_host.hpp:1065; `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:11554-11566) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4429-4434) **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:11467-11604) stays A. | +| TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:3814-3860) | **partial** — the read exists `trail_state` native_host.hpp:1069; `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:9381-9386, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11467-11604, retained best `retained_trail_best` pine_adapter.hpp:287-289, operand / restart rules `source_trail_points` pine_adapter.cpp:8405-8631, `trail_one_shot` pine_adapter.cpp:9470-9599 | n/a | A | — | F:B3 S:T2 | R5-1 | ### 1.3 Sizing bases and reservations @@ -77,15 +77,15 @@ 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:5692-5696, pine_adapter.cpp:5692) | **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:10705-11882) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:925-929, invoked native_execution_consumer.cpp:4808-4826, guarded native_execution_consumer.cpp:4808-4842 | 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:2536-2591) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4848-4850 **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:2488, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:453 (→ SZ10) | +| SZ2 | Host override of the resolved price / units (`resolve_terms` pine_adapter.cpp:10705-11882) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:929-933, invoked native_execution_consumer.cpp:4852-4871, guarded native_execution_consumer.cpp:4852-4887 | 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:2536-2591) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4894-4896 **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:2488, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:453 (→ SZ10) | | SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:2497-2499) | **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:2549, `typed_entry_units` pine_adapter.cpp:2567; `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:2528; `percent_commission_live_equity` pine_adapter.cpp:4121-4161) | **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:2454-2469, `PineSizingSnapshot` pine_adapter.hpp:147-154; fill-time exceptions `default_sized` pine_adapter.cpp:6435-6461) | **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:1375-1392; host-sized terms snap (`ExecutionGridPolicy` native_order.hpp:71-85); exact grid validation `execution_terms_grid_representable` native_execution_consumer.cpp:4863-4866 | 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:1385-1402; host-sized terms snap (`ExecutionGridPolicy` native_order.hpp:71-85); exact grid validation `execution_terms_grid_representable` native_execution_consumer.cpp:4909-4912; the book's own quantities are on the grid -- a whole-scope `ScopeFraction` and a close of one lot's size (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:12253-12281, `compute_exit_reservation` pine_adapter.cpp:4204-4325) | **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:4256-4579, `reserved_exit_qty` pine_adapter.cpp:8757-9067) | **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:444-451, `source_money_floor_lot` pine_adapter.cpp:453; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:4236-4251) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1537-1556 | A | — | O:C8 F:C3 S:M4 S:S5 | R5-1 | +| SZ10 | *(quirk)* Ten-significant-digit money rule (`source_money_round` pine_adapter.cpp:444-451, `source_money_floor_lot` pine_adapter.cpp:453; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:4236-4251) | 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:6496), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7703), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12510), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13106, pine_adapter.cpp:13159) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15454) | 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:2536), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2549, `typed_entry_units` pine_adapter.cpp:2567), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18452) | 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:6965-7316; ledger `close_batch_bar_` pine_adapter.hpp:2026-2037; 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:1074-1186) | n/a | A | — | S:O10 | (single-source) R5-4 confirms S's own split: concept → SZ9, predicate A. | @@ -94,20 +94,20 @@ 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:2302) | **yes** — `allowed_open_directions` native_run_spec.hpp:596 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2899-2907 | 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:2913-2916 | 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:13001-13004) is TV. | -| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:595 → `MaxOpenLots` native_execution_consumer.cpp:2918-2921 | 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:5970-6016, cap left unset `max_open_lots` pine_adapter.cpp:2301-2303) is TV. | -| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13055-13120 and leaves the kernel gate unset pine_adapter.cpp:13054-13056) | **yes** — `initial_margin_fraction` native_run_spec.hpp:597; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2936 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:2795 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. | +| MG1 | Allowed opening directions (`allowed_open_directions` pine_adapter.cpp:2302) | **yes** — `allowed_open_directions` native_run_spec.hpp:596 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2925-2933 | 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:2939-2942 | 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:13001-13004) is TV. | +| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:595 → `MaxOpenLots` native_execution_consumer.cpp:2944-2947 | 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:5970-6016, cap left unset `max_open_lots` pine_adapter.cpp:2301-2303) is TV. | +| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13055-13120 and leaves the kernel gate unset pine_adapter.cpp:13054-13056) | **yes** — `initial_margin_fraction` native_run_spec.hpp:597; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2962 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:2806 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. | | MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13710-13711, `margin_pct` pine_adapter.cpp:13118) | **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:13810-13988, `submit_margin_call_units` pine_adapter.cpp:13949-13995, `schedule_margin_call_path` pine_adapter.cpp:17959-18036) | **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:2895 **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:13741-13777) | **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:975 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:179-200) | **no** **Closed:** `native_liquidation_price()` native_host.hpp:1293 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:16057-16073, `apply_fx_opening_margin_slice` pine_adapter.cpp:18037-18065) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1175 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:16548-16587) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1274 **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 | +| MG6 | Maintenance margin + forced liquidation (`submit_margin_call_slice` pine_adapter.cpp:13810-13988, `submit_margin_call_units` pine_adapter.cpp:13949-13995, `schedule_margin_call_path` pine_adapter.cpp:17959-18036) | **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:2921 **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:13741-13777) | **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:979 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:179-200) | **no** **Closed:** `native_liquidation_price()` native_host.hpp:1297 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:16057-16073, `apply_fx_opening_margin_slice` pine_adapter.cpp:18037-18065) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1179 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:16548-16587) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1278 **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:13090-13093) | **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:13671-13685, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14697-14727) | **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:14740-14974) | **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:13481-13560) | **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:1296 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 | +| MG14 | Risk-day boundary (`chart_day_key` pine_adapter.cpp:13481-13560) | **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:1300 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:13699, 4× shortfall `4.0 * minimum` pine_adapter.cpp:13758, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14466-14506, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:13990-14117, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:13990) | n/a | A | — | F:D2 F:G4 O:D4 S:M4 | R5-1 | | 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:3610-3677, `record_market_review` pine_adapter.cpp:14978-15036) | n/a | A | — | S:R2 | (single-source) | @@ -115,15 +115,15 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| CT1 | Calculate at script-bar close | **yes** — `on_native_bar` native_host.hpp:874, dispatched native_execution_consumer.cpp:7052-7073 (call site `sha256:fc29ccd415732193f24f2370c6aca8d6f8910bca5b95315e63e88f2befe8200b` native_execution_consumer.cpp:7063) | K | — | F:E1 O:F1 | — | -| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2259-2260) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7372-7376 | K | — | F:E2 O:F6 S:C1 | — | -| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:15893) | **yes** — `on_native_bar_open` native_host.hpp:870 runs before the open match native_execution_consumer.cpp:6914-6918; `execute_current` legal native_host.hpp:1077 | 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:6857 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:6908 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1046 is the lookahead-free bar so far at the cursor, 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:856, `NativeInputContext` native_host.hpp:754-759 | 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:681; `begin_coof_recalc` pine_adapter.cpp:5400, `flush_coof_tail` pine_adapter.cpp:16035, `end_coof_recalc` pine_adapter.cpp:5421) | **partial** — `on_native_applied` native_host.hpp:918 fires mid-path after each fill native_execution_consumer.cpp:6768, native_execution_consumer.cpp:6747-6777; commands are legal there `commands_allowed` native_execution_consumer.cpp:1376-1384; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5462-5496, `remaining_path_delivery` native_order.cpp:2037-2045. 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:859 fires per accepted realtime print native_execution_consumer.cpp:6996-7000; 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:284-286. | -| 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:7366-7483 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:908 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:2389-2445) | **yes** — `IntrabarPath` native_run_spec.hpp:362-402, delivery native_execution_consumer.cpp:7310-7501; `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:878, dispatched native_execution_consumer.cpp:7118-7139 (call site `sha256:fc29ccd415732193f24f2370c6aca8d6f8910bca5b95315e63e88f2befe8200b` native_execution_consumer.cpp:7129) | K | — | F:E1 O:F1 | — | +| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2259-2260) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7438-7442 | K | — | F:E2 O:F6 S:C1 | — | +| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:15893) | **yes** — `on_native_bar_open` native_host.hpp:874 runs before the open match native_execution_consumer.cpp:6980-6984; `execute_current` legal native_host.hpp:1081 | 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:6923 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:6974 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1050 is the lookahead-free bar so far at the cursor, 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:860, `NativeInputContext` native_host.hpp:758-763 | 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:681; `begin_coof_recalc` pine_adapter.cpp:5400, `flush_coof_tail` pine_adapter.cpp:16035, `end_coof_recalc` pine_adapter.cpp:5421) | **partial** — `on_native_applied` native_host.hpp:922 fires mid-path after each fill native_execution_consumer.cpp:6834, native_execution_consumer.cpp:6813-6843; commands are legal there `commands_allowed` native_execution_consumer.cpp:1386-1394; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5521-5555, `remaining_path_delivery` native_order.cpp:2039-2047. 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:863 fires per accepted realtime print native_execution_consumer.cpp:7062-7066; 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:284-286. | +| 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:7432-7549 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:912 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:2389-2445) | **yes** — `IntrabarPath` native_run_spec.hpp:362-402, delivery native_execution_consumer.cpp:7376-7567; `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:233-319) 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:5547), 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:681, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1655-1662 | n/a | A | — | F:E4 O:F5 S:C2 | R5-5: the adapter keeps its path | @@ -131,38 +131,38 @@ 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:1513-1516, pumped from the scheduler `scheduler_prepare_security_sequence` pine_strategy_host.cpp:1421-1577; 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:1514 **Closed:** `NativeRunSpec::subscriptions` (`NativeTimeframeSubscription` native_run_spec.hpp:485), or `declare_timeframe_subscriptions` native_host.hpp:1109 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:866 and `native_series_bar(index)` native_host.hpp:1092 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:1407-1423 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, sole caller pine_strategy_host.cpp:1546; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1425-1430 **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:8120 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). | -| 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:908 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. | +| HT1 | HTF series of the same symbol (`request.security`): evaluators registered `scheduler_configure_security_evaluators` pine_strategy_host.cpp:1513-1516, pumped from the scheduler `scheduler_prepare_security_sequence` pine_strategy_host.cpp:1421-1577; 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:1514 **Closed:** `NativeRunSpec::subscriptions` (`NativeTimeframeSubscription` native_run_spec.hpp:485), or `declare_timeframe_subscriptions` native_host.hpp:1113 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:870 and `native_series_bar(index)` native_host.hpp:1096 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:1417-1433 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, sole caller pine_strategy_host.cpp:1546; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1435-1440 **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:8186 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). | +| 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:912 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:856; the Pine scheduler asks the kernel's `script_bucket_completions` native_execution_consumer.cpp:7725-7742, which feeds the same class `TimeframeAggregator` native_execution_consumer.cpp:7732, 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:860; the Pine scheduler asks the kernel's `script_bucket_completions` native_execution_consumer.cpp:7791-7808, which feeds the same class `TimeframeAggregator` native_execution_consumer.cpp:7798, 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:5536-5583, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:6119-6127; `Limit::fill_through` native_order.hpp:239-244 enforced native_execution_consumer.cpp:4956-4969, re-checked after host terms native_execution_consumer.cpp:4953-4973 | 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:4549-4550, clamp native_execution_consumer.cpp:4550 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:11721-11730) is A | -| FP3 | Commission: percent / cash per unit / cash per execution (`fee_kind_for` pine_adapter.cpp:463-470) | **yes** — `NativeFeeKind` native_run_spec.hpp:19-23; `quote_execution_commissions` engine_execution.cpp:1474-1510 | K | — | F:G2 O:H8 S:F1 | — | -| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:256-259, `source_bar_fill` pine_adapter.cpp:11737-11746, `directional_tick` pine_adapter.cpp:301-305) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4012; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6200-6201; 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. | +| 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:5595-5642, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:6178-6186; `Limit::fill_through` native_order.hpp:239-244 enforced native_execution_consumer.cpp:5003-5016, re-checked after host terms native_execution_consumer.cpp:5003-5023 | 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:4576-4577, clamp native_execution_consumer.cpp:4576 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:11721-11730) is A | +| FP3 | Commission: percent / cash per unit / cash per execution (`fee_kind_for` pine_adapter.cpp:463-470) | **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:256-259, `source_bar_fill` pine_adapter.cpp:11737-11746, `directional_tick` pine_adapter.cpp:301-305) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4038; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6260-6261; 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:332-380) 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:1175 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:8550). **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:1179 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:8616). **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:339-387 (used `source_trigger_threshold` pine_adapter.cpp:8822-8827, `exit_limit_trigger` pine_adapter.cpp:9356-9377), `source_bar_fill_tick` pine_adapter.cpp:277-290, raw-vs-booked spelling pine_adapter.cpp:11220-11326, margin-call fill pricing pine_adapter.cpp:13786-13795 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:11446) | 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:2578-2591, closing-row fold `max_runup` engine_orders.cpp:157-171; `owns_lot_excursions` native_host.hpp:1001-1005 | 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:2589-2602, closing-row fold `max_runup` engine_orders.cpp:157-171; `owns_lot_excursions` native_host.hpp:1005-1009 | 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). | | 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:1713) | **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:1438) stay A, measured in §3.7 part 2. | | 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:1192, enabled `stream_observe_actions_` native_execution_consumer.cpp:9124, 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:1343, native_execution_consumer.cpp:10523; 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:9190, 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:1347, native_execution_consumer.cpp:10589; 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:1955 **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` in batch and across a stream's warmup and realtime legs. 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. What a recorded row costs — quadratic on the Pine adapter, O(closed rows) per row in the kernel — is measured and ruled in §3.8 | | 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 | — | @@ -174,9 +174,9 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | OT2 | Symbol info, sessions, calendars, timezones (`timezone` pine_adapter.cpp:2195-2196, `chart_timezone` pine_adapter.cpp:2236-2238) | **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:117-118 | 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:2157, `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:546 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:902 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 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:546 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 57 `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 symbols are a second, disjoint inventory beside the 57 codegen-facing ones (`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:155 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:1444 — from the run's own calendar and input (`present_session_day` native_execution_consumer.cpp:7116), 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:1794 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:155 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:7182), 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:1794 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:287-289) | 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,11 +202,11 @@ 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:739; the source host casts it back `overrides_opaque` pine_strategy_host.cpp:291-294 | 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:743; the source host casts it back `overrides_opaque` pine_strategy_host.cpp:291-294 | 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:882-884 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1812-1815; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1845, `:8670`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2249-2250. 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:887-889 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1822-1825; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1855, `:8677`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2249-2250. 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:445-451) | 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:6272 | 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 native_execution_consumer.cpp:6331 | 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) | @@ -262,12 +262,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 `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:2913-2923 | F O | -| Legality | The existing rule (`commands_allowed` native_execution_consumer.cpp:1376-1383): 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 `sha256:27ab3bf715fdc1eb1b9183cd41b2b8e1cf0d7cb38c1a313a371b285991bcc0b7` native_execution_consumer.cpp:2939-2949 | F O | +| Legality | The existing rule (`commands_allowed` native_execution_consumer.cpp:1386-1393): 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 | -| 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:546 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:902) | 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:546 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. @@ -276,8 +276,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:638-652, `contribute_input` native_execution_consumer.cpp:7859, `seal_script` native_execution_consumer.cpp:7826) 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:6946), 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:643-657, `contribute_input` native_execution_consumer.cpp:7925, `seal_script` native_execution_consumer.cpp:7892) 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:7012), 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 | @@ -285,7 +285,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 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:1334-1418 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. 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:1425); `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:1435); `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). 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". @@ -324,7 +324,7 @@ Union of F §2.vi (9), O §2.vi (5 + 1) and S §2.6 (7), deduplicated. S had no | 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 | | 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:826, `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:830, `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 @@ -332,12 +332,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:870), a tick hook (`on_native_tick` native_host.hpp:859), a post-fill hook (`on_native_applied` native_host.hpp:918), a recalculation hook (`on_native_recalculate` native_host.hpp:893) and a sub-bar hook (`on_native_sub_bar` native_host.hpp:908). 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:874), a tick hook (`on_native_tick` native_host.hpp:863), a post-fill hook (`on_native_applied` native_host.hpp:922), a recalculation hook (`on_native_recalculate` native_host.hpp:897) and a sub-bar hook (`on_native_sub_bar` native_host.hpp:912). 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:8994-9034 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:1046; 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:9060-9100 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:1050; and `set_native_security_feed` documented as the pre-run door for a series' authoritative bars, refused in-run | --- @@ -345,9 +345,9 @@ 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:2141-2278) 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:1620-1624). 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:e3aa487ce8d69db9054576f75839345c0acaab55c3fc0a374c2f9c8c071559ec` AGENTS.md:132-145). A lane that cannot be shown neutral by construction does not merge. +- **(a) NEUTRALITY.** Every new behaviour is opt-in by a spec field or a new request kind. The adapter's `project()` (pine_adapter.cpp:2141-2278) 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:1625-1629). 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:e3aa487ce8d69db9054576f75839345c0acaab55c3fc0a374c2f9c8c071559ec` AGENTS.md:132-145). A lane that cannot be shown neutral by construction does not merge. - **(b) TWIN.** One chosen probe per lane runs through the adapter AND a hand-written `NativeStrategyHost` on the same bars and spec. Trade lists are diffed **by identity** (entry / exit time, price, qty, pnl, commission), never by trade number; then event, ownership and hash detail (S's D → E order). A difference is allowed only where a named TV-quirk row of §1 is active, and each one is itemized. Because 430 test files 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:1262). Every new test unit compiles first against the frozen previous-epoch header closure (fail-before, `CLAUDE.md`). +- **(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:1272). 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. ### 3.2 Lanes: scope, size, dependencies, epochs @@ -406,7 +406,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:2298, 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:5467-5471) and `drain_applied_notifications` (native_execution_consumer.cpp:6127-6140) 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:5526-5530) and `drain_applied_notifications` (native_execution_consumer.cpp:6187-6200) 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:1334-1418 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) | @@ -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:4848-4850). 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 (`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:4894-4896). 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:3253-3442 +`sha256:8c97d3e1d9d6f036a9be0b50a3a43188f1e2eee23da0384fbed242829108c3c3` native_run_spec.hpp:239-302 and `sha256:efad6b9c7bcd3f7d0e459d434ef088a61b18c546d53cfb107f481a227fbe532b` native_execution_consumer.cpp:3279-3468 (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 | @@ -730,7 +730,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:3145-3149) +(`PF_NATIVE_SPEC_EXT_PRICE_GRID`, translated at native_c_host.cpp:3159-3163) 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 @@ -839,7 +839,7 @@ between the halves. The mark itself is the same bar on both. (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 tree**, so all four `if (!std::isnan(fold_exit_trail_peak_))` carries -(engine_execution.cpp:927/:493, the former adapter loop, the shared producer) +(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 kernel's settling path folds; removing the member is an epoch question (it is hashed at engine_state_hash.cpp:72) and is left to a lane that budgets one. @@ -1158,10 +1158,10 @@ 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:937-941); test that a default spec hashes to the pre-change constant | 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 `sha256:620926caaa868e64ede32f547ac1dd0bfe81c9758267593a9a924051208df0a4` native_execution_consumer.hpp:942-946); 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:12916-12983, pine_adapter.cpp:13030); its chronology lives at `sha256:bb89768c9a7b2dc9f6de5943c04de8103ed107c21bb6e184446de0e6a84cf4c2` pine_adapter.cpp:13589-13666 and its excursion sample differs by chronology (`sha256:764a1a69f2940863b4f77cbf9d20267a19ec38e86720796c7081a59ac7245427` pine_strategy_host.cpp:729-759) | 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:2296-2336) | 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:5547) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `sha256:86617ca43bb2847380f1845b07cc9ecd27059814ee80fecf80649a9393dfe8c8` native_execution_consumer.cpp:3430-3439 and `drain_applied_notifications` (native_execution_consumer.cpp:6127-6140); new behaviour only behind new opt-ins | F | +| E4 | **Calc on fill.** Kernel suffix eligibility is continuous along the segment; TV refills only at O/H/L/C waypoints (`sha256:ddad8dc42e078838c0d53fd49e50c31060221e3b82e7d5bbaa0420769daa7d31` pine_scheduler_native.cpp:585-637, `coof_next_waypoint` pine_adapter.cpp:5547) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `sha256:86617ca43bb2847380f1845b07cc9ecd27059814ee80fecf80649a9393dfe8c8` native_execution_consumer.cpp:3456-3465 and `drain_applied_notifications` (native_execution_consumer.cpp:6187-6200); 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:15893-15921) | 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:1600-1619) | 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:332-380) 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 | @@ -1180,7 +1180,7 @@ unchanged after it. | 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:07645b60312b8fe0c03aed82d9b6365d6cd2a4cc4f322bf815c914401a0d9773` pine_state_hash.cpp:207-673); 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:419-433) | 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 (`sha256:de7e074b6a82f71018015f842ca51f4cba526d5b258a4c9b4d6c914ee12c0541` `sha256:b263f6923ef8095700dc254259434739b63a9fb043773b8139c77ef5e1df91b9` native-engine.md:423-437) | 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:824-825 → pine_adapter.cpp:17343 (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 | @@ -1191,7 +1191,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:1280); between realtime inputs there is none — reject or fall back to `AtMatch`? | F: reject with `InvalidQuantityBasis` | +| Q4 | `SizeTime::AtAcceptance` needs a current execution point (`sha256:00d2682bf92be87d76eea9d03004abad74aee8b5ad03e3f611dbb0d6539a99c4` native_host.hpp:1284); 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 | @@ -1229,16 +1229,16 @@ 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:7040, native_execution_consumer.cpp:7040, native_execution_consumer.cpp:7040) 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:7106, native_execution_consumer.cpp:7106, native_execution_consumer.cpp:7106) 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:2298), 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:1003). 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:4944-4986), `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:1007). 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:4994-5036), `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) ```cpp -// native_order.hpp — native_order_v6; OrderIntent grows 5 -> 6 (static_assert native_order.hpp:1342) +// native_order.hpp — native_order_v6; OrderIntent grows 5 -> 6 (static_assert native_order.hpp:1352) struct CashValue { double cash = 0.0; }; // account currency struct EquityFraction { double fraction = 0.0; }; // of marked equity at the sizing point (0.10 = 10 %) using SizeBasis = std::variant; @@ -1256,7 +1256,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:663; terms resolution `sha256:b06b928e9f12217d7d3036f6adf77250c691c2ab1ad69d48e87417dbdfe1adc7` native_execution_consumer.cpp:2905-2951). A host override of `resolve_execution_terms` keeps the last word. Non-representable → `MatchRejectReason::TermsUnresolved` (native_order.hpp:875); 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:673; terms resolution `sha256:b06b928e9f12217d7d3036f6adf77250c691c2ab1ad69d48e87417dbdfe1adc7` native_execution_consumer.cpp:2931-2977). 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. @@ -1283,9 +1283,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:12912-12933). New hashed `RequestDefinition::origin { Host, KernelLiquidation, KernelRisk }`, new `CancelReason::Superseded` on re-price, `MarginCallEvent` joins `CommandEvent` (17 → 18, native_order.hpp:1262). O's alternative, a checkpoint on the excursion walk (`sha256:ed6f4ff668195caf3c953c8ef5dd448c6010949353f7b9ef44d50b764f7488db` native_execution_consumer.cpp:1874-1887), 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:12912-12933). New hashed `RequestDefinition::origin { Host, KernelLiquidation, KernelRisk }`, new `CancelReason::Superseded` on re-price, `MarginCallEvent` joins `CommandEvent` (17 → 18, native_order.hpp:1272). O's alternative, a checkpoint on the excursion walk (`sha256:ed6f4ff668195caf3c953c8ef5dd448c6010949353f7b9ef44d50b764f7488db` native_execution_consumer.cpp:1884-1897), 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:4618-4653 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:4644-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) @@ -1305,10 +1305,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:1603-1604) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6830-6842) 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:3443-3452). `born_on_remaining_path` (native_execution_consumer.cpp:5467-5471) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1655-1662) 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:1053-1060, whose body is native_execution_consumer.cpp:6680-6687). `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:2522); 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:1612-1613) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6896-6908) 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:3469-3478). `born_on_remaining_path` (native_execution_consumer.cpp:5526-5530) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1655-1662) 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:1058-1065, whose body is native_execution_consumer.cpp:6746-6753). `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:2533); 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) @@ -1346,7 +1346,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 (`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:3499-3504) +// 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:3525-3530) // 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; @@ -1358,7 +1358,7 @@ BracketReceipt submit_bracket(NativeStrategyHost&, const BracketSpec&); // emi template class OrderBook; // id -> handle: submit-or-replace, cancel-by-id ``` -- A leg with owner `WaitForApplied` / `BindOpening` and a non-absolute anchor gets its level when the owner's fill arms it (`ArmedEvent` native_order.hpp:1200-1207 already exists) (O). Fallback with no kernel change: the toolkit materializes relative levels from the host's `on_native_applied` (F). +- A leg with owner `WaitForApplied` / `BindOpening` and a non-absolute anchor gets its level when the owner's fill arms it (`ArmedEvent` native_order.hpp:1210-1217 already exists) (O). Fallback with no kernel change: the toolkit materializes relative levels from the host's `on_native_applied` (F). - Gap lane N13 (after R4d): two facts of the arm were missing and are now opt-in fields appended to `WaitForApplied`, each default byte-identical and folded only when set. `NativeArmFirstMatch { AtArmPrint, AfterArmPrint }`: whether the armed leg is a candidate at its owner's fill print or, like a request born in the owner's fill callback, only after it. `NativeArmScope { OwnerLot, Book }`: whether an armed closing leg closes the lot its owner opened or is bound at the arm to the whole position (a `BookClose`), under which it may be `HostSized`. Ruling: generic — both are broker models of a contingent child, neither names a source language — and they are what makes an anchored child the very request a host would submit from `on_native_applied`. The Pine adapter's relative `strategy.exit` legs use both; R4d's owner-lot spelling left a later same-id add open where the source leg closes the book (`rel-pyramid-set-once`, tests/test_adapter_brackets_relower.cpp). - Not adopted (R5-2): S's kernel bracket object, its C symbols `strategy_native_bracket_*` and its TU list (`engine_orders.cpp`, `engine_execution.cpp`). - TUs: `native_order.{hpp,cpp}` (arming), `native_matching.hpp` (`checked_trail_stop`), `native_execution_consumer.cpp` (`prepare_owner_applied` path, command surface). @@ -1374,7 +1374,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:3901-3923, `execute_current` path native_execution_consumer.cpp:3721-3731), `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:3927-3949, `execute_current` path native_execution_consumer.cpp:3747-3757), `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). ### A.8 L9 — risk limits (F P3 + O G7 + S P5) @@ -1391,10 +1391,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:1302) (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:1306) (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. - 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:13465-13563); 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:4618-4653 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:4644-4688 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 1b1f42af..987463bc 100644 --- a/docs/native-settlement.md +++ b/docs/native-settlement.md @@ -39,8 +39,20 @@ A finite quantity request can leave a positive floating residual. No native epsilon silently discards it. `Flatten` explicitly closes all lots without comparing any quantity; a request is never taken for a whole-book close because it is near the book's aggregate, and it closes a lot whole only where its -binary64 FIFO sum reaches the request exactly (below). Quantities whose changes -cannot be represented are refused before settlement. +binary64 FIFO sum reaches the request exactly (below) -- unless the run declares +a quantity tolerance (*Quantity tolerance*, below). A quantity whose change +binary64 cannot represent on the book it meets is refused before settlement: +the inspection answers `UnrepresentableQuantity`, and the execution consumer +ends THAT request with a terminal `MatchRejectedEvent` whose reason is +`MatchRejectReason::UnrepresentableQuantity` (C: +`PF_NATIVE_MATCH_REJECT_UNREPRESENTABLE_QUANTITY`, the `reason` of a +`PF_NATIVE_EVENT_MATCH_REJECTED` row). Nothing moves, no fee is charged, and +the run goes on; the host submits again if it still wants the trade (R5 lane +K-ULP4). Until then the consumer turned the same inspection into a durable +`SettlementFailure` of the whole run -- lifecycle `Failed`, code 6, +discriminator 5, "native settlement inspection failed". Every other status an +inspection can answer (`InvalidBook`, `InvalidPrice`, `InvalidAccounting`, ...) +is a broken book, price or account, and still stops the run. A close that spans lots and ends inside one of them is charged exactly its units. Every lot before the one it ends in closes whole, and C is the binary64 @@ -56,9 +68,20 @@ survivor. For example, lots `{fl(106/84.5), 10}` reduced by `fl(277/84.5)` close `fl(106/84.5)` and `fl(277/84.5 - 106/84.5)`: the two rows add up one ulp above the request, and the execution reports `fl(277/84.5)` closed. A whole lot whose sum falls short of the request is not where it ends; the rest beyond it is real -and the next lot closes it. A split that binary64 cannot hold is still refused: -a rest below half an ulp of its lot, a rest that a whole lot's close cannot -decrement, and a reduction that cannot move the position. +and the next lot closes it. A split that binary64 cannot hold is still refused +-- a rest below half an ulp of its lot, a rest that a whole lot's close cannot +decrement, a running sum a lot's close cannot move, and a reduction that cannot +move the position -- and so is an opening whose book absorbs it or whose +surviving book it absorbs; each is that request's typed refusal above, not the +run's failure. Ordinary decimal quantities reach them. After open 1.5, Reduce +1.3 and open 2.9 the book is `{0.19999999999999996, 2.9}`, and `Reduce 0.2` +needs `2^-54` of the 2.9 lot, below half its ulp (`2^-52`). A close that ends a +few ulps short of a lot keeps those ulps as a dust lot, and a dust lot at the +head of the book then refuses the next `Reduce` whose rest it cannot decrement +(`fl(1.0 - 2^-55)` is 1.0) and the next opening on its side, whose aggregate +absorbs it (`fl(2^-55 + 200)` is 200). `Flatten` closes a dust book; a close of +the lot's own quantity closes it; the quantity tolerance below books all of +these. A close whose units are exactly the binary64 FIFO sum of the lots through one of them -- `fl(C + qty)` equals the units for that lot's full size, as a close @@ -77,6 +100,61 @@ Units strictly between two such sums still end inside a lot and keep its residual. A selection closed by its whole sum was already consumed whole, and `Flatten` needs no quantity at all. +What a native host gets, then, from a quantity request on the exact settlement: +exactly one terminal outcome, and no run stopped for a quantity. A close that spans lots +and ends inside one is one fill charged its request (K-ULP2); a close whose +binary64 FIFO sum reaches its request at a lot closes that lot whole (K-ULP3); +a `Transact` that crosses the book is one fill charged its units (K-ULP1, in +`docs/pages/native-engine.md`); a request the settlement cannot book exactly is +refused, typed, as above, which `inspect_current_execution` shows beforehand as +a `settlement_readiness` of `UnrepresentableQuantity`; and `Flatten` is never +refused for a quantity. On a quantity grid (`NativeRunSpec::quantity_grid`) the +book's own quantities are on the grid: a `ScopeFraction` whose product is its +scope -- `fraction == 1` -- resolves to the scope's held total, which the grid +does not floor, and a close of exactly one lot's binary64 quantity is admitted, +at submit as a `Reduce` or a closing `Transact` and as a host-sized close's +answered units (R5 lane K-ULP4). Before, such a fraction was floored onto the grid +and closed up to a step short, left a dust lot, or found nothing to close, and +the lot's own size was `OffGrid`. A close of a FIFO prefix's sum or of the +held total of a multi-lot book is still gridded; `Flatten` and +`ScopeFraction{1}` are its spellings. + +## Quantity tolerance + +A run that declares `NativeRunSpec::quantity_tolerance = t` (C: the +`quantity_tolerance` tail of `pf_native_run_spec_ext_v1` under +`PF_NATIVE_SPEC_EXT_QUANTITY_TOLERANCE`; finite and positive, else +`NotFinitePositive` on `QuantityTolerance`) makes two quantities within `t` of +each other one quantity to the settlement. It is opt-in and absent by default: +an absent tolerance is the exact walk above, bit for bit, and the spec digest +folds the value only when it is present. Under it: + +- A close that comes within `t` of a FIFO boundary -- the binary64 sum of the + scope's lots through one of them -- ends at that boundary and is charged its + request, as a split its rows cannot add up to is (K-ULP2). If the lot the + request ends in would keep at most `t`, it closes whole; if a whole lot + leaves a rest of at most `t` and the next member lot is larger than `t`, + that lot is not touched; if the next lot is itself at most `t`, it closes + whole instead, so dust behind a boundary is taken rather than left. The + scope's end is a boundary too: a `Reduce` or a crossing `Transact` at most + `t` past the book closes the book and opens nothing. `{0.19999999999999996, + 2.9}` reduced by 0.2 closes the first lot, charged 0.2, and keeps 2.9. +- A lot of at most `t` that a close reaches -- a dust lot at the head of the + book -- closes whole even where binary64 cannot take it off the rest or the + running sum, and the walk goes on: `{2^-55, 2.9}` reduced by 1.0 books the + rows `2^-55` and 1.0 and keeps 1.9. A surviving book of at most `t` beside a + same-side opening stays as it is, beside the new lot. +- What no tolerance makes a quantity stays refused, typed as above: a rest + below half an ulp of the first lot it meets (a request of at most `t` against + a larger lot -- zero is not a boundary to snap to), a rest above `t` that a + lot or the running sum absorbs, a reduction the position absorbs, and an + opening the book absorbs. + +The charge is the request: a snapped close's `closed_units` and +`filled_working` are its units, and its rows add up to the boundary, at most +`t` away. A host picks `t` well below its smallest quantity; the Pine layer's +own rule (next paragraph) uses 1e-10. + The source `execute_partial_exit_qty` adapter retains its existing `1e-10` FIFO endpoint policy. After its existing whole-book Flatten check, it may translate a quantity ending at an interior whole-lot prefix into one selected @@ -103,6 +181,12 @@ 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. +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. ## Reversal to an exact exposure diff --git a/docs/pages/contributing-llm.md b/docs/pages/contributing-llm.md index 7e13f15f..d57fd107 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:231 and - `RELEASE_MIN_TESTS` ci_verify.py:335. Adding rows means raising the floor + *ran* against a floor: `KERNEL_MIN_TESTS` ci_verify.py:235 and + `RELEASE_MIN_TESTS` ci_verify.py:343. Adding rows means raising the floor in the same commit. ## The recipe for a lane @@ -240,8 +240,8 @@ 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:826, or C through the -`strategy_native_*` surface `sha256:1a4d41202e1b32a33ab75c2e42a36285ccb1d061087a8db1c19162b244c5b29f` native_c_api.h:2609. +through `NativeStrategyHost` native_host.hpp:830, or C through the +`strategy_native_*` surface `sha256:1a4d41202e1b32a33ab75c2e42a36285ccb1d061087a8db1c19162b244c5b29f` native_c_api.h:2639. **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 @@ -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:231, `RELEASE_MIN_TESTS` ci_verify.py:335). It +(`KERNEL_MIN_TESTS` ci_verify.py:235, `RELEASE_MIN_TESTS` ci_verify.py:343). 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 c78f286f..49c81c18 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:874`) is +**The callbacks.** Only `on_native_bar` (`native_host.hpp:878`) 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:846`) | once, with the begin's own arguments, before the run starts | *Lifecycle and run identity* | -| `on_native_run_begin` (`native_host.hpp:853`) | 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:856`) | once per accepted confirmed input, before it is aggregated or matched | *Calendar, session, timeframes, warmup* | -| `on_native_tick` (`native_host.hpp:859`) | once per accepted realtime print, before it is matched | *Batch OHLCV vs ticks vs quiet* | -| `on_native_timeframe_bar` (`native_host.hpp:866`) | once per delivered bucket of a declared series | *Higher-timeframe series for native hosts* | -| `on_native_bar_open` (`native_host.hpp:870`) | at the modeled opening, before that point's matching pass | *Native requests* (and its lookahead warning) | -| `on_native_bar` (`native_host.hpp:874`) | the script bar's own calculation | *Calculation timing* | -| `on_native_recalculate` (`native_host.hpp:893`) | 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:908`) | after each retained lower-timeframe sub-bar's whole path | *Sub-bars* | -| `on_native_applied` (`native_host.hpp:918`) | after each applied execution — the calculate-on-fill point | *Native requests* | -| `on_native_margin_call` (`native_host.hpp:981`) | right after the `on_native_applied` of a kernel liquidation's own fill | *Margin and liquidation* | +| `prepare_native_begin` (`native_host.hpp:850`) | once, with the begin's own arguments, before the run starts | *Lifecycle and run identity* | +| `on_native_run_begin` (`native_host.hpp:857`) | 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:860`) | once per accepted confirmed input, before it is aggregated or matched | *Calendar, session, timeframes, warmup* | +| `on_native_tick` (`native_host.hpp:863`) | once per accepted realtime print, before it is matched | *Batch OHLCV vs ticks vs quiet* | +| `on_native_timeframe_bar` (`native_host.hpp:870`) | once per delivered bucket of a declared series | *Higher-timeframe series for native hosts* | +| `on_native_bar_open` (`native_host.hpp:874`) | at the modeled opening, before that point's matching pass | *Native requests* (and its lookahead warning) | +| `on_native_bar` (`native_host.hpp:878`) | the script bar's own calculation | *Calculation timing* | +| `on_native_recalculate` (`native_host.hpp:897`) | 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:912`) | after each retained lower-timeframe sub-bar's whole path | *Sub-bars* | +| `on_native_applied` (`native_host.hpp:922`) | after each applied execution — the calculate-on-fill point | *Native requests* | +| `on_native_margin_call` (`native_host.hpp:985`) | 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:927`), -`validate_execution_precommit` (`native_host.hpp:940`), -`resolve_margin_requirement` (`native_host.hpp:956`), `margin_check_allowed` -(`native_host.hpp:968`), `resolve_margin_call_units` (`native_host.hpp:975`), -`resolve_anchored_level` (`native_host.hpp:992`), `owns_lot_excursions` / -`closed_lot_excursion` (`native_host.hpp:1010`) and the hash seam +the kernel's own answer: `resolve_execution_terms` (`native_host.hpp:931`), +`validate_execution_precommit` (`native_host.hpp:944`), +`resolve_margin_requirement` (`native_host.hpp:960`), `margin_check_allowed` +(`native_host.hpp:972`), `resolve_margin_call_units` (`native_host.hpp:979`), +`resolve_anchored_level` (`native_host.hpp:996`), `owns_lot_excursions` / +`closed_lot_excursion` (`native_host.hpp:1014`) 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:1027`), and one that keeps the default +(`native_host.hpp:1031`), and one that keeps the default `validate_execution_precommit` with `declare_native_precommit_hook(false)` -(`native_host.hpp:1035`). The kernel then makes no bar-open call, and neither +(`native_host.hpp:1039`). 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, @@ -201,6 +201,10 @@ Always set, with documented defaults in the header: Optional, absent unless set: - `quantity_grid`: finite > 0; **admission only**, never resizes quantity +- `quantity_tolerance`: finite > 0, in units; two quantities within it are one + quantity to the settlement (a close within it of a FIFO boundary ends there, + a dust lot a close reaches closes whole). See *What a host gets from a + quantity* below. - `max_abs_units`: finite > 0; opening cap on the resulting book - `max_open_lots`: positive; surviving + new lots - `initial_margin_fraction`: finite > 0 as a fraction, not a percent. Opening @@ -284,12 +288,12 @@ the sub-bar sections of `tests/test_native_calc_timing.cpp`. ### Validating a spec -`validate_native_run_spec(spec)` (`native_run_spec.hpp:806`) answers a -`NativeRunSpecValidation` (`native_run_spec.hpp:789`): a -`NativeRunSpecError` (`native_run_spec.hpp:678`) and the -`NativeRunSpecField` (`native_run_spec.hpp:646`) it first failed on, with +`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 `ok()` and an explicit `operator bool`. `normalize_native_run_spec(spec)` -(`native_run_spec.hpp:816`) validates and rewrites the one admitted literal — +(`native_run_spec.hpp:829`) 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 @@ -307,7 +311,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:892`) is the portable +`native_run_spec_digest(spec)` (`native_run_spec.hpp:905`) 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 @@ -454,11 +458,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:8994-9034`), which admits the begin, checks the +(`native_execution_consumer.cpp:9060-9100`), 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:739`), which the kernel forwards +opaque `overrides_opaque` (`native_host.hpp:743`), 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). @@ -482,7 +486,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:2698-2748`), under the same legality rule; see *Driving the +(`native_c_api.h:2728-2778`), under the same legality rule; see *Driving the kernel from C* below. `native_order::Request` values belong to `native_order_v7` @@ -618,25 +622,28 @@ 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:5467-5471`). Requests accepted before the +`native_execution_consumer.cpp:5526-5530`). 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:6913-6915`). **Lookahead warning:** the +pass (`native_execution_consumer.cpp:6980-6982`). **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:6806`), the +`engine.current_bar_ = open_view` (`native_execution_consumer.cpp:6872`), 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:1046`; C: +`current_partial_bar()` (`native_host.hpp:1050`; C: `strategy_native_partial_bar_v1`), the lookahead-free bar so far at this cursor, or declares `NativeOpenBarView::OpenOnly`, which masks this one callback's bar down to its open. Both are under *The bar so far, and the open-bar view* below. A `quantity_grid`, when present, admits Transact/Reduce quantities on the -exact binary64 grid in `native_order.hpp`. Flatten is not gridded. Rejection -does not rewrite the attempted bits. +exact binary64 grid in `native_order.hpp`, and a close -- a `Reduce`, or a +`Transact` against the book's side -- of exactly one lot's own binary64 +quantity, which settlement arithmetic can move off any decimal grid +(`CommandContext::units_are_lot_quantity`, R5 lane K-ULP4). Flatten is not +gridded. Rejection does not rewrite the attempted bits. **Acceptance is not a fill.** `submit_market` returns `SubmitResult`: @@ -665,8 +672,10 @@ not the acceptance-cycle book: a flatten accepted while flat can still close a same-point opening that already filled. If the book is flat at execution, Reduce/Flatten terminalize `NoEffectEvent`: no execution identity, no fill, no fee, no physical action. `MatchRejectedEvent` is an event ordinal without -execution identity (opening direction, max units/lots, initial margin, or -nonpositive resolved price). An opening denial rejects the **entire** +execution identity (opening direction, max units/lots, initial margin, +nonpositive resolved price, or a quantity the settlement cannot book exactly on +the book it met -- `UnrepresentableQuantity`, which ends that request and not +the run; see *What a host gets from a quantity* below). An opening denial rejects the **entire** Transact, including a proposed close remainder; it does not close old exposure first. Closing-only Reduce/Flatten remain allowed while opening limits are already exceeded. @@ -1808,8 +1817,8 @@ says otherwise, and the C spellings are in | `current_partial_bar()` | the lookahead-free bar so far at this cursor | `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:1328`) | 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:1333`) | 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:1332`) | 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:1337`) | 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 | @@ -1849,23 +1858,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:1444`). The kernel resolves each session day once through +(`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 matching time or the after-calculation coordinate, and the session-day facts fold into no digest. `on_native_input` receives -`NativeInputContext` (`native_host.hpp:754`) instead — the two intervals, the +`NativeInputContext` (`native_host.hpp:758`) instead — the two intervals, the input index and whether this input completes the script interval — and -`on_native_tick` receives `NativeTickContext` (`native_host.hpp:766`), the +`on_native_tick` receives `NativeTickContext` (`native_host.hpp:770`), 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:1258`) enrolls one accepted opening's handle, -`cohort_remove` (`native_host.hpp:1261`) takes it back off, and a request with +(`native_host.hpp:1262`) enrolls one accepted opening's handle, +`cohort_remove` (`native_host.hpp:1265`) 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 @@ -1970,7 +1979,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:7650`), so +(`record_script_report_point`, `native_execution_consumer.cpp:7716`), so ```text broker_state_hash_len == equity_curve_len == script_bars_processed @@ -2738,7 +2747,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:856`). Include +(`on_native_input` `native_host.hpp:860`). Include ``: ```cpp @@ -2764,7 +2773,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:7732`). What it does +(`TimeframeAggregator` `native_execution_consumer.cpp:7798`). 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 @@ -2850,7 +2859,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 (`sha256:2e963d6ab1630db1535bd944dc7406ba649e9d14a589065db25347569fbad150` native_c_api.h:37-39) take a `pf_strategy_t` from `strategy_native_host_create_v1` unchanged — and -`strategy_native_submit_v1` (`native_c_api.h:2648`) obeys the one legality +`strategy_native_submit_v1` (`native_c_api.h:2678`) obeys the one legality rule its C++ spelling does. Rebuild strategy libraries against this engine. An ABI-v4 module without the @@ -3058,6 +3067,43 @@ closed + |opened| can round one ulp either side of the units; the fill's B-ENGINE). Before, a sum one ulp above stopped the run, and one ulp below left a 2^-49-unit remainder that a second fill opened as a dust lot. +### What a host gets from a quantity + +Every quantity request ends in exactly one terminal outcome, and no ordinary +quantity stops the run. The FIFO rules are the settlement's +(`docs/native-settlement.md`, "Physical lots and quantities"); what a host sees: + +- A close that spans lots and ends inside one is one terminal fill whose + `closed_units` and `filled_working` are its request, even where the rows, + the whole lots and the rest, add up one ulp away (R5 lane K-ULP2). +- A close whose binary64 FIFO sum reaches its request at a lot -- the book's + own held total, a FIFO prefix of it -- closes that lot whole and leaves no + dust lot (K-ULP3). +- A crossing `Transact` is charged exactly its units (K-ULP1, above). +- A request the settlement cannot book exactly on the book it meets is refused, + typed: a terminal `MatchRejectedEvent` with + `MatchRejectReason::UnrepresentableQuantity` (C: + `PF_NATIVE_MATCH_REJECT_UNREPRESENTABLE_QUANTITY`), nothing moved, the run + still running (R5 lane K-ULP4). `inspect_current_execution` shows it + beforehand as a `settlement_readiness` of `UnrepresentableQuantity`, and + `execute_current` answers the `MatchRejectedEvent` without throwing. Ordinary + decimal quantities reach it: after open 1.5, `Reduce` 1.3 and open 2.9, a + `Reduce 0.2` needs `2^-54` of the 2.9 lot, and a dust lot a close left at + the head of the book refuses the next close and the next opening on its + side. Until K-ULP4 each of these stopped the run with code 6, discriminator + 5. `Flatten` is never refused for a quantity. +- On a `quantity_grid` the book's own quantities are on the grid: a + `ScopeFraction` whose product is its scope (`fraction == 1`) resolves to the + scope's held total, unfloored, and a close of one lot's own size is admitted + (above). +- `NativeRunSpec::quantity_tolerance` is the opt-in for a host that keeps + decimal quantities, as a broker ledger does: within the tolerance a close + ends at the FIFO boundary it is near, charged its request, and a dust lot a + close reaches closes whole, so such a host's closes neither leave dust nor + meet the refusal above. A C host sets it in the `quantity_tolerance` tail of + `pf_native_run_spec_ext_v1`. The Pine adapter does not declare it and keeps + its own `1e-10` rule (`docs/native-settlement.md`, "Quantity tolerance"). + ### Sizing without a host override `native_order::Sized` is the sixth `OrderIntent`: an opening whose size the @@ -3272,7 +3318,11 @@ because `scope * percent / 100` and `scope * (percent / 100)` are different binary64 values. The result is floored onto `quantity_grid` like every other engine quantity; a fraction that does not buy one whole step is `TermsUnresolved`, and `fraction` outside `(0, 1]` is `InvalidQuantityBasis` at -submit. +submit. The one exception is a fraction whose product is the scope itself -- +`fraction == 1`, "close it all": it resolves to the scope, 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). Neither kind is emitted by the Pine adapter, which keeps resolving its own `HostSized` terms; `native_order` values therefore belong to `native_order_v7`. @@ -3795,15 +3845,18 @@ every series is built from the input (the subscription row itself has no spare word left, so the source rides beside it rather than in it). The event retention (`PF_NATIVE_SPEC_EXT_EVENT_RETENTION`, R5 lane V19-B) is the last: `event_retention`, a `pf_native_event_retention_e` word, and a reserved word -that must be zero. The struct therefore has five published lengths and the -runtime accepts each: `PF_NATIVE_RUN_SPEC_EXT_V1_BASE_SIZE`, -`PF_NATIVE_RUN_SPEC_EXT_V1_RISK_SIZE`, `PF_NATIVE_RUN_SPEC_EXT_V1_POLICY_SIZE`, -`PF_NATIVE_RUN_SPEC_EXT_V1_AUXILIARY_SIZE` (each the offset of the first field +that must be zero. The quantity tolerance (`PF_NATIVE_SPEC_EXT_QUANTITY_TOLERANCE`, +R5 lane K-ULP4) is the last: one `double`, `quantity_tolerance`. The struct +therefore has six published lengths and the runtime accepts each: +`PF_NATIVE_RUN_SPEC_EXT_V1_BASE_SIZE`, `PF_NATIVE_RUN_SPEC_EXT_V1_RISK_SIZE`, +`PF_NATIVE_RUN_SPEC_EXT_V1_POLICY_SIZE`, `PF_NATIVE_RUN_SPEC_EXT_V1_AUXILIARY_SIZE`, +`PF_NATIVE_RUN_SPEC_EXT_V1_RETENTION_SIZE` (each the offset of the first field of the tail behind it, not a literal) and the current `sizeof`; a caller is refused with `PF_NATIVE_E_STRUCT` for a bit whose tail it does not carry, and any other length is refused outright. A caller that does not set the retention bit keeps `PF_NATIVE_EVENT_RETENTION_FULL`, so every caller -compiled before the tail existed reads back the record it always did. A realtime stream appends later feed +compiled before the tail existed reads back the record it always did; one +that does not set the tolerance bit keeps the exact settlement. A realtime stream appends later feed bars with `strategy_native_append_auxiliary_bars_v1`, the C spelling of `append_auxiliary_bars`: `PF_NATIVE_OK`, or `PF_NATIVE_E_STATE` for every by-name refusal above with the reason in `strategy_get_last_error`. @@ -4004,7 +4057,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:826`); the `CapAttachment` constructor belongs +zero-argument (`native_host.hpp:830`); 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`). diff --git a/docs/pages/pine-to-native.md b/docs/pages/pine-to-native.md index f820e566..37ecf258 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -60,19 +60,19 @@ 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:874). Any data structure, any library, any + function (native_host.hpp:878). 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 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 (`Request` native_order.hpp:483); its fate is a typed event — `AcceptedEvent` - (native_order.hpp:981), `ExecutionAppliedEvent` (native_order.hpp:1120), - `MatchRejectedEvent` (native_order.hpp:1082), `CancelledEvent` - (native_order.hpp:1023) — readable in order from `native_events` - (native_host.hpp:1302). + (native_order.hpp:991), `ExecutionAppliedEvent` (native_order.hpp:1130), + `MatchRejectedEvent` (native_order.hpp:1092), `CancelledEvent` + (native_order.hpp:1033) — readable in order from `native_events` + (native_host.hpp:1306). - **Any language.** The same kernel drives from C through a callback table - (`pf_native_callbacks_v1` native_c_api.h:2425), so a host in Rust, Go, Python or Zig needs no C++. + (`pf_native_callbacks_v1` native_c_api.h:2454), so a host in Rust, Go, Python or Zig needs no C++. - **One binary, no transpiler.** Link `libpineforge`, ship an executable or a loadable module. @@ -111,31 +111,31 @@ 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:1169); a C host sends `pf_native_run_spec_v1` -(pineforge.h:546) plus `pf_native_run_spec_ext_v1` (native_c_api.h:2253) -through `strategy_configure_native_ext_v1` (native_c_api.h:3077). Setup is +(native_host.hpp:1173); a C host sends `pf_native_run_spec_v1` +(pineforge.h:546) plus `pf_native_run_spec_ext_v1` (native_c_api.h:2265) +through `strategy_configure_native_ext_v1` (native_c_api.h:3107). 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:546 | `hello_kernel.cpp` / `hello_kernel_c.c` | One atomic setup value. `validate_native_run_spec` native_run_spec.hpp:806 names the offending field before a run begins. | +| `strategy()` | `NativeRunSpec` native_run_spec.hpp:553 | `pf_native_run_spec_v1` pineforge.h:546 | `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:537 | `hello_kernel.cpp` | Required, positive. | | `currency` | `currency` native_run_spec.hpp:570 | `currency` pineforge.h:535 | `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:538 | `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:538 | `native_open_lots_strategy.cpp` | Quoted per execution by `quote_execution_commissions` engine_execution.cpp:1487. 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:538 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:3910-3911. TradingView's round-then-slip order is adapter-only. | -| `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:539 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:7372-7373, 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:541 | `native_open_lots_strategy.cpp` | Caps surviving+new physical lots `sha256:87bd0ff848901478840870d95eb356b35d7d368555ce8eadee8571b8e17f86ec` native_execution_consumer.cpp:2096-2099. Pine's per-cycle *entry count* is a TradingView rule, ruled **adapter-policy** in ADR-0001's table row `max_open_lots`. | -| `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:2091 | `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:2095 | `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:2263 | `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:2263 | `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:859 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:2273 | `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:2277 | `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:589 | `fee_value` pineforge.h:538 | `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:538 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:3936-3937. TradingView's round-then-slip order is adapter-only. | +| `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:539 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:7438-7439, 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:541 | `native_open_lots_strategy.cpp` | Caps surviving+new physical lots `sha256:87bd0ff848901478840870d95eb356b35d7d368555ce8eadee8571b8e17f86ec` native_execution_consumer.cpp:2107-2110. Pine's per-cycle *entry count* is a TradingView rule, ruled **adapter-policy** in ADR-0001's table row `max_open_lots`. | +| `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:2102 | `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:2106 | `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:2276 | `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:2276 | `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:863 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:2286 | `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:2290 | `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:2316 | `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:646 configure a *compiled Pine strategy*, never a native host. | +| `use_bar_magnifier` | `IntrabarPath` native_run_spec.hpp:378 | `intrabar_kind` native_c_api.h:2329 | `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:646 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`. | @@ -265,33 +265,33 @@ 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:927), which sees the kernel-resolved quantity as the facts' +(native_host.hpp:931), 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:2516 for a close. +the same question through `on_close_units` native_c_api.h:2546 for a close. ## Orders {#pine_to_native_map_orders} -Everything here is one call: `submit` native_host.hpp:1186, or `submit_market` -native_host.hpp:1200 for the market-only shorthand, which *refuses* non-market +Everything here is one call: `submit` native_host.hpp:1190, or `submit_market` +native_host.hpp:1204 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:2648 over a -`pf_native_request_v1` native_c_api.h:2083. +is `strategy_native_submit_v1` native_c_api.h:2678 over a +`pf_native_request_v1` native_c_api.h:2093. | 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:2648 | `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:2648 | `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:2648 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:1139. 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. | +| `strategy.entry()` | `Request` native_order.hpp:483 carrying `Transact` native_order.hpp:41 | `strategy_native_submit_v1` native_c_api.h:2678 | `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:2678 | `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:2678 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:1149. 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`) 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:1251 with `NativeRequestField::Label` native_host.hpp:679 | `strategy_native_cancel_where_v1` native_c_api.h:2735 | `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:1222 is the single-request form for a host that kept the handle. | -| `strategy.cancel_all()` | `cancel_all` native_host.hpp:1240 | `strategy_native_cancel_all_v1` native_c_api.h:2714 | `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:1236 before its arm, and all three cancels still address it. | +| `strategy.cancel()` | `cancel_where` native_host.hpp:1255 with `NativeRequestField::Label` native_host.hpp:679 | `strategy_native_cancel_where_v1` native_c_api.h:2765 | `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:1226 is the single-request form for a host that kept the handle. | +| `strategy.cancel_all()` | `cancel_all` native_host.hpp:1244 | `strategy_native_cancel_all_v1` native_c_api.h:2744 | `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:1240 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.direction.all` | `NativeOpenDirections::Both` native_run_spec.hpp:76 | `allowed_open_directions` pineforge.h:539 | `native_risk_limits_strategy.cpp` | The default. | -| `strategy.direction.long` | `NativeOpenDirections::Long` native_run_spec.hpp:74 | `allowed_open_directions` pineforge.h:539 | `tests/test_native_run_spec.cpp` | An opening the direction forbids is refused with `OpeningDirection` native_execution_consumer.cpp:2906. | +| `strategy.direction.long` | `NativeOpenDirections::Long` native_run_spec.hpp:74 | `allowed_open_directions` pineforge.h:539 | `tests/test_native_run_spec.cpp` | An opening the direction forbids is refused with `OpeningDirection` native_execution_consumer.cpp:2932. | | `strategy.direction.short` | `NativeOpenDirections::Short` native_run_spec.hpp:75 | `allowed_open_directions` pineforge.h:539 | `tests/test_native_run_spec.cpp` | Same, the other way. | | `strategy.commission.percent` | `NativeFeeKind::Percent` native_run_spec.hpp:20 | `fee_kind` pineforge.h:538 | `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:538 | `tests/test_native_selected_requests.cpp` | Account currency per unit. | @@ -302,8 +302,8 @@ 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:1193, which emits -`ReplacedEvent` native_order.hpp:997 and can carry the predecessor's live +quantity. Amending a live order is `replace` native_host.hpp:1197, which emits +`ReplacedEvent` native_order.hpp:1007 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 spelling is `strategy_native_replace_v1`, or `strategy_native_replace_ext_v1` @@ -315,9 +315,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 | `arm_scope` / `arm_first_match` native_c_api.h:2165-2166 | `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` native_c_api.h:2100 | `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:2105 | `native_trail_risk_strategy.cpp` | The absolute level the trail arms at. Read the live projection with `trail_state` native_host.hpp:1065 (C: `strategy_native_trail_state_v1` native_c_api.h:2956). 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 | `arm_scope` / `arm_first_match` native_c_api.h:2176-2177 | `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` native_c_api.h:2111 | `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:2116 | `native_trail_risk_strategy.cpp` | The absolute level the trail arms at. Read the live projection with `trail_state` native_host.hpp:1069 (C: `strategy_native_trail_state_v1` native_c_api.h:2986). 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 @@ -339,11 +339,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:992, and carries the installed level - in `ArmedEvent` native_order.hpp:1200. + `resolve_anchored_level` native_host.hpp:996, and carries the installed level + in `ArmedEvent` native_order.hpp:1210. - **Invisible until armed.** `NativeArmVisibility::PendingUntilArmed` native_order.hpp:394 keeps the legs out of `native_working_requests` - native_host.hpp:1236 until the fill, like Pine's pending exit and like a + native_host.hpp:1240 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: @@ -372,10 +372,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:2648 with +each leg is its own `strategy_native_submit_v1` native_c_api.h:2678 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:2166-2167, so a C bracket child arms after its owner's print or over +native_c_api.h:2176-2177, 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 @@ -396,12 +396,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:539 | `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:2906. | +| `strategy.risk.allow_entry_in()` | `allowed_open_directions` native_run_spec.hpp:596 | `allowed_open_directions` pineforge.h:539 | `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:2932. | | `strategy.risk.max_position_size()` | `max_abs_units` native_run_spec.hpp:594 | `max_abs_units` pineforge.h:540 | `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:2299 | `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:2302 | `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:2304 | `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:2306 | `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:1296. | +| `strategy.risk.max_drawdown()` | `NativeRiskLimits::max_drawdown` native_run_spec.hpp:311 | `risk_max_drawdown` native_c_api.h:2312 | `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:2315 | `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:2317 | `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:2319 | `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:1300. | 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 @@ -420,19 +420,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:2273 | `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:2278 | `native_margin_strategy.cpp` | -| how much a liquidation closes | `NativeLiquidationSizing` native_run_spec.hpp:140 | `margin_sizing` native_c_api.h:2268 | `native_margin_strategy.cpp` | -| when the requirement is tested | `NativeLiquidationCheck` native_run_spec.hpp:158 | `margin_check` native_c_api.h:2269 | `native_margin_strategy.cpp` | -| which equity it is tested against | `NativeMarginEquityBasis` native_run_spec.hpp:172 | `margin_equity_basis` native_c_api.h:2336 | `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:2337 | `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:2338 | `native_margin_strategy.cpp` | -| the host's own admission verdict | `validate_execution_precommit` native_host.hpp:940 | — C hosts gate through `on_precommit` native_c_api.h:2541 | `native_margin_strategy.cpp` | -| the host's own requirement | `resolve_margin_requirement` native_host.hpp:956 | `on_margin_requirement` native_c_api.h:2478 | `native_margin_strategy.cpp` | -| which check points run at all | `margin_check_allowed` native_host.hpp:968 | `on_margin_check` native_c_api.h:2487 | `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:926 | `native_fx_roll_strategy.cpp` | -| the host's own liquidation size | `resolve_margin_call_units` native_host.hpp:975 | `on_margin_call_units` native_c_api.h:2494 | `native_margin_strategy.cpp` | -| the liquidation event | `MarginCallEvent` native_order.hpp:1216 delivered to `on_native_margin_call` native_host.hpp:981 | `on_margin_call` native_c_api.h:2442 | `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:2286 | `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:2291 | `native_margin_strategy.cpp` | +| how much a liquidation closes | `NativeLiquidationSizing` native_run_spec.hpp:140 | `margin_sizing` native_c_api.h:2281 | `native_margin_strategy.cpp` | +| when the requirement is tested | `NativeLiquidationCheck` native_run_spec.hpp:158 | `margin_check` native_c_api.h:2282 | `native_margin_strategy.cpp` | +| which equity it is tested against | `NativeMarginEquityBasis` native_run_spec.hpp:172 | `margin_equity_basis` native_c_api.h:2349 | `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:2350 | `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:2351 | `native_margin_strategy.cpp` | +| the host's own admission verdict | `validate_execution_precommit` native_host.hpp:944 | — C hosts gate through `on_precommit` native_c_api.h:2571 | `native_margin_strategy.cpp` | +| the host's own requirement | `resolve_margin_requirement` native_host.hpp:960 | `on_margin_requirement` native_c_api.h:2508 | `native_margin_strategy.cpp` | +| which check points run at all | `margin_check_allowed` native_host.hpp:972 | `on_margin_check` native_c_api.h:2517 | `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:979 | `on_margin_call_units` native_c_api.h:2524 | `native_margin_strategy.cpp` | +| the liquidation event | `MarginCallEvent` native_order.hpp:1226 delivered to `on_native_margin_call` native_host.hpp:985 | `on_margin_call` native_c_api.h:2472 | `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 @@ -446,29 +446,29 @@ openings TradingView takes. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| script body at bar close | `on_native_bar` native_host.hpp:874 | `on_bar` native_c_api.h:2432 | `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:893, whose default forwards here. | -| every calculation, tagged with its reason | `on_native_recalculate` native_host.hpp:893 with `NativeCalculationReason` native_host.hpp:806 | `on_recalculate` native_c_api.h:2460 | `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:807 | `on_recalculate` native_c_api.h:2460 | `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:1744 | `native_market_strategy.cpp` | `Batch`, `Warmup`, `Realtime`. | -| bar-open pre-pass | `on_native_bar_open` native_host.hpp:870 | `on_bar_open` native_c_api.h:2431 | `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:1046 | `strategy_native_partial_bar_v1` native_c_api.h:2941 | `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:918 | `on_applied` native_c_api.h:2434 | `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:5225. | -| raw input before aggregation | `on_native_input` native_host.hpp:856 | `on_input` native_c_api.h:2429 | `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:908 | `on_sub_bar` native_c_api.h:2470 | `tests/test_native_calc_timing.cpp` | `NativeCalculationReason::SubBar` is reserved and never delivered to the recalculate hook — a sub-bar has its own callback. | +| script body at bar close | `on_native_bar` native_host.hpp:878 | `on_bar` native_c_api.h:2462 | `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:897, whose default forwards here. | +| every calculation, tagged with its reason | `on_native_recalculate` native_host.hpp:897 with `NativeCalculationReason` native_host.hpp:810 | `on_recalculate` native_c_api.h:2490 | `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:811 | `on_recalculate` native_c_api.h:2490 | `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:1754 | `native_market_strategy.cpp` | `Batch`, `Warmup`, `Realtime`. | +| bar-open pre-pass | `on_native_bar_open` native_host.hpp:874 | `on_bar_open` native_c_api.h:2461 | `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:1050 | `strategy_native_partial_bar_v1` native_c_api.h:2971 | `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:922 | `on_applied` native_c_api.h:2464 | `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:5284. | +| raw input before aggregation | `on_native_input` native_host.hpp:860 | `on_input` native_c_api.h:2459 | `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:912 | `on_sub_bar` native_c_api.h:2500 | `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:114 with `NativeCoordinate` market_driver.hpp:71 | `pf_native_decision_v1` native_c_api.h:1443 | `hello_kernel.cpp` | Copied onto the callback stack; mutating it changes nothing. In C++ `script_interval` market_driver.hpp:118 and `input_interval` market_driver.hpp:117 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 session-day facts are the three rows below. | -| `session.ismarket` | `in_session` market_driver.hpp:155 | `in_session` native_c_api.h:1461 | `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:1460 is 1. | +| `session.ismarket` | `in_session` market_driver.hpp:155 | `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.isfirstbar` | `opens_session_day` market_driver.hpp:156 | `opens_session_day` native_c_api.h:1462 | `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:157, `closes_session_day_open_ended` market_driver.hpp:158 | `closes_session_day` native_c_api.h:1463 | `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:1060 | `price` native_c_api.h:1523 | `native_selected_strategy.cpp` | The readiness preview `inspect_current_execution` native_host.hpp:1072 is an observation, never an apply token; `execute_current` native_host.hpp:1077 is the apply. | +| `session.islastbar` | `closes_session_day` market_driver.hpp:157, `closes_session_day_open_ended` market_driver.hpp:158 | `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:1064 | `price` native_c_api.h:1533 | `native_selected_strategy.cpp` | The readiness preview `inspect_current_execution` native_host.hpp:1076 is an observation, never an apply token; `execute_current` native_host.hpp:1081 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:1265, - `native_marked_equity` native_host.hpp:1274, `native_open_lots` - native_host.hpp:1270, `native_liquidation_price` native_host.hpp:1293. +1. **Live, inside a callback** — `physical_position` native_host.hpp:1269, + `native_marked_equity` native_host.hpp:1278, `native_open_lots` + native_host.hpp:1274, `native_liquidation_price` native_host.hpp:1297. 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` @@ -495,16 +495,16 @@ runs close the same trades for the same money. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `strategy.account_currency` | `currency` native_run_spec.hpp:570 | `currency` pineforge.h:535 | `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:1175. 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.account_currency` | `currency` native_run_spec.hpp:570 | `currency` pineforge.h:535 | `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:1179. 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:537 | `hello_kernel.cpp` | The value you declared, unchanged by the run. | -| `strategy.equity` | `native_marked_equity` native_host.hpp:1274 | `strategy_native_marked_equity_v1` native_c_api.h:2981 | `native_open_lots_strategy.cpp` | Pine marks at the current `close`; the native call takes the mark explicitly, so the caller says what "now" means. It equals the balance plus the open lots' `unrealized_pnl` native_host.hpp:341, which the example asserts at every calculation. | +| `strategy.equity` | `native_marked_equity` native_host.hpp:1278 | `strategy_native_marked_equity_v1` native_c_api.h:3011 | `native_open_lots_strategy.cpp` | Pine marks at the current `close`; the native call takes the mark explicitly, so the caller says what "now" means. It equals the balance plus the open lots' `unrealized_pnl` native_host.hpp:341, which the example asserts at every calculation. | | `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.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` | The protected C++ accessor is signed (negative for losses), while the C report stores `gross_loss` as a positive magnitude. Convert C to the protected sign with `-gross_loss`; then `netprofit == grossprofit - C.gross_loss`. | | `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:2981 less the balance | `native_open_lots_strategy.cpp` | Takes the price to mark at, like every other native mark. | +| `strategy.openprofit` | `open_profit` `sha256:0490e46ca24b81c82c9549ce38af81be1ea888a60277263108cd26bf3f01e0c7` engine.hpp:1024 | `strategy_native_marked_equity_v1` native_c_api.h:3011 less the balance | `native_open_lots_strategy.cpp` | Takes the price to mark at, like every other native mark. | | `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.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. | @@ -522,22 +522,22 @@ runs close the same trades for the same money. | `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:2761 | `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:2761 | `native_selected_strategy.cpp` | Volume-weighted over the open lots. | -| `strategy.position_entry_name` | `native_open_lots` native_host.hpp:1270 then the last row's `entry_label` native_host.hpp:333 | `strategy_native_open_lot_get_v1` native_c_api.h:2802 | `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:1293 | `strategy_native_liquidation_price_v1` native_c_api.h:2989 | `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:582 | `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:1175 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:2791 | `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:2791 | `native_selected_strategy.cpp` | Volume-weighted over the open lots. | +| `strategy.position_entry_name` | `native_open_lots` native_host.hpp:1274 then the last row's `entry_label` native_host.hpp:333 | `strategy_native_open_lot_get_v1` native_c_api.h:2832 | `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:1297 | `strategy_native_liquidation_price_v1` native_c_api.h:3019 | `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:582 | `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:1179 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:582 | `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:1270 answers one `NativeOpenLot` +`native_open_lots(mark)` native_host.hpp:1274 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:2794 then -`strategy_native_open_lot_get_v1` native_c_api.h:2802 into a -`pf_native_open_lot_v1` native_c_api.h:1720, whose two strings borrow the +`strategy_native_open_lot_count_v1` native_c_api.h:2824 then +`strategy_native_open_lot_get_v1` native_c_api.h:2832 into a +`pf_native_open_lot_v1` native_c_api.h:1730, 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 @@ -550,26 +550,26 @@ native_order.hpp:445 names. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `strategy.opentrades` | `native_open_lots` native_host.hpp:1270 | `strategy_native_open_lot_count_v1` native_c_api.h:2794 | `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:1720 | `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:1720 | `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:1720 | `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:1720 | `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:1720 | `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:1720 | `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:1720 | `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:1720 | `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:1274 | `strategy_native_open_lot_count_v1` native_c_api.h:2824 | `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:1730 | `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:1730 | `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:1730 | `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:1730 | `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:1730 | `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:1730 | `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:1730 | `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:1730 | `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:1720 | `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:1730 | `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:1720 | `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:1730 | `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:1003) keeps its own sampler, so for that +(`owns_lot_excursions` native_host.hpp:1007) keeps its own sampler, so for that run the two excursion fields fold `mark` alone. ## Closed trades {#pine_to_native_map_closed_trades} @@ -591,7 +591,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:1189 | `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:1189 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:1720) 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:1189 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:1730) 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` | | @@ -625,12 +625,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:1109 inside `on_native_run_begin` native_host.hpp:853 | `strategy_native_declare_subscriptions_v1` native_c_api.h:2866 | `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:866 and the latest is pulled with `native_series_bar` native_host.hpp:1092. `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:790 and `pf_native_gaps_e` native_c_api.h:801 (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:3139 | `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:908 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:802 | `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:804 | `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:791 | `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:793 | `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:485 in `subscriptions` native_run_spec.hpp:632, or `declare_timeframe_subscriptions` native_host.hpp:1113 inside `on_native_run_begin` native_host.hpp:857 | `strategy_native_declare_subscriptions_v1` native_c_api.h:2896 | `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:870 and the latest is pulled with `native_series_bar` native_host.hpp:1096. `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:3169 | `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:912 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.currency_rate()` | none — no FX data feed | `strategy_configure_native_fx_curve_v1` pineforge.h:582 | `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. | @@ -693,7 +693,7 @@ class HelloKernel : public pineforge::NativeStrategyHost { The host is a class; the run is one `NativeRunSpec` native_run_spec.hpp:553 naming the clock, instrument, account and fees; `configure_native` -native_host.hpp:1169 applies it and `run` engine.hpp:1709 drives the bars. +native_host.hpp:1173 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: @@ -711,21 +711,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:2425 table handed to -`strategy_native_host_create_v1` native_c_api.h:2607; the run is +The host is a `pf_native_callbacks_v1` native_c_api.h:2454 table handed to +`strategy_native_host_create_v1` native_c_api.h:2637; the run is `pf_native_run_spec_v1` pineforge.h:546 plus `pf_native_run_spec_ext_v1` -native_c_api.h:2253 through `strategy_configure_native_ext_v1` -native_c_api.h:3077, and `strategy_native_run_v1` native_c_api.h:2620 drives +native_c_api.h:2265 through `strategy_configure_native_ext_v1` +native_c_api.h:3107, and `strategy_native_run_v1` native_c_api.h:2650 drives the bars into a `pf_report_t` pineforge.h:469. Three rules make the C door behave like the C++ one: 1. **Every struct is tagged and size-prefixed.** Set `struct_size` - native_c_api.h:2083 and `version` native_c_api.h:2084; a mismatch is refused + native_c_api.h:2094 and `version` native_c_api.h:2095; 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:2847. + `Failed`, readable with `strategy_native_state_v1` native_c_api.h:2877. 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. @@ -733,14 +733,14 @@ Three rules make the C door behave like the C++ one: 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:1072 (its preview has no size-prefixed POD; +native_host.hpp:1076 (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:940 is -`on_precommit`, `resolve_anchored_level` native_host.hpp:992 `on_anchored_level`, +has a C route: `validate_execution_precommit` native_host.hpp:944 is +`on_precommit`, `resolve_anchored_level` native_host.hpp:996 `on_anchored_level`, the price and opening-shape halves of `resolve_execution_terms` -native_host.hpp:927 `on_execution_terms`, `native_sized_units` -native_host.hpp:1287 `strategy_native_sized_units_v1` and the +native_host.hpp:931 `on_execution_terms`, `native_sized_units` +native_host.hpp:1291 `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 lists every public member with either its C spelling or that reason, and @@ -819,7 +819,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:747. +`MarginSideUndeclared` native_run_spec.hpp:760. 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. @@ -834,7 +834,7 @@ void on_native_run_begin() override { } ``` -`native_series_bar(0)` native_host.hpp:1092 then answers the latest completed +`native_series_bar(0)` native_host.hpp:1096 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. @@ -933,13 +933,13 @@ configuring, or stops charging what the Pine declares, fails it. 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:1169 refuses an incomplete value + them, and `configure_native` native_host.hpp:1173 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:1302. + `native_events` native_host.hpp:1306. 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. @@ -1029,8 +1029,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:1302, ownership (which opening a close consumed), and the - continuation hash `native_continuation_hash` native_host.hpp:1343. + native_host.hpp:1306, ownership (which opening a close consumed), and the + continuation hash `native_continuation_hash` native_host.hpp:1347. 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 cbf693c0c35359845b36aedb6a3a0b938d93bc29 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Fri, 25 Sep 2026 20:52:19 +0800 Subject: [PATCH 07/24] Kernel: a fill the request core cannot take off its units is the same typed refusal; the grid admits a Reduce of a FIFO boundary of its scope; the tolerance's digest fold is tagged (R5 lane K-ULP4, review fixes) A review pass of f961f73b (scratch harness, all four twins agreeing) found: 1. Under a quantity tolerance, a book of dust closed by a far larger request ({2^-55}, Reduce 1.0) settled Applied with closed 2^-55, and the request core then could not take 2^-55 off 1.0 (checked_sub_cap): PreparationError{NonrepresentableQuantity}, the run Failed with code 6, discriminator 2 -- where the exact walk refused the request, typed. 2. The same core failure already stopped exact runs on 91d65ad6: a bound opening holding 2^-55 (Transact 0.1, 0.2, 5.0; Reduce 0.3) reduced by 1.0. Fix for both: consume_matched_request answers a CoreFailure::NonrepresentableQuantity from check_execution / prepare_execution -- which check and prepare without a mutation -- as terminal(MatchRejectReason::UnrepresentableQuantity), staged and direct alike. The two C8 point-budget rows (a one-unit PointBudget on a 2^60-unit Transact) pinned the old run failure; test_native_resting_acceptance and test_native_resting_replay_contract now pin the typed refusal, and the first also that the next request fills. 3. The grid exemption matched any lot's size, at submit, for Reduce and Transact: a Book-scope Reduce of a later lot's size was taken FIFO from the on-grid head lot and split it off the grid, and a resting Transact could later open off-grid. Now CommandContext::units_are_scope_boundary: only a Reduce{ExplicitUnits} whose units are a FIFO boundary of its scope (the binary64 sum, in book order, of the scope's lots through one of them: the head lot's own size, a prefix, the whole scope; the book for an Independent request, the bound openings' lots for BindOpening / BindOpenings) is admitted; a Transact is gridded as before. A host-sized close's answered units are admitted when they are its scope's held total. 4. The tolerance folded as a bare value, so {event_retention Full} and {Window, tolerance 2^-1074} shared a digest; it now folds a tag word first. The no-tolerance digest is unchanged (the pin from 91d65ad6 holds). 5. A request of at most t on a dust head ({2^-55, 2.9}, Reduce 1e-11) ends at the dust lot's boundary; that is the documented rule (a boundary within t, zero excluded), now pinned rather than changed. ADR-0001 rule 2 as in f961f73b: (a) the core's quantity refusal is the same venue-neutral typed outcome for the one request; (b) no knob. The ADR row records the C8 move and the battery count: 48 of AUDIT4's 3,091 previously clean scenarios move (40 boundary Reduces, 6 whole-scope fractions, 2 both), each where a rule fired, no other. Witnesses: test_native_unrepresentable_refusal 404 checks (scoped-dust, the boundary cases; on 91d65ad6, spelled 10, 100 of 350 fail), test_native_quantity_tolerance 403 checks (dust-only book refused, dust head with a small request). Docs: native-settlement.md, native-engine.md, the ADR row and design SZ7 say the boundary rule and the core's refusal; citations carried from 87218e62 by the same exact line map (136 moved, two widened spans set to admit_placement_units' own lines). Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit ef180962989871e212b64c7ac3d9fdf768be0428) --- docs/adr/0001-kernel-adapter-boundary.md | 20 +-- docs/design/native-feature-parity.md | 136 +++++++++--------- docs/native-settlement.md | 43 +++--- docs/pages/native-engine.md | 27 ++-- docs/pages/pine-to-native.md | 26 ++-- include/pineforge/native_order.hpp | 25 ++-- src/engine_execution.cpp | 4 +- src/native_execution_consumer.cpp | 74 +++++++--- src/native_execution_consumer.hpp | 12 +- src/native_order.cpp | 5 +- .../relocation-manifest-v18-v19.json | 2 +- tests/test_native_quantity_tolerance.cpp | 22 ++- tests/test_native_resting_acceptance.cpp | 26 ++-- tests/test_native_resting_replay_contract.cpp | 26 ++-- tests/test_native_unrepresentable_refusal.cpp | 107 +++++++++++--- 15 files changed, 349 insertions(+), 206 deletions(-) diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index bfce512b..89af1511 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:878`); also `on_native_input` (`native_host.hpp:860`), `on_native_tick` (`native_host.hpp:863`), `on_native_timeframe_bar` (`native_host.hpp:870`), `on_native_bar_open` — at the modeled opening, before its matching pass - (`native_host.hpp:874`, `on_native_bar_open` `native_execution_consumer.cpp:6980-6982`) — + (`native_host.hpp:874`, `on_native_bar_open` `native_execution_consumer.cpp:7008-7010`) — `on_native_recalculate` for every calculation of the run (`native_host.hpp:897`), `on_native_sub_bar` (`native_host.hpp:912`) and post-fill `on_native_applied` (`native_host.hpp:922`). `on_bar` is `final` (`native_host.hpp:842`). @@ -98,7 +98,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:931`) returns no units is a `TermsUnresolved` rejection - (`TermsUnresolved` `native_execution_consumer.cpp:4894`). + (`TermsUnresolved` `native_execution_consumer.cpp:4910`). - **Observing executions.** Implement `on_native_applied` (`native_host.hpp:922`) — notifications drain FIFO after the outer callback returns — and/or poll `native_events(after_ordinal)` (`native_host.hpp:1306`). Do *not* implement @@ -109,7 +109,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:4867`) | + | Failure model | `NativeFailureCode` (`native_host.hpp:63-77`); `Failed` is latched, nothing resumes. A throwing callback latches `CallbackException` (`CallbackException` `native_execution_consumer.cpp:4883`) | | Cohorts | `cohort_open` (`native_host.hpp:1259`) / `cohort_add` (`:1262`) / `cohort_remove` (`:1265`) — the handle a `BindCohort` owner names | | Mid-callback execution | `current_execution_point` (`native_host.hpp:1064`), `inspect_current_execution` (`native_host.hpp:1076`) / `execute_current` (`native_host.hpp:1081`) | | 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:960`), `margin_check_allowed` (`native_host.hpp:972`), `resolve_margin_call_units` (`native_host.hpp:979`) — and `validate_execution_precommit` (`native_host.hpp:944`), whose `AdmitWithHostMargin` verdict hands the opening check to the host | @@ -450,7 +450,7 @@ see a mechanism, so these rows are held by reading, not by the checker): | the closed rows as the broker-state hash folds them — `BacktestEngine::trades_` under `pineforge-broker-state/v19`, through the consumer's running digest (`NativeExecutionConsumer::closed_rows_digest`, `src/native_execution_consumer.cpp`), and `NativeStrategyHost::native_closed_rows_amended` (`include/pineforge/native_host.hpp`) | the kernel books every row, at the end of the rows; the Pine host amends a booked row inside the applied notification of the execution that booked it (`PineStrategyHost::on_native_applied`: an aggregated chart dates the exit at its chart bar), and after those notifications have returned it reorders a bar's trailing same-bar bracket exits by command sequence (`sort_same_bar_exit_trades`, `src/source/pine_strategy_host.cpp`), naming the first row it moved | **a row is final once the applied notification of the execution that booked it has returned** (R5 lane V19-A). Rule 2 (a), mechanism: the finality point is the kernel's own notification boundary, the same for every host, and what a host that changes a final row supplies is data -- the first row it changed -- so a host of any venue drives the same surface and nothing in it knows Pine. Rule 2 (b), knob: none -- the call reports a change the host has already made and chooses nothing. A final row changed without a name keeps the value the digest took, and the Debug re-fold at each run's end (`verify_closed_rows`) aborts on it; rows removed from the end need no name, because the kernel only appends, so a read over fewer rows or a booking below the final mark forgets them. Every read answers the fold of the six fields v18 walked (entry and exit time and price, quantity, P&L) over the rows as they are, in their order, folded once per row per run instead of once per row per read. Pinned by part 7 of `tests/test_native_state_continuation.cpp` (kernel-only) and by every Debug ctest run, each of whose runs ends in the re-fold. | | the event record a run keeps — `NativeRunSpec::event_retention` (`include/pineforge/native_run_spec.hpp`), the journal window (`WorkingRequestCore::retire_history`, `include/pineforge/native_order.hpp`; `NativeExecutionConsumer::retire_journal`, `src/native_execution_consumer.cpp`) and the reader's acknowledgement (`NativeStrategyHost::native_acknowledge_events`, `native_event_window_start`, `include/pineforge/native_host.hpp`) | the host declares a retention in its spec and acknowledges what it has read; the Pine adapter declares `Window` in `PineExecutionAdapter::project()` and acknowledges its receipt cursor in `observe_terminal_receipts` (and "nothing read yet" at its run begin); the kernel retires at every script-bar boundary | **a reporting policy with a reader acknowledgement** (R5 lane V19-B). Rule 2 (a), mechanism: what a run keeps of its own event record, and a reader telling the kernel what it has consumed, the same for every host; the kernel never retires an event its own live state reads (a queued applied notification, a live deferred group-adjustment chain), and what the journal used to answer beyond that is state (next row). Rule 2 (b), knob: a spec field with no TradingView meaning -- `Window`, `Commands`, `Full` -- folded into the spec digest only off the default; a C caller that does not send the word keeps `Full`, the record its layout was published with. No trade, and no Pine value, depends on it: the adapter reads above its own cursor only. Pinned by `tests/test_native_event_retention.cpp` (kernel-only: `Full` row for row against the pre-window record, the window and the acknowledgement's edges, and the same books booking the same trades with the window closing at every driver point) and `check_event_retention` of `tests/test_native_c_api.c`. | | 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`); the quantity grid's own quantities (`CommandContext::units_are_lot_quantity`, and a `ScopeFraction` whose product is its scope, unfloored); 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; on a quantity grid a whole-scope fraction and a one-lot close that were floored or `OffGrid` now settle -- 54 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`. | +| 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, and a `ScopeFraction` whose product is its scope, unfloored); 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`. | 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 (→ @@ -591,7 +591,7 @@ number names its symbol on this tree. | surface | ruling | reason | |---|---|---| | `cancel_all` (`native_host.hpp:1244`) / `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:2744`). | -| `cohort_add` (`native_host.hpp:1262`) / `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:870`, 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:1262`) / `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:872`, 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:1291`) / `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:3019`). | | `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:1681`) 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`. | @@ -601,7 +601,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:3961`) | **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:582`) | **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:870`) and `NativeTimeframeBarContext::interval` (`native_host.hpp:794`) | **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:7559-7571`); 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:1065-1070`) 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:7587-7599`); 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:1067-1072`) sees the same values, so no hash contract moves. | | `pf_native_lot_excursion_v1` (`native_c_api.h:1867`) | **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) @@ -659,7 +659,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: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` | | `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:8516-8519` (lookahead) and `clear_if_gapped` `:8510-8513` (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:8544-8547` (lookahead) and `clear_if_gapped` `:8538-8541` (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:2226`, `::gaps` `include/pineforge/native_c_api.h:2230` | 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` | @@ -674,9 +674,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:7116`, presenting `NativeDecisionContext::in_session` `include/pineforge/market_driver.hpp:155` 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:7791-7807` | 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:7768-7789` | 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:6519` | 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:7819-7835` | 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:7796-7817` | 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:6547` | 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 diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index c938b939..8f19a738 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:1843, *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:1846, *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). | @@ -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:2678-2689; `entry()` pine_adapter.cpp:5082, `order()` pine_adapter.cpp:10270) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5595-5642; `submit` native_host.hpp:1190 | 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:8133 → `Flatten{}` pine_adapter.cpp:8162-8163; `Reduce{ExplicitUnits}` pine_adapter.cpp:7280-7289; `ReverseTo` pine_adapter.cpp:6114-6115) | **yes** — `OrderIntent` native_order.hpp:229, reduce sizes native_order.hpp:229-236, `ReverseTo` native_order.hpp:60-62, `OpeningShape` native_order.hpp:904-908, `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:2648-3707 → `host.replace` pine_adapter.cpp:2664-2744) | **yes** — `replace` native_host.hpp:1195-1197, `cancel` native_host.hpp:1226; `ReplacedEvent` native_order.hpp:1007-1021; result and event types `SubmitStatus` native_order.hpp:835-861, `ReplacedEvent` native_order.hpp:991-1057 | 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:4515-4561, `cancel_all` pine_adapter.cpp:17806-17821; `live_by_source_key_` pine_adapter.hpp:1983; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:3857-3922) | **partial** — single-handle `cancel` native_host.hpp:1226; no enumeration (`WorkingRequestCore::live()` is consumer-internal native_order.hpp:1664); labels exist but are unindexed (`Request::label` native_order.hpp:485) **Closed:** `cancel_all` native_host.hpp:1244, `cancel_where(comment)` native_host.hpp:1249 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1249 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1240 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. | +| OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:2678-2689; `entry()` pine_adapter.cpp:5082, `order()` pine_adapter.cpp:10270) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5625-5672; `submit` native_host.hpp:1190 | 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:8133 → `Flatten{}` pine_adapter.cpp:8162-8163; `Reduce{ExplicitUnits}` pine_adapter.cpp:7280-7289; `ReverseTo` pine_adapter.cpp:6114-6115) | **yes** — `OrderIntent` native_order.hpp:229, reduce sizes native_order.hpp:229-236, `ReverseTo` native_order.hpp:60-62, `OpeningShape` native_order.hpp:907-911, `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:2648-3707 → `host.replace` pine_adapter.cpp:2664-2744) | **yes** — `replace` native_host.hpp:1195-1197, `cancel` native_host.hpp:1226; `ReplacedEvent` native_order.hpp:1010-1024; result and event types `SubmitStatus` native_order.hpp:835-861, `ReplacedEvent` native_order.hpp:994-1060 | 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:4515-4561, `cancel_all` pine_adapter.cpp:17806-17821; `live_by_source_key_` pine_adapter.hpp:1983; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:3857-3922) | **partial** — single-handle `cancel` native_host.hpp:1226; no enumeration (`WorkingRequestCore::live()` is consumer-internal native_order.hpp:1667); labels exist but are unindexed (`Request::label` native_order.hpp:485) **Closed:** `cancel_all` native_host.hpp:1244, `cancel_where(comment)` native_host.hpp:1249 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1249 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1240 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:3982; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:6965-7316) | **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 | — | -| 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:5303-5338, `cohort_for` pine_adapter.cpp:2442; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7263-7280) | **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:1259, `CancelReason::OwnerGone` native_order.hpp:943 | 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:2687-2693) | **yes** — `Group` / `GroupEffect` native_order.hpp:466-473, `CancelReason::Group` native_order.hpp:942-947, `ReservationReducedEvent` native_order.hpp:1166-1176 (deferred form native_order.hpp:1166-1176) | 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:5303-5338, `cohort_for` pine_adapter.cpp:2442; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7263-7280) | **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:1259, `CancelReason::OwnerGone` native_order.hpp:946 | 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:2687-2693) | **yes** — `Group` / `GroupEffect` native_order.hpp:466-473, `CancelReason::Group` native_order.hpp:945-950, `ReservationReducedEvent` native_order.hpp:1169-1179 (deferred form native_order.hpp:1169-1179) | 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:1081-1082 | 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:12779; lowering `submit_one` pine_adapter.cpp:8830-8842, `BindCohort` pine_adapter.cpp:9331-9337, `submit_leg` pine_adapter.cpp:9355-9379; 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:1843); 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:8435-8438; `materialize_relative_exits` pine_adapter.cpp:17096-17137, pine_adapter.hpp:1812-1824) | **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:5521-5555. `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:996 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. | +| OL10 | TP/SL bracket per entry, armed before the entry fills, OCA between legs (`exit()` pine_adapter.cpp:12779; lowering `submit_one` pine_adapter.cpp:8830-8842, `BindCohort` pine_adapter.cpp:9331-9337, `submit_leg` pine_adapter.cpp:9355-9379; 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:1846); 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:8435-8438; `materialize_relative_exits` pine_adapter.cpp:17096-17137, pine_adapter.hpp:1812-1824) | **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:5551-5585. `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:996 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:18857-18886) | **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:1240 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:1985-2003); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1116-1195); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3261-3428, `materialize_snapshot` pine_adapter.cpp:10670-10694), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1580-1603, `CloseCallsiteState` pine_adapter.hpp:1674-1689; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15406, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15640) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:656-658 | 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:1985-2003); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1116-1195); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3261-3428, `materialize_snapshot` pine_adapter.cpp:10670-10694), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1580-1603, `CloseCallsiteState` pine_adapter.hpp:1674-1689; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15406, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15640) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:657-659 | 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: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:855-887) | n/a | A | — | S:R4 O:A10 | TV half of OL9. **V19D-P1 (R5 lane B-ADAPTER):** a margin-call revival never restores an exit the script withdrew: `strategy.cancel` / `cancel_all` retire every leg of a dormant exit's lifecycle (`retire_cancelled_dormant_exits` pine_adapter.cpp:1795) and the revival skips it (`revive_brackets_after_margin` pine_adapter.cpp:1856), as the lab tv tapes badapter-v19dp1-control / -cancel (NYSE:F 15m) decide it — replayed by `tests/test_adapter_margin_revival_cancel.cpp` | ### 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:5607-5629; `NativeTrailState` native_host.hpp:657-662, `trail_state` native_host.hpp:1069 | 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:5637-5659; `NativeTrailState` native_host.hpp:657-662, `trail_state` native_host.hpp:1069 | 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:11554-11566) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4429-4434) **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:11467-11604) stays A. | +| TR3 | Zero-distance trail, "ride the best, exit on any adverse tick" (adapter substitutes `tick * 0.5` pine_adapter.cpp:11554-11566) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4425-4430) **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:11467-11604) stays A. | | TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:3814-3860) | **partial** — the read exists `trail_state` native_host.hpp:1069; `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:9381-9386, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11467-11604, retained best `retained_trail_best` pine_adapter.hpp:287-289, operand / restart rules `source_trail_points` pine_adapter.cpp:8405-8631, `trail_one_shot` pine_adapter.cpp:9470-9599 | n/a | A | — | F:B3 S:T2 | R5-1 | @@ -77,12 +77,12 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | SZ1 | Explicit units (`Transact{signed}` after a grid floor pine_adapter.cpp:5692-5696, pine_adapter.cpp:5692) | **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:10705-11882) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:929-933, invoked native_execution_consumer.cpp:4852-4871, guarded native_execution_consumer.cpp:4852-4887 | 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:2536-2591) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4894-4896 **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:2488, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:453 (→ SZ10) | +| SZ2 | Host override of the resolved price / units (`resolve_terms` pine_adapter.cpp:10705-11882) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:929-933, invoked native_execution_consumer.cpp:4868-4887, guarded native_execution_consumer.cpp:4868-4903 | 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:2536-2591) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4910-4912 **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:2488, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:453 (→ SZ10) | | SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:2497-2499) | **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:2549, `typed_entry_units` pine_adapter.cpp:2567; `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:2528; `percent_commission_live_equity` pine_adapter.cpp:4121-4161) | **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:2454-2469, `PineSizingSnapshot` pine_adapter.hpp:147-154; fill-time exceptions `default_sized` pine_adapter.cpp:6435-6461) | **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:1385-1402; host-sized terms snap (`ExecutionGridPolicy` native_order.hpp:71-85); exact grid validation `execution_terms_grid_representable` native_execution_consumer.cpp:4909-4912; the book's own quantities are on the grid -- a whole-scope `ScopeFraction` and a close of one lot's size (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:1388-1405; host-sized terms snap (`ExecutionGridPolicy` native_order.hpp:71-85); exact grid validation `execution_terms_grid_representable` native_execution_consumer.cpp:4923-4926; 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:12253-12281, `compute_exit_reservation` pine_adapter.cpp:4204-4325) | **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:4256-4579, `reserved_exit_qty` pine_adapter.cpp:8757-9067) | **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:444-451, `source_money_floor_lot` pine_adapter.cpp:453; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:4236-4251) | 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:2302) | **yes** — `allowed_open_directions` native_run_spec.hpp:596 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2925-2933 | 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:2939-2942 | 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:13001-13004) is TV. | -| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:595 → `MaxOpenLots` native_execution_consumer.cpp:2944-2947 | 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:5970-6016, cap left unset `max_open_lots` pine_adapter.cpp:2301-2303) is TV. | -| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13055-13120 and leaves the kernel gate unset pine_adapter.cpp:13054-13056) | **yes** — `initial_margin_fraction` native_run_spec.hpp:597; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2962 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:2806 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. | +| MG1 | Allowed opening directions (`allowed_open_directions` pine_adapter.cpp:2302) | **yes** — `allowed_open_directions` native_run_spec.hpp:596 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2921-2929 | 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:2935-2938 | 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:13001-13004) is TV. | +| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:595 → `MaxOpenLots` native_execution_consumer.cpp:2940-2943 | 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:5970-6016, cap left unset `max_open_lots` pine_adapter.cpp:2301-2303) is TV. | +| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13055-13120 and leaves the kernel gate unset pine_adapter.cpp:13054-13056) | **yes** — `initial_margin_fraction` native_run_spec.hpp:597; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2958 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:2808 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. | | MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13710-13711, `margin_pct` pine_adapter.cpp:13118) | **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:13810-13988, `submit_margin_call_units` pine_adapter.cpp:13949-13995, `schedule_margin_call_path` pine_adapter.cpp:17959-18036) | **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:2921 **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:13810-13988, `submit_margin_call_units` pine_adapter.cpp:13949-13995, `schedule_margin_call_path` pine_adapter.cpp:17959-18036) | **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:2917 **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:13741-13777) | **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:979 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:179-200) | **no** **Closed:** `native_liquidation_price()` native_host.hpp:1297 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:16057-16073, `apply_fx_opening_margin_slice` pine_adapter.cpp:18037-18065) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1179 but no margin re-check. **Closed by audit lane N6:** `NativeMarginCheckKind::FxRoll` — a step of the declared curve is a check point of its own, measured at the unchanged price before the first point the new rate converts (`tests/test_native_margin_fx_roll.cpp`). The adapter refuses the point by kind, for a measured reason since R5 lane F7: TradingView's rollover is a broker-open checkpoint at the open price, now at any positive margin (`apply_fx_open_margin_slice`, on the one TradingView money / units rule every other checkpoint uses), while the roll point measures at the remaining path's adverse mark and rests the slice there — a carried 1x long on a step bar O 100 L 97 books 0.4116 @ 97 on the adapter's run shape and 0.4036 @ 99, measured a bar early, on a strict calendar, against TradingView's 0.3996 @ 100 (`tests/test_native_margin_hooks_twin.cpp` MG-FX). Both N6 follow-ups are closed (§4.4). **Completed by R5 follow-up lane E3:** the conversion INSTANT is now generic too — every check kind converts at its own cursor's rate (`account_currency_fx_at(cursor.point.effective_time_ms)`) for the requirement, the equity and the level, in batch and stream, so a step landing between a fill's cursor and the clock the engine presents no longer breaches at the fill (`tests/test_native_margin_fx_clock.cpp`; N6's local clock swap inside `fx_roll_margin_check` is removed as redundant). **Extended by R5 follow-up lane E21** to the opening gate's placement site (MG4), and the rate-explicit equity E3 restated beside the engine's accessor is now that accessor's one implementation, `BacktestEngine::marked_equity_at` engine.hpp:729 | K | L4 → N6 → E3 → E21 | O:D8 | (single-source) | @@ -115,15 +115,15 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| CT1 | Calculate at script-bar close | **yes** — `on_native_bar` native_host.hpp:878, dispatched native_execution_consumer.cpp:7118-7139 (call site `sha256:fc29ccd415732193f24f2370c6aca8d6f8910bca5b95315e63e88f2befe8200b` native_execution_consumer.cpp:7129) | K | — | F:E1 O:F1 | — | -| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2259-2260) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7438-7442 | K | — | F:E2 O:F6 S:C1 | — | -| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:15893) | **yes** — `on_native_bar_open` native_host.hpp:874 runs before the open match native_execution_consumer.cpp:6980-6984; `execute_current` legal native_host.hpp:1081 | 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:6923 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:6974 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1050 is the lookahead-free bar so far at the cursor, 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:878, dispatched native_execution_consumer.cpp:7146-7167 (call site `sha256:fc29ccd415732193f24f2370c6aca8d6f8910bca5b95315e63e88f2befe8200b` native_execution_consumer.cpp:7157) | K | — | F:E1 O:F1 | — | +| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2259-2260) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7466-7470 | K | — | F:E2 O:F6 S:C1 | — | +| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:15893) | **yes** — `on_native_bar_open` native_host.hpp:874 runs before the open match native_execution_consumer.cpp:7008-7012; `execute_current` legal native_host.hpp:1081 | 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:6951 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7002 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1050 is the lookahead-free bar so far at the cursor, 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:860, `NativeInputContext` native_host.hpp:758-763 | 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:681; `begin_coof_recalc` pine_adapter.cpp:5400, `flush_coof_tail` pine_adapter.cpp:16035, `end_coof_recalc` pine_adapter.cpp:5421) | **partial** — `on_native_applied` native_host.hpp:922 fires mid-path after each fill native_execution_consumer.cpp:6834, native_execution_consumer.cpp:6813-6843; commands are legal there `commands_allowed` native_execution_consumer.cpp:1386-1394; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5521-5555, `remaining_path_delivery` native_order.cpp:2039-2047. 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:863 fires per accepted realtime print native_execution_consumer.cpp:7062-7066; 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:284-286. | -| 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:7432-7549 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:912 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:2389-2445) | **yes** — `IntrabarPath` native_run_spec.hpp:362-402, delivery native_execution_consumer.cpp:7376-7567; `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. | +| 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:681; `begin_coof_recalc` pine_adapter.cpp:5400, `flush_coof_tail` pine_adapter.cpp:16035, `end_coof_recalc` pine_adapter.cpp:5421) | **partial** — `on_native_applied` native_host.hpp:922 fires mid-path after each fill native_execution_consumer.cpp:6862, native_execution_consumer.cpp:6841-6871; commands are legal there `commands_allowed` native_execution_consumer.cpp:1388-1396; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5551-5585, `remaining_path_delivery` native_order.cpp:2038-2046. 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:863 fires per accepted realtime print native_execution_consumer.cpp:7090-7094; 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:284-286. | +| 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:7460-7577 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:912 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:2389-2445) | **yes** — `IntrabarPath` native_run_spec.hpp:362-402, delivery native_execution_consumer.cpp:7404-7595; `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:233-319) 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:5547), 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:681, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1655-1662 | n/a | A | — | F:E4 O:F5 S:C2 | R5-5: the adapter keeps its path | @@ -132,37 +132,37 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | HT1 | HTF series of the same symbol (`request.security`): evaluators registered `scheduler_configure_security_evaluators` pine_strategy_host.cpp:1513-1516, pumped from the scheduler `scheduler_prepare_security_sequence` pine_strategy_host.cpp:1421-1577; 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:1514 **Closed:** `NativeRunSpec::subscriptions` (`NativeTimeframeSubscription` native_run_spec.hpp:485), or `declare_timeframe_subscriptions` native_host.hpp:1113 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:870 and `native_series_bar(index)` native_host.hpp:1096 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:1417-1433 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, sole caller pine_strategy_host.cpp:1546; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1435-1440 **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:8186 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 c_abi.cpp:861) | **partial (inert)** — pre-run staging is accepted `refuse_source_mutation` native_execution_consumer.cpp:1419-1435 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, sole caller pine_strategy_host.cpp:1546; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1437-1442 **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:8214 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). | | 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:912 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:860; the Pine scheduler asks the kernel's `script_bucket_completions` native_execution_consumer.cpp:7791-7808, which feeds the same class `TimeframeAggregator` native_execution_consumer.cpp:7798, 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:860; the Pine scheduler asks the kernel's `script_bucket_completions` native_execution_consumer.cpp:7819-7836, which feeds the same class `TimeframeAggregator` native_execution_consumer.cpp:7826, 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:5595-5642, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:6178-6186; `Limit::fill_through` native_order.hpp:239-244 enforced native_execution_consumer.cpp:5003-5016, re-checked after host terms native_execution_consumer.cpp:5003-5023 | 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:4576-4577, clamp native_execution_consumer.cpp:4576 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:11721-11730) 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:5625-5672, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:6208-6216; `Limit::fill_through` native_order.hpp:239-244 enforced native_execution_consumer.cpp:5017-5030, re-checked after host terms native_execution_consumer.cpp:5017-5037 | 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:4572-4573, clamp native_execution_consumer.cpp:4572 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:11721-11730) is A | | FP3 | Commission: percent / cash per unit / cash per execution (`fee_kind_for` pine_adapter.cpp:463-470) | **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:256-259, `source_bar_fill` pine_adapter.cpp:11737-11746, `directional_tick` pine_adapter.cpp:301-305) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4038; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6260-6261; 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:256-259, `source_bar_fill` pine_adapter.cpp:11737-11746, `directional_tick` pine_adapter.cpp:301-305) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4034; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6290-6291; 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:332-380) 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:1179 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:8616). **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:1179 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:8644). **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:339-387 (used `source_trigger_threshold` pine_adapter.cpp:8822-8827, `exit_limit_trigger` pine_adapter.cpp:9356-9377), `source_bar_fill_tick` pine_adapter.cpp:277-290, raw-vs-booked spelling pine_adapter.cpp:11220-11326, margin-call fill pricing pine_adapter.cpp:13786-13795 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:11446) | 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:2589-2602, closing-row fold `max_runup` engine_orders.cpp:157-171; `owns_lot_excursions` native_host.hpp:1005-1009 | 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:2591-2604, closing-row fold `max_runup` engine_orders.cpp:157-171; `owns_lot_excursions` native_host.hpp:1005-1009 | 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). | | 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:1713) | **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:1438) stay A, measured in §3.7 part 2. | | 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:9190, 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:1347, native_execution_consumer.cpp:10589; 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:9218, 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:1347, native_execution_consumer.cpp:10617; 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:1955 **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` in batch and across a stream's warmup and realtime legs. 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. What a recorded row costs — quadratic on the Pine adapter, O(closed rows) per row in the kernel — is measured and ruled in §3.8 | | 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:2157, `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:546 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 57 `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 symbols are a second, disjoint inventory beside the 57 codegen-facing ones (`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:155 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:7182), 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:1794 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:155 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:7210), 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:1794 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:287-289) | 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:743; the source host casts it back `overrides_opaque` pine_strategy_host.cpp:291-294 | 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:887-889 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1822-1825; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1855, `:8677`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2249-2250. 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:893-895 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1824-1827; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1857, `:8705`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2249-2250. 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:445-451) | 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:6331 | 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 native_execution_consumer.cpp:6361 | 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) | @@ -262,8 +262,8 @@ PF_API int pf_native_events_v1(pf_strategy_t, uint64_t after_ordinal, pf_native_ | Point | Decision | Src | |---|---|---| | Implementation | One new kernel TU (F `src/native_c_host.cpp`, O `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:2939-2949 | F O | -| Legality | The existing rule (`commands_allowed` native_execution_consumer.cpp:1386-1393): 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 `sha256:27ab3bf715fdc1eb1b9183cd41b2b8e1cf0d7cb38c1a313a371b285991bcc0b7` native_execution_consumer.cpp:2935-2945 | F O | +| Legality | The existing rule (`commands_allowed` native_execution_consumer.cpp:1388-1395): 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 | @@ -276,8 +276,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:643-657, `contribute_input` native_execution_consumer.cpp:7925, `seal_script` native_execution_consumer.cpp:7892) 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:7012), 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:649-663, `contribute_input` native_execution_consumer.cpp:7953, `seal_script` native_execution_consumer.cpp:7920) 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:7040), 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 | @@ -285,7 +285,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 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:1334-1418 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. 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:1435); `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:1437); `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). 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". @@ -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:2905-2972) 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:2906-2973) 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). @@ -336,7 +336,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:9060-9100 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:9088-9128 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:1050; and `set_native_security_feed` documented as the pre-run door for a series' authoritative bars, refused in-run | --- @@ -345,9 +345,9 @@ 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:2141-2278) 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:1625-1629). 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:e3aa487ce8d69db9054576f75839345c0acaab55c3fc0a374c2f9c8c071559ec` AGENTS.md:132-145). A lane that cannot be shown neutral by construction does not merge. +- **(a) NEUTRALITY.** Every new behaviour is opt-in by a spec field or a new request kind. The adapter's `project()` (pine_adapter.cpp:2141-2278) 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:1631-1635). 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:e3aa487ce8d69db9054576f75839345c0acaab55c3fc0a374c2f9c8c071559ec` AGENTS.md:132-145). A lane that cannot be shown neutral by construction does not merge. - **(b) TWIN.** One chosen probe per lane runs through the adapter AND a hand-written `NativeStrategyHost` on the same bars and spec. Trade lists are diffed **by identity** (entry / exit time, price, qty, pnl, commission), never by trade number; then event, ownership and hash detail (S's D → E order). A difference is allowed only where a named TV-quirk row of §1 is active, and each one is itemized. Because 430 test files 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:1272). Every new test unit compiles first against the frozen previous-epoch header closure (fail-before, `CLAUDE.md`). +- **(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:1275). 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. ### 3.2 Lanes: scope, size, dependencies, epochs @@ -406,7 +406,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:2298, 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:5526-5530) and `drain_applied_notifications` (native_execution_consumer.cpp:6187-6200) 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:5556-5560) and `drain_applied_notifications` (native_execution_consumer.cpp:6217-6230) 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:1334-1418 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) | @@ -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:4894-4896). 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 (`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:4910-4912). 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:3279-3468 +`sha256:8c97d3e1d9d6f036a9be0b50a3a43188f1e2eee23da0384fbed242829108c3c3` native_run_spec.hpp:239-302 and `sha256:efad6b9c7bcd3f7d0e459d434ef088a61b18c546d53cfb107f481a227fbe532b` native_execution_consumer.cpp:3275-3464 (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 | @@ -1158,10 +1158,10 @@ 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:942-946); test that a default spec hashes to the pre-change constant | 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 `sha256:620926caaa868e64ede32f547ac1dd0bfe81c9758267593a9a924051208df0a4` native_execution_consumer.hpp:948-952); 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:12916-12983, pine_adapter.cpp:13030); its chronology lives at `sha256:bb89768c9a7b2dc9f6de5943c04de8103ed107c21bb6e184446de0e6a84cf4c2` pine_adapter.cpp:13589-13666 and its excursion sample differs by chronology (`sha256:764a1a69f2940863b4f77cbf9d20267a19ec38e86720796c7081a59ac7245427` pine_strategy_host.cpp:729-759) | 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:2296-2336) | 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:5547) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `sha256:86617ca43bb2847380f1845b07cc9ecd27059814ee80fecf80649a9393dfe8c8` native_execution_consumer.cpp:3456-3465 and `drain_applied_notifications` (native_execution_consumer.cpp:6187-6200); new behaviour only behind new opt-ins | F | +| E4 | **Calc on fill.** Kernel suffix eligibility is continuous along the segment; TV refills only at O/H/L/C waypoints (`sha256:ddad8dc42e078838c0d53fd49e50c31060221e3b82e7d5bbaa0420769daa7d31` pine_scheduler_native.cpp:585-637, `coof_next_waypoint` pine_adapter.cpp:5547) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `sha256:86617ca43bb2847380f1845b07cc9ecd27059814ee80fecf80649a9393dfe8c8` native_execution_consumer.cpp:3452-3461 and `drain_applied_notifications` (native_execution_consumer.cpp:6217-6230); 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:15893-15921) | 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:1600-1619) | 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:332-380) 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 | @@ -1229,16 +1229,16 @@ 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:7106, native_execution_consumer.cpp:7106, native_execution_consumer.cpp:7106) 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:7134, native_execution_consumer.cpp:7134, native_execution_consumer.cpp:7134) 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:2298), 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:1007). 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:4994-5036), `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:5008-5050), `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) ```cpp -// native_order.hpp — native_order_v6; OrderIntent grows 5 -> 6 (static_assert native_order.hpp:1352) +// native_order.hpp — native_order_v6; OrderIntent grows 5 -> 6 (static_assert native_order.hpp:1355) struct CashValue { double cash = 0.0; }; // account currency struct EquityFraction { double fraction = 0.0; }; // of marked equity at the sizing point (0.10 = 10 %) using SizeBasis = std::variant; @@ -1256,7 +1256,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:673; terms resolution `sha256:b06b928e9f12217d7d3036f6adf77250c691c2ab1ad69d48e87417dbdfe1adc7` native_execution_consumer.cpp:2931-2977). 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:675; terms resolution `sha256:b06b928e9f12217d7d3036f6adf77250c691c2ab1ad69d48e87417dbdfe1adc7` native_execution_consumer.cpp:2927-2973). 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. @@ -1283,9 +1283,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:12912-12933). New hashed `RequestDefinition::origin { Host, KernelLiquidation, KernelRisk }`, new `CancelReason::Superseded` on re-price, `MarginCallEvent` joins `CommandEvent` (17 → 18, native_order.hpp:1272). O's alternative, a checkpoint on the excursion walk (`sha256:ed6f4ff668195caf3c953c8ef5dd448c6010949353f7b9ef44d50b764f7488db` native_execution_consumer.cpp:1884-1897), 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:12912-12933). New hashed `RequestDefinition::origin { Host, KernelLiquidation, KernelRisk }`, new `CancelReason::Superseded` on re-price, `MarginCallEvent` joins `CommandEvent` (17 → 18, native_order.hpp:1275). O's alternative, a checkpoint on the excursion walk (`sha256:ed6f4ff668195caf3c953c8ef5dd448c6010949353f7b9ef44d50b764f7488db` native_execution_consumer.cpp:1886-1899), 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:4644-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:4636-4649 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) @@ -1305,10 +1305,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:1612-1613) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6896-6908) 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:3469-3478). `born_on_remaining_path` (native_execution_consumer.cpp:5526-5530) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1655-1662) 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:1058-1065, whose body is native_execution_consumer.cpp:6746-6753). `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:2533); 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:1613-1614) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6924-6936) 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:3465-3474). `born_on_remaining_path` (native_execution_consumer.cpp:5556-5560) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1655-1662) 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:1064-1071, whose body is native_execution_consumer.cpp:6774-6781). `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:2535); 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) @@ -1346,7 +1346,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 (`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:3525-3530) +// 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:3521-3526) // 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; @@ -1358,7 +1358,7 @@ BracketReceipt submit_bracket(NativeStrategyHost&, const BracketSpec&); // emi template class OrderBook; // id -> handle: submit-or-replace, cancel-by-id ``` -- A leg with owner `WaitForApplied` / `BindOpening` and a non-absolute anchor gets its level when the owner's fill arms it (`ArmedEvent` native_order.hpp:1210-1217 already exists) (O). Fallback with no kernel change: the toolkit materializes relative levels from the host's `on_native_applied` (F). +- A leg with owner `WaitForApplied` / `BindOpening` and a non-absolute anchor gets its level when the owner's fill arms it (`ArmedEvent` native_order.hpp:1213-1220 already exists) (O). Fallback with no kernel change: the toolkit materializes relative levels from the host's `on_native_applied` (F). - Gap lane N13 (after R4d): two facts of the arm were missing and are now opt-in fields appended to `WaitForApplied`, each default byte-identical and folded only when set. `NativeArmFirstMatch { AtArmPrint, AfterArmPrint }`: whether the armed leg is a candidate at its owner's fill print or, like a request born in the owner's fill callback, only after it. `NativeArmScope { OwnerLot, Book }`: whether an armed closing leg closes the lot its owner opened or is bound at the arm to the whole position (a `BookClose`), under which it may be `HostSized`. Ruling: generic — both are broker models of a contingent child, neither names a source language — and they are what makes an anchored child the very request a host would submit from `on_native_applied`. The Pine adapter's relative `strategy.exit` legs use both; R4d's owner-lot spelling left a later same-id add open where the source leg closes the book (`rel-pyramid-set-once`, tests/test_adapter_brackets_relower.cpp). - Not adopted (R5-2): S's kernel bracket object, its C symbols `strategy_native_bracket_*` and its TU list (`engine_orders.cpp`, `engine_execution.cpp`). - TUs: `native_order.{hpp,cpp}` (arming), `native_matching.hpp` (`checked_trail_stop`), `native_execution_consumer.cpp` (`prepare_owner_applied` path, command surface). @@ -1374,7 +1374,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:3927-3949, `execute_current` path native_execution_consumer.cpp:3747-3757), `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:3923-3945, `execute_current` path native_execution_consumer.cpp:3743-3753), `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). ### A.8 L9 — risk limits (F P3 + O G7 + S P5) @@ -1394,7 +1394,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:1306) (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. - 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:13465-13563); 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:4644-4688 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:4636-4649 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 987463bc..7b38ee5f 100644 --- a/docs/native-settlement.md +++ b/docs/native-settlement.md @@ -50,9 +50,15 @@ ends THAT request with a terminal `MatchRejectedEvent` whose reason is the run goes on; the host submits again if it still wants the trade (R5 lane K-ULP4). Until then the consumer turned the same inspection into a durable `SettlementFailure` of the whole run -- lifecycle `Failed`, code 6, -discriminator 5, "native settlement inspection failed". Every other status an -inspection can answer (`InvalidBook`, `InvalidPrice`, `InvalidAccounting`, ...) -is a broken book, price or account, and still stops the run. +discriminator 5, "native settlement inspection failed". A fill the request's +own units cannot absorb is the same typed refusal: the request core checks, +before anything is written, that the fill can be taken off the request's +remaining units and point budget, and a scope of dust closed by a far larger +request (a bound opening holding `2^-55`, reduced by 1.0) or a one-unit budget +on a `2^60`-unit request cannot (it stopped the run with code 6, +discriminator 2). Every other status an inspection can answer (`InvalidBook`, +`InvalidPrice`, `InvalidAccounting`, ...) is a broken book, price or account, +and still stops the run. A close that spans lots and ends inside one of them is charged exactly its units. Every lot before the one it ends in closes whole, and C is the binary64 @@ -111,13 +117,16 @@ a `settlement_readiness` of `UnrepresentableQuantity`; and `Flatten` is never refused for a quantity. On a quantity grid (`NativeRunSpec::quantity_grid`) the book's own quantities are on the grid: a `ScopeFraction` whose product is its scope -- `fraction == 1` -- resolves to the scope's held total, which the grid -does not floor, and a close of exactly one lot's binary64 quantity is admitted, -at submit as a `Reduce` or a closing `Transact` and as a host-sized close's -answered units (R5 lane K-ULP4). Before, such a fraction was floored onto the grid -and closed up to a step short, left a dust lot, or found nothing to close, and -the lot's own size was `OffGrid`. A close of a FIFO prefix's sum or of the -held total of a multi-lot book is still gridded; `Flatten` and -`ScopeFraction{1}` are its spellings. +does not floor; a `Reduce` whose units are a FIFO boundary of its scope -- the +binary64 sum, in book order, of the scope's lots through one of them: the head +lot's own size, a prefix, the whole scope -- is admitted at submit and closes +whole lots; and a host-sized close answered with its scope's held total is +admitted at the candidate (R5 lane K-ULP4). Before, such a fraction was floored +onto the grid and closed up to a step short, left a dust lot, or found nothing +to close, and the book's own sizes were `OffGrid`. A `Transact` is still +gridded (it can open), and so is a `Reduce` of a quantity that is no boundary +of its scope: a later lot's size would be taken FIFO from the head lot and +split it off the grid. ## Quantity tolerance @@ -133,9 +142,10 @@ folds the value only when it is present. Under it: scope's lots through one of them -- ends at that boundary and is charged its request, as a split its rows cannot add up to is (K-ULP2). If the lot the request ends in would keep at most `t`, it closes whole; if a whole lot - leaves a rest of at most `t` and the next member lot is larger than `t`, - that lot is not touched; if the next lot is itself at most `t`, it closes - whole instead, so dust behind a boundary is taken rather than left. The + leaves a rest above zero and at most `t` and the next member lot is larger + than `t`, that lot is not touched; if the next lot is itself at most `t`, it + closes whole instead, so dust behind a boundary is taken rather than left (a + request exactly at a boundary still ends there, as the exact walk does). The scope's end is a boundary too: a `Reduce` or a crossing `Transact` at most `t` past the book closes the book and opens nothing. `{0.19999999999999996, 2.9}` reduced by 0.2 closes the first lot, charged 0.2, and keeps 2.9. @@ -146,9 +156,10 @@ folds the value only when it is present. Under it: same-side opening stays as it is, beside the new lot. - What no tolerance makes a quantity stays refused, typed as above: a rest below half an ulp of the first lot it meets (a request of at most `t` against - a larger lot -- zero is not a boundary to snap to), a rest above `t` that a - lot or the running sum absorbs, a reduction the position absorbs, and an - opening the book absorbs. + a larger lot -- zero is not a boundary to snap to; a dust lot's own boundary + is one), a rest above `t` that a lot or the running sum absorbs, a reduction + the position absorbs, an opening the book absorbs, and a book or scope of + dust closed by a far larger request, whose units the dust cannot move. The charge is the request: a snapped close's `closed_units` and `filled_working` are its units, and its rows add up to the boundary, at most diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 49c81c18..1b4a025c 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -458,7 +458,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:9060-9100`), which admits the begin, checks the +(`native_execution_consumer.cpp:9088-9128`), 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 @@ -622,13 +622,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:5526-5530`). Requests accepted before the +`native_execution_consumer.cpp:5556-5560`). 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:6980-6982`). **Lookahead warning:** the +pass (`native_execution_consumer.cpp:7008-7010`). **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:6872`), the +`engine.current_bar_ = open_view` (`native_execution_consumer.cpp:6900`), 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 @@ -639,10 +639,11 @@ callback's bar down to its open. Both are under *The bar so far, and the open-bar view* below. A `quantity_grid`, when present, admits Transact/Reduce quantities on the -exact binary64 grid in `native_order.hpp`, and a close -- a `Reduce`, or a -`Transact` against the book's side -- of exactly one lot's own binary64 -quantity, which settlement arithmetic can move off any decimal grid -(`CommandContext::units_are_lot_quantity`, R5 lane K-ULP4). Flatten is not +exact binary64 grid in `native_order.hpp`, and a `Reduce` whose units are a +FIFO boundary of its scope -- the binary64 sum, in book order, of the scope's +lots through one of them: the head lot's own size, a prefix, the whole scope -- +which settlement arithmetic can move off any decimal grid +(`CommandContext::units_are_scope_boundary`, R5 lane K-ULP4). Flatten is not gridded. Rejection does not rewrite the attempted bits. **Acceptance is not a fill.** `submit_market` returns `SubmitResult`: @@ -1979,7 +1980,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:7716`), so +(`record_script_report_point`, `native_execution_consumer.cpp:7744`), so ```text broker_state_hash_len == equity_curve_len == script_bars_processed @@ -2773,7 +2774,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:7798`). What it does +(`TimeframeAggregator` `native_execution_consumer.cpp:7826`). 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 @@ -3094,8 +3095,10 @@ quantity stops the run. The FIFO rules are the settlement's 5. `Flatten` is never refused for a quantity. - On a `quantity_grid` the book's own quantities are on the grid: a `ScopeFraction` whose product is its scope (`fraction == 1`) resolves to the - scope's held total, unfloored, and a close of one lot's own size is admitted - (above). + scope's held total, unfloored, and a `Reduce` of a FIFO boundary of its + scope is admitted (above). A fill the request's own units cannot absorb -- + a scope of dust closed by a far larger request -- is the same typed + refusal, from the request core's check. - `NativeRunSpec::quantity_tolerance` is the opt-in for a host that keeps decimal quantities, as a broker ledger does: within the tolerance a close ends at the FIFO boundary it is near, charged its request, and a dust lot a diff --git a/docs/pages/pine-to-native.md b/docs/pages/pine-to-native.md index 37ecf258..d7454812 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -67,9 +67,9 @@ gain: and nothing comes from a chart. - **Typed orders and typed outcomes.** A request is a value (`Request` native_order.hpp:483); its fate is a typed event — `AcceptedEvent` - (native_order.hpp:991), `ExecutionAppliedEvent` (native_order.hpp:1130), - `MatchRejectedEvent` (native_order.hpp:1092), `CancelledEvent` - (native_order.hpp:1033) — readable in order from `native_events` + (native_order.hpp:994), `ExecutionAppliedEvent` (native_order.hpp:1133), + `MatchRejectedEvent` (native_order.hpp:1095), `CancelledEvent` + (native_order.hpp:1036) — readable in order from `native_events` (native_host.hpp:1306). - **Any language.** The same kernel drives from C through a callback table (`pf_native_callbacks_v1` native_c_api.h:2454), so a host in Rust, Go, Python or Zig needs no C++. @@ -124,9 +124,9 @@ rather than defaulted. | `currency` | `currency` native_run_spec.hpp:570 | `currency` pineforge.h:535 | `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:538 | `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:538 | `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:538 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:3936-3937. TradingView's round-then-slip order is adapter-only. | -| `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:539 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:7438-7439, 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:541 | `native_open_lots_strategy.cpp` | Caps surviving+new physical lots `sha256:87bd0ff848901478840870d95eb356b35d7d368555ce8eadee8571b8e17f86ec` native_execution_consumer.cpp:2107-2110. Pine's per-cycle *entry count* is a TradingView rule, ruled **adapter-policy** in ADR-0001's table row `max_open_lots`. | +| `slippage` | `slippage_ticks` native_run_spec.hpp:583 | `slippage_ticks` pineforge.h:538 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:3932-3933. TradingView's round-then-slip order is adapter-only. | +| `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:539 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:7466-7467, 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:541 | `native_open_lots_strategy.cpp` | Caps surviving+new physical lots `sha256:87bd0ff848901478840870d95eb356b35d7d368555ce8eadee8571b8e17f86ec` native_execution_consumer.cpp:2109-2112. Pine's per-cycle *entry count* is a TradingView rule, ruled **adapter-policy** in ADR-0001's table row `max_open_lots`. | | `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:2102 | `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:2106 | `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:2276 | `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. | @@ -283,7 +283,7 @@ is `strategy_native_submit_v1` native_c_api.h:2678 over a | `strategy.entry()` | `Request` native_order.hpp:483 carrying `Transact` native_order.hpp:41 | `strategy_native_submit_v1` native_c_api.h:2678 | `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:2678 | `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:2678 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:1149. 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`) closes the whole scope unfloored. | +| `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:1152. 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`) 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:1255 with `NativeRequestField::Label` native_host.hpp:679 | `strategy_native_cancel_where_v1` native_c_api.h:2765 | `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:1226 is the single-request form for a host that kept the handle. | | `strategy.cancel_all()` | `cancel_all` native_host.hpp:1244 | `strategy_native_cancel_all_v1` native_c_api.h:2744 | `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:1240 before its arm, and all three cancels still address it. | @@ -291,7 +291,7 @@ is `strategy_native_submit_v1` native_c_api.h:2678 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:539 | `native_risk_limits_strategy.cpp` | The default. | -| `strategy.direction.long` | `NativeOpenDirections::Long` native_run_spec.hpp:74 | `allowed_open_directions` pineforge.h:539 | `tests/test_native_run_spec.cpp` | An opening the direction forbids is refused with `OpeningDirection` native_execution_consumer.cpp:2932. | +| `strategy.direction.long` | `NativeOpenDirections::Long` native_run_spec.hpp:74 | `allowed_open_directions` pineforge.h:539 | `tests/test_native_run_spec.cpp` | An opening the direction forbids is refused with `OpeningDirection` native_execution_consumer.cpp:2928. | | `strategy.direction.short` | `NativeOpenDirections::Short` native_run_spec.hpp:75 | `allowed_open_directions` pineforge.h:539 | `tests/test_native_run_spec.cpp` | Same, the other way. | | `strategy.commission.percent` | `NativeFeeKind::Percent` native_run_spec.hpp:20 | `fee_kind` pineforge.h:538 | `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:538 | `tests/test_native_selected_requests.cpp` | Account currency per unit. | @@ -303,7 +303,7 @@ 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:1197, which emits -`ReplacedEvent` native_order.hpp:1007 and can carry the predecessor's live +`ReplacedEvent` native_order.hpp:1010 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 spelling is `strategy_native_replace_v1`, or `strategy_native_replace_ext_v1` @@ -340,7 +340,7 @@ once. Natively they are separate and each is nameable: `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:996, and carries the installed level - in `ArmedEvent` native_order.hpp:1210. + in `ArmedEvent` native_order.hpp:1213. - **Invisible until armed.** `NativeArmVisibility::PendingUntilArmed` native_order.hpp:394 keeps the legs out of `native_working_requests` native_host.hpp:1240 until the fill, like Pine's pending exit and like a @@ -396,7 +396,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:539 | `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:2932. | +| `strategy.risk.allow_entry_in()` | `allowed_open_directions` native_run_spec.hpp:596 | `allowed_open_directions` pineforge.h:539 | `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:2928. | | `strategy.risk.max_position_size()` | `max_abs_units` native_run_spec.hpp:594 | `max_abs_units` pineforge.h:540 | `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:2312 | `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:2315 | `native_risk_limits_strategy.cpp` | Percent is of the day's opening equity. Blocks to the end of its own day. | @@ -432,7 +432,7 @@ liquidation level, and the ticket the forced close is booked under. | which check points run at all | `margin_check_allowed` native_host.hpp:972 | `on_margin_check` native_c_api.h:2517 | `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:979 | `on_margin_call_units` native_c_api.h:2524 | `native_margin_strategy.cpp` | -| the liquidation event | `MarginCallEvent` native_order.hpp:1226 delivered to `on_native_margin_call` native_host.hpp:985 | `on_margin_call` native_c_api.h:2472 | `native_margin_strategy.cpp` | +| the liquidation event | `MarginCallEvent` native_order.hpp:1229 delivered to `on_native_margin_call` native_host.hpp:985 | `on_margin_call` native_c_api.h:2472 | `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 @@ -452,7 +452,7 @@ openings TradingView takes. | `barstate.isrealtime` | `NativeStateView::phase` native_host.hpp:285 with `NativeRunPhase` native_host.hpp:41 | `pf_native_state_v1` native_c_api.h:1754 | `native_market_strategy.cpp` | `Batch`, `Warmup`, `Realtime`. | | bar-open pre-pass | `on_native_bar_open` native_host.hpp:874 | `on_bar_open` native_c_api.h:2461 | `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:1050 | `strategy_native_partial_bar_v1` native_c_api.h:2971 | `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:922 | `on_applied` native_c_api.h:2464 | `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:5284. | +| a fill, as it happens | `on_native_applied` native_host.hpp:922 | `on_applied` native_c_api.h:2464 | `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:5314. | | raw input before aggregation | `on_native_input` native_host.hpp:860 | `on_input` native_c_api.h:2459 | `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:912 | `on_sub_bar` native_c_api.h:2500 | `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:114 with `NativeCoordinate` market_driver.hpp:71 | `pf_native_decision_v1` native_c_api.h:1443 | `hello_kernel.cpp` | Copied onto the callback stack; mutating it changes nothing. In C++ `script_interval` market_driver.hpp:118 and `input_interval` market_driver.hpp:117 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 session-day facts are the three rows below. | diff --git a/include/pineforge/native_order.hpp b/include/pineforge/native_order.hpp index 8d52a56f..2adbcbf7 100644 --- a/include/pineforge/native_order.hpp +++ b/include/pineforge/native_order.hpp @@ -885,9 +885,12 @@ enum class MatchRejectReason : std::uint8_t { /// UnrepresentableQuantity: a close split binary64 cannot hold (a rest /// below half an ulp of the lot it ends in, a whole lot that cannot /// decrement the rest or move the running sum), a reduction or transaction - /// the position absorbs, or an opening the surviving book absorbs. The - /// request ends here, nothing moves, and the run goes on; the same request - /// stopped the whole run before R5 lane K-ULP4. + /// the position absorbs, or an opening the surviving book absorbs; or the + /// request core cannot take the fill off the request's own units + /// (CoreFailure::NonrepresentableQuantity: a scope of dust closed by a far + /// larger request, a point budget too small to move its remaining units). + /// The request ends here, nothing moves, and the run goes on; the same + /// request stopped the whole run before R5 lane K-ULP4. UnrepresentableQuantity = 10, }; @@ -1415,15 +1418,15 @@ struct CommandContext { std::optional sizing_scope; std::optional sizing_price; bool sizing_admissible = true; - /// The request's explicit close quantity -- a Reduce's ExplicitUnits, or - /// the units of a Transact against the book's side -- is the binary64 - /// quantity of one of the book's own lots. Such a quantity is on the + /// A Reduce's ExplicitUnits are a FIFO boundary of its scope: the binary64 + /// sum, in book order, of the scope's lots through one of them -- the head + /// lot's own size, a prefix, the whole scope. Such a quantity is on the /// quantity grid whatever the grid predicate answers: the grid admits the - /// quantities a host chooses, and a lot's size is the book's own, which - /// settlement arithmetic can move off any decimal grid (R5 lane K-ULP4). - /// The execution consumer measures it; appended last, like the members - /// above. - bool units_are_lot_quantity = false; + /// quantities a host chooses, and these are the book's own, which + /// settlement arithmetic can move off any decimal grid; closing them takes + /// whole lots and splits none (R5 lane K-ULP4). The execution consumer + /// measures it at submit; appended last, like the members above. + bool units_are_scope_boundary = false; }; struct EvaluationContext { diff --git a/src/engine_execution.cpp b/src/engine_execution.cpp index 80f227af..91d342f2 100644 --- a/src/engine_execution.cpp +++ b/src/engine_execution.cpp @@ -180,8 +180,8 @@ CloseSplit next_close_split(const PyramidEntry& lot, bool closes, bool flatten, // the running sum closes whole with the close that reaches it. Each such end // is charged the request, as a split its rows cannot add up to is (above): // - the lot the request ends in would keep at most t: it closes whole; -// - a rest of at most t is left after a whole lot, and the next member lot -// is larger than t: the walk ends before it, which is not touched; a next +// - a rest above zero and at most t is left after a whole lot, and the next +// member lot is larger than t: the walk ends before it, untouched; a next // lot of at most t closes whole instead (the rule above), so a dust lot // behind the boundary is taken, not left; after the scope's last lot the // caller ends the walk the same way (finish_tolerant_walk); diff --git a/src/native_execution_consumer.cpp b/src/native_execution_consumer.cpp index be699096..1aa2324a 100644 --- a/src/native_execution_consumer.cpp +++ b/src/native_execution_consumer.cpp @@ -330,8 +330,10 @@ void hash_spec(F& f, const NativeRunSpec& spec, f.u(static_cast(spec.event_retention)); } // K-ULP4: the quantity tolerance folds only where a host declared one, so - // every spec that declares none keeps its digest. + // every spec that declares none keeps its digest; a tag word first, so the + // value's bits can never read as another optional fold's word. if (spec.quantity_tolerance) { + f.u(0x4b554c5034544f4cULL); // "KULP4TOL" f.d(*spec.quantity_tolerance); } } @@ -461,8 +463,8 @@ bool explicit_reduction_units_representable(double units, double exposure) noexc } // `own_quantity`: the answered units are the book's own -- a closing size equal -// to one lot's binary64 quantity, or a fraction's whole scope -- which the -// quantity grid admits as they stand (R5 lane K-ULP4). +// to its scope's held total, or a fraction's whole scope -- which the quantity +// grid admits as they stand (R5 lane K-ULP4). bool execution_terms_grid_representable( const native_order::ExecutionTerms& terms, const native_order::HostSized* host_sized, bool unresolved, @@ -2820,19 +2822,13 @@ native_order::CommandContext NativeExecutionConsumer::make_command_context( ? current_frame_->point.decision.coordinate.effective_time_ms : decision_floor(); if (const auto* spec = spec_ptr()) { ctx.quantity_grid = spec->quantity_grid; - // A close of exactly one of the book's own lots is on the grid as it - // stands (CommandContext::units_are_lot_quantity; R5 lane K-ULP4). + // A Reduce whose units are a FIFO boundary of its scope is on the grid + // as it stands (CommandContext::units_are_scope_boundary; R5 lane K-ULP4). if (spec->quantity_grid && engine.position_side_ != PositionSide::FLAT) { if (const auto* reduce = std::get_if(&request.intent)) { if (const auto* units = std::get_if(&reduce->size)) { - ctx.units_are_lot_quantity = lot_quantity(engine, units->units); + ctx.units_are_scope_boundary = scope_boundary_units(engine, request, units->units); } - } else if (const auto* transact = std::get_if( - &request.intent)) { - const bool closes = (engine.position_side_ == PositionSide::LONG) - == (transact->signed_units < 0.0); - ctx.units_are_lot_quantity = closes - && lot_quantity(engine, std::abs(transact->signed_units)); } } // A Sized request freezes its sizing price here when it asked for the @@ -4652,10 +4648,30 @@ bool NativeExecutionConsumer::admit_placement_units( nullptr); } -bool NativeExecutionConsumer::lot_quantity(const BacktestEngine& engine, - double units) noexcept { +bool NativeExecutionConsumer::scope_boundary_units(const BacktestEngine& engine, + const native_order::Request& request, + double units) noexcept { + const auto* one = std::get_if(&request.owner); + const auto* many = std::get_if(&request.owner); + if (!one && !many && !std::holds_alternative(request.owner)) { + return false; + } + if ((one && one->cycle != engine.position_cycle_seq_) + || (many && many->cycle != engine.position_cycle_seq_)) { + return false; + } + double sum = 0.0; for (const auto& lot : engine.pyramid_entries_) { - if (lot.qty == units) return true; + const std::uint64_t incarnation = lot.entry_incarnation; + if (one && incarnation != one->opening.incarnation) continue; + if (many && std::none_of(many->openings.begin(), many->openings.end(), + [&](const native_order::RequestHandle& opening) { + return opening.incarnation == incarnation; + })) { + continue; + } + sum += lot.qty; + if (sum == units) return true; } return false; } @@ -4905,11 +4921,9 @@ std::optional NativeExecutionConsumer::consume_mat if (terms.units && (!std::isfinite(*terms.units) || *terms.units < 0.0)) { return terminal(native_order::MatchRejectReason::InvalidTerms, terms); } - const auto* grid_spec = spec_ptr(); - const bool own_quantity = closing_size && terms.units && grid_spec - && grid_spec->quantity_grid + const bool own_quantity = closing_size && terms.units && ((whole_scope && kernel_units && *terms.units == *kernel_units) - || lot_quantity(engine, *terms.units)); + || *terms.units == terms_facts.scope_exposure_units); if (!execution_terms_grid_representable( terms, host_sized, unresolved, terms_facts.scope_exposure_units, spec_ptr(), own_quantity)) { @@ -5114,9 +5128,21 @@ std::optional NativeExecutionConsumer::consume_mat // settlement; the pair holds a token across it. std::optional token; bool ready = false; + // A fill the request's own units cannot absorb -- a scope of dust + // closed by a far larger request, whose remaining units the close + // cannot move -- is the same typed refusal, before anything is + // written: the request core checks and prepares without a mutation + // (R5 lane K-ULP4). + const auto unrepresentable = [](const native_order::PreparationError& error) { + return error.code == native_order::CoreFailure::NonrepresentableQuantity; + }; if (direct_mutation_) { auto checked = requests_.check_execution(handle, proposal, next_timeline_ordinal_); if (const auto* error = std::get_if(&checked)) { + if (unrepresentable(*error)) { + return terminal(native_order::MatchRejectReason::UnrepresentableQuantity, + nonidentity_attempt); + } fail_preparation(engine, *error, NativeFailureOperation::Settlement); return std::nullopt; } @@ -5124,6 +5150,10 @@ std::optional NativeExecutionConsumer::consume_mat } else { auto prepared = requests_.prepare_execution(handle, proposal, next_timeline_ordinal_); if (const auto* error = std::get_if(&prepared)) { + if (unrepresentable(*error)) { + return terminal(native_order::MatchRejectReason::UnrepresentableQuantity, + nonidentity_attempt); + } fail_preparation(engine, *error, NativeFailureOperation::Settlement); return std::nullopt; } @@ -6433,11 +6463,9 @@ NativeCurrentExecutionPreview NativeExecutionConsumer::inspect_current_execution out.terms_rejection = native_order::MatchRejectReason::InvalidTerms; return out; } - const auto* grid_spec = spec_ptr(); - const bool own_quantity = closing_size && terms.units && grid_spec - && grid_spec->quantity_grid + const bool own_quantity = closing_size && terms.units && ((whole_scope && kernel_units && *terms.units == *kernel_units) - || lot_quantity(engine, *terms.units)); + || *terms.units == facts.scope_exposure_units); if (!execution_terms_grid_representable( terms, host_sized, unresolved, facts.scope_exposure_units, spec_ptr(), own_quantity)) { diff --git a/src/native_execution_consumer.hpp b/src/native_execution_consumer.hpp index 6a626d3c..76ed3166 100644 --- a/src/native_execution_consumer.hpp +++ b/src/native_execution_consumer.hpp @@ -598,9 +598,15 @@ class NativeExecutionConsumer final : public IExecutionConsumer { std::optional resolve_sized_units( const BacktestEngine& engine, const native_order::LiveRequest& live, const NativeExecutionTermsFacts& facts, bool* whole_scope = nullptr) const; - // Whether `units` is the binary64 quantity of one of the book's own lots, - // which the quantity grid admits as it stands (R5 lane K-ULP4). - static bool lot_quantity(const BacktestEngine& engine, double units) noexcept; + // Whether `units` is a FIFO boundary of the request's scope -- the binary64 + // sum of the scope's lots, in book order, through one of them: the head + // lot's own size, a prefix, the whole scope. The scope is the book for an + // Independent request and the bound openings' lots for BindOpening / + // BindOpenings; any other owner has none here. The quantity grid admits + // such a Reduce as it stands (R5 lane K-ULP4). + static bool scope_boundary_units(const BacktestEngine& engine, + const native_order::Request& request, + double units) noexcept; double sibling_claimed_units(const native_order::LiveRequest& live) const noexcept; // L3b placement-time sizing. sizing_point_price answers the price a Sized // request's basis converts at when it is accepted; placement_scope_units diff --git a/src/native_order.cpp b/src/native_order.cpp index 90cffc75..275ee510 100644 --- a/src/native_order.cpp +++ b/src/native_order.cpp @@ -1553,7 +1553,7 @@ std::optional WorkingRequestCore::validate_request( if (const auto* units = explicit_size(*reduce)) { if (!finite_positive(units->units)) return RequestRejectReason::InvalidQuantity; if (!on_optional_grid(units->units, context.quantity_grid) - && !context.units_are_lot_quantity) { + && !context.units_are_scope_boundary) { return RequestRejectReason::OffGrid; } } else if (const auto* fraction = fraction_size(*reduce)) { @@ -1591,8 +1591,7 @@ std::optional WorkingRequestCore::validate_request( } } else if (const auto* transact = as_transact(request.intent)) { if (!finite_nonzero(transact->signed_units)) return RequestRejectReason::InvalidQuantity; - if (!on_optional_grid(transact->signed_units, context.quantity_grid) - && !context.units_are_lot_quantity) { + if (!on_optional_grid(transact->signed_units, context.quantity_grid)) { return RequestRejectReason::OffGrid; } } else if (reverse_to) { diff --git a/tests/fixtures/native_cpp_abi/host-fc7aad6/relocation-manifest-v18-v19.json b/tests/fixtures/native_cpp_abi/host-fc7aad6/relocation-manifest-v18-v19.json index 7dafe02c..22078f10 100644 --- a/tests/fixtures/native_cpp_abi/host-fc7aad6/relocation-manifest-v18-v19.json +++ b/tests/fixtures/native_cpp_abi/host-fc7aad6/relocation-manifest-v18-v19.json @@ -27,7 +27,7 @@ "ReplaceOptions::keep_handle", "ReplaceOptions::keep_binding", "MatchRejectReason::UnrepresentableQuantity", - "CommandContext::units_are_lot_quantity", + "CommandContext::units_are_scope_boundary", "NativeRunSpec::quantity_tolerance", "NativeRunSpecField::QuantityTolerance" ], diff --git a/tests/test_native_quantity_tolerance.cpp b/tests/test_native_quantity_tolerance.cpp index 51696a89..3eb523b5 100644 --- a/tests/test_native_quantity_tolerance.cpp +++ b/tests/test_native_quantity_tolerance.cpp @@ -29,8 +29,11 @@ // closes it whole // exact-control a close exactly at a boundary, and one far from any // still-refused a request of at most t on a larger lot (zero is not a -// boundary to snap to), and a rest above t a huge lot -// absorbs: typed MatchRejected(UnrepresentableQuantity) +// boundary to snap to), a rest above t a huge lot +// absorbs, and a dust-only book closed by a far larger +// request: typed MatchRejected(UnrepresentableQuantity) +// dust-head-small a request of at most t on {2^-55, 2.9}: the dust lot's +// boundary is the one within t, so only it closes // spec validation (NotFinitePositive on QuantityTolerance), // the digest pin, and the value folded when present // c-host the extension's quantity-tolerance tail, its mask @@ -397,6 +400,21 @@ void settlement_cases() { {{Step::Transact, 1.0}, {Step::Transact, 3.0}, {Step::Reduce, 1.0}}}); filled(o, 2, 1.0, 0.0, 1.0, {1.0}, {3.0}); } + // A book of dust closed by a far larger request: the dust lot closes whole, + // but the rest 1.0 is far above t and the request's units cannot be moved + // by 2^-55 -- the request core's check, the same typed refusal as exact. + { + const auto o = run_case({"dust-only-book", {{Step::Transact, 0x1p-55}, {Step::Reduce, 1.0}}}); + refused(o, 1, {0x1p-55}); + } + // A request of at most t on a dust head: the dust lot's own boundary is + // within t of the request, so the close ends there and the 2.9 lot is not + // touched (a boundary, not zero). + { + const auto o = run_case({"dust-head-small-request", + {{Step::Transact, 0x1p-55}, {Step::Transact, 2.9}, {Step::Reduce, 1e-11}}}); + filled(o, 2, 1e-11, 0.0, 1e-11, {0x1p-55}, {2.9}); + } // Zero is not a boundary: 2^-54 of a 2.9 lot cannot move the position. { const auto o = run_case({"still-refused-dust-request", diff --git a/tests/test_native_resting_acceptance.cpp b/tests/test_native_resting_acceptance.cpp index 41ec1353..5adea9f2 100644 --- a/tests/test_native_resting_acceptance.cpp +++ b/tests/test_native_resting_acceptance.cpp @@ -317,20 +317,28 @@ void failed_is_permanent(Host& h, int64_t next_offset, double price) { CHECK(h.physical_position().lot_count == position.lot_count); } +// A one-unit point budget cannot move a 2^60-unit request's remaining units +// (fl(2^60 - 1) is 2^60): the request core refuses the fill before anything is +// written. Since R5 lane K-ULP4 that is the request's typed refusal -- a +// MatchRejectedEvent with MatchRejectReason::UnrepresentableQuantity -- and the +// run goes on; it failed the run with SettlementFailure / +// NonrepresentableQuantity before. void capacity_progress_failure(double sign) { Host h; start(h, sign > 0 ? "C8-long" : "C8-short"); auto r = tx(sign * 0x1p60); r.capacity = no::PointBudget{1}; const auto handle = put(h,r); - CHECK(!h.input(0,100)); REQUIRE(h.native_state().kind == NativeLifecycleKind::Failed); - const auto failure = h.native_state().failure; - CHECK(failure.code == NativeFailureCode::SettlementFailure); - CHECK(failure.operation == NativeFailureOperation::Settlement); - CHECK(failure.discriminator == static_cast(no::CoreFailure::NonrepresentableQuantity)); - CHECK(native_failure_has_recipient(failure.context)); - CHECK(failure.context.recipient.incarnation == handle.incarnation); + CHECK(h.input(0,100)); CHECK(h.native_state().kind != NativeLifecycleKind::Failed); + const auto rejected = events(h); + REQUIRE(rejected.size() == 1); + CHECK(rejected[0].handle().incarnation == handle.incarnation); + CHECK(rejected[0].reason == no::MatchRejectReason::UnrepresentableQuantity); CHECK(fills(h,handle).empty()); CHECK(events(h).empty()); CHECK(h.trade_count() == 0); near(h.physical_position().signed_units,0); - near(h.native_marked_equity(100),10000); failed_is_permanent(h,1,100); + near(h.native_marked_equity(100),10000); + // The run goes on: the next request fills. + const auto next = put(h, tx(sign * 1.0)); + CHECK(h.input(1,100)); + CHECK(fills(h,next).size() == 1); } void check_group_failure(Host& h, const no::RequestHandle& filler, @@ -425,7 +433,7 @@ int main() { run_case(sign>0?"OHLC long unrelated insertion":"OHLC short unrelated insertion",[&]{chronological_crossings(sign,true);}); run_case(sign>0?"C9 long transaction first":"C9 short transaction first",[&]{flat_incarnation_order(sign,true);}); run_case(sign>0?"C9 long close first":"C9 short close first",[&]{flat_incarnation_order(sign,false);}); - run_case(sign>0?"C8 long precommit arithmetic failure":"C8 short precommit arithmetic failure",[&]{capacity_progress_failure(sign);}); + run_case(sign>0?"C8 long precommit arithmetic refusal":"C8 short precommit arithmetic refusal",[&]{capacity_progress_failure(sign);}); run_case(sign>0?"G5 long postcommit subtraction failure":"G5 short postcommit subtraction failure",[&]{group_subtraction_failure(sign);}); } run_case("G5 pending 2^60 plus 1",[]{pending_addition_failure(false);}); diff --git a/tests/test_native_resting_replay_contract.cpp b/tests/test_native_resting_replay_contract.cpp index db102ffc..fbadf718 100644 --- a/tests/test_native_resting_replay_contract.cpp +++ b/tests/test_native_resting_replay_contract.cpp @@ -1257,6 +1257,12 @@ void replay_confirmed_bar(double sign) { CHECK(trail_b.incarnation == trail_a.incarnation); } +// A one-unit point budget cannot move a 2^60-unit request's remaining units +// (fl(2^60 - 1) is 2^60): the request core refuses the fill. Since R5 lane +// K-ULP4 that is the request's typed refusal -- a MatchRejectedEvent with +// MatchRejectReason::UnrepresentableQuantity -- and the run goes on; it failed +// the run with SettlementFailure / NonrepresentableQuantity before. Both +// replays refuse identically. void replay_failed_capacity(double sign) { Host a, b; const char* key = sign > 0 ? "P2-C8-L" : "P2-C8-S"; @@ -1264,20 +1270,16 @@ void replay_failed_capacity(double sign) { auto r = tx(sign * 0x1p60, "huge"); r.capacity = no::PointBudget{1}; const auto ha = put(a, r), hb = put(b, r); same_hosts("accepted", a, b); - CHECK(!a.input(0, 100)); CHECK(!b.input(0, 100)); - same_hosts("failed", a, b); - REQUIRE(a.native_state().kind == NativeLifecycleKind::Failed); - CHECK(a.native_state().failure.code == NativeFailureCode::SettlementFailure); - CHECK(a.native_state().failure.discriminator - == static_cast(no::CoreFailure::NonrepresentableQuantity)); - CHECK(native_failure_has_recipient(a.native_state().failure.context)); - CHECK(a.native_state().failure.context.recipient.incarnation == ha.incarnation); + CHECK(a.input(0, 100)); CHECK(b.input(0, 100)); + same_hosts("refused", a, b); + CHECK(a.native_state().kind != NativeLifecycleKind::Failed); + const auto rejected = events_of(a); + REQUIRE(rejected.size() == 1); + CHECK(rejected[0].handle().incarnation == ha.incarnation); + CHECK(rejected[0].reason == no::MatchRejectReason::UnrepresentableQuantity); CHECK(fills(a, ha).empty()); CHECK(events_of(a).empty()); same_d(a.physical_position().signed_units, 0); - failed_is_permanent(a, 1); - failed_is_permanent(b, 1); - same_hosts("refused", a, b); CHECK(hb.incarnation == ha.incarnation); } @@ -1800,7 +1802,7 @@ int main() { [&] { replay_owner_expiry_replace(sign); }); run_case(sign > 0 ? "identical confirmed-bar long" : "identical confirmed-bar short", [&] { replay_confirmed_bar(sign); }); - run_case(sign > 0 ? "failed capacity prefix long" : "failed capacity prefix short", + run_case(sign > 0 ? "refused capacity prefix long" : "refused capacity prefix short", [&] { replay_failed_capacity(sign); }); run_case(sign > 0 ? "failed group prefix long" : "failed group prefix short", [&] { replay_failed_group(sign); }); diff --git a/tests/test_native_unrepresentable_refusal.cpp b/tests/test_native_unrepresentable_refusal.cpp index 97854eb4..bd24115b 100644 --- a/tests/test_native_unrepresentable_refusal.cpp +++ b/tests/test_native_unrepresentable_refusal.cpp @@ -12,8 +12,9 @@ // failed", on ordinary decimal quantities (AUDIT4-opus kulp NF1 / NF2). // // On a quantity grid, the book's own quantities are on the grid: a fraction -// whose product is its scope resolves to the scope, unfloored, and a close of -// exactly one lot's binary64 size is admitted (NF3 / NF4). +// whose product is its scope resolves to the scope, unfloored, and a Reduce of +// a FIFO boundary of its scope -- the binary64 sum of the scope's lots through +// one of them -- is admitted (NF3 / NF4). // // survivor open 1.5, Reduce 1.3, open 2.9: the book is // {fl(1.5 - 1.3), 2.9}; Reduce 0.2 needs 2^-54 of the @@ -35,9 +36,16 @@ // nothing to close), 0.19999999999999857 (closed one // step), {0.10000000000000142, 0.2} (left 2^-50 dust); // each now closes the book whole -// grid-own-lot Reduce and Transact of a lot's own off-grid size: they -// were OffGrid at submit; an off-grid quantity that is no -// lot's size is still OffGrid +// grid-own-lot, a Reduce of a FIFO boundary of its scope -- the head +// grid-boundaries, lot's own off-grid size, a prefix, the whole book, a +// grid-bound-* bound opening's lot -- was OffGrid at submit and now +// closes whole lots; a Book-scope Reduce of a later lot's +// size (it would split the head lot off-grid), any +// Transact and any other off-grid quantity are still +// OffGrid +// scoped-dust a bound scope of 2^-55 closed by Reduce 1.0: the +// request core cannot take 2^-55 off 1.0 -- the same +// typed refusal (code 6, discriminator 2 on 91d65ad6) // grid-after-dust the AUDIT4 F5b/c/d continuations: open, an oversized // Reduce and a reversal after the whole-scope close // controls the lot's own size and the host's total, which close @@ -46,9 +54,10 @@ // // Fail-before: compiled against 91d65ad6 the TU stops at its first use of // MatchRejectReason::UnrepresentableQuantity (no such enumerator). With that -// name spelled 10 it runs there and fails 84 of 292 checks: every refusal case -// stops its run with code 6, discriminator 5, and every grid case keeps its -// residual, its dust lot or its OffGrid refusal; only the controls pass. +// name spelled 10 it runs there and fails 100 of 350 checks: every refusal +// case stops its run with code 6, discriminator 5 (scoped-dust with +// discriminator 2), and every grid case keeps its residual, its dust lot or +// its OffGrid refusal; only the controls pass. // Source-free: the kernel-only profile registers the row. #include "../src/engine_internal.hpp" #include "../src/native_execution_consumer.hpp" @@ -94,8 +103,9 @@ constexpr unsigned kUnrepresentable = const double kSurvivor = 1.5 - 1.3; // 0x1.9999999999998p-3 struct Step { - enum Kind { Transact, Reduce, Fraction, Flatten } kind = Transact; + enum Kind { Transact, Reduce, Fraction, Flatten, BoundReduce } kind = Transact; double value = 0.0; + int bind = -1; // BoundReduce: the step whose opening the Reduce binds to }; struct Case { @@ -165,6 +175,7 @@ no::Request request_of(const Step& s) { case Step::Reduce: r.intent = no::Reduce{no::ExplicitUnits{s.value}}; break; case Step::Fraction: r.intent = no::Reduce{no::ScopeFraction{s.value}}; break; case Step::Flatten: r.intent = no::Flatten{}; break; + case Step::BoundReduce: r.intent = no::Reduce{no::ExplicitUnits{s.value}}; break; } r.label = "step"; return r; @@ -184,7 +195,14 @@ class Host final : public NativeStrategyHost { if (k > 0 && k - 1 < steps.size()) steps[k - 1].book = book(); if (k >= case_.steps.size()) return; StepOutcome out; - const auto res = submit(request_of(case_.steps[k])); + auto request = request_of(case_.steps[k]); + const int bind = case_.steps[k].bind; + if (case_.steps[k].kind == Step::BoundReduce && bind >= 0 + && static_cast(bind) < handles.size() && handles[bind]) { + request.owner = no::BindOpening{*handles[bind], position_cycle_seq_}; + } + const auto res = submit(request); + handles.push_back(res.handle); if (res.status != no::SubmitStatus::Accepted || !res.handle) { out.submit = res.reason ? static_cast(*res.reason) : -2; incarnations.push_back(0); @@ -223,6 +241,7 @@ class Host final : public NativeStrategyHost { std::vector steps; std::vector incarnations; + std::vector> handles; std::optional preview; int current_kind = -1; unsigned current_reason = 0; @@ -402,6 +421,7 @@ int c_on_bar(void* user, const pf_bar_t*, const pf_native_decision_v1*) { r.intent_value = step.value; break; case Step::Flatten: r.intent = PF_NATIVE_INTENT_FLATTEN; break; + case Step::BoundReduce: return 1; // the C cases bind nothing } std::uint64_t inc = 0; std::uint32_t reject = 0; @@ -595,6 +615,21 @@ void refusal_cases() { refused(o, 1, {0x1p-55}); filled(o, 2, 0x1p-55, 0.0, 0x1p-55, {}); } + // A scope of dust closed by a far larger request: the stage caps the close + // to the scope's 2^-55, which settles, but the request's 1.0 cannot be + // moved by 2^-55 -- the request core's check, answered the same way + // (on 91d65ad6: code 6, discriminator 2, NonrepresentableQuantity). + { + const double dust = 0.2 - (0.3 - 0.1); + const auto o = run_case({"scoped-dust", + {{Step::Transact, 0.1}, {Step::Transact, 0.2}, {Step::Transact, 5.0}, + {Step::Reduce, 0.3}, {Step::BoundReduce, 1.0, 1}, {Step::BoundReduce, dust, 1}}}); + CHECK(dust == 0x1p-55); + filled(o, 3, 0.3, 0.0, 0.3, {dust, 5.0}); + refused(o, 4, {dust, 5.0}); + // The dust lot's own size closes it. + filled(o, 5, dust, 0.0, dust, {5.0}); + } // execute_current answers the refusal as a MatchRejectedEvent (alternative 3 // of NativeCurrentExecutionResult) and does not throw; the preview shows the // readiness first. @@ -660,29 +695,59 @@ void grid_cases() { {Step::Fraction, 0.75}}, 0.1}); filled(o, 3, 0.1, 0.0, 0.1, {left_b - 0.1}); } - // A close of exactly one lot's own size is admitted on the grid, spelled - // Reduce or Transact; any other off-grid quantity is still OffGrid. + // A Reduce whose units are a FIFO boundary of its scope -- the head lot's + // own size, a prefix, the whole book -- is admitted on the grid and closes + // whole lots. { const auto o = run_case({"grid-own-lot", {{Step::Transact, 39.0}, {Step::Transact, 0.2}, {Step::Transact, -39.1}, {Step::Reduce, left_a}}, 0.1}); filled(o, 3, left_a, 0.0, left_a, {}); } + // {fl(39 - 38.9), 0.2, 0.3}: the head lot's own size, then the prefix of + // the first two lots, each off the 0.1 grid. { - const auto o = run_case({"grid-own-lot-transact", - {{Step::Transact, 39.0}, {Step::Transact, 0.2}, {Step::Transact, -39.1}, - {Step::Transact, -left_a}}, 0.1}); - filled(o, 3, left_a, -0.0, left_a, {}); + const auto o = run_case({"grid-boundaries-head", + {{Step::Transact, 39.0}, {Step::Transact, 0.2}, {Step::Reduce, 38.9}, + {Step::Transact, 0.3}, {Step::Reduce, left_c}}, 0.1}); + filled(o, 4, left_c, 0.0, left_c, {0.2, 0.3}); + CHECK(o.steps.size() == 5 && o.steps[4].rows == std::vector({left_c})); } { + const double prefix = left_c + 0.2; + CHECK(!no::quantity_on_grid(prefix, 0.1)); + const auto o = run_case({"grid-boundaries-prefix", + {{Step::Transact, 39.0}, {Step::Transact, 0.2}, {Step::Reduce, 38.9}, + {Step::Transact, 0.3}, {Step::Reduce, prefix}}, 0.1}); + filled(o, 4, prefix, 0.0, prefix, {0.3}); + CHECK(o.steps.size() == 5 && o.steps[4].rows == std::vector({left_c, 0.2})); + } + // A bound scope's own boundary: its opening's lot. + { + const double big = 1000.1 - 1000.0; // 0x1.99999999a3p-4 on a 0.1 grid + CHECK(!no::quantity_on_grid(big, 0.1)); + const auto o = run_case({"grid-bound-boundary", + {{Step::Transact, 0.5}, {Step::Transact, 1000.1}, {Step::BoundReduce, 1000.0, 1}, + {Step::BoundReduce, big, 1}}, 0.1}); + filled(o, 2, 1000.0, 0.0, 1000.0, {0.5, big}); + filled(o, 3, big, 0.0, big, {0.5}); + } + // Not a boundary: a Book-scope Reduce of the second lot's size would take + // it FIFO from the first, on-grid lot and leave an off-grid split, so it + // is still OffGrid; so is any Transact (it could open), and any other + // off-grid quantity. + { + const double big = 1000.1 - 1000.0; const auto o = run_case({"grid-off-grid-still-refused", - {{Step::Transact, 39.0}, {Step::Transact, 0.2}, {Step::Transact, -39.1}, - {Step::Reduce, std::nextafter(left_a, 0.0)}, {Step::Transact, left_a}}, 0.1}); - CHECK(o.steps.size() == 5); - if (o.steps.size() == 5) { + {{Step::Transact, 0.5}, {Step::Transact, 1000.1}, {Step::BoundReduce, 1000.0, 1}, + {Step::Reduce, big}, {Step::Transact, -0.5 - big}, + {Step::Reduce, std::nextafter(0.5 + big, 0.0)}}, 0.1}); + CHECK(o.steps.size() == 6); + if (o.steps.size() == 6) { CHECK(o.steps[3].submit == static_cast(no::RequestRejectReason::OffGrid)); - // An opening on the lot's own side is not a close of it. CHECK(o.steps[4].submit == static_cast(no::RequestRejectReason::OffGrid)); + CHECK(o.steps[5].submit == static_cast(no::RequestRejectReason::OffGrid)); + CHECK(o.steps[5].book == std::vector({0.5, big})); } } // AUDIT4 F5b/c/d: after the whole-scope close nothing is left to trip on. From a6e34acf8f36b138fa383f8795f153ad40451f6d Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Fri, 25 Sep 2026 21:13:14 +0800 Subject: [PATCH 08/24] Kernel: the grid holds a boundary Reduce to its boundary at the candidate too; the preview's reach stated (R5 lane K-ULP4, second review) The second review of the lane (ef180962) confirmed findings 1-5 fixed and found one more gap: the FIFO-boundary exemption was measured only at submit, so a Reduce admitted as the head boundary could match after the book changed. {A', 0.5} with A' = fl(1000.1 - 1000) on a 0.1 grid: a bound close of A' and an Independent Reduce{A'} in one callback -- the second took A' FIFO from the on-grid 0.5 lot and left {0.39999999999997726}. - make_command_context sets CommandContext::units_are_scope_boundary only for a request of ImmediateRemaining capacity (one that settles in one fill); a point-budget Reduce of a boundary is gridded as before. - consume_matched_request holds the candidate to the same test (grid_boundary_holds): an off-grid Reduce{ExplicitUnits} -- which only the exemption admits -- that is no longer a FIFO boundary of its scope ends with MatchRejectReason::UnrepresentableQuantity before its inspection; nothing moves. Runs without a quantity grid and on-grid requests are not touched (the check answers true first). - The preview does not run the request core's check or this re-check: the docs (native_host.hpp's NativeCurrentExecutionPreview, native-settlement.md, native-engine.md) now say the settlement's refusal shows in inspect_current_execution and the other two are made at execution only. ADR-0001 rule 2 unchanged from f961f73b (the ADR row now says the boundary is held again at the candidate). Witness test_native_unrepresentable_refusal: 431 checks (grid-boundary-moved, grid-boundary-budget). Citations carried from ef180962 by the exact line map (308 moved, none stuck or grown). Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit d2932403a52098a26472a41b93e4847406ee529b) --- docs/adr/0001-kernel-adapter-boundary.md | 74 ++++----- docs/design/native-feature-parity.md | 150 +++++++++--------- docs/native-settlement.md | 13 +- docs/pages/contributing-llm.md | 2 +- docs/pages/native-engine.md | 84 +++++----- docs/pages/pine-to-native.md | 120 +++++++------- include/pineforge/native_host.hpp | 5 +- include/pineforge/native_order.hpp | 14 +- src/native_execution_consumer.cpp | 25 ++- src/native_execution_consumer.hpp | 5 + tests/test_native_unrepresentable_refusal.cpp | 60 ++++++- 11 files changed, 322 insertions(+), 230 deletions(-) diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index 89af1511..5fd6fc72 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:830`), an abstract subclass of + `pineforge::NativeStrategyHost` (`native_host.hpp:833`), 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:830`) carrying the +Subclass `NativeStrategyHost`: a **zero-argument** host (`native_host.hpp:833`) 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,18 +72,18 @@ 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:1173`), then feed. + continues live. Configure with `configure_native(spec)` (`native_host.hpp:1176`), then feed. - **Callbacks are not close-only.** `on_native_bar` is the pure-virtual script-bar calculation - (`native_host.hpp:878`); also `on_native_input` (`native_host.hpp:860`), `on_native_tick` - (`native_host.hpp:863`), `on_native_timeframe_bar` (`native_host.hpp:870`), + (`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:874`, `on_native_bar_open` `native_execution_consumer.cpp:7008-7010`) — - `on_native_recalculate` for every calculation of the run (`native_host.hpp:897`), - `on_native_sub_bar` (`native_host.hpp:912`) and post-fill - `on_native_applied` (`native_host.hpp:922`). `on_bar` is `final` (`native_host.hpp:842`). + (`native_host.hpp:877`, `on_native_bar_open` `native_execution_consumer.cpp:7029-7031`) — + `on_native_recalculate` for every calculation of the run (`native_host.hpp:900`), + `on_native_sub_bar` (`native_host.hpp:915`) and post-fill + `on_native_applied` (`native_host.hpp:925`). `on_bar` is `final` (`native_host.hpp:845`). - **Requests.** `submit` / `replace` / `cancel` / `submit_market` / `replace_market` - (`native_host.hpp:1204-1209`), plus `cancel_all` (`native_host.hpp:1244`) and `cancel_where` - (`native_host.hpp:1249`). A request carries one of five triggers — Market, Limit (with a + (`native_host.hpp:1207-1212`), plus `cancel_all` (`native_host.hpp:1247`) and `cancel_where` + (`native_host.hpp:1252`). 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` @@ -96,12 +96,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:931`) returns no + unresolved `HostSized` whose `resolve_execution_terms` (`native_host.hpp:934`) returns no units is a `TermsUnresolved` rejection - (`TermsUnresolved` `native_execution_consumer.cpp:4910`). -- **Observing executions.** Implement `on_native_applied` (`native_host.hpp:922`) — notifications + (`TermsUnresolved` `native_execution_consumer.cpp:4923`). +- **Observing executions.** Implement `on_native_applied` (`native_host.hpp:925`) — notifications drain FIFO after the outer callback returns — and/or poll `native_events(after_ordinal)` - (`native_host.hpp:1306`). Do *not* implement + (`native_host.hpp:1309`). 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`: @@ -109,13 +109,13 @@ 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:4883`) | - | Cohorts | `cohort_open` (`native_host.hpp:1259`) / `cohort_add` (`:1262`) / `cohort_remove` (`:1265`) — the handle a `BindCohort` owner names | - | Mid-callback execution | `current_execution_point` (`native_host.hpp:1064`), `inspect_current_execution` (`native_host.hpp:1076`) / `execute_current` (`native_host.hpp:1081`) | - | 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:960`), `margin_check_allowed` (`native_host.hpp:972`), `resolve_margin_call_units` (`native_host.hpp:979`) — and `validate_execution_precommit` (`native_host.hpp:944`), 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:1300`); the breach appends a `NativeRiskEvent` to the command history | - | State reads | `physical_position()` (`native_host.hpp:1269`) / `native_marked_equity(mark)` (`native_host.hpp:1278`) / `native_open_lots(mark)` (`native_host.hpp:1274`), `trail_state(handle)` (`native_host.hpp:1069`), 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:1050`) 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:4896`) | + | Cohorts | `cohort_open` (`native_host.hpp:1262`) / `cohort_add` (`:1265`) / `cohort_remove` (`:1268`) — the handle a `BindCohort` owner names | + | Mid-callback execution | `current_execution_point` (`native_host.hpp:1067`), `inspect_current_execution` (`native_host.hpp:1079`) / `execute_current` (`native_host.hpp:1084`) | + | 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:963`), `margin_check_allowed` (`native_host.hpp:975`), `resolve_margin_call_units` (`native_host.hpp:982`) — and `validate_execution_precommit` (`native_host.hpp:947`), 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:1303`); the breach appends a `NativeRiskEvent` to the command history | + | State reads | `physical_position()` (`native_host.hpp:1272`) / `native_marked_equity(mark)` (`native_host.hpp:1281`) / `native_open_lots(mark)` (`native_host.hpp:1277`), `trail_state(handle)` (`native_host.hpp:1072`), 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:1053`) 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 (`NativeFeeKind` `native_run_spec.hpp:19`), a scalar account FX, close timing, the quantity @@ -142,9 +142,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:1113`) from inside `on_native_run_begin`, with - `on_native_timeframe_bar` (`native_host.hpp:870`) and `native_series_bar` - (`native_host.hpp:1096`) reading them back, and `NativeAuxiliaryFeed` + + (`native_host.hpp:1116`) from inside `on_native_run_begin`, with + `on_native_timeframe_bar` (`native_host.hpp:873`) and `native_series_bar` + (`native_host.hpp:1099`) 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 @@ -154,7 +154,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:1179`). + `configure_native_fx_curve` (`native_host.hpp:1182`). - **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 @@ -167,7 +167,7 @@ summary of it. `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. - **Reproducibility.** Two digests, one wrapping the other. `native_continuation_hash()` - (`native_host.hpp:1347`) returns the consumer's event/continuation hash. **A digest names + (`native_host.hpp:1350`) 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 @@ -284,7 +284,7 @@ citations, 264 are in `src/source/` and its headers; 6 sit in kernel files `sha256:e3312d2dd7ad7067ad41b26dfb3b502293dcdc5b98ed91563bd89f9482bb2b64` engine_aux_security.cpp:5-12, 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:743`), which the kernel never dereferences; the + and forwards it as `overrides_opaque` (`native_host.hpp:746`), 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 @@ -450,7 +450,7 @@ see a mechanism, so these rows are held by reading, not by the checker): | the closed rows as the broker-state hash folds them — `BacktestEngine::trades_` under `pineforge-broker-state/v19`, through the consumer's running digest (`NativeExecutionConsumer::closed_rows_digest`, `src/native_execution_consumer.cpp`), and `NativeStrategyHost::native_closed_rows_amended` (`include/pineforge/native_host.hpp`) | the kernel books every row, at the end of the rows; the Pine host amends a booked row inside the applied notification of the execution that booked it (`PineStrategyHost::on_native_applied`: an aggregated chart dates the exit at its chart bar), and after those notifications have returned it reorders a bar's trailing same-bar bracket exits by command sequence (`sort_same_bar_exit_trades`, `src/source/pine_strategy_host.cpp`), naming the first row it moved | **a row is final once the applied notification of the execution that booked it has returned** (R5 lane V19-A). Rule 2 (a), mechanism: the finality point is the kernel's own notification boundary, the same for every host, and what a host that changes a final row supplies is data -- the first row it changed -- so a host of any venue drives the same surface and nothing in it knows Pine. Rule 2 (b), knob: none -- the call reports a change the host has already made and chooses nothing. A final row changed without a name keeps the value the digest took, and the Debug re-fold at each run's end (`verify_closed_rows`) aborts on it; rows removed from the end need no name, because the kernel only appends, so a read over fewer rows or a booking below the final mark forgets them. Every read answers the fold of the six fields v18 walked (entry and exit time and price, quantity, P&L) over the rows as they are, in their order, folded once per row per run instead of once per row per read. Pinned by part 7 of `tests/test_native_state_continuation.cpp` (kernel-only) and by every Debug ctest run, each of whose runs ends in the re-fold. | | the event record a run keeps — `NativeRunSpec::event_retention` (`include/pineforge/native_run_spec.hpp`), the journal window (`WorkingRequestCore::retire_history`, `include/pineforge/native_order.hpp`; `NativeExecutionConsumer::retire_journal`, `src/native_execution_consumer.cpp`) and the reader's acknowledgement (`NativeStrategyHost::native_acknowledge_events`, `native_event_window_start`, `include/pineforge/native_host.hpp`) | the host declares a retention in its spec and acknowledges what it has read; the Pine adapter declares `Window` in `PineExecutionAdapter::project()` and acknowledges its receipt cursor in `observe_terminal_receipts` (and "nothing read yet" at its run begin); the kernel retires at every script-bar boundary | **a reporting policy with a reader acknowledgement** (R5 lane V19-B). Rule 2 (a), mechanism: what a run keeps of its own event record, and a reader telling the kernel what it has consumed, the same for every host; the kernel never retires an event its own live state reads (a queued applied notification, a live deferred group-adjustment chain), and what the journal used to answer beyond that is state (next row). Rule 2 (b), knob: a spec field with no TradingView meaning -- `Window`, `Commands`, `Full` -- folded into the spec digest only off the default; a C caller that does not send the word keeps `Full`, the record its layout was published with. No trade, and no Pine value, depends on it: the adapter reads above its own cursor only. Pinned by `tests/test_native_event_retention.cpp` (kernel-only: `Full` row for row against the pre-window record, the window and the acknowledgement's edges, and the same books booking the same trades with the window closing at every driver point) and `check_event_retention` of `tests/test_native_c_api.c`. | | 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, and a `ScopeFraction` whose product is its scope, unfloored); 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`. | +| 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 and held again at its candidate, and a `ScopeFraction` whose product is its scope, unfloored); 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`. | 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 (→ @@ -590,9 +590,9 @@ number names its symbol on this tree. | surface | ruling | reason | |---|---|---| -| `cancel_all` (`native_host.hpp:1244`) / `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:2744`). | -| `cohort_add` (`native_host.hpp:1262`) / `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:872`, 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:1291`) / `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:3019`). | +| `cancel_all` (`native_host.hpp:1247`) / `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:2744`). | +| `cohort_add` (`native_host.hpp:1265`) / `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:872`, 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:1294`) / `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:3019`). | | `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:1681`) 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. | @@ -600,8 +600,8 @@ number names its symbol on this tree. | the enumerated `int32` fields of `pf_pending_order_v1_t` (`strategy_pending_order_get` `pineforge.h:1059`) | **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:3961`) | **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:582`) | **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:870`) and `NativeTimeframeBarContext::interval` (`native_host.hpp:794`) | **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:7587-7599`); 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:1067-1072`) sees the same values, so no hash contract moves. | +| `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:7608-7620`); 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:1067-1072`) sees the same values, so no hash contract moves. | | `pf_native_lot_excursion_v1` (`native_c_api.h:1867`) | **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) @@ -659,7 +659,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: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` | | `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:8544-8547` (lookahead) and `clear_if_gapped` `:8538-8541` (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:8565-8568` (lookahead) and `clear_if_gapped` `:8559-8562` (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:2226`, `::gaps` `include/pineforge/native_c_api.h:2230` | 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` | @@ -676,7 +676,7 @@ The table records, at the tree the doc wave read, what each block described and | `present_session_day` `src/native_execution_consumer.cpp:7116`, presenting `NativeDecisionContext::in_session` `include/pineforge/market_driver.hpp:155` 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:7819-7835` | 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:7796-7817` | 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:6547` | 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::execute_current` `src/native_execution_consumer.cpp:6568` | 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 @@ -743,7 +743,7 @@ no new kernel policy. 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. 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:1007`; the market-if-touched + excursion ownership, `owns_lot_excursions` `native_host.hpp:1010`; 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 8f19a738..0b866dc4 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:1846, *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:1852, *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). | @@ -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:2678-2689; `entry()` pine_adapter.cpp:5082, `order()` pine_adapter.cpp:10270) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5625-5672; `submit` native_host.hpp:1190 | 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:8133 → `Flatten{}` pine_adapter.cpp:8162-8163; `Reduce{ExplicitUnits}` pine_adapter.cpp:7280-7289; `ReverseTo` pine_adapter.cpp:6114-6115) | **yes** — `OrderIntent` native_order.hpp:229, reduce sizes native_order.hpp:229-236, `ReverseTo` native_order.hpp:60-62, `OpeningShape` native_order.hpp:907-911, `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:2648-3707 → `host.replace` pine_adapter.cpp:2664-2744) | **yes** — `replace` native_host.hpp:1195-1197, `cancel` native_host.hpp:1226; `ReplacedEvent` native_order.hpp:1010-1024; result and event types `SubmitStatus` native_order.hpp:835-861, `ReplacedEvent` native_order.hpp:994-1060 | 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:4515-4561, `cancel_all` pine_adapter.cpp:17806-17821; `live_by_source_key_` pine_adapter.hpp:1983; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:3857-3922) | **partial** — single-handle `cancel` native_host.hpp:1226; no enumeration (`WorkingRequestCore::live()` is consumer-internal native_order.hpp:1667); labels exist but are unindexed (`Request::label` native_order.hpp:485) **Closed:** `cancel_all` native_host.hpp:1244, `cancel_where(comment)` native_host.hpp:1249 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1249 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1240 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. | +| OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:2678-2689; `entry()` pine_adapter.cpp:5082, `order()` pine_adapter.cpp:10270) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5646-5693; `submit` native_host.hpp:1193 | 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:8133 → `Flatten{}` pine_adapter.cpp:8162-8163; `Reduce{ExplicitUnits}` pine_adapter.cpp:7280-7289; `ReverseTo` pine_adapter.cpp:6114-6115) | **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 | — | +| OL3 | Amend / cancel one working order (`submit_or_replace` pine_adapter.cpp:2648-3707 → `host.replace` pine_adapter.cpp:2664-2744) | **yes** — `replace` native_host.hpp:1198-1200, `cancel` native_host.hpp:1229; `ReplacedEvent` native_order.hpp:1013-1027; result and event types `SubmitStatus` native_order.hpp:835-861, `ReplacedEvent` native_order.hpp:997-1063 | 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:4515-4561, `cancel_all` pine_adapter.cpp:17806-17821; `live_by_source_key_` pine_adapter.hpp:1983; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:3857-3922) | **partial** — single-handle `cancel` native_host.hpp:1229; no enumeration (`WorkingRequestCore::live()` is consumer-internal native_order.hpp:1673); labels exist but are unindexed (`Request::label` native_order.hpp:485) **Closed:** `cancel_all` native_host.hpp:1247, `cancel_where(comment)` native_host.hpp:1252 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1252 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1243 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:3982; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:6965-7316) | **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 | — | -| 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:5303-5338, `cohort_for` pine_adapter.cpp:2442; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7263-7280) | **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:1259, `CancelReason::OwnerGone` native_order.hpp:946 | 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:2687-2693) | **yes** — `Group` / `GroupEffect` native_order.hpp:466-473, `CancelReason::Group` native_order.hpp:945-950, `ReservationReducedEvent` native_order.hpp:1169-1179 (deferred form native_order.hpp:1169-1179) | 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:1081-1082 | K | — | F:A9 | (single-source row; O:F2 cites the same call) | +| 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:5303-5338, `cohort_for` pine_adapter.cpp:2442; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7263-7280) | **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:1262, `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:2687-2693) | **yes** — `Group` / `GroupEffect` native_order.hpp:466-473, `CancelReason::Group` native_order.hpp:948-953, `ReservationReducedEvent` native_order.hpp:1172-1182 (deferred form native_order.hpp:1172-1182) | 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:1084-1085 | 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:12779; lowering `submit_one` pine_adapter.cpp:8830-8842, `BindCohort` pine_adapter.cpp:9331-9337, `submit_leg` pine_adapter.cpp:9355-9379; 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:1846); 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:8435-8438; `materialize_relative_exits` pine_adapter.cpp:17096-17137, pine_adapter.hpp:1812-1824) | **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:5551-5585. `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:996 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:18857-18886) | **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:1240 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:1985-2003); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1116-1195); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3261-3428, `materialize_snapshot` pine_adapter.cpp:10670-10694), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1580-1603, `CloseCallsiteState` pine_adapter.hpp:1674-1689; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15406, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15640) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:657-659 | 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:12779; lowering `submit_one` pine_adapter.cpp:8830-8842, `BindCohort` pine_adapter.cpp:9331-9337, `submit_leg` pine_adapter.cpp:9355-9379; 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:1852); 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:8435-8438; `materialize_relative_exits` pine_adapter.cpp:17096-17137, pine_adapter.hpp:1812-1824) | **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:5572-5606. `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:999 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:18857-18886) | **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:1243 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:1985-2003); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1116-1195); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3261-3428, `materialize_snapshot` pine_adapter.cpp:10670-10694), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1580-1603, `CloseCallsiteState` pine_adapter.hpp:1674-1689; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15406, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15640) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:659-661 | 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: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:855-887) | n/a | A | — | S:R4 O:A10 | TV half of OL9. **V19D-P1 (R5 lane B-ADAPTER):** a margin-call revival never restores an exit the script withdrew: `strategy.cancel` / `cancel_all` retire every leg of a dormant exit's lifecycle (`retire_cancelled_dormant_exits` pine_adapter.cpp:1795) and the revival skips it (`revive_brackets_after_margin` pine_adapter.cpp:1856), as the lab tv tapes badapter-v19dp1-control / -cancel (NYSE:F 15m) decide it — replayed by `tests/test_adapter_margin_revival_cancel.cpp` | ### 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:5637-5659; `NativeTrailState` native_host.hpp:657-662, `trail_state` native_host.hpp:1069 | 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:5658-5680; `NativeTrailState` native_host.hpp:657-662, `trail_state` native_host.hpp:1072 | 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:11554-11566) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4425-4430) **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:11467-11604) stays A. | -| TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:3814-3860) | **partial** — the read exists `trail_state` native_host.hpp:1069; `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:11554-11566) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4428-4433) **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:11467-11604) stays A. | +| TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:3814-3860) | **partial** — the read exists `trail_state` native_host.hpp:1072; `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:9381-9386, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11467-11604, retained best `retained_trail_best` pine_adapter.hpp:287-289, operand / restart rules `source_trail_points` pine_adapter.cpp:8405-8631, `trail_one_shot` pine_adapter.cpp:9470-9599 | n/a | A | — | F:B3 S:T2 | R5-1 | ### 1.3 Sizing bases and reservations @@ -77,12 +77,12 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | SZ1 | Explicit units (`Transact{signed}` after a grid floor pine_adapter.cpp:5692-5696, pine_adapter.cpp:5692) | **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:10705-11882) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:929-933, invoked native_execution_consumer.cpp:4868-4887, guarded native_execution_consumer.cpp:4868-4903 | 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:2536-2591) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4910-4912 **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:2488, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:453 (→ SZ10) | +| SZ2 | Host override of the resolved price / units (`resolve_terms` pine_adapter.cpp:10705-11882) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:932-936, invoked native_execution_consumer.cpp:4881-4900, guarded native_execution_consumer.cpp:4881-4916 | 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:2536-2591) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4923-4925 **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:2488, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:453 (→ SZ10) | | SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:2497-2499) | **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:2549, `typed_entry_units` pine_adapter.cpp:2567; `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:2528; `percent_commission_live_equity` pine_adapter.cpp:4121-4161) | **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:2454-2469, `PineSizingSnapshot` pine_adapter.hpp:147-154; fill-time exceptions `default_sized` pine_adapter.cpp:6435-6461) | **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:1388-1405; host-sized terms snap (`ExecutionGridPolicy` native_order.hpp:71-85); exact grid validation `execution_terms_grid_representable` native_execution_consumer.cpp:4923-4926; 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:1391-1408; host-sized terms snap (`ExecutionGridPolicy` native_order.hpp:71-85); exact grid validation `execution_terms_grid_representable` native_execution_consumer.cpp:4936-4939; 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:12253-12281, `compute_exit_reservation` pine_adapter.cpp:4204-4325) | **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:4256-4579, `reserved_exit_qty` pine_adapter.cpp:8757-9067) | **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:444-451, `source_money_floor_lot` pine_adapter.cpp:453; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:4236-4251) | 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 | @@ -100,14 +100,14 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13055-13120 and leaves the kernel gate unset pine_adapter.cpp:13054-13056) | **yes** — `initial_margin_fraction` native_run_spec.hpp:597; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2958 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:2808 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. | | MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13710-13711, `margin_pct` pine_adapter.cpp:13118) | **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:13810-13988, `submit_margin_call_units` pine_adapter.cpp:13949-13995, `schedule_margin_call_path` pine_adapter.cpp:17959-18036) | **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:2917 **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:13741-13777) | **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:979 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:179-200) | **no** **Closed:** `native_liquidation_price()` native_host.hpp:1297 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:16057-16073, `apply_fx_opening_margin_slice` pine_adapter.cpp:18037-18065) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1179 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:16548-16587) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1278 **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 | +| MG7 | Liquidation slice sizing (grid floor, ×4 restore, one-contract fallback `source_margin_units` pine_adapter.cpp:13741-13777) | **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:982 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:179-200) | **no** **Closed:** `native_liquidation_price()` native_host.hpp:1300 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:16057-16073, `apply_fx_opening_margin_slice` pine_adapter.cpp:18037-18065) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1182 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:16548-16587) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1281 **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:13090-13093) | **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:13671-13685, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14697-14727) | **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:14740-14974) | **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:13481-13560) | **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:1300 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 | +| MG14 | Risk-day boundary (`chart_day_key` pine_adapter.cpp:13481-13560) | **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:1303 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:13699, 4× shortfall `4.0 * minimum` pine_adapter.cpp:13758, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14466-14506, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:13990-14117, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:13990) | n/a | A | — | F:D2 F:G4 O:D4 S:M4 | R5-1 | | 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:3610-3677, `record_market_review` pine_adapter.cpp:14978-15036) | n/a | A | — | S:R2 | (single-source) | @@ -115,15 +115,15 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| CT1 | Calculate at script-bar close | **yes** — `on_native_bar` native_host.hpp:878, dispatched native_execution_consumer.cpp:7146-7167 (call site `sha256:fc29ccd415732193f24f2370c6aca8d6f8910bca5b95315e63e88f2befe8200b` native_execution_consumer.cpp:7157) | K | — | F:E1 O:F1 | — | -| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2259-2260) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7466-7470 | K | — | F:E2 O:F6 S:C1 | — | -| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:15893) | **yes** — `on_native_bar_open` native_host.hpp:874 runs before the open match native_execution_consumer.cpp:7008-7012; `execute_current` legal native_host.hpp:1081 | 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:6951 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7002 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1050 is the lookahead-free bar so far at the cursor, 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:860, `NativeInputContext` native_host.hpp:758-763 | 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:681; `begin_coof_recalc` pine_adapter.cpp:5400, `flush_coof_tail` pine_adapter.cpp:16035, `end_coof_recalc` pine_adapter.cpp:5421) | **partial** — `on_native_applied` native_host.hpp:922 fires mid-path after each fill native_execution_consumer.cpp:6862, native_execution_consumer.cpp:6841-6871; commands are legal there `commands_allowed` native_execution_consumer.cpp:1388-1396; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5551-5585, `remaining_path_delivery` native_order.cpp:2038-2046. 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:863 fires per accepted realtime print native_execution_consumer.cpp:7090-7094; 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:284-286. | -| 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:7460-7577 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:912 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:2389-2445) | **yes** — `IntrabarPath` native_run_spec.hpp:362-402, delivery native_execution_consumer.cpp:7404-7595; `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:881, dispatched native_execution_consumer.cpp:7167-7188 (call site `sha256:fc29ccd415732193f24f2370c6aca8d6f8910bca5b95315e63e88f2befe8200b` native_execution_consumer.cpp:7178) | K | — | F:E1 O:F1 | — | +| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2259-2260) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7487-7491 | K | — | F:E2 O:F6 S:C1 | — | +| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:15893) | **yes** — `on_native_bar_open` native_host.hpp:877 runs before the open match native_execution_consumer.cpp:7029-7033; `execute_current` legal native_host.hpp:1084 | 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:6972 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7023 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1053 is the lookahead-free bar so far at the cursor, 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:681; `begin_coof_recalc` pine_adapter.cpp:5400, `flush_coof_tail` pine_adapter.cpp:16035, `end_coof_recalc` pine_adapter.cpp:5421) | **partial** — `on_native_applied` native_host.hpp:925 fires mid-path after each fill native_execution_consumer.cpp:6883, native_execution_consumer.cpp:6862-6892; commands are legal there `commands_allowed` native_execution_consumer.cpp:1388-1396; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5572-5606, `remaining_path_delivery` native_order.cpp:2038-2046. 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:7111-7115; 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:284-286. | +| 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:7481-7598 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:915 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:2389-2445) | **yes** — `IntrabarPath` native_run_spec.hpp:362-402, delivery native_execution_consumer.cpp:7425-7616; `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:233-319) 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:5547), 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:681, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1655-1662 | n/a | A | — | F:E4 O:F5 S:C2 | R5-5: the adapter keeps its path | @@ -131,38 +131,38 @@ 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:1513-1516, pumped from the scheduler `scheduler_prepare_security_sequence` pine_strategy_host.cpp:1421-1577; 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:1514 **Closed:** `NativeRunSpec::subscriptions` (`NativeTimeframeSubscription` native_run_spec.hpp:485), or `declare_timeframe_subscriptions` native_host.hpp:1113 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:870 and `native_series_bar(index)` native_host.hpp:1096 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:1419-1435 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, sole caller pine_strategy_host.cpp:1546; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1437-1442 **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:8214 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). | -| 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:912 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. | +| HT1 | HTF series of the same symbol (`request.security`): evaluators registered `scheduler_configure_security_evaluators` pine_strategy_host.cpp:1513-1516, pumped from the scheduler `scheduler_prepare_security_sequence` pine_strategy_host.cpp:1421-1577; 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:1514 **Closed:** `NativeRunSpec::subscriptions` (`NativeTimeframeSubscription` native_run_spec.hpp:485), or `declare_timeframe_subscriptions` native_host.hpp:1116 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:873 and `native_series_bar(index)` native_host.hpp:1099 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:1419-1435 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, sole caller pine_strategy_host.cpp:1546; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1437-1442 **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:8235 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). | +| 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:915 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:860; the Pine scheduler asks the kernel's `script_bucket_completions` native_execution_consumer.cpp:7819-7836, which feeds the same class `TimeframeAggregator` native_execution_consumer.cpp:7826, 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:7819-7836, which feeds the same class `TimeframeAggregator` native_execution_consumer.cpp:7826, 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:5625-5672, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:6208-6216; `Limit::fill_through` native_order.hpp:239-244 enforced native_execution_consumer.cpp:5017-5030, re-checked after host terms native_execution_consumer.cpp:5017-5037 | 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:4572-4573, clamp native_execution_consumer.cpp:4572 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:11721-11730) 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:5646-5693, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:6229-6237; `Limit::fill_through` native_order.hpp:239-244 enforced native_execution_consumer.cpp:5030-5043, re-checked after host terms native_execution_consumer.cpp:5030-5050 | 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:4575-4576, clamp native_execution_consumer.cpp:4575 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:11721-11730) is A | | FP3 | Commission: percent / cash per unit / cash per execution (`fee_kind_for` pine_adapter.cpp:463-470) | **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:256-259, `source_bar_fill` pine_adapter.cpp:11737-11746, `directional_tick` pine_adapter.cpp:301-305) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4034; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6290-6291; 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:256-259, `source_bar_fill` pine_adapter.cpp:11737-11746, `directional_tick` pine_adapter.cpp:301-305) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4037; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6311-6312; 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:332-380) 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:1179 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:8644). **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:1182 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:8665). **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:339-387 (used `source_trigger_threshold` pine_adapter.cpp:8822-8827, `exit_limit_trigger` pine_adapter.cpp:9356-9377), `source_bar_fill_tick` pine_adapter.cpp:277-290, raw-vs-booked spelling pine_adapter.cpp:11220-11326, margin-call fill pricing pine_adapter.cpp:13786-13795 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:11446) | 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:2591-2604, closing-row fold `max_runup` engine_orders.cpp:157-171; `owns_lot_excursions` native_host.hpp:1005-1009 | 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:2591-2604, closing-row fold `max_runup` engine_orders.cpp:157-171; `owns_lot_excursions` native_host.hpp:1008-1012 | 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). | | 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:1713) | **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:1438) stay A, measured in §3.7 part 2. | | 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:9218, 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:1347, native_execution_consumer.cpp:10617; 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:9239, 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:1350, 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:1955 **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` in batch and across a stream's warmup and realtime legs. 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. What a recorded row costs — quadratic on the Pine adapter, O(closed rows) per row in the kernel — is measured and ruled in §3.8 | | 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:2157, `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:546 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 57 `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 symbols are a second, disjoint inventory beside the 57 codegen-facing ones (`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:155 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:7210), 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:1794 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:155 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:7231), 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:1794 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:287-289) | 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,11 +202,11 @@ 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:743; the source host casts it back `overrides_opaque` pine_strategy_host.cpp:291-294 | 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:746; the source host casts it back `overrides_opaque` pine_strategy_host.cpp:291-294 | 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:893-895 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1824-1827; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1857, `:8705`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2249-2250. 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:898-900 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1824-1827; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1857, `:8726`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2249-2250. 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:445-451) | 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:6361 | 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 native_execution_consumer.cpp:6382 | 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) | @@ -276,8 +276,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:649-663, `contribute_input` native_execution_consumer.cpp:7953, `seal_script` native_execution_consumer.cpp:7920) 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:7040), 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:654-668, `contribute_input` native_execution_consumer.cpp:7974, `seal_script` native_execution_consumer.cpp:7920) 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:7061), 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 | @@ -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:2906-2973) 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:2908-2975) 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). @@ -324,7 +324,7 @@ Union of F §2.vi (9), O §2.vi (5 + 1) and S §2.6 (7), deduplicated. S had no | 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 | | 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:830, `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:833, `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 @@ -332,12 +332,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:874), a tick hook (`on_native_tick` native_host.hpp:863), a post-fill hook (`on_native_applied` native_host.hpp:922), a recalculation hook (`on_native_recalculate` native_host.hpp:897) and a sub-bar hook (`on_native_sub_bar` native_host.hpp:912). 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:877), a tick hook (`on_native_tick` native_host.hpp:866), a post-fill hook (`on_native_applied` native_host.hpp:925), a recalculation hook (`on_native_recalculate` native_host.hpp:900) and a sub-bar hook (`on_native_sub_bar` native_host.hpp:915). 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:9088-9128 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:1050; 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:9109-9149 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:1053; and `set_native_security_feed` documented as the pre-run door for a series' authoritative bars, refused in-run | --- @@ -345,9 +345,9 @@ 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:2141-2278) 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:1631-1635). 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:e3aa487ce8d69db9054576f75839345c0acaab55c3fc0a374c2f9c8c071559ec` AGENTS.md:132-145). A lane that cannot be shown neutral by construction does not merge. +- **(a) NEUTRALITY.** Every new behaviour is opt-in by a spec field or a new request kind. The adapter's `project()` (pine_adapter.cpp:2141-2278) 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:1636-1640). 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:e3aa487ce8d69db9054576f75839345c0acaab55c3fc0a374c2f9c8c071559ec` AGENTS.md:132-145). A lane that cannot be shown neutral by construction does not merge. - **(b) TWIN.** One chosen probe per lane runs through the adapter AND a hand-written `NativeStrategyHost` on the same bars and spec. Trade lists are diffed **by identity** (entry / exit time, price, qty, pnl, commission), never by trade number; then event, ownership and hash detail (S's D → E order). A difference is allowed only where a named TV-quirk row of §1 is active, and each one is itemized. Because 430 test files 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:1275). Every new test unit compiles first against the frozen previous-epoch header closure (fail-before, `CLAUDE.md`). +- **(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:1278). 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. ### 3.2 Lanes: scope, size, dependencies, epochs @@ -406,7 +406,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:2298, 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:5556-5560) and `drain_applied_notifications` (native_execution_consumer.cpp:6217-6230) 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:5577-5581) and `drain_applied_notifications` (native_execution_consumer.cpp:6238-6251) 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:1334-1418 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) | @@ -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:4910-4912). 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 (`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:4923-4925). 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) | @@ -1158,10 +1158,10 @@ 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:948-952); test that a default spec hashes to the pre-change constant | 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 `sha256:620926caaa868e64ede32f547ac1dd0bfe81c9758267593a9a924051208df0a4` native_execution_consumer.hpp:953-957); 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:12916-12983, pine_adapter.cpp:13030); its chronology lives at `sha256:bb89768c9a7b2dc9f6de5943c04de8103ed107c21bb6e184446de0e6a84cf4c2` pine_adapter.cpp:13589-13666 and its excursion sample differs by chronology (`sha256:764a1a69f2940863b4f77cbf9d20267a19ec38e86720796c7081a59ac7245427` pine_strategy_host.cpp:729-759) | 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:2296-2336) | 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:5547) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `sha256:86617ca43bb2847380f1845b07cc9ecd27059814ee80fecf80649a9393dfe8c8` native_execution_consumer.cpp:3452-3461 and `drain_applied_notifications` (native_execution_consumer.cpp:6217-6230); new behaviour only behind new opt-ins | F | +| E4 | **Calc on fill.** Kernel suffix eligibility is continuous along the segment; TV refills only at O/H/L/C waypoints (`sha256:ddad8dc42e078838c0d53fd49e50c31060221e3b82e7d5bbaa0420769daa7d31` pine_scheduler_native.cpp:585-637, `coof_next_waypoint` pine_adapter.cpp:5547) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `sha256:86617ca43bb2847380f1845b07cc9ecd27059814ee80fecf80649a9393dfe8c8` native_execution_consumer.cpp:3452-3461 and `drain_applied_notifications` (native_execution_consumer.cpp:6238-6251); 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:15893-15921) | 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:1600-1619) | 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:332-380) 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 | @@ -1191,7 +1191,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:1284); between realtime inputs there is none — reject or fall back to `AtMatch`? | F: reject with `InvalidQuantityBasis` | +| Q4 | `SizeTime::AtAcceptance` needs a current execution point (`sha256:00d2682bf92be87d76eea9d03004abad74aee8b5ad03e3f611dbb0d6539a99c4` native_host.hpp:1287); 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 | @@ -1229,16 +1229,16 @@ 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:7134, native_execution_consumer.cpp:7134, native_execution_consumer.cpp:7134) 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:7155, native_execution_consumer.cpp:7155, native_execution_consumer.cpp:7155) 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:2298), 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:1007). 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:5008-5050), `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:1010). 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:5021-5063), `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) ```cpp -// native_order.hpp — native_order_v6; OrderIntent grows 5 -> 6 (static_assert native_order.hpp:1355) +// native_order.hpp — native_order_v6; OrderIntent grows 5 -> 6 (static_assert native_order.hpp:1358) struct CashValue { double cash = 0.0; }; // account currency struct EquityFraction { double fraction = 0.0; }; // of marked equity at the sizing point (0.10 = 10 %) using SizeBasis = std::variant; @@ -1283,9 +1283,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:12912-12933). New hashed `RequestDefinition::origin { Host, KernelLiquidation, KernelRisk }`, new `CancelReason::Superseded` on re-price, `MarginCallEvent` joins `CommandEvent` (17 → 18, native_order.hpp:1275). O's alternative, a checkpoint on the excursion walk (`sha256:ed6f4ff668195caf3c953c8ef5dd448c6010949353f7b9ef44d50b764f7488db` native_execution_consumer.cpp:1886-1899), 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:12912-12933). New hashed `RequestDefinition::origin { Host, KernelLiquidation, KernelRisk }`, new `CancelReason::Superseded` on re-price, `MarginCallEvent` joins `CommandEvent` (17 → 18, native_order.hpp:1278). O's alternative, a checkpoint on the excursion walk (`sha256:ed6f4ff668195caf3c953c8ef5dd448c6010949353f7b9ef44d50b764f7488db` native_execution_consumer.cpp:1886-1899), 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:4636-4649 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:4639-4652 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) @@ -1305,9 +1305,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:1613-1614) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6924-6936) 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:3465-3474). `born_on_remaining_path` (native_execution_consumer.cpp:5556-5560) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1655-1662) 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:1064-1071, whose body is native_execution_consumer.cpp:6774-6781). `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:1615-1616) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6945-6957) 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:3465-3474). `born_on_remaining_path` (native_execution_consumer.cpp:5577-5581) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1655-1662) 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:1069-1076, whose body is native_execution_consumer.cpp:6795-6802). `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:2535); 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). @@ -1346,7 +1346,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 (`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:3521-3526) +// 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:3524-3529) // 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; @@ -1358,7 +1358,7 @@ BracketReceipt submit_bracket(NativeStrategyHost&, const BracketSpec&); // emi template class OrderBook; // id -> handle: submit-or-replace, cancel-by-id ``` -- A leg with owner `WaitForApplied` / `BindOpening` and a non-absolute anchor gets its level when the owner's fill arms it (`ArmedEvent` native_order.hpp:1213-1220 already exists) (O). Fallback with no kernel change: the toolkit materializes relative levels from the host's `on_native_applied` (F). +- A leg with owner `WaitForApplied` / `BindOpening` and a non-absolute anchor gets its level when the owner's fill arms it (`ArmedEvent` native_order.hpp:1216-1223 already exists) (O). Fallback with no kernel change: the toolkit materializes relative levels from the host's `on_native_applied` (F). - Gap lane N13 (after R4d): two facts of the arm were missing and are now opt-in fields appended to `WaitForApplied`, each default byte-identical and folded only when set. `NativeArmFirstMatch { AtArmPrint, AfterArmPrint }`: whether the armed leg is a candidate at its owner's fill print or, like a request born in the owner's fill callback, only after it. `NativeArmScope { OwnerLot, Book }`: whether an armed closing leg closes the lot its owner opened or is bound at the arm to the whole position (a `BookClose`), under which it may be `HostSized`. Ruling: generic — both are broker models of a contingent child, neither names a source language — and they are what makes an anchored child the very request a host would submit from `on_native_applied`. The Pine adapter's relative `strategy.exit` legs use both; R4d's owner-lot spelling left a later same-id add open where the source leg closes the book (`rel-pyramid-set-once`, tests/test_adapter_brackets_relower.cpp). - Not adopted (R5-2): S's kernel bracket object, its C symbols `strategy_native_bracket_*` and its TU list (`engine_orders.cpp`, `engine_execution.cpp`). - TUs: `native_order.{hpp,cpp}` (arming), `native_matching.hpp` (`checked_trail_stop`), `native_execution_consumer.cpp` (`prepare_owner_applied` path, command surface). @@ -1374,7 +1374,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:3923-3945, `execute_current` path native_execution_consumer.cpp:3743-3753), `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:3926-3948, `execute_current` path native_execution_consumer.cpp:3746-3756), `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). ### A.8 L9 — risk limits (F P3 + O G7 + S P5) @@ -1391,10 +1391,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:1306) (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:1309) (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. - 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:13465-13563); 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:4636-4649 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:4639-4652 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 7b38ee5f..4b3ff18e 100644 --- a/docs/native-settlement.md +++ b/docs/native-settlement.md @@ -112,15 +112,20 @@ and ends inside one is one fill charged its request (K-ULP2); a close whose binary64 FIFO sum reaches its request at a lot closes that lot whole (K-ULP3); a `Transact` that crosses the book is one fill charged its units (K-ULP1, in `docs/pages/native-engine.md`); a request the settlement cannot book exactly is -refused, typed, as above, which `inspect_current_execution` shows beforehand as -a `settlement_readiness` of `UnrepresentableQuantity`; and `Flatten` is never +refused, typed, as above -- the settlement's own refusal shows beforehand in +`inspect_current_execution` as a `settlement_readiness` of +`UnrepresentableQuantity`, while the request core's check and the grid's +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: a `ScopeFraction` whose product is its scope -- `fraction == 1` -- resolves to the scope's held total, which the grid does not floor; a `Reduce` whose units are a FIFO boundary of its scope -- the binary64 sum, in book order, of the scope's lots through one of them: the head -lot's own size, a prefix, the whole scope -- is admitted at submit and closes -whole lots; and a host-sized close answered with its scope's held total is +lot's own size, a prefix, the whole scope -- is admitted at submit, for a +request that settles in one fill (no point budget), and closes whole lots; its +candidate holds it to the same test, so one that meets a changed book, on which +it is no longer a boundary, is refused, typed, instead of splitting a lot off +the grid; and a host-sized close answered with its scope's held total is admitted at the candidate (R5 lane K-ULP4). Before, such a fraction was floored onto the grid and closed up to a step short, left a dust lot, or found nothing to close, and the book's own sizes were `OffGrid`. A `Transact` is still diff --git a/docs/pages/contributing-llm.md b/docs/pages/contributing-llm.md index d57fd107..aeb476a7 100644 --- a/docs/pages/contributing-llm.md +++ b/docs/pages/contributing-llm.md @@ -240,7 +240,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:830, or C through the +through `NativeStrategyHost` native_host.hpp:833, or C through the `strategy_native_*` surface `sha256:1a4d41202e1b32a33ab75c2e42a36285ccb1d061087a8db1c19162b244c5b29f` native_c_api.h:2639. **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 1b4a025c..78657c0c 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:878`) is +**The callbacks.** Only `on_native_bar` (`native_host.hpp:881`) 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:850`) | once, with the begin's own arguments, before the run starts | *Lifecycle and run identity* | -| `on_native_run_begin` (`native_host.hpp:857`) | 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:860`) | once per accepted confirmed input, before it is aggregated or matched | *Calendar, session, timeframes, warmup* | -| `on_native_tick` (`native_host.hpp:863`) | once per accepted realtime print, before it is matched | *Batch OHLCV vs ticks vs quiet* | -| `on_native_timeframe_bar` (`native_host.hpp:870`) | once per delivered bucket of a declared series | *Higher-timeframe series for native hosts* | -| `on_native_bar_open` (`native_host.hpp:874`) | at the modeled opening, before that point's matching pass | *Native requests* (and its lookahead warning) | -| `on_native_bar` (`native_host.hpp:878`) | the script bar's own calculation | *Calculation timing* | -| `on_native_recalculate` (`native_host.hpp:897`) | 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:912`) | after each retained lower-timeframe sub-bar's whole path | *Sub-bars* | -| `on_native_applied` (`native_host.hpp:922`) | after each applied execution — the calculate-on-fill point | *Native requests* | -| `on_native_margin_call` (`native_host.hpp:985`) | right after the `on_native_applied` of a kernel liquidation's own fill | *Margin and liquidation* | +| `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:900`) | 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:915`) | after each retained lower-timeframe sub-bar's whole path | *Sub-bars* | +| `on_native_applied` (`native_host.hpp:925`) | after each applied execution — the calculate-on-fill point | *Native requests* | +| `on_native_margin_call` (`native_host.hpp:988`) | 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:931`), -`validate_execution_precommit` (`native_host.hpp:944`), -`resolve_margin_requirement` (`native_host.hpp:960`), `margin_check_allowed` -(`native_host.hpp:972`), `resolve_margin_call_units` (`native_host.hpp:979`), -`resolve_anchored_level` (`native_host.hpp:996`), `owns_lot_excursions` / -`closed_lot_excursion` (`native_host.hpp:1014`) and the hash seam +the kernel's own answer: `resolve_execution_terms` (`native_host.hpp:934`), +`validate_execution_precommit` (`native_host.hpp:947`), +`resolve_margin_requirement` (`native_host.hpp:963`), `margin_check_allowed` +(`native_host.hpp:975`), `resolve_margin_call_units` (`native_host.hpp:982`), +`resolve_anchored_level` (`native_host.hpp:999`), `owns_lot_excursions` / +`closed_lot_excursion` (`native_host.hpp:1017`) 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:1031`), and one that keeps the default +(`native_host.hpp:1034`), and one that keeps the default `validate_execution_precommit` with `declare_native_precommit_hook(false)` -(`native_host.hpp:1039`). The kernel then makes no bar-open call, and neither +(`native_host.hpp:1042`). 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, @@ -458,11 +458,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:9088-9128`), which admits the begin, checks the +(`native_execution_consumer.cpp:9109-9149`), 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:743`), which the kernel forwards +opaque `overrides_opaque` (`native_host.hpp:746`), 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). @@ -622,17 +622,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:5556-5560`). Requests accepted before the +`native_execution_consumer.cpp:5577-5581`). 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:7008-7010`). **Lookahead warning:** the +pass (`native_execution_consumer.cpp:7029-7031`). **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:6900`), the +`engine.current_bar_ = open_view` (`native_execution_consumer.cpp:6921`), 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:1050`; C: +`current_partial_bar()` (`native_host.hpp:1053`; C: `strategy_native_partial_bar_v1`), the lookahead-free bar so far at this cursor, or declares `NativeOpenBarView::OpenOnly`, which masks this one callback's bar down to its open. Both are under *The bar so far, and the @@ -643,8 +643,10 @@ exact binary64 grid in `native_order.hpp`, and a `Reduce` whose units are a FIFO boundary of its scope -- the binary64 sum, in book order, of the scope's lots through one of them: the head lot's own size, a prefix, the whole scope -- which settlement arithmetic can move off any decimal grid -(`CommandContext::units_are_scope_boundary`, R5 lane K-ULP4). Flatten is not -gridded. Rejection does not rewrite the attempted bits. +(`CommandContext::units_are_scope_boundary`, R5 lane K-ULP4), for a request +that settles in one fill; its candidate is held to the same test, and one that +meets a changed book is refused with `MatchRejectReason::UnrepresentableQuantity`. +Flatten is not gridded. Rejection does not rewrite the attempted bits. **Acceptance is not a fill.** `submit_market` returns `SubmitResult`: @@ -1818,8 +1820,8 @@ says otherwise, and the C spellings are in | `current_partial_bar()` | the lookahead-free bar so far at this cursor | `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:1332`) | 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:1337`) | 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:1335`) | 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:1340`) | 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 | @@ -1866,16 +1868,16 @@ 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:758`) instead — the two intervals, the +`NativeInputContext` (`native_host.hpp:761`) instead — the two intervals, the input index and whether this input completes the script interval — and -`on_native_tick` receives `NativeTickContext` (`native_host.hpp:770`), the +`on_native_tick` receives `NativeTickContext` (`native_host.hpp:773`), 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:1262`) enrolls one accepted opening's handle, -`cohort_remove` (`native_host.hpp:1265`) takes it back off, and a request with +(`native_host.hpp:1265`) enrolls one accepted opening's handle, +`cohort_remove` (`native_host.hpp:1268`) 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 @@ -1980,7 +1982,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:7744`), so +(`record_script_report_point`, `native_execution_consumer.cpp:7765`), so ```text broker_state_hash_len == equity_curve_len == script_bars_processed @@ -2748,7 +2750,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:860`). Include +(`on_native_input` `native_host.hpp:863`). Include ``: ```cpp @@ -3085,9 +3087,11 @@ quantity stops the run. The FIFO rules are the settlement's typed: a terminal `MatchRejectedEvent` with `MatchRejectReason::UnrepresentableQuantity` (C: `PF_NATIVE_MATCH_REJECT_UNREPRESENTABLE_QUANTITY`), nothing moved, the run - still running (R5 lane K-ULP4). `inspect_current_execution` shows it - beforehand as a `settlement_readiness` of `UnrepresentableQuantity`, and - `execute_current` answers the `MatchRejectedEvent` without throwing. Ordinary + still running (R5 lane K-ULP4). `inspect_current_execution` shows the + settlement's refusal beforehand as a `settlement_readiness` of + `UnrepresentableQuantity` (the request core's check and the grid's boundary + re-check are made at execution only), and `execute_current` answers the + `MatchRejectedEvent` without throwing. Ordinary decimal quantities reach it: after open 1.5, `Reduce` 1.3 and open 2.9, a `Reduce 0.2` needs `2^-54` of the 2.9 lot, and a dust lot a close left at the head of the book refuses the next close and the next opening on its @@ -4060,7 +4064,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:830`); the `CapAttachment` constructor belongs +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`). diff --git a/docs/pages/pine-to-native.md b/docs/pages/pine-to-native.md index d7454812..129b1f4a 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -60,17 +60,17 @@ 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:878). Any data structure, any library, any + function (native_host.hpp:881). 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 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 (`Request` native_order.hpp:483); its fate is a typed event — `AcceptedEvent` - (native_order.hpp:994), `ExecutionAppliedEvent` (native_order.hpp:1133), - `MatchRejectedEvent` (native_order.hpp:1095), `CancelledEvent` - (native_order.hpp:1036) — readable in order from `native_events` - (native_host.hpp:1306). + (native_order.hpp:997), `ExecutionAppliedEvent` (native_order.hpp:1136), + `MatchRejectedEvent` (native_order.hpp:1098), `CancelledEvent` + (native_order.hpp:1039) — readable in order from `native_events` + (native_host.hpp:1309). - **Any language.** The same kernel drives from C through a callback table (`pf_native_callbacks_v1` native_c_api.h:2454), so a host in Rust, Go, Python or Zig needs no C++. - **One binary, no transpiler.** Link `libpineforge`, ship an executable or a @@ -111,7 +111,7 @@ 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:1173); a C host sends `pf_native_run_spec_v1` +(native_host.hpp:1176); a C host sends `pf_native_run_spec_v1` (pineforge.h:546) plus `pf_native_run_spec_ext_v1` (native_c_api.h:2265) through `strategy_configure_native_ext_v1` (native_c_api.h:3107). Setup is atomic: `Ready` → begin → `Completed`, and an incomplete value is refused @@ -124,13 +124,13 @@ rather than defaulted. | `currency` | `currency` native_run_spec.hpp:570 | `currency` pineforge.h:535 | `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:538 | `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:538 | `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:538 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:3932-3933. TradingView's round-then-slip order is adapter-only. | -| `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:539 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:7466-7467, 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:538 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:3935-3936. TradingView's round-then-slip order is adapter-only. | +| `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:539 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:7487-7488, 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:541 | `native_open_lots_strategy.cpp` | Caps surviving+new physical lots `sha256:87bd0ff848901478840870d95eb356b35d7d368555ce8eadee8571b8e17f86ec` native_execution_consumer.cpp:2109-2112. Pine's per-cycle *entry count* is a TradingView rule, ruled **adapter-policy** in ADR-0001's table row `max_open_lots`. | | `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:2102 | `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:2106 | `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:2276 | `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:2276 | `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:863 is one call per accepted realtime print whatever the trigger is. | +| `calc_on_every_tick` | `NativeCalculationTrigger` native_run_spec.hpp:94 | `calculation` native_c_api.h:2276 | `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:2286 | `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:2290 | `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). | @@ -265,14 +265,14 @@ 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:931), which sees the kernel-resolved quantity as the facts' +(native_host.hpp:934), 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:2546 for a close. ## Orders {#pine_to_native_map_orders} -Everything here is one call: `submit` native_host.hpp:1190, or `submit_market` -native_host.hpp:1204 for the market-only shorthand, which *refuses* non-market +Everything here is one call: `submit` native_host.hpp:1193, or `submit_market` +native_host.hpp:1207 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:2678 over a @@ -283,10 +283,10 @@ is `strategy_native_submit_v1` native_c_api.h:2678 over a | `strategy.entry()` | `Request` native_order.hpp:483 carrying `Transact` native_order.hpp:41 | `strategy_native_submit_v1` native_c_api.h:2678 | `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:2678 | `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:2678 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:1152. 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`) closes the whole scope unfloored. | +| `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:1155. 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`) 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:1255 with `NativeRequestField::Label` native_host.hpp:679 | `strategy_native_cancel_where_v1` native_c_api.h:2765 | `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:1226 is the single-request form for a host that kept the handle. | -| `strategy.cancel_all()` | `cancel_all` native_host.hpp:1244 | `strategy_native_cancel_all_v1` native_c_api.h:2744 | `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:1240 before its arm, and all three cancels still address it. | +| `strategy.cancel()` | `cancel_where` native_host.hpp:1258 with `NativeRequestField::Label` native_host.hpp:679 | `strategy_native_cancel_where_v1` native_c_api.h:2765 | `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:1229 is the single-request form for a host that kept the handle. | +| `strategy.cancel_all()` | `cancel_all` native_host.hpp:1247 | `strategy_native_cancel_all_v1` native_c_api.h:2744 | `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:1243 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. | @@ -302,8 +302,8 @@ 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:1197, which emits -`ReplacedEvent` native_order.hpp:1010 and can carry the predecessor's live +quantity. Amending a live order is `replace` native_host.hpp:1200, which emits +`ReplacedEvent` native_order.hpp:1013 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 spelling is `strategy_native_replace_v1`, or `strategy_native_replace_ext_v1` @@ -317,7 +317,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 | `arm_scope` / `arm_first_match` native_c_api.h:2176-2177 | `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` native_c_api.h:2111 | `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:2116 | `native_trail_risk_strategy.cpp` | The absolute level the trail arms at. Read the live projection with `trail_state` native_host.hpp:1069 (C: `strategy_native_trail_state_v1` native_c_api.h:2986). 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:2116 | `native_trail_risk_strategy.cpp` | The absolute level the trail arms at. Read the live projection with `trail_state` native_host.hpp:1072 (C: `strategy_native_trail_state_v1` native_c_api.h:2986). 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 @@ -339,11 +339,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:996, and carries the installed level - in `ArmedEvent` native_order.hpp:1213. + `resolve_anchored_level` native_host.hpp:999, and carries the installed level + in `ArmedEvent` native_order.hpp:1216. - **Invisible until armed.** `NativeArmVisibility::PendingUntilArmed` native_order.hpp:394 keeps the legs out of `native_working_requests` - native_host.hpp:1240 until the fill, like Pine's pending exit and like a + native_host.hpp:1243 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: @@ -401,7 +401,7 @@ block is the `PF_NATIVE_SPEC_EXT_RISK` native_c_api.h:735 tail. | `strategy.risk.max_drawdown()` | `NativeRiskLimits::max_drawdown` native_run_spec.hpp:311 | `risk_max_drawdown` native_c_api.h:2312 | `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:2315 | `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:2317 | `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:2319 | `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:1300. | +| `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:2319 | `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:1303. | 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 @@ -427,12 +427,12 @@ liquidation level, and the ticket the forced close is booked under. | which equity it is tested against | `NativeMarginEquityBasis` native_run_spec.hpp:172 | `margin_equity_basis` native_c_api.h:2349 | `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:2350 | `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:2351 | `native_margin_strategy.cpp` | -| the host's own admission verdict | `validate_execution_precommit` native_host.hpp:944 | — C hosts gate through `on_precommit` native_c_api.h:2571 | `native_margin_strategy.cpp` | -| the host's own requirement | `resolve_margin_requirement` native_host.hpp:960 | `on_margin_requirement` native_c_api.h:2508 | `native_margin_strategy.cpp` | -| which check points run at all | `margin_check_allowed` native_host.hpp:972 | `on_margin_check` native_c_api.h:2517 | `native_margin_strategy.cpp` | +| the host's own admission verdict | `validate_execution_precommit` native_host.hpp:947 | — C hosts gate through `on_precommit` native_c_api.h:2571 | `native_margin_strategy.cpp` | +| the host's own requirement | `resolve_margin_requirement` native_host.hpp:963 | `on_margin_requirement` native_c_api.h:2508 | `native_margin_strategy.cpp` | +| which check points run at all | `margin_check_allowed` native_host.hpp:975 | `on_margin_check` native_c_api.h:2517 | `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:979 | `on_margin_call_units` native_c_api.h:2524 | `native_margin_strategy.cpp` | -| the liquidation event | `MarginCallEvent` native_order.hpp:1229 delivered to `on_native_margin_call` native_host.hpp:985 | `on_margin_call` native_c_api.h:2472 | `native_margin_strategy.cpp` | +| the host's own liquidation size | `resolve_margin_call_units` native_host.hpp:982 | `on_margin_call_units` native_c_api.h:2524 | `native_margin_strategy.cpp` | +| the liquidation event | `MarginCallEvent` native_order.hpp:1232 delivered to `on_native_margin_call` native_host.hpp:988 | `on_margin_call` native_c_api.h:2472 | `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 @@ -446,29 +446,29 @@ openings TradingView takes. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| script body at bar close | `on_native_bar` native_host.hpp:878 | `on_bar` native_c_api.h:2462 | `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:897, whose default forwards here. | -| every calculation, tagged with its reason | `on_native_recalculate` native_host.hpp:897 with `NativeCalculationReason` native_host.hpp:810 | `on_recalculate` native_c_api.h:2490 | `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:811 | `on_recalculate` native_c_api.h:2490 | `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:2462 | `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:900, whose default forwards here. | +| every calculation, tagged with its reason | `on_native_recalculate` native_host.hpp:900 with `NativeCalculationReason` native_host.hpp:813 | `on_recalculate` native_c_api.h:2490 | `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:2490 | `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:1754 | `native_market_strategy.cpp` | `Batch`, `Warmup`, `Realtime`. | -| bar-open pre-pass | `on_native_bar_open` native_host.hpp:874 | `on_bar_open` native_c_api.h:2461 | `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:1050 | `strategy_native_partial_bar_v1` native_c_api.h:2971 | `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:922 | `on_applied` native_c_api.h:2464 | `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:5314. | -| raw input before aggregation | `on_native_input` native_host.hpp:860 | `on_input` native_c_api.h:2459 | `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:912 | `on_sub_bar` native_c_api.h:2500 | `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:2461 | `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:1053 | `strategy_native_partial_bar_v1` native_c_api.h:2971 | `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:925 | `on_applied` native_c_api.h:2464 | `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:5335. | +| raw input before aggregation | `on_native_input` native_host.hpp:863 | `on_input` native_c_api.h:2459 | `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:915 | `on_sub_bar` native_c_api.h:2500 | `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:114 with `NativeCoordinate` market_driver.hpp:71 | `pf_native_decision_v1` native_c_api.h:1443 | `hello_kernel.cpp` | Copied onto the callback stack; mutating it changes nothing. In C++ `script_interval` market_driver.hpp:118 and `input_interval` market_driver.hpp:117 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 session-day facts are the three rows below. | | `session.ismarket` | `in_session` market_driver.hpp:155 | `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.isfirstbar` | `opens_session_day` market_driver.hpp:156 | `opens_session_day` native_c_api.h:1462 | `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:157, `closes_session_day_open_ended` market_driver.hpp:158 | `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:1064 | `price` native_c_api.h:1533 | `native_selected_strategy.cpp` | The readiness preview `inspect_current_execution` native_host.hpp:1076 is an observation, never an apply token; `execute_current` native_host.hpp:1081 is the apply. | +| the price at the current point | `current_execution_point` native_host.hpp:1067 | `price` native_c_api.h:1533 | `native_selected_strategy.cpp` | The readiness preview `inspect_current_execution` native_host.hpp:1079 is an observation, never an apply token; `execute_current` native_host.hpp:1084 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:1269, - `native_marked_equity` native_host.hpp:1278, `native_open_lots` - native_host.hpp:1274, `native_liquidation_price` native_host.hpp:1297. +1. **Live, inside a callback** — `physical_position` native_host.hpp:1272, + `native_marked_equity` native_host.hpp:1281, `native_open_lots` + native_host.hpp:1277, `native_liquidation_price` native_host.hpp:1300. 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` @@ -495,9 +495,9 @@ runs close the same trades for the same money. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `strategy.account_currency` | `currency` native_run_spec.hpp:570 | `currency` pineforge.h:535 | `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:1179. 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.account_currency` | `currency` native_run_spec.hpp:570 | `currency` pineforge.h:535 | `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:1182. 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:537 | `hello_kernel.cpp` | The value you declared, unchanged by the run. | -| `strategy.equity` | `native_marked_equity` native_host.hpp:1278 | `strategy_native_marked_equity_v1` native_c_api.h:3011 | `native_open_lots_strategy.cpp` | Pine marks at the current `close`; the native call takes the mark explicitly, so the caller says what "now" means. It equals the balance plus the open lots' `unrealized_pnl` native_host.hpp:341, which the example asserts at every calculation. | +| `strategy.equity` | `native_marked_equity` native_host.hpp:1281 | `strategy_native_marked_equity_v1` native_c_api.h:3011 | `native_open_lots_strategy.cpp` | Pine marks at the current `close`; the native call takes the mark explicitly, so the caller says what "now" means. It equals the balance plus the open lots' `unrealized_pnl` native_host.hpp:341, which the example asserts at every calculation. | | `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.grossprofit` | `gross_profit` engine.hpp:968 | `pf_trade_stats_t` pineforge.h:291 | `native_open_lots_strategy.cpp` | | @@ -524,15 +524,15 @@ runs close the same trades for the same money. | `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:2791 | `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:2791 | `native_selected_strategy.cpp` | Volume-weighted over the open lots. | -| `strategy.position_entry_name` | `native_open_lots` native_host.hpp:1274 then the last row's `entry_label` native_host.hpp:333 | `strategy_native_open_lot_get_v1` native_c_api.h:2832 | `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:1297 | `strategy_native_liquidation_price_v1` native_c_api.h:3019 | `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:582 | `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:1179 stages an immutable timestamped curve; codegen emits the Pine call as the identity, because no fixed table reproduces TradingView's series. | +| `strategy.position_entry_name` | `native_open_lots` native_host.hpp:1277 then the last row's `entry_label` native_host.hpp:333 | `strategy_native_open_lot_get_v1` native_c_api.h:2832 | `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:1300 | `strategy_native_liquidation_price_v1` native_c_api.h:3019 | `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:582 | `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:1182 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:582 | `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:1274 answers one `NativeOpenLot` +`native_open_lots(mark)` native_host.hpp:1277 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:2824 then @@ -550,7 +550,7 @@ native_order.hpp:445 names. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `strategy.opentrades` | `native_open_lots` native_host.hpp:1274 | `strategy_native_open_lot_count_v1` native_c_api.h:2824 | `native_open_lots_strategy.cpp` | The row count, which equals `lot_count` native_host.hpp:299. | +| `strategy.opentrades` | `native_open_lots` native_host.hpp:1277 | `strategy_native_open_lot_count_v1` native_c_api.h:2824 | `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:1730 | `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:1730 | `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:1730 | `native_open_lots_strategy.cpp` | Script-bar index of the opening fill. | @@ -569,7 +569,7 @@ native_order.hpp:445 names. 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:1007) keeps its own sampler, so for that +(`owns_lot_excursions` native_host.hpp:1010) keeps its own sampler, so for that run the two excursion fields fold `mark` alone. ## Closed trades {#pine_to_native_map_closed_trades} @@ -625,8 +625,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:1113 inside `on_native_run_begin` native_host.hpp:857 | `strategy_native_declare_subscriptions_v1` native_c_api.h:2896 | `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:870 and the latest is pulled with `native_series_bar` native_host.hpp:1096. `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:3169 | `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:912 instead. | +| `request.security()` | `NativeTimeframeSubscription` native_run_spec.hpp:485 in `subscriptions` native_run_spec.hpp:632, or `declare_timeframe_subscriptions` native_host.hpp:1116 inside `on_native_run_begin` native_host.hpp:860 | `strategy_native_declare_subscriptions_v1` native_c_api.h:2896 | `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:1099. `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:3169 | `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:915 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. | @@ -693,7 +693,7 @@ class HelloKernel : public pineforge::NativeStrategyHost { The host is a class; the run is one `NativeRunSpec` native_run_spec.hpp:553 naming the clock, instrument, account and fees; `configure_native` -native_host.hpp:1173 applies it and `run` engine.hpp:1709 drives the bars. +native_host.hpp:1176 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: @@ -733,14 +733,14 @@ Three rules make the C door behave like the C++ one: 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:1076 (its preview has no size-prefixed POD; +native_host.hpp:1079 (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:944 is -`on_precommit`, `resolve_anchored_level` native_host.hpp:996 `on_anchored_level`, +has a C route: `validate_execution_precommit` native_host.hpp:947 is +`on_precommit`, `resolve_anchored_level` native_host.hpp:999 `on_anchored_level`, the price and opening-shape halves of `resolve_execution_terms` -native_host.hpp:931 `on_execution_terms`, `native_sized_units` -native_host.hpp:1291 `strategy_native_sized_units_v1` and the +native_host.hpp:934 `on_execution_terms`, `native_sized_units` +native_host.hpp:1294 `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 lists every public member with either its C spelling or that reason, and @@ -834,7 +834,7 @@ void on_native_run_begin() override { } ``` -`native_series_bar(0)` native_host.hpp:1096 then answers the latest completed +`native_series_bar(0)` native_host.hpp:1099 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. @@ -933,13 +933,13 @@ configuring, or stops charging what the Pine declares, fails it. 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:1173 refuses an incomplete value + them, and `configure_native` native_host.hpp:1176 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:1306. + `native_events` native_host.hpp:1309. 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. @@ -1029,8 +1029,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:1306, ownership (which opening a close consumed), and the - continuation hash `native_continuation_hash` native_host.hpp:1347. + native_host.hpp:1309, ownership (which opening a close consumed), and the + continuation hash `native_continuation_hash` native_host.hpp:1350. 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_host.hpp b/include/pineforge/native_host.hpp index 968a6c68..0e2812c3 100644 --- a/include/pineforge/native_host.hpp +++ b/include/pineforge/native_host.hpp @@ -704,7 +704,10 @@ struct NativeCurrentExecution { /// readiness of execution::Status::UnrepresentableQuantity is a quantity the /// settlement cannot book exactly on this book, which execute_current answers /// as a MatchRejectedEvent with MatchRejectReason::UnrepresentableQuantity; -/// the run goes on. +/// the run goes on. The request core's check that the fill can be taken off +/// the request's own units, and the quantity grid's re-check of a boundary +/// Reduce, are made at execution only, so a request whose preview is Applied +/// can still be refused that way. struct NativeCurrentExecutionPreview { std::optional refusal; std::optional settlement_readiness; diff --git a/include/pineforge/native_order.hpp b/include/pineforge/native_order.hpp index 2adbcbf7..85747989 100644 --- a/include/pineforge/native_order.hpp +++ b/include/pineforge/native_order.hpp @@ -888,9 +888,12 @@ enum class MatchRejectReason : std::uint8_t { /// the position absorbs, or an opening the surviving book absorbs; or the /// request core cannot take the fill off the request's own units /// (CoreFailure::NonrepresentableQuantity: a scope of dust closed by a far - /// larger request, a point budget too small to move its remaining units). - /// The request ends here, nothing moves, and the run goes on; the same - /// request stopped the whole run before R5 lane K-ULP4. + /// larger request, a point budget too small to move its remaining units); + /// or an off-grid Reduce the grid admitted as a FIFO boundary of its scope + /// meets a book on which it no longer is one + /// (CommandContext::units_are_scope_boundary). The request ends here, + /// nothing moves, and the run goes on; the settlement's and the core's + /// cases stopped the whole run before R5 lane K-ULP4. UnrepresentableQuantity = 10, }; @@ -1425,7 +1428,10 @@ struct CommandContext { /// quantities a host chooses, and these are the book's own, which /// settlement arithmetic can move off any decimal grid; closing them takes /// whole lots and splits none (R5 lane K-ULP4). The execution consumer - /// measures it at submit; appended last, like the members above. + /// measures it at submit, for a request of ImmediateRemaining capacity, + /// and holds the candidate to the same test: a book that changed before + /// the match refuses it (MatchRejectReason::UnrepresentableQuantity). + /// Appended last, like the members above. bool units_are_scope_boundary = false; }; diff --git a/src/native_execution_consumer.cpp b/src/native_execution_consumer.cpp index 1aa2324a..aa0a7733 100644 --- a/src/native_execution_consumer.cpp +++ b/src/native_execution_consumer.cpp @@ -2823,8 +2823,11 @@ native_order::CommandContext NativeExecutionConsumer::make_command_context( if (const auto* spec = spec_ptr()) { ctx.quantity_grid = spec->quantity_grid; // A Reduce whose units are a FIFO boundary of its scope is on the grid - // as it stands (CommandContext::units_are_scope_boundary; R5 lane K-ULP4). - if (spec->quantity_grid && engine.position_side_ != PositionSide::FLAT) { + // as it stands (CommandContext::units_are_scope_boundary; R5 lane + // K-ULP4) -- one that closes in one fill, so the candidate can hold it + // to the same test (grid_boundary_holds). + if (spec->quantity_grid && engine.position_side_ != PositionSide::FLAT + && std::holds_alternative(request.capacity)) { if (const auto* reduce = std::get_if(&request.intent)) { if (const auto* units = std::get_if(&reduce->size)) { ctx.units_are_scope_boundary = scope_boundary_units(engine, request, units->units); @@ -4648,6 +4651,16 @@ bool NativeExecutionConsumer::admit_placement_units( nullptr); } +bool NativeExecutionConsumer::grid_boundary_holds(const BacktestEngine& engine, + const native_order::LiveRequest& live) const { + const auto* spec = spec_ptr(); + if (!spec || !spec->quantity_grid) return true; + const auto* reduce = std::get_if(&live.request().intent); + const auto* units = reduce ? std::get_if(&reduce->size) : nullptr; + if (!units || native_order::quantity_on_grid(units->units, *spec->quantity_grid)) return true; + return scope_boundary_units(engine, live.request(), units->units); +} + bool NativeExecutionConsumer::scope_boundary_units(const BacktestEngine& engine, const native_order::Request& request, double units) noexcept { @@ -5087,6 +5100,14 @@ std::optional NativeExecutionConsumer::consume_mat } } + // An off-grid Reduce the grid admitted at submit as a FIFO boundary of + // its scope is admitted only while it is one: if the book changed + // before it matched, closing it would split a lot off the grid, so it + // ends here with the same typed refusal (R5 lane K-ULP4). + if (!grid_boundary_holds(engine, *live)) { + return terminal(native_order::MatchRejectReason::UnrepresentableQuantity, + nonidentity_attempt); + } auto candidate = inspect_candidate(engine, *live, evaluation.cursor, resolved_price, execution_fx, plan ? &*plan : nullptr); const auto& inspect = candidate.inspect; diff --git a/src/native_execution_consumer.hpp b/src/native_execution_consumer.hpp index 76ed3166..0925d890 100644 --- a/src/native_execution_consumer.hpp +++ b/src/native_execution_consumer.hpp @@ -607,6 +607,11 @@ class NativeExecutionConsumer final : public IExecutionConsumer { static bool scope_boundary_units(const BacktestEngine& engine, const native_order::Request& request, double units) noexcept; + // An off-grid Reduce{ExplicitUnits} (admitted at submit as a FIFO boundary + // of its scope) is still one at its candidate; every other request, and + // any run without a quantity grid, answers true (R5 lane K-ULP4). + bool grid_boundary_holds(const BacktestEngine& engine, + const native_order::LiveRequest& live) const; double sibling_claimed_units(const native_order::LiveRequest& live) const noexcept; // L3b placement-time sizing. sizing_point_price answers the price a Sized // request's basis converts at when it is accepted; placement_scope_units diff --git a/tests/test_native_unrepresentable_refusal.cpp b/tests/test_native_unrepresentable_refusal.cpp index bd24115b..7b9676c5 100644 --- a/tests/test_native_unrepresentable_refusal.cpp +++ b/tests/test_native_unrepresentable_refusal.cpp @@ -43,6 +43,9 @@ // size (it would split the head lot off-grid), any // Transact and any other off-grid quantity are still // OffGrid +// grid-boundary-* the boundary is held at the candidate too: a book that +// changed after submit refuses it, typed; a point-budget +// Reduce of a boundary is gridded // scoped-dust a bound scope of 2^-55 closed by Reduce 1.0: the // request core cannot take 2^-55 off 1.0 -- the same // typed refusal (code 6, discriminator 2 on 91d65ad6) @@ -103,9 +106,14 @@ constexpr unsigned kUnrepresentable = const double kSurvivor = 1.5 - 1.3; // 0x1.9999999999998p-3 struct Step { - enum Kind { Transact, Reduce, Fraction, Flatten, BoundReduce } kind = Transact; + enum Kind { + Transact, Reduce, Fraction, Flatten, BoundReduce, + BudgetReduce, // Reduce{value} with a PointBudget{1} capacity + BoundThenReduce, // in ONE callback: BoundReduce{value, bind}, then an + // Independent Reduce{value}; the step reports the second + } kind = Transact; double value = 0.0; - int bind = -1; // BoundReduce: the step whose opening the Reduce binds to + int bind = -1; // BoundReduce / BoundThenReduce: the step whose opening it binds to }; struct Case { @@ -175,7 +183,14 @@ no::Request request_of(const Step& s) { case Step::Reduce: r.intent = no::Reduce{no::ExplicitUnits{s.value}}; break; case Step::Fraction: r.intent = no::Reduce{no::ScopeFraction{s.value}}; break; case Step::Flatten: r.intent = no::Flatten{}; break; - case Step::BoundReduce: r.intent = no::Reduce{no::ExplicitUnits{s.value}}; break; + case Step::BoundReduce: + case Step::BoundThenReduce: + r.intent = no::Reduce{no::ExplicitUnits{s.value}}; + break; + case Step::BudgetReduce: + r.intent = no::Reduce{no::ExplicitUnits{s.value}}; + r.capacity = no::PointBudget{1.0}; + break; } r.label = "step"; return r; @@ -197,10 +212,17 @@ class Host final : public NativeStrategyHost { StepOutcome out; auto request = request_of(case_.steps[k]); const int bind = case_.steps[k].bind; - if (case_.steps[k].kind == Step::BoundReduce && bind >= 0 - && static_cast(bind) < handles.size() && handles[bind]) { + const bool bound = case_.steps[k].kind == Step::BoundReduce + || case_.steps[k].kind == Step::BoundThenReduce; + if (bound && bind >= 0 && static_cast(bind) < handles.size() + && handles[bind]) { request.owner = no::BindOpening{*handles[bind], position_cycle_seq_}; } + if (case_.steps[k].kind == Step::BoundThenReduce) { + // The bound close goes first; the Independent one is the step. + (void)submit(request); + request.owner = no::Independent{}; + } const auto res = submit(request); handles.push_back(res.handle); if (res.status != no::SubmitStatus::Accepted || !res.handle) { @@ -421,7 +443,10 @@ int c_on_bar(void* user, const pf_bar_t*, const pf_native_decision_v1*) { r.intent_value = step.value; break; case Step::Flatten: r.intent = PF_NATIVE_INTENT_FLATTEN; break; - case Step::BoundReduce: return 1; // the C cases bind nothing + case Step::BoundReduce: + case Step::BudgetReduce: + case Step::BoundThenReduce: + return 1; // the C cases use none of these } std::uint64_t inc = 0; std::uint32_t reject = 0; @@ -750,6 +775,29 @@ void grid_cases() { CHECK(o.steps[5].book == std::vector({0.5, big})); } } + // The boundary must still hold when the Reduce matches. {A', 0.5}, A' = + // fl(1000.1 - 1000): a bound close of A' and an Independent Reduce{A'} in + // one callback -- the second is the head boundary at submit, but by its + // candidate the bound close has taken A', and on {0.5} it would split the + // on-grid lot off the grid: typed refusal, the 0.5 lot untouched. + { + const double big = 1000.1 - 1000.0; + const auto o = run_case({"grid-boundary-moved", + {{Step::Transact, 1000.1}, {Step::BoundReduce, 1000.0, 0}, {Step::Transact, 0.5}, + {Step::BoundThenReduce, big, 0}}, 0.1}); + filled(o, 2, 0.0, 0.5, 0.5, {big, 0.5}); + refused(o, 3, {0.5}); + } + // The exemption is for a close that settles in one fill: a point-budget + // Reduce of the same boundary is gridded. + { + const double big = 1000.1 - 1000.0; + const auto o = run_case({"grid-boundary-budget", + {{Step::Transact, 1000.1}, {Step::BoundReduce, 1000.0, 0}, {Step::Transact, 0.5}, + {Step::BudgetReduce, big}}, 0.1}); + CHECK(o.steps.size() == 4 + && o.steps[3].submit == static_cast(no::RequestRejectReason::OffGrid)); + } // AUDIT4 F5b/c/d: after the whole-scope close nothing is left to trip on. const std::vector dust_book = { {Step::Transact, 39.0}, {Step::Transact, 0.2}, {Step::Reduce, 38.9}, {Step::Fraction, 1.0}}; From c30ef7e1ea4760a0fb16196dddc51bf5ea8222f6 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Fri, 25 Sep 2026 23:04:00 +0800 Subject: [PATCH 09/24] Kernel: the grid's candidate tests what it settles; a whole-scope fraction is the gross scope settled in one fill (R5 lane K-ULP4, third review) The third review of the lane (d2932403) proved three defects in the grid rules, each with a probe (copied to the lane scratch, review3/): 1. The candidate re-check refused a whole-book close once the book had shrunk below it. A stop Reduce{fl(0.5 + A')}, A' = fl(1000.1 - 1000), is admitted on a 0.1 grid as the whole book; a partial close of 0.3 leaves {0.2, A'}; the stop fired and was refused (reason 10), where with no grid it closes the book. A quantity at least the scope's held total closes every lot whole, so the candidate now admits it. 2. The re-check tested the request's original units, not what the candidate settles. An OCA-Reduce opening's fill lowered a boundary Reduce's remaining to fl(U - 0.3), no boundary of {0.5, A', 0.3}, and the close split the on-grid 0.5 lot to 0.19999999999997725; a take-profit's fill lowered a stop's remaining to exactly the book it met, and the stop was refused. grid_boundary_holds now tests the remaining units, capped as inspect_candidate caps them: on the grid, a boundary of the scope as it stands, or at least its held total; anything else is the typed refusal. 3. A fraction of one resolved unfloored for any scope whose product it is: net of a live sibling's claim (it closed 0.30000000000002275 and left 0.19999999999997725; with the sibling claiming 0.6 it booked a 2.3e-14 close and left 0.49999999999997724), under a pending OCA-Reduce deduction, under a point budget. resolve_sized_units now resolves unfloored only the gross scope as it stands (scope == the scope's held total) for a request that settles it in one fill (ImmediateRemaining, no PendingDeferred), and own_quantity admits a closing size equal to the scope's held total under the same condition; everything else is floored exactly as before. Nothing else moves: the kulp battery (C++, C, staged; seeds 11/22/33), the decimal host at tolerance 0 / 1e-10 / 1e-9, the findings probes and the clean-set census are byte-identical to d2932403's (48/3091 moved, 48 fired). No run without a quantity grid is touched (the new conditions reduce to the old return value there). ADR-0001 rule 2 unchanged from f961f73b: (a) mechanism -- the grid admits the book's own quantities for every venue, now measured on what settles; (b) no knob. The ADR row states the refined reach. Tests: test_native_unrepresentable_refusal gains seven scripted cases (grid-stop-after-partial and its no-grid twin, grid-oca-lowered, grid-oca-take-then-stop, grid-fraction-net, -net-dust, -oca-pending, -budget): 547 checks. Fail-before: linked against d2932403's execution consumer the TU fails 23 of 547 checks, every one in the new cases. No new ctest row, floors unchanged. Citations carried from d2932403 by the exact line map (112 moved, none stuck; two spans lost the deleted line). Still open, pre-existing and not the grid's (91d65ad6 fails identically): an OCA-Reduce member's fill smaller than half an ulp of a sibling's remaining cannot be deducted from it, and the run stops with code 6, discriminator 7 (UnrepresentableReservation) after the fill is booked -- the post-commit group-effect contract the G5 rows pin. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 66dd8742c01035d0cdc6397058ff5866a52f86c8) --- docs/adr/0001-kernel-adapter-boundary.md | 20 +- docs/design/native-feature-parity.md | 116 +++--- docs/native-settlement.md | 35 +- docs/pages/native-engine.md | 37 +- docs/pages/pine-to-native.md | 14 +- include/pineforge/native_order.hpp | 20 +- src/native_execution_consumer.cpp | 90 +++-- src/native_execution_consumer.hpp | 20 +- tests/test_native_unrepresentable_refusal.cpp | 333 +++++++++++++++++- 9 files changed, 526 insertions(+), 159 deletions(-) diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index 5fd6fc72..ceffc8cf 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:7029-7031`) — + (`native_host.hpp:877`, `on_native_bar_open` `native_execution_consumer.cpp:7053-7055`) — `on_native_recalculate` for every calculation of the run (`native_host.hpp:900`), `on_native_sub_bar` (`native_host.hpp:915`) and post-fill `on_native_applied` (`native_host.hpp:925`). `on_bar` is `final` (`native_host.hpp:845`). @@ -98,7 +98,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:934`) returns no units is a `TermsUnresolved` rejection - (`TermsUnresolved` `native_execution_consumer.cpp:4923`). + (`TermsUnresolved` `native_execution_consumer.cpp:4950`). - **Observing executions.** Implement `on_native_applied` (`native_host.hpp:925`) — notifications drain FIFO after the outer callback returns — and/or poll `native_events(after_ordinal)` (`native_host.hpp:1309`). Do *not* implement @@ -109,7 +109,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:4896`) | + | Failure model | `NativeFailureCode` (`native_host.hpp:63-77`); `Failed` is latched, nothing resumes. A throwing callback latches `CallbackException` (`CallbackException` `native_execution_consumer.cpp:4923`) | | Cohorts | `cohort_open` (`native_host.hpp:1262`) / `cohort_add` (`:1265`) / `cohort_remove` (`:1268`) — the handle a `BindCohort` owner names | | Mid-callback execution | `current_execution_point` (`native_host.hpp:1067`), `inspect_current_execution` (`native_host.hpp:1079`) / `execute_current` (`native_host.hpp:1084`) | | 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:963`), `margin_check_allowed` (`native_host.hpp:975`), `resolve_margin_call_units` (`native_host.hpp:982`) — and `validate_execution_precommit` (`native_host.hpp:947`), whose `AdmitWithHostMargin` verdict hands the opening check to the host | @@ -450,7 +450,7 @@ see a mechanism, so these rows are held by reading, not by the checker): | the closed rows as the broker-state hash folds them — `BacktestEngine::trades_` under `pineforge-broker-state/v19`, through the consumer's running digest (`NativeExecutionConsumer::closed_rows_digest`, `src/native_execution_consumer.cpp`), and `NativeStrategyHost::native_closed_rows_amended` (`include/pineforge/native_host.hpp`) | the kernel books every row, at the end of the rows; the Pine host amends a booked row inside the applied notification of the execution that booked it (`PineStrategyHost::on_native_applied`: an aggregated chart dates the exit at its chart bar), and after those notifications have returned it reorders a bar's trailing same-bar bracket exits by command sequence (`sort_same_bar_exit_trades`, `src/source/pine_strategy_host.cpp`), naming the first row it moved | **a row is final once the applied notification of the execution that booked it has returned** (R5 lane V19-A). Rule 2 (a), mechanism: the finality point is the kernel's own notification boundary, the same for every host, and what a host that changes a final row supplies is data -- the first row it changed -- so a host of any venue drives the same surface and nothing in it knows Pine. Rule 2 (b), knob: none -- the call reports a change the host has already made and chooses nothing. A final row changed without a name keeps the value the digest took, and the Debug re-fold at each run's end (`verify_closed_rows`) aborts on it; rows removed from the end need no name, because the kernel only appends, so a read over fewer rows or a booking below the final mark forgets them. Every read answers the fold of the six fields v18 walked (entry and exit time and price, quantity, P&L) over the rows as they are, in their order, folded once per row per run instead of once per row per read. Pinned by part 7 of `tests/test_native_state_continuation.cpp` (kernel-only) and by every Debug ctest run, each of whose runs ends in the re-fold. | | the event record a run keeps — `NativeRunSpec::event_retention` (`include/pineforge/native_run_spec.hpp`), the journal window (`WorkingRequestCore::retire_history`, `include/pineforge/native_order.hpp`; `NativeExecutionConsumer::retire_journal`, `src/native_execution_consumer.cpp`) and the reader's acknowledgement (`NativeStrategyHost::native_acknowledge_events`, `native_event_window_start`, `include/pineforge/native_host.hpp`) | the host declares a retention in its spec and acknowledges what it has read; the Pine adapter declares `Window` in `PineExecutionAdapter::project()` and acknowledges its receipt cursor in `observe_terminal_receipts` (and "nothing read yet" at its run begin); the kernel retires at every script-bar boundary | **a reporting policy with a reader acknowledgement** (R5 lane V19-B). Rule 2 (a), mechanism: what a run keeps of its own event record, and a reader telling the kernel what it has consumed, the same for every host; the kernel never retires an event its own live state reads (a queued applied notification, a live deferred group-adjustment chain), and what the journal used to answer beyond that is state (next row). Rule 2 (b), knob: a spec field with no TradingView meaning -- `Window`, `Commands`, `Full` -- folded into the spec digest only off the default; a C caller that does not send the word keeps `Full`, the record its layout was published with. No trade, and no Pine value, depends on it: the adapter reads above its own cursor only. Pinned by `tests/test_native_event_retention.cpp` (kernel-only: `Full` row for row against the pre-window record, the window and the acknowledgement's edges, and the same books booking the same trades with the window closing at every driver point) and `check_event_retention` of `tests/test_native_c_api.c`. | | 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 and held again at its candidate, and a `ScopeFraction` whose product is its scope, unfloored); 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`. | +| 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`. | 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 (→ @@ -591,7 +591,7 @@ number names its symbol on this tree. | surface | ruling | reason | |---|---|---| | `cancel_all` (`native_host.hpp:1247`) / `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:2744`). | -| `cohort_add` (`native_host.hpp:1265`) / `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:872`, 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:1265`) / `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:1294`) / `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:3019`). | | `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:1681`) 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`. | @@ -601,7 +601,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:3961`) | **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:582`) | **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:7608-7620`); 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:1067-1072`) 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:7632-7644`); 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:1076-1081`) sees the same values, so no hash contract moves. | | `pf_native_lot_excursion_v1` (`native_c_api.h:1867`) | **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) @@ -659,7 +659,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: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` | | `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:8565-8568` (lookahead) and `clear_if_gapped` `:8559-8562` (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:8589-8592` (lookahead) and `clear_if_gapped` `:8583-8586` (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:2226`, `::gaps` `include/pineforge/native_c_api.h:2230` | 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` | @@ -674,9 +674,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:7116`, presenting `NativeDecisionContext::in_session` `include/pineforge/market_driver.hpp:155` 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:7819-7835` | 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:7796-7817` | 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:6568` | 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:7843-7859` | 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:7820-7841` | 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:6592` | 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 diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 0b866dc4..7448c955 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:2678-2689; `entry()` pine_adapter.cpp:5082, `order()` pine_adapter.cpp:10270) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5646-5693; `submit` native_host.hpp:1193 | K | — | F:A1 O:A1 S:O2 | — | +| OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:2678-2689; `entry()` pine_adapter.cpp:5082, `order()` pine_adapter.cpp:10270) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5672-5719; `submit` native_host.hpp:1193 | 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:8133 → `Flatten{}` pine_adapter.cpp:8162-8163; `Reduce{ExplicitUnits}` pine_adapter.cpp:7280-7289; `ReverseTo` pine_adapter.cpp:6114-6115) | **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 | — | | OL3 | Amend / cancel one working order (`submit_or_replace` pine_adapter.cpp:2648-3707 → `host.replace` pine_adapter.cpp:2664-2744) | **yes** — `replace` native_host.hpp:1198-1200, `cancel` native_host.hpp:1229; `ReplacedEvent` native_order.hpp:1013-1027; result and event types `SubmitStatus` native_order.hpp:835-861, `ReplacedEvent` native_order.hpp:997-1063 | 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:4515-4561, `cancel_all` pine_adapter.cpp:17806-17821; `live_by_source_key_` pine_adapter.hpp:1983; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:3857-3922) | **partial** — single-handle `cancel` native_host.hpp:1229; no enumeration (`WorkingRequestCore::live()` is consumer-internal native_order.hpp:1673); labels exist but are unindexed (`Request::label` native_order.hpp:485) **Closed:** `cancel_all` native_host.hpp:1247, `cancel_where(comment)` native_host.hpp:1252 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1252 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1243 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,18 +57,18 @@ 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:1084-1085 | 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:12779; lowering `submit_one` pine_adapter.cpp:8830-8842, `BindCohort` pine_adapter.cpp:9331-9337, `submit_leg` pine_adapter.cpp:9355-9379; 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:1852); 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:8435-8438; `materialize_relative_exits` pine_adapter.cpp:17096-17137, pine_adapter.hpp:1812-1824) | **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:5572-5606. `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:999 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:8435-8438; `materialize_relative_exits` pine_adapter.cpp:17096-17137, pine_adapter.hpp:1812-1824) | **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:5598-5632. `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:999 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:18857-18886) | **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:1243 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:1985-2003); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1116-1195); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3261-3428, `materialize_snapshot` pine_adapter.cpp:10670-10694), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1580-1603, `CloseCallsiteState` pine_adapter.hpp:1674-1689; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15406, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15640) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:659-661 | 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:1985-2003); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1116-1195); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3261-3428, `materialize_snapshot` pine_adapter.cpp:10670-10694), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1580-1603, `CloseCallsiteState` pine_adapter.hpp:1674-1689; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15406, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15640) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:660-662 | 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: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:855-887) | n/a | A | — | S:R4 O:A10 | TV half of OL9. **V19D-P1 (R5 lane B-ADAPTER):** a margin-call revival never restores an exit the script withdrew: `strategy.cancel` / `cancel_all` retire every leg of a dormant exit's lifecycle (`retire_cancelled_dormant_exits` pine_adapter.cpp:1795) and the revival skips it (`revive_brackets_after_margin` pine_adapter.cpp:1856), as the lab tv tapes badapter-v19dp1-control / -cancel (NYSE:F 15m) decide it — replayed by `tests/test_adapter_margin_revival_cancel.cpp` | ### 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:5658-5680; `NativeTrailState` native_host.hpp:657-662, `trail_state` native_host.hpp:1072 | 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:5684-5706; `NativeTrailState` native_host.hpp:657-662, `trail_state` native_host.hpp:1072 | 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:11554-11566) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4428-4433) **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:11467-11604) stays A. | +| TR3 | Zero-distance trail, "ride the best, exit on any adverse tick" (adapter substitutes `tick * 0.5` pine_adapter.cpp:11554-11566) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4438-4443) **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:11467-11604) stays A. | | TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:3814-3860) | **partial** — the read exists `trail_state` native_host.hpp:1072; `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:9381-9386, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11467-11604, retained best `retained_trail_best` pine_adapter.hpp:287-289, operand / restart rules `source_trail_points` pine_adapter.cpp:8405-8631, `trail_one_shot` pine_adapter.cpp:9470-9599 | n/a | A | — | F:B3 S:T2 | R5-1 | @@ -77,12 +77,12 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | SZ1 | Explicit units (`Transact{signed}` after a grid floor pine_adapter.cpp:5692-5696, pine_adapter.cpp:5692) | **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:10705-11882) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:932-936, invoked native_execution_consumer.cpp:4881-4900, guarded native_execution_consumer.cpp:4881-4916 | 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:2536-2591) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4923-4925 **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:2488, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:453 (→ SZ10) | +| SZ2 | Host override of the resolved price / units (`resolve_terms` pine_adapter.cpp:10705-11882) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:932-936, invoked native_execution_consumer.cpp:4909-4927, guarded native_execution_consumer.cpp:4909-4943 | 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:2536-2591) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4950-4952 **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:2488, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:453 (→ SZ10) | | SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:2497-2499) | **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:2549, `typed_entry_units` pine_adapter.cpp:2567; `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:2528; `percent_commission_live_equity` pine_adapter.cpp:4121-4161) | **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:2454-2469, `PineSizingSnapshot` pine_adapter.hpp:147-154; fill-time exceptions `default_sized` pine_adapter.cpp:6435-6461) | **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:1391-1408; host-sized terms snap (`ExecutionGridPolicy` native_order.hpp:71-85); exact grid validation `execution_terms_grid_representable` native_execution_consumer.cpp:4936-4939; 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:1391-1408; host-sized terms snap (`ExecutionGridPolicy` native_order.hpp:71-85); exact grid validation `execution_terms_grid_representable` native_execution_consumer.cpp:4962-4965; 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:12253-12281, `compute_exit_reservation` pine_adapter.cpp:4204-4325) | **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:4256-4579, `reserved_exit_qty` pine_adapter.cpp:8757-9067) | **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:444-451, `source_money_floor_lot` pine_adapter.cpp:453; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:4236-4251) | 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:2302) | **yes** — `allowed_open_directions` native_run_spec.hpp:596 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2921-2929 | 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:2935-2938 | 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:13001-13004) is TV. | -| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:595 → `MaxOpenLots` native_execution_consumer.cpp:2940-2943 | 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:5970-6016, cap left unset `max_open_lots` pine_adapter.cpp:2301-2303) is TV. | -| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13055-13120 and leaves the kernel gate unset pine_adapter.cpp:13054-13056) | **yes** — `initial_margin_fraction` native_run_spec.hpp:597; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2958 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:2808 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. | +| MG1 | Allowed opening directions (`allowed_open_directions` pine_adapter.cpp:2302) | **yes** — `allowed_open_directions` native_run_spec.hpp:596 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2931-2939 | 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:2945-2948 | 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:13001-13004) is TV. | +| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:595 → `MaxOpenLots` native_execution_consumer.cpp:2950-2953 | 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:5970-6016, cap left unset `max_open_lots` pine_adapter.cpp:2301-2303) is TV. | +| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13055-13120 and leaves the kernel gate unset pine_adapter.cpp:13054-13056) | **yes** — `initial_margin_fraction` native_run_spec.hpp:597; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2968 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:2817 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. | | MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13710-13711, `margin_pct` pine_adapter.cpp:13118) | **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:13810-13988, `submit_margin_call_units` pine_adapter.cpp:13949-13995, `schedule_margin_call_path` pine_adapter.cpp:17959-18036) | **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:2917 **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:13810-13988, `submit_margin_call_units` pine_adapter.cpp:13949-13995, `schedule_margin_call_path` pine_adapter.cpp:17959-18036) | **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:2927 **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:13741-13777) | **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:982 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:179-200) | **no** **Closed:** `native_liquidation_price()` native_host.hpp:1300 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:16057-16073, `apply_fx_opening_margin_slice` pine_adapter.cpp:18037-18065) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1182 but no margin re-check. **Closed by audit lane N6:** `NativeMarginCheckKind::FxRoll` — a step of the declared curve is a check point of its own, measured at the unchanged price before the first point the new rate converts (`tests/test_native_margin_fx_roll.cpp`). The adapter refuses the point by kind, for a measured reason since R5 lane F7: TradingView's rollover is a broker-open checkpoint at the open price, now at any positive margin (`apply_fx_open_margin_slice`, on the one TradingView money / units rule every other checkpoint uses), while the roll point measures at the remaining path's adverse mark and rests the slice there — a carried 1x long on a step bar O 100 L 97 books 0.4116 @ 97 on the adapter's run shape and 0.4036 @ 99, measured a bar early, on a strict calendar, against TradingView's 0.3996 @ 100 (`tests/test_native_margin_hooks_twin.cpp` MG-FX). Both N6 follow-ups are closed (§4.4). **Completed by R5 follow-up lane E3:** the conversion INSTANT is now generic too — every check kind converts at its own cursor's rate (`account_currency_fx_at(cursor.point.effective_time_ms)`) for the requirement, the equity and the level, in batch and stream, so a step landing between a fill's cursor and the clock the engine presents no longer breaches at the fill (`tests/test_native_margin_fx_clock.cpp`; N6's local clock swap inside `fx_roll_margin_check` is removed as redundant). **Extended by R5 follow-up lane E21** to the opening gate's placement site (MG4), and the rate-explicit equity E3 restated beside the engine's accessor is now that accessor's one implementation, `BacktestEngine::marked_equity_at` engine.hpp:729 | K | L4 → N6 → E3 → E21 | O:D8 | (single-source) | @@ -115,15 +115,15 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| CT1 | Calculate at script-bar close | **yes** — `on_native_bar` native_host.hpp:881, dispatched native_execution_consumer.cpp:7167-7188 (call site `sha256:fc29ccd415732193f24f2370c6aca8d6f8910bca5b95315e63e88f2befe8200b` native_execution_consumer.cpp:7178) | K | — | F:E1 O:F1 | — | -| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2259-2260) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7487-7491 | K | — | F:E2 O:F6 S:C1 | — | -| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:15893) | **yes** — `on_native_bar_open` native_host.hpp:877 runs before the open match native_execution_consumer.cpp:7029-7033; `execute_current` legal native_host.hpp:1084 | 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:6972 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7023 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1053 is the lookahead-free bar so far at the cursor, 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:881, dispatched native_execution_consumer.cpp:7191-7212 (call site `sha256:fc29ccd415732193f24f2370c6aca8d6f8910bca5b95315e63e88f2befe8200b` native_execution_consumer.cpp:7202) | K | — | F:E1 O:F1 | — | +| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2259-2260) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7511-7515 | K | — | F:E2 O:F6 S:C1 | — | +| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:15893) | **yes** — `on_native_bar_open` native_host.hpp:877 runs before the open match native_execution_consumer.cpp:7053-7057; `execute_current` legal native_host.hpp:1084 | 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:6996 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7047 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1053 is the lookahead-free bar so far at the cursor, 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:681; `begin_coof_recalc` pine_adapter.cpp:5400, `flush_coof_tail` pine_adapter.cpp:16035, `end_coof_recalc` pine_adapter.cpp:5421) | **partial** — `on_native_applied` native_host.hpp:925 fires mid-path after each fill native_execution_consumer.cpp:6883, native_execution_consumer.cpp:6862-6892; commands are legal there `commands_allowed` native_execution_consumer.cpp:1388-1396; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5572-5606, `remaining_path_delivery` native_order.cpp:2038-2046. 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:7111-7115; 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:284-286. | -| 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:7481-7598 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:915 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:2389-2445) | **yes** — `IntrabarPath` native_run_spec.hpp:362-402, delivery native_execution_consumer.cpp:7425-7616; `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. | +| 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:681; `begin_coof_recalc` pine_adapter.cpp:5400, `flush_coof_tail` pine_adapter.cpp:16035, `end_coof_recalc` pine_adapter.cpp:5421) | **partial** — `on_native_applied` native_host.hpp:925 fires mid-path after each fill native_execution_consumer.cpp:6907, native_execution_consumer.cpp:6886-6916; commands are legal there `commands_allowed` native_execution_consumer.cpp:1397-1405; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5598-5632, `remaining_path_delivery` native_order.cpp:2038-2046. 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:7135-7139; 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:284-286. | +| 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:7505-7622 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:915 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:2389-2445) | **yes** — `IntrabarPath` native_run_spec.hpp:362-402, delivery native_execution_consumer.cpp:7449-7640; `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:233-319) 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:5547), 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:681, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1655-1662 | n/a | A | — | F:E4 O:F5 S:C2 | R5-5: the adapter keeps its path | @@ -132,37 +132,37 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | HT1 | HTF series of the same symbol (`request.security`): evaluators registered `scheduler_configure_security_evaluators` pine_strategy_host.cpp:1513-1516, pumped from the scheduler `scheduler_prepare_security_sequence` pine_strategy_host.cpp:1421-1577; 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:1514 **Closed:** `NativeRunSpec::subscriptions` (`NativeTimeframeSubscription` native_run_spec.hpp:485), or `declare_timeframe_subscriptions` native_host.hpp:1116 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:873 and `native_series_bar(index)` native_host.hpp:1099 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:1419-1435 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, sole caller pine_strategy_host.cpp:1546; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1437-1442 **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:8235 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 c_abi.cpp:861) | **partial (inert)** — pre-run staging is accepted `refuse_source_mutation` native_execution_consumer.cpp:1428-1444 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, sole caller pine_strategy_host.cpp:1546; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1446-1451 **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:8259 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). | | 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:915 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:7819-7836, which feeds the same class `TimeframeAggregator` native_execution_consumer.cpp:7826, 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:7843-7860, which feeds the same class `TimeframeAggregator` native_execution_consumer.cpp:7850, 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:5646-5693, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:6229-6237; `Limit::fill_through` native_order.hpp:239-244 enforced native_execution_consumer.cpp:5030-5043, re-checked after host terms native_execution_consumer.cpp:5030-5050 | 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:4575-4576, clamp native_execution_consumer.cpp:4575 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:11721-11730) 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:5672-5719, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:6255-6263; `Limit::fill_through` native_order.hpp:239-244 enforced native_execution_consumer.cpp:5056-5069, re-checked after host terms native_execution_consumer.cpp:5056-5076 | 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:4585-4586, clamp native_execution_consumer.cpp:4585 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:11721-11730) is A | | FP3 | Commission: percent / cash per unit / cash per execution (`fee_kind_for` pine_adapter.cpp:463-470) | **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:256-259, `source_bar_fill` pine_adapter.cpp:11737-11746, `directional_tick` pine_adapter.cpp:301-305) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4037; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6311-6312; 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:256-259, `source_bar_fill` pine_adapter.cpp:11737-11746, `directional_tick` pine_adapter.cpp:301-305) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4047; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6337-6338; 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:332-380) 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:1182 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:8665). **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:1182 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:8689). **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:339-387 (used `source_trigger_threshold` pine_adapter.cpp:8822-8827, `exit_limit_trigger` pine_adapter.cpp:9356-9377), `source_bar_fill_tick` pine_adapter.cpp:277-290, raw-vs-booked spelling pine_adapter.cpp:11220-11326, margin-call fill pricing pine_adapter.cpp:13786-13795 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:11446) | 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:2591-2604, closing-row fold `max_runup` engine_orders.cpp:157-171; `owns_lot_excursions` native_host.hpp:1008-1012 | 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:2600-2613, closing-row fold `max_runup` engine_orders.cpp:157-171; `owns_lot_excursions` native_host.hpp:1008-1012 | 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). | | 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:1713) | **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:1438) stay A, measured in §3.7 part 2. | | 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:9239, 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:1350, 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:9263, 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:1350, native_execution_consumer.cpp:10662; 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:1955 **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` in batch and across a stream's warmup and realtime legs. 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. What a recorded row costs — quadratic on the Pine adapter, O(closed rows) per row in the kernel — is measured and ruled in §3.8 | | 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:2157, `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:546 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 57 `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 symbols are a second, disjoint inventory beside the 57 codegen-facing ones (`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:155 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:7231), 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:1794 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:155 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:7255), 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:1794 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:287-289) | 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:291-294 | 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:898-900 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1824-1827; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1857, `:8726`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2249-2250. 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:900-902 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1833-1836; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1866, `:8750`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2249-2250. 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:445-451) | 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:6382 | 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 native_execution_consumer.cpp:6408 | 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) | @@ -262,8 +262,8 @@ PF_API int pf_native_events_v1(pf_strategy_t, uint64_t after_ordinal, pf_native_ | Point | Decision | Src | |---|---|---| | Implementation | One new kernel TU (F `src/native_c_host.cpp`, O `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:2935-2945 | F O | -| Legality | The existing rule (`commands_allowed` native_execution_consumer.cpp:1388-1395): 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 `sha256:27ab3bf715fdc1eb1b9183cd41b2b8e1cf0d7cb38c1a313a371b285991bcc0b7` native_execution_consumer.cpp:2945-2955 | F O | +| Legality | The existing rule (`commands_allowed` native_execution_consumer.cpp:1397-1404): 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 | @@ -276,8 +276,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:654-668, `contribute_input` native_execution_consumer.cpp:7974, `seal_script` native_execution_consumer.cpp:7920) 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:7061), 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-670, `contribute_input` native_execution_consumer.cpp:7998, `seal_script` native_execution_consumer.cpp:7944) 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:7085), 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 | @@ -285,7 +285,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 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:1334-1418 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. 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:1437); `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:1446); `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). 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". @@ -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:2908-2975) 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:2909-2976) 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). @@ -336,7 +336,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:9109-9149 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:9133-9173 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:1053; and `set_native_security_feed` documented as the pre-run door for a series' authoritative bars, refused in-run | --- @@ -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:2141-2278) 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:1636-1640). 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:e3aa487ce8d69db9054576f75839345c0acaab55c3fc0a374c2f9c8c071559ec` AGENTS.md:132-145). A lane that cannot be shown neutral by construction does not merge. +- **(a) NEUTRALITY.** Every new behaviour is opt-in by a spec field or a new request kind. The adapter's `project()` (pine_adapter.cpp:2141-2278) 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:1638-1642). 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:e3aa487ce8d69db9054576f75839345c0acaab55c3fc0a374c2f9c8c071559ec` AGENTS.md:132-145). A lane that cannot be shown neutral by construction does not merge. - **(b) TWIN.** One chosen probe per lane runs through the adapter AND a hand-written `NativeStrategyHost` on the same bars and spec. Trade lists are diffed **by identity** (entry / exit time, price, qty, pnl, commission), never by trade number; then event, ownership and hash detail (S's D → E order). A difference is allowed only where a named TV-quirk row of §1 is active, and each one is itemized. Because 430 test files 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:1278). 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. @@ -406,7 +406,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:2298, 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:5577-5581) and `drain_applied_notifications` (native_execution_consumer.cpp:6238-6251) 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:5603-5607) and `drain_applied_notifications` (native_execution_consumer.cpp:6264-6277) 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:1334-1418 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) | @@ -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:4923-4925). 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 (`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:4950-4952). 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:3275-3464 +`sha256:8c97d3e1d9d6f036a9be0b50a3a43188f1e2eee23da0384fbed242829108c3c3` native_run_spec.hpp:239-302 and `sha256:efad6b9c7bcd3f7d0e459d434ef088a61b18c546d53cfb107f481a227fbe532b` native_execution_consumer.cpp:3285-3474 (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 | @@ -1158,10 +1158,10 @@ 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:953-957); test that a default spec hashes to the pre-change constant | 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 `sha256:620926caaa868e64ede32f547ac1dd0bfe81c9758267593a9a924051208df0a4` native_execution_consumer.hpp:955-959); 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:12916-12983, pine_adapter.cpp:13030); its chronology lives at `sha256:bb89768c9a7b2dc9f6de5943c04de8103ed107c21bb6e184446de0e6a84cf4c2` pine_adapter.cpp:13589-13666 and its excursion sample differs by chronology (`sha256:764a1a69f2940863b4f77cbf9d20267a19ec38e86720796c7081a59ac7245427` pine_strategy_host.cpp:729-759) | 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:2296-2336) | 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:5547) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `sha256:86617ca43bb2847380f1845b07cc9ecd27059814ee80fecf80649a9393dfe8c8` native_execution_consumer.cpp:3452-3461 and `drain_applied_notifications` (native_execution_consumer.cpp:6238-6251); new behaviour only behind new opt-ins | F | +| E4 | **Calc on fill.** Kernel suffix eligibility is continuous along the segment; TV refills only at O/H/L/C waypoints (`sha256:ddad8dc42e078838c0d53fd49e50c31060221e3b82e7d5bbaa0420769daa7d31` pine_scheduler_native.cpp:585-637, `coof_next_waypoint` pine_adapter.cpp:5547) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `sha256:86617ca43bb2847380f1845b07cc9ecd27059814ee80fecf80649a9393dfe8c8` native_execution_consumer.cpp:3462-3471 and `drain_applied_notifications` (native_execution_consumer.cpp:6264-6277); 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:15893-15921) | 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:1600-1619) | 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:332-380) 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 | @@ -1229,11 +1229,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:7155, native_execution_consumer.cpp:7155, native_execution_consumer.cpp:7155) 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:7179, native_execution_consumer.cpp:7179, native_execution_consumer.cpp:7179) 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:2298), 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:1010). 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:5021-5063), `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:5047-5089), `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) @@ -1256,7 +1256,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:675; terms resolution `sha256:b06b928e9f12217d7d3036f6adf77250c691c2ab1ad69d48e87417dbdfe1adc7` native_execution_consumer.cpp:2927-2973). 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 `sha256:b06b928e9f12217d7d3036f6adf77250c691c2ab1ad69d48e87417dbdfe1adc7` native_execution_consumer.cpp:2937-2983). 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. @@ -1283,9 +1283,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:12912-12933). New hashed `RequestDefinition::origin { Host, KernelLiquidation, KernelRisk }`, new `CancelReason::Superseded` on re-price, `MarginCallEvent` joins `CommandEvent` (17 → 18, native_order.hpp:1278). O's alternative, a checkpoint on the excursion walk (`sha256:ed6f4ff668195caf3c953c8ef5dd448c6010949353f7b9ef44d50b764f7488db` native_execution_consumer.cpp:1886-1899), 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:12912-12933). New hashed `RequestDefinition::origin { Host, KernelLiquidation, KernelRisk }`, new `CancelReason::Superseded` on re-price, `MarginCallEvent` joins `CommandEvent` (17 → 18, native_order.hpp:1278). O's alternative, a checkpoint on the excursion walk (`sha256:ed6f4ff668195caf3c953c8ef5dd448c6010949353f7b9ef44d50b764f7488db` native_execution_consumer.cpp:1895-1908), 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:4639-4652 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:4649-4662 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) @@ -1305,10 +1305,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:1615-1616) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6945-6957) 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:3465-3474). `born_on_remaining_path` (native_execution_consumer.cpp:5577-5581) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1655-1662) 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:1069-1076, whose body is native_execution_consumer.cpp:6795-6802). `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:2535); 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:1616-1617) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6969-6981) 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:3475-3484). `born_on_remaining_path` (native_execution_consumer.cpp:5603-5607) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1655-1662) 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:1071-1078, whose body is native_execution_consumer.cpp:6819-6826). `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:2544); 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) @@ -1346,7 +1346,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 (`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:3524-3529) +// 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:3534-3539) // 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; @@ -1374,7 +1374,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:3926-3948, `execute_current` path native_execution_consumer.cpp:3746-3756), `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:3936-3958, `execute_current` path native_execution_consumer.cpp:3756-3766), `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). ### A.8 L9 — risk limits (F P3 + O G7 + S P5) @@ -1394,7 +1394,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:1309) (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. - 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:13465-13563); 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:4639-4652 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:4649-4662 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 4b3ff18e..bf91cf85 100644 --- a/docs/native-settlement.md +++ b/docs/native-settlement.md @@ -117,21 +117,26 @@ refused, typed, as above -- the settlement's own refusal shows beforehand in `UnrepresentableQuantity`, while the request core's check and the grid's 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: a `ScopeFraction` whose product is its -scope -- `fraction == 1` -- resolves to the scope's held total, which the grid -does not floor; a `Reduce` whose units are a FIFO boundary of its scope -- the -binary64 sum, in book order, of the scope's lots through one of them: the head -lot's own size, a prefix, the whole scope -- is admitted at submit, for a -request that settles in one fill (no point budget), and closes whole lots; its -candidate holds it to the same test, so one that meets a changed book, on which -it is no longer a boundary, is refused, typed, instead of splitting a lot off -the grid; and a host-sized close answered with its scope's held total is -admitted at the candidate (R5 lane K-ULP4). Before, such a fraction was floored -onto the grid and closed up to a step short, left a dust lot, or found nothing -to close, and the book's own sizes were `OffGrid`. A `Transact` is still -gridded (it can open), and so is a `Reduce` of a quantity that is no boundary -of its scope: a later lot's size would be taken FIFO from the head lot and -split it off the grid. +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 +`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 +another total, is floored as before); a `Reduce` whose units are a FIFO +boundary of its scope -- the binary64 sum, in book order, of the scope's lots +through one of them: the head lot's own size, a prefix, the whole scope -- is +admitted at submit and closes whole lots; its candidate settles what then +remains -- which an OCA-Reduce sibling's fill can lower -- only on the grid, at +a boundary of the scope as it stands, or at least at the scope's held total, +which closes every lot whole (a whole-book stop still closes the book after a +partial close shrank it), and refuses anything else, typed, instead of +splitting a lot off the grid; and a host-sized close answered with its scope's +held total is admitted at the candidate (R5 lane K-ULP4). Before, such a +fraction was floored onto the grid and closed up to a step short, left a dust +lot, or found nothing to close, and the book's own sizes were `OffGrid`. A +`Transact` is still gridded (it can open), and so is a `Reduce` of a quantity +that is no boundary of its scope: a later lot's size would be taken FIFO from +the head lot and split it off the grid. ## Quantity tolerance diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 78657c0c..5828a21d 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -458,7 +458,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:9109-9149`), which admits the begin, checks the +(`native_execution_consumer.cpp:9133-9173`), 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 @@ -622,13 +622,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:5577-5581`). Requests accepted before the +`native_execution_consumer.cpp:5603-5607`). 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:7029-7031`). **Lookahead warning:** the +pass (`native_execution_consumer.cpp:7053-7055`). **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:6921`), the +`engine.current_bar_ = open_view` (`native_execution_consumer.cpp:6945`), 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 @@ -644,8 +644,9 @@ FIFO boundary of its scope -- the binary64 sum, in book order, of the scope's lots through one of them: the head lot's own size, a prefix, the whole scope -- which settlement arithmetic can move off any decimal grid (`CommandContext::units_are_scope_boundary`, R5 lane K-ULP4), for a request -that settles in one fill; its candidate is held to the same test, and one that -meets a changed book is refused with `MatchRejectReason::UnrepresentableQuantity`. +that settles in one fill; its candidate settles what then remains only on the +grid, at a boundary of the scope as it stands or at least at its held total, +and refuses anything else with `MatchRejectReason::UnrepresentableQuantity`. Flatten is not gridded. Rejection does not rewrite the attempted bits. **Acceptance is not a fill.** `submit_market` returns `SubmitResult`: @@ -1982,7 +1983,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:7765`), so +(`record_script_report_point`, `native_execution_consumer.cpp:7789`), so ```text broker_state_hash_len == equity_curve_len == script_bars_processed @@ -2776,7 +2777,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:7826`). What it does +(`TimeframeAggregator` `native_execution_consumer.cpp:7850`). 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 @@ -3098,9 +3099,10 @@ quantity stops the run. The FIFO rules are the settlement's side. Until K-ULP4 each of these stopped the run with code 6, discriminator 5. `Flatten` is never refused for a quantity. - On a `quantity_grid` the book's own quantities are on the grid: a - `ScopeFraction` whose product is its scope (`fraction == 1`) resolves to the - scope's held total, unfloored, and a `Reduce` of a FIFO boundary of its - scope is admitted (above). A fill the request's own units cannot absorb -- + `ScopeFraction` whose product is its scope's held total as it stands + (`fraction == 1` of the gross scope, settled in one fill) resolves to that + total, unfloored, and a `Reduce` of a FIFO boundary of its scope is + admitted (above). A fill the request's own units cannot absorb -- a scope of dust closed by a far larger request -- is the same typed refusal, from the request core's check. - `NativeRunSpec::quantity_tolerance` is the opt-in for a host that keeps @@ -3325,11 +3327,14 @@ because `scope * percent / 100` and `scope * (percent / 100)` are different binary64 values. The result is floored onto `quantity_grid` like every other engine quantity; a fraction that does not buy one whole step is `TermsUnresolved`, and `fraction` outside `(0, 1]` is `InvalidQuantityBasis` at -submit. The one exception is a fraction whose product is the scope itself -- -`fraction == 1`, "close it all": it resolves to the scope, 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). +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. 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 129b1f4a..24f15c1f 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -124,9 +124,9 @@ rather than defaulted. | `currency` | `currency` native_run_spec.hpp:570 | `currency` pineforge.h:535 | `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:538 | `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:538 | `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:538 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:3935-3936. TradingView's round-then-slip order is adapter-only. | -| `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:539 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:7487-7488, 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:541 | `native_open_lots_strategy.cpp` | Caps surviving+new physical lots `sha256:87bd0ff848901478840870d95eb356b35d7d368555ce8eadee8571b8e17f86ec` native_execution_consumer.cpp:2109-2112. Pine's per-cycle *entry count* is a TradingView rule, ruled **adapter-policy** in ADR-0001's table row `max_open_lots`. | +| `slippage` | `slippage_ticks` native_run_spec.hpp:583 | `slippage_ticks` pineforge.h:538 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:3945-3946. TradingView's round-then-slip order is adapter-only. | +| `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:539 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:7511-7512, 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:541 | `native_open_lots_strategy.cpp` | Caps surviving+new physical lots `sha256:87bd0ff848901478840870d95eb356b35d7d368555ce8eadee8571b8e17f86ec` native_execution_consumer.cpp:2118-2121. Pine's per-cycle *entry count* is a TradingView rule, ruled **adapter-policy** in ADR-0001's table row `max_open_lots`. | | `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:2102 | `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:2106 | `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:2276 | `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. | @@ -283,7 +283,7 @@ is `strategy_native_submit_v1` native_c_api.h:2678 over a | `strategy.entry()` | `Request` native_order.hpp:483 carrying `Transact` native_order.hpp:41 | `strategy_native_submit_v1` native_c_api.h:2678 | `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:2678 | `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:2678 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:1155. 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`) closes the whole scope unfloored. | +| `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:1155. 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:1258 with `NativeRequestField::Label` native_host.hpp:679 | `strategy_native_cancel_where_v1` native_c_api.h:2765 | `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:1229 is the single-request form for a host that kept the handle. | | `strategy.cancel_all()` | `cancel_all` native_host.hpp:1247 | `strategy_native_cancel_all_v1` native_c_api.h:2744 | `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:1243 before its arm, and all three cancels still address it. | @@ -291,7 +291,7 @@ is `strategy_native_submit_v1` native_c_api.h:2678 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:539 | `native_risk_limits_strategy.cpp` | The default. | -| `strategy.direction.long` | `NativeOpenDirections::Long` native_run_spec.hpp:74 | `allowed_open_directions` pineforge.h:539 | `tests/test_native_run_spec.cpp` | An opening the direction forbids is refused with `OpeningDirection` native_execution_consumer.cpp:2928. | +| `strategy.direction.long` | `NativeOpenDirections::Long` native_run_spec.hpp:74 | `allowed_open_directions` pineforge.h:539 | `tests/test_native_run_spec.cpp` | An opening the direction forbids is refused with `OpeningDirection` native_execution_consumer.cpp:2938. | | `strategy.direction.short` | `NativeOpenDirections::Short` native_run_spec.hpp:75 | `allowed_open_directions` pineforge.h:539 | `tests/test_native_run_spec.cpp` | Same, the other way. | | `strategy.commission.percent` | `NativeFeeKind::Percent` native_run_spec.hpp:20 | `fee_kind` pineforge.h:538 | `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:538 | `tests/test_native_selected_requests.cpp` | Account currency per unit. | @@ -396,7 +396,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:539 | `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:2928. | +| `strategy.risk.allow_entry_in()` | `allowed_open_directions` native_run_spec.hpp:596 | `allowed_open_directions` pineforge.h:539 | `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:2938. | | `strategy.risk.max_position_size()` | `max_abs_units` native_run_spec.hpp:594 | `max_abs_units` pineforge.h:540 | `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:2312 | `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:2315 | `native_risk_limits_strategy.cpp` | Percent is of the day's opening equity. Blocks to the end of its own day. | @@ -452,7 +452,7 @@ openings TradingView takes. | `barstate.isrealtime` | `NativeStateView::phase` native_host.hpp:285 with `NativeRunPhase` native_host.hpp:41 | `pf_native_state_v1` native_c_api.h:1754 | `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:2461 | `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:1053 | `strategy_native_partial_bar_v1` native_c_api.h:2971 | `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:925 | `on_applied` native_c_api.h:2464 | `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:5335. | +| a fill, as it happens | `on_native_applied` native_host.hpp:925 | `on_applied` native_c_api.h:2464 | `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:5361. | | raw input before aggregation | `on_native_input` native_host.hpp:863 | `on_input` native_c_api.h:2459 | `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:915 | `on_sub_bar` native_c_api.h:2500 | `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:114 with `NativeCoordinate` market_driver.hpp:71 | `pf_native_decision_v1` native_c_api.h:1443 | `hello_kernel.cpp` | Copied onto the callback stack; mutating it changes nothing. In C++ `script_interval` market_driver.hpp:118 and `input_interval` market_driver.hpp:117 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 session-day facts are the three rows below. | diff --git a/include/pineforge/native_order.hpp b/include/pineforge/native_order.hpp index 85747989..415d9db7 100644 --- a/include/pineforge/native_order.hpp +++ b/include/pineforge/native_order.hpp @@ -889,8 +889,8 @@ enum class MatchRejectReason : std::uint8_t { /// request core cannot take the fill off the request's own units /// (CoreFailure::NonrepresentableQuantity: a scope of dust closed by a far /// larger request, a point budget too small to move its remaining units); - /// or an off-grid Reduce the grid admitted as a FIFO boundary of its scope - /// meets a book on which it no longer is one + /// or what an off-grid Reduce the grid admitted as a FIFO boundary of its + /// scope settles no longer closes whole lots of the book it meets /// (CommandContext::units_are_scope_boundary). The request ends here, /// nothing moves, and the run goes on; the settlement's and the core's /// cases stopped the whole run before R5 lane K-ULP4. @@ -1424,14 +1424,14 @@ struct CommandContext { /// A Reduce's ExplicitUnits are a FIFO boundary of its scope: the binary64 /// sum, in book order, of the scope's lots through one of them -- the head /// lot's own size, a prefix, the whole scope. Such a quantity is on the - /// quantity grid whatever the grid predicate answers: the grid admits the - /// quantities a host chooses, and these are the book's own, which - /// settlement arithmetic can move off any decimal grid; closing them takes - /// whole lots and splits none (R5 lane K-ULP4). The execution consumer - /// measures it at submit, for a request of ImmediateRemaining capacity, - /// and holds the candidate to the same test: a book that changed before - /// the match refuses it (MatchRejectReason::UnrepresentableQuantity). - /// Appended last, like the members above. + /// quantity grid whatever the grid predicate answers: these are the book's + /// own quantities, which settlement arithmetic can move off any decimal + /// grid; closing them takes whole lots and splits none (R5 lane K-ULP4). + /// The execution consumer measures it at submit, for a request of + /// ImmediateRemaining capacity; its candidate settles what then remains + /// only on the grid, at a boundary of the scope as it stands or at least at + /// its held total, and refuses anything else (MatchRejectReason:: + /// UnrepresentableQuantity). Appended last, like the members above. bool units_are_scope_boundary = false; }; diff --git a/src/native_execution_consumer.cpp b/src/native_execution_consumer.cpp index aa0a7733..77c24a02 100644 --- a/src/native_execution_consumer.cpp +++ b/src/native_execution_consumer.cpp @@ -462,9 +462,18 @@ bool explicit_reduction_units_representable(double units, double exposure) noexc return order_action::plan(exposure, order_action::Reduce{units}).has_value(); } -// `own_quantity`: the answered units are the book's own -- a closing size equal -// to its scope's held total, or a fraction's whole scope -- which the quantity -// grid admits as they stand (R5 lane K-ULP4). +// 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); +} + +// `own_quantity`: the answered units are the book's own -- a closing size, +// settled in one fill, equal to its scope's held total (which a fraction of one +// resolves to) -- which the quantity grid admits as they stand (R5 lane K-ULP4). bool execution_terms_grid_representable( const native_order::ExecutionTerms& terms, const native_order::HostSized* host_sized, bool unresolved, @@ -2824,13 +2833,14 @@ native_order::CommandContext NativeExecutionConsumer::make_command_context( ctx.quantity_grid = spec->quantity_grid; // A Reduce whose units are a FIFO boundary of its scope is on the grid // as it stands (CommandContext::units_are_scope_boundary; R5 lane - // K-ULP4) -- one that closes in one fill, so the candidate can hold it - // to the same test (grid_boundary_holds). + // K-ULP4) -- one that closes in one fill, so the candidate can hold + // what it settles to the same test (grid_boundary_holds). if (spec->quantity_grid && engine.position_side_ != PositionSide::FLAT && std::holds_alternative(request.capacity)) { if (const auto* reduce = std::get_if(&request.intent)) { if (const auto* units = std::get_if(&reduce->size)) { - ctx.units_are_scope_boundary = scope_boundary_units(engine, request, units->units); + ctx.units_are_scope_boundary = + scope_boundary_units(engine, request, units->units, false); } } } @@ -4652,18 +4662,31 @@ bool NativeExecutionConsumer::admit_placement_units( } bool NativeExecutionConsumer::grid_boundary_holds(const BacktestEngine& engine, - const native_order::LiveRequest& live) const { + const native_order::LiveRequest& live, + const native_order::MatchCursor& cursor) const { const auto* spec = spec_ptr(); if (!spec || !spec->quantity_grid) return true; const auto* reduce = std::get_if(&live.request().intent); const auto* units = reduce ? std::get_if(&reduce->size) : nullptr; if (!units || native_order::quantity_on_grid(units->units, *spec->quantity_grid)) return true; - return scope_boundary_units(engine, live.request(), units->units); + // Admitted only as a FIFO boundary of its scope. What this candidate + // settles -- the remaining units, which an OCA-Reduce sibling's fill can + // lower, capped as inspect_candidate caps them -- must still close whole + // lots: be on the grid, a boundary of the scope as it stands, or at least + // its whole held total. + const auto* remaining = std::get_if(&live.remaining); + if (!remaining) return true; + double qty = remaining->q; + if (const auto* allowance = std::get_if(&live.allowance)) { + if (allowance->point_ordinal == cursor.point.ordinal) qty = std::min(qty, allowance->left); + } + if (native_order::quantity_on_grid(qty, *spec->quantity_grid)) return true; + return scope_boundary_units(engine, live.request(), qty, /*or_whole_scope=*/true); } bool NativeExecutionConsumer::scope_boundary_units(const BacktestEngine& engine, const native_order::Request& request, - double units) noexcept { + double units, bool or_whole_scope) noexcept { const auto* one = std::get_if(&request.owner); const auto* many = std::get_if(&request.owner); if (!one && !many && !std::holds_alternative(request.owner)) { @@ -4686,13 +4709,15 @@ bool NativeExecutionConsumer::scope_boundary_units(const BacktestEngine& engine, sum += lot.qty; if (sum == units) return true; } - return false; + // At least the scope's whole held total closes every lot of it whole: the + // walk takes each lot while the rest exceeds it, and a bound scope's close + // is capped to what the scope holds. + return or_whole_scope && units >= sum; } std::optional NativeExecutionConsumer::resolve_sized_units( const BacktestEngine& engine, const native_order::LiveRequest& live, - const NativeExecutionTermsFacts& facts, bool* whole_scope) const { - if (whole_scope) *whole_scope = false; + const NativeExecutionTermsFacts& facts) const { const auto* spec = spec_ptr(); if (!spec) return std::nullopt; if (const auto* native_sized = sized_intent(live)) { @@ -4731,13 +4756,16 @@ std::optional NativeExecutionConsumer::resolve_sized_units( // caller converts percent -> fraction itself, so no second rounding step // enters here. const double units = scope * fraction->fraction; - // A fraction whose product is the scope itself -- fraction 1, "close it - // all" -- resolves to the scope, which the grid does not floor. The scope - // is the binary64 fold of the book's own lots, which 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 (R5 lane K-ULP4). - if (units == scope) { - if (whole_scope) *whole_scope = true; + // A fraction whose product is its scope's own held total -- fraction 1, + // "close it all", of the gross scope as it stands -- resolves to that + // total, which the grid does not floor, for a request that settles it in + // one fill. The total is the binary64 fold of the book's own lots, which + // 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)) { return scope; } return representable_units(units, native_order::ExecutionGridPolicy::SnapToGrid, @@ -4880,9 +4908,8 @@ std::optional NativeExecutionConsumer::consume_mat // and publishes the result as the facts' remaining units, so a host // override of resolve_execution_terms still has the last word. std::optional kernel_units; - bool whole_scope = false; if (unresolved && (native_sized || scope_fraction)) { - kernel_units = resolve_sized_units(engine, *live, terms_facts, &whole_scope); + kernel_units = resolve_sized_units(engine, *live, terms_facts); if (kernel_units) { terms_facts.remaining = native_order::RemainingUnits{*kernel_units}; } @@ -4934,9 +4961,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 - && ((whole_scope && kernel_units && *terms.units == *kernel_units) - || *terms.units == terms_facts.scope_exposure_units); + const bool own_quantity = closing_size && terms.units && settles_in_one_fill(*live) + && *terms.units == terms_facts.scope_exposure_units; if (!execution_terms_grid_representable( terms, host_sized, unresolved, terms_facts.scope_exposure_units, spec_ptr(), own_quantity)) { @@ -5101,10 +5127,10 @@ std::optional NativeExecutionConsumer::consume_mat } // An off-grid Reduce the grid admitted at submit as a FIFO boundary of - // its scope is admitted only while it is one: if the book changed - // before it matched, closing it would split a lot off the grid, so it - // ends here with the same typed refusal (R5 lane K-ULP4). - if (!grid_boundary_holds(engine, *live)) { + // its scope settles only what still closes whole lots: if its book or + // remaining units changed so that closing them would split a lot off + // the grid, it ends here with the same typed refusal (R5 lane K-ULP4). + if (!grid_boundary_holds(engine, *live, evaluation.cursor)) { return terminal(native_order::MatchRejectReason::UnrepresentableQuantity, nonidentity_attempt); } @@ -6434,9 +6460,8 @@ NativeCurrentExecutionPreview NativeExecutionConsumer::inspect_current_execution native_order::NativeCandidatePriceKind::CurrentQuote, command.price_rule, raw_price, default_resolved); std::optional kernel_units; - bool whole_scope = false; if (unresolved && (native_sized || scope_fraction)) { - kernel_units = resolve_sized_units(engine, *live, facts, &whole_scope); + kernel_units = resolve_sized_units(engine, *live, facts); if (kernel_units) facts.remaining = native_order::RemainingUnits{*kernel_units}; } native_order::ExecutionTerms terms; @@ -6484,9 +6509,8 @@ NativeCurrentExecutionPreview NativeExecutionConsumer::inspect_current_execution out.terms_rejection = native_order::MatchRejectReason::InvalidTerms; return out; } - const bool own_quantity = closing_size && terms.units - && ((whole_scope && kernel_units && *terms.units == *kernel_units) - || *terms.units == facts.scope_exposure_units); + const bool own_quantity = closing_size && terms.units && settles_in_one_fill(*live) + && *terms.units == facts.scope_exposure_units; if (!execution_terms_grid_representable( terms, host_sized, unresolved, facts.scope_exposure_units, spec_ptr(), own_quantity)) { diff --git a/src/native_execution_consumer.hpp b/src/native_execution_consumer.hpp index 0925d890..bf9b2396 100644 --- a/src/native_execution_consumer.hpp +++ b/src/native_execution_consumer.hpp @@ -593,25 +593,27 @@ class NativeExecutionConsumer final : public IExecutionConsumer { // L3 kernel sizing bases. resolve_sized_units answers the units a Sized // opening or a ScopeFraction reduce claims at this candidate; nullopt is // the nonrepresentable basis the matching path reports as TermsUnresolved. - // `whole_scope`, when given, says whether the answer is a fraction's - // whole scope, which the quantity grid does not floor (R5 lane K-ULP4). std::optional resolve_sized_units( const BacktestEngine& engine, const native_order::LiveRequest& live, - const NativeExecutionTermsFacts& facts, bool* whole_scope = nullptr) const; + const NativeExecutionTermsFacts& facts) const; // Whether `units` is a FIFO boundary of the request's scope -- the binary64 // sum of the scope's lots, in book order, through one of them: the head - // lot's own size, a prefix, the whole scope. The scope is the book for an + // lot's own size, a prefix, the whole scope -- or, with `or_whole_scope`, + // at least the whole scope's held total. The scope is the book for an // Independent request and the bound openings' lots for BindOpening / // BindOpenings; any other owner has none here. The quantity grid admits // such a Reduce as it stands (R5 lane K-ULP4). static bool scope_boundary_units(const BacktestEngine& engine, const native_order::Request& request, - double units) noexcept; - // An off-grid Reduce{ExplicitUnits} (admitted at submit as a FIFO boundary - // of its scope) is still one at its candidate; every other request, and - // any run without a quantity grid, answers true (R5 lane K-ULP4). + double units, bool or_whole_scope) noexcept; + // What an off-grid Reduce{ExplicitUnits} (admitted at submit as a FIFO + // boundary of its scope) settles at this candidate still closes whole lots: + // it is on the grid, a boundary of the scope as it stands, or at least the + // scope's whole held total. Every other request, and any run without a + // quantity grid, answers true (R5 lane K-ULP4). bool grid_boundary_holds(const BacktestEngine& engine, - const native_order::LiveRequest& live) const; + const native_order::LiveRequest& live, + const native_order::MatchCursor& cursor) const; double sibling_claimed_units(const native_order::LiveRequest& live) const noexcept; // L3b placement-time sizing. sizing_point_price answers the price a Sized // request's basis converts at when it is accepted; placement_scope_units diff --git a/tests/test_native_unrepresentable_refusal.cpp b/tests/test_native_unrepresentable_refusal.cpp index 7b9676c5..0479dd6e 100644 --- a/tests/test_native_unrepresentable_refusal.cpp +++ b/tests/test_native_unrepresentable_refusal.cpp @@ -52,6 +52,17 @@ // grid-after-dust the AUDIT4 F5b/c/d continuations: open, an oversized // Reduce and a reversal after the whole-scope close // controls the lot's own size and the host's total, which close +// grid-stop-*, scripted, on {0.5, A'}: the candidate tests what it +// grid-oca-* settles -- a whole-book stop fired after a partial +// close shrank the book closes it whole, as with no grid; +// an OCA-Reduce opening's fill that lowers a boundary +// Reduce's remaining to no boundary is a typed refusal; +// a take-profit's fill that lowers the stop's remaining +// to the book it meets closes that book whole +// grid-fraction-* a fraction of one net of a live sibling's claim, with a +// pending OCA-Reduce deduction or under a point budget +// settles something else, and is floored exactly as a +// fraction just below one is // Every C++ case runs on the request core's staged and direct paths, which // must agree bit for bit, hashes included, and every run completes. // @@ -60,7 +71,11 @@ // name spelled 10 it runs there and fails 100 of 350 checks: every refusal // case stops its run with code 6, discriminator 5 (scoped-dust with // discriminator 2), and every grid case keeps its residual, its dust lot or -// its OffGrid refusal; only the controls pass. +// its OffGrid refusal; only the controls pass. The scripted cases came with +// the lane's third review: linked against d2932403's execution consumer they +// fail 23 of 547 checks -- the stops are refused, the OCA-lowered close splits +// the 0.5 lot to 0.19999999999997725, and each fraction closes its unfloored +// scope instead of the floored one. // Source-free: the kernel-only profile registers the row. #include "../src/engine_internal.hpp" #include "../src/native_execution_consumer.hpp" @@ -73,10 +88,13 @@ #include #include #include +#include #include +#include #include #include #include +#include #include #include @@ -836,11 +854,324 @@ void grid_cases() { } } +// ---- Scripted cases: resting triggers, OCA-Reduce groups, claims ------------ +// Each bar may place requests. Bars 0-4 build {0.5, A'} with A' = fl(1000.1 - +// 1000) off the 0.1 grid: open 0.5, open 1000.1, then a close of 1000 bound to +// the second opening. The book's own quantities are 0.5, U = fl(0.5 + A') and +// what settlement leaves of them. + +class ScriptHost; +using Script = std::map>; + +class ScriptHost final : public NativeStrategyHost { +public: + ScriptHost(Script script, bool direct) : script_(std::move(script)) { + 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 auto it = script_.find(bar_++); + if (it != script_.end()) it->second(*this); + } + void put(const std::string& label, no::Request r) { + r.label = label; + const auto res = submit(r); + if (res.status == no::SubmitStatus::Accepted && res.handle) handles[label] = *res.handle; + else submits[label] = res.reason ? static_cast(*res.reason) : -2; + } + std::vector book() const { + std::vector out; + for (const auto& lot : native_open_lots(std::numeric_limits::quiet_NaN())) { + out.push_back(lot.signed_units); + } + return out; + } + std::vector row_units(std::size_t first, std::size_t n) const { + std::vector out; + for (std::size_t i = first; i < first + n && i < trades_.size(); ++i) { + out.push_back(trades_[i].qty); + } + return out; + } + std::int64_t cycle() const { return position_cycle_seq_; } + + std::map handles; + std::map submits; // the refused submits' RequestRejectReason + +private: + Script script_; + int bar_ = 0; +}; + +struct Terminal { + int kind = 0; // 0 none, 1 applied, 2 MatchRejected, 3 NoEffect + unsigned reason = 0; // the MatchRejectReason + std::vector closed, rows; + bool operator==(const Terminal& o) const { + return kind == o.kind && reason == o.reason && closed == o.closed && rows == o.rows; + } +}; + +struct ScriptOutcome { + bool completed = false; + unsigned code = 0, discriminator = 0; + std::map submits; + std::map terminals; + std::vector book; + std::uint64_t continuation = 0, broker = 0; + bool operator==(const ScriptOutcome& o) const { + return completed == o.completed && code == o.code && discriminator == o.discriminator + && submits == o.submits && terminals == o.terminals && book == o.book + && continuation == o.continuation && broker == o.broker; + } +}; + +ScriptOutcome run_script_once(const char* name, const Script& script, std::optional grid, + const std::vector& bars, bool direct) { + ScriptHost host(script, direct); + Case c{name, {}, grid}; + CHECK(host.configure_native(spec_for(c)).status == NativeSetupStatus::Applied); + host.run(bars.data(), static_cast(bars.size())); + ScriptOutcome out; + const auto state = host.native_state(); + out.completed = state.kind == NativeLifecycleKind::Completed; + out.code = static_cast(state.failure.code); + out.discriminator = static_cast(state.failure.discriminator); + out.submits = host.submits; + for (const auto& [label, handle] : host.handles) { + Terminal t; + for (const auto& row : host.native_events(0)) { + if (!row.command) continue; + if (const auto* a = std::get_if(&*row.command)) { + if (a->handle().incarnation != handle.incarnation) continue; + t.closed.push_back(a->closed_units); + const auto rows = host.row_units(a->first_trade_index, a->closed_trade_count); + t.rows.insert(t.rows.end(), rows.begin(), rows.end()); + if (a->terminal) t.kind = 1; + } else if (const auto* r = std::get_if(&*row.command)) { + if (r->handle().incarnation != handle.incarnation) continue; + t.kind = 2; + t.reason = static_cast(r->reason); + } else if (const auto* e = std::get_if(&*row.command)) { + if (e->handle().incarnation == handle.incarnation) t.kind = 3; + } + } + out.terminals[label] = t; + } + out.book = host.book(); + out.continuation = host.native_continuation_hash(); + out.broker = host.broker_state_hash(); + return out; +} + +// Both twins, required equal and complete. +ScriptOutcome run_script(const char* name, const Script& script, std::optional grid, + const std::vector& bars) { + scenario = name; + const ScriptOutcome staged = run_script_once(name, script, grid, bars, false); + const ScriptOutcome direct = run_script_once(name, script, grid, bars, true); + std::printf("%s: completed=%d code=%u book:", name, staged.completed ? 1 : 0, staged.code); + for (double lot : staged.book) std::printf(" %a", lot); + std::printf("\n"); + for (const auto& [label, t] : staged.terminals) { + std::printf(" %-12s terminal=%d reason=%u closed:", label.c_str(), t.kind, t.reason); + for (double q : t.closed) std::printf(" %a", q); + std::printf(" rows:"); + for (double q : t.rows) std::printf(" %a", q); + std::printf("\n"); + } + CHECK(staged == direct); + CHECK(staged.completed); + CHECK(staged.code == 0 && staged.discriminator == 0); + return staged; +} + +std::vector flat_bars(int n, std::map> high_low = {}) { + std::vector bars; + for (int i = 0; i < n; ++i) { + double high = kPrice, low = kPrice; + const auto it = high_low.find(i); + if (it != high_low.end()) std::tie(high, low) = it->second; + bars.push_back(Bar{kPrice, high, low, kPrice, 10.0, 60'000LL * (i + 1)}); + } + return bars; +} + +no::Request script_request(no::OrderIntent intent) { + no::Request r; + r.intent = std::move(intent); + return r; +} + +// {0.5, A'} by bar 4. +Script dust_book_script() { + Script s; + s[0] = [](ScriptHost& h) { h.put("open-0.5", script_request(no::Transact{0.5})); }; + s[2] = [](ScriptHost& h) { h.put("open-1000.1", script_request(no::Transact{1000.1})); }; + s[4] = [](ScriptHost& h) { + auto r = script_request(no::Reduce{no::ExplicitUnits{1000.0}}); + r.owner = no::BindOpening{h.handles.at("open-1000.1"), h.cycle()}; + h.put("bound-1000", r); + }; + return s; +} + +const Terminal& terminal_of(const ScriptOutcome& o, const char* label) { + static const Terminal none; + const auto it = o.terminals.find(label); + CHECK(it != o.terminals.end()); + return it == o.terminals.end() ? none : it->second; +} + +void scripted_grid_cases() { + const double a = 1000.1 - 1000.0; // A' = 0x1.99999999a3p-4 + const double u = 0.5 + a; // U, the book's own total + const double two = 0.5 - 0.3; // what a close of 0.3 leaves of the 0.5 lot + CHECK(!no::quantity_on_grid(a, 0.1) && !no::quantity_on_grid(u, 0.1)); + CHECK(!no::quantity_on_grid(two + a, 0.1)); + // A whole-book stop, placed as the book's own total, fires after a partial + // close shrank the book below it: what it settles is at least the scope's + // whole held total, so it closes every lot whole -- as it does with no grid. + { + auto s = dust_book_script(); + s[6] = [u](ScriptHost& h) { + auto stop = script_request(no::Reduce{no::ExplicitUnits{u}}); + stop.trigger = no::Stop{90.0}; + h.put("stop", stop); + }; + s[8] = [](ScriptHost& h) { h.put("take-0.3", script_request(no::Reduce{no::ExplicitUnits{0.3}})); }; + const auto bars = flat_bars(20, {{14, {kPrice, 89.0}}}); + const auto o = run_script("grid-stop-after-partial", s, 0.1, bars); + const auto& stop = terminal_of(o, "stop"); + CHECK(o.submits.empty()); + CHECK(stop.kind == 1); + CHECK(stop.closed == std::vector({two + a})); + CHECK(stop.rows == std::vector({two, a})); + CHECK(o.book.empty()); + const auto free = run_script("grid-stop-after-partial-no-grid", s, std::nullopt, bars); + CHECK(terminal_of(free, "stop") == stop); + } + // An OCA-Reduce opening fills before the boundary Reduce in its group and + // lowers the Reduce's remaining to fl(U - 0.3), which is no boundary of + // {0.5, A', 0.3}: closing it would split the on-grid 0.5 lot off the grid, + // so it is refused, typed, and the book is untouched. + { + auto s = dust_book_script(); + s[6] = [u](ScriptHost& h) { + auto open = script_request(no::Transact{0.3}); + open.group = no::Member{7, 2, no::GroupEffect::Reduce}; + h.put("oca-open", open); + auto close = script_request(no::Reduce{no::ExplicitUnits{u}}); + close.group = no::Member{7, 1, no::GroupEffect::Reduce}; + h.put("oca-close", close); + }; + const auto o = run_script("grid-oca-lowered", s, 0.1, flat_bars(12)); + CHECK(o.submits.empty()); + CHECK(terminal_of(o, "oca-open").kind == 1); + const auto& close = terminal_of(o, "oca-close"); + CHECK(close.kind == 2 && close.reason == kUnrepresentable); + CHECK(close.closed.empty() && close.rows.empty()); + CHECK(o.book == std::vector({0.5, a, 0.3})); + } + // A take-profit in the stop's OCA-Reduce group fills first; the stop's + // remaining, fl(U - 0.3), is the book it then meets, {0.2, A'}, and closes + // it whole. + { + CHECK(u - 0.3 == two + a); + auto s = dust_book_script(); + s[6] = [u](ScriptHost& h) { + auto stop = script_request(no::Reduce{no::ExplicitUnits{u}}); + stop.trigger = no::Stop{90.0}; + stop.group = no::Member{5, 1, no::GroupEffect::Reduce}; + h.put("stop", stop); + auto take = script_request(no::Reduce{no::ExplicitUnits{0.3}}); + take.trigger = no::Limit{110.0}; + take.group = no::Member{5, 2, no::GroupEffect::Reduce}; + h.put("take", take); + }; + const auto o = run_script("grid-oca-take-then-stop", s, 0.1, + flat_bars(20, {{10, {111.0, kPrice}}, {14, {kPrice, 89.0}}})); + CHECK(o.submits.empty()); + CHECK(terminal_of(o, "take").kind == 1); + const auto& stop = terminal_of(o, "stop"); + CHECK(stop.kind == 1); + CHECK(stop.closed == std::vector({two + a})); + CHECK(stop.rows == std::vector({two, a})); + CHECK(o.book.empty()); + } + // A fraction of one resolves unfloored only to the gross scope as it + // stands, settled in one fill. Net of a live sibling's claim, with a + // pending OCA-Reduce deduction, or under a point budget it settles + // something else and is floored as before: the same outcome, bit for bit, + // as a fraction just below one, which is always floored. + const double below_one = std::nextafter(1.0, 0.0); + const auto fraction = [](double f, no::ScopeClaim claim) { + return script_request(no::Reduce{no::ScopeFraction{f, claim}}); + }; + const auto floored_like = [&](const char* name, double sibling, + std::function place) { + const auto script_for = [&](double f) { + auto s = dust_book_script(); + if (sibling > 0.0) { + s[6] = [sibling, place, f](ScriptHost& h) { + auto resting = script_request(no::Reduce{no::ExplicitUnits{sibling}}); + resting.trigger = no::Limit{150.0}; + h.put("sibling", resting); + place(h, f); + }; + } else { + s[6] = [place, f](ScriptHost& h) { place(h, f); }; + } + return s; + }; + const auto one = run_script(name, script_for(1.0), 0.1, flat_bars(24)); + const auto floored = run_script((std::string(name) + "-control").c_str(), + script_for(below_one), 0.1, flat_bars(24)); + scenario = name; + CHECK(one.submits == floored.submits); + CHECK(one.terminals.count("fraction") == 1 && floored.terminals.count("fraction") == 1); + CHECK(terminal_of(one, "fraction") == terminal_of(floored, "fraction")); + CHECK(one.book == floored.book); + return one; + }; + { + const auto o = floored_like("grid-fraction-net", 0.3, [&](ScriptHost& h, double f) { + h.put("fraction", fraction(f, no::ScopeClaim::NetOfSiblings)); + }); + const auto& t = terminal_of(o, "fraction"); + CHECK(t.kind == 1 && t.closed.size() == 1); + CHECK(!t.closed.empty() && no::quantity_on_grid(t.closed[0], 0.1)); + } + { + const auto o = floored_like("grid-fraction-net-dust", 0.6, [&](ScriptHost& h, double f) { + h.put("fraction", fraction(f, no::ScopeClaim::NetOfSiblings)); + }); + const auto& t = terminal_of(o, "fraction"); + CHECK(t.kind == 2 + && t.reason == static_cast(no::MatchRejectReason::TermsUnresolved)); + CHECK(o.book == std::vector({0.5, a})); + } + floored_like("grid-fraction-oca-pending", 0.0, [&](ScriptHost& h, double f) { + auto take = script_request(no::Reduce{no::ExplicitUnits{0.3}}); + take.group = no::Member{9, 1, no::GroupEffect::Reduce}; + h.put("take", take); + auto all = fraction(f, no::ScopeClaim::Gross); + all.group = no::Member{9, 2, no::GroupEffect::Reduce}; + h.put("fraction", all); + }); + floored_like("grid-fraction-budget", 0.0, [&](ScriptHost& h, double f) { + auto all = fraction(f, no::ScopeClaim::Gross); + all.capacity = no::PointBudget{0.2}; + h.put("fraction", all); + }); +} + } // namespace int main() { refusal_cases(); grid_cases(); + scripted_grid_cases(); std::printf("test_native_unrepresentable_refusal: %d checks, %d failures\n", checks, failures); return failures == 0 ? 0 : 1; } From b945385c46dc73872aaddc9cd09f6827b708d6fc Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Fri, 25 Sep 2026 23:24:05 +0800 Subject: [PATCH 10/24] Docs: the one quantity case that still stops a run -- an OCA-Reduce sibling's reservation -- stated where the pages promise none (R5 lane K-ULP4) native-settlement.md ("What a native host gets") and native-engine.md ("What a host gets from a quantity") said no run stops for a quantity. The third review's p5b probe shows one that does, before and after this lane alike: in an OCA group with GroupEffect::Reduce, a member's fill smaller than half an ulp of a sibling's remaining units cannot be deducted from them, and after the fill is booked the run fails with code 6, discriminator 7 (CoreFailure::UnrepresentableReservation). Probe output (91d65ad6 and 66dd8742 identical): the fraction's 2.2759572004815709e-14 fill is applied, then "lifecycle=FAILED code=6 disc=7 err=native working-request preparation failed". Both pages now carry the exception; native-settlement.md also says that a sibling's fill lowers a member's remaining units without regard to the grid (the reviewer's c_probe: an on-grid Reduce 0.4 lowered by a bound Flatten sibling's off-grid fill settles 0.29999999999997728 on 91d65ad6 and on 66dd8742 alike), so the grid's re-check holds only the Reduce it admitted as a boundary. Docs only; anchors 1678/1678, doc lint clean. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit f1af50dca5ad19438c90ed127faebad0fd20c499) INT25 merge: the six K-ULP4 picks merge their pages on digit-normalised text (int16_digit_merge, a citation both sides moved mapped from the picked commit's tree); here the pages are re-anchored once for the lane: 25 citations carried through the exact line map from 409ebad1 or f1af50dc, and the six CONTRIBUTING / contributing-llm citations of the floors set to ci_verify.py:236 and :344. Every anchor cites the text one parent cited (verify_carry: 1672 exact, 6 = the floor lines). --- CONTRIBUTING.md | 4 ++-- docs/adr/0001-kernel-adapter-boundary.md | 8 +++---- docs/design/native-feature-parity.md | 30 ++++++++++++------------ docs/native-settlement.md | 19 +++++++++++---- docs/pages/contributing-llm.md | 6 ++--- docs/pages/native-engine.md | 13 +++++++--- docs/pages/pine-to-native.md | 8 +++---- 7 files changed, 53 insertions(+), 35 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 65bfb597..8b3e827e 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:235 and `RELEASE_MIN_TESTS` -ci_verify.py:343. +floor — `KERNEL_MIN_TESTS` ci_verify.py:236 and `RELEASE_MIN_TESTS` +ci_verify.py:344. 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 ceffc8cf..ea68c846 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -598,7 +598,7 @@ number names its symbol on this tree. | `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:965`) 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:1059`) | **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:3961`) | **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: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:582`) | **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:7632-7644`); 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:1076-1081`) sees the same values, so no hash contract moves. | @@ -673,9 +673,9 @@ 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:7116`, presenting `NativeDecisionContext::in_session` `include/pineforge/market_driver.hpp:155` 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:7843-7859` | 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:7820-7841` | 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) | +| `present_session_day` `src/native_execution_consumer.cpp:7255`, presenting `NativeDecisionContext::in_session` `include/pineforge/market_driver.hpp:155` 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:7864-7880` | 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:7841-7862` | 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:6592` | 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 diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 7448c955..255f76c3 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -82,7 +82,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:2497-2499) | **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:2549, `typed_entry_units` pine_adapter.cpp:2567; `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:2528; `percent_commission_live_equity` pine_adapter.cpp:4121-4161) | **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:2454-2469, `PineSizingSnapshot` pine_adapter.hpp:147-154; fill-time exceptions `default_sized` pine_adapter.cpp:6435-6461) | **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:1391-1408; host-sized terms snap (`ExecutionGridPolicy` native_order.hpp:71-85); exact grid validation `execution_terms_grid_representable` native_execution_consumer.cpp:4962-4965; 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:1391-1408; host-sized terms snap (`ExecutionGridPolicy` native_order.hpp:71-85); exact grid validation `execution_terms_grid_representable` native_execution_consumer.cpp:4966-4969; 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:12253-12281, `compute_exit_reservation` pine_adapter.cpp:4204-4325) | **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:4256-4579, `reserved_exit_qty` pine_adapter.cpp:8757-9067) | **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:444-451, `source_money_floor_lot` pine_adapter.cpp:453; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:4236-4251) | 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:2302) | **yes** — `allowed_open_directions` native_run_spec.hpp:596 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2931-2939 | 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:2945-2948 | 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:13001-13004) is TV. | -| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:595 → `MaxOpenLots` native_execution_consumer.cpp:2950-2953 | 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:5970-6016, cap left unset `max_open_lots` pine_adapter.cpp:2301-2303) is TV. | -| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13055-13120 and leaves the kernel gate unset pine_adapter.cpp:13054-13056) | **yes** — `initial_margin_fraction` native_run_spec.hpp:597; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2968 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:2817 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. | +| MG1 | Allowed opening directions (`allowed_open_directions` pine_adapter.cpp:2302) | **yes** — `allowed_open_directions` native_run_spec.hpp:596 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2934-2942 | 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:2948-2951 | 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:13001-13004) is TV. | +| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:595 → `MaxOpenLots` native_execution_consumer.cpp:2953-2956 | 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:5970-6016, cap left unset `max_open_lots` pine_adapter.cpp:2301-2303) is TV. | +| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13055-13120 and leaves the kernel gate unset pine_adapter.cpp:13054-13056) | **yes** — `initial_margin_fraction` native_run_spec.hpp:597; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2971 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:2817 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. | | MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13710-13711, `margin_pct` pine_adapter.cpp:13118) | **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:13810-13988, `submit_margin_call_units` pine_adapter.cpp:13949-13995, `schedule_margin_call_path` pine_adapter.cpp:17959-18036) | **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:2927 **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:13810-13988, `submit_margin_call_units` pine_adapter.cpp:13949-13995, `schedule_margin_call_path` pine_adapter.cpp:17959-18036) | **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:2930 **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:13741-13777) | **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:982 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:179-200) | **no** **Closed:** `native_liquidation_price()` native_host.hpp:1300 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:16057-16073, `apply_fx_opening_margin_slice` pine_adapter.cpp:18037-18065) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1182 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) | @@ -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:915 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:7843-7860, which feeds the same class `TimeframeAggregator` native_execution_consumer.cpp:7850, 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:7864-7881, which feeds the same class `TimeframeAggregator` native_execution_consumer.cpp:7871, 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 @@ -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:2945-2955 | F O | +| 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:2948-2958 | F O | | Legality | The existing rule (`commands_allowed` native_execution_consumer.cpp:1397-1404): 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 | @@ -276,7 +276,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-670, `contribute_input` native_execution_consumer.cpp:7998, `seal_script` native_execution_consumer.cpp:7944) 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-670, `contribute_input` native_execution_consumer.cpp:7998, `seal_script` native_execution_consumer.cpp:7965) 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:7085), 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 | @@ -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:2909-2976) 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:2918-2985) 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). @@ -314,7 +314,7 @@ 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:2992-2994, `sha256:4c401fc6bd6ae89db330fea9f609b75539ff3db1f8060b0fdb0541cbbaf478ee` native_run_spec.hpp:19-177, `sha256:f4041a4df37f679d9055e856e1cc247a9ce5e1e80f01a49f9cdd822631f352c6` native_run_spec.hpp:174-175 | O S | +| 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:3027-3029, `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:1600-1601 | 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 | @@ -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:3285-3474 +`sha256:8c97d3e1d9d6f036a9be0b50a3a43188f1e2eee23da0384fbed242829108c3c3` native_run_spec.hpp:239-302 and `sha256:efad6b9c7bcd3f7d0e459d434ef088a61b18c546d53cfb107f481a227fbe532b` native_execution_consumer.cpp:3288-3477 (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 | @@ -1161,7 +1161,7 @@ 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 `sha256:620926caaa868e64ede32f547ac1dd0bfe81c9758267593a9a924051208df0a4` native_execution_consumer.hpp:955-959); 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:12916-12983, pine_adapter.cpp:13030); its chronology lives at `sha256:bb89768c9a7b2dc9f6de5943c04de8103ed107c21bb6e184446de0e6a84cf4c2` pine_adapter.cpp:13589-13666 and its excursion sample differs by chronology (`sha256:764a1a69f2940863b4f77cbf9d20267a19ec38e86720796c7081a59ac7245427` pine_strategy_host.cpp:729-759) | 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:2296-2336) | 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:5547) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `sha256:86617ca43bb2847380f1845b07cc9ecd27059814ee80fecf80649a9393dfe8c8` native_execution_consumer.cpp:3462-3471 and `drain_applied_notifications` (native_execution_consumer.cpp:6264-6277); new behaviour only behind new opt-ins | F | +| E4 | **Calc on fill.** Kernel suffix eligibility is continuous along the segment; TV refills only at O/H/L/C waypoints (`sha256:ddad8dc42e078838c0d53fd49e50c31060221e3b82e7d5bbaa0420769daa7d31` pine_scheduler_native.cpp:585-637, `coof_next_waypoint` pine_adapter.cpp:5547) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `sha256:86617ca43bb2847380f1845b07cc9ecd27059814ee80fecf80649a9393dfe8c8` native_execution_consumer.cpp:3465-3474 and `drain_applied_notifications` (native_execution_consumer.cpp:6264-6277); 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:15893-15921) | 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:1600-1619) | 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:332-380) 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 | @@ -1256,7 +1256,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:2937-2983). 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 `sha256:b06b928e9f12217d7d3036f6adf77250c691c2ab1ad69d48e87417dbdfe1adc7` native_execution_consumer.cpp:2940-2986). 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. @@ -1306,7 +1306,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:1616-1617) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6969-6981) 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:3475-3484). `born_on_remaining_path` (native_execution_consumer.cpp:5603-5607) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1655-1662) is calibrated against it. +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:3478-3487). `born_on_remaining_path` (native_execution_consumer.cpp:5603-5607) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1655-1662) 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:1071-1078, whose body is native_execution_consumer.cpp:6819-6826). `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:2544); 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/native-settlement.md b/docs/native-settlement.md index bf91cf85..2d08ec3b 100644 --- a/docs/native-settlement.md +++ b/docs/native-settlement.md @@ -107,10 +107,11 @@ residual. A selection closed by its whole sum was already consumed whole, and `Flatten` needs no quantity at all. What a native host gets, then, from a quantity request on the exact settlement: -exactly one terminal outcome, and no run stopped for a quantity. A close that spans lots -and ends inside one is one fill charged its request (K-ULP2); a close whose -binary64 FIFO sum reaches its request at a lot closes that lot whole (K-ULP3); -a `Transact` that crosses the book is one fill charged its units (K-ULP1, in +exactly one terminal outcome, and no run stopped for its quantity (one group +case is left, below). A close that spans lots and ends inside one is one fill +charged its request (K-ULP2); a close whose binary64 FIFO sum reaches its +request at a lot closes that lot whole (K-ULP3); a `Transact` that crosses the +book is one fill charged its units (K-ULP1, in `docs/pages/native-engine.md`); a request the settlement cannot book exactly is refused, typed, as above -- the settlement's own refusal shows beforehand in `inspect_current_execution` as a `settlement_readiness` of @@ -138,6 +139,16 @@ lot, or found nothing to close, and the book's own sizes were `OffGrid`. A that is no boundary of its scope: a later lot's size would be taken FIFO from the head lot and split it off the grid. +One case still stops the run, as it did before K-ULP4, and it is the request +core's, not the settlement's: in an OCA group whose effect is +`GroupEffect::Reduce`, a member's fill smaller than half an ulp of a sibling's +remaining units cannot be deducted from them, and after the fill is booked the +run fails with code 6, discriminator 7 (`CoreFailure::UnrepresentableReservation`). +It takes a dust-sized fill -- the close of a dust lot -- beside a far larger +resting sibling. A sibling's fill also lowers a member's remaining units +without regard to the quantity grid; the grid's re-check above holds only the +`Reduce` it admitted as a boundary. + ## Quantity tolerance A run that declares `NativeRunSpec::quantity_tolerance = t` (C: the diff --git a/docs/pages/contributing-llm.md b/docs/pages/contributing-llm.md index aeb476a7..68f1b26f 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:235 and - `RELEASE_MIN_TESTS` ci_verify.py:343. Adding rows means raising the floor + *ran* against a floor: `KERNEL_MIN_TESTS` ci_verify.py:236 and + `RELEASE_MIN_TESTS` ci_verify.py:344. 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:235, `RELEASE_MIN_TESTS` ci_verify.py:343). It +(`KERNEL_MIN_TESTS` ci_verify.py:236, `RELEASE_MIN_TESTS` ci_verify.py:344). 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 5828a21d..dd3dd97c 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -2777,7 +2777,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:7850`). What it does +(`TimeframeAggregator` `native_execution_consumer.cpp:7871`). 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 @@ -3074,8 +3074,9 @@ a 2^-49-unit remainder that a second fill opened as a dust lot. ### What a host gets from a quantity Every quantity request ends in exactly one terminal outcome, and no ordinary -quantity stops the run. The FIFO rules are the settlement's -(`docs/native-settlement.md`, "Physical lots and quantities"); what a host sees: +quantity stops the run (one OCA-group case is left, last below). The FIFO +rules are the settlement's (`docs/native-settlement.md`, "Physical lots and +quantities"); what a host sees: - A close that spans lots and ends inside one is one terminal fill whose `closed_units` and `filled_working` are its request, even where the rows, @@ -3112,6 +3113,12 @@ quantity stops the run. The FIFO rules are the settlement's meet the refusal above. A C host sets it in the `quantity_tolerance` tail of `pf_native_run_spec_ext_v1`. The Pine adapter does not declare it and keeps its own `1e-10` rule (`docs/native-settlement.md`, "Quantity tolerance"). +- Still a run failure, as before K-ULP4: in an OCA group with + `GroupEffect::Reduce`, a member's fill smaller than half an ulp of a + sibling's remaining units cannot be deducted from them, and after the fill + is booked the run fails with code 6, discriminator 7 + (`CoreFailure::UnrepresentableReservation`) -- a dust-sized fill beside a far + larger resting sibling. ### Sizing without a host override diff --git a/docs/pages/pine-to-native.md b/docs/pages/pine-to-native.md index 24f15c1f..ce5607d6 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -291,7 +291,7 @@ is `strategy_native_submit_v1` native_c_api.h:2678 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:539 | `native_risk_limits_strategy.cpp` | The default. | -| `strategy.direction.long` | `NativeOpenDirections::Long` native_run_spec.hpp:74 | `allowed_open_directions` pineforge.h:539 | `tests/test_native_run_spec.cpp` | An opening the direction forbids is refused with `OpeningDirection` native_execution_consumer.cpp:2938. | +| `strategy.direction.long` | `NativeOpenDirections::Long` native_run_spec.hpp:74 | `allowed_open_directions` pineforge.h:539 | `tests/test_native_run_spec.cpp` | An opening the direction forbids is refused with `OpeningDirection` native_execution_consumer.cpp:2941. | | `strategy.direction.short` | `NativeOpenDirections::Short` native_run_spec.hpp:75 | `allowed_open_directions` pineforge.h:539 | `tests/test_native_run_spec.cpp` | Same, the other way. | | `strategy.commission.percent` | `NativeFeeKind::Percent` native_run_spec.hpp:20 | `fee_kind` pineforge.h:538 | `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:538 | `tests/test_native_selected_requests.cpp` | Account currency per unit. | @@ -396,7 +396,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:539 | `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:2938. | +| `strategy.risk.allow_entry_in()` | `allowed_open_directions` native_run_spec.hpp:596 | `allowed_open_directions` pineforge.h:539 | `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:2941. | | `strategy.risk.max_position_size()` | `max_abs_units` native_run_spec.hpp:594 | `max_abs_units` pineforge.h:540 | `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:2312 | `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:2315 | `native_risk_limits_strategy.cpp` | Percent is of the day's opening equity. Blocks to the end of its own day. | @@ -455,9 +455,9 @@ openings TradingView takes. | a fill, as it happens | `on_native_applied` native_host.hpp:925 | `on_applied` native_c_api.h:2464 | `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:5361. | | raw input before aggregation | `on_native_input` native_host.hpp:863 | `on_input` native_c_api.h:2459 | `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:915 | `on_sub_bar` native_c_api.h:2500 | `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:114 with `NativeCoordinate` market_driver.hpp:71 | `pf_native_decision_v1` native_c_api.h:1443 | `hello_kernel.cpp` | Copied onto the callback stack; mutating it changes nothing. In C++ `script_interval` market_driver.hpp:118 and `input_interval` market_driver.hpp:117 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 session-day facts are the three rows below. | +| `time`, `bar_index`, the bar's calendar interval, session facts | `NativeDecisionContext` market_driver.hpp:114 with `NativeCoordinate` market_driver.hpp:71 | `pf_native_decision_v1` native_c_api.h:1454 | `hello_kernel.cpp` | Copied onto the callback stack; mutating it changes nothing. In C++ `script_interval` market_driver.hpp:118 and `input_interval` market_driver.hpp:117 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 session-day facts are the three rows below. | | `session.ismarket` | `in_session` market_driver.hpp:155 | `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.isfirstbar` | `opens_session_day` market_driver.hpp:156 | `opens_session_day` native_c_api.h:1462 | `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.isfirstbar` | `opens_session_day` market_driver.hpp:156 | `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:157, `closes_session_day_open_ended` market_driver.hpp:158 | `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:1067 | `price` native_c_api.h:1533 | `native_selected_strategy.cpp` | The readiness preview `inspect_current_execution` native_host.hpp:1079 is an observation, never an apply token; `execute_current` native_host.hpp:1084 is the apply. | From 42fec568c8fc08cee4b07c0be2121b5b4d23a786 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 02:26:51 +0800 Subject: [PATCH 11/24] INT25 merge: C-SURFACE-1's failure-discriminator row fails its run through a refused anchored level C-SURFACE-1's check_csurface_failure_discriminator provoked its settlement failure with a Transact whose point budget cannot be booked (intent 2^60, capacity 1.0). R5 lane K-ULP4 rules exactly that request a typed MatchRejected(UnrepresentableQuantity) refusal: the run completes, and the row's four checks failed on the merged tree ("nonrepresentable request did not fail settlement (got 0, want -11)"). The row's purpose is the C state's copy of the kernel's nonzero failure discriminator, so it keeps every assertion (PF_NATIVE_E_RUN_FAILED, failure code SETTLEMENT_FAILURE, operation SETTLEMENT, discriminator != 0) and takes the failure from a path K-ULP4 keeps: the host answers an anchored leg's arm (on_anchored_level) with a negative level, which the kernel's representability check refuses at the arm, as test_native_anchored_legs' l7b-hook-broken row pins in C++. K-ULP4's own C row (check_unrepresentable_quantity) pins the typed refusal of the old request. Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/test_native_c_api.c | 51 ++++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/tests/test_native_c_api.c b/tests/test_native_c_api.c index 5698b00e..ca7cc99b 100644 --- a/tests/test_native_c_api.c +++ b/tests/test_native_c_api.c @@ -8650,16 +8650,46 @@ typedef struct csurface_postrun_state { typedef struct csurface_failure_state { pf_strategy_t host; int submit_rc; + int leg_rc; + int calculations; + uint64_t parent; + uint64_t leg; } csurface_failure_state; -static int csurface_nonrepresentable_on_begin(void* user) { +/* R5 lane K-ULP4 made a quantity the settlement cannot book that request's + * typed MatchRejected refusal, so this row takes its settlement failure from + * a level the kernel's representability check refuses instead: the host + * answers an anchored leg's arm with a negative level (INT25). */ +static int csurface_nonrepresentable_level(void* user, const pf_native_anchored_level_view_v1* view, + double* level) { + (void)user; + (void)view; + *level = -1.0; + return PF_NATIVE_ANSWER_PROVIDED; +} + +static int csurface_nonrepresentable_on_bar(void* user, const pf_bar_t* bar, + const pf_native_decision_v1* at) { csurface_failure_state* state = (csurface_failure_state*)user; - pf_native_request_v1 request = blank_request(); + pf_native_request_v1 request; + (void)bar; + (void)at; + if (state->calculations++ != 0) return 0; + request = blank_request(); request.intent = PF_NATIVE_INTENT_TRANSACT; - request.intent_value = 0x1p60; - request.capacity = PF_NATIVE_CAPACITY_POINT_BUDGET; - request.capacity_units = 1.0; - state->submit_rc = strategy_native_submit_v1(state->host, &request, NULL, NULL); + request.intent_value = 2.0; + request.trigger = PF_NATIVE_TRIGGER_MARKET; + state->submit_rc = strategy_native_submit_v1(state->host, &request, &state->parent, NULL); + request = blank_request(); + request.intent = PF_NATIVE_INTENT_REDUCE; + request.reduce_size = PF_NATIVE_REDUCE_OWNER_OPENED; + request.trigger = PF_NATIVE_TRIGGER_LIMIT; + request.anchor = PF_NATIVE_ANCHOR_FROM_OWNER_FILL; + request.anchor_offset = 2.0; + request.owner = PF_NATIVE_OWNER_WAIT_FOR_APPLIED; + request.owner_n = 1u; + request.owner_incarnations = &state->parent; + state->leg_rc = strategy_native_submit_v1(state->host, &request, &state->leg, NULL); return 0; } @@ -8672,7 +8702,8 @@ static void check_csurface_failure_discriminator(void) { int n = 0; memset(&state, 0, sizeof(state)); table = blank_callbacks(&state); - table.on_run_begin = csurface_nonrepresentable_on_begin; + table.on_bar = csurface_nonrepresentable_on_bar; + table.on_anchored_level = csurface_nonrepresentable_level; state.host = strategy_native_host_create_v1(&table); CHECK(state.host != NULL, "discriminator host create failed"); if (!state.host) return; @@ -8681,9 +8712,11 @@ static void check_csurface_failure_discriminator(void) { "discriminator host configure failed"); bars = pf_twin_bars(&n); CHECK_EQ_INT(strategy_native_run_v1(state.host, bars, n, NULL), PF_NATIVE_E_RUN_FAILED, - "nonrepresentable request did not fail settlement"); + "nonrepresentable level did not fail settlement"); CHECK_EQ_INT(state.submit_rc, PF_NATIVE_OK, - "nonrepresentable request was rejected at submission"); + "the anchored leg's owner was rejected at submission"); + CHECK_EQ_INT(state.leg_rc, PF_NATIVE_OK, + "the anchored leg was rejected at submission"); memset(&result, 0, sizeof(result)); result.struct_size = (uint32_t)sizeof(result); CHECK_EQ_INT(strategy_native_state_v1(state.host, &result), PF_NATIVE_OK, From fac8389a8e335c0eceae2bc95c366b658df854b1 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Fri, 25 Sep 2026 22:01:13 +0800 Subject: [PATCH 12/24] v19 hash step (R5 lane V19-FIX): a cancelled exit stays cancelled, cancelling and recording cost what is live, the dead path_order_ goes Audit X2 and X9 (AUDIT4-opus section 7), inside the unreleased v19 epoch; the source extension stays pineforge-source-adapter/v4. - strategy.cancel / strategy.cancel_all retire every leg of each exit row they name (retire_cancelled_exits): the live ones they take off the book, caused at the Cancelled event (a pair hold earlier in the same callback no longer turns the withdrawal into a refused conflicting replay), the dormant ones and the ones the adapter took off the book itself, each caused past the lifecycle's last step; and they drop a gapped stop a declined reversal parked for the next margin-call slice (pending_margin_revivals_). A margin call no longer revives an exit the script cancelled while it was live or parked. TradingView decides it, on NYSE:F 15, 2025-04-15..05-20, range proof covered: v19fix-cancel-before- pair, v19fix-cancel-at-1000, v19fix-gapped-stop-cancel and v19fix-gapped-stop-cancel-after-pair all book the badapter-v19dp1-cancel file (tv_trades sha256 662ce8c8...: margin calls 12 @10.39 and 92 @10.57, 836 open), and v19fix-gapped-stop-control (bdf3d470...) the parked stop's close without a cancel (X 928 @10.39). All six AUDIT4 v19dp1 variants now print the ab9714be log byte for byte, and the gapped-stop variants book what ab9714be books. - K1 (erase_retired_rows) no longer holds a withdrawn leg for the revival; it keeps one only while its origin is live or opened, the one question (origin_leg_consumed) that still reads it. An exit cancelled and re-placed every bar at one level keeps 3 rows, not one per bar: x1.9 to x2.1 per doubling up to 64,000 bars (x4.7 to x4.9 before, 57 s at 64,000 for the cancel/re-place leg), strategy.cancel_all + exit at position_avg_price * 0.95 likewise. - release_closed_cohort_origins: at a bar open after a receipt read that saw a command event, an origin whose request chain no longer works and that holds no open lot leaves its kernel roster (cohort_remove); a replace successor's open lot keeps every older member of its roster. A flip recorded per bar costs x2.0 per doubling from 4,000 to 32,000 bars (x2.45, x2.80, x3.06 before). Cost: one removal per closed opening; the 100 public slots (spark, magnifier on) run +0.54 % instructions at the median, +1.57 % at most (slots 007 and 030). - PineExecutionAdapter::path_order_ (written, folded, never read) and set_path_order are removed. Named hash move: every Pine host-extension, broker-state and recorded per-bar value, and the kernel continuation wherever an origin leaves a roster. Trades move only on the revival shape the two tapes prove. Re-pinned (each TU's harvest switch; the base 91d65ad6 harvest reproduces every old pin; no non-hash declaration or field moved; the host-extension fold, recorded rows and final hash taken out of test_adapter_quiet_bar and test_publication_witness, both print identical tables on base and lane): tests/test_adapter_report_relower.cpp tests/test_adapter_recording_hash_witness.cpp tests/test_adapter_continuation_view.cpp tests/test_adapter_receipts_in_place.cpp tests/test_adapter_lookup_index_witness_data.hpp tests/test_adapter_quiet_bar_pinned.inc tests/test_adapter_command_allocations.cpp tests/test_adapter_host_view_memo_data.hpp tests/test_publication_witness_pinned.inc Pine witness (ReissueReplay x64 on btcusdt_15m_7d): 10045334278681590956 -> 9305091844641472748. Tests: test_adapter_margin_revival_cancel gains the five tapes, the other cancel spellings and rows on a chart with no quantity grid (base: 89 of 291 checks fail); test_adapter_live_state_scaling gains the constant-level re-cancel (recording off and on), the cancel_all shape and the flip, each with a small leg calibrated to at least 0.1 s CPU, rows and roster members bounded, the churn's ratio gated, and --row; test_adapter_live_state_ equivalence --margin-revival: the erasing run's retention in variants 2 and 3 (expectation corrected, no trade moves). Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 336c9acde83321c87851e6d0b35e9d37e9fbd1b3) INT25 merge: test_adapter_live_state_scaling keeps RATIO-HARDEN's calibration (every gated leg doubles until its best sample takes kMinLegSeconds, both legs checked against it), which sizes this lane's constant / cancel_all / flip rows too, so the lane's own pre-sizing loop goes; the ratio gate keeps this lane's churn gating (CHECK(ratio < kShapeBound) for every workload). --- include/pineforge/source/pine_adapter.hpp | 32 +- src/source/pine_adapter.cpp | 315 +++- src/source/pine_state_hash.cpp | 1 - src/source/pine_strategy_host.cpp | 1 - tests/CMakeLists.txt | 5 +- tests/test_adapter_command_allocations.cpp | 49 +- tests/test_adapter_continuation_view.cpp | 683 ++++--- tests/test_adapter_host_view_memo_data.hpp | 41 +- tests/test_adapter_live_state_equivalence.cpp | 26 +- tests/test_adapter_live_state_scaling.cpp | 155 +- ...test_adapter_lookup_index_witness_data.hpp | 1596 +++++++++++++---- tests/test_adapter_margin_revival_cancel.cpp | 212 ++- 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.cpp | 13 +- tests/test_publication_witness_pinned.inc | 317 ++-- 18 files changed, 4289 insertions(+), 1546 deletions(-) diff --git a/include/pineforge/source/pine_adapter.hpp b/include/pineforge/source/pine_adapter.hpp index f91ec826..b34dc796 100644 --- a/include/pineforge/source/pine_adapter.hpp +++ b/include/pineforge/source/pine_adapter.hpp @@ -1286,7 +1286,6 @@ class PineExecutionAdapter { void set_configuration(const PineStrategyConfig& config) noexcept; void set_staged_configuration(const StagedConfiguration& staged); void set_begin_mode(bool is_stream, bool bar_magnifier = false) noexcept; - void set_path_order(NativePathOrder path_order) noexcept; NativeRunSpec project(const PineStrategyConfig&, const StagedConfiguration&, const NativeBeginArgs&, @@ -1701,6 +1700,10 @@ class PineExecutionAdapter { // R5 lane V19-E: erases the placement rows no reader can reach again // (pine_adapter.cpp explains the rule); run at every bar open. void erase_retired_rows(const NativeDecisionContext&); + // R5 lane V19-FIX: takes out of the kernel's cohort rosters every origin + // that can no longer be bound (pine_adapter.cpp explains the rule); run + // at every bar open. + void release_closed_cohort_origins(); bool lifecycle_readable(const PlacementSnapshot&) const noexcept; // Writes one trade's exit phase, refolding when the write lands below the // final mark. @@ -1927,10 +1930,15 @@ class PineExecutionAdapter { void suspend_coof_declined_reversal_at_open( const Bar&, const NativeDecisionContext&); void hold_reversal_pair_brackets(const SourceId&); - // A script cancel of a dormant exit (strategy.cancel of its id, or - // cancel_all when `id` is null) retires every leg of its lifecycle, so no - // later margin call revives it (R5 lane B-ADAPTER, V19D-P1). - void retire_cancelled_dormant_exits(const SourceId* id); + // A script cancel (strategy.cancel of an id, or cancel_all when `id` is + // null) retires every leg of the lifecycle of each exit it withdrew -- + // the live requests in `cancelled` (incarnation, Cancelled event ordinal) + // and every other exit row it names -- so no later margin call revives + // one (R5 lane B-ADAPTER, V19D-P1, for the dormant ones; R5 lane V19-FIX + // for the rest). + void retire_cancelled_exits( + const SourceId* id, + const std::vector>& cancelled); void purge_brackets_after_applied_reversal(const PlacementSnapshot&); void revive_brackets_after_margin( const native_order::ExecutionAppliedEvent&, const NativeDecisionContext&); @@ -1968,7 +1976,6 @@ class PineExecutionAdapter { NativeStrategyHost* host_ = nullptr; PineStrategyConfig config_{}; StagedConfiguration staged_{}; - NativePathOrder path_order_ = NativePathOrder::Auto; mutable std::uint64_t run_counter_ = 0; std::uint64_t source_sequence_ = 0; std::uint64_t command_ordinal_ = 0; @@ -2176,6 +2183,19 @@ class PineExecutionAdapter { // the fields the test reads never change once a row is placed, and a // row that could supersede the leg is newer than every row it covers. std::vector> unsuperseded; + // R5 lane V19-FIX: release_closed_cohort_origins()'s working sets -- + // the chain roots of the working requests, the openings of the open + // lots and those no roster holds as a root, the origins leaving a + // roster (cohort, incarnation), and the handle a lot is looked up by + // (its run identity copied only when it changes) -- and whether a + // receipt read has seen a command event since its last pass (a memo: + // with none, the pass would take nothing off; a run begin sets it). + std::vector working_roots; + std::vector lot_openings; + std::vector successor_lots; + std::vector> released_origins; + native_order::RequestHandle lot_probe{}; + bool roster_pass_due = true; void clear() noexcept { roots.clear(); askable_origins.clear(); diff --git a/src/source/pine_adapter.cpp b/src/source/pine_adapter.cpp index 9c527b28..c6b62ffa 100644 --- a/src/source/pine_adapter.cpp +++ b/src/source/pine_adapter.cpp @@ -57,6 +57,15 @@ bool same_exit_levels(const PineExitLevels& left, const PineExitLevels& right) n && same_double_bits(left.loss_ticks, right.loss_ticks); } +// Every leg of an exit's lifecycle retired: the script withdrew the exit +// (strategy.cancel / strategy.cancel_all, retire_cancelled_exits), a state no +// suspension produces (Suspend retires at most the trail leg). No margin call +// revives such an exit. +bool withdrawn(const exit_legs::Lifecycle& legs) noexcept { + return legs.retired(exit_legs::Leg::Stop) && legs.retired(exit_legs::Leg::Limit) + && legs.retired(exit_legs::Leg::Trail); +} + bool finite_positive(double value) noexcept { return std::isfinite(value) && value > 0.0; } @@ -1782,41 +1791,77 @@ void PineExecutionAdapter::hold_reversal_pair_brackets(const SourceId& from_entr } } -// strategy.cancel / strategy.cancel_all withdraw an exit whatever its state, -// and a dormant exit -- one a declined reversal held (finding-311) -- is no -// exception: TradingView never brings a cancelled exit back. The lab tv tapes -// badapter-v19dp1-control / -cancel (NYSE:F 15, 2025-04-30 .. 05-06) show one -// script with and without strategy.cancel("X"): the margin call revives the -// standing dormant X and closes the position through it; after the cancel it -// does not, and a second margin call follows. The legacy engine skipped a -// cancelled order in the same pass (ab9714be pine_fills.cpp:2065-2076). The -// lifecycle records it where the revival reads it: every leg retired, a state -// no suspension produces (Suspend retires at most the trail leg). -void PineExecutionAdapter::retire_cancelled_dormant_exits(const SourceId* id) { +// strategy.cancel / strategy.cancel_all withdraw an exit whatever its state: +// a live exit the cancel takes off the book, and a dormant one -- an exit a +// declined reversal held (finding-311) -- alike. TradingView never brings a +// cancelled exit back. The lab tv tapes badapter-v19dp1-control / -cancel +// (NYSE:F 15, 2025-04-30 .. 05-06) show one script with and without +// strategy.cancel("X") on 05-01, while X is dormant: the margin call revives +// the standing dormant X and closes the position through it; after the cancel +// it does not, and a second margin call follows. The tapes +// v19fix-cancel-before-pair and v19fix-cancel-at-1000 (R5 lane V19-FIX) cancel +// X while it is still live -- on 04-30 15:45 before the reversal pair, and on +// 04-30 10:00 -- and book the same two margin-call slices, 836 left open: the +// declined pair's suspension still makes the cancelled leg dormant on 05-01, +// and the revival must not read that as a standing exit. The legacy engine +// skipped any cancelled order in the same pass (ab9714be +// pine_fills.cpp:2065-2076). The lifecycle records the withdrawal where the +// revival reads it: every leg retired (`withdrawn`), a state no suspension +// produces. `cancelled` names the live requests this cancel withdrew, each +// with the ordinal of its Cancelled event: that event is the cause, so the +// withdrawal lands after whatever the same callback did to the lifecycle +// before (a pair hold at the callback's own ordinal would otherwise make it a +// conflicting replay, and the lifecycle would refuse it). Every other exit +// row the cancel names -- a dormant one, or one the adapter itself took off +// the book on the script's behalf -- is found by id, caused at the callback, +// or just past the lifecycle's last step when that step is no older. +// +// The adapter takes an exit off the book on the script's behalf where +// TradingView keeps it pending: a gapped exit stop a declined reversal +// retires waits in pending_margin_revivals_ for the next margin-call slice +// (apply_reversal_gap_bracket_policy), and the declined pair's suspension +// can still make its row dormant. A cancel drops the waiting revival and +// withdraws the row -- the lab tv tapes v19fix-gapped-stop-control / -cancel +// (stop 10.15, which the 05-01 open gaps through) close 928 through X at the +// margin call without the cancel and book the cancel tape's trades with it. +void PineExecutionAdapter::retire_cancelled_exits( + const SourceId* id, + const std::vector>& cancelled) { const auto point = detail::callback_point(require_host()); if (!point) return; const auto domain = config_.calc_on_order_fills ? exit_legs::Domain::FillRecalc : exit_legs::Domain::Ordinary; - const exit_legs::Frame cause{point->decision.coordinate.ordinal, - point->decision.coordinate.interval_index, domain, exit_legs::Phase::Observation}; - // As in hold_reversal_pair_brackets: no erased lifecycle is read again. - PINEFORGE_AUDIT_PLACEMENT_SCAN(placement_, - [&](std::uint64_t, const PlacementSnapshot& candidate) { - return lifecycle_readable(candidate); - }); - for (auto row : placement_) { - auto& candidate = row.second; + const auto retire_legs = [&](PlacementSnapshot& candidate, std::uint64_t event) { const bool exit = candidate.family == PineOrderFamily::ExitLimit || candidate.family == PineOrderFamily::ExitStop || candidate.family == PineOrderFamily::ExitTrail; - if (!exit || (id && candidate.source_id != *id) || !candidate.legs.dormant() - || !candidate.legs.target().incarnation) { - continue; - } + if (!exit || !candidate.legs.target().incarnation || withdrawn(candidate.legs)) return; + // Never at or before the lifecycle's last step: a dormant row a pair + // hold suspended earlier in this callback took the callback's own + // ordinal, and the lifecycle refuses a second, different action there + // (a conflicting replay) as it refuses an older one. + if (const auto& last = candidate.legs.last_action(); last && last->cause.event >= event) + event = last->cause.event + 1; + const exit_legs::Frame cause{event, point->decision.coordinate.interval_index, domain, + exit_legs::Phase::Observation}; const exit_legs::Action action{candidate.legs.target(), candidate.legs.revision(), cause, exit_legs::Cancel{{exit_legs::Leg::Stop, exit_legs::Leg::Limit, exit_legs::Leg::Trail}}}; (void)candidate.legs.apply(candidate.legs.target(), action); + }; + for (const auto& [incarnation, event] : cancelled) { + const auto found = placement_.find(incarnation); + if (found != placement_.end()) retire_legs(found->second, event); + } + // As in hold_reversal_pair_brackets: no erased lifecycle is read again. + PINEFORGE_AUDIT_PLACEMENT_SCAN(placement_, + [&](std::uint64_t, const PlacementSnapshot& candidate) { + return lifecycle_readable(candidate); + }); + for (auto row : placement_) { + auto& candidate = row.second; + if (id && candidate.source_id != *id) continue; + retire_legs(candidate, point->decision.coordinate.ordinal); } } @@ -1890,19 +1935,22 @@ void PineExecutionAdapter::revive_brackets_after_margin( std::optional marketable; native_order::RequestHandle marketable_handle{}; // No erased row is a revival candidate (K1 keeps every lifecycle of the - // current cycle), and no candidate's superseded answer rests on an erased + // current cycle the script has not withdrawn, and the revival skips a + // withdrawn one), and no candidate's superseded answer rests on an erased // row: K2 keeps every row that names it as its predecessor, K3 the first // re-issue that supersedes it, and one supersessor answers the test. PINEFORGE_AUDIT_PLACEMENT_SCAN(placement_, [&](std::uint64_t, const PlacementSnapshot& candidate) { return candidate.placement_cycle == current_position_cycle_ - && candidate.legs.dormant() && candidate.legs.target().incarnation != 0; + && candidate.legs.dormant() && candidate.legs.target().incarnation != 0 + && !withdrawn(candidate.legs); }); #if PINEFORGE_PLACEMENT_AUDIT for (const auto& retained : placement_) { const auto& candidate = retained.second; if (retained.second.placement_cycle != current_position_cycle_ - || !candidate.legs.dormant() || !candidate.legs.target().incarnation) { + || !candidate.legs.dormant() || !candidate.legs.target().incarnation + || withdrawn(candidate.legs)) { continue; } const auto inc = retained.first; @@ -1941,13 +1989,10 @@ void PineExecutionAdapter::revive_brackets_after_margin( || !candidate.legs.target().incarnation) { continue; } - // The script cancelled it (retire_cancelled_dormant_exits): every leg - // is retired, and a cancelled exit is never revived (V19D-P1). - if (candidate.legs.retired(exit_legs::Leg::Stop) - && candidate.legs.retired(exit_legs::Leg::Limit) - && candidate.legs.retired(exit_legs::Leg::Trail)) { - continue; - } + // The script cancelled it, live or dormant (retire_cancelled_exits): + // every leg is retired, and a cancelled exit is never revived + // (V19D-P1, R5 lane V19-FIX). + if (withdrawn(candidate.legs)) continue; // A same-id exit re-issued after the slice REPLACED this bracket: the // successor carries the cycle's legs, and reviving the superseded parent // here would fire the finished cycle's already-touched level against the @@ -2048,6 +2093,7 @@ void PineExecutionAdapter::reset_for_run() { pine_view_host_ = nullptr; // Incarnations start over with the run. retired_row_scratch_.unsuperseded.clear(); + retired_row_scratch_.roster_pass_due = true; if (auto* index = adapter_lookup_index(bound_consumer(), this, run_counter_, false)) index->clear(); admission_journal.reset(); @@ -2156,7 +2202,6 @@ void PineExecutionAdapter::reset_for_run() { trail_state_at_open_.clear(); stream_mode_ = false; bar_magnifier_ = false; - path_order_ = NativePathOrder::Auto; short_seed_ = {}; pending_short_seed_ = {}; short_seed_long_candidate_ = {}; @@ -2180,9 +2225,6 @@ void PineExecutionAdapter::set_begin_mode(bool is_stream, bool bar_magnifier) no stream_mode_ = is_stream; bar_magnifier_ = bar_magnifier; } -void PineExecutionAdapter::set_path_order(NativePathOrder path_order) noexcept { - path_order_ = path_order; -} NativeRunSpec PineExecutionAdapter::project(const PineStrategyConfig& config, const StagedConfiguration& staged, @@ -2776,10 +2818,13 @@ std::uint64_t retired_rows_erased() noexcept { return rows_erased.load(std::memo // pair hold rewrite lifecycles for it), and exit() asks whether a leg bound to // an origin that is live or opened was consumed. A row of an ended cycle // bound to any other origin stays unreadable: cycles only advance, and an -// origin opens only while its request works. +// origin opens only while its request works. So does a leg the script +// withdrew (R5 lane V19-FIX), whatever its cycle, unless it is bound to such +// an origin: the revival never picks it, so what the suspension and the hold +// write into it is read by nobody but that consumed-leg question. bool PineExecutionAdapter::lifecycle_readable(const PlacementSnapshot& row) const noexcept { if (row.legs.target().incarnation == 0) return false; - if (row.placement_cycle >= current_position_cycle_) return true; + if (row.placement_cycle >= current_position_cycle_ && !withdrawn(row.legs)) return true; const std::uint64_t origin = row.bracket_origin.incarnation; if (origin == 0) return false; for (const auto& handle : live_handles_) @@ -2853,8 +2898,18 @@ bool PineExecutionAdapter::lifecycle_readable(const PlacementSnapshot& row) cons // as the leg is a candidate, so a leg superseded now stays superseded for the // rest of its cycle: the revival never picks it again, and the suspension and // the pair hold only rewrite its own lifecycle, which nothing else reads. A -// cycle that cancels and re-places its exits every bar (the cancelled legs -// keep their target) no longer holds a row per bar. +// cycle that cancels and re-places its exits every bar at a moving level no +// longer holds a row per bar. +// +// R5 lane V19-FIX: nor a leg the script withdrew (every leg retired, by +// strategy.cancel or strategy.cancel_all whether it was live or dormant: +// retire_cancelled_exits), whatever its cycle, unless it is bound to an +// askable origin. The revival skips a withdrawn leg, so it is no candidate +// and K2/K3 keep nothing for it; the suspension and the pair hold only +// rewrite its own lifecycle, which only origin_leg_consumed reads, and only +// for an origin that is live or opened. A cycle that cancels and re-places an +// exit every bar at the same level -- no re-issue at another stop supersedes +// it -- no longer holds a row per bar either. void PineExecutionAdapter::erase_retired_rows(const NativeDecisionContext& context) { // The previous bars' trades are final: their exit phases fold once. for (; exit_phase_final_ < trade_exit_phase_.size(); ++exit_phase_final_) { @@ -2889,9 +2944,9 @@ void PineExecutionAdapter::erase_retired_rows(const NativeDecisionContext& conte askable_origins.end()); // The pins a row's own fields decide, whatever else names it. const auto pinned_by_itself = [&](const PlacementSnapshot& value) { - // K1 + // K1 (a withdrawn leg by its origin alone) if (value.legs.target().incarnation != 0 - && (value.placement_cycle >= cycle + && ((value.placement_cycle >= cycle && !withdrawn(value.legs)) || contains(askable_origins, value.bracket_origin.incarnation))) { return true; } @@ -2904,11 +2959,12 @@ void PineExecutionAdapter::erase_retired_rows(const NativeDecisionContext& conte return (value.family == PineOrderFamily::Entry || value.family == PineOrderFamily::Order) && value.projection_created_bar >= interval - 1; }; - // K1's cycle clause alone: a leg lifecycle of the current cycle bound to - // no askable origin (a live one is askable itself). + // K1's cycle clause alone: a leg lifecycle of the current cycle, not + // withdrawn, bound to no askable origin (a live one is askable itself). const auto held_by_cycle = [&](const PlacementSnapshot& value) { return value.legs.target().incarnation != 0 && value.placement_cycle == cycle && exit_family(value.family) && !value.from_entry.empty() + && !withdrawn(value.legs) && !contains(askable_origins, value.bracket_origin.incarnation); }; auto& held = sweep.held; @@ -3121,8 +3177,8 @@ void PineExecutionAdapter::erase_retired_rows(const NativeDecisionContext& conte } // What the live rows contribute to the scan pins (K7, K8), and the - // current-cycle revival candidates (K2, K3): rows with a leg lifecycle, - // which K1 keeps. + // current-cycle revival candidates (K2, K3): rows with a leg lifecycle + // the script has not withdrawn, which K1 keeps. for (const auto& handle : live_handles_) { const auto found = placement_.find(handle.incarnation); if (found == placement_.end()) continue; @@ -3141,7 +3197,8 @@ void PineExecutionAdapter::erase_retired_rows(const NativeDecisionContext& conte for (const auto& row : placement_) { const auto& value = row.second; if (value.legs.target().incarnation == 0 || value.placement_cycle != cycle - || !exit_family(value.family) || value.from_entry.empty()) { + || !exit_family(value.family) || value.from_entry.empty() + || withdrawn(value.legs)) { continue; } if (unpinned && !contains(askable_origins, row.first) @@ -3258,6 +3315,139 @@ void PineExecutionAdapter::erase_retired_rows(const NativeDecisionContext& conte placement_.compact(); } +// R5 lane V19-FIX: the kernel's cohort rosters hold what can still be bound. +// +// Every opening this adapter accepts joins its id's kernel roster +// (cohort_add, submit_or_replace), and the kernel folds every member of every +// roster into each continuation read (hash_cohorts), so a run that recorded a +// hash per bar paid for every opening it ever made at every bar. A roster is +// matching authority for the lots it names and nothing else: a CohortClose or +// BindCohort request reads it only through the open lots +// (NativeExecutionConsumer::cohort_contains over the book's lots). An origin +// whose chain has no working request and no open lot can never be bound +// again -- nothing opens a lot under a request that stopped working -- so at +// every bar open, after observe_terminal_receipts, each such origin leaves +// its kernel roster (cohort_remove). The adapter's own roster +// (CohortFacts::origins) keeps it: its questions are about every origin the +// id ever had. The roster then costs what is live. +// +// A roster holds chain roots; a working request names its root in its +// definition, and a lot names the request that filled it, which is the root +// unless it is a replace successor. The root of a successor's lot is not a +// public fact, but it is older than the successor (incarnations only grow): +// while such a lot is open, every member of its roster older than it stays. +// +// A member can only become unbindable through a command event -- a request +// ends, a lot closes -- and observe_terminal_receipts reads every one of them +// on its full path, which marks the pass due; a bar open after quiet reads +// only has nothing to take off, and skips the pass. +void PineExecutionAdapter::release_closed_cohort_origins() { + auto& scratch = retired_row_scratch_; + if (!scratch.roster_pass_due) return; + scratch.roster_pass_due = false; + IExecutionConsumer* consumer = bound_consumer(); + if (!consumer) return; + auto* pine = pine_view_of(host_); + if (!pine) return; + const auto& core = as_native_consumer(*consumer).request_core(); + std::size_t members = 0; + for (const auto& roster : core.cohorts()) members += roster.origins.size(); + if (members == 0) return; + // The common case, in a direct scan with nothing allocated or sorted: + // every member still opened a lot that is open, or roots a working + // request, so none can go. + const auto root_of = [](const native_order::LiveRequest& row) { + return row.definition->root ? row.definition->root->incarnation + : row.handle().incarnation; + }; + constexpr std::size_t kDirectScan = 64; + if (members * (pine->pyramid_entries_.size() + core.live().size()) <= kDirectScan) { + bool every_member_bound = true; + for (const auto& roster : core.cohorts()) { + for (const auto& origin : roster.origins) { + const std::uint64_t incarnation = origin.incarnation; + const bool bound = std::any_of(pine->pyramid_entries_.begin(), + pine->pyramid_entries_.end(), [&](const PyramidEntry& lot) { + return lot.entry_incarnation == incarnation; + }) + || std::any_of(core.live().begin(), core.live().end(), + [&](const native_order::LiveRequest& row) { + return root_of(row) == incarnation; + }); + if (!bound) { + every_member_bound = false; + break; + } + } + if (!every_member_bound) break; + } + if (every_member_bound) return; + } + auto& working = scratch.working_roots; + auto& lots = scratch.lot_openings; + auto& successor_lots = scratch.successor_lots; + auto& released = scratch.released_origins; + working.clear(); + lots.clear(); + successor_lots.clear(); + released.clear(); + for (const auto& live : core.live()) working.push_back(root_of(live)); + std::sort(working.begin(), working.end()); + for (const auto& lot : pine->pyramid_entries_) lots.push_back(lot.entry_incarnation); + std::sort(lots.begin(), lots.end()); + lots.erase(std::unique(lots.begin(), lots.end()), lots.end()); + const auto member = [](const std::vector& origins, + std::uint64_t incarnation) { + const auto at = std::lower_bound(origins.begin(), origins.end(), incarnation, + [](const native_order::RequestHandle& origin, std::uint64_t value) { + return origin.incarnation < value; + }); + return at != origins.end() && at->incarnation == incarnation; + }; + // The lots no roster names as a root: a replace successor's (or an + // opening no roster holds). + for (const auto opening : lots) { + if (opening == 0) continue; + bool root = false; + for (const auto& roster : core.cohorts()) { + if (member(roster.origins, opening)) { + root = true; + break; + } + } + if (!root) successor_lots.push_back(opening); + } + auto& probe = scratch.lot_probe; + for (const auto& roster : core.cohorts()) { + const auto& origins = roster.origins; + if (origins.empty()) continue; + if (probe.run != origins.front().run) probe.run = origins.front().run; + // The newest open successor lot this roster binds: every member older + // than it may be its root. + std::uint64_t kept_below = 0; + for (const auto opening : successor_lots) { + probe.incarnation = opening; + if (core.cohort_contains(roster.handle, probe)) kept_below = opening; + } + for (const auto& origin : origins) { + const std::uint64_t incarnation = origin.incarnation; + if (incarnation < kept_below + || std::binary_search(working.begin(), working.end(), incarnation) + || std::binary_search(lots.begin(), lots.end(), incarnation)) { + continue; + } + released.emplace_back(roster.handle, incarnation); + } + } + if (released.empty()) return; + auto& host = require_host(); + for (const auto& [cohort, incarnation] : released) { + native_order::RequestHandle origin = probe; + origin.incarnation = incarnation; + host.cohort_remove(cohort, std::move(origin)); + } +} + void PineExecutionAdapter::maybe_activate_short_seed_plan() { // The legacy sort qualifies at the broker boundary, never midway through // the source callback that created the three objects. on_bar_open owns @@ -5199,6 +5389,9 @@ void PineExecutionAdapter::observe_terminal_receipts() { return; } } + // A command event may have ended a request or closed a lot: the next bar + // open's roster pass has work (release_closed_cohort_origins). + retired_row_scratch_.roster_pass_due = true; const auto state = detail::run_state(host); const bool in_place = consumer != nullptr; // The terminal watermark is folded into the broker-state hash @@ -11187,25 +11380,40 @@ void PineExecutionAdapter::cancel(const SourceId& id) { pending_bracket_legs_.erase(std::remove_if(pending_bracket_legs_.begin(), pending_bracket_legs_.end(), [&](const PendingBracketLeg& leg) { return leg.snapshot.source_id == id; }), pending_bracket_legs_.end()); + // A gapped stop waiting for a margin-call revival is withdrawn with the + // rest of the id (retire_cancelled_exits). + pending_margin_revivals_.erase(std::remove_if(pending_margin_revivals_.begin(), + pending_margin_revivals_.end(), [&](const PendingMarginRevival& revival) { + return revival.snapshot.source_id == id; + }), pending_margin_revivals_.end()); std::vector matches; for (const auto& handle : live_handles_) { const auto snapshot = placement_.find(handle.incarnation); if (snapshot != placement_.end() && snapshot->second.source_id == id) matches.push_back(handle); } + std::vector> cancelled; for (const auto& handle : matches) { const auto result = require_host().cancel(handle); - if (result.status == native_order::CancelStatus::Cancelled) retire(handle); + if (result.status == native_order::CancelStatus::Cancelled) { + retire(handle); + cancelled.emplace_back(handle.incarnation, result.event_ordinal); + } } - retire_cancelled_dormant_exits(&id); + retire_cancelled_exits(&id, cancelled); } void PineExecutionAdapter::cancel_all() { const auto handles = live_handles_; + std::vector> cancelled; for (const auto& handle : handles) { const auto result = require_host().cancel(handle); - if (result.status == native_order::CancelStatus::Cancelled) retire(handle); + if (result.status == native_order::CancelStatus::Cancelled) { + retire(handle); + cancelled.emplace_back(handle.incarnation, result.event_ordinal); + } } - retire_cancelled_dormant_exits(nullptr); + retire_cancelled_exits(nullptr, cancelled); + pending_margin_revivals_.clear(); bracket_families_.clear(); pending_bracket_legs_.clear(); pending_entries_.clear(); @@ -15896,6 +16104,7 @@ void PineExecutionAdapter::on_bar_open(const Bar& bar, const NativeDecisionConte // per-origin bracket legs cannot close a different cohort member. observe_terminal_receipts(); erase_retired_rows(context); + release_closed_cohort_origins(); trail_state_at_open_.clear(); for (const auto& handle : live_handles_) { const auto placement = placement_.find(handle.incarnation); diff --git a/src/source/pine_state_hash.cpp b/src/source/pine_state_hash.cpp index 935dca30..6404f077 100644 --- a/src/source/pine_state_hash.cpp +++ b/src/source/pine_state_hash.cpp @@ -243,7 +243,6 @@ void source::PineExecutionAdapter::hash_state(BrokerStateHashSink& f) const { f.i(config_.pyramiding); f.d(config_.commission_value); f.i(config_.commission_type); f.i(config_.slippage); f.d(config_.margin_long); f.d(config_.margin_short); f.b(config_.close_entries_rule_any); f.b(config_.src_series_active); - f.i(static_cast(path_order_)); f.s(staged_.syminfo.ticker); f.s(staged_.syminfo.tickerid); f.s(staged_.syminfo.currency); f.s(staged_.syminfo.basecurrency); f.s(staged_.syminfo.type); f.s(staged_.syminfo.timezone); f.s(staged_.syminfo.session); f.s(staged_.syminfo.volumetype); f.s(staged_.syminfo.description); diff --git a/src/source/pine_strategy_host.cpp b/src/source/pine_strategy_host.cpp index 50108083..8e2a1f49 100644 --- a/src/source/pine_strategy_host.cpp +++ b/src/source/pine_strategy_host.cpp @@ -320,7 +320,6 @@ void source::PineStrategyHost::prepare_native_begin(const NativeBeginArgs& args) ? NativePathOrder::HighFirst : (path_order_mode_ == 2 ? NativePathOrder::LowFirst : NativePathOrder::Auto); - adapter_.set_path_order(path_order); const NativeRunSpec spec = adapter_.project(effective, staged, args, path_order); const auto setup = configure_native(spec); if (setup.status != NativeSetupStatus::Applied) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 153c37eb..87662d8a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1293,8 +1293,9 @@ list(APPEND TEST_SOURCES test_adapter_range_end_fx) list(APPEND TEST_SOURCES test_native_script_bucket_completions) # R5 lane B-ADAPTER (item 7, V19D-P1): a margin call revives a dormant exit # the script left standing and never one it cancelled -- the lab tv tapes -# badapter-v19dp1-control / -cancel replayed on their NYSE:F bars. -# Source-bound. +# badapter-v19dp1-control / -cancel replayed on their NYSE:F bars; R5 lane +# V19-FIX adds the tapes v19fix-cancel-before-pair / -at-1000 (an exit +# cancelled while live) and the other cancel spellings. Source-bound. list(APPEND TEST_SOURCES test_adapter_margin_revival_cancel) # R5 lane B-ADAPTER (item 5, F3 finding 2): a current execution converts at its # cursor's rate, threaded through the terms facts, inspection, preview and diff --git a/tests/test_adapter_command_allocations.cpp b/tests/test_adapter_command_allocations.cpp index 94baeed8..80d0c401 100644 --- a/tests/test_adapter_command_allocations.cpp +++ b/tests/test_adapter_command_allocations.cpp @@ -291,23 +291,40 @@ constexpr Script kScripts[] = {Script::ExitReissue, Script::FarReissue, Script:: // Script::EntryReissue/false/true: rows_digest 0x1781a3542d87a7c9ull -> 0xf4677e5c1916337full, final_hash 5868802478351644387ull -> 9541687758077282854ull // Script::EntryReissue/true/false: rows_digest 0x592d4b5617d2b6fcull -> 0x379d985d7c93b652ull, final_hash 11220719029914513450ull -> 7904694474988279479ull // Script::EntryReissue/true/true: rows_digest 0x4afd65bc6d977d03ull -> 0x2fd97b436aac3c5dull, final_hash 12913321619721074841ull -> 8582209322861755784ull +// expectation corrected (V19-FIX, 32 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; 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; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree: +// Script::ExitReissue/false/false: rows_digest 0x378e8175f07c1d5dull -> 0xab588834fadd3803ull, final_hash 5455427604675993531ull -> 5699976773346796219ull +// Script::ExitReissue/false/true: rows_digest 0x9d581c7efdf21626ull -> 0x31d20a68825b4041ull, final_hash 5124001802418948517ull -> 5368550971089751205ull +// Script::ExitReissue/true/false: rows_digest 0x123a4d26c1440e48ull -> 0xcecfebbbe86fc767ull, final_hash 10475037478956129116ull -> 6863695800300943516ull +// Script::ExitReissue/true/true: rows_digest 0xb8720a7926815bb3ull -> 0x6209f50bb7d9d6acull, final_hash 12173222249000782328ull -> 1107562895774304952ull +// Script::FarReissue/false/false: rows_digest 0xdece58a579c5d8e0ull -> 0xdf6b23adbf03e66eull, final_hash 7214211385822448325ull -> 873772099948613669ull +// Script::FarReissue/false/true: rows_digest 0x0bb13210fd7346c1ull -> 0xa3863ebe522e6dbeull, final_hash 10717416948830322455ull -> 4285418729649473335ull +// Script::FarReissue/true/false: rows_digest 0xfa15c45dc0196e57ull -> 0x688ef29ae4ccdebeull, final_hash 14180767476349608391ull -> 1841254732555988423ull +// Script::FarReissue/true/true: rows_digest 0xdd4e85623e945fdaull -> 0xf4c8662f18c74e7bull, final_hash 12801755778883560301ull -> 462243035089940333ull +// Script::EntryReissue/false/false: rows_digest 0x4530d69ae4e1a298ull -> 0x80ea582d7a285f56ull, final_hash 17005496595750190628ull -> 2970596418120183524ull +// Script::EntryReissue/false/true: rows_digest 0xf4677e5c1916337full -> 0xa4e881bd0166e308ull, final_hash 9541687758077282854ull -> 10388992876317270182ull +// Script::EntryReissue/true/false: rows_digest 0x379d985d7c93b652ull -> 0xc01d27d0c8fd1cadull, final_hash 7904694474988279479ull -> 8910455723513195319ull +// Script::EntryReissue/true/true: rows_digest 0x2fd97b436aac3c5dull -> 0x2bf51a0396f73773ull, final_hash 8582209322861755784ull -> 5882765811046345928ull +// Script::TrailReissue/false/false: rows_digest 0x08a2398c615ddd5bull -> 0x27b9526d5e960dafull, final_hash 8598862747086083069ull -> 1551066713552723293ull +// 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 const Pin kPins[] = { - {Script::ExitReissue, false, false, 0x378e8175f07c1d5dull, 60ull, 5455427604675993531ull, 0x584fc4b9ebbba609ull, 4}, - {Script::ExitReissue, false, true, 0x9d581c7efdf21626ull, 60ull, 5124001802418948517ull, 0x584fc4b9ebbba609ull, 4}, - {Script::ExitReissue, true, false, 0x123a4d26c1440e48ull, 60ull, 10475037478956129116ull, 0x41e6c3b1e0481c20ull, 4}, - {Script::ExitReissue, true, true, 0xb8720a7926815bb3ull, 60ull, 12173222249000782328ull, 0x41e6c3b1e0481c20ull, 4}, - {Script::FarReissue, false, false, 0xdece58a579c5d8e0ull, 60ull, 7214211385822448325ull, 0x7aac154ea6a8629aull, 4}, - {Script::FarReissue, false, true, 0x0bb13210fd7346c1ull, 60ull, 10717416948830322455ull, 0x7aac154ea6a8629aull, 4}, - {Script::FarReissue, true, false, 0xfa15c45dc0196e57ull, 60ull, 14180767476349608391ull, 0xfdb435b76f2571b6ull, 4}, - {Script::FarReissue, true, true, 0xdd4e85623e945fdaull, 60ull, 12801755778883560301ull, 0xfdb435b76f2571b6ull, 4}, - {Script::EntryReissue, false, false, 0x4530d69ae4e1a298ull, 60ull, 17005496595750190628ull, 0xfa79a63a9a8de06aull, 29}, - {Script::EntryReissue, false, true, 0xf4677e5c1916337full, 60ull, 9541687758077282854ull, 0xdc986aa5f5cc536aull, 29}, - {Script::EntryReissue, true, false, 0x379d985d7c93b652ull, 60ull, 7904694474988279479ull, 0xfa79a63a9a8de06aull, 29}, - {Script::EntryReissue, true, true, 0x2fd97b436aac3c5dull, 60ull, 8582209322861755784ull, 0xdc986aa5f5cc536aull, 29}, - {Script::TrailReissue, false, false, 0x08a2398c615ddd5bull, 60ull, 8598862747086083069ull, 0xd64b560956c46b33ull, 3}, - {Script::TrailReissue, false, true, 0x56954a0db7786b71ull, 60ull, 5779925080749936105ull, 0x5241bc8c55a4a4a3ull, 3}, - {Script::TrailReissue, true, false, 0x09315718e551137full, 60ull, 269689715775399305ull, 0xcdcafa54f49f508aull, 3}, - {Script::TrailReissue, true, true, 0x5ef417ff409d6fadull, 60ull, 3401687048604417982ull, 0x45d4c2aceeb466faull, 3}, + {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}, }; 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 cebaf931..abd411d9 100644 --- a/tests/test_adapter_continuation_view.cpp +++ b/tests/test_adapter_continuation_view.cpp @@ -329,14 +329,22 @@ void emit(const char* name, const std::vector& reads) { // 14580145596766604804ull -> 7253611708149050520ull // 12763565106297155564ull -> 13208968445305919179ull // 12763565106297155564ull -> 13208968445305919179ull +// expectation corrected (V19-FIX, k_chart, 7 of 7 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; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree: +// 2543779130970217232ull -> 14621553252499744072ull +// 2482407147974245352ull -> 1560069015253079270ull +// 17904934979399943420ull -> 8181232920474966978ull +// 7253611708149050520ull -> 8150352125352055815ull +// 13208968445305919179ull -> 4370795498617211977ull +// 10324478866479362969ull -> 10361542891827113526ull +// 13208968445305919179ull -> 4370795498617211977ull constexpr std::uint64_t k_chart[] = { - 2543779130970217232ull, - 2482407147974245352ull, - 17904934979399943420ull, - 7253611708149050520ull, - 13208968445305919179ull, - 10324478866479362969ull, - 13208968445305919179ull, + 14621553252499744072ull, + 1560069015253079270ull, + 8181232920474966978ull, + 8150352125352055815ull, + 4370795498617211977ull, + 10361542891827113526ull, + 4370795498617211977ull, }; // 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 @@ -358,14 +366,22 @@ constexpr std::uint64_t k_chart[] = { // 17362997550053029786ull -> 3037174366487220809ull // 1693323071214726469ull -> 4411147221463257853ull // 1693323071214726469ull -> 4411147221463257853ull +// expectation corrected (V19-FIX, k_magnifier, 7 of 7 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; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree: +// 2055499121853794215ull -> 1310098789583311035ull +// 14570450431216633494ull -> 9252967425603149690ull +// 12633570209086223403ull -> 9717019523752667192ull +// 3037174366487220809ull -> 14284557384012157011ull +// 4411147221463257853ull -> 15984354419168920510ull +// 11201307460949430058ull -> 16607038826027722339ull +// 4411147221463257853ull -> 15984354419168920510ull constexpr std::uint64_t k_magnifier[] = { - 2055499121853794215ull, - 14570450431216633494ull, - 12633570209086223403ull, - 3037174366487220809ull, - 4411147221463257853ull, - 11201307460949430058ull, - 4411147221463257853ull, + 1310098789583311035ull, + 9252967425603149690ull, + 9717019523752667192ull, + 14284557384012157011ull, + 15984354419168920510ull, + 16607038826027722339ull, + 15984354419168920510ull, }; // 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 @@ -377,12 +393,17 @@ constexpr std::uint64_t k_magnifier[] = { // 17786335956283624301ull -> 10940756508045292790ull // 4311320861969680288ull -> 5992953984750558008ull // 4311320861969680288ull -> 5992953984750558008ull +// expectation corrected (V19-FIX, k_aggregated, 4 of 5 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; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree: +// 10940756508045292790ull -> 11387000028137044299ull +// 5992953984750558008ull -> 4995545673543620333ull +// 5456021430637490595ull -> 9893483468656556008ull +// 5992953984750558008ull -> 4995545673543620333ull constexpr std::uint64_t k_aggregated[] = { 7408940017323371281ull, - 10940756508045292790ull, - 5992953984750558008ull, - 5456021430637490595ull, - 5992953984750558008ull, + 11387000028137044299ull, + 4995545673543620333ull, + 9893483468656556008ull, + 4995545673543620333ull, }; // 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 @@ -394,12 +415,17 @@ constexpr std::uint64_t k_aggregated[] = { // 11362706686723722160ull -> 2877250321201605922ull // 8515292501914737744ull -> 18112393076487589516ull // 8515292501914737744ull -> 18112393076487589516ull +// expectation corrected (V19-FIX, k_aggregated_magnifier, 4 of 5 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; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree: +// 2877250321201605922ull -> 301774797075971540ull +// 18112393076487589516ull -> 6106527879897255797ull +// 13595293692156698476ull -> 14842205570428090150ull +// 18112393076487589516ull -> 6106527879897255797ull constexpr std::uint64_t k_aggregated_magnifier[] = { 2376229218543853263ull, - 2877250321201605922ull, - 18112393076487589516ull, - 13595293692156698476ull, - 18112393076487589516ull, + 301774797075971540ull, + 6106527879897255797ull, + 14842205570428090150ull, + 6106527879897255797ull, }; // 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 @@ -426,17 +452,28 @@ constexpr std::uint64_t k_aggregated_magnifier[] = { // 4634335044637977031ull -> 220118684227198753ull // 15964838513564229050ull -> 5365537423784642334ull // 15964838513564229050ull -> 5365537423784642334ull +// expectation corrected (V19-FIX, k_coof, 10 of 10 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; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree: +// 8160477046829279565ull -> 10706142720152936082ull +// 8683725853452377701ull -> 14042469651802437968ull +// 16612330879755983674ull -> 1992762524201176782ull +// 3284793633481963595ull -> 9972018108613272455ull +// 9898269111674054383ull -> 9011525226083087013ull +// 15200193571573177943ull -> 7755254022538140640ull +// 220118684227198753ull -> 16056359684121154720ull +// 5365537423784642334ull -> 10916717879615654635ull +// 17202960931486753898ull -> 17916071608255404005ull +// 5365537423784642334ull -> 10916717879615654635ull constexpr std::uint64_t k_coof[] = { - 8160477046829279565ull, - 8683725853452377701ull, - 16612330879755983674ull, - 3284793633481963595ull, - 9898269111674054383ull, - 15200193571573177943ull, - 220118684227198753ull, - 5365537423784642334ull, - 17202960931486753898ull, - 5365537423784642334ull, + 10706142720152936082ull, + 14042469651802437968ull, + 1992762524201176782ull, + 9972018108613272455ull, + 9011525226083087013ull, + 7755254022538140640ull, + 16056359684121154720ull, + 10916717879615654635ull, + 17916071608255404005ull, + 10916717879615654635ull, }; // 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 @@ -632,78 +669,150 @@ constexpr std::uint64_t k_coof[] = { // 4258395109503891217ull -> 11989522059105970117ull // 14202785402043642137ull -> 14967623866548664000ull // 12763565106297155564ull -> 13208968445305919179ull +// expectation corrected (V19-FIX, k_recording, 71 of 72 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; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree: +// 2543779130970217232ull -> 14621553252499744072ull +// 13638291719838344805ull -> 16947653718640119089ull +// 17904934979399943420ull -> 8181232920474966978ull +// 4916994404502736826ull -> 384032772146433484ull +// 13208968445305919179ull -> 4370795498617211977ull +// 10324478866479362969ull -> 10361542891827113526ull +// 13208968445305919179ull -> 4370795498617211977ull +// 10364728431969872992ull -> 11635388043626139808ull +// 6773723984513471634ull -> 5137642655964282706ull +// 17279077540384140970ull -> 1067158101705749258ull +// 273555295542222474ull -> 17589216349291364682ull +// 8791634334263737749ull -> 13103826456263923669ull +// 925668679100948855ull -> 14125475791418217750ull +// 8777619534496248594ull -> 5496734832958426517ull +// 17725129497555317017ull -> 3300864403891436803ull +// 8008837844251753449ull -> 14334667028075329258ull +// 5594741258697960485ull -> 19027534220486873ull +// 15079181317456407482ull -> 17400684420983527587ull +// 17089349116617098209ull -> 16843748232757858737ull +// 18200208521056000396ull -> 5910420545552211065ull +// 15241045489297703683ull -> 4037494890945270795ull +// 1762199573452402758ull -> 13685270228904493022ull +// 11385708691232883797ull -> 12610051923577992823ull +// 17964225683996893310ull -> 13419685112760992640ull +// 4534429732864628476ull -> 6561776234336247499ull +// 17467596193696644527ull -> 11286535916465691925ull +// 16401650137151877883ull -> 9286401377316929631ull +// 16808577086136594190ull -> 11941619648187175172ull +// 12964209703146465173ull -> 2781110012097249044ull +// 16463685165528796935ull -> 5152088055254729287ull +// 17915487794021341896ull -> 14925422330047382771ull +// 15978106025216492785ull -> 18068979986855895194ull +// 1109289945107352897ull -> 3293728938989802792ull +// 15892759426148401507ull -> 3040154494312885091ull +// 17112119348245367605ull -> 3914527074597397115ull +// 8581843276479262047ull -> 6028800054409062637ull +// 7131344654937855853ull -> 17539544361470737737ull +// 14792600582710493575ull -> 17497902664474484401ull +// 1239841573570926230ull -> 16821147754446235025ull +// 5444729147284661655ull -> 3501965129295230921ull +// 3245433697010211721ull -> 17136543747664596622ull +// 4676939050322725348ull -> 6796102544471338071ull +// 7350099411866253542ull -> 10258488113034267824ull +// 343377133147017054ull -> 15437545425183691961ull +// 9598996651795533367ull -> 10208872586329130020ull +// 14030542661451411676ull -> 14368668855499836453ull +// 9219466268922236720ull -> 628171839165946395ull +// 6348273631916125388ull -> 10702221356985455012ull +// 1051431539591183352ull -> 2314157047087909764ull +// 2350462348068176311ull -> 12920471494293590838ull +// 5268079867744237915ull -> 8752737450779041921ull +// 2325247658527992044ull -> 7994102271719463356ull +// 6174548207444471958ull -> 4741946402210887143ull +// 8596767173582221714ull -> 739366574823717504ull +// 986987187009250324ull -> 3655914840365203865ull +// 13994596646214270165ull -> 7448010022416009296ull +// 2602688672529104322ull -> 11266041923012349372ull +// 3911601850678352661ull -> 16061447354858643806ull +// 9593288478406285213ull -> 6641998937765131086ull +// 4236346333119319709ull -> 15745847663605277053ull +// 15363079072004362933ull -> 2232785799096296677ull +// 18107900300087896759ull -> 12057790076704791676ull +// 17921584433192374312ull -> 14126847163984952264ull +// 5816305515259473479ull -> 6703125855558665936ull +// 10970209394144352158ull -> 750370809058189391ull +// 7950770882156242927ull -> 727242285392145435ull +// 18330351249071377885ull -> 9029503590661437192ull +// 13975661199209811620ull -> 6205861443650071106ull +// 11989522059105970117ull -> 5548300568428407565ull +// 14967623866548664000ull -> 4191735526950747220ull +// 13208968445305919179ull -> 4370795498617211977ull constexpr std::uint64_t k_recording[] = { - 2543779130970217232ull, - 13638291719838344805ull, - 17904934979399943420ull, - 4916994404502736826ull, - 13208968445305919179ull, - 10324478866479362969ull, - 13208968445305919179ull, - 10364728431969872992ull, - 6773723984513471634ull, - 17279077540384140970ull, - 273555295542222474ull, - 8791634334263737749ull, - 925668679100948855ull, - 8777619534496248594ull, - 17725129497555317017ull, - 8008837844251753449ull, - 5594741258697960485ull, - 15079181317456407482ull, - 17089349116617098209ull, - 18200208521056000396ull, - 15241045489297703683ull, - 1762199573452402758ull, - 11385708691232883797ull, - 17964225683996893310ull, - 4534429732864628476ull, - 17467596193696644527ull, - 16401650137151877883ull, - 16808577086136594190ull, - 12964209703146465173ull, - 16463685165528796935ull, - 17915487794021341896ull, - 15978106025216492785ull, - 1109289945107352897ull, - 15892759426148401507ull, - 17112119348245367605ull, - 8581843276479262047ull, - 7131344654937855853ull, - 14792600582710493575ull, - 1239841573570926230ull, - 5444729147284661655ull, - 3245433697010211721ull, - 4676939050322725348ull, - 7350099411866253542ull, - 343377133147017054ull, - 9598996651795533367ull, - 14030542661451411676ull, - 9219466268922236720ull, - 6348273631916125388ull, - 1051431539591183352ull, - 2350462348068176311ull, - 5268079867744237915ull, - 2325247658527992044ull, - 6174548207444471958ull, - 8596767173582221714ull, - 986987187009250324ull, - 13994596646214270165ull, - 2602688672529104322ull, - 3911601850678352661ull, - 9593288478406285213ull, - 4236346333119319709ull, - 15363079072004362933ull, - 18107900300087896759ull, - 17921584433192374312ull, - 5816305515259473479ull, - 10970209394144352158ull, - 7950770882156242927ull, - 18330351249071377885ull, - 13975661199209811620ull, - 11989522059105970117ull, - 14967623866548664000ull, - 13208968445305919179ull, + 14621553252499744072ull, + 16947653718640119089ull, + 8181232920474966978ull, + 384032772146433484ull, + 4370795498617211977ull, + 10361542891827113526ull, + 4370795498617211977ull, + 11635388043626139808ull, + 5137642655964282706ull, + 1067158101705749258ull, + 17589216349291364682ull, + 13103826456263923669ull, + 14125475791418217750ull, + 5496734832958426517ull, + 3300864403891436803ull, + 14334667028075329258ull, + 19027534220486873ull, + 17400684420983527587ull, + 16843748232757858737ull, + 5910420545552211065ull, + 4037494890945270795ull, + 13685270228904493022ull, + 12610051923577992823ull, + 13419685112760992640ull, + 6561776234336247499ull, + 11286535916465691925ull, + 9286401377316929631ull, + 11941619648187175172ull, + 2781110012097249044ull, + 5152088055254729287ull, + 14925422330047382771ull, + 18068979986855895194ull, + 3293728938989802792ull, + 3040154494312885091ull, + 3914527074597397115ull, + 6028800054409062637ull, + 17539544361470737737ull, + 17497902664474484401ull, + 16821147754446235025ull, + 3501965129295230921ull, + 17136543747664596622ull, + 6796102544471338071ull, + 10258488113034267824ull, + 15437545425183691961ull, + 10208872586329130020ull, + 14368668855499836453ull, + 628171839165946395ull, + 10702221356985455012ull, + 2314157047087909764ull, + 12920471494293590838ull, + 8752737450779041921ull, + 7994102271719463356ull, + 4741946402210887143ull, + 739366574823717504ull, + 3655914840365203865ull, + 7448010022416009296ull, + 11266041923012349372ull, + 16061447354858643806ull, + 6641998937765131086ull, + 15745847663605277053ull, + 2232785799096296677ull, + 12057790076704791676ull, + 14126847163984952264ull, + 6703125855558665936ull, + 750370809058189391ull, + 727242285392145435ull, + 9029503590661437192ull, + 6205861443650071106ull, + 5548300568428407565ull, + 4191735526950747220ull, + 4370795498617211977ull, 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): @@ -856,61 +965,116 @@ constexpr std::uint64_t k_recording[] = { // 14751690121258578908ull -> 10443374725851587239ull // 14751690121258578908ull -> 10443374725851587239ull // 14751690121258578908ull -> 10443374725851587239ull +// expectation corrected (V19-FIX, k_stream, 54 of 54 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; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree: +// 7287402210134811723ull -> 16510252855932086823ull +// 6258262929575333087ull -> 14048324983519873828ull +// 3613812001802944490ull -> 17804079663198632649ull +// 14256716415753092846ull -> 7665081442761407167ull +// 18066710111273402560ull -> 1510562666313522433ull +// 14450906895180287465ull -> 16253369998080909938ull +// 3845678135899710043ull -> 15976513930905472129ull +// 7810034545682398155ull -> 3846751227738950664ull +// 15431396809014267622ull -> 15450124388103584652ull +// 15040106242869486882ull -> 2225665425051551577ull +// 15078256075606303379ull -> 15615312821753524918ull +// 7105826286621370478ull -> 11958084298410326056ull +// 807892153055512972ull -> 7978187604899367877ull +// 8496224644929899065ull -> 876738446875159256ull +// 17064616757066975641ull -> 3498362567022607364ull +// 15196193893632458680ull -> 7053403205296459880ull +// 7052726630314129836ull -> 11858356529053237ull +// 8205504961151388183ull -> 5137924053122990913ull +// 1165523721440006008ull -> 1455779883697863925ull +// 10509778316010069121ull -> 1450972605221627516ull +// 11128902767468582530ull -> 6748150359618158603ull +// 12820182186070573599ull -> 17100694157255179539ull +// 1003285434194249605ull -> 16383805058165184155ull +// 2576933522647501972ull -> 14458762612387161134ull +// 9459669056269242935ull -> 7578874640041129857ull +// 15040561571098226314ull -> 1091721997205765161ull +// 16298324343950560662ull -> 4972829120680344696ull +// 7295767240173679712ull -> 11905106519757767589ull +// 11303232892540595619ull -> 14990642672847258155ull +// 9108531251772622477ull -> 18051484560037243770ull +// 17375367742411431301ull -> 7846354589731725875ull +// 13980272045865766065ull -> 1584652476433904101ull +// 7431106375660941195ull -> 12788838416146221578ull +// 8791307287609743160ull -> 859043901073302035ull +// 14145180498168891911ull -> 5668633829937549152ull +// 11868273362524112458ull -> 7252967951263295973ull +// 9898567439601994650ull -> 13226339972060618816ull +// 2093231171095230746ull -> 1981332788226950674ull +// 5084138944843925215ull -> 17570152268719247518ull +// 15691180810974081764ull -> 808203791867106338ull +// 4498955898635597792ull -> 8732491230892848194ull +// 16786848328132643741ull -> 960688443443831230ull +// 18123708192449447342ull -> 8967860587877234272ull +// 4395122555036264306ull -> 12765059320032069741ull +// 10159557292487317173ull -> 3138802922902822053ull +// 12231697623938131977ull -> 6653748162035956560ull +// 5309579489344409296ull -> 12107272552955766436ull +// 8546285193301548912ull -> 11629978161529728893ull +// 6138899047874310618ull -> 12532033173457401429ull +// 10443374725851587239ull -> 8444087394523764362ull +// 5882164565997988658ull -> 13899343461558324542ull +// 10443374725851587239ull -> 8444087394523764362ull +// 11033218740417797382ull -> 14079671624423886806ull +// 10443374725851587239ull -> 8444087394523764362ull constexpr std::uint64_t k_stream[] = { - 7287402210134811723ull, - 6258262929575333087ull, - 3613812001802944490ull, - 14256716415753092846ull, - 18066710111273402560ull, - 14450906895180287465ull, - 3845678135899710043ull, - 7810034545682398155ull, - 15431396809014267622ull, - 15040106242869486882ull, - 15078256075606303379ull, - 7105826286621370478ull, - 807892153055512972ull, - 8496224644929899065ull, - 17064616757066975641ull, - 15196193893632458680ull, - 7052726630314129836ull, - 8205504961151388183ull, - 1165523721440006008ull, - 10509778316010069121ull, - 11128902767468582530ull, - 12820182186070573599ull, - 1003285434194249605ull, - 2576933522647501972ull, - 9459669056269242935ull, - 15040561571098226314ull, - 16298324343950560662ull, - 7295767240173679712ull, - 11303232892540595619ull, - 9108531251772622477ull, - 17375367742411431301ull, - 13980272045865766065ull, - 7431106375660941195ull, - 8791307287609743160ull, - 14145180498168891911ull, - 11868273362524112458ull, - 9898567439601994650ull, - 2093231171095230746ull, - 5084138944843925215ull, - 15691180810974081764ull, - 4498955898635597792ull, - 16786848328132643741ull, - 18123708192449447342ull, - 4395122555036264306ull, - 10159557292487317173ull, - 12231697623938131977ull, - 5309579489344409296ull, - 8546285193301548912ull, - 6138899047874310618ull, - 10443374725851587239ull, - 5882164565997988658ull, - 10443374725851587239ull, - 11033218740417797382ull, - 10443374725851587239ull, + 16510252855932086823ull, + 14048324983519873828ull, + 17804079663198632649ull, + 7665081442761407167ull, + 1510562666313522433ull, + 16253369998080909938ull, + 15976513930905472129ull, + 3846751227738950664ull, + 15450124388103584652ull, + 2225665425051551577ull, + 15615312821753524918ull, + 11958084298410326056ull, + 7978187604899367877ull, + 876738446875159256ull, + 3498362567022607364ull, + 7053403205296459880ull, + 11858356529053237ull, + 5137924053122990913ull, + 1455779883697863925ull, + 1450972605221627516ull, + 6748150359618158603ull, + 17100694157255179539ull, + 16383805058165184155ull, + 14458762612387161134ull, + 7578874640041129857ull, + 1091721997205765161ull, + 4972829120680344696ull, + 11905106519757767589ull, + 14990642672847258155ull, + 18051484560037243770ull, + 7846354589731725875ull, + 1584652476433904101ull, + 12788838416146221578ull, + 859043901073302035ull, + 5668633829937549152ull, + 7252967951263295973ull, + 13226339972060618816ull, + 1981332788226950674ull, + 17570152268719247518ull, + 808203791867106338ull, + 8732491230892848194ull, + 960688443443831230ull, + 8967860587877234272ull, + 12765059320032069741ull, + 3138802922902822053ull, + 6653748162035956560ull, + 12107272552955766436ull, + 11629978161529728893ull, + 12532033173457401429ull, + 8444087394523764362ull, + 13899343461558324542ull, + 8444087394523764362ull, + 14079671624423886806ull, + 8444087394523764362ull, }; // 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 @@ -1010,42 +1174,78 @@ constexpr std::uint64_t k_stream[] = { // 1739880003141880127ull -> 13980272045865766065ull // 2467030153255185580ull -> 7431106375660941195ull // 2467030153255185580ull -> 7431106375660941195ull +// expectation corrected (V19-FIX, k_stream_aborted, 35 of 35 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; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree: +// 7287402210134811723ull -> 16510252855932086823ull +// 6258262929575333087ull -> 14048324983519873828ull +// 3613812001802944490ull -> 17804079663198632649ull +// 14256716415753092846ull -> 7665081442761407167ull +// 18066710111273402560ull -> 1510562666313522433ull +// 14450906895180287465ull -> 16253369998080909938ull +// 3845678135899710043ull -> 15976513930905472129ull +// 7810034545682398155ull -> 3846751227738950664ull +// 15431396809014267622ull -> 15450124388103584652ull +// 15040106242869486882ull -> 2225665425051551577ull +// 15078256075606303379ull -> 15615312821753524918ull +// 7105826286621370478ull -> 11958084298410326056ull +// 807892153055512972ull -> 7978187604899367877ull +// 8496224644929899065ull -> 876738446875159256ull +// 17064616757066975641ull -> 3498362567022607364ull +// 15196193893632458680ull -> 7053403205296459880ull +// 7052726630314129836ull -> 11858356529053237ull +// 8205504961151388183ull -> 5137924053122990913ull +// 1165523721440006008ull -> 1455779883697863925ull +// 10509778316010069121ull -> 1450972605221627516ull +// 11128902767468582530ull -> 6748150359618158603ull +// 12820182186070573599ull -> 17100694157255179539ull +// 1003285434194249605ull -> 16383805058165184155ull +// 2576933522647501972ull -> 14458762612387161134ull +// 9459669056269242935ull -> 7578874640041129857ull +// 15040561571098226314ull -> 1091721997205765161ull +// 16298324343950560662ull -> 4972829120680344696ull +// 7295767240173679712ull -> 11905106519757767589ull +// 11303232892540595619ull -> 14990642672847258155ull +// 9108531251772622477ull -> 18051484560037243770ull +// 17375367742411431301ull -> 7846354589731725875ull +// 13980272045865766065ull -> 1584652476433904101ull +// 7431106375660941195ull -> 12788838416146221578ull +// 13404512392582016370ull -> 6600350751158217016ull +// 7431106375660941195ull -> 12788838416146221578ull constexpr std::uint64_t k_stream_aborted[] = { - 7287402210134811723ull, - 6258262929575333087ull, - 3613812001802944490ull, - 14256716415753092846ull, - 18066710111273402560ull, - 14450906895180287465ull, - 3845678135899710043ull, - 7810034545682398155ull, - 15431396809014267622ull, - 15040106242869486882ull, - 15078256075606303379ull, - 7105826286621370478ull, - 807892153055512972ull, - 8496224644929899065ull, - 17064616757066975641ull, - 15196193893632458680ull, - 7052726630314129836ull, - 8205504961151388183ull, - 1165523721440006008ull, - 10509778316010069121ull, - 11128902767468582530ull, - 12820182186070573599ull, - 1003285434194249605ull, - 2576933522647501972ull, - 9459669056269242935ull, - 15040561571098226314ull, - 16298324343950560662ull, - 7295767240173679712ull, - 11303232892540595619ull, - 9108531251772622477ull, - 17375367742411431301ull, - 13980272045865766065ull, - 7431106375660941195ull, - 13404512392582016370ull, - 7431106375660941195ull, + 16510252855932086823ull, + 14048324983519873828ull, + 17804079663198632649ull, + 7665081442761407167ull, + 1510562666313522433ull, + 16253369998080909938ull, + 15976513930905472129ull, + 3846751227738950664ull, + 15450124388103584652ull, + 2225665425051551577ull, + 15615312821753524918ull, + 11958084298410326056ull, + 7978187604899367877ull, + 876738446875159256ull, + 3498362567022607364ull, + 7053403205296459880ull, + 11858356529053237ull, + 5137924053122990913ull, + 1455779883697863925ull, + 1450972605221627516ull, + 6748150359618158603ull, + 17100694157255179539ull, + 16383805058165184155ull, + 14458762612387161134ull, + 7578874640041129857ull, + 1091721997205765161ull, + 4972829120680344696ull, + 11905106519757767589ull, + 14990642672847258155ull, + 18051484560037243770ull, + 7846354589731725875ull, + 1584652476433904101ull, + 12788838416146221578ull, + 6600350751158217016ull, + 12788838416146221578ull, }; // 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 @@ -1064,13 +1264,20 @@ constexpr std::uint64_t k_stream_aborted[] = { // 18435485463499933528ull -> 2482407147974245352ull // 16410967088932597466ull -> 7856356957291164459ull // 16410967088932597466ull -> 7856356957291164459ull +// expectation corrected (V19-FIX, k_aborted, 6 of 6 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; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree: +// 2543779130970217232ull -> 14621553252499744072ull +// 2482407147974245352ull -> 1560069015253079270ull +// 17904934979399943420ull -> 8181232920474966978ull +// 7856356957291164459ull -> 12415576349368044635ull +// 4177885075058309128ull -> 14564863596030526264ull +// 7856356957291164459ull -> 12415576349368044635ull constexpr std::uint64_t k_aborted[] = { - 2543779130970217232ull, - 2482407147974245352ull, - 17904934979399943420ull, - 7856356957291164459ull, - 4177885075058309128ull, - 7856356957291164459ull, + 14621553252499744072ull, + 1560069015253079270ull, + 8181232920474966978ull, + 12415576349368044635ull, + 14564863596030526264ull, + 12415576349368044635ull, }; // 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 @@ -1112,22 +1319,38 @@ constexpr std::uint64_t k_aborted[] = { // 5493020030945000624ull -> 4221562876897277718ull // 11175928055869418645ull -> 17064616757066975641ull // 11175928055869418645ull -> 17064616757066975641ull +// expectation corrected (V19-FIX, k_reused, 15 of 15 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; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree: +// 2543779130970217232ull -> 14621553252499744072ull +// 2482407147974245352ull -> 1560069015253079270ull +// 17904934979399943420ull -> 8181232920474966978ull +// 7253611708149050520ull -> 8150352125352055815ull +// 2543779130970217232ull -> 14621553252499744072ull +// 2482407147974245352ull -> 1560069015253079270ull +// 17904934979399943420ull -> 8181232920474966978ull +// 7253611708149050520ull -> 8150352125352055815ull +// 13208968445305919179ull -> 4370795498617211977ull +// 10324478866479362969ull -> 10361542891827113526ull +// 13208968445305919179ull -> 4370795498617211977ull +// 4221562876897277718ull -> 1258618990550900905ull +// 17064616757066975641ull -> 3498362567022607364ull +// 15316717102556938983ull -> 9810780513184341561ull +// 17064616757066975641ull -> 3498362567022607364ull constexpr std::uint64_t k_reused[] = { - 2543779130970217232ull, - 2482407147974245352ull, - 17904934979399943420ull, - 7253611708149050520ull, - 2543779130970217232ull, - 2482407147974245352ull, - 17904934979399943420ull, - 7253611708149050520ull, - 13208968445305919179ull, - 10324478866479362969ull, - 13208968445305919179ull, - 4221562876897277718ull, - 17064616757066975641ull, - 15316717102556938983ull, - 17064616757066975641ull, + 14621553252499744072ull, + 1560069015253079270ull, + 8181232920474966978ull, + 8150352125352055815ull, + 14621553252499744072ull, + 1560069015253079270ull, + 8181232920474966978ull, + 8150352125352055815ull, + 4370795498617211977ull, + 10361542891827113526ull, + 4370795498617211977ull, + 1258618990550900905ull, + 3498362567022607364ull, + 9810780513184341561ull, + 3498362567022607364ull, }; // 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 @@ -1159,18 +1382,30 @@ constexpr std::uint64_t k_reused[] = { // 14580145596766604804ull -> 7253611708149050520ull // 12763565106297155564ull -> 13208968445305919179ull // 12763565106297155564ull -> 13208968445305919179ull +// expectation corrected (V19-FIX, k_refused, 11 of 11 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; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree: +// 10324478866479362969ull -> 10361542891827113526ull +// 13208968445305919179ull -> 4370795498617211977ull +// 10324478866479362969ull -> 10361542891827113526ull +// 13208968445305919179ull -> 4370795498617211977ull +// 2543779130970217232ull -> 14621553252499744072ull +// 2482407147974245352ull -> 1560069015253079270ull +// 17904934979399943420ull -> 8181232920474966978ull +// 7253611708149050520ull -> 8150352125352055815ull +// 13208968445305919179ull -> 4370795498617211977ull +// 10324478866479362969ull -> 10361542891827113526ull +// 13208968445305919179ull -> 4370795498617211977ull constexpr std::uint64_t k_refused[] = { - 10324478866479362969ull, - 13208968445305919179ull, - 10324478866479362969ull, - 13208968445305919179ull, - 2543779130970217232ull, - 2482407147974245352ull, - 17904934979399943420ull, - 7253611708149050520ull, - 13208968445305919179ull, - 10324478866479362969ull, - 13208968445305919179ull, + 10361542891827113526ull, + 4370795498617211977ull, + 10361542891827113526ull, + 4370795498617211977ull, + 14621553252499744072ull, + 1560069015253079270ull, + 8181232920474966978ull, + 8150352125352055815ull, + 4370795498617211977ull, + 10361542891827113526ull, + 4370795498617211977ull, }; // 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 d0d9faf5..b949adf0 100644 --- a/tests/test_adapter_host_view_memo_data.hpp +++ b/tests/test_adapter_host_view_memo_data.hpp @@ -74,31 +74,46 @@ // -> fnv=e337022ac3d8b681 net=71.786509342348978 hash=11831117278087605757 // rebuilt/2: fnv=5e2bd1d9860df94c net=71.786509342348751 hash=10773891632206972342 // -> fnv=e337022ac3d8b681 net=71.786509342348978 hash=11831117278087605757 +// expectation corrected (V19-FIX): every run's broker-state hash moved once more, 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; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree; +// trades, trade digests, net profits and errors did not move: +// batch/A: hash=11831117278087605757 -> hash=13831511007419341774 +// batch/B: hash=17469022339767313045 -> hash=16942810041893704776 +// stream/A: hash=1782579242963268431 -> hash=3001610611355826173 +// stream/B: hash=3909006125715448229 -> hash=8241673901617223988 +// interleaved/A: hash=1782579242963268431 -> hash=3001610611355826173 +// interleaved/B: hash=3909006125715448229 -> hash=8241673901617223988 +// round_robin/A1: hash=1782579242963268431 -> hash=3001610611355826173 +// round_robin/B: hash=3909006125715448229 -> hash=8241673901617223988 +// round_robin/A2: hash=1782579242963268431 -> hash=3001610611355826173 +// rebuilt/0: hash=11831117278087605757 -> hash=13831511007419341774 +// rebuilt/1: hash=17469022339767313045 -> hash=16942810041893704776 +// rebuilt/2: hash=11831117278087605757 -> hash=13831511007419341774 +// rebuilt/3: hash=17469022339767313045 -> hash=16942810041893704776 constexpr Pinned kPinned[] = { {"batch/A", - "trades=23 fnv=e337022ac3d8b681 net=71.786509342348978 hash=11831117278087605757 error=''"}, + "trades=23 fnv=e337022ac3d8b681 net=71.786509342348978 hash=13831511007419341774 error=''"}, {"batch/B", - "trades=35 fnv=9fbd9e232cb9843a net=-268.32270548365625 hash=17469022339767313045 error=''"}, + "trades=35 fnv=9fbd9e232cb9843a net=-268.32270548365625 hash=16942810041893704776 error=''"}, {"stream/A", - "trades=23 fnv=a0e7b82126f9e8de net=17.321792890020646 hash=1782579242963268431 error=''"}, + "trades=23 fnv=a0e7b82126f9e8de net=17.321792890020646 hash=3001610611355826173 error=''"}, {"stream/B", - "trades=35 fnv=3eaaaacd90998d64 net=-306.89379320283734 hash=3909006125715448229 error=''"}, + "trades=35 fnv=3eaaaacd90998d64 net=-306.89379320283734 hash=8241673901617223988 error=''"}, {"interleaved/A", - "trades=23 fnv=a0e7b82126f9e8de net=17.321792890020646 hash=1782579242963268431 error=''"}, + "trades=23 fnv=a0e7b82126f9e8de net=17.321792890020646 hash=3001610611355826173 error=''"}, {"interleaved/B", - "trades=35 fnv=3eaaaacd90998d64 net=-306.89379320283734 hash=3909006125715448229 error=''"}, + "trades=35 fnv=3eaaaacd90998d64 net=-306.89379320283734 hash=8241673901617223988 error=''"}, {"round_robin/A1", - "trades=23 fnv=a0e7b82126f9e8de net=17.321792890020646 hash=1782579242963268431 error=''"}, + "trades=23 fnv=a0e7b82126f9e8de net=17.321792890020646 hash=3001610611355826173 error=''"}, {"round_robin/B", - "trades=35 fnv=3eaaaacd90998d64 net=-306.89379320283734 hash=3909006125715448229 error=''"}, + "trades=35 fnv=3eaaaacd90998d64 net=-306.89379320283734 hash=8241673901617223988 error=''"}, {"round_robin/A2", - "trades=23 fnv=a0e7b82126f9e8de net=17.321792890020646 hash=1782579242963268431 error=''"}, + "trades=23 fnv=a0e7b82126f9e8de net=17.321792890020646 hash=3001610611355826173 error=''"}, {"rebuilt/0", - "trades=23 fnv=e337022ac3d8b681 net=71.786509342348978 hash=11831117278087605757 error=''"}, + "trades=23 fnv=e337022ac3d8b681 net=71.786509342348978 hash=13831511007419341774 error=''"}, {"rebuilt/1", - "trades=35 fnv=9fbd9e232cb9843a net=-268.32270548365625 hash=17469022339767313045 error=''"}, + "trades=35 fnv=9fbd9e232cb9843a net=-268.32270548365625 hash=16942810041893704776 error=''"}, {"rebuilt/2", - "trades=23 fnv=e337022ac3d8b681 net=71.786509342348978 hash=11831117278087605757 error=''"}, + "trades=23 fnv=e337022ac3d8b681 net=71.786509342348978 hash=13831511007419341774 error=''"}, {"rebuilt/3", - "trades=35 fnv=9fbd9e232cb9843a net=-268.32270548365625 hash=17469022339767313045 error=''"}, + "trades=35 fnv=9fbd9e232cb9843a net=-268.32270548365625 hash=16942810041893704776 error=''"}, }; diff --git a/tests/test_adapter_live_state_equivalence.cpp b/tests/test_adapter_live_state_equivalence.cpp index 048986c7..9fec15e6 100644 --- a/tests/test_adapter_live_state_equivalence.cpp +++ b/tests/test_adapter_live_state_equivalence.cpp @@ -969,6 +969,15 @@ int reissue_binding_main() { // origin is opened) and its first re-placement (K3) while the second // and third are erased, and the revival skips the leg as the reference // does. +// R5 lane V19-FIX: a leg the script withdrew (strategy.cancel, live or +// dormant: every leg retired) is no revival candidate, and K1 holds it only +// while its origin is live or opened. So variant 2's erasing run no longer +// holds the cancelled dynamic leg (it has no origin; expectation corrected: +// held dormant -> erased), while variant 4's, bound to the opened short, is +// held as before; and variant 3's cancel on bar 4 withdraws the dormant leg, +// so K3 keeps no re-placement for it and the first one goes with the others +// (expectation corrected: held -> erased). Every run still equals its +// retaining twin, and no trade moves. // The seeded runs play the same moves on random tapes, both sides; across // them margin calls land, revivals restore legs, and dormant candidates the // reference still holds are gone from the erasing run. @@ -1044,19 +1053,28 @@ int margin_revival_main() { CHECK(find(at_erased, 4, 129.0) == nullptr); } if (variant == 3) { - // K1 keeps the leg (its origin is opened), K3 its first - // re-placement. + // K1 keeps the leg (its origin is opened); the cancel withdrew it, + // so K3 keeps no re-placement for it (R5 lane V19-FIX: the + // first re-placement was held before). CHECK(leg_erased && leg_erased->dormant && !leg_erased->restored); - CHECK(find(at_erased, 4, 129.0) != nullptr); + CHECK(find(at_erased, 4, 129.0) == nullptr); } if (variant == 2 || variant == 4) { // Cancelled: still dormant, never restored, and the margin call's // is the one trade (expectation corrected: restored, 2 trades -> // not restored, 1 trade -- see the note above). CHECK(leg_kept->dormant && !leg_kept->restored); - CHECK(leg_erased && leg_erased->dormant && !leg_erased->restored); CHECK(erased.trades == 1); } + if (variant == 2) { + // Withdrawn and bound to no origin: K1 released it (R5 lane + // V19-FIX; it was held dormant before). + CHECK(leg_erased == nullptr); + } + if (variant == 4) { + // Withdrawn, but bound to the opened short: K1 holds it. + CHECK(leg_erased && leg_erased->dormant && !leg_erased->restored); + } if (variant == 5 || variant == 6) { // Left standing: restored in both runs, and it closed the rest of // the short. diff --git a/tests/test_adapter_live_state_scaling.cpp b/tests/test_adapter_live_state_scaling.cpp index 1f79f6e5..36c49f31 100644 --- a/tests/test_adapter_live_state_scaling.cpp +++ b/tests/test_adapter_live_state_scaling.cpp @@ -21,10 +21,11 @@ // groups re-issued around the fill price every bar, one filling within a few // bars and cancelling its sibling, the other closing the rest), so rows die // by replacement, by fill and by cancellation, cycle after cycle. The ratio -// gate is the replay's: the churn's recorded rows also carry the kernel's -// continuation, whose cohort rosters keep every opening the adapter accepted -// (the adapter never removes one, a V19-A finding outside this extension), -// so its ratio is printed, and its retained rows are held to the same bound. +// gate was the replay's alone: the churn's recorded rows also carry the +// kernel's continuation, whose cohort rosters kept every opening the adapter +// accepted (a V19-A finding outside this extension), so its ratio was only +// printed. Since R5 lane V19-FIX the adapter takes an origin that can no +// longer be bound off its roster, and the churn's ratio is gated too. // // Fail-before, this TU compiled unchanged against the base tree be372243 // (macOS arm64, Release): it builds, and fails both bounds -- the replay took @@ -83,11 +84,45 @@ // bar-open walk over every earlier bar of the run, rows still bounded (5), // measured 8.27 at 32,000 / 128,000 -- each fails the ratio gate. // +// R5 lane V19-FIX (audit X2, X9) adds three workloads, each timed at a +// calibrated small leg (bars doubling until one run takes kMinLegSeconds of +// CPU) and four times that size, their sizes in turn: +// - the re-cancel at a CONSTANT level: the held long's exit cancelled and +// re-placed every bar at one stop, with recording off and on. The +// cancelled leg kept its lifecycle target, no re-issue at another stop +// superseded it, so K1 held one row per bar for the whole cycle and the +// sweep walked them all at every bar open. A cancel now retires every leg +// of what it withdrew, and K1 lets a withdrawn leg go; +// - strategy.cancel_all() and a re-placed exit at position_avg_price() x +// 0.95 (a constant level too), with recording off: the same rows; +// - a flip with recording on: an entry every second bar, closed on the +// next. Every accepted opening joined its id's kernel roster for good, +// and the kernel folds every roster member into each continuation read, +// so each recorded row cost the openings the run had made. The adapter +// now takes an origin that can no longer be bound out of the kernel's +// roster at the next bar open; the rows below also hold the roster to +// what is live when the run ends, and the churn's ratio is gated now. +// Fail-before, this TU against the lane's base 91d65ad6 (macOS arm64, +// Release, a git archive; the base is too slow for whole rows, so single legs, +// --leg): the constant level without recording took 2.23 s at 16,000 bars and +// 57.13 s at 64,000 (x25.6), retaining 16,000 and 64,000 rows; cancel_all +// 1.89 s and 51.93 s (x27.4), the same rows; the constant level with +// recording 1.36 s at 1,000 bars; the flip 0.207 s at 4,000 bars and 1.427 s +// at 16,000 (x6.9), its rosters ending with 2,000 and 8,000 members. Three +// mutants of the product (Mac, Release, --row): K1 holding a withdrawn leg +// again fails the cancel_all row at x25.88 with 8,000 and 32,000 rows; no +// roster removal fails the flip at x6.96 with 2,000 and 8,000 members; the +// removal kept but a walk over every origin the run accepted at every bar +// fails the flip at x11.99, on the ratio alone. +// // Single-leg mode for peak-RSS rows: `test_adapter_live_state_scaling --leg -// replay|churn|straddle|recancel [--no-recording]` runs one leg and -// prints its CPU time, retained rows and ru_maxrss. +// replay|churn|straddle|recancel|constant|cancelall|flip +// [--no-recording]` runs one leg and prints its CPU time, retained rows and +// ru_maxrss. #include +#include "../src/native_execution_consumer.hpp" + #include #include @@ -124,7 +159,7 @@ int failures = 0; constexpr double kNa = std::numeric_limits::quiet_NaN(); constexpr std::int64_t T = 1736121600000LL; -enum class Workload { Replay, Churn, Straddle, Recancel }; +enum class Workload { Replay, Churn, Straddle, Recancel, Constant, CancelAll, Flip }; long triangle(int index, int period) { const int phase = index % (2 * period); @@ -186,6 +221,32 @@ class ScalingHost final : public source::PineStrategyHost { } return; } + if (workload_ == Workload::Constant) { + // The same, at one stop far below the tape: no re-issue at + // another stop ever supersedes a cancelled leg. + if (i == 0) strategy_entry("L", true, kNa, kNa, 1.0); + if (position > 0.0) { + strategy_cancel("X"); + strategy_exit("X", "L", kNa, 10.0); + } + return; + } + if (workload_ == Workload::CancelAll) { + // cancel_all, then the exit again at 95 % of the entry price + // (below the tape's lowest low): a constant level as well. + if (i == 0) strategy_entry("L", true, kNa, kNa, 1.0); + if (position > 0.0) { + strategy_cancel_all(); + strategy_exit("X", "L", kNa, position_avg_price() * 0.95); + } + return; + } + if (workload_ == Workload::Flip) { + // One opening every second bar, closed on the next. + if (i % 2 == 0) strategy_entry("L", true, kNa, kNa, 1.0); + else strategy_close("L"); + return; + } if (workload_ == Workload::Straddle) { // One trade, then one flat position cycle for the rest of the run: // the entry opens at bar 8 and its take-profit, a quarter above the @@ -221,6 +282,13 @@ class ScalingHost final : public source::PineStrategyHost { std::size_t retained_rows() const { return (adapter_.*access(PlacementTag{})).size(); } std::size_t recorded_rows() const { return broker_state_hashes_.size(); } + // The origins the kernel's cohort rosters still hold (R5 lane V19-FIX). + std::size_t roster_members() const { + std::size_t members = 0; + for (const auto& roster : as_native_consumer(execution_consumer()).request_core().cohorts()) + members += roster.origins.size(); + return members; + } private: Workload workload_; @@ -232,6 +300,7 @@ struct Leg { int trades = 0; std::size_t rows = 0; std::size_t recorded = 0; + std::size_t roster = 0; }; Leg replay(Workload workload, const std::vector& bars, bool recording = true) { @@ -243,6 +312,7 @@ Leg replay(Workload workload, const std::vector& bars, bool recording = tru leg.trades = host.trade_count(); leg.rows = host.retained_rows(); leg.recorded = host.recorded_rows(); + leg.roster = host.roster_members(); CHECK(host.last_error().empty()); CHECK(host.native_state().kind == NativeLifecycleKind::Completed); // Recording mode read the hash once per script bar. @@ -285,6 +355,9 @@ 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. constexpr std::size_t kRowBound = 64; +// The kernel roster members a run can still hold when it ends: the openings +// 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 @@ -304,9 +377,12 @@ void interleaved_best(Workload workload, const std::vector& small_tape, } void cost_and_rows_are_live(Workload workload, const char* name, bool recording = true) { - // The straddle and the re-cancel run four times longer: their bars cost + // The straddle and the re-cancels run four times longer: their bars cost // microseconds. - const bool short_bars = workload == Workload::Straddle || workload == Workload::Recancel; + const bool lane_rows = workload == Workload::Constant || workload == Workload::CancelAll + || workload == Workload::Flip; + const bool short_bars = workload == Workload::Straddle || workload == Workload::Recancel + || lane_rows; const int scale = short_bars ? 4 : 1; int bars = (gated() ? kBars : kBars / 4) * scale; const std::int64_t step = workload == Workload::Straddle ? kDay : kMinute; @@ -328,12 +404,12 @@ void cost_and_rows_are_live(Workload workload, const char* name, bool recording bars *= 2; } 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), " - "%d bars %.4fs (%d trades, %zu rows retained), ratio %.2f (bound %.1f), " - "rows bound %zu\n", + 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), " + "ratio %.2f (bound %.1f), rows bound %zu, roster bound %zu\n", name, recording ? "recording" : "no recording", bars, small.seconds, - small.trades, small.rows, bars * 4, large.seconds, large.trades, large.rows, - ratio, kShapeBound, kRowBound); + small.trades, small.rows, small.roster, bars * 4, large.seconds, large.trades, + large.rows, large.roster, ratio, kShapeBound, kRowBound, kRosterBound); CHECK(small.seconds > 0.0); // The workloads did what they describe. if (workload == Workload::Churn) { @@ -344,27 +420,39 @@ void cost_and_rows_are_live(Workload workload, const char* name, bool recording CHECK(small.trades == 1); CHECK(large.trades == 1); } - if (workload == Workload::Recancel) { + if (workload == Workload::Recancel || workload == Workload::Constant + || workload == Workload::CancelAll) { // The position is held to the end: no trade closes. CHECK(small.trades == 0); CHECK(large.trades == 0); } - // Memory: what the adapter retains does not grow with the run. + if (workload == Workload::Flip) { + // One round trip per two bars; the last opening may still be open. + CHECK(small.trades >= bars / 2 - 1); + CHECK(large.trades >= 2 * bars - 1); + } + // Memory: what the adapter retains does not grow with the run, and + // neither do the kernel's rosters. CHECK(small.rows <= kRowBound); CHECK(large.rows <= kRowBound); + CHECK(small.roster <= kRosterBound); + CHECK(large.roster <= kRosterBound); if (!gated()) { std::printf(" (ratio not gated: non-Release library)\n"); return; } CHECK(small.seconds >= kMinLegSeconds); CHECK(large.seconds >= kMinLegSeconds); - if (workload != Workload::Churn) CHECK(ratio < kShapeBound); + CHECK(ratio < kShapeBound); } int single_leg(const char* workload_name, const char* bars_text, bool recording) { const Workload workload = std::strcmp(workload_name, "churn") == 0 ? Workload::Churn : std::strcmp(workload_name, "straddle") == 0 ? Workload::Straddle : std::strcmp(workload_name, "recancel") == 0 ? Workload::Recancel + : std::strcmp(workload_name, "constant") == 0 ? Workload::Constant + : std::strcmp(workload_name, "cancelall") == 0 ? Workload::CancelAll + : std::strcmp(workload_name, "flip") == 0 ? Workload::Flip : Workload::Replay; const int bars = std::atoi(bars_text); if (bars <= 0) return 2; @@ -373,26 +461,39 @@ int single_leg(const char* workload_name, const char* bars_text, bool recording) struct rusage usage {}; getrusage(RUSAGE_SELF, &usage); std::printf("leg %s bars=%d recording=%d cpu_seconds=%.6f trades=%d rows_retained=%zu " - "recorded=%zu ru_maxrss=%ld\n", + "roster_members=%zu recorded=%zu ru_maxrss=%ld\n", workload_name, bars, recording ? 1 : 0, leg.seconds, leg.trades, leg.rows, - leg.recorded, static_cast(usage.ru_maxrss)); + leg.roster, leg.recorded, static_cast(usage.ru_maxrss)); return failures == 0 ? 0 : 1; } } // namespace int main(int argc, char** argv) { - // --leg [--no-recording] + // --leg [--no-recording] if ((argc == 4 || argc == 5) && std::strcmp(argv[1], "--leg") == 0) return single_leg(argv[2], argv[3], !(argc == 5 && std::strcmp(argv[4], "--no-recording") == 0)); - cost_and_rows_are_live(Workload::Replay, "exit re-issued every bar"); - cost_and_rows_are_live(Workload::Churn, "position cycles with brackets"); - cost_and_rows_are_live(Workload::Straddle, "two exits re-issued every bar through a flat cycle", - false); - cost_and_rows_are_live(Workload::Straddle, "two exits re-issued every bar through a flat cycle"); - cost_and_rows_are_live(Workload::Recancel, "an exit cancelled and re-placed every bar", false); - cost_and_rows_are_live(Workload::Recancel, "an exit cancelled and re-placed every bar"); + // --row : only that workload's rows (a mutant is timed row by row). + const char* only = argc == 3 && std::strcmp(argv[1], "--row") == 0 ? argv[2] : nullptr; + const auto row = [&](const char* key, Workload workload, const char* name, + bool recording = true) { + if (!only || std::strcmp(only, key) == 0) + cost_and_rows_are_live(workload, name, recording); + }; + row("replay", Workload::Replay, "exit re-issued every bar"); + row("churn", Workload::Churn, "position cycles with brackets"); + row("straddle", Workload::Straddle, "two exits re-issued every bar through a flat cycle", + false); + row("straddle", Workload::Straddle, "two exits re-issued every bar through a flat cycle"); + row("recancel", Workload::Recancel, "an exit cancelled and re-placed every bar", false); + row("recancel", Workload::Recancel, "an exit cancelled and re-placed every bar"); + row("constant", Workload::Constant, "an exit cancelled and re-placed every bar at one stop", + false); + row("constant", Workload::Constant, "an exit cancelled and re-placed every bar at one stop"); + row("cancelall", Workload::CancelAll, + "cancel_all and an exit at the entry price x 0.95 every bar", false); + row("flip", Workload::Flip, "an opening every second bar, closed on the next"); if (failures == 0) std::printf("test_adapter_live_state_scaling: ok\n"); return failures == 0 ? 0 : 1; } diff --git a/tests/test_adapter_lookup_index_witness_data.hpp b/tests/test_adapter_lookup_index_witness_data.hpp index bfe89884..ce08a937 100644 --- a/tests/test_adapter_lookup_index_witness_data.hpp +++ b/tests/test_adapter_lookup_index_witness_data.hpp @@ -267,39 +267,136 @@ // 2372878689946457249ull -> 12276848500238226962ull // 11551455189769543158ull -> 1287632709215711658ull // 3653829772705383872ull -> 8142912162810668916ull +// expectation corrected (V19-FIX, kFlip_rows, 96 of 96 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: +// 16253745060943107386ull -> 15092016227729062170ull +// 6472742653990025708ull -> 4449511944144175244ull +// 17926733298352674229ull -> 14173574221892226517ull +// 9270508530197190558ull -> 11141868197266883358ull +// 14112857094849237241ull -> 5770620033420772121ull +// 8414229983564740285ull -> 3157789451207414333ull +// 10746359752997281489ull -> 15624036521156382865ull +// 11327342917346748139ull -> 17987828433123328363ull +// 15741663249864576466ull -> 3782132892539745074ull +// 166295673203555379ull -> 14406538727996760883ull +// 9847631621295824459ull -> 14845895464439448971ull +// 10040586177661954357ull -> 14487807311329646997ull +// 3652453489893413892ull -> 14748183486665371140ull +// 3118670462798758878ull -> 12768807557967508254ull +// 10474078126198609341ull -> 9713166925369100573ull +// 7341378889877344687ull -> 16480235611441211183ull +// 12126087592554352147ull -> 6732375251738161555ull +// 14331224259361586586ull -> 9492220356384667994ull +// 12515314674426934458ull -> 7836886659987736698ull +// 9439093768586643905ull -> 11041759722035017537ull +// 10128726204145726512ull -> 1923076241747348592ull +// 4634971025409324795ull -> 16491371029791645211ull +// 17587856368132401413ull -> 1800320518007831877ull +// 16208960391397884708ull -> 571647502003500868ull +// 11312078043216078629ull -> 2739724503975357253ull +// 2426969803114689844ull -> 9819507431665488276ull +// 3585933631879943122ull -> 9945110324091627954ull +// 2832259810118672891ull -> 11949472718108945819ull +// 18036177934940221953ull -> 9762923116876284513ull +// 10910619071257953634ull -> 18027851627748731010ull +// 14748644346596201690ull -> 1366892124494590874ull +// 8369406916701711452ull -> 14363491720205864892ull +// 16428080042721354525ull -> 5814793330491118429ull +// 12645947721876847095ull -> 14054560180567661527ull +// 18114252141353250107ull -> 7046697962876325595ull +// 15330226044055985432ull -> 13964478754584930616ull +// 9390307090700532432ull -> 10828030611661121392ull +// 16343781248227329115ull -> 16763687418918419771ull +// 15750031037491489846ull -> 14089257237702634390ull +// 1920575849542106629ull -> 4126250144527807077ull +// 3948868447696874647ull -> 18195401520799288599ull +// 420747675298026086ull -> 1257461765259766246ull +// 8589019265853627390ull -> 859001078874563294ull +// 1932373874262959092ull -> 4425771211202385044ull +// 8008055771925310094ull -> 11354019857073859374ull +// 11723426290217688774ull -> 14877394547945152326ull +// 16345742681569462730ull -> 6393077053177388522ull +// 2342381644090032383ull -> 7888781154589438239ull +// 383523862624226896ull -> 6757741011218066608ull +// 8996196586333759313ull -> 6658586324753656657ull +// 16372787877139837275ull -> 4423113805183200923ull +// 14587437596550304251ull -> 16281985077084422619ull +// 9338617930083389575ull -> 1045183813508792423ull +// 18318981450271767799ull -> 9313942597962496343ull +// 7991288521865037042ull -> 12979192255068283250ull +// 17315494415976528516ull -> 4141974475510294052ull +// 14493416948736096779ull -> 7255579447356474699ull +// 10904867566523519441ull -> 11500737887308332433ull +// 1910142022272010465ull -> 11396364648984166081ull +// 14596443360128266905ull -> 17999943307750433945ull +// 15640468833805945335ull -> 1207486754606428343ull +// 9480962714899056581ull -> 13698332948978760389ull +// 17439597072441155065ull -> 11589549934838942681ull +// 9283442237309108337ull -> 9964006416615122225ull +// 13887576500881556772ull -> 16845154769434317988ull +// 11009251971773506108ull -> 7334258627544164572ull +// 1993562540044187938ull -> 4908786545179256930ull +// 11198983186829429491ull -> 3299406129086178611ull +// 8544270679079914718ull -> 5312427811860831678ull +// 11437988267758711561ull -> 11839292439534512009ull +// 1873828430418123285ull -> 10875737639895929077ull +// 16459463093228960608ull -> 11779092014895227520ull +// 1905201141289511792ull -> 4793765047901110384ull +// 7250790518215158252ull -> 1012289705290011692ull +// 11738692487929168838ull -> 1667267280248270342ull +// 921162842506145629ull -> 8455992061434657085ull +// 4155418123858507648ull -> 15957521919212433920ull +// 4606502214704611574ull -> 10969413931083416374ull +// 16276477075807039258ull -> 7170250270476017946ull +// 15149357106454099128ull -> 15006273935975173912ull +// 16272710302921956893ull -> 11184968259335349501ull +// 17443936260886638735ull -> 6276009473983766703ull +// 4911609722655096685ull -> 1757274001997493069ull +// 17157248667384903589ull -> 7772193038207188421ull +// 5719066807449383162ull -> 15686643752958926138ull +// 6024089650767371155ull -> 17800105557473441299ull +// 7246596243020160272ull -> 5863137483546792400ull +// 6163119472326242714ull -> 7816936454583767258ull +// 12460237501453530537ull -> 6161705640611834793ull +// 6059397725321007418ull -> 7780124240324826458ull +// 2907562931088811874ull -> 13353384012423321986ull +// 7844383250641231845ull -> 9198310857862468197ull +// 7579803494196991757ull -> 388071097687117133ull +// 12276848500238226962ull -> 17288887437003769330ull +// 1287632709215711658ull -> 3196584109952517226ull +// 8142912162810668916ull -> 9440871734801450516ull constexpr std::uint64_t kFlip_rows[] = { - 16253745060943107386ull, 6472742653990025708ull, 17926733298352674229ull, - 9270508530197190558ull, 14112857094849237241ull, 8414229983564740285ull, - 10746359752997281489ull, 11327342917346748139ull, 15741663249864576466ull, - 166295673203555379ull, 9847631621295824459ull, 10040586177661954357ull, - 3652453489893413892ull, 3118670462798758878ull, 10474078126198609341ull, - 7341378889877344687ull, 12126087592554352147ull, 14331224259361586586ull, - 12515314674426934458ull, 9439093768586643905ull, 10128726204145726512ull, - 4634971025409324795ull, 17587856368132401413ull, 16208960391397884708ull, - 11312078043216078629ull, 2426969803114689844ull, 3585933631879943122ull, - 2832259810118672891ull, 18036177934940221953ull, 10910619071257953634ull, - 14748644346596201690ull, 8369406916701711452ull, 16428080042721354525ull, - 12645947721876847095ull, 18114252141353250107ull, 15330226044055985432ull, - 9390307090700532432ull, 16343781248227329115ull, 15750031037491489846ull, - 1920575849542106629ull, 3948868447696874647ull, 420747675298026086ull, - 8589019265853627390ull, 1932373874262959092ull, 8008055771925310094ull, - 11723426290217688774ull, 16345742681569462730ull, 2342381644090032383ull, - 383523862624226896ull, 8996196586333759313ull, 16372787877139837275ull, - 14587437596550304251ull, 9338617930083389575ull, 18318981450271767799ull, - 7991288521865037042ull, 17315494415976528516ull, 14493416948736096779ull, - 10904867566523519441ull, 1910142022272010465ull, 14596443360128266905ull, - 15640468833805945335ull, 9480962714899056581ull, 17439597072441155065ull, - 9283442237309108337ull, 13887576500881556772ull, 11009251971773506108ull, - 1993562540044187938ull, 11198983186829429491ull, 8544270679079914718ull, - 11437988267758711561ull, 1873828430418123285ull, 16459463093228960608ull, - 1905201141289511792ull, 7250790518215158252ull, 11738692487929168838ull, - 921162842506145629ull, 4155418123858507648ull, 4606502214704611574ull, - 16276477075807039258ull, 15149357106454099128ull, 16272710302921956893ull, - 17443936260886638735ull, 4911609722655096685ull, 17157248667384903589ull, - 5719066807449383162ull, 6024089650767371155ull, 7246596243020160272ull, - 6163119472326242714ull, 12460237501453530537ull, 6059397725321007418ull, - 2907562931088811874ull, 7844383250641231845ull, 7579803494196991757ull, - 12276848500238226962ull, 1287632709215711658ull, 8142912162810668916ull, + 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, }; // 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 @@ -564,44 +661,142 @@ constexpr std::uint64_t kFlip_rows[] = { // 11205667894700736094ull -> 9845813721495779641ull // 8315881640058290398ull -> 10443580480075697786ull // 13448281260411235360ull -> 5230704585495255452ull +// expectation corrected (V19-FIX, kFlip_reads, 96 of 96 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: +// 6478528705710872936ull -> 10304825573213448968ull +// 4062680184287948818ull -> 14774003860189224946ull +// 8046245203875479380ull -> 11801395281262233716ull +// 17446194831643237479ull -> 8061934143473682151ull +// 9827868313573449750ull -> 15983140295339383862ull +// 16661672661516579093ull -> 5166653331022736661ull +// 13481905532966543760ull -> 5250563911700661104ull +// 17888735029477130037ull -> 7793104124436293557ull +// 18186272301380887129ull -> 1336784375798636377ull +// 9706501920346457156ull -> 6339830337021579012ull +// 6271101303749260291ull -> 16252798169114118083ull +// 3895961377347356996ull -> 10937184901662388644ull +// 9841792095339395472ull -> 18064999594731702480ull +// 13691158137853641884ull -> 3546952347196282172ull +// 11640887122827588923ull -> 13395340388338915323ull +// 11002870047446674402ull -> 8229077577636098658ull +// 17695099847328686786ull -> 6541813106524409698ull +// 7531398212200172065ull -> 25242829173903041ull +// 6039807541864526871ull -> 8874377228534856759ull +// 5492859970288906028ull -> 13648938798136741164ull +// 2560655468091152271ull -> 11200265295650397519ull +// 8797288134016690854ull -> 11420272068366920678ull +// 2357616186618578823ull -> 3753005678972800967ull +// 10154240680251318968ull -> 4896870620618706776ull +// 13283392474027762849ull -> 6895518920767105217ull +// 9593370961360490613ull -> 7636324161499144341ull +// 18412961234764701974ull -> 8785945007507579830ull +// 12741743633477973534ull -> 5297575674933281534ull +// 11441968842250654539ull -> 13537048838690550187ull +// 2756927217436013436ull -> 16051941383930194236ull +// 12644920543666896556ull -> 10552551845124524652ull +// 5845845022367936569ull -> 15672638477538649241ull +// 17399714529314999505ull -> 14086945637463962641ull +// 10324781946236826819ull -> 10134206932178793443ull +// 8256089344562233159ull -> 11512602377523606119ull +// 2987202095041381918ull -> 2958147687598254910ull +// 16086572264982522588ull -> 6458026266382303356ull +// 3451336343320410059ull -> 1094964303920914155ull +// 6409963276685534018ull -> 6484787112426264738ull +// 6441785875061584235ull -> 12388725274527404683ull +// 18114824801124390954ull -> 17043242768143307178ull +// 13081710816759699017ull -> 5476835457134592009ull +// 724362360295368150ull -> 2025850526353442102ull +// 8027790875600026810ull -> 1451154197906290714ull +// 805271295488462765ull -> 6635868355983485389ull +// 17345766064751009099ull -> 7018381114706383243ull +// 6549223694028461514ull -> 13248420308395735018ull +// 1946910973279252907ull -> 774767130641043275ull +// 3692115263936378099ull -> 13252007359081704531ull +// 9088102552887757047ull -> 6750492291307654391ull +// 11607288484901616317ull -> 2031163009273496765ull +// 8213619675654811878ull -> 9769809183116681670ull +// 10316073271182050754ull -> 14493685479901302050ull +// 11031350490524374511ull -> 4876420665985364975ull +// 2121751619081235518ull -> 12501338337948389822ull +// 5676986458275301299ull -> 14207736975056792147ull +// 6828354087191835165ull -> 10604319388773724637ull +// 5520127346134787783ull -> 1181294512016461575ull +// 12453149886807094670ull -> 5062201717018553710ull +// 8102639479813734460ull -> 5819082412660383228ull +// 10223611487998353446ull -> 4925793200130354182ull +// 9964037087197491965ull -> 14181407321277195773ull +// 245342923715259987ull -> 18275617508396411315ull +// 969373393475095615ull -> 4666214838307866879ull +// 6035803215035915967ull -> 13997906523391047231ull +// 2480131396524111528ull -> 11459677085401802248ull +// 4330447007451501092ull -> 5810149534385663204ull +// 15461158577847566122ull -> 8487172727633256810ull +// 16958333545353574080ull -> 7417382166705753312ull +// 8701947342227069272ull -> 9462378120791609816ull +// 4052165017544932007ull -> 12680325011617368327ull +// 7192655814271415670ull -> 7441435328919647574ull +// 10519387515851508397ull -> 13407951422463106989ull +// 13581544321164632037ull -> 9986964831043767237ull +// 8327424455040633729ull -> 14464180164790259585ull +// 1479162619290600479ull -> 5220370176240914239ull +// 12868939282858468537ull -> 3518323198598619449ull +// 15583168565953590910ull -> 2133255899590215934ull +// 12117235987602557150ull -> 3011009182271535838ull +// 6179020928250082455ull -> 14836384915404578807ull +// 13952448398464881364ull -> 9555956714003340276ull +// 6239568557435516091ull -> 11964636267214528571ull +// 15334410999887155388ull -> 16921397335982440924ull +// 14917885563552015346ull -> 10586397832022517650ull +// 7163508901456790242ull -> 18096821421422867234ull +// 15846563730244524649ull -> 14108486707933080521ull +// 18249442230432153208ull -> 2822797701958308088ull +// 13409761787512645014ull -> 8446901108100253910ull +// 5080240816212633333ull -> 9675426367378755701ull +// 18392787400696255367ull -> 4615585398233598887ull +// 13441365450637354287ull -> 4951618999197916175ull +// 1337575926927626260ull -> 13485186398181682580ull +// 16438216603439850862ull -> 2264266327693067886ull +// 9845813721495779641ull -> 7513550480889044889ull +// 10443580480075697786ull -> 14297335183847666490ull +// 5230704585495255452ull -> 3496786386085197052ull constexpr std::uint64_t kFlip_reads[] = { - 6478528705710872936ull, 4062680184287948818ull, 8046245203875479380ull, - 17446194831643237479ull, 9827868313573449750ull, 16661672661516579093ull, - 13481905532966543760ull, 17888735029477130037ull, 18186272301380887129ull, - 9706501920346457156ull, 6271101303749260291ull, 3895961377347356996ull, - 9841792095339395472ull, 13691158137853641884ull, 11640887122827588923ull, - 11002870047446674402ull, 17695099847328686786ull, 7531398212200172065ull, - 6039807541864526871ull, 5492859970288906028ull, 2560655468091152271ull, - 8797288134016690854ull, 2357616186618578823ull, 10154240680251318968ull, - 13283392474027762849ull, 9593370961360490613ull, 18412961234764701974ull, - 12741743633477973534ull, 11441968842250654539ull, 2756927217436013436ull, - 12644920543666896556ull, 5845845022367936569ull, 17399714529314999505ull, - 10324781946236826819ull, 8256089344562233159ull, 2987202095041381918ull, - 16086572264982522588ull, 3451336343320410059ull, 6409963276685534018ull, - 6441785875061584235ull, 18114824801124390954ull, 13081710816759699017ull, - 724362360295368150ull, 8027790875600026810ull, 805271295488462765ull, - 17345766064751009099ull, 6549223694028461514ull, 1946910973279252907ull, - 3692115263936378099ull, 9088102552887757047ull, 11607288484901616317ull, - 8213619675654811878ull, 10316073271182050754ull, 11031350490524374511ull, - 2121751619081235518ull, 5676986458275301299ull, 6828354087191835165ull, - 5520127346134787783ull, 12453149886807094670ull, 8102639479813734460ull, - 10223611487998353446ull, 9964037087197491965ull, 245342923715259987ull, - 969373393475095615ull, 6035803215035915967ull, 2480131396524111528ull, - 4330447007451501092ull, 15461158577847566122ull, 16958333545353574080ull, - 8701947342227069272ull, 4052165017544932007ull, 7192655814271415670ull, - 10519387515851508397ull, 13581544321164632037ull, 8327424455040633729ull, - 1479162619290600479ull, 12868939282858468537ull, 15583168565953590910ull, - 12117235987602557150ull, 6179020928250082455ull, 13952448398464881364ull, - 6239568557435516091ull, 15334410999887155388ull, 14917885563552015346ull, - 7163508901456790242ull, 15846563730244524649ull, 18249442230432153208ull, - 13409761787512645014ull, 5080240816212633333ull, 18392787400696255367ull, - 13441365450637354287ull, 1337575926927626260ull, 16438216603439850862ull, - 9845813721495779641ull, 10443580480075697786ull, 5230704585495255452ull, + 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, }; // 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). -constexpr std::uint64_t kFlip_final = 8142912162810668916ull; +// 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; constexpr Trade kFlip_trades[] = { {1736121720000LL, 1736121840000LL, 102, 106, 1, 0}, {1736121780000LL, 1736121840000LL, 103, 106, 1, 0}, @@ -1001,47 +1196,168 @@ constexpr Trade kFlip_trades[] = { // 1517657825142525256ull -> 847914663948102735ull // 8136121796878598192ull -> 13892044217556855428ull // 14748775395945483089ull -> 13660803373778369733ull +// expectation corrected (V19-FIX, kBrackets_rows, 120 of 120 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: +// 8334872870019025748ull -> 910134550716034036ull +// 13360709217017657591ull -> 10119054608448654679ull +// 10535429502228176464ull -> 5666477157831623024ull +// 10641993224395477495ull -> 202804637143337815ull +// 5793564647093953626ull -> 4829290339771875162ull +// 942762013534911765ull -> 10813129365163527157ull +// 13457899148930125708ull -> 2873082008237019980ull +// 106456520101564644ull -> 17093419766211306788ull +// 5039829652836662017ull -> 17114718258109977185ull +// 3877689383445607833ull -> 13157751279945694777ull +// 3743994968894951004ull -> 218728992015652860ull +// 6976703206381129071ull -> 14366067783818258415ull +// 2610850650332144130ull -> 2053665803239548226ull +// 4936473648863707472ull -> 16605355454280111952ull +// 1085392781096946239ull -> 12012249101044242847ull +// 11547451756940786931ull -> 6571732001567235539ull +// 13254470163010421332ull -> 1030372738842403124ull +// 3054428054409708210ull -> 5009615712759518098ull +// 5203201378060961523ull -> 11188453092947462355ull +// 920649191826636597ull -> 7315481592762531637ull +// 2836007152066398480ull -> 15172698683941778544ull +// 16238499406853183015ull -> 13076695872824005863ull +// 11753583692182122582ull -> 8630952403774920278ull +// 6076889528022189571ull -> 5446218045192577731ull +// 4468524040073098362ull -> 8868444455935332378ull +// 1361671273630249456ull -> 11935211056196573296ull +// 17391245497122418223ull -> 10851985789766247663ull +// 16690246567520485150ull -> 13162717003573213214ull +// 14649788234546076789ull -> 3878781687326319893ull +// 3784717069003472756ull -> 17299684057647085908ull +// 7708186704992227901ull -> 3804735542114435005ull +// 16290128891025612881ull -> 10757881489481288273ull +// 9239139951755611761ull -> 7304910952741449329ull +// 9710527705088653691ull -> 6446837817330552219ull +// 6051840080337340855ull -> 517463869567802231ull +// 10374587154133501878ull -> 16486081082077568694ull +// 5992285686787781017ull -> 8342654310771400857ull +// 10896667026894783964ull -> 18315162927804124892ull +// 5383487050914091575ull -> 11131921289741383191ull +// 4164450837646137464ull -> 4272804311264028152ull +// 14304037982303945547ull -> 17329153790653310123ull +// 9109177592995806849ull -> 2805380709705245281ull +// 6311779371154440247ull -> 11779111360073316823ull +// 11194256911518451624ull -> 169194399111162280ull +// 3750895309451911440ull -> 11895438524630435024ull +// 16408018385433470870ull -> 11609073112680438102ull +// 14975720111829249703ull -> 13966776254915757287ull +// 17820534183914471237ull -> 429534206234234885ull +// 4889518241866659644ull -> 15196669054502239260ull +// 3430785674293839292ull -> 11931512644904705052ull +// 5598504095355581177ull -> 11456573762602716761ull +// 6017115083129523200ull -> 15718773719268670624ull +// 752875482364389342ull -> 10989825173285828350ull +// 4652308351718405867ull -> 11346633789448934891ull +// 15854604489738417824ull -> 1699367467603584576ull +// 462792556154458247ull -> 9509380442542362055ull +// 2145723172090300887ull -> 5849765746586438263ull +// 842211564644927246ull -> 15258883296401804974ull +// 12834429359306991318ull -> 12668399090838414326ull +// 13124663479216301872ull -> 17428702340345096816ull +// 17974433890954091027ull -> 16044980621491544275ull +// 18075211672652087747ull -> 7858613838309440451ull +// 12187303804130240035ull -> 4973377506472252675ull +// 9441276880580012986ull -> 15666991206354451962ull +// 4095544721703157039ull -> 427471937805578063ull +// 14711337735174794359ull -> 13475650653372852055ull +// 847914663948102735ull -> 8639057947851950159ull +// 13892044217556855428ull -> 5938463183864513700ull +// 13660803373778369733ull -> 5648745424618098437ull +// 9591750462089292470ull -> 447259644375350934ull +// 14029860775276681961ull -> 16648371541827537609ull +// 4194622471163317744ull -> 16429366851237249424ull +// 624809080135772504ull -> 279187655991346264ull +// 8623898081821661192ull -> 15953137644069010440ull +// 3548111120677580005ull -> 7431687286361549413ull +// 6342994024223956793ull -> 5023789027380833817ull +// 9740810065714372190ull -> 10248295502726720638ull +// 1298463433058162992ull -> 640594980859648944ull +// 15905966409817540109ull -> 448592334131119277ull +// 6033160696117752362ull -> 16276383010288949834ull +// 3474677652469595527ull -> 442889274262864391ull +// 8884713555397588050ull -> 4333712493194412434ull +// 2368591388795353640ull -> 14568797647822043048ull +// 13245537231214579266ull -> 2692075964144184930ull +// 17941028023959938660ull -> 13961322833977115428ull +// 7923221904968562468ull -> 15127421684109569508ull +// 15072616057125816801ull -> 14509829626384044961ull +// 9858497254962719885ull -> 5027884074442838157ull +// 3883010623068849745ull -> 9918179728360057649ull +// 11410471682079354635ull -> 3559274869490871147ull +// 8518738390065125497ull -> 2619065441802592089ull +// 14762342688950574794ull -> 9017534287281633674ull +// 4297413482127048039ull -> 10142753397763051047ull +// 11795221103021645870ull -> 1825167456880839822ull +// 496873695776002218ull -> 12808870162637299498ull +// 7155950455884465865ull -> 16833369335393909673ull +// 13086042835321872912ull -> 18259761263316881488ull +// 7090010914641837446ull -> 2751903624558806694ull +// 10833394612776220029ull -> 12840688134865061533ull +// 16367631127682803649ull -> 17213452481114196577ull +// 14608636943754743167ull -> 110464992498916575ull +// 10859278198316118452ull -> 2694244057601629716ull +// 13565466691059087919ull -> 4590220084927802863ull +// 12304198591857105265ull -> 2437199406152424049ull +// 17233249480785501339ull -> 8159801410563329851ull +// 12382064500819248439ull -> 14735699852171413911ull +// 18404885541293014879ull -> 9157222081581125695ull +// 13638680490319341502ull -> 1085291491204167422ull +// 16783483336094040187ull -> 6346057658078798747ull +// 5838591129327424874ull -> 9775349377282567210ull +// 1810381457022293037ull -> 18290376404771660045ull +// 11822942635648263435ull -> 14109635784428120043ull +// 7381071206689070265ull -> 6793532772339349305ull +// 11672360243871279823ull -> 12315454366145997871ull +// 3645503420049892317ull -> 3340912588114346141ull +// 16749284443003276407ull -> 10626957428750553111ull +// 1499338885233160011ull -> 16133891667902416811ull +// 16808495481113052294ull -> 18129147092791966950ull +// 14610337086459375067ull -> 12096596197056575067ull +// 3288451699475749751ull -> 12419458164604031287ull constexpr std::uint64_t kBrackets_rows[] = { - 8334872870019025748ull, 13360709217017657591ull, 10535429502228176464ull, - 10641993224395477495ull, 5793564647093953626ull, 942762013534911765ull, - 13457899148930125708ull, 106456520101564644ull, 5039829652836662017ull, - 3877689383445607833ull, 3743994968894951004ull, 6976703206381129071ull, - 2610850650332144130ull, 4936473648863707472ull, 1085392781096946239ull, - 11547451756940786931ull, 13254470163010421332ull, 3054428054409708210ull, - 5203201378060961523ull, 920649191826636597ull, 2836007152066398480ull, - 16238499406853183015ull, 11753583692182122582ull, 6076889528022189571ull, - 4468524040073098362ull, 1361671273630249456ull, 17391245497122418223ull, - 16690246567520485150ull, 14649788234546076789ull, 3784717069003472756ull, - 7708186704992227901ull, 16290128891025612881ull, 9239139951755611761ull, - 9710527705088653691ull, 6051840080337340855ull, 10374587154133501878ull, - 5992285686787781017ull, 10896667026894783964ull, 5383487050914091575ull, - 4164450837646137464ull, 14304037982303945547ull, 9109177592995806849ull, - 6311779371154440247ull, 11194256911518451624ull, 3750895309451911440ull, - 16408018385433470870ull, 14975720111829249703ull, 17820534183914471237ull, - 4889518241866659644ull, 3430785674293839292ull, 5598504095355581177ull, - 6017115083129523200ull, 752875482364389342ull, 4652308351718405867ull, - 15854604489738417824ull, 462792556154458247ull, 2145723172090300887ull, - 842211564644927246ull, 12834429359306991318ull, 13124663479216301872ull, - 17974433890954091027ull, 18075211672652087747ull, 12187303804130240035ull, - 9441276880580012986ull, 4095544721703157039ull, 14711337735174794359ull, - 847914663948102735ull, 13892044217556855428ull, 13660803373778369733ull, - 9591750462089292470ull, 14029860775276681961ull, 4194622471163317744ull, - 624809080135772504ull, 8623898081821661192ull, 3548111120677580005ull, - 6342994024223956793ull, 9740810065714372190ull, 1298463433058162992ull, - 15905966409817540109ull, 6033160696117752362ull, 3474677652469595527ull, - 8884713555397588050ull, 2368591388795353640ull, 13245537231214579266ull, - 17941028023959938660ull, 7923221904968562468ull, 15072616057125816801ull, - 9858497254962719885ull, 3883010623068849745ull, 11410471682079354635ull, - 8518738390065125497ull, 14762342688950574794ull, 4297413482127048039ull, - 11795221103021645870ull, 496873695776002218ull, 7155950455884465865ull, - 13086042835321872912ull, 7090010914641837446ull, 10833394612776220029ull, - 16367631127682803649ull, 14608636943754743167ull, 10859278198316118452ull, - 13565466691059087919ull, 12304198591857105265ull, 17233249480785501339ull, - 12382064500819248439ull, 18404885541293014879ull, 13638680490319341502ull, - 16783483336094040187ull, 5838591129327424874ull, 1810381457022293037ull, - 11822942635648263435ull, 7381071206689070265ull, 11672360243871279823ull, - 3645503420049892317ull, 16749284443003276407ull, 1499338885233160011ull, - 16808495481113052294ull, 14610337086459375067ull, 3288451699475749751ull, + 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, }; // 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 @@ -1384,52 +1700,174 @@ constexpr std::uint64_t kBrackets_rows[] = { // 4996303423142256143ull -> 18257799409042362488ull // 17077817639821690453ull -> 16240753891665587609ull // 4078065063577028156ull -> 15037192126176481292ull +// expectation corrected (V19-FIX, kBrackets_reads, 120 of 120 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: +// 4739892221332779582ull -> 174544879155911710ull +// 5655887366017616157ull -> 6847890701157152189ull +// 3028232177184384512ull -> 14236720572403425120ull +// 12402145005580069251ull -> 9323947307018159523ull +// 7671123206500769573ull -> 6706848899178691109ull +// 17484900505784987776ull -> 6612524546470024288ull +// 3583941090692055958ull -> 10668883385252112854ull +// 2221649510857572706ull -> 231581670271637154ull +// 18050251846835627007ull -> 13242222575440997023ull +// 12714528612331435395ull -> 17290172295419987875ull +// 7455648808205154596ull -> 3265116288063283204ull +// 10216388403410372825ull -> 8205670759236763993ull +// 17007195037053402558ull -> 112561924048020734ull +// 5793211091864040239ull -> 15428098460808809263ull +// 2109629087352076693ull -> 17567572508977123509ull +// 15985919644494457206ull -> 11229049955366294166ull +// 9906593648070761032ull -> 18321533072536116520ull +// 12122784839908138940ull -> 16105561550759864220ull +// 16063154401943613081ull -> 483522463541730617ull +// 3445300903183661048ull -> 9570911977664884728ull +// 10228481268834253523ull -> 7580047313596786995ull +// 14527585151417812459ull -> 4665256402668508715ull +// 7555416728816736808ull -> 421497521705913896ull +// 16293455827009617666ull -> 17087607459663445442ull +// 1766535187314285157ull -> 12527499930243019525ull +// 1896935393898740897ull -> 2971437309355039777ull +// 18156741507825770935ull -> 9863935661287706359ull +// 701187128729410238ull -> 6851589387112729438ull +// 4537949753393122304ull -> 10761885404547615008ull +// 12650812916241334473ull -> 5179210173701881257ull +// 250835776492827583ull -> 12700378635574144447ull +// 18192620613385455776ull -> 12732163138534186016ull +// 13611956761963604903ull -> 14016674439498449031ull +// 5524169265206156078ull -> 11327403139305040686ull +// 939566071450927629ull -> 5747816375553482093ull +// 8037399307531457396ull -> 12386497247962690388ull +// 13689726928377016737ull -> 16040095552360636577ull +// 9921983481590252372ull -> 17340479382499593300ull +// 10073660989609170451ull -> 12046954179175030003ull +// 12133237742538308118ull -> 3330771664215515798ull +// 13542820496152574438ull -> 2985817778317811366ull +// 2209185195462674729ull -> 3949828394086364361ull +// 10237976682105629587ull -> 16198687903297035507ull +// 5995559652838844381ull -> 11881556093292430173ull +// 15548141245561332611ull -> 10043888482375429571ull +// 14291969925207703845ull -> 7435285615030433029ull +// 7079976588964726412ull -> 9041137245577600492ull +// 5044403353664586014ull -> 993655137088931422ull +// 8788222457651749511ull -> 16182190595469348839ull +// 11154217415156650290ull -> 5762190984609537746ull +// 10094428578958215502ull -> 87663964338170542ull +// 6566395239097541438ull -> 15322366287499145950ull +// 11900883726574879716ull -> 13403031039768262916ull +// 14835933664748750470ull -> 13795061532521471462ull +// 11488414138166673767ull -> 6407369734919200583ull +// 13974836420689757110ull -> 8575404273882652022ull +// 6271847513377659337ull -> 962815066506496169ull +// 14959523785375708996ull -> 13267308122545089892ull +// 6351166509631355881ull -> 11888142924425674761ull +// 4222230896953780291ull -> 12543244761273968195ull +// 7421902330351143996ull -> 743116436140857884ull +// 4757994808803652993ull -> 13428981689322201921ull +// 5089147002887001763ull -> 3888528505659186179ull +// 1303500404069559832ull -> 16920970929338735704ull +// 578883850662080644ull -> 11486836585396753956ull +// 15276383335327924996ull -> 3912352931341674340ull +// 18257799409042362488ull -> 488391584166124984ull +// 16240753891665587609ull -> 594818243880175417ull +// 15037192126176481292ull -> 5897410099435466572ull +// 14335142723472814114ull -> 6292341051554398594ull +// 10604938595666936785ull -> 5652341650549197297ull +// 76980935641048994ull -> 15500575164592052866ull +// 6962084663696965213ull -> 6616463239552538973ull +// 18042301448600920960ull -> 6924796937138718592ull +// 5623844503447218087ull -> 12199564954470416935ull +// 13837962124961092738ull -> 1262923056330410530ull +// 5889176865827454703ull -> 14580867601298688079ull +// 16248267745437393157ull -> 695300185954652101ull +// 9526124435653635313ull -> 4911858693999487313ull +// 9814892380038309609ull -> 4052266239546809097ull +// 4497261540158274334ull -> 16261647227390403486ull +// 4377762421420690979ull -> 9269996099404471907ull +// 11012194229893243922ull -> 17364765293714899346ull +// 12729418279697368570ull -> 9253740477123675482ull +// 12807805204908430147ull -> 3598536848022417347ull +// 1626409207785359757ull -> 15772584097012446157ull +// 15493843079506229016ull -> 13220139554987575832ull +// 14596289153400320409ull -> 5696897036054960153ull +// 4128425286998852037ull -> 16985841889497997093ull +// 15363543654703513962ull -> 10415231377040584138ull +// 3636090022491604600ull -> 12984613912057588504ull +// 17688319270564581023ull -> 598945118998222943ull +// 4194108682630897238ull -> 6912671141973928854ull +// 2613113596262334944ull -> 18429543982731833536ull +// 13329703789369519650ull -> 5917122860046364322ull +// 15817115559197810333ull -> 14977518267164775357ull +// 1122513368883747779ull -> 15294004932482913795ull +// 6278377288530944554ull -> 14861726070560837386ull +// 14848095210822121228ull -> 14467806706499675692ull +// 8461391603033169466ull -> 6260501953792293914ull +// 7516222533364987119ull -> 8227642000683598159ull +// 7990175175250616296ull -> 16328367771257271624ull +// 1303013540635210243ull -> 2080811162938952899ull +// 14097200175659872331ull -> 4230200989955191115ull +// 14761731365878354193ull -> 2243443394831501937ull +// 7397905735500448184ull -> 14575945386085730904ull +// 16657656482942213791ull -> 7236032703621894143ull +// 14117635983584982925ull -> 11053348144569293517ull +// 10091852819729333069ull -> 12741152113770246445ull +// 8369770667117612021ull -> 6345919259715423413ull +// 6400810446088210191ull -> 4116643839905048431ull +// 11060519004157166686ull -> 13283796175120077278ull +// 839532712499273094ull -> 14167185253504234566ull +// 13781942388328867938ull -> 11430800879610209506ull +// 11920010702058403336ull -> 14033714145752220328ull +// 3959719828402442743ull -> 739675710812434647ull +// 4748180202727637282ull -> 11662106361049752258ull +// 8670934686211779845ull -> 8075508688952706341ull +// 12646157929630605187ull -> 3795209660336936451ull +// 5822263896316998189ull -> 10247852932638933229ull constexpr std::uint64_t kBrackets_reads[] = { - 4739892221332779582ull, 5655887366017616157ull, 3028232177184384512ull, - 12402145005580069251ull, 7671123206500769573ull, 17484900505784987776ull, - 3583941090692055958ull, 2221649510857572706ull, 18050251846835627007ull, - 12714528612331435395ull, 7455648808205154596ull, 10216388403410372825ull, - 17007195037053402558ull, 5793211091864040239ull, 2109629087352076693ull, - 15985919644494457206ull, 9906593648070761032ull, 12122784839908138940ull, - 16063154401943613081ull, 3445300903183661048ull, 10228481268834253523ull, - 14527585151417812459ull, 7555416728816736808ull, 16293455827009617666ull, - 1766535187314285157ull, 1896935393898740897ull, 18156741507825770935ull, - 701187128729410238ull, 4537949753393122304ull, 12650812916241334473ull, - 250835776492827583ull, 18192620613385455776ull, 13611956761963604903ull, - 5524169265206156078ull, 939566071450927629ull, 8037399307531457396ull, - 13689726928377016737ull, 9921983481590252372ull, 10073660989609170451ull, - 12133237742538308118ull, 13542820496152574438ull, 2209185195462674729ull, - 10237976682105629587ull, 5995559652838844381ull, 15548141245561332611ull, - 14291969925207703845ull, 7079976588964726412ull, 5044403353664586014ull, - 8788222457651749511ull, 11154217415156650290ull, 10094428578958215502ull, - 6566395239097541438ull, 11900883726574879716ull, 14835933664748750470ull, - 11488414138166673767ull, 13974836420689757110ull, 6271847513377659337ull, - 14959523785375708996ull, 6351166509631355881ull, 4222230896953780291ull, - 7421902330351143996ull, 4757994808803652993ull, 5089147002887001763ull, - 1303500404069559832ull, 578883850662080644ull, 15276383335327924996ull, - 18257799409042362488ull, 16240753891665587609ull, 15037192126176481292ull, - 14335142723472814114ull, 10604938595666936785ull, 76980935641048994ull, - 6962084663696965213ull, 18042301448600920960ull, 5623844503447218087ull, - 13837962124961092738ull, 5889176865827454703ull, 16248267745437393157ull, - 9526124435653635313ull, 9814892380038309609ull, 4497261540158274334ull, - 4377762421420690979ull, 11012194229893243922ull, 12729418279697368570ull, - 12807805204908430147ull, 1626409207785359757ull, 15493843079506229016ull, - 14596289153400320409ull, 4128425286998852037ull, 15363543654703513962ull, - 3636090022491604600ull, 17688319270564581023ull, 4194108682630897238ull, - 2613113596262334944ull, 13329703789369519650ull, 15817115559197810333ull, - 1122513368883747779ull, 6278377288530944554ull, 14848095210822121228ull, - 8461391603033169466ull, 7516222533364987119ull, 7990175175250616296ull, - 1303013540635210243ull, 14097200175659872331ull, 14761731365878354193ull, - 7397905735500448184ull, 16657656482942213791ull, 14117635983584982925ull, - 10091852819729333069ull, 8369770667117612021ull, 6400810446088210191ull, - 11060519004157166686ull, 839532712499273094ull, 13781942388328867938ull, - 11920010702058403336ull, 3959719828402442743ull, 4748180202727637282ull, - 8670934686211779845ull, 12646157929630605187ull, 5822263896316998189ull, + 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, }; // 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. -constexpr std::uint64_t kBrackets_final = 3288451699475749751ull; +// 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; constexpr Trade kBrackets_trades[] = { {1736121900000LL, 1736121960000LL, 105.5, 108.25, 1, 0}, {1736121900000LL, 1736124000000LL, 105.5, 106.5, 1, 0}, @@ -1765,61 +2203,222 @@ constexpr Trade kBrackets_trades[] = { // 17793508491377013222ull -> 5880827786329181412ull // 8582559418133536449ull -> 309376478011650575ull // 1521244548062449214ull -> 7955431514292085542ull +// expectation corrected (V19-FIX, kChains_rows, 160 of 160 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: +// 18118773846703172955ull -> 18068305618654701499ull +// 5721802392324366115ull -> 7631310091482109571ull +// 10905164654339285863ull -> 3548794957611195047ull +// 5013225652888131384ull -> 9238067768069886424ull +// 10468378600657706840ull -> 15801514563772350680ull +// 4984150131456004790ull -> 17906063143479564342ull +// 149999620063792309ull -> 14314488034311577333ull +// 13837273285924174060ull -> 10547448244184431820ull +// 17253349212883400798ull -> 7123489397519609406ull +// 4968656904641771691ull -> 1602213910524840811ull +// 12996647306096577911ull -> 9787597445052736151ull +// 12050434476245562318ull -> 17601291968173311470ull +// 1949115689775911010ull -> 5776057093989319106ull +// 236707637652846182ull -> 5050622455696579782ull +// 12978903933461153109ull -> 14615448368462347221ull +// 8305476406764741103ull -> 17881255140791172527ull +// 15440782951821405120ull -> 12765402170990768512ull +// 849170925203470684ull -> 17731227218325476252ull +// 3041524397010971513ull -> 1751899773874851993ull +// 16432853769520231056ull -> 17508832565167325456ull +// 13722835947488180503ull -> 9700834346346522071ull +// 6797677290394154837ull -> 1282406435677068885ull +// 5503293816192598557ull -> 3633697705136355869ull +// 7380873684757359371ull -> 7615819126236245163ull +// 18384931392887404742ull -> 483990815715194534ull +// 17576550742654107858ull -> 17161117397723763954ull +// 10034429714885644353ull -> 13706192373597017281ull +// 17685959618480428830ull -> 8349535569627227390ull +// 14067861181115832148ull -> 1043431932614584276ull +// 9826400445982679683ull -> 14601237308263653315ull +// 4865195924370756178ull -> 5546360134897835890ull +// 18075243038383432276ull -> 6847183254477845044ull +// 4533567546042864084ull -> 780618388197566036ull +// 11925944213371644183ull -> 1869891317481091735ull +// 16917928354769909352ull -> 14666026974162495080ull +// 8952258716287445237ull -> 2462504664095454997ull +// 14076435456943559315ull -> 9050713444551814707ull +// 3293983021922835245ull -> 18273004200638640909ull +// 17563125179719782110ull -> 2005853044969644190ull +// 4792423586669271067ull -> 3149925508439969851ull +// 4901113439335395035ull -> 5854844698006220443ull +// 16082409071399819038ull -> 9381219014372881502ull +// 4311096251268871657ull -> 18324992465300580137ull +// 1566309696546720234ull -> 1461894063981629002ull +// 17726580741032482463ull -> 17437165102333019839ull +// 16176976223887152299ull -> 8044544612955041611ull +// 13932002980086809234ull -> 16351394341610307986ull +// 11101212487626629907ull -> 14412883952315230739ull +// 2116212219075360821ull -> 10721912884113213397ull +// 17813414502748774299ull -> 12992048657322263867ull +// 3267914628515888566ull -> 3339483349076882774ull +// 6356007132094407182ull -> 12589462032708666350ull +// 4215131085225742667ull -> 4157120851447613547ull +// 12626433458790281132ull -> 14533664878044930220ull +// 10587303005062577641ull -> 10713585920480808169ull +// 7797918666513653329ull -> 2002537927364435313ull +// 2466985235384963498ull -> 10170703978995689194ull +// 5061056481733569174ull -> 3860537231187349238ull +// 12984321220918302464ull -> 13713491075625798656ull +// 5452376178295345207ull -> 608981548879404887ull +// 14498213029221461761ull -> 13504708270136522593ull +// 522673331324384805ull -> 6427407450358014981ull +// 16739608802754949521ull -> 4929862301177442641ull +// 874962087675576020ull -> 2268853989619820532ull +// 1959313128068629851ull -> 6103887638466893787ull +// 9731615380264829425ull -> 11509749389073642609ull +// 15498268764305233621ull -> 18133726165290830677ull +// 291679968012038958ull -> 15915101668393006094ull +// 7315837474539069482ull -> 5109293539399095562ull +// 1066471724404445284ull -> 4762639120474267940ull +// 5602073364694883392ull -> 16578121073498307040ull +// 16585610110763320757ull -> 12298653987130833045ull +// 13406768573395435523ull -> 14615479905553429955ull +// 6915885164272192560ull -> 8498021699752483664ull +// 5313064440454632415ull -> 2237995378251169439ull +// 1419621243717341938ull -> 3400521619940694194ull +// 14643340977241322574ull -> 13553756526528636558ull +// 9459945304488478545ull -> 2109463370083716881ull +// 9109697759725517514ull -> 14168593821002454122ull +// 1219461677574489843ull -> 16341261596541620307ull +// 12645921053035414466ull -> 10041235417076058114ull +// 15833188279448154371ull -> 7909486330303743747ull +// 5649939024562436297ull -> 11640249140605321865ull +// 13071768051260827031ull -> 1871933448816404727ull +// 11568658891815584332ull -> 10857133212749050668ull +// 6290101024068816282ull -> 11914116902843745018ull +// 13548551240180606747ull -> 8338386234307664155ull +// 12026711874876808479ull -> 7693563294105136383ull +// 14878434897636020963ull -> 4393637264257635043ull +// 13070664192553016004ull -> 10817114663804369156ull +// 14329020100371110726ull -> 777552857201851974ull +// 1332651198786017705ull -> 4318860778166919945ull +// 4660979186818922927ull -> 3440765181079071375ull +// 7970649955584453902ull -> 18150517180267406702ull +// 12155241108605697947ull -> 3822860841058540379ull +// 321497671633783036ull -> 18408233930596919548ull +// 47846474967638397ull -> 15483250115370548957ull +// 1671638252455736222ull -> 12187081253452044158ull +// 7790931379237604703ull -> 15096704468692058143ull +// 7500491082226400733ull -> 351395931644162269ull +// 9959498186595129767ull -> 7681099562665395463ull +// 15231291829387910681ull -> 621673586916640985ull +// 7049317154435768499ull -> 1548670563434988819ull +// 4713622522116913420ull -> 15018421197641490508ull +// 13925586471965680337ull -> 14843102830690661585ull +// 13961077882107161481ull -> 18078373819427574057ull +// 9209982318968595522ull -> 2156517456339179906ull +// 16107922465231525099ull -> 1550063316561336971ull +// 12844042088199461132ull -> 682120972313074028ull +// 10673796068659129559ull -> 13222505441996051255ull +// 6539433182101695928ull -> 894762928574935768ull +// 5491285260911185746ull -> 6921956189824964594ull +// 176635766546517344ull -> 1470489317032329184ull +// 14211319399911923184ull -> 3757845431780800368ull +// 6477361675946931539ull -> 4291423602824827155ull +// 4971517538682628777ull -> 11888096106209469513ull +// 6645509333494969014ull -> 3227573574798329590ull +// 11898848437243843302ull -> 13089038313828989382ull +// 10339823206102932994ull -> 7508021719959037954ull +// 3298465695018491736ull -> 2308535366701241976ull +// 9864693173596772933ull -> 1246288425489315813ull +// 16605413220240271824ull -> 11433845872149110256ull +// 7808368893265143162ull -> 10295145752395079770ull +// 2237167686903514697ull -> 2890366903333661801ull +// 935658226810976854ull -> 9960659940131424438ull +// 4127037701450033065ull -> 14272369748982538569ull +// 18363414842122287163ull -> 17076452457032941051ull +// 13191419082357622418ull -> 3424791957006073618ull +// 15806858531294707365ull -> 1811248193508923525ull +// 4721485444630187392ull -> 14588001261256884000ull +// 3549592649840309629ull -> 13923059957978311005ull +// 12710830089997450586ull -> 16983695715066873498ull +// 12955855415227207952ull -> 608375160627328848ull +// 5725245848576944251ull -> 8684084180111108347ull +// 7500716494359964824ull -> 15679623628995063704ull +// 8302348919103896615ull -> 8464528085679884071ull +// 10799284346593205143ull -> 14932854744042668023ull +// 4249607111020853351ull -> 569982362832841031ull +// 6109395940973773015ull -> 7496415600731368183ull +// 6780056337592452791ull -> 6021406431901867511ull +// 1114155478699985615ull -> 14375074717677642639ull +// 9392050258964895061ull -> 15805523719787038613ull +// 3347981783688428817ull -> 12126947588968916209ull +// 6230424217801699337ull -> 11019915590207223465ull +// 2806910658457969846ull -> 12152802468277950710ull +// 8802450588385362367ull -> 9974141365018685887ull +// 2289779556170894642ull -> 7966238851102234546ull +// 634241390328092891ull -> 9747060662483912123ull +// 12809472706891653006ull -> 1717605098271755502ull +// 457816419735313420ull -> 9342227818610348684ull +// 12399146377539427774ull -> 2114030550145565950ull +// 17528325728839007813ull -> 418023209795270565ull +// 152442338603575994ull -> 15770927766697271034ull +// 11572652761878710989ull -> 12606063420003019757ull +// 4303976492072199477ull -> 1303915722987961013ull +// 15159036098880811789ull -> 11281002575808296045ull +// 7315581594832985476ull -> 5386973648178682980ull +// 5880827786329181412ull -> 7792848154392720004ull +// 309376478011650575ull -> 65916750986234223ull +// 7955431514292085542ull -> 13869421282421271494ull constexpr std::uint64_t kChains_rows[] = { - 18118773846703172955ull, 5721802392324366115ull, 10905164654339285863ull, - 5013225652888131384ull, 10468378600657706840ull, 4984150131456004790ull, - 149999620063792309ull, 13837273285924174060ull, 17253349212883400798ull, - 4968656904641771691ull, 12996647306096577911ull, 12050434476245562318ull, - 1949115689775911010ull, 236707637652846182ull, 12978903933461153109ull, - 8305476406764741103ull, 15440782951821405120ull, 849170925203470684ull, - 3041524397010971513ull, 16432853769520231056ull, 13722835947488180503ull, - 6797677290394154837ull, 5503293816192598557ull, 7380873684757359371ull, - 18384931392887404742ull, 17576550742654107858ull, 10034429714885644353ull, - 17685959618480428830ull, 14067861181115832148ull, 9826400445982679683ull, - 4865195924370756178ull, 18075243038383432276ull, 4533567546042864084ull, - 11925944213371644183ull, 16917928354769909352ull, 8952258716287445237ull, - 14076435456943559315ull, 3293983021922835245ull, 17563125179719782110ull, - 4792423586669271067ull, 4901113439335395035ull, 16082409071399819038ull, - 4311096251268871657ull, 1566309696546720234ull, 17726580741032482463ull, - 16176976223887152299ull, 13932002980086809234ull, 11101212487626629907ull, - 2116212219075360821ull, 17813414502748774299ull, 3267914628515888566ull, - 6356007132094407182ull, 4215131085225742667ull, 12626433458790281132ull, - 10587303005062577641ull, 7797918666513653329ull, 2466985235384963498ull, - 5061056481733569174ull, 12984321220918302464ull, 5452376178295345207ull, - 14498213029221461761ull, 522673331324384805ull, 16739608802754949521ull, - 874962087675576020ull, 1959313128068629851ull, 9731615380264829425ull, - 15498268764305233621ull, 291679968012038958ull, 7315837474539069482ull, - 1066471724404445284ull, 5602073364694883392ull, 16585610110763320757ull, - 13406768573395435523ull, 6915885164272192560ull, 5313064440454632415ull, - 1419621243717341938ull, 14643340977241322574ull, 9459945304488478545ull, - 9109697759725517514ull, 1219461677574489843ull, 12645921053035414466ull, - 15833188279448154371ull, 5649939024562436297ull, 13071768051260827031ull, - 11568658891815584332ull, 6290101024068816282ull, 13548551240180606747ull, - 12026711874876808479ull, 14878434897636020963ull, 13070664192553016004ull, - 14329020100371110726ull, 1332651198786017705ull, 4660979186818922927ull, - 7970649955584453902ull, 12155241108605697947ull, 321497671633783036ull, - 47846474967638397ull, 1671638252455736222ull, 7790931379237604703ull, - 7500491082226400733ull, 9959498186595129767ull, 15231291829387910681ull, - 7049317154435768499ull, 4713622522116913420ull, 13925586471965680337ull, - 13961077882107161481ull, 9209982318968595522ull, 16107922465231525099ull, - 12844042088199461132ull, 10673796068659129559ull, 6539433182101695928ull, - 5491285260911185746ull, 176635766546517344ull, 14211319399911923184ull, - 6477361675946931539ull, 4971517538682628777ull, 6645509333494969014ull, - 11898848437243843302ull, 10339823206102932994ull, 3298465695018491736ull, - 9864693173596772933ull, 16605413220240271824ull, 7808368893265143162ull, - 2237167686903514697ull, 935658226810976854ull, 4127037701450033065ull, - 18363414842122287163ull, 13191419082357622418ull, 15806858531294707365ull, - 4721485444630187392ull, 3549592649840309629ull, 12710830089997450586ull, - 12955855415227207952ull, 5725245848576944251ull, 7500716494359964824ull, - 8302348919103896615ull, 10799284346593205143ull, 4249607111020853351ull, - 6109395940973773015ull, 6780056337592452791ull, 1114155478699985615ull, - 9392050258964895061ull, 3347981783688428817ull, 6230424217801699337ull, - 2806910658457969846ull, 8802450588385362367ull, 2289779556170894642ull, - 634241390328092891ull, 12809472706891653006ull, 457816419735313420ull, - 12399146377539427774ull, 17528325728839007813ull, 152442338603575994ull, - 11572652761878710989ull, 4303976492072199477ull, 15159036098880811789ull, - 7315581594832985476ull, 5880827786329181412ull, 309376478011650575ull, - 7955431514292085542ull, + 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, }; // 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 @@ -2143,65 +2742,227 @@ constexpr std::uint64_t kChains_rows[] = { // 14893409322887798170ull -> 17398681471387928472ull // 7315663365262730666ull -> 4358702080325541764ull // 16480822453162985221ull -> 7087356504073994303ull +// expectation corrected (V19-FIX, kChains_reads, 160 of 160 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: +// 10994365209851312641ull -> 9570007664485953953ull +// 9171485715101441201ull -> 1562834205308840657ull +// 3851915246182749004ull -> 9925417414736888460ull +// 7650601343065671865ull -> 16797489091140150745ull +// 263316775310121531ull -> 8864771617498668987ull +// 7296510145726328670ull -> 10813620960703012830ull +// 9864225168885622691ull -> 9917769484038902883ull +// 895073665865934818ull -> 11399566133788975554ull +// 7145347660252596108ull -> 13065799061037800748ull +// 16486333425657542593ull -> 10473842100614025729ull +// 12643194660285950335ull -> 10356315431649319903ull +// 15244747761858210895ull -> 6718697686702398031ull +// 7457554648435358182ull -> 3085131610026641094ull +// 9837723569881115049ull -> 7920913286497161545ull +// 13095189290873306271ull -> 6965112488735434783ull +// 12875659237611994934ull -> 17824918572352876790ull +// 16953932876161061968ull -> 431137757590538768ull +// 6847341155969714930ull -> 5096278457151630514ull +// 18354141311418120448ull -> 16629172470762388288ull +// 15313203108460696833ull -> 11652117991390508417ull +// 3503760402046585664ull -> 2618729299425190784ull +// 12921606448138572817ull -> 212332156515024145ull +// 6250603969607620355ull -> 12246593551859292419ull +// 2447345348598126306ull -> 12545038087661601474ull +// 8782947477736227136ull -> 1460766259544383584ull +// 5907304967191693611ull -> 10861778317578249995ull +// 14152120017029678225ull -> 20932238954851345ull +// 4327958575338663904ull -> 18286529377979065696ull +// 1038143747656349093ull -> 5666326454301174821ull +// 17505782017925099478ull -> 6228635006760178710ull +// 4676781042483750172ull -> 9906791842642905532ull +// 10873764052061071005ull -> 2721687798792728445ull +// 14429131150373654552ull -> 16653381899846261400ull +// 7376377076551705364ull -> 16503363491014194836ull +// 8239111211340423856ull -> 5987209830733009584ull +// 14699351684973995873ull -> 11879508822085901633ull +// 718896306812707859ull -> 4428380683177019699ull +// 9478533484473513605ull -> 6702580677339138469ull +// 18322959341746452490ull -> 12470873918402201930ull +// 11214826924363137265ull -> 7755601604835325521ull +// 6945847207278585058ull -> 17100550686359839522ull +// 2890410016268205221ull -> 9982417146672614117ull +// 13194699958822839553ull -> 16219053929893216833ull +// 17547780154330793556ull -> 8335500495109512692ull +// 9642656522784665864ull -> 17499407126158105512ull +// 10013306476471471237ull -> 8520742716124176357ull +// 4549846520202049346ull -> 6969237881725548098ull +// 2856226591911740140ull -> 6167898056600340972ull +// 18099590536457223418ull -> 10796080984723706778ull +// 10667203245975437537ull -> 2659129401831175873ull +// 13025421663814015357ull -> 4113136533365713821ull +// 3194242923218047188ull -> 16959859587150714548ull +// 15492978369958790341ull -> 11977596023171988133ull +// 12071216712939505134ull -> 3997121899712306414ull +// 8980507376974384773ull -> 302851387997930277ull +// 4012167150452338076ull -> 7004784366449869436ull +// 17118852714703308524ull -> 17731084291599622060ull +// 5338420945723863200ull -> 2201184572610804096ull +// 7317581721729870626ull -> 15700916709431740066ull +// 2005671846936550327ull -> 4158286781895941687ull +// 4466106088477031227ull -> 5992524740679929691ull +// 15342451359488060213ull -> 1449426275268650261ull +// 8064729482873412913ull -> 18162824504997853425ull +// 5180809561895156134ull -> 14899520499454266758ull +// 8396488811520125016ull -> 13393817499128108248ull +// 7042761172915969538ull -> 10711743217016099970ull +// 2295772549721001659ull -> 2691444242389935931ull +// 16596397512462594399ull -> 8294060884410393919ull +// 3733617700714248424ull -> 12352805812450672776ull +// 17897127833416813440ull -> 696110624400704576ull +// 13804806771253600000ull -> 10843628086540515552ull +// 205483701717920681ull -> 8971856522667982633ull +// 11046288491755971110ull -> 11210433033614009510ull +// 6028257700892537112ull -> 8384649526502906040ull +// 6966472733455287549ull -> 2393883974530973309ull +// 7575505086338584214ull -> 4665626305905607638ull +// 3479005093165436523ull -> 8867654732330596907ull +// 7533762921086436365ull -> 4416041359039881293ull +// 9070106458428696798ull -> 13719154741412155838ull +// 4246322818261661580ull -> 7098510039070530028ull +// 4135619669160501215ull -> 10831403092179419039ull +// 16501866430644628462ull -> 8578164481500217838ull +// 3762261688633661663ull -> 14156042948797799583ull +// 9611061831317637907ull -> 18079113731505369907ull +// 9247630629825510880ull -> 11217579006112077760ull +// 2474233007650053267ull -> 578274504487938291ull +// 18068433488437395240ull -> 12858268482564452648ull +// 1687887581045174187ull -> 13457093904989238219ull +// 18019906547243126183ull -> 7535108913864740263ull +// 15913429127556208585ull -> 4941944547036948489ull +// 10248484429653866187ull -> 15143761260194159051ull +// 15777644472000745484ull -> 3169992748722096748ull +// 17886504052910168414ull -> 2935645212104261694ull +// 11093299563553764549ull -> 1503156579032907045ull +// 2760751264822464183ull -> 12825108615254555255ull +// 11512152247032069127ull -> 11152144432285654023ull +// 5864587936366665442ull -> 14374425704408319362ull +// 9513015950610148722ull -> 13181125330469706258ull +// 14751705142316976023ull -> 8173826051536434519ull +// 10595093074965783954ull -> 3445997924383545490ull +// 5794892224269635875ull -> 6587016227990886659ull +// 12296330589453345021ull -> 4779228536467194685ull +// 18119724232866615727ull -> 13151647749864074127ull +// 16917016109709286484ull -> 9173722582032837076ull +// 13516597005930623455ull -> 14434113364655604703ull +// 18048785039199604146ull -> 749323963540978962ull +// 5908467659220954270ull -> 9886586197280394206ull +// 12220513153422045244ull -> 17593161773407266268ull +// 1892919283593332554ull -> 16651394056260196842ull +// 9744910659931011408ull -> 5523306718191251504ull +// 2841623837892969156ull -> 12137483147531194660ull +// 4448698567008606970ull -> 1385603333851191898ull +// 10044355025900478161ull -> 4496586139598675537ull +// 6619067607883940224ull -> 8942420997722419200ull +// 14740790554481155649ull -> 16186100995544900609ull +// 6149624683079787649ull -> 10571664598865577953ull +// 17678431304186603579ull -> 9783208691725612411ull +// 4811419915667451106ull -> 17727619977969867138ull +// 1410348094137693562ull -> 12308607250191287290ull +// 5514023027088859418ull -> 13136936254003785978ull +// 7367757565996126146ull -> 4799706175857105538ull +// 5698019932431966478ull -> 15342824296389601806ull +// 17079056151355087834ull -> 10278727004657844890ull +// 3727006442705397750ull -> 40192321974480982ull +// 12013301242958972251ull -> 9783890578221359931ull +// 2652953654492532566ull -> 11215532213811366070ull +// 9500399560654130107ull -> 14480169342549825019ull +// 3670339869733234350ull -> 15414792187977659182ull +// 13513071197642988332ull -> 8493082597081956684ull +// 8393059641656542330ull -> 8564012831232873370ull +// 13735686237112802831ull -> 5491576330870414255ull +// 7780054649019222100ull -> 14022487705176382100ull +// 2357350844911843400ull -> 4124805857279107720ull +// 7460876853287449991ull -> 15467889468829316615ull +// 17563334255403092843ull -> 7295497316328640107ull +// 14919823440853378679ull -> 15082002607429366135ull +// 17556563294294675481ull -> 12499158975010305465ull +// 10739316886314519973ull -> 3208794190730501637ull +// 11360889351113912365ull -> 15064216799612457293ull +// 1654806705693835396ull -> 16974691537614575812ull +// 11985027333077447084ull -> 1426687806059238508ull +// 1151851592645907797ull -> 17375680129214845717ull +// 2937733331601919226ull -> 12487972796056251866ull +// 7897932018066236343ull -> 8080964710494461719ull +// 14456634454921700953ull -> 1415692574356635417ull +// 8600437238609088843ull -> 9772128015242412363ull +// 1167806001739593055ull -> 18331800931612105183ull +// 3406629553580275188ull -> 15912555856562133268ull +// 10158177221441065900ull -> 16674262793511329036ull +// 4456358513359663836ull -> 12709672117837958972ull +// 780105729075268909ull -> 533937075939181997ull +// 12682245921451973090ull -> 13997036310862179778ull +// 9133142210918405194ull -> 2303353295064390890ull +// 543053342179958753ull -> 13136856999282398145ull +// 14435405334762127255ull -> 9661353626625839895ull +// 14005783244877794299ull -> 7809505236198215515ull +// 10945573428159210840ull -> 9689360779147915768ull +// 17398681471387928472ull -> 7546140302872708152ull +// 4358702080325541764ull -> 11057158687385367268ull +// 7087356504073994303ull -> 930681743930080351ull constexpr std::uint64_t kChains_reads[] = { - 10994365209851312641ull, 9171485715101441201ull, 3851915246182749004ull, - 7650601343065671865ull, 263316775310121531ull, 7296510145726328670ull, - 9864225168885622691ull, 895073665865934818ull, 7145347660252596108ull, - 16486333425657542593ull, 12643194660285950335ull, 15244747761858210895ull, - 7457554648435358182ull, 9837723569881115049ull, 13095189290873306271ull, - 12875659237611994934ull, 16953932876161061968ull, 6847341155969714930ull, - 18354141311418120448ull, 15313203108460696833ull, 3503760402046585664ull, - 12921606448138572817ull, 6250603969607620355ull, 2447345348598126306ull, - 8782947477736227136ull, 5907304967191693611ull, 14152120017029678225ull, - 4327958575338663904ull, 1038143747656349093ull, 17505782017925099478ull, - 4676781042483750172ull, 10873764052061071005ull, 14429131150373654552ull, - 7376377076551705364ull, 8239111211340423856ull, 14699351684973995873ull, - 718896306812707859ull, 9478533484473513605ull, 18322959341746452490ull, - 11214826924363137265ull, 6945847207278585058ull, 2890410016268205221ull, - 13194699958822839553ull, 17547780154330793556ull, 9642656522784665864ull, - 10013306476471471237ull, 4549846520202049346ull, 2856226591911740140ull, - 18099590536457223418ull, 10667203245975437537ull, 13025421663814015357ull, - 3194242923218047188ull, 15492978369958790341ull, 12071216712939505134ull, - 8980507376974384773ull, 4012167150452338076ull, 17118852714703308524ull, - 5338420945723863200ull, 7317581721729870626ull, 2005671846936550327ull, - 4466106088477031227ull, 15342451359488060213ull, 8064729482873412913ull, - 5180809561895156134ull, 8396488811520125016ull, 7042761172915969538ull, - 2295772549721001659ull, 16596397512462594399ull, 3733617700714248424ull, - 17897127833416813440ull, 13804806771253600000ull, 205483701717920681ull, - 11046288491755971110ull, 6028257700892537112ull, 6966472733455287549ull, - 7575505086338584214ull, 3479005093165436523ull, 7533762921086436365ull, - 9070106458428696798ull, 4246322818261661580ull, 4135619669160501215ull, - 16501866430644628462ull, 3762261688633661663ull, 9611061831317637907ull, - 9247630629825510880ull, 2474233007650053267ull, 18068433488437395240ull, - 1687887581045174187ull, 18019906547243126183ull, 15913429127556208585ull, - 10248484429653866187ull, 15777644472000745484ull, 17886504052910168414ull, - 11093299563553764549ull, 2760751264822464183ull, 11512152247032069127ull, - 5864587936366665442ull, 9513015950610148722ull, 14751705142316976023ull, - 10595093074965783954ull, 5794892224269635875ull, 12296330589453345021ull, - 18119724232866615727ull, 16917016109709286484ull, 13516597005930623455ull, - 18048785039199604146ull, 5908467659220954270ull, 12220513153422045244ull, - 1892919283593332554ull, 9744910659931011408ull, 2841623837892969156ull, - 4448698567008606970ull, 10044355025900478161ull, 6619067607883940224ull, - 14740790554481155649ull, 6149624683079787649ull, 17678431304186603579ull, - 4811419915667451106ull, 1410348094137693562ull, 5514023027088859418ull, - 7367757565996126146ull, 5698019932431966478ull, 17079056151355087834ull, - 3727006442705397750ull, 12013301242958972251ull, 2652953654492532566ull, - 9500399560654130107ull, 3670339869733234350ull, 13513071197642988332ull, - 8393059641656542330ull, 13735686237112802831ull, 7780054649019222100ull, - 2357350844911843400ull, 7460876853287449991ull, 17563334255403092843ull, - 14919823440853378679ull, 17556563294294675481ull, 10739316886314519973ull, - 11360889351113912365ull, 1654806705693835396ull, 11985027333077447084ull, - 1151851592645907797ull, 2937733331601919226ull, 7897932018066236343ull, - 14456634454921700953ull, 8600437238609088843ull, 1167806001739593055ull, - 3406629553580275188ull, 10158177221441065900ull, 4456358513359663836ull, - 780105729075268909ull, 12682245921451973090ull, 9133142210918405194ull, - 543053342179958753ull, 14435405334762127255ull, 14005783244877794299ull, - 10945573428159210840ull, 17398681471387928472ull, 4358702080325541764ull, - 7087356504073994303ull, + 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, }; // 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). -constexpr std::uint64_t kChains_final = 7955431514292085542ull; +// 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; constexpr Trade kChains_trades[] = { {1736121720000LL, 1736122260000LL, 102.75, 111, 1, 0}, {1736122680000LL, 1736123220000LL, 113.75, 118, 1, 0}, @@ -2405,39 +3166,136 @@ constexpr Trade kChains_trades[] = { // 2112333233909935153ull -> 11285362832740584634ull // 11487163724744671195ull -> 1235712972626989438ull // 1252183101027139840ull -> 7666575660803139734ull +// expectation corrected (V19-FIX, kMargin_rows, 96 of 96 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: +// 10391482925526448806ull -> 9541482286738450374ull +// 15595848579680573481ull -> 10604897191185230729ull +// 6349339140023370947ull -> 10430957227712956259ull +// 2445966623322950236ull -> 17038118833282111132ull +// 15396553341350904753ull -> 6880506736019270193ull +// 3269843801636275260ull -> 2574896616226754364ull +// 4333436585203871266ull -> 14467806575653580994ull +// 17731648912738156023ull -> 13935231533587438999ull +// 11124060594953235723ull -> 1848048834540881643ull +// 8248693236318619849ull -> 18329579972079141513ull +// 9785564647014518543ull -> 12355688239988236079ull +// 4339738185546695351ull -> 10991245773765040855ull +// 1508047838628401244ull -> 5685214256158276764ull +// 9575924538342165974ull -> 17160692497390069334ull +// 12057042300463699945ull -> 4009833773659380105ull +// 16384331053786292474ull -> 13777884366772416026ull +// 2586713898351333107ull -> 8958056086241362995ull +// 3036993465457955600ull -> 12207636356642772400ull +// 10141780918348163900ull -> 3459076114230420156ull +// 1217568232840056040ull -> 14921426272357766472ull +// 5398614394057038478ull -> 2345813046963814958ull +// 11847954351065032106ull -> 285001323442461930ull +// 11497942490735416147ull -> 16917342746817816499ull +// 7490321406664226895ull -> 18052258256818146447ull +// 11643565515365771872ull -> 4781946747460841696ull +// 3134741681720951596ull -> 8888939313581979724ull +// 14390526037225228565ull -> 17314806764712940533ull +// 11035056772969385318ull -> 4508121597259689478ull +// 7106018614043445025ull -> 11060315425557244321ull +// 7531084140349706487ull -> 14574236380430121079ull +// 17510361031833182962ull -> 12392320350472046194ull +// 9476720764674435903ull -> 12250313226238695007ull +// 12792417114503468842ull -> 11116092899196726058ull +// 8493907912804594178ull -> 3440822300002062498ull +// 12184654815078845415ull -> 6172478543095956935ull +// 16819691608252987693ull -> 11864943854655903053ull +// 5847969150045365989ull -> 7350714231396417061ull +// 8570582354795532253ull -> 18236477525793941949ull +// 9520156983820025091ull -> 18289535579996812515ull +// 15309747880262381445ull -> 15597457755908201189ull +// 10208086176547949743ull -> 11910235219191833807ull +// 16381105109090382155ull -> 9451964279490960683ull +// 9026080853435626990ull -> 4710825118275752110ull +// 17761744552072441510ull -> 12851808306320095878ull +// 7949108536321142675ull -> 8820052433058937619ull +// 1944298717910085558ull -> 15533164234013109494ull +// 15610576016807331239ull -> 16547449805254710983ull +// 6676202736522594579ull -> 2596701226294842163ull +// 10733118415350041200ull -> 17704360989892006448ull +// 16789219928108064100ull -> 5312178085029999460ull +// 4933122624075625098ull -> 681706230469294026ull +// 12429227959950642556ull -> 11208354125027713564ull +// 7668464129625382061ull -> 13848046050944922829ull +// 17552900934506910740ull -> 16629321510727683252ull +// 9862047928658527616ull -> 489654745066395968ull +// 11012462170135633811ull -> 17259859321488242835ull +// 6149744915740424944ull -> 15549072595885790096ull +// 12879371966248782675ull -> 1505011103482109555ull +// 2626896410226084347ull -> 15289097285747984347ull +// 6882458144581711602ull -> 1572915281639359538ull +// 4751290656575815224ull -> 11273305892586396472ull +// 12439674314920856025ull -> 13328838480036624025ull +// 15384210514960608763ull -> 13633704569586434331ull +// 9500903566061480884ull -> 17431948770960738100ull +// 12579607369947078196ull -> 12155616429848711764ull +// 10501142041142713822ull -> 14869620479028624382ull +// 4661627703089338053ull -> 6849688694600040229ull +// 2480358993685433042ull -> 11485893754569323922ull +// 541217539005727471ull -> 8360410264576159919ull +// 10813096452768688587ull -> 461812385935867019ull +// 341302038568912043ull -> 2682358547448593291ull +// 2456429645035657850ull -> 9291455346649087450ull +// 3671675816582694427ull -> 13216043467298716443ull +// 8834685852269157635ull -> 359821378104950531ull +// 164315810147764124ull -> 16017291574242025244ull +// 8750384636650045294ull -> 18391126290698081198ull +// 1470521484059601186ull -> 9855977842531731938ull +// 3396398907832766327ull -> 5556828515909880503ull +// 10003792016596952192ull -> 2022314578154758976ull +// 17981508671091845229ull -> 9416710584143900557ull +// 6198119627835915217ull -> 6983558299012049585ull +// 3298971065180151700ull -> 5499696029215209460ull +// 12453502159385225964ull -> 10875028956400699532ull +// 11876896775434160835ull -> 2974831859594208707ull +// 3756558076518208243ull -> 9582163290358494515ull +// 8942079368245153625ull -> 9192322421103943129ull +// 35581439849211377ull -> 11542997064059872401ull +// 5146077685101920520ull -> 1957400908434967464ull +// 8606650768378373015ull -> 12038816259661624215ull +// 5571186892689896224ull -> 14854368408513559808ull +// 6223779691587819680ull -> 4246614380518911616ull +// 382669786521825489ull -> 2355707666069181745ull +// 13692985352971257544ull -> 5599822345970336136ull +// 11285362832740584634ull -> 2471724750358094554ull +// 1235712972626989438ull -> 11160534888434597406ull +// 7666575660803139734ull -> 10114708526056156918ull constexpr std::uint64_t kMargin_rows[] = { - 10391482925526448806ull, 15595848579680573481ull, 6349339140023370947ull, - 2445966623322950236ull, 15396553341350904753ull, 3269843801636275260ull, - 4333436585203871266ull, 17731648912738156023ull, 11124060594953235723ull, - 8248693236318619849ull, 9785564647014518543ull, 4339738185546695351ull, - 1508047838628401244ull, 9575924538342165974ull, 12057042300463699945ull, - 16384331053786292474ull, 2586713898351333107ull, 3036993465457955600ull, - 10141780918348163900ull, 1217568232840056040ull, 5398614394057038478ull, - 11847954351065032106ull, 11497942490735416147ull, 7490321406664226895ull, - 11643565515365771872ull, 3134741681720951596ull, 14390526037225228565ull, - 11035056772969385318ull, 7106018614043445025ull, 7531084140349706487ull, - 17510361031833182962ull, 9476720764674435903ull, 12792417114503468842ull, - 8493907912804594178ull, 12184654815078845415ull, 16819691608252987693ull, - 5847969150045365989ull, 8570582354795532253ull, 9520156983820025091ull, - 15309747880262381445ull, 10208086176547949743ull, 16381105109090382155ull, - 9026080853435626990ull, 17761744552072441510ull, 7949108536321142675ull, - 1944298717910085558ull, 15610576016807331239ull, 6676202736522594579ull, - 10733118415350041200ull, 16789219928108064100ull, 4933122624075625098ull, - 12429227959950642556ull, 7668464129625382061ull, 17552900934506910740ull, - 9862047928658527616ull, 11012462170135633811ull, 6149744915740424944ull, - 12879371966248782675ull, 2626896410226084347ull, 6882458144581711602ull, - 4751290656575815224ull, 12439674314920856025ull, 15384210514960608763ull, - 9500903566061480884ull, 12579607369947078196ull, 10501142041142713822ull, - 4661627703089338053ull, 2480358993685433042ull, 541217539005727471ull, - 10813096452768688587ull, 341302038568912043ull, 2456429645035657850ull, - 3671675816582694427ull, 8834685852269157635ull, 164315810147764124ull, - 8750384636650045294ull, 1470521484059601186ull, 3396398907832766327ull, - 10003792016596952192ull, 17981508671091845229ull, 6198119627835915217ull, - 3298971065180151700ull, 12453502159385225964ull, 11876896775434160835ull, - 3756558076518208243ull, 8942079368245153625ull, 35581439849211377ull, - 5146077685101920520ull, 8606650768378373015ull, 5571186892689896224ull, - 6223779691587819680ull, 382669786521825489ull, 13692985352971257544ull, - 11285362832740584634ull, 1235712972626989438ull, 7666575660803139734ull, + 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, }; // 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 @@ -2633,43 +3491,141 @@ constexpr std::uint64_t kMargin_rows[] = { // 11785322440343757366ull -> 2750403077460828887ull // 13605241102969921577ull -> 2808017024047588845ull // 5203288488010852098ull -> 8904074757957821441ull +// expectation corrected (V19-FIX, kMargin_reads, 96 of 96 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: +// 4035985399428692187ull -> 11431893337342736571ull +// 1232006444449075935ull -> 10771769035459469695ull +// 5435309673424081711ull -> 1401088416158264207ull +// 1766476708570521729ull -> 5990666036994939969ull +// 3655303844318042077ull -> 10588539850905902429ull +// 12922028321601496098ull -> 9641395323288135458ull +// 8709560236833488466ull -> 16415676128453702962ull +// 4118837385245375835ull -> 3214841672955981307ull +// 17400074083990051802ull -> 9140481207117864186ull +// 3178414084162580543ull -> 6758874244345350527ull +// 6371817265648974287ull -> 7448761481640431279ull +// 1998787443020742945ull -> 11899269473854959745ull +// 2209967451210305061ull -> 17623756564591150437ull +// 13757160630871344766ull -> 3283115927808803070ull +// 17622777025425038873ull -> 18313686748628355321ull +// 3760825361580050108ull -> 10517443878246423516ull +// 381055648202838443ull -> 14391554715371743723ull +// 4768082096704091334ull -> 7815903094041248998ull +// 2705901981232375764ull -> 11128615458197398484ull +// 18177804552095955890ull -> 16868017582914550674ull +// 4039442215909809281ull -> 17318292280187111457ull +// 3223165367611640158ull -> 236539664791149342ull +// 6873540798137376913ull -> 12561323379654259441ull +// 311799305987407926ull -> 17901584105989623670ull +// 17868270745902922626ull -> 1819495894130837698ull +// 13824798500412113300ull -> 12913364709271178836ull +// 5416464695796100146ull -> 4003529127479307250ull +// 1483949399042852678ull -> 17850722142819534182ull +// 10775445312521056249ull -> 10755120431723778073ull +// 11971111075379177656ull -> 1731312764757835608ull +// 17451170437642507755ull -> 13748410692460811819ull +// 5398170428680916776ull -> 12896133474356421192ull +// 3788440496136635316ull -> 9031531055096256852ull +// 2722366354813550527ull -> 8322755136826696671ull +// 15475526787278984737ull -> 8233453449656235425ull +// 9349230546277747223ull -> 18274892380474204343ull +// 15346531142013654005ull -> 9098889810842207669ull +// 6710240904065794709ull -> 11981003970545376949ull +// 1932519926389311039ull -> 5208192149483445791ull +// 18124466075963182723ull -> 13826614122871749667ull +// 309561345762469727ull -> 16775818024782332415ull +// 6226425728073939724ull -> 15803353690065673036ull +// 9677602238862594794ull -> 8607451910366293802ull +// 13797705951430273830ull -> 15510545365186449286ull +// 16950033600727185290ull -> 5406260086936373546ull +// 202238422461245923ull -> 2510786366313163875ull +// 8213602081543249531ull -> 15538776584588985979ull +// 1895798527986568543ull -> 12229521714344207135ull +// 10481985752198546366ull -> 2212599425252585470ull +// 16200306185762164111ull -> 10390126991709196687ull +// 16356792500887837464ull -> 13200005853939750424ull +// 12250903106604498853ull -> 6847492913241704581ull +// 13096941503135141455ull -> 15796662482477301615ull +// 17672997587265903886ull -> 5118863315631314158ull +// 16487525810830595285ull -> 4129944798799153557ull +// 5071996736690127076ull -> 11319393888042736100ull +// 1237453235050027683ull -> 14187452542143074307ull +// 16962173224800952016ull -> 1074727836785604464ull +// 2402525651872222099ull -> 17195817214176484787ull +// 12147984833355334129ull -> 1825647699469508785ull +// 16193008267625149869ull -> 4268279429926179501ull +// 3561610851409822198ull -> 16957209788610236342ull +// 13638897187419866363ull -> 3395896204678737755ull +// 16930929057378288947ull -> 3101221902009845683ull +// 4563268599662803189ull -> 14178655193129027029ull +// 16305199438269264788ull -> 11497559614257964788ull +// 6686075444759067991ull -> 2301625628626628663ull +// 13273246750374706050ull -> 16134456750164415938ull +// 16946874685843410256ull -> 7927341186129459600ull +// 1057885952390065930ull -> 11762885871454101194ull +// 3403612441812454219ull -> 3662730897532267179ull +// 6797355776142692664ull -> 5485569566800147480ull +// 1539125512225642215ull -> 11083493162941664231ull +// 13028005556777737981ull -> 4553141082613530877ull +// 686331008537965628ull -> 9461970734864272828ull +// 1290340063780633646ull -> 14341766711532273518ull +// 13495774484286298031ull -> 11087165755233401455ull +// 288331098246094444ull -> 5001440655061293740ull +// 6292276834078525821ull -> 15047261415908188989ull +// 4565471599277523508ull -> 12156664521037383764ull +// 13702081127331537645ull -> 8898477763532118925ull +// 3276749151491562950ull -> 17667654592610722150ull +// 3267368365332615306ull -> 6706429179434019306ull +// 15193593149195204102ull -> 1344134952986164358ull +// 16739332046903895833ull -> 7134564545056141913ull +// 13830310554763200569ull -> 755502931104769977ull +// 7817256702940604037ull -> 18177522707802576037ull +// 5125251883652564917ull -> 1635613291101838741ull +// 5897162925629512641ull -> 15997917115278936545ull +// 11237170178269194824ull -> 17554126746769759592ull +// 14852404787740883657ull -> 3806995878355289449ull +// 3617046065695943782ull -> 15405908675426402054ull +// 7637897372680702849ull -> 1441245154396427777ull +// 2750403077460828887ull -> 14397269933515551863ull +// 2808017024047588845ull -> 12219341892567703629ull +// 8904074757957821441ull -> 2752726173101561857ull constexpr std::uint64_t kMargin_reads[] = { - 4035985399428692187ull, 1232006444449075935ull, 5435309673424081711ull, - 1766476708570521729ull, 3655303844318042077ull, 12922028321601496098ull, - 8709560236833488466ull, 4118837385245375835ull, 17400074083990051802ull, - 3178414084162580543ull, 6371817265648974287ull, 1998787443020742945ull, - 2209967451210305061ull, 13757160630871344766ull, 17622777025425038873ull, - 3760825361580050108ull, 381055648202838443ull, 4768082096704091334ull, - 2705901981232375764ull, 18177804552095955890ull, 4039442215909809281ull, - 3223165367611640158ull, 6873540798137376913ull, 311799305987407926ull, - 17868270745902922626ull, 13824798500412113300ull, 5416464695796100146ull, - 1483949399042852678ull, 10775445312521056249ull, 11971111075379177656ull, - 17451170437642507755ull, 5398170428680916776ull, 3788440496136635316ull, - 2722366354813550527ull, 15475526787278984737ull, 9349230546277747223ull, - 15346531142013654005ull, 6710240904065794709ull, 1932519926389311039ull, - 18124466075963182723ull, 309561345762469727ull, 6226425728073939724ull, - 9677602238862594794ull, 13797705951430273830ull, 16950033600727185290ull, - 202238422461245923ull, 8213602081543249531ull, 1895798527986568543ull, - 10481985752198546366ull, 16200306185762164111ull, 16356792500887837464ull, - 12250903106604498853ull, 13096941503135141455ull, 17672997587265903886ull, - 16487525810830595285ull, 5071996736690127076ull, 1237453235050027683ull, - 16962173224800952016ull, 2402525651872222099ull, 12147984833355334129ull, - 16193008267625149869ull, 3561610851409822198ull, 13638897187419866363ull, - 16930929057378288947ull, 4563268599662803189ull, 16305199438269264788ull, - 6686075444759067991ull, 13273246750374706050ull, 16946874685843410256ull, - 1057885952390065930ull, 3403612441812454219ull, 6797355776142692664ull, - 1539125512225642215ull, 13028005556777737981ull, 686331008537965628ull, - 1290340063780633646ull, 13495774484286298031ull, 288331098246094444ull, - 6292276834078525821ull, 4565471599277523508ull, 13702081127331537645ull, - 3276749151491562950ull, 3267368365332615306ull, 15193593149195204102ull, - 16739332046903895833ull, 13830310554763200569ull, 7817256702940604037ull, - 5125251883652564917ull, 5897162925629512641ull, 11237170178269194824ull, - 14852404787740883657ull, 3617046065695943782ull, 7637897372680702849ull, - 2750403077460828887ull, 2808017024047588845ull, 8904074757957821441ull, + 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, }; // 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). -constexpr std::uint64_t kMargin_final = 7666575660803139734ull; +// 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; 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_margin_revival_cancel.cpp b/tests/test_adapter_margin_revival_cancel.cpp index 408a8517..0b10b83a 100644 --- a/tests/test_adapter_margin_revival_cancel.cpp +++ b/tests/test_adapter_margin_revival_cancel.cpp @@ -26,6 +26,47 @@ * second "Margin call" 92 @10.57 on 05-06 10:00, and 836 still open when the * chart ends -- the cancelled X never comes back. Both runs below replay the * same script on the same bars (the fixture) and must book exactly the tape. + * + * R5 lane V19-FIX (audit X2): the adapter still revived an exit the script + * cancelled while it was LIVE. strategy.cancel retired the lifecycle of a + * dormant exit only, so once the declined pair's suspension made the + * cancelled leg dormant on 05-01, the margin call read it as a standing exit + * and closed 928 through it. Two more lab tv tapes on the same chart and + * window decide it, each the cancel tape's script with the cancel moved: + * - v19fix-cancel-before-pair: strategy.cancel("X") on 04-30 15:45, before + * the pair (tv_trades sha256 662ce8c8..., pine 1d6c305b..., range proof + * covered); + * - v19fix-cancel-at-1000: strategy.cancel("X") on 04-30 10:00, the bar + * after the short fills (tv_trades sha256 662ce8c8..., pine 13ed512b..., + * range proof covered). + * Both book exactly the cancel tape (the same tv_trades.csv bytes): margin + * calls 12 @10.39 and 92 @10.57, 836 open. The other spellings of the audit's + * variants follow the same rule and book the same trades: strategy.cancel_all + * on the cancel tape's bar and before the pair, and strategy.cancel after the + * pair on its bar. The cancel now retires the lifecycle of every exit it takes + * off the book as well as every dormant one it names. + * + * The adapter has a second road back. When the 05-01 open gaps through X's + * stop, the declined reversal takes X off the book at the pair's command and + * parks it for the next margin-call slice (apply_reversal_gap_bracket_policy, + * pending_margin_revivals_). Three more tapes with X's stop at 10.15, which + * the 05-01 09:30 open (10.15) gaps through: + * - v19fix-gapped-stop-control: no cancel -- "Margin call" 12 @10.39 and X + * 928 @10.39 on 05-02 (tv_trades sha256 bdf3d470...); + * - v19fix-gapped-stop-cancel: strategy.cancel("X") on 05-01 09:30; + * - v19fix-gapped-stop-cancel-after-pair: strategy.cancel("X") on 04-30 + * 15:45 after the pair, in the callback that parked X; + * both cancels book the cancel tape's file again (662ce8c8...). A cancel now + * drops the parked revival and withdraws every exit row its id names. + * + * A chart with no quantity grid moves the pair's hold to the command itself + * (strategy.close's all-in reversal pair holds the standing bracket at once), + * so there the hold and the cancel share one callback: before the lane a + * cancel after the pair was refused by the lifecycle as a conflicting replay + * of the hold's step, and one before it was overwritten by the hold. No tape + * covers such a chart; the rows below hold the tapes' rule on it -- every + * cancel spelling books what the cancel on the dormant exit books there, and + * the same chart without a cancel still closes through X. */ #include @@ -81,14 +122,40 @@ bool at(const Clock& c, int month, int day, int hour, int minute) { return c.month == month && c.day == day && c.hour == hour && c.minute == minute; } +// Where the script cancels X, and how. +enum class Cancel { + None, // tape badapter-v19dp1-control + Dormant, // tape badapter-v19dp1-cancel: strategy.cancel("X"), 05-01 09:30 + DormantAll, // strategy.cancel_all(), 05-01 09:30 + BeforePair, // tape v19fix-cancel-before-pair: strategy.cancel("X"), 04-30 15:45 + AfterPair, // strategy.cancel("X"), 04-30 15:45, after the pair + AllBeforePair, // strategy.cancel_all(), 04-30 15:45, before the pair + Earlier, // tape v19fix-cancel-at-1000: strategy.cancel("X"), 04-30 10:00 +}; + +const char* spelling(Cancel cancel) { + switch (cancel) { + case Cancel::None: return "no cancel"; + case Cancel::Dormant: return "strategy.cancel on 05-01 09:30 (X dormant)"; + case Cancel::DormantAll: return "strategy.cancel_all on 05-01 09:30 (X dormant)"; + case Cancel::BeforePair: return "strategy.cancel on 04-30 15:45 before the pair (X live)"; + case Cancel::AfterPair: return "strategy.cancel on 04-30 15:45 after the pair"; + case Cancel::AllBeforePair: return "strategy.cancel_all on 04-30 15:45 before the pair"; + case Cancel::Earlier: return "strategy.cancel on 04-30 10:00 (X live)"; + } + return "?"; +} + // The tapes' script: 10 000 of capital, percent-of-equity 100 by default, -// margin 100 both ways, no fee, a one-share grid (NYSE:F's lot). +// margin 100 both ways, no fee, a one-share grid (NYSE:F's lot) unless +// `grid` is false. class Host : public pineforge::source::PineStrategyHost { public: - explicit Host(bool cancel) : cancel_(cancel) { + explicit Host(Cancel cancel, bool grid = true, double stop = 10.25) + : cancel_(cancel), stop_(stop) { initial_capital_ = 10000.0; syminfo_mintick_ = 0.01; - qty_step_ = 1.0; + qty_step_ = grid ? 1.0 : 0.0; default_qty_type_ = QtyType::PERCENT_OF_EQUITY; default_qty_value_ = 100.0; commission_type_ = CommissionType::PERCENT; @@ -104,17 +171,23 @@ class Host : public pineforge::source::PineStrategyHost { const Clock c = new_york(bar.timestamp); if (at(c, 4, 29, 15, 45)) { strategy_entry("S", false, kNaN, kNaN, 940.0); - strategy_exit("X", "S", kNaN, 10.25); + strategy_exit("X", "S", kNaN, stop_); } + if (cancel_ == Cancel::Earlier && at(c, 4, 30, 10, 0)) strategy_cancel("X"); if (at(c, 4, 30, 15, 45)) { + if (cancel_ == Cancel::BeforePair) strategy_cancel("X"); + if (cancel_ == Cancel::AllBeforePair) strategy_cancel_all(); strategy_entry("L", true); strategy_close("S", "Reverse to Long"); + if (cancel_ == Cancel::AfterPair) strategy_cancel("X"); } - if (cancel_ && at(c, 5, 1, 9, 30)) strategy_cancel("X"); + if (cancel_ == Cancel::Dormant && at(c, 5, 1, 9, 30)) strategy_cancel("X"); + if (cancel_ == Cancel::DormantAll && at(c, 5, 1, 9, 30)) strategy_cancel_all(); } using BacktestEngine::range_end_trades_; private: - bool cancel_; + Cancel cancel_; + double stop_; }; std::vector bars() { @@ -166,7 +239,7 @@ void check_rows(const Host& host, const std::vector& tape) { void the_revival_closes_through_a_live_dormant_exit() { const auto feed = bars(); - Host host(false); + Host host(Cancel::None); host.run(feed.data(), static_cast(feed.size())); CHECK(host.last_error().empty()); // badapter-v19dp1-control. @@ -177,12 +250,14 @@ void the_revival_closes_through_a_live_dormant_exit() { CHECK(host.live_position_size() == 0.0); } -void a_cancelled_exit_is_not_revived() { +// badapter-v19dp1-cancel, v19fix-cancel-before-pair and v19fix-cancel-at-1000 +// (one tv_trades.csv): two margin-call slices, X never closes, 836 open. +void books_the_cancel_tape(Cancel cancel, double stop = 10.25) { const auto feed = bars(); - Host host(true); + Host host(cancel, true, stop); host.run(feed.data(), static_cast(feed.size())); CHECK(host.last_error().empty()); - // badapter-v19dp1-cancel: two margin-call slices, X never closes. + std::printf(" %s, stop %.2f\n", spelling(cancel), stop); check_rows(host, { {12.0, 10.39, 5, 2, 9, 30, "__margin_call__", "Margin call", -3.48}, {92.0, 10.57, 5, 6, 10, 0, "__margin_call__", "Margin call", -43.24}, @@ -193,6 +268,107 @@ void a_cancelled_exit_is_not_revived() { CHECK(!host.range_end_trades_.empty() && host.range_end_trades_[0].qty == 836.0); } +void a_cancelled_exit_is_not_revived() { books_the_cancel_tape(Cancel::Dormant); } + +void an_exit_cancelled_live_before_the_pair_is_not_revived() { + books_the_cancel_tape(Cancel::BeforePair); +} + +void an_exit_cancelled_live_on_an_earlier_bar_is_not_revived() { + books_the_cancel_tape(Cancel::Earlier); +} + +void every_cancel_spelling_books_the_cancel_tape() { + for (Cancel cancel : {Cancel::DormantAll, Cancel::AfterPair, Cancel::AllBeforePair}) + books_the_cancel_tape(cancel); +} + +// The gapped stop: without a cancel the parked X closes the rest at the +// margin call (v19fix-gapped-stop-control) ... +void a_gapped_stop_parked_for_the_margin_call_closes_through_x() { + const auto feed = bars(); + Host host(Cancel::None, true, 10.15); + host.run(feed.data(), static_cast(feed.size())); + CHECK(host.last_error().empty()); + check_rows(host, { + {12.0, 10.39, 5, 2, 9, 30, "__margin_call__", "Margin call", -3.48}, + {928.0, 10.39, 5, 2, 9, 30, "X", "", -269.12}, + }); + CHECK(host.live_position_size() == 0.0); +} + +// ... and a cancel keeps it parked for good (v19fix-gapped-stop-cancel, +// v19fix-gapped-stop-cancel-after-pair; cancel_all and the cancel before the +// pair untaped). +void a_cancelled_gapped_stop_is_not_revived() { + for (Cancel cancel : {Cancel::Dormant, Cancel::AfterPair, Cancel::DormantAll, + Cancel::AllBeforePair, Cancel::BeforePair}) + books_the_cancel_tape(cancel, 10.15); +} + +struct Booked { + std::vector trades; + double open = 0.0; + int through_x = 0; + std::string error; +}; + +Booked run_without_grid(Cancel cancel) { + const auto feed = bars(); + Host host(cancel, false); + host.run(feed.data(), static_cast(feed.size())); + Booked out; + for (int k = 0; k < host.trade_count(); ++k) { + out.trades.push_back(host.get_trade(k)); + if (out.trades.back().exit_id == "X") ++out.through_x; + } + out.open = host.live_position_size(); + out.error = host.last_error(); + return out; +} + +bool same_trades(const Booked& a, const Booked& b) { + if (a.trades.size() != b.trades.size() || a.open != b.open) return false; + for (std::size_t k = 0; k < a.trades.size(); ++k) { + const Trade& x = a.trades[k]; + const Trade& y = b.trades[k]; + if (x.is_long != y.is_long || x.qty != y.qty || x.entry_price != y.entry_price + || x.exit_price != y.exit_price || x.entry_time != y.entry_time + || x.exit_time != y.exit_time || x.exit_id != y.exit_id || x.pnl != y.pnl) { + return false; + } + } + return true; +} + +// No quantity grid: the pair holds X at the command, in the cancel's callback. +void without_a_grid_every_cancel_books_the_dormant_cancel() { + const Booked standing = run_without_grid(Cancel::None); + const Booked dormant = run_without_grid(Cancel::Dormant); + CHECK(standing.error.empty()); + CHECK(dormant.error.empty()); + std::printf(" no cancel: %zu trades, %d through X, open %.4f\n", standing.trades.size(), + standing.through_x, standing.open); + std::printf(" dormant cancel: %zu trades, %d through X, open %.4f\n", + dormant.trades.size(), dormant.through_x, dormant.open); + // Not vacuous: without a cancel the margin call closes through X ... + CHECK(standing.through_x == 1); + CHECK(standing.open == 0.0); + // ... and the cancel on the dormant exit keeps it closed. + CHECK(dormant.through_x == 0); + CHECK(dormant.open < 0.0); + CHECK(dormant.trades.size() >= 2); + for (Cancel cancel : {Cancel::BeforePair, Cancel::AfterPair, Cancel::AllBeforePair, + Cancel::Earlier, Cancel::DormantAll}) { + const Booked booked = run_without_grid(cancel); + std::printf(" %s: %zu trades, %d through X, open %.4f\n", spelling(cancel), + booked.trades.size(), booked.through_x, booked.open); + CHECK(booked.error.empty()); + CHECK(booked.through_x == 0); + CHECK(same_trades(booked, dormant)); + } +} + void test(const char* name, void (*fn)()) { const int before = failures; std::printf("-- %s\n", name); @@ -207,7 +383,19 @@ int main() { the_revival_closes_through_a_live_dormant_exit); test("a margin call does not revive an exit the script cancelled (tape: cancel)", a_cancelled_exit_is_not_revived); - std::printf("R5 B-ADAPTER margin revival after a cancel: %d checks, %d failures\n", checks, - failures); + test("nor one it cancelled while live, before the pair (tape: v19fix-cancel-before-pair)", + an_exit_cancelled_live_before_the_pair_is_not_revived); + test("nor one it cancelled while live, on an earlier bar (tape: v19fix-cancel-at-1000)", + an_exit_cancelled_live_on_an_earlier_bar_is_not_revived); + test("cancel_all, and a cancel after the pair, book the cancel tape too", + every_cancel_spelling_books_the_cancel_tape); + test("a gapped stop parked for the margin call closes through X (tape: gapped-stop-control)", + a_gapped_stop_parked_for_the_margin_call_closes_through_x); + test("a cancelled gapped stop is not revived (tapes: gapped-stop-cancel, -cancel-after-pair)", + a_cancelled_gapped_stop_is_not_revived); + test("without a quantity grid (the pair holds X at the command) no cancel is revived", + without_a_grid_every_cancel_books_the_dormant_cancel); + std::printf("R5 B-ADAPTER / V19-FIX margin revival after a cancel: %d checks, %d failures\n", + checks, failures); return failures ? 1 : 0; } diff --git a/tests/test_adapter_quiet_bar_pinned.inc b/tests/test_adapter_quiet_bar_pinned.inc index 49fc6ed7..bf85b883 100644 --- a/tests/test_adapter_quiet_bar_pinned.inc +++ b/tests/test_adapter_quiet_bar_pinned.inc @@ -625,277 +625,550 @@ // Random48/0/0: boundary 0xd305a0f9d0984075ull -> 0xd0c92fe6fc290e0bull // Random48/1/1: boundary 0x0630c1ad99e83057ull -> 0xd39f4122a0d0a91dull, rows 0x37e26f78b1f4e325ull -> 0xd439da8c48c337ddull // Random48/1/0: boundary 0x0630c1ad99e83057ull -> 0xd39f4122a0d0a91dull +// expectation corrected (V19-FIX, 680 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; 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; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree: +// Quiet/0/1: boundary 0xc1c15d580d2b91ddull -> 0x1c2b2e37a44f6877ull, rows 0x6f2ef9fb0f46bdb6ull -> 0x81799b58f0e052b1ull, final_hash 0xfd89dcb68ffd9f6full -> 0xe0aa4c0240939bcfull +// Quiet/0/0: boundary 0xc1c15d580d2b91ddull -> 0x1c2b2e37a44f6877ull, final_hash 0xfd89dcb68ffd9f6full -> 0xe0aa4c0240939bcfull +// Quiet/1/1: boundary 0x25925b8843f8c940ull -> 0x0a542471817c0616ull, rows 0x3f01d574c39a983full -> 0x4210b9ca5a7d77e6ull, final_hash 0x08472c64f2a3b409ull -> 0x2e91db500a83cbe9ull +// Quiet/1/0: boundary 0x25925b8843f8c940ull -> 0x0a542471817c0616ull, final_hash 0x08472c64f2a3b409ull -> 0x2e91db500a83cbe9ull +// HoldLongFull/0/1: boundary 0xf7ee6fe81728a11eull -> 0xbb82b8cd85ffaae1ull, rows 0x245fab3eff4d7559ull -> 0xae96abef4356f11full, final_hash 0x68a09e458dc41ed9ull -> 0xbe87239166611559ull +// HoldLongFull/0/0: boundary 0xf7ee6fe81728a11eull -> 0xbb82b8cd85ffaae1ull, final_hash 0x68a09e458dc41ed9ull -> 0xbe87239166611559ull +// HoldLongFull/1/1: boundary 0x74ba83122973ae7bull -> 0x83f82e6a0b0a5677ull, rows 0xa773cd9e004bc143ull -> 0x7700c6fdca38234full, final_hash 0xf7d8d3d0b26bcfb1ull -> 0xb6f097b6a6318731ull +// HoldLongFull/1/0: boundary 0x74ba83122973ae7bull -> 0x83f82e6a0b0a5677ull, final_hash 0xf7d8d3d0b26bcfb1ull -> 0xb6f097b6a6318731ull +// MarginCalls/0/1: boundary 0xf01f77fadacf9b1bull -> 0x3633b5fd46e3ee1eull, rows 0x720c4e8e6c1151d7ull -> 0x35e47773037c0f4dull, final_hash 0x1423cb11eff138efull -> 0x64d4054219e25f2full +// MarginCalls/0/0: boundary 0xf01f77fadacf9b1bull -> 0x3633b5fd46e3ee1eull, final_hash 0x1423cb11eff138efull -> 0x64d4054219e25f2full +// MarginCalls/1/1: boundary 0x04552ef5dd6b0248ull -> 0x96ca6ef83164c685ull, rows 0x34a90870ea8ff4dfull -> 0x4b1bc134021b04cbull, final_hash 0x39b0b7d450ea1d23ull -> 0xc10c1fc9e88e7c23ull +// MarginCalls/1/0: boundary 0x04552ef5dd6b0248ull -> 0x96ca6ef83164c685ull, final_hash 0x39b0b7d450ea1d23ull -> 0xc10c1fc9e88e7c23ull +// MarginCallCycles/0/1: boundary 0xe517ae23a9d9cd84ull -> 0x38656d5e12929625ull, rows 0x09d31e44e56049a0ull -> 0x5c4aa443e8e76ef9ull, final_hash 0xd6e5d9e3761d70f7ull -> 0x55f80f5340e670b7ull +// MarginCallCycles/0/0: boundary 0xe517ae23a9d9cd84ull -> 0x38656d5e12929625ull, final_hash 0xd6e5d9e3761d70f7ull -> 0x55f80f5340e670b7ull +// MarginCallCycles/1/1: boundary 0x74fb06c3e47e091eull -> 0xc969906a8f7b57bbull, rows 0x3015bbe850ee059dull -> 0x67e6e63eb71a661cull, final_hash 0x1a3d548af01065b8ull -> 0x8e9e930063b52698ull +// MarginCallCycles/1/0: boundary 0x74fb06c3e47e091eull -> 0xc969906a8f7b57bbull, final_hash 0x1a3d548af01065b8ull -> 0x8e9e930063b52698ull +// Bracket/0/1: boundary 0xad121243d42812f0ull -> 0x5ceffdf162a625baull, rows 0xc34f99dbe945355dull -> 0x2c0b9b25e51c3426ull, final_hash 0xe393d0e31b6c86dbull -> 0xb62be9f335a690fbull +// Bracket/0/0: boundary 0xad121243d42812f0ull -> 0x5ceffdf162a625baull, final_hash 0xe393d0e31b6c86dbull -> 0xb62be9f335a690fbull +// Bracket/1/1: boundary 0x42b52abab2bf3209ull -> 0x53093fd52be58e6aull, rows 0xee291f58ffb7864dull -> 0x26c6fc455e7a9e3full, final_hash 0xa953041c662027b9ull -> 0x2fbb7d2d1228c959ull +// Bracket/1/0: boundary 0x42b52abab2bf3209ull -> 0x53093fd52be58e6aull, final_hash 0xa953041c662027b9ull -> 0x2fbb7d2d1228c959ull +// Trail/0/1: boundary 0x38951256c0c8ae91ull -> 0x5211ba6f22808f15ull, rows 0x630e44444886533full -> 0x5fd4f2baecf12c67ull, final_hash 0x0a3d59197f3fbeeeull -> 0x1ca55654b8ff3a6eull +// Trail/0/0: boundary 0x38951256c0c8ae91ull -> 0x5211ba6f22808f15ull, final_hash 0x0a3d59197f3fbeeeull -> 0x1ca55654b8ff3a6eull +// Trail/1/1: boundary 0x85f94fedc6115fe3ull -> 0x596611dc371ff159ull, rows 0x100ea0b5c2385430ull -> 0x5ebc78f3f36b3da8ull, final_hash 0x913d53654651ef64ull -> 0x2d3ea22c1cd861e4ull +// Trail/1/0: boundary 0x85f94fedc6115fe3ull -> 0x596611dc371ff159ull, final_hash 0x913d53654651ef64ull -> 0x2d3ea22c1cd861e4ull +// DualStops/0/1: boundary 0xbe6893046a46cb96ull -> 0xf53e1aa110098577ull, rows 0x95b5ee2aec79ec7cull -> 0xa45979a387e09947ull, final_hash 0x3ebb8ec1e50f9ed1ull -> 0x9d20a3a0e3c4b991ull +// DualStops/0/0: boundary 0xbe6893046a46cb96ull -> 0xf53e1aa110098577ull, final_hash 0x3ebb8ec1e50f9ed1ull -> 0x9d20a3a0e3c4b991ull +// DualStops/1/1: boundary 0xebbf122a6c461306ull -> 0xd56451041c2c2c4full, rows 0xb4bc6d57975026c4ull -> 0x3525fdbfa98d36bcull, final_hash 0x7ee6f826d7537cc1ull -> 0x0e2c1cdfb96b6301ull +// DualStops/1/0: boundary 0xebbf122a6c461306ull -> 0xd56451041c2c2c4full, final_hash 0x7ee6f826d7537cc1ull -> 0x0e2c1cdfb96b6301ull +// InvertedStops/0/1: boundary 0x049f870d696c86d6ull -> 0xa3daa491b43f22e9ull, rows 0x6c97ddc3cc3bc830ull -> 0x054c4b8ef1709dc3ull, final_hash 0x92d980a9427423eeull -> 0x056329caffb5a22eull +// InvertedStops/0/0: boundary 0x049f870d696c86d6ull -> 0xa3daa491b43f22e9ull, final_hash 0x92d980a9427423eeull -> 0x056329caffb5a22eull +// InvertedStops/1/1: boundary 0x77643e8c893982c3ull -> 0x9b50fac734e90657ull, rows 0xfaede1fa4ce13f90ull -> 0x2d65179a434eebf0ull, final_hash 0x18c13857d9dfa263ull -> 0xab655e08a941e123ull +// InvertedStops/1/0: boundary 0x77643e8c893982c3ull -> 0x9b50fac734e90657ull, final_hash 0x18c13857d9dfa263ull -> 0xab655e08a941e123ull +// PendingParent/0/1: boundary 0x6721cf5983214d9eull -> 0x47212775031c61c2ull, rows 0xc139da1140ad7563ull -> 0xfd5dbde93e8d520aull, final_hash 0xab2eaed98b0f009aull -> 0xe74974df53ef097aull +// PendingParent/0/0: boundary 0x6721cf5983214d9eull -> 0x47212775031c61c2ull, final_hash 0xab2eaed98b0f009aull -> 0xe74974df53ef097aull +// PendingParent/1/1: boundary 0x298f49712d4af19aull -> 0x08f4fa130f86a0daull, rows 0xa624ebd2d2ec64a5ull -> 0xbd725b4c1d8be787ull, final_hash 0x660fa440f23c5696ull -> 0x27cdc3dedee10a56ull +// PendingParent/1/0: boundary 0x298f49712d4af19aull -> 0x08f4fa130f86a0daull, final_hash 0x660fa440f23c5696ull -> 0x27cdc3dedee10a56ull +// GappedAdd/0/1: boundary 0x801838fc90033739ull -> 0x8bd71987fffc51e9ull, rows 0x8259b732e34ac913ull -> 0x42f9dd5f352155d8ull, final_hash 0x58b9e653d25a4647ull -> 0xaa98c069e5a019a7ull +// GappedAdd/0/0: boundary 0x801838fc90033739ull -> 0x8bd71987fffc51e9ull, final_hash 0x58b9e653d25a4647ull -> 0xaa98c069e5a019a7ull +// GappedAdd/1/1: boundary 0x8429bf8f9c179793ull -> 0x7b93202bd79c1393ull, rows 0xa8491ce8abf7d3ffull -> 0xa4421baf09234dd7ull, final_hash 0x803cb88784ce9026ull -> 0xe086e35f4f128b46ull +// GappedAdd/1/0: boundary 0x8429bf8f9c179793ull -> 0x7b93202bd79c1393ull, final_hash 0x803cb88784ce9026ull -> 0xe086e35f4f128b46ull +// DeclinedReversal/0/1: boundary 0xd0083ac2e7a9723eull -> 0xfcb9145b39ec7b5aull, rows 0x8567c0614fd76d26ull -> 0xbbddf0514f17aef7ull, final_hash 0x8f1c4a1a41f5a3bcull -> 0x764450acb0596bfcull +// DeclinedReversal/0/0: boundary 0xd0083ac2e7a9723eull -> 0xfcb9145b39ec7b5aull, final_hash 0x8f1c4a1a41f5a3bcull -> 0x764450acb0596bfcull +// DeclinedReversal/1/1: boundary 0x0fd76da84e65f1c0ull -> 0x4a1378eba012ef40ull, rows 0x1eb7bc54208e0534ull -> 0x16ea86de895d5b40ull, final_hash 0x22d70277dc1fc0a4ull -> 0x14734f0e113371e4ull +// DeclinedReversal/1/0: boundary 0x0fd76da84e65f1c0ull -> 0x4a1378eba012ef40ull, final_hash 0x22d70277dc1fc0a4ull -> 0x14734f0e113371e4ull +// CalcOnFills/0/1: boundary 0xdd17a201bb4d06b2ull -> 0x7641907936993779ull, rows 0x52de59217319fd8aull -> 0x3777fe6ece242880ull, final_hash 0xfe5dc3ec475477d9ull -> 0x2583ce2bc6a3c1d9ull +// CalcOnFills/0/0: boundary 0xdd17a201bb4d06b2ull -> 0x7641907936993779ull, final_hash 0xfe5dc3ec475477d9ull -> 0x2583ce2bc6a3c1d9ull +// CalcOnFills/1/1: boundary 0xaa733db98dd5e688ull -> 0xd120e8fefb0dc236ull, rows 0x07c506ff68fc1a7bull -> 0xdbb28de374d30bc3ull, final_hash 0xcd17129763baf911ull -> 0xad6e26cf69199431ull +// CalcOnFills/1/0: boundary 0xaa733db98dd5e688ull -> 0xd120e8fefb0dc236ull, final_hash 0xcd17129763baf911ull -> 0xad6e26cf69199431ull +// CloseOrders/0/1: boundary 0x31b0c8d9e01f772full -> 0x78ff3352f7085436ull, rows 0x09c999edd76d4fdaull -> 0x896bcdcf15c8a4c6ull, final_hash 0x2d39ec28cc86f100ull -> 0x8e67de695bfb2c60ull +// CloseOrders/0/0: boundary 0x31b0c8d9e01f772full -> 0x78ff3352f7085436ull, final_hash 0x2d39ec28cc86f100ull -> 0x8e67de695bfb2c60ull +// CloseOrders/1/1: boundary 0xf65b916be995d95full -> 0xe00ae8be3caf3315ull, rows 0x755bccd5cafc099eull -> 0x2584350c36f68309ull, final_hash 0x6281f01aff005efeull -> 0xa998448999ff9b1eull +// CloseOrders/1/0: boundary 0xf65b916be995d95full -> 0xe00ae8be3caf3315ull, final_hash 0x6281f01aff005efeull -> 0xa998448999ff9b1eull +// CloseOrdersSlipped/0/1: boundary 0xf41f34b44f14ed1bull -> 0x58e9160429c03bcaull, rows 0x88cca9165f3f9ca5ull -> 0xd3632e32e35ef626ull, final_hash 0x184081a51dee14b6ull -> 0x13f82da0279b5836ull +// CloseOrdersSlipped/0/0: boundary 0xf41f34b44f14ed1bull -> 0x58e9160429c03bcaull, final_hash 0x184081a51dee14b6ull -> 0x13f82da0279b5836ull +// CloseOrdersSlipped/1/1: boundary 0xefc7d84261df23b7ull -> 0x3507072a3e435b83ull, rows 0x0952c5f9a055c769ull -> 0xc5d77a9b36fdec85ull, final_hash 0xdbb9a359df5b7ba7ull -> 0xc7b132391c547427ull +// CloseOrdersSlipped/1/0: boundary 0xefc7d84261df23b7ull -> 0x3507072a3e435b83ull, final_hash 0xdbb9a359df5b7ba7ull -> 0xc7b132391c547427ull +// IntradayLoss/0/1: boundary 0x45d88fb7f59423e4ull -> 0x644e48072a025ba8ull, rows 0xf597c17beff44c38ull -> 0x0421711a90fdf429ull, final_hash 0xb18331055f670401ull -> 0xecca5625486ab2e1ull +// IntradayLoss/0/0: boundary 0x45d88fb7f59423e4ull -> 0x644e48072a025ba8ull, final_hash 0xb18331055f670401ull -> 0xecca5625486ab2e1ull +// IntradayLoss/1/1: boundary 0xdccf9450e8e7c00cull -> 0xce3032bd544d7840ull, rows 0x488f8a32366e0cafull -> 0x4e9d2d333bd5f470ull, final_hash 0x08be7263bb9faf0eull -> 0x4e071c66b57dfeaeull +// IntradayLoss/1/0: boundary 0xdccf9450e8e7c00cull -> 0xce3032bd544d7840ull, final_hash 0x08be7263bb9faf0eull -> 0x4e071c66b57dfeaeull +// IntradayCapDeferred/0/1: boundary 0x477d0d155ce99044ull -> 0x93a814e5f6e8feb7ull, rows 0xd36b92908117e322ull -> 0xbbbb2712dbbd9ac8ull, final_hash 0xd7b1c15ee8d83724ull -> 0x77c142bb11eab624ull +// IntradayCapDeferred/0/0: boundary 0x477d0d155ce99044ull -> 0x93a814e5f6e8feb7ull, final_hash 0xd7b1c15ee8d83724ull -> 0x77c142bb11eab624ull +// IntradayCapDeferred/1/1: boundary 0x1b27e4ab14777901ull -> 0xe23e69831f88d404ull, rows 0xbc24b0cb7453804bull -> 0x63469b00b6747db8ull, final_hash 0x58271303a14cab9bull -> 0x72de8fef40c4c89bull +// IntradayCapDeferred/1/0: boundary 0x1b27e4ab14777901ull -> 0xe23e69831f88d404ull, final_hash 0x58271303a14cab9bull -> 0x72de8fef40c4c89bull +// PreopenSlice/0/1: boundary 0x85bac68b85fa1b10ull -> 0x9f401876dd941c1full, rows 0xba4b398db082d988ull -> 0xbbce87ebf0d3f638ull, final_hash 0xc4645f9196ec1c52ull -> 0xf73c6812e8dd0f92ull +// PreopenSlice/0/0: boundary 0x85bac68b85fa1b10ull -> 0x9f401876dd941c1full, final_hash 0xc4645f9196ec1c52ull -> 0xf73c6812e8dd0f92ull +// PreopenSlice/1/1: boundary 0x3c681ec4f8ea88e9ull -> 0x98345cf423b84f80ull, rows 0xcfdbbdabfcceb515ull -> 0x36db489e43fd8218ull, final_hash 0x43fed91080dfbea0ull -> 0xa5916c1cffd7bae0ull +// PreopenSlice/1/0: boundary 0x3c681ec4f8ea88e9ull -> 0x98345cf423b84f80ull, final_hash 0x43fed91080dfbea0ull -> 0xa5916c1cffd7bae0ull +// SameBarBatch/0/1: boundary 0xd8c6014359e3436dull -> 0xcfa0ce2dfed4b2e4ull, rows 0x56ce4bb7913dd63dull -> 0x7de7b618b505f51eull, final_hash 0x1e3f3c2b9879352full -> 0x0bb34499121582afull +// SameBarBatch/0/0: boundary 0xd8c6014359e3436dull -> 0xcfa0ce2dfed4b2e4ull, final_hash 0x1e3f3c2b9879352full -> 0x0bb34499121582afull +// SameBarBatch/1/1: boundary 0xb74c80da185d6f7cull -> 0x4557f9683348bf23ull, rows 0xd3659fc4f7228afcull -> 0x79d43d1295ddc801ull, final_hash 0xcc0463b3d75fe13cull -> 0x26a5e7a6d5104dbcull +// SameBarBatch/1/0: boundary 0xb74c80da185d6f7cull -> 0x4557f9683348bf23ull, final_hash 0xcc0463b3d75fe13cull -> 0x26a5e7a6d5104dbcull +// CommissionedShort/0/1: boundary 0xb3b9a9d400a706b7ull -> 0x18b476bd2a3db96dull, rows 0xe36126265b8bf576ull -> 0x54b6ba2e7a2e11c2ull, final_hash 0x23caa9e57f9f6fb8ull -> 0x48a9193b2c1c1118ull +// CommissionedShort/0/0: boundary 0xb3b9a9d400a706b7ull -> 0x18b476bd2a3db96dull, final_hash 0x23caa9e57f9f6fb8ull -> 0x48a9193b2c1c1118ull +// CommissionedShort/1/1: boundary 0x1fba48a99176b809ull -> 0xb24ff7e17f80c347ull, rows 0x8599d32cc4e3f365ull -> 0x20608b72a057ada4ull, final_hash 0xc1bb33f8955d526dull -> 0x3ef1eaec89e6430dull +// CommissionedShort/1/0: boundary 0x1fba48a99176b809ull -> 0xb24ff7e17f80c347ull, final_hash 0xc1bb33f8955d526dull -> 0x3ef1eaec89e6430dull +// AccountFx/0/1: boundary 0x7ea7cc0696e4e706ull -> 0xaeec7af5859fe6b3ull, rows 0x5d06a315cd828e3aull -> 0x37039a16e8c77a7bull, final_hash 0xf3a8b7577af41cbcull -> 0x2a28b4e61cd5cbbcull +// AccountFx/0/0: boundary 0x7ea7cc0696e4e706ull -> 0xaeec7af5859fe6b3ull, final_hash 0xf3a8b7577af41cbcull -> 0x2a28b4e61cd5cbbcull +// Stream/0/1: boundary 0xb558b6c7840b2228ull -> 0x49574a22705f52c0ull, rows 0x34ad20d2694b5c4eull -> 0x3cade1c0a6b65b75ull, final_hash 0xc9c5e2681a211fdaull -> 0x8c9bb935bf0c32daull +// Stream/0/0: boundary 0xb558b6c7840b2228ull -> 0x49574a22705f52c0ull, final_hash 0xc9c5e2681a211fdaull -> 0x8c9bb935bf0c32daull +// Random01/0/1: boundary 0x7b26a7a4fd8ca837ull -> 0xc538d8fca421c703ull, rows 0x8b38d2c0ee3ae09bull -> 0xc035295ef347b140ull, final_hash 0x266b3c97e2229932ull -> 0x166e28e2277135d2ull +// Random01/0/0: boundary 0x7b26a7a4fd8ca837ull -> 0xc538d8fca421c703ull, final_hash 0x266b3c97e2229932ull -> 0x166e28e2277135d2ull +// Random01/1/1: boundary 0x240a7d06f5858192ull -> 0x5b8b5e29d258d71cull, rows 0x5a4bde6c69234036ull -> 0x16a72508f5fdd125ull, final_hash 0xc2da049b01eede0eull -> 0xa0becd4ec596146eull +// Random01/1/0: boundary 0x240a7d06f5858192ull -> 0x5b8b5e29d258d71cull, final_hash 0xc2da049b01eede0eull -> 0xa0becd4ec596146eull +// Random02/0/1: boundary 0xf50e670c28558c70ull -> 0xf933c58464603966ull, rows 0xc237af8c6ea5d8a0ull -> 0x8549ba7d7e735482ull, final_hash 0x785608d44d074ae6ull -> 0xd9870babe5d18626ull +// Random02/0/0: boundary 0xf50e670c28558c70ull -> 0xf933c58464603966ull, final_hash 0x785608d44d074ae6ull -> 0xd9870babe5d18626ull +// Random02/1/1: boundary 0x32b5473f018894caull -> 0x25ba5f1040df3b56ull, rows 0xbbb6be89045a263dull -> 0x1593a8e96a0c90a9ull, final_hash 0x537ce6356efa7dcdull -> 0x0fb50af38c25c54dull +// Random02/1/0: boundary 0x32b5473f018894caull -> 0x25ba5f1040df3b56ull, final_hash 0x537ce6356efa7dcdull -> 0x0fb50af38c25c54dull +// Random03/0/1: boundary 0x4de9d3289f070b10ull -> 0xb5a3f885ba0143fdull, rows 0xc623f8fada361795ull -> 0xd91b7712cc62209bull, final_hash 0xbc66b161650e945aull -> 0xd93ef819aff693f3ull +// Random03/0/0: boundary 0x4de9d3289f070b10ull -> 0xb5a3f885ba0143fdull, final_hash 0xbc66b161650e945aull -> 0xd93ef819aff693f3ull +// Random03/1/1: boundary 0x74c92954e88611faull -> 0x36230659e7bf570eull, rows 0xff0a6422eb94899eull -> 0xdb83075a5690e996ull, final_hash 0x66e224895c00a770ull -> 0x23acbc0041308125ull +// Random03/1/0: boundary 0x74c92954e88611faull -> 0x36230659e7bf570eull, final_hash 0x66e224895c00a770ull -> 0x23acbc0041308125ull +// Random04/0/1: boundary 0xc803c2750b61157dull -> 0x0c41be29e4497a7cull, rows 0x32e6028cb8a54dc8ull -> 0x1fdaabb2a1c312cdull, final_hash 0x84461297f142b9efull -> 0x542f1b01ef32d535ull +// Random04/0/0: boundary 0xc803c2750b61157dull -> 0x0c41be29e4497a7cull, final_hash 0x84461297f142b9efull -> 0x542f1b01ef32d535ull +// Random04/1/1: boundary 0xaf56d9261eb50536ull -> 0x93ade030662e0b77ull, rows 0xfece30308981fb23ull -> 0x5beecc5dc910feb8ull, final_hash 0xcd21899a90f1d564ull -> 0x254f4f4933be69e0ull +// Random04/1/0: boundary 0xaf56d9261eb50536ull -> 0x93ade030662e0b77ull, final_hash 0xcd21899a90f1d564ull -> 0x254f4f4933be69e0ull +// Random05/0/1: boundary 0xc8efa2a84661d593ull -> 0x38db302327d01e80ull, rows 0x90b1a6e9fbe4e13full -> 0x2461425b8e878f0dull, final_hash 0x2dd0cf2ce3ea761eull -> 0xe32002fbd2d6ea3eull +// Random05/0/0: boundary 0xc8efa2a84661d593ull -> 0x38db302327d01e80ull, final_hash 0x2dd0cf2ce3ea761eull -> 0xe32002fbd2d6ea3eull +// Random05/1/1: boundary 0x18519fd8ab2ecf78ull -> 0xe3ae54dee0ba6db5ull, rows 0x967a69d448f6b43aull -> 0xc1c6474702bc75e0ull, final_hash 0x1733e9f06b55202cull -> 0x70343cf2d03a75ebull +// Random05/1/0: boundary 0x18519fd8ab2ecf78ull -> 0xe3ae54dee0ba6db5ull, final_hash 0x1733e9f06b55202cull -> 0x70343cf2d03a75ebull +// Random06/0/1: boundary 0x2bfc102809cb61b6ull -> 0xecb44da2cbdb5c42ull, rows 0xe71623e0f3912192ull -> 0x5485171eedfc86cfull, final_hash 0xbe2d610924b8c00dull -> 0xff2efe1925e45a9dull +// Random06/0/0: boundary 0x2bfc102809cb61b6ull -> 0xecb44da2cbdb5c42ull, final_hash 0xbe2d610924b8c00dull -> 0xff2efe1925e45a9dull +// Random06/1/1: boundary 0xe00ef146710c5f72ull -> 0x65cc03efd59ec6b5ull, rows 0xf468c8529e501c5full -> 0x2dce2153546130dbull, final_hash 0x814550b3d8ab202aull -> 0x1130940f86f108d2ull +// Random06/1/0: boundary 0xe00ef146710c5f72ull -> 0x65cc03efd59ec6b5ull, final_hash 0x814550b3d8ab202aull -> 0x1130940f86f108d2ull +// Random07/0/1: boundary 0x4a60ce0b1c4fb021ull -> 0x7bf47df91573ab9cull, rows 0xd3043f597a7a6f7eull -> 0x3c1feb47a4d6d81eull, final_hash 0xbfd9ebdfdd9a9abbull -> 0x438a828933e58c5bull +// Random07/0/0: boundary 0x4a60ce0b1c4fb021ull -> 0x7bf47df91573ab9cull, final_hash 0xbfd9ebdfdd9a9abbull -> 0x438a828933e58c5bull +// Random07/1/1: boundary 0x8f487efcaf6960daull -> 0x29fcc776be13bdf5ull, rows 0x9ddf86f7c192467bull -> 0x9a5ab1b2b00efff1ull, final_hash 0xb858f3c326ae5bebull -> 0x353c5f0e5bc1c22bull +// Random07/1/0: boundary 0x8f487efcaf6960daull -> 0x29fcc776be13bdf5ull, final_hash 0xb858f3c326ae5bebull -> 0x353c5f0e5bc1c22bull +// Random08/0/1: boundary 0xdf32ef61cd4e19eeull -> 0xc2f66d9e5f4943c7ull, rows 0xfd32cff83453f293ull -> 0xf4a38750150093e6ull, final_hash 0x23acd097ec38f43aull -> 0x9ce7eb6ee2b122baull +// Random08/0/0: boundary 0xdf32ef61cd4e19eeull -> 0xc2f66d9e5f4943c7ull, final_hash 0x23acd097ec38f43aull -> 0x9ce7eb6ee2b122baull +// Random08/1/1: boundary 0x869ee3e727ef0b05ull -> 0xbc7e1d017519ee12ull, rows 0x64f491f787b05affull -> 0xc18ef29ec0f4fa8eull, final_hash 0x11144c9c51cb89f3ull -> 0xbd83191789f76873ull +// Random08/1/0: boundary 0x869ee3e727ef0b05ull -> 0xbc7e1d017519ee12ull, final_hash 0x11144c9c51cb89f3ull -> 0xbd83191789f76873ull +// Random09/0/1: boundary 0xf123ed994915e172ull -> 0x400c46d70e6e84bcull, rows 0xe262ad1d42b22aa2ull -> 0xc58cc1d4b5bd3580ull, final_hash 0x24d07dfd4c311e09ull -> 0x1f6cdd2e111e016aull +// Random09/0/0: boundary 0xf123ed994915e172ull -> 0x400c46d70e6e84bcull, final_hash 0x24d07dfd4c311e09ull -> 0x1f6cdd2e111e016aull +// Random09/1/1: boundary 0x28f03f19f3977a71ull -> 0x32d4b35f029bad56ull, rows 0x29fa1939a71c5214ull -> 0x447742874dde46e0ull, final_hash 0xf22f979ee11a143full -> 0x3d99e7abdd92a773ull +// Random09/1/0: boundary 0x28f03f19f3977a71ull -> 0x32d4b35f029bad56ull, final_hash 0xf22f979ee11a143full -> 0x3d99e7abdd92a773ull +// Random10/0/1: boundary 0xb3861d80e682c693ull -> 0xe529db3277bf6dd9ull, rows 0xe5b5b2919d650a05ull -> 0x1a655455a6bd3b6full, final_hash 0x92408fc414e06780ull -> 0xffdc77023cb52b44ull +// Random10/0/0: boundary 0xb3861d80e682c693ull -> 0xe529db3277bf6dd9ull, final_hash 0x92408fc414e06780ull -> 0xffdc77023cb52b44ull +// Random10/1/1: boundary 0x69fe10c28684def0ull -> 0xb3a8db733c3f30bdull, rows 0x10dfb593a8ec827cull -> 0xa2424fa6550c3ef1ull, final_hash 0x6a8371ba10724f52ull -> 0x61b614a68f564bf0ull +// Random10/1/0: boundary 0x69fe10c28684def0ull -> 0xb3a8db733c3f30bdull, final_hash 0x6a8371ba10724f52ull -> 0x61b614a68f564bf0ull +// Random11/0/1: boundary 0x90100a03a86d0cdbull -> 0x805038d1cb0a3848ull, rows 0x016b0db6bef2264cull -> 0xebc3fde6db4a37f3ull, final_hash 0x185f0857180c5a28ull -> 0x27bc15c5c142c728ull +// Random11/0/0: boundary 0x90100a03a86d0cdbull -> 0x805038d1cb0a3848ull, final_hash 0x185f0857180c5a28ull -> 0x27bc15c5c142c728ull +// Random11/1/1: boundary 0x84170dd33baa79d9ull -> 0x4a739854a58a3db9ull, rows 0x1a59bcbe0a910135ull -> 0xc91f1e4519586a06ull, final_hash 0x41861834717a9917ull -> 0x8af717fd3fa04097ull +// Random11/1/0: boundary 0x84170dd33baa79d9ull -> 0x4a739854a58a3db9ull, final_hash 0x41861834717a9917ull -> 0x8af717fd3fa04097ull +// Random12/0/1: boundary 0x3d418ef44da4cd2eull -> 0xb9a9bb3fafab7349ull, rows 0xe892e674e3e6e0caull -> 0xe93fb7b821f6a7feull, final_hash 0x4e9bd726c9f377b7ull -> 0x29c94b5df165789aull +// Random12/0/0: boundary 0x3d418ef44da4cd2eull -> 0xb9a9bb3fafab7349ull, final_hash 0x4e9bd726c9f377b7ull -> 0x29c94b5df165789aull +// Random12/1/1: boundary 0x1fc620580d6c6a8cull -> 0x298ab491e3caa61full, rows 0xba2f51ae1c4ffcaeull -> 0xf8e78400f1c148acull, final_hash 0x5209c01b2d9075bbull -> 0x927c4394206b42b2ull +// Random12/1/0: boundary 0x1fc620580d6c6a8cull -> 0x298ab491e3caa61full, final_hash 0x5209c01b2d9075bbull -> 0x927c4394206b42b2ull +// Random13/0/1: boundary 0xc330ac11cbbddd41ull -> 0xf255fd63895e7badull, rows 0xe0a3d24e78674f95ull -> 0x41470781ab6bef64ull, final_hash 0xefab5484e3574594ull -> 0x57de0e46784b0f54ull +// Random13/0/0: boundary 0xc330ac11cbbddd41ull -> 0xf255fd63895e7badull, final_hash 0xefab5484e3574594ull -> 0x57de0e46784b0f54ull +// Random13/1/1: boundary 0xcdb3c6546824667dull -> 0x7abd074c56aca8c6ull, rows 0x653d5c4469730325ull -> 0x41de5734424c47edull, final_hash 0x027984836057c1fbull -> 0xa6590b3d3950e7bbull +// Random13/1/0: boundary 0xcdb3c6546824667dull -> 0x7abd074c56aca8c6ull, final_hash 0x027984836057c1fbull -> 0xa6590b3d3950e7bbull +// Random14/0/1: boundary 0x1bc30bb176abfe75ull -> 0x6b59b79be4590a72ull, rows 0x38ac385d83173e6dull -> 0x6d4cbed6a7d3b423ull, final_hash 0x240563decf410a07ull -> 0x918405ca8ede2f52ull +// Random14/0/0: boundary 0x1bc30bb176abfe75ull -> 0x6b59b79be4590a72ull, final_hash 0x240563decf410a07ull -> 0x918405ca8ede2f52ull +// Random14/1/1: boundary 0xf70698eaf69fe0f7ull -> 0x8dfaddf560dc80caull, rows 0x32373b160f2cb0a4ull -> 0x13e689009402d1e0ull, final_hash 0x2a207608d53d6e31ull -> 0x70415e0fc326b13cull +// Random14/1/0: boundary 0xf70698eaf69fe0f7ull -> 0x8dfaddf560dc80caull, final_hash 0x2a207608d53d6e31ull -> 0x70415e0fc326b13cull +// Random15/0/1: boundary 0x8d19883bfd7b9827ull -> 0x1fe125a3fb2912dfull, rows 0x617a8b521ffd19a8ull -> 0x9ef69d4200bfb809ull, final_hash 0xc5c912afdfb5429eull -> 0xf8da94854e79d89eull +// Random15/0/0: boundary 0x8d19883bfd7b9827ull -> 0x1fe125a3fb2912dfull, final_hash 0xc5c912afdfb5429eull -> 0xf8da94854e79d89eull +// Random15/1/1: boundary 0xce37c225571916fbull -> 0x7b17bf5a43044023ull, rows 0x5ea9fcc2c1385c3full -> 0xdeec1e0290d1370dull, final_hash 0xf1a244775b55839dull -> 0x01758143c50ac69dull +// Random15/1/0: boundary 0xce37c225571916fbull -> 0x7b17bf5a43044023ull, final_hash 0xf1a244775b55839dull -> 0x01758143c50ac69dull +// Random16/0/1: boundary 0x41c794238409bacbull -> 0xde7f3e61caa50135ull, rows 0xe3000785740c45c4ull -> 0x9593cb63a9a06f14ull, final_hash 0xdbb1c1b8835a2f52ull -> 0xc99a0f083d0b2507ull +// Random16/0/0: boundary 0x41c794238409bacbull -> 0xde7f3e61caa50135ull, final_hash 0xdbb1c1b8835a2f52ull -> 0xc99a0f083d0b2507ull +// Random16/1/1: boundary 0xb469144e85e0a627ull -> 0xffbc34c7947c611bull, rows 0x5adace02ce0745bcull -> 0xa2b9ee52a1338391ull, final_hash 0xd6a03051ba6c30efull -> 0x1f80513beefa0b1aull +// Random16/1/0: boundary 0xb469144e85e0a627ull -> 0xffbc34c7947c611bull, final_hash 0xd6a03051ba6c30efull -> 0x1f80513beefa0b1aull +// Random17/0/1: boundary 0x6304fa3c28e07e49ull -> 0x15bab2d913fb1d81ull, rows 0xf1ed3675ad25ec11ull -> 0x0a3d587bbe912dcfull, final_hash 0xfda9c1caff36448bull -> 0x7185bfaebb406df5ull +// Random17/0/0: boundary 0x6304fa3c28e07e49ull -> 0x15bab2d913fb1d81ull, final_hash 0xfda9c1caff36448bull -> 0x7185bfaebb406df5ull +// Random17/1/1: boundary 0x91c41f4b53c60db8ull -> 0xa978788efbcc1631ull, rows 0x4927f647a64a6aa1ull -> 0xbcbe24594a8acfcaull, final_hash 0xb386dd8b4f22a827ull -> 0x285eaf39ec559587ull +// Random17/1/0: boundary 0x91c41f4b53c60db8ull -> 0xa978788efbcc1631ull, final_hash 0xb386dd8b4f22a827ull -> 0x285eaf39ec559587ull +// Random18/0/1: boundary 0x2f90853903ca8075ull -> 0x90ed08a2949b1fb6ull, rows 0x9614b30d8372e1b6ull -> 0x665cda1c725bc1b2ull, final_hash 0x827427f9640b4e97ull -> 0xe16bbfbfa58dbeb1ull +// Random18/0/0: boundary 0x2f90853903ca8075ull -> 0x90ed08a2949b1fb6ull, final_hash 0x827427f9640b4e97ull -> 0xe16bbfbfa58dbeb1ull +// Random18/1/1: boundary 0xd759ce46e3583dd6ull -> 0xbf864c04890cf915ull, rows 0x4d62656618897636ull -> 0x807a82ee057fbb5dull, final_hash 0x35a46c04db974779ull -> 0x5c359872c6618dd3ull +// Random18/1/0: boundary 0xd759ce46e3583dd6ull -> 0xbf864c04890cf915ull, final_hash 0x35a46c04db974779ull -> 0x5c359872c6618dd3ull +// Random19/0/1: boundary 0x0829e86674bb90a3ull -> 0x3c5cc975f928b775ull, rows 0x0c808692940a05a0ull -> 0xea691a951590ece8ull, final_hash 0x26e403c4671fad9full -> 0x17d53da80c3a895full +// Random19/0/0: boundary 0x0829e86674bb90a3ull -> 0x3c5cc975f928b775ull, final_hash 0x26e403c4671fad9full -> 0x17d53da80c3a895full +// Random19/1/1: boundary 0x62aea0c1de6a6c13ull -> 0x92536ad17b9fbf26ull, rows 0xf4f4a620ee4a7ab5ull -> 0xabc10759f5e1ffa1ull, final_hash 0xb4aa06caa7722a48ull -> 0x168791086f51c368ull +// Random19/1/0: boundary 0x62aea0c1de6a6c13ull -> 0x92536ad17b9fbf26ull, final_hash 0xb4aa06caa7722a48ull -> 0x168791086f51c368ull +// Random20/0/1: boundary 0x8caa054b399b232aull -> 0x90b20d166e1357e0ull, rows 0x4fc0f37c9645d067ull -> 0x808745030887aa13ull, final_hash 0x9facee5fae21aa7cull -> 0x3968c24c22199036ull +// Random20/0/0: boundary 0x8caa054b399b232aull -> 0x90b20d166e1357e0ull, final_hash 0x9facee5fae21aa7cull -> 0x3968c24c22199036ull +// Random20/1/1: boundary 0x0bed3f4e6ae2ef8full -> 0xa82d4e53d2192a5full, rows 0xbc77d10bf614561dull -> 0x8e1411c055e37af3ull, final_hash 0x0d9fbc941287b1cfull -> 0x5c29f15f02417fcaull +// Random20/1/0: boundary 0x0bed3f4e6ae2ef8full -> 0xa82d4e53d2192a5full, final_hash 0x0d9fbc941287b1cfull -> 0x5c29f15f02417fcaull +// Random21/0/1: boundary 0x546f79d5d3874035ull -> 0xe1da26acd373548aull, rows 0x57db503858f2c920ull -> 0xad828c8396454565ull, final_hash 0x6a7c93c51185957aull -> 0xd6a6f90f609ca64aull +// Random21/0/0: boundary 0x546f79d5d3874035ull -> 0xe1da26acd373548aull, final_hash 0x6a7c93c51185957aull -> 0xd6a6f90f609ca64aull +// Random21/1/1: boundary 0x61ab9c4cf365da62ull -> 0x1b2388f0f789a7d1ull, rows 0x6034eb0b07bc00b6ull -> 0xc1ca64a0715ca8b4ull, final_hash 0x8a67a9fe7abd9c00ull -> 0x08da0b92c166b77cull +// Random21/1/0: boundary 0x61ab9c4cf365da62ull -> 0x1b2388f0f789a7d1ull, final_hash 0x8a67a9fe7abd9c00ull -> 0x08da0b92c166b77cull +// Random22/0/1: boundary 0x7264962e97a25a80ull -> 0xef05a2f01ca0c05cull, rows 0x744915b658d1f5d1ull -> 0xb151ebfe9a44ebebull, final_hash 0x1a6bcb392d46b371ull -> 0x1b1b4c42bccc2191ull +// Random22/0/0: boundary 0x7264962e97a25a80ull -> 0xef05a2f01ca0c05cull, final_hash 0x1a6bcb392d46b371ull -> 0x1b1b4c42bccc2191ull +// Random22/1/1: boundary 0x47d74c02fc2519e4ull -> 0x1624d3651edc9089ull, rows 0x7443f9b7143dacd2ull -> 0x2a2f5457686fdb7full, final_hash 0xa04d40cbbd623427ull -> 0xb361721c9b7f57e7ull +// Random22/1/0: boundary 0x47d74c02fc2519e4ull -> 0x1624d3651edc9089ull, final_hash 0xa04d40cbbd623427ull -> 0xb361721c9b7f57e7ull +// Random23/0/1: boundary 0xfa4d25cb6d9764cbull -> 0xc50cf90af093d6e9ull, rows 0xdea9813bde333195ull -> 0x629f1e11fd65d1cdull, final_hash 0x5058d81da9e2cf89ull -> 0x9f14134203a5c6e9ull +// Random23/0/0: boundary 0xfa4d25cb6d9764cbull -> 0xc50cf90af093d6e9ull, final_hash 0x5058d81da9e2cf89ull -> 0x9f14134203a5c6e9ull +// Random23/1/1: boundary 0xd2626d74d1ddbe27ull -> 0x61be7e60a493c3efull, rows 0x55431bc925b28038ull -> 0x7182d347041d20e2ull, final_hash 0x510aa1745bdba1c7ull -> 0xcac3f08a3f6d7067ull +// Random23/1/0: boundary 0xd2626d74d1ddbe27ull -> 0x61be7e60a493c3efull, final_hash 0x510aa1745bdba1c7ull -> 0xcac3f08a3f6d7067ull +// Random24/0/1: boundary 0x1972ae0bc903694dull -> 0xadbbd26ef9c19430ull, rows 0x2b240d2a365b8703ull -> 0x7cea484a04d1bc6dull, final_hash 0x82e72364e836634full -> 0x880c60f0ef5d5a7dull +// Random24/0/0: boundary 0x1972ae0bc903694dull -> 0xadbbd26ef9c19430ull, final_hash 0x82e72364e836634full -> 0x880c60f0ef5d5a7dull +// Random24/1/1: boundary 0x3df4ec3fe0efc36full -> 0x91d34f247834f4e9ull, rows 0x8ee35827bfb734d3ull -> 0xf9fa951efcc4b1caull, final_hash 0x521e8d8456c37b6dull -> 0xc06e2e1df8db5231ull +// Random24/1/0: boundary 0x3df4ec3fe0efc36full -> 0x91d34f247834f4e9ull, final_hash 0x521e8d8456c37b6dull -> 0xc06e2e1df8db5231ull +// Random25/0/1: boundary 0x5fde9097294eff95ull -> 0x873e291d0017c5dcull, rows 0xeb739137b8b09c27ull -> 0xd20a941e2361c0dfull, final_hash 0x74d22f0254d1d14aull -> 0x7573fe39c01fb659ull +// Random25/0/0: boundary 0x5fde9097294eff95ull -> 0x873e291d0017c5dcull, final_hash 0x74d22f0254d1d14aull -> 0x7573fe39c01fb659ull +// Random25/1/1: boundary 0x61de85290b80e276ull -> 0x9006cf963c9d2db4ull, rows 0x3e2dace4bb30d118ull -> 0x39a1e5e11f193964ull, final_hash 0xe27506e279646458ull -> 0xb372cc6219683abdull +// Random25/1/0: boundary 0x61de85290b80e276ull -> 0x9006cf963c9d2db4ull, final_hash 0xe27506e279646458ull -> 0xb372cc6219683abdull +// Random26/0/1: boundary 0xf3bbd59278bbf967ull -> 0x406a1ebe98ed8ee5ull, rows 0x1bc27907f389539aull -> 0xd559a888703734a3ull, final_hash 0x86792f22105fe886ull -> 0x758182130fa5a986ull +// Random26/0/0: boundary 0xf3bbd59278bbf967ull -> 0x406a1ebe98ed8ee5ull, final_hash 0x86792f22105fe886ull -> 0x758182130fa5a986ull +// Random26/1/1: boundary 0x14e61f95689ec8dfull -> 0x28ce140c5a29687dull, rows 0x2117af0b816b84ebull -> 0x3d8702945df8ba26ull, final_hash 0x32b7fe08ce5005d2ull -> 0xeeb06d8c2a0c4772ull +// Random26/1/0: boundary 0x14e61f95689ec8dfull -> 0x28ce140c5a29687dull, final_hash 0x32b7fe08ce5005d2ull -> 0xeeb06d8c2a0c4772ull +// Random27/0/1: boundary 0x053cfa54c61f3bd4ull -> 0x51e3ce5734d5737cull, rows 0x49d4da1649eefffbull -> 0x35105942d4fa267full, final_hash 0x2b62c8cc0129f74cull -> 0x030065ad81ba4087ull +// Random27/0/0: boundary 0x053cfa54c61f3bd4ull -> 0x51e3ce5734d5737cull, final_hash 0x2b62c8cc0129f74cull -> 0x030065ad81ba4087ull +// Random27/1/1: boundary 0x26f99a57bee85343ull -> 0x81fa7403c9f754c1ull, rows 0x4ff364a8de81f569ull -> 0x4f43080f0a6c1974ull, final_hash 0xd48238aab27e8f44ull -> 0x900de7e225926b9bull +// Random27/1/0: boundary 0x26f99a57bee85343ull -> 0x81fa7403c9f754c1ull, final_hash 0xd48238aab27e8f44ull -> 0x900de7e225926b9bull +// Random28/0/1: boundary 0x5afaa18295f32ff1ull -> 0xa0bb8c1925814fe3ull, rows 0xca9c90143f0b6a95ull -> 0x8e8763454e7d2dcaull, final_hash 0xe019b821239c3290ull -> 0xe51c2406011cd87dull +// Random28/0/0: boundary 0x5afaa18295f32ff1ull -> 0xa0bb8c1925814fe3ull, final_hash 0xe019b821239c3290ull -> 0xe51c2406011cd87dull +// Random28/1/1: boundary 0x6d2667eba32721eeull -> 0xdb2802eb6d8819c7ull, rows 0x3c23578f7764250aull -> 0x910e834a102ac932ull, final_hash 0xdb07854a60f415d7ull -> 0x7406a539e21e57ceull +// Random28/1/0: boundary 0x6d2667eba32721eeull -> 0xdb2802eb6d8819c7ull, final_hash 0xdb07854a60f415d7ull -> 0x7406a539e21e57ceull +// Random29/0/1: boundary 0xd1301252d9b58f96ull -> 0x2bbdb428b4a78935ull, rows 0xa789410c6a57a22aull -> 0x6c7b12c7d5aa45e4ull, final_hash 0x595deac764871057ull -> 0xfd3d2fb6be6132d7ull +// Random29/0/0: boundary 0xd1301252d9b58f96ull -> 0x2bbdb428b4a78935ull, final_hash 0x595deac764871057ull -> 0xfd3d2fb6be6132d7ull +// Random29/1/1: boundary 0x1a25b83f5e6fed46ull -> 0x2bc2551fd2128e20ull, rows 0x85c4d848e014bb3cull -> 0xb9728dfab890bfd1ull, final_hash 0x13fb019ad44bec5aull -> 0x082924a3760602daull +// Random29/1/0: boundary 0x1a25b83f5e6fed46ull -> 0x2bc2551fd2128e20ull, final_hash 0x13fb019ad44bec5aull -> 0x082924a3760602daull +// Random30/0/1: boundary 0xa9365ffd65e05a09ull -> 0x585a1a9f34476a29ull, rows 0xc88de1ce1dcfafe1ull -> 0x5d09652b7dc7603full, final_hash 0x134c921985714562ull -> 0x8e3b3ec00a1454a2ull +// Random30/0/0: boundary 0xa9365ffd65e05a09ull -> 0x585a1a9f34476a29ull, final_hash 0x134c921985714562ull -> 0x8e3b3ec00a1454a2ull +// Random30/1/1: boundary 0x1facc2c1cfd4c971ull -> 0x8de0587cbe97fc0aull, rows 0x941084ae8c1b103bull -> 0x2fc1ebe82b1783eeull, final_hash 0xf2973241ae3d2097ull -> 0x7ffde30dc93fc9d7ull +// Random30/1/0: boundary 0x1facc2c1cfd4c971ull -> 0x8de0587cbe97fc0aull, final_hash 0xf2973241ae3d2097ull -> 0x7ffde30dc93fc9d7ull +// Random31/0/1: boundary 0x8eaa06b36232a908ull -> 0x3b5f096d5bca7d49ull, rows 0xc98390a9e23599d3ull -> 0xd72b574c253a553dull, final_hash 0xb86249f3491f97b0ull -> 0x466ccec496cced70ull +// Random31/0/0: boundary 0x8eaa06b36232a908ull -> 0x3b5f096d5bca7d49ull, final_hash 0xb86249f3491f97b0ull -> 0x466ccec496cced70ull +// Random31/1/1: boundary 0xf90b0737ee4a025aull -> 0xc14041a700ffb6c1ull, rows 0x09ec11f94ae3f675ull -> 0xe3f3a394089513b1ull, final_hash 0xc7f3afe378a05865ull -> 0xc8e0343f71af4c85ull +// Random31/1/0: boundary 0xf90b0737ee4a025aull -> 0xc14041a700ffb6c1ull, final_hash 0xc7f3afe378a05865ull -> 0xc8e0343f71af4c85ull +// Random32/0/1: boundary 0x32da5eed595ee5b4ull -> 0x7bb420637675ac73ull, rows 0xec4a0ece9475e1d3ull -> 0x7a70d1e9f78179c9ull, final_hash 0x4b6c0af81cc429f5ull -> 0x0e03ccad16dc63fdull +// Random32/0/0: boundary 0x32da5eed595ee5b4ull -> 0x7bb420637675ac73ull, final_hash 0x4b6c0af81cc429f5ull -> 0x0e03ccad16dc63fdull +// Random32/1/1: boundary 0xe733831f206a336aull -> 0x8bd26473f6dc3b55ull, rows 0xb4483230298afef5ull -> 0xeb260eee22e520f0ull, final_hash 0xe6f2b4aa4d8611a1ull -> 0x597150e94c1a4012ull +// Random32/1/0: boundary 0xe733831f206a336aull -> 0x8bd26473f6dc3b55ull, final_hash 0xe6f2b4aa4d8611a1ull -> 0x597150e94c1a4012ull +// Random33/0/1: boundary 0x33bf503af7204823ull -> 0xbfcfebe96632bd39ull, rows 0x39a8de40423427d1ull -> 0x116bc2f57887ba4bull, final_hash 0xbe29a314289e811cull -> 0xf8a0e855a842d39cull +// Random33/0/0: boundary 0x33bf503af7204823ull -> 0xbfcfebe96632bd39ull, final_hash 0xbe29a314289e811cull -> 0xf8a0e855a842d39cull +// Random33/1/1: boundary 0x7f150e22e1c3c861ull -> 0xe17f0353f37c41f1ull, rows 0xda9af32172dac1a4ull -> 0x0ba98d74e8b769b7ull, final_hash 0x1122e3cca5beb09dull -> 0xb76d3ca699ea4ebdull +// Random33/1/0: boundary 0x7f150e22e1c3c861ull -> 0xe17f0353f37c41f1ull, final_hash 0x1122e3cca5beb09dull -> 0xb76d3ca699ea4ebdull +// Random34/0/1: boundary 0x9191daa7f287cff2ull -> 0xea1b68237d95805full, rows 0x1351d75d55b69237ull -> 0xb29d9ea005d9c282ull, final_hash 0x53efe9facdfed383ull -> 0xb03d41aea9c00c63ull +// Random34/0/0: boundary 0x9191daa7f287cff2ull -> 0xea1b68237d95805full, final_hash 0x53efe9facdfed383ull -> 0xb03d41aea9c00c63ull +// Random34/1/1: boundary 0xe3bd733dbb7dd142ull -> 0x4ecacf8ab2d85c47ull, rows 0xcc37232ed176c590ull -> 0x294fe821fef3729dull, final_hash 0x3c1c21ded8a43a85ull -> 0xe4e38b6ada294505ull +// Random34/1/0: boundary 0xe3bd733dbb7dd142ull -> 0x4ecacf8ab2d85c47ull, final_hash 0x3c1c21ded8a43a85ull -> 0xe4e38b6ada294505ull +// Random35/0/1: boundary 0xa1dad91ce6005cbbull -> 0xeb16fb1a6bc80c17ull, rows 0xac8c4802e8a8b254ull -> 0x7265444996669f20ull, final_hash 0xc8f7b4decfbdeed1ull -> 0xb971c6bc6d7ac182ull +// Random35/0/0: boundary 0xa1dad91ce6005cbbull -> 0xeb16fb1a6bc80c17ull, final_hash 0xc8f7b4decfbdeed1ull -> 0xb971c6bc6d7ac182ull +// Random35/1/1: boundary 0x6ce4272e31e5439eull -> 0x193ec2cb9a163900ull, rows 0xf840ac1e5c21e71full -> 0x9233c78214742aa3ull, final_hash 0x0020809913835e3dull -> 0xa9bdcf22e28567b2ull +// Random35/1/0: boundary 0x6ce4272e31e5439eull -> 0x193ec2cb9a163900ull, final_hash 0x0020809913835e3dull -> 0xa9bdcf22e28567b2ull +// Random36/0/1: boundary 0xb6ed02402286f1daull -> 0x3e3529f5adea5e8aull, rows 0xb93cb982740efaf2ull -> 0x16d9ee97193355f5ull, final_hash 0x99b1254e54e63283ull -> 0x32a2c44c6f794a8cull +// Random36/0/0: boundary 0xb6ed02402286f1daull -> 0x3e3529f5adea5e8aull, final_hash 0x99b1254e54e63283ull -> 0x32a2c44c6f794a8cull +// Random36/1/1: boundary 0xe4e7726a4db33f47ull -> 0x99c3ad89ead2d6e0ull, rows 0xbbc67cbeef5f3d57ull -> 0x144bd2f3c5b9c0f8ull, final_hash 0xaf91a8392845aa5aull -> 0xe6c283fd13b0660cull +// Random36/1/0: boundary 0xe4e7726a4db33f47ull -> 0x99c3ad89ead2d6e0ull, final_hash 0xaf91a8392845aa5aull -> 0xe6c283fd13b0660cull +// Random37/0/1: boundary 0xf50f40fe8bb210c2ull -> 0x3a89ff92c403a3e7ull, rows 0x6830953a611d9b95ull -> 0xddda8d77310d918full, final_hash 0x218cf254fd19cf8aull -> 0xff9c74898bf3a72aull +// Random37/0/0: boundary 0xf50f40fe8bb210c2ull -> 0x3a89ff92c403a3e7ull, final_hash 0x218cf254fd19cf8aull -> 0xff9c74898bf3a72aull +// Random37/1/1: boundary 0x44783e84e44eaf3bull -> 0x8a36a09ee42999ecull, rows 0x5024d58d139a41c8ull -> 0xf12dd4800e96f492ull, final_hash 0x7d6333767228433aull -> 0x9f14bee9808ee8baull +// Random37/1/0: boundary 0x44783e84e44eaf3bull -> 0x8a36a09ee42999ecull, final_hash 0x7d6333767228433aull -> 0x9f14bee9808ee8baull +// Random38/0/1: boundary 0x9b34eab31c08b2e9ull -> 0x36ed3eee0da888c2ull, rows 0x611d2e69cd985562ull -> 0x2bf58dbe1c5bd9acull, final_hash 0xf756c93d8d25c11cull -> 0x5b82e26291364fc0ull +// Random38/0/0: boundary 0x9b34eab31c08b2e9ull -> 0x36ed3eee0da888c2ull, final_hash 0xf756c93d8d25c11cull -> 0x5b82e26291364fc0ull +// Random38/1/1: boundary 0x6b22f5a3ffc931ceull -> 0xa3225feaf19b98feull, rows 0xa60b5a9b3c472885ull -> 0x59c34402fcf68e2aull, final_hash 0xf2c798a5bba88d39ull -> 0x135197c1ef34cbbdull +// Random38/1/0: boundary 0x6b22f5a3ffc931ceull -> 0xa3225feaf19b98feull, final_hash 0xf2c798a5bba88d39ull -> 0x135197c1ef34cbbdull +// Random39/0/1: boundary 0x271e3d6676599351ull -> 0x9bcdaebb98702055ull, rows 0xd43319fbe54e5588ull -> 0xf4194fbb3f8e63f4ull, final_hash 0xbbf808ba5ac7b67eull -> 0x3026f01eb73dfcdaull +// Random39/0/0: boundary 0x271e3d6676599351ull -> 0x9bcdaebb98702055ull, final_hash 0xbbf808ba5ac7b67eull -> 0x3026f01eb73dfcdaull +// Random39/1/1: boundary 0x4b1fcedefee60801ull -> 0x5fa98522adc27aafull, rows 0xd3eb60fe967c52eeull -> 0x50b22f98ba965993ull, final_hash 0x6842fdc2e3fbde5dull -> 0x123f0c5b09aeae91ull +// Random39/1/0: boundary 0x4b1fcedefee60801ull -> 0x5fa98522adc27aafull, final_hash 0x6842fdc2e3fbde5dull -> 0x123f0c5b09aeae91ull +// Random40/0/1: boundary 0x246be525b446fce2ull -> 0x9f1a94ed263d51dfull, rows 0x9d244e811d5ca386ull -> 0x6f59acfa20c1f416ull, final_hash 0x933f71890476b2bbull -> 0x95af814f9835cb65ull +// Random40/0/0: boundary 0x246be525b446fce2ull -> 0x9f1a94ed263d51dfull, final_hash 0x933f71890476b2bbull -> 0x95af814f9835cb65ull +// Random40/1/1: boundary 0x04841e1d0f981de5ull -> 0xf0451d9a693e54e3ull, rows 0xc3ce4c5ae0eef6cdull -> 0x01721733b1316376ull, final_hash 0x41541580ac45b572ull -> 0x8fdb15e706d2b994ull +// Random40/1/0: boundary 0x04841e1d0f981de5ull -> 0xf0451d9a693e54e3ull, final_hash 0x41541580ac45b572ull -> 0x8fdb15e706d2b994ull +// Random41/0/1: boundary 0x01aece7db4fb0866ull -> 0x6509ace183e7d2cdull, rows 0x1b30dc30f66b89bcull -> 0x957c5244d07600b9ull, final_hash 0x691121b701b14a94ull -> 0xcc1203a69591b134ull +// Random41/0/0: boundary 0x01aece7db4fb0866ull -> 0x6509ace183e7d2cdull, final_hash 0x691121b701b14a94ull -> 0xcc1203a69591b134ull +// Random41/1/1: boundary 0x6a009217c0648447ull -> 0xd2d43ab5fdd2ef0bull, rows 0x94eeabc325d68785ull -> 0xb907bbe1bb2da083ull, final_hash 0x29d310f0bf1d7a89ull -> 0xc55d2d003a5bec69ull +// Random41/1/0: boundary 0x6a009217c0648447ull -> 0xd2d43ab5fdd2ef0bull, final_hash 0x29d310f0bf1d7a89ull -> 0xc55d2d003a5bec69ull +// Random42/0/1: boundary 0x983f3224390b3aa8ull -> 0xa0ee848263988a3dull, rows 0xdd47cc8bbcfac978ull -> 0x1f2ad0a937e24f39ull, final_hash 0xd5c7b24ea3676694ull -> 0xb124b9f52e8cf9f2ull +// Random42/0/0: boundary 0x983f3224390b3aa8ull -> 0xa0ee848263988a3dull, final_hash 0xd5c7b24ea3676694ull -> 0xb124b9f52e8cf9f2ull +// Random42/1/1: boundary 0x5391d71c90732f54ull -> 0x7f6282e75cc72bb0ull, rows 0x95d22736dbced96dull -> 0x23bb6075acaedb2eull, final_hash 0xa3fed64ce5a4b1f9ull -> 0xdb2120d5edab4d9full +// Random42/1/0: boundary 0x5391d71c90732f54ull -> 0x7f6282e75cc72bb0ull, final_hash 0xa3fed64ce5a4b1f9ull -> 0xdb2120d5edab4d9full +// Random43/0/1: boundary 0x3721d2c0c27485e6ull -> 0x83e2926a6d6dec9cull, rows 0x83e3892349cf9593ull -> 0x784d62a813c85f48ull, final_hash 0xeb9aef460c2a6b69ull -> 0x5d4d3ed83aa4608cull +// Random43/0/0: boundary 0x3721d2c0c27485e6ull -> 0x83e2926a6d6dec9cull, final_hash 0xeb9aef460c2a6b69ull -> 0x5d4d3ed83aa4608cull +// Random43/1/1: boundary 0x38e80c0ff09bd6efull -> 0xb9a953e47994593full, rows 0xb48d33783f27e610ull -> 0x879899b5598309eeull, final_hash 0x0584f3141a2ac711ull -> 0xb6e20a498ce1b25aull +// Random43/1/0: boundary 0x38e80c0ff09bd6efull -> 0xb9a953e47994593full, final_hash 0x0584f3141a2ac711ull -> 0xb6e20a498ce1b25aull +// Random44/0/1: boundary 0x40302399ca38ec05ull -> 0xdae57d80c4d2a634ull, rows 0x4bdc4b7a1c4ca040ull -> 0xf7669a1535370da9ull, final_hash 0x55eec4db28687678ull -> 0xc555f0e88b5eb378ull +// Random44/0/0: boundary 0x40302399ca38ec05ull -> 0xdae57d80c4d2a634ull, final_hash 0x55eec4db28687678ull -> 0xc555f0e88b5eb378ull +// Random44/1/1: boundary 0xb8a256af96ab69efull -> 0x360b704190f157adull, rows 0xf56d5278e6ece7caull -> 0x5b2a75cdabd7235cull, final_hash 0x49bc82761efd9785ull -> 0xde36f7f290af13a5ull +// Random44/1/0: boundary 0xb8a256af96ab69efull -> 0x360b704190f157adull, final_hash 0x49bc82761efd9785ull -> 0xde36f7f290af13a5ull +// Random45/0/1: boundary 0x2c707948f442e0a5ull -> 0xbf351c50f4a44f8full, rows 0xe0be91403cc03881ull -> 0xfee900af4573bbc1ull, final_hash 0x49b18e779e62a8fbull -> 0x7f2867c87c78723eull +// Random45/0/0: boundary 0x2c707948f442e0a5ull -> 0xbf351c50f4a44f8full, final_hash 0x49b18e779e62a8fbull -> 0x7f2867c87c78723eull +// Random45/1/1: boundary 0x8c2ae0a418e236daull -> 0x81ba9360ff25c670ull, rows 0xe32f963f031175d8ull -> 0xe345a7e76585b1deull, final_hash 0x6fe2fe4de0b41257ull -> 0x6c13a376f0a1a840ull +// Random45/1/0: boundary 0x8c2ae0a418e236daull -> 0x81ba9360ff25c670ull, final_hash 0x6fe2fe4de0b41257ull -> 0x6c13a376f0a1a840ull +// Random46/0/1: boundary 0xf293162843324a42ull -> 0x3ad713968f2546d9ull, rows 0x2fe9f58c05bd5391ull -> 0x84dd0930114cd829ull, final_hash 0x138273f0186f99a8ull -> 0x6bb382cfb976e148ull +// Random46/0/0: boundary 0xf293162843324a42ull -> 0x3ad713968f2546d9ull, final_hash 0x138273f0186f99a8ull -> 0x6bb382cfb976e148ull +// Random46/1/1: boundary 0x5c518c7c16ad6eb9ull -> 0x997f2e0ee9786d91ull, rows 0x7114f97576e05de1ull -> 0x1ce86cd2c755bea3ull, final_hash 0x02c866a863d14e29ull -> 0x556134f8585397c9ull +// Random46/1/0: boundary 0x5c518c7c16ad6eb9ull -> 0x997f2e0ee9786d91ull, final_hash 0x02c866a863d14e29ull -> 0x556134f8585397c9ull +// Random47/0/1: boundary 0xeb703ef1728b8316ull -> 0x8ce025f147bbdb2dull, rows 0xcf65482f5b03def1ull -> 0x74895c0d69d6b4a8ull, final_hash 0x6d733b355b0b4883ull -> 0x31ec37588708d5f1ull +// Random47/0/0: boundary 0xeb703ef1728b8316ull -> 0x8ce025f147bbdb2dull, final_hash 0x6d733b355b0b4883ull -> 0x31ec37588708d5f1ull +// Random47/1/1: boundary 0x25be4848720c17deull -> 0xebf0544eb982483aull, rows 0xb3c87f095d3d086dull -> 0xca410d7365dca790ull, final_hash 0xc9b10c1db4c2a51aull -> 0xa8aff582382caf48ull +// Random47/1/0: boundary 0x25be4848720c17deull -> 0xebf0544eb982483aull, final_hash 0xc9b10c1db4c2a51aull -> 0xa8aff582382caf48ull +// Random48/0/1: boundary 0xd0c92fe6fc290e0bull -> 0xaa6b18e4254e2f68ull, rows 0x0af2126a53f8f7b9ull -> 0x2f80186c69547a10ull, final_hash 0x840184ac798a5de2ull -> 0xaff43b38e6bc02acull +// 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 constexpr Pinned kPinned[] = { - {"Quiet", 0, 1, 0xc1c15d580d2b91ddull, 0x6f2ef9fb0f46bdb6ull, 0x47fe0d7eaf8e51e3ull, 0xfd89dcb68ffd9f6full, 40, 40, 0}, - {"Quiet", 0, 0, 0xc1c15d580d2b91ddull, 0x14650fb0739d0383ull, 0x47fe0d7eaf8e51e3ull, 0xfd89dcb68ffd9f6full, 40, 0, 0}, - {"Quiet", 1, 1, 0x25925b8843f8c940ull, 0x3f01d574c39a983full, 0x47fe0d7eaf8e51e3ull, 0x08472c64f2a3b409ull, 40, 40, 0}, - {"Quiet", 1, 0, 0x25925b8843f8c940ull, 0x14650fb0739d0383ull, 0x47fe0d7eaf8e51e3ull, 0x08472c64f2a3b409ull, 40, 0, 0}, - {"HoldLongFull", 0, 1, 0xf7ee6fe81728a11eull, 0x245fab3eff4d7559ull, 0xa32454de68248651ull, 0x68a09e458dc41ed9ull, 40, 40, 1}, - {"HoldLongFull", 0, 0, 0xf7ee6fe81728a11eull, 0x14650fb0739d0383ull, 0xa32454de68248651ull, 0x68a09e458dc41ed9ull, 40, 0, 1}, - {"HoldLongFull", 1, 1, 0x74ba83122973ae7bull, 0xa773cd9e004bc143ull, 0xa32454de68248651ull, 0xf7d8d3d0b26bcfb1ull, 40, 40, 1}, - {"HoldLongFull", 1, 0, 0x74ba83122973ae7bull, 0x14650fb0739d0383ull, 0xa32454de68248651ull, 0xf7d8d3d0b26bcfb1ull, 40, 0, 1}, - {"MarginCalls", 0, 1, 0xf01f77fadacf9b1bull, 0x720c4e8e6c1151d7ull, 0x259f52c0c76aafe9ull, 0x1423cb11eff138efull, 36, 36, 3}, - {"MarginCalls", 0, 0, 0xf01f77fadacf9b1bull, 0x14650fb0739d0383ull, 0x259f52c0c76aafe9ull, 0x1423cb11eff138efull, 36, 0, 3}, - {"MarginCalls", 1, 1, 0x04552ef5dd6b0248ull, 0x34a90870ea8ff4dfull, 0x92cc9a49e414ed25ull, 0x39b0b7d450ea1d23ull, 36, 36, 2}, - {"MarginCalls", 1, 0, 0x04552ef5dd6b0248ull, 0x14650fb0739d0383ull, 0x92cc9a49e414ed25ull, 0x39b0b7d450ea1d23ull, 36, 0, 2}, - {"MarginCallCycles", 0, 1, 0xe517ae23a9d9cd84ull, 0x09d31e44e56049a0ull, 0x657a702082e43f34ull, 0xd6e5d9e3761d70f7ull, 64, 64, 1}, - {"MarginCallCycles", 0, 0, 0xe517ae23a9d9cd84ull, 0x14650fb0739d0383ull, 0x657a702082e43f34ull, 0xd6e5d9e3761d70f7ull, 64, 0, 1}, - {"MarginCallCycles", 1, 1, 0x74fb06c3e47e091eull, 0x3015bbe850ee059dull, 0x9b23f7fef31dec0cull, 0x1a3d548af01065b8ull, 64, 64, 3}, - {"MarginCallCycles", 1, 0, 0x74fb06c3e47e091eull, 0x14650fb0739d0383ull, 0x9b23f7fef31dec0cull, 0x1a3d548af01065b8ull, 64, 0, 3}, - {"Bracket", 0, 1, 0xad121243d42812f0ull, 0xc34f99dbe945355dull, 0xc17c8a18143849dcull, 0xe393d0e31b6c86dbull, 48, 48, 1}, - {"Bracket", 0, 0, 0xad121243d42812f0ull, 0x14650fb0739d0383ull, 0xc17c8a18143849dcull, 0xe393d0e31b6c86dbull, 48, 0, 1}, - {"Bracket", 1, 1, 0x42b52abab2bf3209ull, 0xee291f58ffb7864dull, 0xd474aeacc0bce022ull, 0xa953041c662027b9ull, 48, 48, 1}, - {"Bracket", 1, 0, 0x42b52abab2bf3209ull, 0x14650fb0739d0383ull, 0xd474aeacc0bce022ull, 0xa953041c662027b9ull, 48, 0, 1}, - {"Trail", 0, 1, 0x38951256c0c8ae91ull, 0x630e44444886533full, 0xd244c7dc0fdc94a1ull, 0x0a3d59197f3fbeeeull, 48, 48, 1}, - {"Trail", 0, 0, 0x38951256c0c8ae91ull, 0x14650fb0739d0383ull, 0xd244c7dc0fdc94a1ull, 0x0a3d59197f3fbeeeull, 48, 0, 1}, - {"Trail", 1, 1, 0x85f94fedc6115fe3ull, 0x100ea0b5c2385430ull, 0xd244c7dc0fdc94a1ull, 0x913d53654651ef64ull, 48, 48, 1}, - {"Trail", 1, 0, 0x85f94fedc6115fe3ull, 0x14650fb0739d0383ull, 0xd244c7dc0fdc94a1ull, 0x913d53654651ef64ull, 48, 0, 1}, - {"DualStops", 0, 1, 0xbe6893046a46cb96ull, 0x95b5ee2aec79ec7cull, 0xb19c9aea35851016ull, 0x3ebb8ec1e50f9ed1ull, 40, 40, 1}, - {"DualStops", 0, 0, 0xbe6893046a46cb96ull, 0x14650fb0739d0383ull, 0xb19c9aea35851016ull, 0x3ebb8ec1e50f9ed1ull, 40, 0, 1}, - {"DualStops", 1, 1, 0xebbf122a6c461306ull, 0xb4bc6d57975026c4ull, 0xb19c9aea35851016ull, 0x7ee6f826d7537cc1ull, 40, 40, 1}, - {"DualStops", 1, 0, 0xebbf122a6c461306ull, 0x14650fb0739d0383ull, 0xb19c9aea35851016ull, 0x7ee6f826d7537cc1ull, 40, 0, 1}, - {"InvertedStops", 0, 1, 0x049f870d696c86d6ull, 0x6c97ddc3cc3bc830ull, 0x089245abb04b9cb7ull, 0x92d980a9427423eeull, 40, 40, 4}, - {"InvertedStops", 0, 0, 0x049f870d696c86d6ull, 0x14650fb0739d0383ull, 0x089245abb04b9cb7ull, 0x92d980a9427423eeull, 40, 0, 4}, - {"InvertedStops", 1, 1, 0x77643e8c893982c3ull, 0xfaede1fa4ce13f90ull, 0x089245abb04b9cb7ull, 0x18c13857d9dfa263ull, 40, 40, 4}, - {"InvertedStops", 1, 0, 0x77643e8c893982c3ull, 0x14650fb0739d0383ull, 0x089245abb04b9cb7ull, 0x18c13857d9dfa263ull, 40, 0, 4}, - {"PendingParent", 0, 1, 0x6721cf5983214d9eull, 0xc139da1140ad7563ull, 0x47fe0d7eaf8e51e3ull, 0xab2eaed98b0f009aull, 40, 40, 0}, - {"PendingParent", 0, 0, 0x6721cf5983214d9eull, 0x14650fb0739d0383ull, 0x47fe0d7eaf8e51e3ull, 0xab2eaed98b0f009aull, 40, 0, 0}, - {"PendingParent", 1, 1, 0x298f49712d4af19aull, 0xa624ebd2d2ec64a5ull, 0x47fe0d7eaf8e51e3ull, 0x660fa440f23c5696ull, 40, 40, 0}, - {"PendingParent", 1, 0, 0x298f49712d4af19aull, 0x14650fb0739d0383ull, 0x47fe0d7eaf8e51e3ull, 0x660fa440f23c5696ull, 40, 0, 0}, - {"GappedAdd", 0, 1, 0x801838fc90033739ull, 0x8259b732e34ac913ull, 0x6528238d0771c54eull, 0x58b9e653d25a4647ull, 40, 40, 2}, - {"GappedAdd", 0, 0, 0x801838fc90033739ull, 0x14650fb0739d0383ull, 0x6528238d0771c54eull, 0x58b9e653d25a4647ull, 40, 0, 2}, - {"GappedAdd", 1, 1, 0x8429bf8f9c179793ull, 0xa8491ce8abf7d3ffull, 0x6528238d0771c54eull, 0x803cb88784ce9026ull, 40, 40, 2}, - {"GappedAdd", 1, 0, 0x8429bf8f9c179793ull, 0x14650fb0739d0383ull, 0x6528238d0771c54eull, 0x803cb88784ce9026ull, 40, 0, 2}, - {"DeclinedReversal", 0, 1, 0xd0083ac2e7a9723eull, 0x8567c0614fd76d26ull, 0xc3448bef59024d51ull, 0x8f1c4a1a41f5a3bcull, 24, 24, 1}, - {"DeclinedReversal", 0, 0, 0xd0083ac2e7a9723eull, 0x14650fb0739d0383ull, 0xc3448bef59024d51ull, 0x8f1c4a1a41f5a3bcull, 24, 0, 1}, - {"DeclinedReversal", 1, 1, 0x0fd76da84e65f1c0ull, 0x1eb7bc54208e0534ull, 0xc3448bef59024d51ull, 0x22d70277dc1fc0a4ull, 24, 24, 1}, - {"DeclinedReversal", 1, 0, 0x0fd76da84e65f1c0ull, 0x14650fb0739d0383ull, 0xc3448bef59024d51ull, 0x22d70277dc1fc0a4ull, 24, 0, 1}, - {"CalcOnFills", 0, 1, 0xdd17a201bb4d06b2ull, 0x52de59217319fd8aull, 0x4603f8c446ca5c1eull, 0xfe5dc3ec475477d9ull, 57, 40, 10}, - {"CalcOnFills", 0, 0, 0xdd17a201bb4d06b2ull, 0x14650fb0739d0383ull, 0x4603f8c446ca5c1eull, 0xfe5dc3ec475477d9ull, 57, 0, 10}, - {"CalcOnFills", 1, 1, 0xaa733db98dd5e688ull, 0x07c506ff68fc1a7bull, 0x57aa00467e66c075ull, 0xcd17129763baf911ull, 54, 40, 8}, - {"CalcOnFills", 1, 0, 0xaa733db98dd5e688ull, 0x14650fb0739d0383ull, 0x57aa00467e66c075ull, 0xcd17129763baf911ull, 54, 0, 8}, - {"CloseOrders", 0, 1, 0x31b0c8d9e01f772full, 0x09c999edd76d4fdaull, 0x36ebbdb119ff2815ull, 0x2d39ec28cc86f100ull, 48, 48, 16}, - {"CloseOrders", 0, 0, 0x31b0c8d9e01f772full, 0x14650fb0739d0383ull, 0x36ebbdb119ff2815ull, 0x2d39ec28cc86f100ull, 48, 0, 16}, - {"CloseOrders", 1, 1, 0xf65b916be995d95full, 0x755bccd5cafc099eull, 0x8261dcdcaf8f5751ull, 0x6281f01aff005efeull, 48, 48, 16}, - {"CloseOrders", 1, 0, 0xf65b916be995d95full, 0x14650fb0739d0383ull, 0x8261dcdcaf8f5751ull, 0x6281f01aff005efeull, 48, 0, 16}, - {"CloseOrdersSlipped", 0, 1, 0xf41f34b44f14ed1bull, 0x88cca9165f3f9ca5ull, 0x0f201395d3fe211full, 0x184081a51dee14b6ull, 36, 36, 2}, - {"CloseOrdersSlipped", 0, 0, 0xf41f34b44f14ed1bull, 0x14650fb0739d0383ull, 0x0f201395d3fe211full, 0x184081a51dee14b6ull, 36, 0, 2}, - {"CloseOrdersSlipped", 1, 1, 0xefc7d84261df23b7ull, 0x0952c5f9a055c769ull, 0x0f201395d3fe211full, 0xdbb9a359df5b7ba7ull, 36, 36, 2}, - {"CloseOrdersSlipped", 1, 0, 0xefc7d84261df23b7ull, 0x14650fb0739d0383ull, 0x0f201395d3fe211full, 0xdbb9a359df5b7ba7ull, 36, 0, 2}, - {"IntradayLoss", 0, 1, 0x45d88fb7f59423e4ull, 0xf597c17beff44c38ull, 0x60c9f061d60a69edull, 0xb18331055f670401ull, 40, 40, 1}, - {"IntradayLoss", 0, 0, 0x45d88fb7f59423e4ull, 0x14650fb0739d0383ull, 0x60c9f061d60a69edull, 0xb18331055f670401ull, 40, 0, 1}, - {"IntradayLoss", 1, 1, 0xdccf9450e8e7c00cull, 0x488f8a32366e0cafull, 0x60c9f061d60a69edull, 0x08be7263bb9faf0eull, 40, 40, 1}, - {"IntradayLoss", 1, 0, 0xdccf9450e8e7c00cull, 0x14650fb0739d0383ull, 0x60c9f061d60a69edull, 0x08be7263bb9faf0eull, 40, 0, 1}, - {"IntradayCapDeferred", 0, 1, 0x477d0d155ce99044ull, 0xd36b92908117e322ull, 0x2fa61401cccb3845ull, 0xd7b1c15ee8d83724ull, 24, 24, 1}, - {"IntradayCapDeferred", 0, 0, 0x477d0d155ce99044ull, 0x14650fb0739d0383ull, 0x2fa61401cccb3845ull, 0xd7b1c15ee8d83724ull, 24, 0, 1}, - {"IntradayCapDeferred", 1, 1, 0x1b27e4ab14777901ull, 0xbc24b0cb7453804bull, 0x6517fe8ea7437b74ull, 0x58271303a14cab9bull, 24, 24, 1}, - {"IntradayCapDeferred", 1, 0, 0x1b27e4ab14777901ull, 0x14650fb0739d0383ull, 0x6517fe8ea7437b74ull, 0x58271303a14cab9bull, 24, 0, 1}, - {"PreopenSlice", 0, 1, 0x85bac68b85fa1b10ull, 0xba4b398db082d988ull, 0x96bb4615ab0ad33bull, 0xc4645f9196ec1c52ull, 36, 36, 1}, - {"PreopenSlice", 0, 0, 0x85bac68b85fa1b10ull, 0x14650fb0739d0383ull, 0x96bb4615ab0ad33bull, 0xc4645f9196ec1c52ull, 36, 0, 1}, - {"PreopenSlice", 1, 1, 0x3c681ec4f8ea88e9ull, 0xcfdbbdabfcceb515ull, 0x96bb4615ab0ad33bull, 0x43fed91080dfbea0ull, 36, 36, 1}, - {"PreopenSlice", 1, 0, 0x3c681ec4f8ea88e9ull, 0x14650fb0739d0383ull, 0x96bb4615ab0ad33bull, 0x43fed91080dfbea0ull, 36, 0, 1}, - {"SameBarBatch", 0, 1, 0xd8c6014359e3436dull, 0x56ce4bb7913dd63dull, 0xa1c83f47f2e97c0dull, 0x1e3f3c2b9879352full, 36, 36, 8}, - {"SameBarBatch", 0, 0, 0xd8c6014359e3436dull, 0x14650fb0739d0383ull, 0xa1c83f47f2e97c0dull, 0x1e3f3c2b9879352full, 36, 0, 8}, - {"SameBarBatch", 1, 1, 0xb74c80da185d6f7cull, 0xd3659fc4f7228afcull, 0xa1c83f47f2e97c0dull, 0xcc0463b3d75fe13cull, 36, 36, 8}, - {"SameBarBatch", 1, 0, 0xb74c80da185d6f7cull, 0x14650fb0739d0383ull, 0xa1c83f47f2e97c0dull, 0xcc0463b3d75fe13cull, 36, 0, 8}, - {"CommissionedShort", 0, 1, 0xb3b9a9d400a706b7ull, 0xe36126265b8bf576ull, 0x35b634c9ced2d3caull, 0x23caa9e57f9f6fb8ull, 32, 32, 1}, - {"CommissionedShort", 0, 0, 0xb3b9a9d400a706b7ull, 0x14650fb0739d0383ull, 0x35b634c9ced2d3caull, 0x23caa9e57f9f6fb8ull, 32, 0, 1}, - {"CommissionedShort", 1, 1, 0x1fba48a99176b809ull, 0x8599d32cc4e3f365ull, 0x35b634c9ced2d3caull, 0xc1bb33f8955d526dull, 32, 32, 1}, - {"CommissionedShort", 1, 0, 0x1fba48a99176b809ull, 0x14650fb0739d0383ull, 0x35b634c9ced2d3caull, 0xc1bb33f8955d526dull, 32, 0, 1}, - {"AccountFx", 0, 1, 0x7ea7cc0696e4e706ull, 0x5d06a315cd828e3aull, 0x3b853d0c008c3078ull, 0xf3a8b7577af41cbcull, 48, 48, 1}, - {"AccountFx", 0, 0, 0x7ea7cc0696e4e706ull, 0x14650fb0739d0383ull, 0x3b853d0c008c3078ull, 0xf3a8b7577af41cbcull, 48, 0, 1}, - {"Stream", 0, 1, 0xb558b6c7840b2228ull, 0x34ad20d2694b5c4eull, 0xce38e7485b22ec04ull, 0xc9c5e2681a211fdaull, 40, 40, 5}, - {"Stream", 0, 0, 0xb558b6c7840b2228ull, 0x14650fb0739d0383ull, 0xce38e7485b22ec04ull, 0xc9c5e2681a211fdaull, 40, 0, 5}, - {"Random01", 0, 1, 0x7b26a7a4fd8ca837ull, 0x8b38d2c0ee3ae09bull, 0x5f25a36c2031c262ull, 0x266b3c97e2229932ull, 55, 55, 15}, - {"Random01", 0, 0, 0x7b26a7a4fd8ca837ull, 0x14650fb0739d0383ull, 0x5f25a36c2031c262ull, 0x266b3c97e2229932ull, 55, 0, 15}, - {"Random01", 1, 1, 0x240a7d06f5858192ull, 0x5a4bde6c69234036ull, 0x81d6c3334c3a4ed0ull, 0xc2da049b01eede0eull, 55, 55, 15}, - {"Random01", 1, 0, 0x240a7d06f5858192ull, 0x14650fb0739d0383ull, 0x81d6c3334c3a4ed0ull, 0xc2da049b01eede0eull, 55, 0, 15}, - {"Random02", 0, 1, 0xf50e670c28558c70ull, 0xc237af8c6ea5d8a0ull, 0xec986a4dcdf52208ull, 0x785608d44d074ae6ull, 58, 40, 12}, - {"Random02", 0, 0, 0xf50e670c28558c70ull, 0x14650fb0739d0383ull, 0xec986a4dcdf52208ull, 0x785608d44d074ae6ull, 58, 0, 12}, - {"Random02", 1, 1, 0x32b5473f018894caull, 0xbbb6be89045a263dull, 0x95d130f42b9a5776ull, 0x537ce6356efa7dcdull, 58, 40, 12}, - {"Random02", 1, 0, 0x32b5473f018894caull, 0x14650fb0739d0383ull, 0x95d130f42b9a5776ull, 0x537ce6356efa7dcdull, 58, 0, 12}, - {"Random03", 0, 1, 0x4de9d3289f070b10ull, 0xc623f8fada361795ull, 0x922d2ca744f5f225ull, 0xbc66b161650e945aull, 44, 44, 12}, - {"Random03", 0, 0, 0x4de9d3289f070b10ull, 0x14650fb0739d0383ull, 0x922d2ca744f5f225ull, 0xbc66b161650e945aull, 44, 0, 12}, - {"Random03", 1, 1, 0x74c92954e88611faull, 0xff0a6422eb94899eull, 0xe213d492a23dfa58ull, 0x66e224895c00a770ull, 44, 44, 12}, - {"Random03", 1, 0, 0x74c92954e88611faull, 0x14650fb0739d0383ull, 0xe213d492a23dfa58ull, 0x66e224895c00a770ull, 44, 0, 12}, - {"Random04", 0, 1, 0xc803c2750b61157dull, 0x32e6028cb8a54dc8ull, 0xc687874028817a5bull, 0x84461297f142b9efull, 44, 44, 18}, - {"Random04", 0, 0, 0xc803c2750b61157dull, 0x14650fb0739d0383ull, 0xc687874028817a5bull, 0x84461297f142b9efull, 44, 0, 18}, - {"Random04", 1, 1, 0xaf56d9261eb50536ull, 0xfece30308981fb23ull, 0x37389353e6ac8baeull, 0xcd21899a90f1d564ull, 44, 44, 18}, - {"Random04", 1, 0, 0xaf56d9261eb50536ull, 0x14650fb0739d0383ull, 0x37389353e6ac8baeull, 0xcd21899a90f1d564ull, 44, 0, 18}, - {"Random05", 0, 1, 0xc8efa2a84661d593ull, 0x90b1a6e9fbe4e13full, 0xae3b2415c28fc5cbull, 0x2dd0cf2ce3ea761eull, 58, 53, 3}, - {"Random05", 0, 0, 0xc8efa2a84661d593ull, 0x14650fb0739d0383ull, 0xae3b2415c28fc5cbull, 0x2dd0cf2ce3ea761eull, 58, 0, 3}, - {"Random05", 1, 1, 0x18519fd8ab2ecf78ull, 0x967a69d448f6b43aull, 0x6b60d1d77a27186full, 0x1733e9f06b55202cull, 66, 53, 10}, - {"Random05", 1, 0, 0x18519fd8ab2ecf78ull, 0x14650fb0739d0383ull, 0x6b60d1d77a27186full, 0x1733e9f06b55202cull, 66, 0, 10}, - {"Random06", 0, 1, 0x2bfc102809cb61b6ull, 0xe71623e0f3912192ull, 0xb724c9aadc73f827ull, 0xbe2d610924b8c00dull, 46, 46, 13}, - {"Random06", 0, 0, 0x2bfc102809cb61b6ull, 0x14650fb0739d0383ull, 0xb724c9aadc73f827ull, 0xbe2d610924b8c00dull, 46, 0, 13}, - {"Random06", 1, 1, 0xe00ef146710c5f72ull, 0xf468c8529e501c5full, 0xb1c8ba35c0dd393dull, 0x814550b3d8ab202aull, 46, 46, 13}, - {"Random06", 1, 0, 0xe00ef146710c5f72ull, 0x14650fb0739d0383ull, 0xb1c8ba35c0dd393dull, 0x814550b3d8ab202aull, 46, 0, 13}, - {"Random07", 0, 1, 0x4a60ce0b1c4fb021ull, 0xd3043f597a7a6f7eull, 0x93cafb8ccdf82d3full, 0xbfd9ebdfdd9a9abbull, 46, 46, 12}, - {"Random07", 0, 0, 0x4a60ce0b1c4fb021ull, 0x14650fb0739d0383ull, 0x93cafb8ccdf82d3full, 0xbfd9ebdfdd9a9abbull, 46, 0, 12}, - {"Random07", 1, 1, 0x8f487efcaf6960daull, 0x9ddf86f7c192467bull, 0xf516d07702627571ull, 0xb858f3c326ae5bebull, 46, 46, 12}, - {"Random07", 1, 0, 0x8f487efcaf6960daull, 0x14650fb0739d0383ull, 0xf516d07702627571ull, 0xb858f3c326ae5bebull, 46, 0, 12}, - {"Random08", 0, 1, 0xdf32ef61cd4e19eeull, 0xfd32cff83453f293ull, 0x1d5e1165b05daf8full, 0x23acd097ec38f43aull, 93, 59, 24}, - {"Random08", 0, 0, 0xdf32ef61cd4e19eeull, 0x14650fb0739d0383ull, 0x1d5e1165b05daf8full, 0x23acd097ec38f43aull, 93, 0, 24}, - {"Random08", 1, 1, 0x869ee3e727ef0b05ull, 0x64f491f787b05affull, 0x7f7cc33b43e81caeull, 0x11144c9c51cb89f3ull, 93, 59, 24}, - {"Random08", 1, 0, 0x869ee3e727ef0b05ull, 0x14650fb0739d0383ull, 0x7f7cc33b43e81caeull, 0x11144c9c51cb89f3ull, 93, 0, 24}, - {"Random09", 0, 1, 0xf123ed994915e172ull, 0xe262ad1d42b22aa2ull, 0x8abae7302d7911beull, 0x24d07dfd4c311e09ull, 58, 58, 2}, - {"Random09", 0, 0, 0xf123ed994915e172ull, 0x14650fb0739d0383ull, 0x8abae7302d7911beull, 0x24d07dfd4c311e09ull, 58, 0, 2}, - {"Random09", 1, 1, 0x28f03f19f3977a71ull, 0x29fa1939a71c5214ull, 0x99aae055f2d8091dull, 0xf22f979ee11a143full, 58, 58, 2}, - {"Random09", 1, 0, 0x28f03f19f3977a71ull, 0x14650fb0739d0383ull, 0x99aae055f2d8091dull, 0xf22f979ee11a143full, 58, 0, 2}, - {"Random10", 0, 1, 0xb3861d80e682c693ull, 0xe5b5b2919d650a05ull, 0x70ed423d22403976ull, 0x92408fc414e06780ull, 71, 51, 24}, - {"Random10", 0, 0, 0xb3861d80e682c693ull, 0x14650fb0739d0383ull, 0x70ed423d22403976ull, 0x92408fc414e06780ull, 71, 0, 24}, - {"Random10", 1, 1, 0x69fe10c28684def0ull, 0x10dfb593a8ec827cull, 0x2d930b1932bc89bdull, 0x6a8371ba10724f52ull, 68, 51, 23}, - {"Random10", 1, 0, 0x69fe10c28684def0ull, 0x14650fb0739d0383ull, 0x2d930b1932bc89bdull, 0x6a8371ba10724f52ull, 68, 0, 23}, - {"Random11", 0, 1, 0x90100a03a86d0cdbull, 0x016b0db6bef2264cull, 0x2f92b1af8d2cfafcull, 0x185f0857180c5a28ull, 60, 60, 3}, - {"Random11", 0, 0, 0x90100a03a86d0cdbull, 0x14650fb0739d0383ull, 0x2f92b1af8d2cfafcull, 0x185f0857180c5a28ull, 60, 0, 3}, - {"Random11", 1, 1, 0x84170dd33baa79d9ull, 0x1a59bcbe0a910135ull, 0xad6d6d2577cca5c9ull, 0x41861834717a9917ull, 60, 60, 8}, - {"Random11", 1, 0, 0x84170dd33baa79d9ull, 0x14650fb0739d0383ull, 0xad6d6d2577cca5c9ull, 0x41861834717a9917ull, 60, 0, 8}, - {"Random12", 0, 1, 0x3d418ef44da4cd2eull, 0xe892e674e3e6e0caull, 0x9ec5e7b69521e874ull, 0x4e9bd726c9f377b7ull, 92, 62, 22}, - {"Random12", 0, 0, 0x3d418ef44da4cd2eull, 0x14650fb0739d0383ull, 0x9ec5e7b69521e874ull, 0x4e9bd726c9f377b7ull, 92, 0, 22}, - {"Random12", 1, 1, 0x1fc620580d6c6a8cull, 0xba2f51ae1c4ffcaeull, 0xf2c6cada55accbe3ull, 0x5209c01b2d9075bbull, 93, 62, 22}, - {"Random12", 1, 0, 0x1fc620580d6c6a8cull, 0x14650fb0739d0383ull, 0xf2c6cada55accbe3ull, 0x5209c01b2d9075bbull, 93, 0, 22}, - {"Random13", 0, 1, 0xc330ac11cbbddd41ull, 0xe0a3d24e78674f95ull, 0xb5e34ee7b3268a86ull, 0xefab5484e3574594ull, 60, 60, 16}, - {"Random13", 0, 0, 0xc330ac11cbbddd41ull, 0x14650fb0739d0383ull, 0xb5e34ee7b3268a86ull, 0xefab5484e3574594ull, 60, 0, 16}, - {"Random13", 1, 1, 0xcdb3c6546824667dull, 0x653d5c4469730325ull, 0xab774513033e4b33ull, 0x027984836057c1fbull, 60, 60, 16}, - {"Random13", 1, 0, 0xcdb3c6546824667dull, 0x14650fb0739d0383ull, 0xab774513033e4b33ull, 0x027984836057c1fbull, 60, 0, 16}, - {"Random14", 0, 1, 0x1bc30bb176abfe75ull, 0x38ac385d83173e6dull, 0xbcda0382be163781ull, 0x240563decf410a07ull, 49, 49, 17}, - {"Random14", 0, 0, 0x1bc30bb176abfe75ull, 0x14650fb0739d0383ull, 0xbcda0382be163781ull, 0x240563decf410a07ull, 49, 0, 17}, - {"Random14", 1, 1, 0xf70698eaf69fe0f7ull, 0x32373b160f2cb0a4ull, 0x8bca41c28a85c67aull, 0x2a207608d53d6e31ull, 49, 49, 17}, - {"Random14", 1, 0, 0xf70698eaf69fe0f7ull, 0x14650fb0739d0383ull, 0x8bca41c28a85c67aull, 0x2a207608d53d6e31ull, 49, 0, 17}, - {"Random15", 0, 1, 0x8d19883bfd7b9827ull, 0x617a8b521ffd19a8ull, 0xa729d04861d8f487ull, 0xc5c912afdfb5429eull, 42, 42, 11}, - {"Random15", 0, 0, 0x8d19883bfd7b9827ull, 0x14650fb0739d0383ull, 0xa729d04861d8f487ull, 0xc5c912afdfb5429eull, 42, 0, 11}, - {"Random15", 1, 1, 0xce37c225571916fbull, 0x5ea9fcc2c1385c3full, 0xeabf56d064294b39ull, 0xf1a244775b55839dull, 42, 42, 11}, - {"Random15", 1, 0, 0xce37c225571916fbull, 0x14650fb0739d0383ull, 0xeabf56d064294b39ull, 0xf1a244775b55839dull, 42, 0, 11}, - {"Random16", 0, 1, 0x41c794238409bacbull, 0xe3000785740c45c4ull, 0xf751fbce2a822df8ull, 0xdbb1c1b8835a2f52ull, 54, 54, 17}, - {"Random16", 0, 0, 0x41c794238409bacbull, 0x14650fb0739d0383ull, 0xf751fbce2a822df8ull, 0xdbb1c1b8835a2f52ull, 54, 0, 17}, - {"Random16", 1, 1, 0xb469144e85e0a627ull, 0x5adace02ce0745bcull, 0xeb8b68e57975e307ull, 0xd6a03051ba6c30efull, 54, 54, 17}, - {"Random16", 1, 0, 0xb469144e85e0a627ull, 0x14650fb0739d0383ull, 0xeb8b68e57975e307ull, 0xd6a03051ba6c30efull, 54, 0, 17}, - {"Random17", 0, 1, 0x6304fa3c28e07e49ull, 0xf1ed3675ad25ec11ull, 0xf5006d7b3dcd5c43ull, 0xfda9c1caff36448bull, 49, 47, 1}, - {"Random17", 0, 0, 0x6304fa3c28e07e49ull, 0x14650fb0739d0383ull, 0xf5006d7b3dcd5c43ull, 0xfda9c1caff36448bull, 49, 0, 1}, - {"Random17", 1, 1, 0x91c41f4b53c60db8ull, 0x4927f647a64a6aa1ull, 0x364d848056b18418ull, 0xb386dd8b4f22a827ull, 50, 47, 2}, - {"Random17", 1, 0, 0x91c41f4b53c60db8ull, 0x14650fb0739d0383ull, 0x364d848056b18418ull, 0xb386dd8b4f22a827ull, 50, 0, 2}, - {"Random18", 0, 1, 0x2f90853903ca8075ull, 0x9614b30d8372e1b6ull, 0x946751d8a6dd7292ull, 0x827427f9640b4e97ull, 66, 62, 3}, - {"Random18", 0, 0, 0x2f90853903ca8075ull, 0x14650fb0739d0383ull, 0x946751d8a6dd7292ull, 0x827427f9640b4e97ull, 66, 0, 3}, - {"Random18", 1, 1, 0xd759ce46e3583dd6ull, 0x4d62656618897636ull, 0x946751d8a6dd7292ull, 0x35a46c04db974779ull, 66, 62, 3}, - {"Random18", 1, 0, 0xd759ce46e3583dd6ull, 0x14650fb0739d0383ull, 0x946751d8a6dd7292ull, 0x35a46c04db974779ull, 66, 0, 3}, - {"Random19", 0, 1, 0x0829e86674bb90a3ull, 0x0c808692940a05a0ull, 0xcfd0b979ead5a43cull, 0x26e403c4671fad9full, 40, 40, 10}, - {"Random19", 0, 0, 0x0829e86674bb90a3ull, 0x14650fb0739d0383ull, 0xcfd0b979ead5a43cull, 0x26e403c4671fad9full, 40, 0, 10}, - {"Random19", 1, 1, 0x62aea0c1de6a6c13ull, 0xf4f4a620ee4a7ab5ull, 0x011bec7774b82ec9ull, 0xb4aa06caa7722a48ull, 40, 40, 11}, - {"Random19", 1, 0, 0x62aea0c1de6a6c13ull, 0x14650fb0739d0383ull, 0x011bec7774b82ec9ull, 0xb4aa06caa7722a48ull, 40, 0, 11}, - {"Random20", 0, 1, 0x8caa054b399b232aull, 0x4fc0f37c9645d067ull, 0x35a6da11cd35b29cull, 0x9facee5fae21aa7cull, 70, 57, 21}, - {"Random20", 0, 0, 0x8caa054b399b232aull, 0x14650fb0739d0383ull, 0x35a6da11cd35b29cull, 0x9facee5fae21aa7cull, 70, 0, 21}, - {"Random20", 1, 1, 0x0bed3f4e6ae2ef8full, 0xbc77d10bf614561dull, 0xafad93e39c8617d9ull, 0x0d9fbc941287b1cfull, 72, 57, 21}, - {"Random20", 1, 0, 0x0bed3f4e6ae2ef8full, 0x14650fb0739d0383ull, 0xafad93e39c8617d9ull, 0x0d9fbc941287b1cfull, 72, 0, 21}, - {"Random21", 0, 1, 0x546f79d5d3874035ull, 0x57db503858f2c920ull, 0x7d29f9137d1bc337ull, 0x6a7c93c51185957aull, 54, 54, 18}, - {"Random21", 0, 0, 0x546f79d5d3874035ull, 0x14650fb0739d0383ull, 0x7d29f9137d1bc337ull, 0x6a7c93c51185957aull, 54, 0, 18}, - {"Random21", 1, 1, 0x61ab9c4cf365da62ull, 0x6034eb0b07bc00b6ull, 0x32f61d041209dc84ull, 0x8a67a9fe7abd9c00ull, 54, 54, 18}, - {"Random21", 1, 0, 0x61ab9c4cf365da62ull, 0x14650fb0739d0383ull, 0x32f61d041209dc84ull, 0x8a67a9fe7abd9c00ull, 54, 0, 18}, - {"Random22", 0, 1, 0x7264962e97a25a80ull, 0x744915b658d1f5d1ull, 0x603c10590ec68778ull, 0x1a6bcb392d46b371ull, 68, 49, 13}, - {"Random22", 0, 0, 0x7264962e97a25a80ull, 0x14650fb0739d0383ull, 0x603c10590ec68778ull, 0x1a6bcb392d46b371ull, 68, 0, 13}, - {"Random22", 1, 1, 0x47d74c02fc2519e4ull, 0x7443f9b7143dacd2ull, 0x29259a278cc3600bull, 0xa04d40cbbd623427ull, 68, 49, 13}, - {"Random22", 1, 0, 0x47d74c02fc2519e4ull, 0x14650fb0739d0383ull, 0x29259a278cc3600bull, 0xa04d40cbbd623427ull, 68, 0, 13}, - {"Random23", 0, 1, 0xfa4d25cb6d9764cbull, 0xdea9813bde333195ull, 0xf0e62f804df9e022ull, 0x5058d81da9e2cf89ull, 56, 54, 1}, - {"Random23", 0, 0, 0xfa4d25cb6d9764cbull, 0x14650fb0739d0383ull, 0xf0e62f804df9e022ull, 0x5058d81da9e2cf89ull, 56, 0, 1}, - {"Random23", 1, 1, 0xd2626d74d1ddbe27ull, 0x55431bc925b28038ull, 0xf0e62f804df9e022ull, 0x510aa1745bdba1c7ull, 56, 54, 1}, - {"Random23", 1, 0, 0xd2626d74d1ddbe27ull, 0x14650fb0739d0383ull, 0xf0e62f804df9e022ull, 0x510aa1745bdba1c7ull, 56, 0, 1}, - {"Random24", 0, 1, 0x1972ae0bc903694dull, 0x2b240d2a365b8703ull, 0xcdafd09a1c076093ull, 0x82e72364e836634full, 60, 60, 16}, - {"Random24", 0, 0, 0x1972ae0bc903694dull, 0x14650fb0739d0383ull, 0xcdafd09a1c076093ull, 0x82e72364e836634full, 60, 0, 16}, - {"Random24", 1, 1, 0x3df4ec3fe0efc36full, 0x8ee35827bfb734d3ull, 0xe744bb83904b658aull, 0x521e8d8456c37b6dull, 60, 60, 16}, - {"Random24", 1, 0, 0x3df4ec3fe0efc36full, 0x14650fb0739d0383ull, 0xe744bb83904b658aull, 0x521e8d8456c37b6dull, 60, 0, 16}, - {"Random25", 0, 1, 0x5fde9097294eff95ull, 0xeb739137b8b09c27ull, 0x5ff4203ec9ac379eull, 0x74d22f0254d1d14aull, 44, 44, 8}, - {"Random25", 0, 0, 0x5fde9097294eff95ull, 0x14650fb0739d0383ull, 0x5ff4203ec9ac379eull, 0x74d22f0254d1d14aull, 44, 0, 8}, - {"Random25", 1, 1, 0x61de85290b80e276ull, 0x3e2dace4bb30d118ull, 0x5ff4203ec9ac379eull, 0xe27506e279646458ull, 44, 44, 8}, - {"Random25", 1, 0, 0x61de85290b80e276ull, 0x14650fb0739d0383ull, 0x5ff4203ec9ac379eull, 0xe27506e279646458ull, 44, 0, 8}, - {"Random26", 0, 1, 0xf3bbd59278bbf967ull, 0x1bc27907f389539aull, 0xd9522717c794d53bull, 0x86792f22105fe886ull, 42, 42, 12}, - {"Random26", 0, 0, 0xf3bbd59278bbf967ull, 0x14650fb0739d0383ull, 0xd9522717c794d53bull, 0x86792f22105fe886ull, 42, 0, 12}, - {"Random26", 1, 1, 0x14e61f95689ec8dfull, 0x2117af0b816b84ebull, 0xf7b8acd382701dd1ull, 0x32b7fe08ce5005d2ull, 42, 42, 12}, - {"Random26", 1, 0, 0x14e61f95689ec8dfull, 0x14650fb0739d0383ull, 0xf7b8acd382701dd1ull, 0x32b7fe08ce5005d2ull, 42, 0, 12}, - {"Random27", 0, 1, 0x053cfa54c61f3bd4ull, 0x49d4da1649eefffbull, 0x3c0165d3836f406aull, 0x2b62c8cc0129f74cull, 50, 50, 22}, - {"Random27", 0, 0, 0x053cfa54c61f3bd4ull, 0x14650fb0739d0383ull, 0x3c0165d3836f406aull, 0x2b62c8cc0129f74cull, 50, 0, 22}, - {"Random27", 1, 1, 0x26f99a57bee85343ull, 0x4ff364a8de81f569ull, 0x97addcfee41a4dd8ull, 0xd48238aab27e8f44ull, 50, 50, 21}, - {"Random27", 1, 0, 0x26f99a57bee85343ull, 0x14650fb0739d0383ull, 0x97addcfee41a4dd8ull, 0xd48238aab27e8f44ull, 50, 0, 21}, - {"Random28", 0, 1, 0x5afaa18295f32ff1ull, 0xca9c90143f0b6a95ull, 0xc447f3f5058cd81bull, 0xe019b821239c3290ull, 54, 54, 19}, - {"Random28", 0, 0, 0x5afaa18295f32ff1ull, 0x14650fb0739d0383ull, 0xc447f3f5058cd81bull, 0xe019b821239c3290ull, 54, 0, 19}, - {"Random28", 1, 1, 0x6d2667eba32721eeull, 0x3c23578f7764250aull, 0xee17522a5f3c15dfull, 0xdb07854a60f415d7ull, 54, 54, 19}, - {"Random28", 1, 0, 0x6d2667eba32721eeull, 0x14650fb0739d0383ull, 0xee17522a5f3c15dfull, 0xdb07854a60f415d7ull, 54, 0, 19}, - {"Random29", 0, 1, 0xd1301252d9b58f96ull, 0xa789410c6a57a22aull, 0xe6aec4ce1ae7ccd1ull, 0x595deac764871057ull, 57, 57, 1}, - {"Random29", 0, 0, 0xd1301252d9b58f96ull, 0x14650fb0739d0383ull, 0xe6aec4ce1ae7ccd1ull, 0x595deac764871057ull, 57, 0, 1}, - {"Random29", 1, 1, 0x1a25b83f5e6fed46ull, 0x85c4d848e014bb3cull, 0xe6aec4ce1ae7ccd1ull, 0x13fb019ad44bec5aull, 57, 57, 1}, - {"Random29", 1, 0, 0x1a25b83f5e6fed46ull, 0x14650fb0739d0383ull, 0xe6aec4ce1ae7ccd1ull, 0x13fb019ad44bec5aull, 57, 0, 1}, - {"Random30", 0, 1, 0xa9365ffd65e05a09ull, 0xc88de1ce1dcfafe1ull, 0x260809f2d3657490ull, 0x134c921985714562ull, 61, 61, 1}, - {"Random30", 0, 0, 0xa9365ffd65e05a09ull, 0x14650fb0739d0383ull, 0x260809f2d3657490ull, 0x134c921985714562ull, 61, 0, 1}, - {"Random30", 1, 1, 0x1facc2c1cfd4c971ull, 0x941084ae8c1b103bull, 0x260809f2d3657490ull, 0xf2973241ae3d2097ull, 61, 61, 1}, - {"Random30", 1, 0, 0x1facc2c1cfd4c971ull, 0x14650fb0739d0383ull, 0x260809f2d3657490ull, 0xf2973241ae3d2097ull, 61, 0, 1}, - {"Random31", 0, 1, 0x8eaa06b36232a908ull, 0xc98390a9e23599d3ull, 0xcad16841aad879ddull, 0xb86249f3491f97b0ull, 49, 49, 9}, - {"Random31", 0, 0, 0x8eaa06b36232a908ull, 0x14650fb0739d0383ull, 0xcad16841aad879ddull, 0xb86249f3491f97b0ull, 49, 0, 9}, - {"Random31", 1, 1, 0xf90b0737ee4a025aull, 0x09ec11f94ae3f675ull, 0x2569d07bfd939198ull, 0xc7f3afe378a05865ull, 49, 49, 9}, - {"Random31", 1, 0, 0xf90b0737ee4a025aull, 0x14650fb0739d0383ull, 0x2569d07bfd939198ull, 0xc7f3afe378a05865ull, 49, 0, 9}, - {"Random32", 0, 1, 0x32da5eed595ee5b4ull, 0xec4a0ece9475e1d3ull, 0x4994badb687c8750ull, 0x4b6c0af81cc429f5ull, 62, 62, 19}, - {"Random32", 0, 0, 0x32da5eed595ee5b4ull, 0x14650fb0739d0383ull, 0x4994badb687c8750ull, 0x4b6c0af81cc429f5ull, 62, 0, 19}, - {"Random32", 1, 1, 0xe733831f206a336aull, 0xb4483230298afef5ull, 0xb1a7a66b620c4fd5ull, 0xe6f2b4aa4d8611a1ull, 62, 62, 19}, - {"Random32", 1, 0, 0xe733831f206a336aull, 0x14650fb0739d0383ull, 0xb1a7a66b620c4fd5ull, 0xe6f2b4aa4d8611a1ull, 62, 0, 19}, - {"Random33", 0, 1, 0x33bf503af7204823ull, 0x39a8de40423427d1ull, 0xf8d13762bc3ab9c0ull, 0xbe29a314289e811cull, 51, 51, 13}, - {"Random33", 0, 0, 0x33bf503af7204823ull, 0x14650fb0739d0383ull, 0xf8d13762bc3ab9c0ull, 0xbe29a314289e811cull, 51, 0, 13}, - {"Random33", 1, 1, 0x7f150e22e1c3c861ull, 0xda9af32172dac1a4ull, 0xca5862380ffcef2full, 0x1122e3cca5beb09dull, 51, 51, 13}, - {"Random33", 1, 0, 0x7f150e22e1c3c861ull, 0x14650fb0739d0383ull, 0xca5862380ffcef2full, 0x1122e3cca5beb09dull, 51, 0, 13}, - {"Random34", 0, 1, 0x9191daa7f287cff2ull, 0x1351d75d55b69237ull, 0x81535eb933e496e7ull, 0x53efe9facdfed383ull, 51, 51, 1}, - {"Random34", 0, 0, 0x9191daa7f287cff2ull, 0x14650fb0739d0383ull, 0x81535eb933e496e7ull, 0x53efe9facdfed383ull, 51, 0, 1}, - {"Random34", 1, 1, 0xe3bd733dbb7dd142ull, 0xcc37232ed176c590ull, 0xa788aa75c6b931e3ull, 0x3c1c21ded8a43a85ull, 51, 51, 2}, - {"Random34", 1, 0, 0xe3bd733dbb7dd142ull, 0x14650fb0739d0383ull, 0xa788aa75c6b931e3ull, 0x3c1c21ded8a43a85ull, 51, 0, 2}, - {"Random35", 0, 1, 0xa1dad91ce6005cbbull, 0xac8c4802e8a8b254ull, 0xa8b1e0f14c6cc49aull, 0xc8f7b4decfbdeed1ull, 63, 63, 24}, - {"Random35", 0, 0, 0xa1dad91ce6005cbbull, 0x14650fb0739d0383ull, 0xa8b1e0f14c6cc49aull, 0xc8f7b4decfbdeed1ull, 63, 0, 24}, - {"Random35", 1, 1, 0x6ce4272e31e5439eull, 0xf840ac1e5c21e71full, 0xb2b344c283516d30ull, 0x0020809913835e3dull, 63, 63, 24}, - {"Random35", 1, 0, 0x6ce4272e31e5439eull, 0x14650fb0739d0383ull, 0xb2b344c283516d30ull, 0x0020809913835e3dull, 63, 0, 24}, - {"Random36", 0, 1, 0xb6ed02402286f1daull, 0xb93cb982740efaf2ull, 0x8ee686191c3d0539ull, 0x99b1254e54e63283ull, 87, 60, 21}, - {"Random36", 0, 0, 0xb6ed02402286f1daull, 0x14650fb0739d0383ull, 0x8ee686191c3d0539ull, 0x99b1254e54e63283ull, 87, 0, 21}, - {"Random36", 1, 1, 0xe4e7726a4db33f47ull, 0xbbc67cbeef5f3d57ull, 0x10b01a5c8873f5d5ull, 0xaf91a8392845aa5aull, 87, 60, 21}, - {"Random36", 1, 0, 0xe4e7726a4db33f47ull, 0x14650fb0739d0383ull, 0x10b01a5c8873f5d5ull, 0xaf91a8392845aa5aull, 87, 0, 21}, - {"Random37", 0, 1, 0xf50f40fe8bb210c2ull, 0x6830953a611d9b95ull, 0x6b11a4e13499e50dull, 0x218cf254fd19cf8aull, 71, 45, 18}, - {"Random37", 0, 0, 0xf50f40fe8bb210c2ull, 0x14650fb0739d0383ull, 0x6b11a4e13499e50dull, 0x218cf254fd19cf8aull, 71, 0, 18}, - {"Random37", 1, 1, 0x44783e84e44eaf3bull, 0x5024d58d139a41c8ull, 0x15dfd0d0fb5f649aull, 0x7d6333767228433aull, 70, 45, 17}, - {"Random37", 1, 0, 0x44783e84e44eaf3bull, 0x14650fb0739d0383ull, 0x15dfd0d0fb5f649aull, 0x7d6333767228433aull, 70, 0, 17}, - {"Random38", 0, 1, 0x9b34eab31c08b2e9ull, 0x611d2e69cd985562ull, 0x3a8c8a474889316eull, 0xf756c93d8d25c11cull, 54, 54, 25}, - {"Random38", 0, 0, 0x9b34eab31c08b2e9ull, 0x14650fb0739d0383ull, 0x3a8c8a474889316eull, 0xf756c93d8d25c11cull, 54, 0, 25}, - {"Random38", 1, 1, 0x6b22f5a3ffc931ceull, 0xa60b5a9b3c472885ull, 0x084c597474eaa400ull, 0xf2c798a5bba88d39ull, 54, 54, 25}, - {"Random38", 1, 0, 0x6b22f5a3ffc931ceull, 0x14650fb0739d0383ull, 0x084c597474eaa400ull, 0xf2c798a5bba88d39ull, 54, 0, 25}, - {"Random39", 0, 1, 0x271e3d6676599351ull, 0xd43319fbe54e5588ull, 0xc193386eb35a8e65ull, 0xbbf808ba5ac7b67eull, 44, 44, 19}, - {"Random39", 0, 0, 0x271e3d6676599351ull, 0x14650fb0739d0383ull, 0xc193386eb35a8e65ull, 0xbbf808ba5ac7b67eull, 44, 0, 19}, - {"Random39", 1, 1, 0x4b1fcedefee60801ull, 0xd3eb60fe967c52eeull, 0xeea051f0749b0428ull, 0x6842fdc2e3fbde5dull, 44, 44, 19}, - {"Random39", 1, 0, 0x4b1fcedefee60801ull, 0x14650fb0739d0383ull, 0xeea051f0749b0428ull, 0x6842fdc2e3fbde5dull, 44, 0, 19}, - {"Random40", 0, 1, 0x246be525b446fce2ull, 0x9d244e811d5ca386ull, 0x76e2d6a7f41b161cull, 0x933f71890476b2bbull, 77, 53, 18}, - {"Random40", 0, 0, 0x246be525b446fce2ull, 0x14650fb0739d0383ull, 0x76e2d6a7f41b161cull, 0x933f71890476b2bbull, 77, 0, 18}, - {"Random40", 1, 1, 0x04841e1d0f981de5ull, 0xc3ce4c5ae0eef6cdull, 0x9a84d463fe03ec91ull, 0x41541580ac45b572ull, 78, 53, 19}, - {"Random40", 1, 0, 0x04841e1d0f981de5ull, 0x14650fb0739d0383ull, 0x9a84d463fe03ec91ull, 0x41541580ac45b572ull, 78, 0, 19}, - {"Random41", 0, 1, 0x01aece7db4fb0866ull, 0x1b30dc30f66b89bcull, 0x4b1ec3cc1bd0909cull, 0x691121b701b14a94ull, 51, 40, 6}, - {"Random41", 0, 0, 0x01aece7db4fb0866ull, 0x14650fb0739d0383ull, 0x4b1ec3cc1bd0909cull, 0x691121b701b14a94ull, 51, 0, 6}, - {"Random41", 1, 1, 0x6a009217c0648447ull, 0x94eeabc325d68785ull, 0x4b1ec3cc1bd0909cull, 0x29d310f0bf1d7a89ull, 51, 40, 6}, - {"Random41", 1, 0, 0x6a009217c0648447ull, 0x14650fb0739d0383ull, 0x4b1ec3cc1bd0909cull, 0x29d310f0bf1d7a89ull, 51, 0, 6}, - {"Random42", 0, 1, 0x983f3224390b3aa8ull, 0xdd47cc8bbcfac978ull, 0x75b6953551fe9575ull, 0xd5c7b24ea3676694ull, 60, 60, 12}, - {"Random42", 0, 0, 0x983f3224390b3aa8ull, 0x14650fb0739d0383ull, 0x75b6953551fe9575ull, 0xd5c7b24ea3676694ull, 60, 0, 12}, - {"Random42", 1, 1, 0x5391d71c90732f54ull, 0x95d22736dbced96dull, 0xf736fbadc411c223ull, 0xa3fed64ce5a4b1f9ull, 60, 60, 12}, - {"Random42", 1, 0, 0x5391d71c90732f54ull, 0x14650fb0739d0383ull, 0xf736fbadc411c223ull, 0xa3fed64ce5a4b1f9ull, 60, 0, 12}, - {"Random43", 0, 1, 0x3721d2c0c27485e6ull, 0x83e3892349cf9593ull, 0xbd8ba613082e0da5ull, 0xeb9aef460c2a6b69ull, 63, 63, 14}, - {"Random43", 0, 0, 0x3721d2c0c27485e6ull, 0x14650fb0739d0383ull, 0xbd8ba613082e0da5ull, 0xeb9aef460c2a6b69ull, 63, 0, 14}, - {"Random43", 1, 1, 0x38e80c0ff09bd6efull, 0xb48d33783f27e610ull, 0x572d3f9f9521e7caull, 0x0584f3141a2ac711ull, 63, 63, 14}, - {"Random43", 1, 0, 0x38e80c0ff09bd6efull, 0x14650fb0739d0383ull, 0x572d3f9f9521e7caull, 0x0584f3141a2ac711ull, 63, 0, 14}, - {"Random44", 0, 1, 0x40302399ca38ec05ull, 0x4bdc4b7a1c4ca040ull, 0x513c7ae44c865ec8ull, 0x55eec4db28687678ull, 69, 47, 15}, - {"Random44", 0, 0, 0x40302399ca38ec05ull, 0x14650fb0739d0383ull, 0x513c7ae44c865ec8ull, 0x55eec4db28687678ull, 69, 0, 15}, - {"Random44", 1, 1, 0xb8a256af96ab69efull, 0xf56d5278e6ece7caull, 0x1a79f44215a3981eull, 0x49bc82761efd9785ull, 69, 47, 15}, - {"Random44", 1, 0, 0xb8a256af96ab69efull, 0x14650fb0739d0383ull, 0x1a79f44215a3981eull, 0x49bc82761efd9785ull, 69, 0, 15}, - {"Random45", 0, 1, 0x2c707948f442e0a5ull, 0xe0be91403cc03881ull, 0xf543670d7cc61817ull, 0x49b18e779e62a8fbull, 99, 63, 25}, - {"Random45", 0, 0, 0x2c707948f442e0a5ull, 0x14650fb0739d0383ull, 0xf543670d7cc61817ull, 0x49b18e779e62a8fbull, 99, 0, 25}, - {"Random45", 1, 1, 0x8c2ae0a418e236daull, 0xe32f963f031175d8ull, 0xa2aef1f38f464b4dull, 0x6fe2fe4de0b41257ull, 99, 63, 24}, - {"Random45", 1, 0, 0x8c2ae0a418e236daull, 0x14650fb0739d0383ull, 0xa2aef1f38f464b4dull, 0x6fe2fe4de0b41257ull, 99, 0, 24}, - {"Random46", 0, 1, 0xf293162843324a42ull, 0x2fe9f58c05bd5391ull, 0x8fe7667934a81f87ull, 0x138273f0186f99a8ull, 52, 52, 1}, - {"Random46", 0, 0, 0xf293162843324a42ull, 0x14650fb0739d0383ull, 0x8fe7667934a81f87ull, 0x138273f0186f99a8ull, 52, 0, 1}, - {"Random46", 1, 1, 0x5c518c7c16ad6eb9ull, 0x7114f97576e05de1ull, 0x8fe7667934a81f87ull, 0x02c866a863d14e29ull, 52, 52, 1}, - {"Random46", 1, 0, 0x5c518c7c16ad6eb9ull, 0x14650fb0739d0383ull, 0x8fe7667934a81f87ull, 0x02c866a863d14e29ull, 52, 0, 1}, - {"Random47", 0, 1, 0xeb703ef1728b8316ull, 0xcf65482f5b03def1ull, 0x072cedccf12d6a31ull, 0x6d733b355b0b4883ull, 57, 57, 13}, - {"Random47", 0, 0, 0xeb703ef1728b8316ull, 0x14650fb0739d0383ull, 0x072cedccf12d6a31ull, 0x6d733b355b0b4883ull, 57, 0, 13}, - {"Random47", 1, 1, 0x25be4848720c17deull, 0xb3c87f095d3d086dull, 0x33811b67f07874f3ull, 0xc9b10c1db4c2a51aull, 57, 57, 13}, - {"Random47", 1, 0, 0x25be4848720c17deull, 0x14650fb0739d0383ull, 0x33811b67f07874f3ull, 0xc9b10c1db4c2a51aull, 57, 0, 13}, - {"Random48", 0, 1, 0xd0c92fe6fc290e0bull, 0x0af2126a53f8f7b9ull, 0x62a7be3a970dee6aull, 0x840184ac798a5de2ull, 48, 48, 9}, - {"Random48", 0, 0, 0xd0c92fe6fc290e0bull, 0x14650fb0739d0383ull, 0x62a7be3a970dee6aull, 0x840184ac798a5de2ull, 48, 0, 9}, - {"Random48", 1, 1, 0xd39f4122a0d0a91dull, 0xd439da8c48c337ddull, 0xee7c0d61bfb7dd62ull, 0x1969d9a52bdd51c2ull, 48, 48, 9}, - {"Random48", 1, 0, 0xd39f4122a0d0a91dull, 0x14650fb0739d0383ull, 0xee7c0d61bfb7dd62ull, 0x1969d9a52bdd51c2ull, 48, 0, 9}, + {"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, 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}, + {"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, 0x4603f8c446ca5c1eull, 0x2583ce2bc6a3c1d9ull, 57, 40, 10}, + {"CalcOnFills", 0, 0, 0x7641907936993779ull, 0x14650fb0739d0383ull, 0x4603f8c446ca5c1eull, 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}, + {"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, 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}, + {"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, 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}, + {"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, 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}, + {"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, 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}, + {"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}, + {"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, 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}, + {"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}, + {"Random22", 1, 0, 0x1624d3651edc9089ull, 0x14650fb0739d0383ull, 0x29259a278cc3600bull, 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, 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}, + {"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}, + {"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, 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}, + {"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}, + {"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, 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}, + {"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}, + {"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, 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}, + {"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}, + {"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}, + {"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}, + {"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}, + {"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, 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}, }; diff --git a/tests/test_adapter_receipts_in_place.cpp b/tests/test_adapter_receipts_in_place.cpp index 072b32ef..a93bacda 100644 --- a/tests/test_adapter_receipts_in_place.cpp +++ b/tests/test_adapter_receipts_in_place.cpp @@ -399,10 +399,21 @@ void emit(Scenario scenario, bool magnifier, const Observed& got) { // 901832621209212828ull -> 15813806343021779355ull // 13528996515355458594ull -> 3478712054363587307ull // 9684931586165891051ull -> 8064893742935713930ull +// expectation corrected (V19-FIX, kDeclined_rows, 10 of 10 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 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: +// 10471252865781374815ull -> 16367413721785097631ull +// 7671906297724115341ull -> 8673411908040166317ull +// 2234871632290885580ull -> 711509299982175852ull +// 11660583350044382565ull -> 7197939734931235205ull +// 10795645000653949847ull -> 7029670217763137463ull +// 3876877678934974053ull -> 3635687300847380421ull +// 5719519402935312619ull -> 16534761561291369739ull +// 15813806343021779355ull -> 10876124900683281275ull +// 3478712054363587307ull -> 10020927749345911019ull +// 8064893742935713930ull -> 8691527508212156618ull constexpr std::uint64_t kDeclined_rows[] = { - 10471252865781374815ull, 7671906297724115341ull, 2234871632290885580ull, 11660583350044382565ull, - 10795645000653949847ull, 3876877678934974053ull, 5719519402935312619ull, 15813806343021779355ull, - 3478712054363587307ull, 8064893742935713930ull, + 16367413721785097631ull, 8673411908040166317ull, 711509299982175852ull, 7197939734931235205ull, + 7029670217763137463ull, 3635687300847380421ull, 16534761561291369739ull, 10876124900683281275ull, + 10020927749345911019ull, 8691527508212156618ull, }; constexpr std::uint64_t kDeclined_at_bar[] = { 4ull, 0ull, 12ull, 9ull, @@ -427,7 +438,8 @@ constexpr std::uint64_t kDeclined_folded[] = { // expectation corrected: kDeclined_final 1667123404342696863ull -> 1945621519056434088ull, 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): 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). -constexpr std::uint64_t kDeclined_final = 8064893742935713930ull; +// 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; constexpr Trade kDeclined_trades[] = { {1736121660000LL, 1736121960000LL, 100, 95, 100, 0}, }; @@ -459,10 +471,21 @@ constexpr Trade kDeclined_trades[] = { // 13176393922301188696ull -> 11561237993722196187ull // 5693363764903822630ull -> 8752407783783115299ull // 13182614947071830947ull -> 13196624524101047406ull +// expectation corrected (V19-FIX, kDeclinedMag_rows, 10 of 10 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 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: +// 2386459447381651821ull -> 17043975752116642733ull +// 6021168500418484266ull -> 10606185829452660874ull +// 16446546055123594696ull -> 15448675564357825832ull +// 9282458332898651101ull -> 15175044830743125885ull +// 15456921776568509967ull -> 6450443484594143151ull +// 9589349023649693661ull -> 2398923910778006973ull +// 12629404118906576790ull -> 8459031541244945398ull +// 11561237993722196187ull -> 1773947867498079483ull +// 8752407783783115299ull -> 15294623478765439011ull +// 13196624524101047406ull -> 483092226467101870ull constexpr std::uint64_t kDeclinedMag_rows[] = { - 2386459447381651821ull, 6021168500418484266ull, 16446546055123594696ull, 9282458332898651101ull, - 15456921776568509967ull, 9589349023649693661ull, 12629404118906576790ull, 11561237993722196187ull, - 8752407783783115299ull, 13196624524101047406ull, + 17043975752116642733ull, 10606185829452660874ull, 15448675564357825832ull, 15175044830743125885ull, + 6450443484594143151ull, 2398923910778006973ull, 8459031541244945398ull, 1773947867498079483ull, + 15294623478765439011ull, 483092226467101870ull, }; constexpr std::uint64_t kDeclinedMag_at_bar[] = { 4ull, 0ull, 12ull, 0ull, @@ -487,7 +510,8 @@ constexpr std::uint64_t kDeclinedMag_folded[] = { // expectation corrected: kDeclinedMag_final 16277588925252464314ull -> 6442860896266831741ull, 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): 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). -constexpr std::uint64_t kDeclinedMag_final = 13196624524101047406ull; +// 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; constexpr Trade kDeclinedMag_trades[] = { {1736121660000LL, 1736121960000LL, 100, 95, 100, 0}, }; @@ -508,9 +532,17 @@ constexpr Trade kDeclinedMag_trades[] = { // 9246256503314484488ull -> 8695591448261818412ull // 7573510687443965598ull -> 15937329709211664256ull // 18153826259251315829ull -> 10634995016395674704ull +// expectation corrected (V19-FIX, kRevive_rows, 7 of 7 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 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: +// 11381905052979082886ull -> 13340112391979823686ull +// 7711150994119040374ull -> 3080039388426713206ull +// 12033590382836458311ull -> 7902869859755540775ull +// 11344228619804537871ull -> 18430037521897822255ull +// 8695591448261818412ull -> 8604888200074309324ull +// 15937329709211664256ull -> 10980118139031631968ull +// 10634995016395674704ull -> 17030525150602589392ull constexpr std::uint64_t kRevive_rows[] = { - 11381905052979082886ull, 7711150994119040374ull, 12033590382836458311ull, 11344228619804537871ull, - 8695591448261818412ull, 15937329709211664256ull, 10634995016395674704ull, + 13340112391979823686ull, 3080039388426713206ull, 7902869859755540775ull, 18430037521897822255ull, + 8604888200074309324ull, 10980118139031631968ull, 17030525150602589392ull, }; constexpr std::uint64_t kRevive_at_bar[] = { 4ull, 0ull, 15ull, 10ull, @@ -530,7 +562,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). -constexpr std::uint64_t kRevive_final = 10634995016395674704ull; +// 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; constexpr Trade kRevive_trades[] = { {1736121660000LL, 1736121780000LL, 100, 170, 47.058823529411768, 0}, {1736121660000LL, 1736121840000LL, 100, 180, 52.941176470588232, 0}, @@ -552,9 +585,17 @@ constexpr Trade kRevive_trades[] = { // 9108172091352313389ull -> 9719574618747977040ull // 13205705026122939030ull -> 16705937982676897010ull // 11245820422146263909ull -> 403749858405626294ull +// expectation corrected (V19-FIX, kReviveMag_rows, 7 of 7 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 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: +// 14608739080977528633ull -> 9323387230543354105ull +// 3414911638394215429ull -> 17230544106411439877ull +// 13152262788765868075ull -> 8558481171000670347ull +// 9127833906595294915ull -> 11778096795536835427ull +// 9719574618747977040ull -> 6288134072033682736ull +// 16705937982676897010ull -> 5799450267550970898ull +// 403749858405626294ull -> 16015312550992593014ull constexpr std::uint64_t kReviveMag_rows[] = { - 14608739080977528633ull, 3414911638394215429ull, 13152262788765868075ull, 9127833906595294915ull, - 9719574618747977040ull, 16705937982676897010ull, 403749858405626294ull, + 9323387230543354105ull, 17230544106411439877ull, 8558481171000670347ull, 11778096795536835427ull, + 6288134072033682736ull, 5799450267550970898ull, 16015312550992593014ull, }; constexpr std::uint64_t kReviveMag_at_bar[] = { 4ull, 0ull, 15ull, 0ull, @@ -574,7 +615,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). -constexpr std::uint64_t kReviveMag_final = 403749858405626294ull; +// 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; constexpr Trade kReviveMag_trades[] = { {1736121660000LL, 1736121840000LL, 100, 175, 100, 0}, }; @@ -595,9 +637,17 @@ constexpr Trade kReviveMag_trades[] = { // 16254425278702601285ull -> 10109285908224112591ull // 12056339472046727078ull -> 10060812652362367792ull // 5566037634063544638ull -> 12854015805238324476ull +// expectation corrected (V19-FIX, kCascade_rows, 7 of 7 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 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: +// 7093386270889089222ull -> 551096499902803590ull +// 18110309630310117569ull -> 6515737401085179265ull +// 6621058774966980276ull -> 13117702113570278612ull +// 8896173852282142540ull -> 1257083709566184300ull +// 10109285908224112591ull -> 1236032771344307983ull +// 10060812652362367792ull -> 17545089905645669552ull +// 12854015805238324476ull -> 11022588141404321180ull constexpr std::uint64_t kCascade_rows[] = { - 7093386270889089222ull, 18110309630310117569ull, 6621058774966980276ull, 8896173852282142540ull, - 10109285908224112591ull, 10060812652362367792ull, 12854015805238324476ull, + 551096499902803590ull, 6515737401085179265ull, 13117702113570278612ull, 1257083709566184300ull, + 1236032771344307983ull, 17545089905645669552ull, 11022588141404321180ull, }; constexpr std::uint64_t kCascade_at_bar[] = { 4ull, 0ull, 15ull, 10ull, @@ -617,7 +667,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). -constexpr std::uint64_t kCascade_final = 12854015805238324476ull; +// 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; constexpr Trade kCascade_trades[] = { {1736121660000LL, 1736121780000LL, 100, 170, 47.058823529411768, 0}, {1736121660000LL, 1736121780000LL, 100, 170, 52.941176470588232, 0}, @@ -639,9 +690,17 @@ constexpr Trade kCascade_trades[] = { // 6423023268752600282ull -> 2820795946605591416ull // 7685185556116772882ull -> 8320281576492738244ull // 8438336155355340534ull -> 8969517760469763972ull +// expectation corrected (V19-FIX, kCascadeMag_rows, 7 of 7 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 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: +// 4473825047915283433ull -> 15338041165786710953ull +// 227172169403486164ull -> 9326370104822563476ull +// 17659296469268981537ull -> 2546778736934085057ull +// 2132167081514435162ull -> 15492970885942622778ull +// 2820795946605591416ull -> 16758361987609330968ull +// 8320281576492738244ull -> 4624738216223074340ull +// 8969517760469763972ull -> 2074679355477699332ull constexpr std::uint64_t kCascadeMag_rows[] = { - 4473825047915283433ull, 227172169403486164ull, 17659296469268981537ull, 2132167081514435162ull, - 2820795946605591416ull, 8320281576492738244ull, 8969517760469763972ull, + 15338041165786710953ull, 9326370104822563476ull, 2546778736934085057ull, 15492970885942622778ull, + 16758361987609330968ull, 4624738216223074340ull, 2074679355477699332ull, }; constexpr std::uint64_t kCascadeMag_at_bar[] = { 4ull, 0ull, 15ull, 0ull, @@ -661,7 +720,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). -constexpr std::uint64_t kCascadeMag_final = 8969517760469763972ull; +// 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; constexpr Trade kCascadeMag_trades[] = { {1736121660000LL, 1736121840000LL, 100, 168, 19.047619047619047, 0}, {1736121660000LL, 1736121840000LL, 100, 168, 80.952380952380949, 0}, @@ -778,17 +838,58 @@ constexpr Trade kCascadeMag_trades[] = { // 9088484067275656470ull -> 6753420119756311418ull // 11937924564662907819ull -> 1593360149240348528ull // 7215366721055711217ull -> 13709538166202381782ull +// expectation corrected (V19-FIX, kBrackets_rows, 40 of 40 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 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: +// 4931640785442886345ull -> 16217277159719941353ull +// 17478909815634119236ull -> 8338318974496761540ull +// 15352487419733065212ull -> 4387288109901666172ull +// 12654730253382767814ull -> 17194843007024741926ull +// 189181168054283670ull -> 13764914223976981334ull +// 12777030162683706874ull -> 5165427215729956474ull +// 4366298018774171687ull -> 4634967915219578727ull +// 13567975051191501448ull -> 16692219291368299752ull +// 2490861395787358522ull -> 14748223541663733754ull +// 6163669939419568773ull -> 3456524395963272453ull +// 14839279797350066889ull -> 12434621022045497961ull +// 11327888575643522147ull -> 16335369385158554819ull +// 13772309817367178166ull -> 12991044494042544886ull +// 7176477766377336287ull -> 4973527342518025567ull +// 14704665947556605284ull -> 17883762509965744260ull +// 2349824741170149922ull -> 2061839802394366722ull +// 4447048162144867767ull -> 9666386366910181655ull +// 4383713959049933942ull -> 4436881865564527894ull +// 963865382596244388ull -> 16715420632714991716ull +// 8291728019901913574ull -> 1529353304450412038ull +// 11549289641401549142ull -> 1035717575522872886ull +// 6197162411608442270ull -> 16225013858806894718ull +// 13970368985631889286ull -> 8409169559076599494ull +// 2244340637825718596ull -> 9707763992756481156ull +// 4014491293609474968ull -> 2831826005880704344ull +// 17710204879557390911ull -> 17431835404196442687ull +// 4325432014176081106ull -> 9788230484951507538ull +// 5076958827136290263ull -> 13167836072962096695ull +// 16041380806474883206ull -> 9063096009681232646ull +// 7151463586388941655ull -> 5410108115990088215ull +// 12003189017867500329ull -> 13040426354353828713ull +// 13568581922318463703ull -> 14801076939233187415ull +// 17538111680233578939ull -> 16815914518686289691ull +// 1502260751703873765ull -> 2747558237080166725ull +// 501321500839750044ull -> 8660899489027983548ull +// 10369076632468418401ull -> 17102303971583026849ull +// 6084936684912175710ull -> 15533897202822525630ull +// 6753420119756311418ull -> 3654409746117594394ull +// 1593360149240348528ull -> 13081935113382965168ull +// 13709538166202381782ull -> 10127071882762265462ull constexpr std::uint64_t kBrackets_rows[] = { - 4931640785442886345ull, 17478909815634119236ull, 15352487419733065212ull, 12654730253382767814ull, - 189181168054283670ull, 12777030162683706874ull, 4366298018774171687ull, 13567975051191501448ull, - 2490861395787358522ull, 6163669939419568773ull, 14839279797350066889ull, 11327888575643522147ull, - 13772309817367178166ull, 7176477766377336287ull, 14704665947556605284ull, 2349824741170149922ull, - 4447048162144867767ull, 4383713959049933942ull, 963865382596244388ull, 8291728019901913574ull, - 11549289641401549142ull, 6197162411608442270ull, 13970368985631889286ull, 2244340637825718596ull, - 4014491293609474968ull, 17710204879557390911ull, 4325432014176081106ull, 5076958827136290263ull, - 16041380806474883206ull, 7151463586388941655ull, 12003189017867500329ull, 13568581922318463703ull, - 17538111680233578939ull, 1502260751703873765ull, 501321500839750044ull, 10369076632468418401ull, - 6084936684912175710ull, 6753420119756311418ull, 1593360149240348528ull, 13709538166202381782ull, + 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, }; constexpr std::uint64_t kBrackets_at_bar[] = { 4ull, 0ull, 25ull, 20ull, @@ -858,7 +959,8 @@ constexpr std::uint64_t kBrackets_folded[] = { // expectation corrected: kBrackets_final 12275462523833622145ull -> 15408496303561531855ull, 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): 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). -constexpr std::uint64_t kBrackets_final = 13709538166202381782ull; +// 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; constexpr Trade kBrackets_trades[] = { {1736121660000LL, 1736121660000LL, 100.75, 101, 2, 0}, {1736122020000LL, 1736122260000LL, 100.75, 102, 2, 0}, @@ -978,17 +1080,58 @@ constexpr Trade kBrackets_trades[] = { // 13669172117143703422ull -> 17217996485244538122ull // 5803040722061771621ull -> 16847584222090599594ull // 10498718013143678943ull -> 17233175003301036440ull +// expectation corrected (V19-FIX, kBracketsMag_rows, 40 of 40 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 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: +// 357160593917001728ull -> 16815971932027180960ull +// 1367088814885629166ull -> 8203166635201027950ull +// 287323235851632294ull -> 17138741981140426534ull +// 16045756699822731600ull -> 1925655849137931056ull +// 13135689560889444372ull -> 14052428529967425108ull +// 11854950712426707972ull -> 9832333236513193860ull +// 7925187025432663817ull -> 16191684320998506441ull +// 11361883085123599477ull -> 8371860663058915477ull +// 4104072211950622040ull -> 8552593071486288536ull +// 7434875672168557443ull -> 1454522877014641667ull +// 5786698095493413500ull -> 2707471427108666620ull +// 16482929592882674954ull -> 16372300294283464170ull +// 5269590940989060304ull -> 13159511447411425168ull +// 12091950803929664533ull -> 7564232112284716309ull +// 13563671046532211882ull -> 1543613811599180298ull +// 12001241435211789402ull -> 15537720168749660922ull +// 16774375451174745758ull -> 1622379678095332862ull +// 4224856795496726447ull -> 12005994129115025487ull +// 16977193853773916951ull -> 5070356265405608919ull +// 12075224046164885783ull -> 13665825986379254999ull +// 13164217118227363754ull -> 5146373771364769642ull +// 12622146661531112630ull -> 6239145605357339126ull +// 7736835571275902542ull -> 3824110391792374830ull +// 14635256632315754316ull -> 6558902854431182828ull +// 4064924431338044193ull -> 8689860431737549825ull +// 3581813565948608081ull -> 7395806610126361265ull +// 6489641499513072013ull -> 4003508704365535789ull +// 8318149665198121584ull -> 15338849660478731120ull +// 4314907606986288281ull -> 12122897657346658873ull +// 124222954004018752ull -> 14082864916763771424ull +// 12885886306814112472ull -> 4695176159045771576ull +// 10568636995920463418ull -> 11640860124898684186ull +// 13435524033915440050ull -> 7934415857533705714ull +// 15055255835012471428ull -> 2180712887517991172ull +// 10282307508665938714ull -> 13696242844024611290ull +// 2500841977660255815ull -> 4388532892304967463ull +// 3687457776980188995ull -> 886913764131836035ull +// 17217996485244538122ull -> 6026087689782884746ull +// 16847584222090599594ull -> 7992738227861920074ull +// 17233175003301036440ull -> 2507370523932097816ull constexpr std::uint64_t kBracketsMag_rows[] = { - 357160593917001728ull, 1367088814885629166ull, 287323235851632294ull, 16045756699822731600ull, - 13135689560889444372ull, 11854950712426707972ull, 7925187025432663817ull, 11361883085123599477ull, - 4104072211950622040ull, 7434875672168557443ull, 5786698095493413500ull, 16482929592882674954ull, - 5269590940989060304ull, 12091950803929664533ull, 13563671046532211882ull, 12001241435211789402ull, - 16774375451174745758ull, 4224856795496726447ull, 16977193853773916951ull, 12075224046164885783ull, - 13164217118227363754ull, 12622146661531112630ull, 7736835571275902542ull, 14635256632315754316ull, - 4064924431338044193ull, 3581813565948608081ull, 6489641499513072013ull, 8318149665198121584ull, - 4314907606986288281ull, 124222954004018752ull, 12885886306814112472ull, 10568636995920463418ull, - 13435524033915440050ull, 15055255835012471428ull, 10282307508665938714ull, 2500841977660255815ull, - 3687457776980188995ull, 17217996485244538122ull, 16847584222090599594ull, 17233175003301036440ull, + 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, }; constexpr std::uint64_t kBracketsMag_at_bar[] = { 4ull, 0ull, 25ull, 0ull, @@ -1058,7 +1201,8 @@ constexpr std::uint64_t kBracketsMag_folded[] = { // expectation corrected: kBracketsMag_final 4480275903615486608ull -> 16366316158039625809ull, 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): 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). -constexpr std::uint64_t kBracketsMag_final = 17233175003301036440ull; +// 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; constexpr Trade kBracketsMag_trades[] = { {1736121660000LL, 1736121660000LL, 100.75, 101.25, 2, 0}, {1736122020000LL, 1736122260000LL, 100.75, 102, 2, 0}, @@ -1149,17 +1293,58 @@ constexpr Trade kBracketsMag_trades[] = { // 7461966957553468455ull -> 7242134762483671893ull // 8230861333685497064ull -> 1865370244366172271ull // 2748401044783380800ull -> 10203291295434148760ull +// expectation corrected (V19-FIX, kGroups_rows, 40 of 40 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 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: +// 2251190330680026869ull -> 428451890117060597ull +// 7887834686192888166ull -> 16045090594768170214ull +// 3855968558637463435ull -> 18166866907386903819ull +// 2546062310896875234ull -> 6958941312543333314ull +// 600437454323137621ull -> 12357763379855329333ull +// 16674487917741010732ull -> 10319108494843725228ull +// 4610683579624476637ull -> 8057235825342986589ull +// 15092384830490081514ull -> 775027953877311050ull +// 14071508027291531173ull -> 14610483346660466821ull +// 6111309149023879865ull -> 1198104509264338521ull +// 18103735437698418633ull -> 15842771393076564009ull +// 3959243710668744815ull -> 5342656873780671151ull +// 3952210482411450856ull -> 2165268433984104ull +// 3490143314173465755ull -> 16270682949794220379ull +// 14538602064706366786ull -> 14994318600896901474ull +// 5218037555664020499ull -> 3023615581009694835ull +// 225708733966286232ull -> 13496533941019930808ull +// 9782719631545392594ull -> 6174715221277441970ull +// 14940244590911598032ull -> 5113739070112146000ull +// 12346330238722784478ull -> 1772495940269705534ull +// 13467268729476158219ull -> 10686035606077590379ull +// 16944155163988596044ull -> 17379645375024748460ull +// 3768322604539017323ull -> 3810215059026298411ull +// 17328156936260981351ull -> 6980015280280300871ull +// 4766285883157907692ull -> 17653590613423189804ull +// 11123152710249472138ull -> 5097055414090822762ull +// 1712228033902812862ull -> 16040617079938685278ull +// 8137321895545044533ull -> 7474835689400851509ull +// 4135452977285764992ull -> 3588654748202396640ull +// 1035023176731878356ull -> 482256096822295700ull +// 10069202913180375095ull -> 491618572355040023ull +// 12120236779844291703ull -> 15447520409861182807ull +// 3940468084958498734ull -> 13546661642780035054ull +// 16394284185178237666ull -> 586417508450537570ull +// 1347962035407032299ull -> 16805284807511709227ull +// 11984308608154771468ull -> 14582173341406959084ull +// 14161529370115150918ull -> 4670485499468050406ull +// 7242134762483671893ull -> 8464039108905730677ull +// 1865370244366172271ull -> 16757005045255032303ull +// 10203291295434148760ull -> 14459029476982242904ull constexpr std::uint64_t kGroups_rows[] = { - 2251190330680026869ull, 7887834686192888166ull, 3855968558637463435ull, 2546062310896875234ull, - 600437454323137621ull, 16674487917741010732ull, 4610683579624476637ull, 15092384830490081514ull, - 14071508027291531173ull, 6111309149023879865ull, 18103735437698418633ull, 3959243710668744815ull, - 3952210482411450856ull, 3490143314173465755ull, 14538602064706366786ull, 5218037555664020499ull, - 225708733966286232ull, 9782719631545392594ull, 14940244590911598032ull, 12346330238722784478ull, - 13467268729476158219ull, 16944155163988596044ull, 3768322604539017323ull, 17328156936260981351ull, - 4766285883157907692ull, 11123152710249472138ull, 1712228033902812862ull, 8137321895545044533ull, - 4135452977285764992ull, 1035023176731878356ull, 10069202913180375095ull, 12120236779844291703ull, - 3940468084958498734ull, 16394284185178237666ull, 1347962035407032299ull, 11984308608154771468ull, - 14161529370115150918ull, 7242134762483671893ull, 1865370244366172271ull, 10203291295434148760ull, + 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, }; constexpr std::uint64_t kGroups_at_bar[] = { 4ull, 0ull, 9ull, 0ull, @@ -1228,7 +1413,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). -constexpr std::uint64_t kGroups_final = 10203291295434148760ull; +// 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; constexpr Trade kGroups_trades[] = { {1736121840000LL, 1736122020000LL, 102.25, 100.75, 1, 0}, {1736122020000LL, 1736122500000LL, 100.25, 100.5, 2, 0}, @@ -1323,17 +1509,58 @@ constexpr Trade kGroups_trades[] = { // 7651563840620793587ull -> 6230002762718235250ull // 10759678117498166630ull -> 14578437808706486766ull // 17786987883651578348ull -> 12557658352255323163ull +// expectation corrected (V19-FIX, kGroupsMag_rows, 40 of 40 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 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: +// 1771071767131182639ull -> 18395077400277767983ull +// 2319264234820563061ull -> 12064304672483746293ull +// 3968516947995724204ull -> 14523426078186844972ull +// 13748929559384796587ull -> 8862832225214863179ull +// 14337866189563969968ull -> 14588621505662414032ull +// 121300902977360074ull -> 4447156622707225162ull +// 13930328507840437447ull -> 4361493804042466119ull +// 11293074932416602936ull -> 1804450653994911768ull +// 17806644710829561148ull -> 14783451729418173724ull +// 11345590561057098901ull -> 8082972508033422197ull +// 16975877748262402349ull -> 8979544420957768909ull +// 10562941317592452443ull -> 7493375864971444891ull +// 12227261626285808416ull -> 2222992351194986144ull +// 3101225148965538227ull -> 11771896598749089907ull +// 3221854327620463942ull -> 12560649180545194470ull +// 17879106457247694620ull -> 14421558832844187772ull +// 12516768081916550737ull -> 4296761660805133425ull +// 1139783168782783090ull -> 742061714794648722ull +// 12488380805259075729ull -> 14109205439874640145ull +// 17657402560985447898ull -> 6057192239224243194ull +// 329254244800689628ull -> 18247263908222071868ull +// 3662055418137315433ull -> 17174776223039553865ull +// 7047152031506478874ull -> 13727420337195600730ull +// 1983174958676155844ull -> 2818312375125287588ull +// 923225174573989794ull -> 9915087812300743522ull +// 18280876562855262839ull -> 7161399393808283031ull +// 13719457309818612247ull -> 2862680592861432951ull +// 1969493590092048305ull -> 1307007383947855281ull +// 4113075276561899954ull -> 18356049985413183826ull +// 13646979131324573792ull -> 15935937615335005856ull +// 15106529513333095294ull -> 16415296467281494110ull +// 17890990718808582694ull -> 16803171265864625638ull +// 6506735898811973185ull -> 10957401283361829601ull +// 9170129921256422232ull -> 11252967136817419064ull +// 7575139446008419839ull -> 4760661672048575327ull +// 11761619212592004855ull -> 14915144339907298167ull +// 7566433005087203435ull -> 575503570718308907ull +// 6230002762718235250ull -> 5741952581690045810ull +// 14578437808706486766ull -> 1055864288829102030ull +// 12557658352255323163ull -> 16431710829567235323ull constexpr std::uint64_t kGroupsMag_rows[] = { - 1771071767131182639ull, 2319264234820563061ull, 3968516947995724204ull, 13748929559384796587ull, - 14337866189563969968ull, 121300902977360074ull, 13930328507840437447ull, 11293074932416602936ull, - 17806644710829561148ull, 11345590561057098901ull, 16975877748262402349ull, 10562941317592452443ull, - 12227261626285808416ull, 3101225148965538227ull, 3221854327620463942ull, 17879106457247694620ull, - 12516768081916550737ull, 1139783168782783090ull, 12488380805259075729ull, 17657402560985447898ull, - 329254244800689628ull, 3662055418137315433ull, 7047152031506478874ull, 1983174958676155844ull, - 923225174573989794ull, 18280876562855262839ull, 13719457309818612247ull, 1969493590092048305ull, - 4113075276561899954ull, 13646979131324573792ull, 15106529513333095294ull, 17890990718808582694ull, - 6506735898811973185ull, 9170129921256422232ull, 7575139446008419839ull, 11761619212592004855ull, - 7566433005087203435ull, 6230002762718235250ull, 14578437808706486766ull, 12557658352255323163ull, + 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, }; constexpr std::uint64_t kGroupsMag_at_bar[] = { 4ull, 0ull, 9ull, 0ull, @@ -1402,7 +1629,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). -constexpr std::uint64_t kGroupsMag_final = 12557658352255323163ull; +// 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; constexpr Trade kGroupsMag_trades[] = { {1736121840000LL, 1736122020000LL, 102.25, 100.75, 1, 0}, {1736122020000LL, 1736122500000LL, 100.25, 100.5, 2, 0}, @@ -1497,17 +1725,58 @@ constexpr Trade kGroupsMag_trades[] = { // 18011553307993037888ull -> 1600250331183838308ull // 9349597393465852520ull -> 11097052730880109561ull // 3665904849405917965ull -> 13378853722070711842ull +// expectation corrected (V19-FIX, kStopLimit_rows, 40 of 40 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 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: +// 4015678377754031763ull -> 4402211772532359251ull +// 9277256445902250361ull -> 5449557300220864793ull +// 7787615283403152830ull -> 2575787073227669374ull +// 9402632213295178026ull -> 9938418473191636202ull +// 15777447031447382573ull -> 10058127381824367565ull +// 1608910765213702288ull -> 1315470856055524560ull +// 8731220795971939073ull -> 15263698702667167777ull +// 5600775303740731350ull -> 13220310601440668918ull +// 6788919839149980292ull -> 15409637845860903908ull +// 17608363530245642196ull -> 6446798858981198964ull +// 18227871926589828513ull -> 10451839227886399841ull +// 9190871252466246446ull -> 11816001997725235374ull +// 13585156238110074232ull -> 1932657111780796664ull +// 4377935914087343184ull -> 5608877402063171472ull +// 484123207012063444ull -> 4816751693073396948ull +// 16208949663969524763ull -> 406036428804793947ull +// 12701173708241788309ull -> 8411172447822998997ull +// 15919837620478786311ull -> 9566812430047964551ull +// 7210516455815976589ull -> 7047212701932505101ull +// 10435577839346943286ull -> 12944086715960255318ull +// 16020477384123899678ull -> 2932986234293574878ull +// 6460320043547129038ull -> 3851484164654454670ull +// 2173034416395231487ull -> 5793783382051423327ull +// 1745821339674292435ull -> 17080321546688673331ull +// 2929233302710984265ull -> 15357710425425554761ull +// 5366701996650499488ull -> 17224699499285056096ull +// 9271513753148892055ull -> 10136441707663244279ull +// 4539748013823005514ull -> 17829354381425508170ull +// 15991039173201268618ull -> 12155373643870828842ull +// 13840553339973172792ull -> 10715131222481220760ull +// 848639617528874544ull -> 4867968313932752304ull +// 3081441532474388176ull -> 4064453165671217168ull +// 16913374911412348286ull -> 1769313611189948830ull +// 3098179540339637900ull -> 4504345708664910252ull +// 16397906308196786061ull -> 782093603809331885ull +// 16552233276333719913ull -> 18267060294147680041ull +// 17559531532379237941ull -> 2944439646726608949ull +// 1600250331183838308ull -> 2477382922949126500ull +// 11097052730880109561ull -> 9214756763675162553ull +// 13378853722070711842ull -> 6336494971372918338ull constexpr std::uint64_t kStopLimit_rows[] = { - 4015678377754031763ull, 9277256445902250361ull, 7787615283403152830ull, 9402632213295178026ull, - 15777447031447382573ull, 1608910765213702288ull, 8731220795971939073ull, 5600775303740731350ull, - 6788919839149980292ull, 17608363530245642196ull, 18227871926589828513ull, 9190871252466246446ull, - 13585156238110074232ull, 4377935914087343184ull, 484123207012063444ull, 16208949663969524763ull, - 12701173708241788309ull, 15919837620478786311ull, 7210516455815976589ull, 10435577839346943286ull, - 16020477384123899678ull, 6460320043547129038ull, 2173034416395231487ull, 1745821339674292435ull, - 2929233302710984265ull, 5366701996650499488ull, 9271513753148892055ull, 4539748013823005514ull, - 15991039173201268618ull, 13840553339973172792ull, 848639617528874544ull, 3081441532474388176ull, - 16913374911412348286ull, 3098179540339637900ull, 16397906308196786061ull, 16552233276333719913ull, - 17559531532379237941ull, 1600250331183838308ull, 11097052730880109561ull, 13378853722070711842ull, + 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, }; constexpr std::uint64_t kStopLimit_at_bar[] = { 4ull, 0ull, 13ull, 11ull, @@ -1576,7 +1845,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). -constexpr std::uint64_t kStopLimit_final = 13378853722070711842ull; +// 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; constexpr Trade kStopLimit_trades[] = { {1736121660000LL, 1736121780000LL, 100.5, 101.5, 1, 0}, {1736121660000LL, 1736121900000LL, 100.5, 102, 1, 0}, @@ -1674,17 +1944,58 @@ constexpr Trade kStopLimit_trades[] = { // 4812994545824716644ull -> 10084593582520942716ull // 7351634746524896233ull -> 7804430760616484748ull // 15015254700001776595ull -> 12421926773651986280ull +// expectation corrected (V19-FIX, kStopLimitMag_rows, 40 of 40 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 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: +// 1426413119368727739ull -> 11157594904456853627ull +// 11729774567786367716ull -> 8401675850175966532ull +// 14624533615872801840ull -> 14768083129611579792ull +// 10980939852557468738ull -> 2813084369439848034ull +// 4981677886757282483ull -> 680081896452532403ull +// 7056152781752200150ull -> 3114044039354922134ull +// 222204814676374947ull -> 18101581505709266307ull +// 7215345707064513231ull -> 3765585083570211695ull +// 12693358032236412920ull -> 5126585860831024376ull +// 3137132028661502508ull -> 15762785015022297388ull +// 12339842671295629499ull -> 14002286754420941147ull +// 10538758131920122431ull -> 9222864100890729823ull +// 13851917084666735043ull -> 6646548798123240355ull +// 13459118864209887574ull -> 6473759346034722294ull +// 7161694476307517243ull -> 12710876809005991003ull +// 14598182289160501452ull -> 3124661257347549740ull +// 8829564412694297346ull -> 2631460917473729378ull +// 15975791651856127856ull -> 6218399147747637008ull +// 4147368694952482168ull -> 7885746142563048312ull +// 17133134770366795920ull -> 7465774284363189296ull +// 9213888144382479152ull -> 15336532554253228688ull +// 4003628671076185617ull -> 3052942228447511665ull +// 10964253458288959331ull -> 11131484997342707171ull +// 6180087392066919114ull -> 12129492814419792298ull +// 9070734654282114891ull -> 16582911731330051275ull +// 4908014039146416295ull -> 14696325906442206119ull +// 4046059271741912732ull -> 13365743854012499388ull +// 2049239309329407579ull -> 271739893287090075ull +// 15583436013445152044ull -> 1688196853701772460ull +// 3810321856910520893ull -> 1042572030152166013ull +// 5617254463899745622ull -> 18233864280626554198ull +// 16003016603397951197ull -> 14647413247529149725ull +// 8704315988438976676ull -> 4738394210991551364ull +// 1726448424722868060ull -> 10052202710057429660ull +// 16728008101916756905ull -> 3980637583370132169ull +// 8640413929875652210ull -> 11984610609265407890ull +// 15589794694703369101ull -> 17391162522930315245ull +// 10084593582520942716ull -> 17028258441606210396ull +// 7804430760616484748ull -> 8328930773731943340ull +// 12421926773651986280ull -> 9254963348917194152ull constexpr std::uint64_t kStopLimitMag_rows[] = { - 1426413119368727739ull, 11729774567786367716ull, 14624533615872801840ull, 10980939852557468738ull, - 4981677886757282483ull, 7056152781752200150ull, 222204814676374947ull, 7215345707064513231ull, - 12693358032236412920ull, 3137132028661502508ull, 12339842671295629499ull, 10538758131920122431ull, - 13851917084666735043ull, 13459118864209887574ull, 7161694476307517243ull, 14598182289160501452ull, - 8829564412694297346ull, 15975791651856127856ull, 4147368694952482168ull, 17133134770366795920ull, - 9213888144382479152ull, 4003628671076185617ull, 10964253458288959331ull, 6180087392066919114ull, - 9070734654282114891ull, 4908014039146416295ull, 4046059271741912732ull, 2049239309329407579ull, - 15583436013445152044ull, 3810321856910520893ull, 5617254463899745622ull, 16003016603397951197ull, - 8704315988438976676ull, 1726448424722868060ull, 16728008101916756905ull, 8640413929875652210ull, - 15589794694703369101ull, 10084593582520942716ull, 7804430760616484748ull, 12421926773651986280ull, + 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, }; constexpr std::uint64_t kStopLimitMag_at_bar[] = { 4ull, 0ull, 13ull, 0ull, @@ -1753,7 +2064,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). -constexpr std::uint64_t kStopLimitMag_final = 12421926773651986280ull; +// 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; constexpr Trade kStopLimitMag_trades[] = { {1736121660000LL, 1736121780000LL, 100.25, 101.5, 1, 0}, {1736121660000LL, 1736121900000LL, 100.25, 102, 1, 0}, @@ -1803,11 +2115,28 @@ constexpr Trade kStopLimitMag_trades[] = { // 4265099920160034432ull -> 117536285131979531ull // 11117851636219666803ull -> 15011555328205275466ull // 15339934536417004376ull -> 10880804929896122760ull +// expectation corrected (V19-FIX, kQuiet_rows, 16 of 16 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 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: +// 2251190330680026869ull -> 428451890117060597ull +// 15910380323894423478ull -> 1939867743749048694ull +// 14880679048257127815ull -> 11195157439215676551ull +// 16864304942012172441ull -> 13964997473137927097ull +// 2793384994174118460ull -> 9212479816748968188ull +// 7368859546216696052ull -> 16000164592560197492ull +// 7236161865638212397ull -> 8130410293855426317ull +// 9180718770365304561ull -> 10300840897873716753ull +// 534753123503696323ull -> 16568174616242417603ull +// 257995351140209626ull -> 17876633525817055002ull +// 4505359601117927185ull -> 10659236710582653713ull +// 13588639401292320613ull -> 17983194377250571525ull +// 316557109720746540ull -> 2047181318263126988ull +// 117536285131979531ull -> 14672633943864425963ull +// 15011555328205275466ull -> 5161029633467225738ull +// 10880804929896122760ull -> 7726211904455540872ull constexpr std::uint64_t kQuiet_rows[] = { - 2251190330680026869ull, 15910380323894423478ull, 14880679048257127815ull, 16864304942012172441ull, - 2793384994174118460ull, 7368859546216696052ull, 7236161865638212397ull, 9180718770365304561ull, - 534753123503696323ull, 257995351140209626ull, 4505359601117927185ull, 13588639401292320613ull, - 316557109720746540ull, 117536285131979531ull, 15011555328205275466ull, 10880804929896122760ull, + 428451890117060597ull, 1939867743749048694ull, 11195157439215676551ull, 13964997473137927097ull, + 9212479816748968188ull, 16000164592560197492ull, 8130410293855426317ull, 10300840897873716753ull, + 16568174616242417603ull, 17876633525817055002ull, 10659236710582653713ull, 17983194377250571525ull, + 2047181318263126988ull, 14672633943864425963ull, 5161029633467225738ull, 7726211904455540872ull, }; constexpr std::uint64_t kQuiet_at_bar[] = { 4ull, 0ull, 9ull, 0ull, @@ -1840,7 +2169,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). -constexpr std::uint64_t kQuiet_final = 10880804929896122760ull; +// 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; constexpr Trade kQuiet_trades[] = { {1736121720000LL, 1736121840000LL, 101.5, 102.25, 2, 0}, }; @@ -1879,11 +2209,28 @@ constexpr Trade kQuiet_trades[] = { // 14660228885507672831ull -> 10276732342521370229ull // 17539868185903775948ull -> 7149263525418787648ull // 15979450874488009551ull -> 18272199307858808366ull +// expectation corrected (V19-FIX, kQuietMag_rows, 16 of 16 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 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: +// 1771071767131182639ull -> 18395077400277767983ull +// 11511402876228497534ull -> 8974918290184464318ull +// 13505881934138930069ull -> 9820360325097478805ull +// 17435988220685787619ull -> 9357979130704319683ull +// 3320320633831017015ull -> 1380600006529331319ull +// 14967137603421812042ull -> 3158417560156675530ull +// 8853087616657613471ull -> 3834533453170743615ull +// 18314669170025281607ull -> 4903111063815600551ull +// 10755318122153801645ull -> 8341995541182971309ull +// 10163627219265018624ull -> 1420418759607814080ull +// 114643937275360575ull -> 6268521046740087103ull +// 598916062260217647ull -> 8200284679939302671ull +// 13904582099442899970ull -> 4063870167578037858ull +// 10276732342521370229ull -> 14848461357115496341ull +// 7149263525418787648ull -> 8190184809671648256ull +// 18272199307858808366ull -> 15117606282418226478ull constexpr std::uint64_t kQuietMag_rows[] = { - 1771071767131182639ull, 11511402876228497534ull, 13505881934138930069ull, 17435988220685787619ull, - 3320320633831017015ull, 14967137603421812042ull, 8853087616657613471ull, 18314669170025281607ull, - 10755318122153801645ull, 10163627219265018624ull, 114643937275360575ull, 598916062260217647ull, - 13904582099442899970ull, 10276732342521370229ull, 7149263525418787648ull, 18272199307858808366ull, + 18395077400277767983ull, 8974918290184464318ull, 9820360325097478805ull, 9357979130704319683ull, + 1380600006529331319ull, 3158417560156675530ull, 3834533453170743615ull, 4903111063815600551ull, + 8341995541182971309ull, 1420418759607814080ull, 6268521046740087103ull, 8200284679939302671ull, + 4063870167578037858ull, 14848461357115496341ull, 8190184809671648256ull, 15117606282418226478ull, }; constexpr std::uint64_t kQuietMag_at_bar[] = { 4ull, 0ull, 9ull, 0ull, @@ -1916,7 +2263,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). -constexpr std::uint64_t kQuietMag_final = 18272199307858808366ull; +// 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; 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 04c924ab..2df1349f 100644 --- a/tests/test_adapter_recording_hash_witness.cpp +++ b/tests/test_adapter_recording_hash_witness.cpp @@ -342,39 +342,72 @@ void emit(Scenario scenario, const Observed& got) { // 8364385477967995923ull -> 11423048270061715692ull // 3220915131726720827ull -> 13118814681321380879ull // 6186330499779649827ull -> 8426887344131973464ull +// expectation corrected (V19-FIX, kSubmit_rows, 32 of 32 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: +// 16885932052227463976ull -> 8933562200260968840ull +// 1832414573103394886ull -> 16496088491414191334ull +// 6662732155889098474ull -> 17745030456223078154ull +// 16935908075746659915ull -> 15976454321452418827ull +// 8670184406548067167ull -> 13867778238676275167ull +// 17311759586000019611ull -> 10620212436062486555ull +// 5110535151530170856ull -> 16387014991078439144ull +// 17050466140112728026ull -> 5372310558802353946ull +// 4513821336585640874ull -> 8117928017652874474ull +// 9506584848066022008ull -> 12227480580368604280ull +// 34481296660224287ull -> 11671431865371234655ull +// 16226829862460102889ull -> 15852753888607827401ull +// 5715736824005291633ull -> 4564599765874863441ull +// 16672763487273857144ull -> 12753515022268993432ull +// 11194061843429059943ull -> 16693007526387479527ull +// 2985158813983697164ull -> 9553609781489681132ull +// 14754578871476587428ull -> 9723295982254899748ull +// 1606482689304043337ull -> 9596448656160987273ull +// 5495385592042698151ull -> 14172117111020274407ull +// 2522232666137668356ull -> 1811404642983946404ull +// 4085198162701970644ull -> 6600438489637577460ull +// 7117747547444830272ull -> 1823325663201613856ull +// 14444814725304782334ull -> 671600216917334430ull +// 1707217372995824821ull -> 14450998569007950837ull +// 12266542813062942244ull -> 18123601071981623076ull +// 7182805577633544037ull -> 5788029478896071429ull +// 10829573004894599276ull -> 5791781074908951980ull +// 7789554680157984885ull -> 8180458203668078357ull +// 13305448560089262767ull -> 10842259428116090863ull +// 11423048270061715692ull -> 7789401651194802796ull +// 13118814681321380879ull -> 16644199069697802031ull +// 8426887344131973464ull -> 13033963886965614456ull constexpr std::uint64_t kSubmit_rows[] = { - 16885932052227463976ull, - 1832414573103394886ull, - 6662732155889098474ull, - 16935908075746659915ull, - 8670184406548067167ull, - 17311759586000019611ull, - 5110535151530170856ull, - 17050466140112728026ull, - 4513821336585640874ull, - 9506584848066022008ull, - 34481296660224287ull, - 16226829862460102889ull, - 5715736824005291633ull, - 16672763487273857144ull, - 11194061843429059943ull, - 2985158813983697164ull, - 14754578871476587428ull, - 1606482689304043337ull, - 5495385592042698151ull, - 2522232666137668356ull, - 4085198162701970644ull, - 7117747547444830272ull, - 14444814725304782334ull, - 1707217372995824821ull, - 12266542813062942244ull, - 7182805577633544037ull, - 10829573004894599276ull, - 7789554680157984885ull, - 13305448560089262767ull, - 11423048270061715692ull, - 13118814681321380879ull, - 8426887344131973464ull, + 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, }; // 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 @@ -390,17 +423,25 @@ constexpr std::uint64_t kSubmit_rows[] = { // 15777849557804947821ull -> 3006894002872600101ull // 4845782216021612032ull -> 14061760729711229018ull // 10452897047571570010ull -> 11455159794740779404ull +// expectation corrected (V19-FIX, kSubmit_reads, 6 of 6 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: +// 10505763622515464111ull -> 5783964658104710927ull +// 7912407605268438972ull -> 9790324176931117308ull +// 14277317571452203719ull -> 8640216615515382983ull +// 3006894002872600101ull -> 18016456059351763749ull +// 14061760729711229018ull -> 6161691521401770042ull +// 11455159794740779404ull -> 11652988695302303084ull constexpr std::uint64_t kSubmit_reads[] = { - 10505763622515464111ull, - 7912407605268438972ull, - 14277317571452203719ull, - 3006894002872600101ull, - 14061760729711229018ull, - 11455159794740779404ull, + 5783964658104710927ull, + 9790324176931117308ull, + 8640216615515382983ull, + 18016456059351763749ull, + 6161691521401770042ull, + 11652988695302303084ull, }; // 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). -constexpr std::uint64_t kSubmit_final = 8426887344131973464ull; +// 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; constexpr Trade kSubmit_trades[] = { {1736121720000LL, 1736122260000LL, 101.5, 102, 2, 0}, {1736122080000LL, 1736122620000LL, 100, 101, 2, 0}, @@ -473,39 +514,72 @@ constexpr Trade kSubmit_trades[] = { // 3960465168252133786ull -> 1078279461778315641ull // 6210507725413841856ull -> 13355807928615532752ull // 2578691877718696634ull -> 836327192465797433ull +// expectation corrected (V19-FIX, kReplace_rows, 32 of 32 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: +// 16885932052227463976ull -> 8933562200260968840ull +// 17547842047240632273ull -> 3907924323473264241ull +// 13652197343159488211ull -> 14461884941578386867ull +// 6374388350102285126ull -> 16511157315480471622ull +// 1467195078376742887ull -> 3959552939271361991ull +// 11968361826170869554ull -> 2555551956738490898ull +// 4128651056395996559ull -> 10502649610878884111ull +// 15934564140823409182ull -> 18306494604560422814ull +// 11303720518502300590ull -> 17185448195161859406ull +// 11904649057339333172ull -> 15450494133515205524ull +// 11048733698266497284ull -> 6827766899990563012ull +// 16674821699842999755ull -> 181096906100466027ull +// 16437822789666463784ull -> 3679474327996900584ull +// 2093244317622874278ull -> 5841986886798320038ull +// 12114907797348974860ull -> 1819868140276584460ull +// 3352113756351333150ull -> 4420902119403666462ull +// 9656428624069983049ull -> 13480284355483224649ull +// 18340684566969229321ull -> 7175317954498263753ull +// 13581693612191648950ull -> 17073623445577360118ull +// 2146300135587413999ull -> 17009991270755579471ull +// 2784997083595937551ull -> 12090343301005594287ull +// 6656116134246569845ull -> 13041638478193314581ull +// 2967091859373217631ull -> 1923525511758336863ull +// 7218379802597322779ull -> 1774789224381390235ull +// 4433033628872207973ull -> 15509873019280549637ull +// 18318911876151815972ull -> 18049355505317406500ull +// 7063083486589508720ull -> 9113574443316558064ull +// 1398286556357525565ull -> 52835018303832477ull +// 14302900766917140512ull -> 17600220618483225632ull +// 1078279461778315641ull -> 4090055281926467129ull +// 13355807928615532752ull -> 10973342611545588080ull +// 836327192465797433ull -> 17124982032285163865ull constexpr std::uint64_t kReplace_rows[] = { - 16885932052227463976ull, - 17547842047240632273ull, - 13652197343159488211ull, - 6374388350102285126ull, - 1467195078376742887ull, - 11968361826170869554ull, - 4128651056395996559ull, - 15934564140823409182ull, - 11303720518502300590ull, - 11904649057339333172ull, - 11048733698266497284ull, - 16674821699842999755ull, - 16437822789666463784ull, - 2093244317622874278ull, - 12114907797348974860ull, - 3352113756351333150ull, - 9656428624069983049ull, - 18340684566969229321ull, - 13581693612191648950ull, - 2146300135587413999ull, - 2784997083595937551ull, - 6656116134246569845ull, - 2967091859373217631ull, - 7218379802597322779ull, - 4433033628872207973ull, - 18318911876151815972ull, - 7063083486589508720ull, - 1398286556357525565ull, - 14302900766917140512ull, - 1078279461778315641ull, - 13355807928615532752ull, - 836327192465797433ull, + 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, }; // 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 @@ -525,19 +599,29 @@ constexpr std::uint64_t kReplace_rows[] = { // 4839696889052561061ull -> 1583090386514829653ull // 11379263290234193203ull -> 10955971990176778399ull // 10164053459169499104ull -> 5763828109735666890ull +// expectation corrected (V19-FIX, kReplace_reads, 8 of 8 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: +// 6332508079166398204ull -> 2326526227949002076ull +// 8080051585834415455ull -> 13390056939270062271ull +// 18331632290663282909ull -> 10021657182331917789ull +// 3083764297320273482ull -> 14242637685443801194ull +// 1018849945992994654ull -> 711594472462182014ull +// 1583090386514829653ull -> 4857350833193818325ull +// 10955971990176778399ull -> 4317633836685601759ull +// 5763828109735666890ull -> 1066029825997878730ull constexpr std::uint64_t kReplace_reads[] = { - 6332508079166398204ull, - 8080051585834415455ull, - 18331632290663282909ull, - 3083764297320273482ull, - 1018849945992994654ull, - 1583090386514829653ull, - 10955971990176778399ull, - 5763828109735666890ull, + 2326526227949002076ull, + 13390056939270062271ull, + 10021657182331917789ull, + 14242637685443801194ull, + 711594472462182014ull, + 4857350833193818325ull, + 4317633836685601759ull, + 1066029825997878730ull, }; // 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). -constexpr std::uint64_t kReplace_final = 836327192465797433ull; +// 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; constexpr Trade kReplace_trades[] = { {1736122080000LL, 1736122620000LL, 100.25, 101.25, 2, 0}, }; @@ -608,39 +692,72 @@ constexpr Trade kReplace_trades[] = { // 7652020385574100696ull -> 6317668264567744969ull // 13501287520271667536ull -> 7015387317176510425ull // 12693965216151243664ull -> 12078141763174815190ull +// expectation corrected (V19-FIX, kCancel_rows, 32 of 32 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: +// 16885932052227463976ull -> 8933562200260968840ull +// 2527405596089513137ull -> 12907991694751175761ull +// 1474215827418029803ull -> 15492238351520247563ull +// 9280260176724097047ull -> 4949954141317803287ull +// 15321716256000571609ull -> 6869043455093337913ull +// 12073207399592924275ull -> 16700973384839330835ull +// 14164035305485858490ull -> 13907977122257783994ull +// 10545706318800751488ull -> 13690802804532944832ull +// 1775489816151474656ull -> 6641415404822534272ull +// 318726799888700949ull -> 17617700982422257653ull +// 1586484278605998308ull -> 8957395621418722884ull +// 11382308868487550244ull -> 15533748013016881700ull +// 10872772640051750680ull -> 10113927666003718936ull +// 4875115919163760185ull -> 10748025209891027257ull +// 7483740104829087331ull -> 7405357053482683011ull +// 1767653502614296251ull -> 14210999101428183707ull +// 3897954235023743852ull -> 5089886391592128332ull +// 3572309019897074723ull -> 1890214210532379459ull +// 6080227582324520130ull -> 15715071324930605218ull +// 16204570696603541185ull -> 12760333961958415553ull +// 8962979779500613143ull -> 1349906610605744215ull +// 3890978689718480682ull -> 11872699637369758186ull +// 7128828949941915251ull -> 6278706775858866003ull +// 15641506033444665247ull -> 8489616688255836383ull +// 17384785312098846605ull -> 8131243489711240877ull +// 11912041959994493277ull -> 73524659065323965ull +// 2066894531641232293ull -> 13120948974577109893ull +// 18138164710109019672ull -> 13036340438400450584ull +// 13099705273927855288ull -> 17320509982992261848ull +// 6317668264567744969ull -> 16001552197493914473ull +// 7015387317176510425ull -> 12375923388653592921ull +// 12078141763174815190ull -> 17283281126178883990ull constexpr std::uint64_t kCancel_rows[] = { - 16885932052227463976ull, - 2527405596089513137ull, - 1474215827418029803ull, - 9280260176724097047ull, - 15321716256000571609ull, - 12073207399592924275ull, - 14164035305485858490ull, - 10545706318800751488ull, - 1775489816151474656ull, - 318726799888700949ull, - 1586484278605998308ull, - 11382308868487550244ull, - 10872772640051750680ull, - 4875115919163760185ull, - 7483740104829087331ull, - 1767653502614296251ull, - 3897954235023743852ull, - 3572309019897074723ull, - 6080227582324520130ull, - 16204570696603541185ull, - 8962979779500613143ull, - 3890978689718480682ull, - 7128828949941915251ull, - 15641506033444665247ull, - 17384785312098846605ull, - 11912041959994493277ull, - 2066894531641232293ull, - 18138164710109019672ull, - 13099705273927855288ull, - 6317668264567744969ull, - 7015387317176510425ull, - 12078141763174815190ull, + 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, }; // 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 @@ -682,30 +799,51 @@ constexpr std::uint64_t kCancel_rows[] = { // 16248277367358811299ull -> 332263738452834604ull // 8861077440513858845ull -> 11019138486722009680ull // 6769299827481558054ull -> 1811755191633478047ull +// expectation corrected (V19-FIX, kCancel_reads, 19 of 19 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: +// 14973544158008760732ull -> 5066383747043332732ull +// 11798837567516325992ull -> 14642164039413495816ull +// 12592890165665480327ull -> 1826995519720684135ull +// 12442547841905437363ull -> 10600767793424807315ull +// 4200291776743362805ull -> 18413531801076714261ull +// 6978595633330587854ull -> 7162595064832704878ull +// 4620048550389167469ull -> 8771487694918498925ull +// 4486444215607032147ull -> 3727599241559000403ull +// 14610824379851499445ull -> 2036989596869214901ull +// 13884124081678429497ull -> 15857207205534857561ull +// 8996981246421058099ull -> 5509984618602967315ull +// 16696428861158375881ull -> 227260307711721321ull +// 7037104328891838191ull -> 14543777879962894095ull +// 12474725854359419609ull -> 2123158589903514105ull +// 14933168323669631219ull -> 11488931589024505587ull +// 13091180856139824017ull -> 6035902334985171665ull +// 332263738452834604ull -> 6330997617635358220ull +// 11019138486722009680ull -> 7622013860668800144ull +// 1811755191633478047ull -> 7135414897549443327ull constexpr std::uint64_t kCancel_reads[] = { - 14973544158008760732ull, - 11798837567516325992ull, - 12592890165665480327ull, - 12442547841905437363ull, - 4200291776743362805ull, - 6978595633330587854ull, - 4620048550389167469ull, - 4486444215607032147ull, - 14610824379851499445ull, - 13884124081678429497ull, - 8996981246421058099ull, - 16696428861158375881ull, - 7037104328891838191ull, - 12474725854359419609ull, - 14933168323669631219ull, - 13091180856139824017ull, - 332263738452834604ull, - 11019138486722009680ull, - 1811755191633478047ull, + 5066383747043332732ull, + 14642164039413495816ull, + 1826995519720684135ull, + 10600767793424807315ull, + 18413531801076714261ull, + 7162595064832704878ull, + 8771487694918498925ull, + 3727599241559000403ull, + 2036989596869214901ull, + 15857207205534857561ull, + 5509984618602967315ull, + 227260307711721321ull, + 14543777879962894095ull, + 2123158589903514105ull, + 11488931589024505587ull, + 6035902334985171665ull, + 6330997617635358220ull, + 7622013860668800144ull, + 7135414897549443327ull, }; // 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). -constexpr std::uint64_t kCancel_final = 12078141763174815190ull; +// 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; constexpr Trade kCancel_trades[] = { {1736122980000LL, 1736123340000LL, 101, 102, 2, 0}, }; @@ -776,39 +914,72 @@ constexpr Trade kCancel_trades[] = { // 4219282542331499751ull -> 312036520819661464ull // 16213827754665597595ull -> 15975581040180058350ull // 14677771083385306600ull -> 15465526605242472570ull +// expectation corrected (V19-FIX, kOca_rows, 32 of 32 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: +// 16885932052227463976ull -> 8933562200260968840ull +// 1764667717626824743ull -> 16488569157039504391ull +// 4322727655245959010ull -> 15308668032647221442ull +// 1395244849641228558ull -> 11025020666800289870ull +// 2140358424951924404ull -> 17376859899643293076ull +// 9699116680553236170ull -> 18031995174781967338ull +// 7946281078129361061ull -> 9135991105341529701ull +// 7217030725170179233ull -> 12521501026388244289ull +// 13130200612041399664ull -> 17387999442892602416ull +// 5287199831653622788ull -> 17523447275006301316ull +// 3506631918949547901ull -> 700544763663270973ull +// 4465527976539731669ull -> 6419555011776275829ull +// 5688012065610201252ull -> 9501522493702417732ull +// 15517873125024367971ull -> 2198037920276336707ull +// 5873090822233458790ull -> 3899965171878021222ull +// 8742784406691043605ull -> 17400108061693215989ull +// 3606250785412036839ull -> 6268695127500094535ull +// 8169195814552631507ull -> 15386469441856039635ull +// 14272420827081783476ull -> 1827308857775093108ull +// 16588921592304671538ull -> 9002315039531479122ull +// 3210670836326271236ull -> 4336820592134659556ull +// 2166897132651157340ull -> 1568941454279137340ull +// 8856912385722987180ull -> 3586528324041147724ull +// 8380781416426648794ull -> 9921466732535290906ull +// 6191340745574236276ull -> 2210251816763789972ull +// 6226269427383285383ull -> 12540125623729258183ull +// 15465297086043070479ull -> 4922139678029842511ull +// 10592838278007696860ull -> 14880777043208119420ull +// 15117205657257447887ull -> 766085567512147215ull +// 312036520819661464ull -> 12667708910662510744ull +// 15975581040180058350ull -> 17116188563837257134ull +// 15465526605242472570ull -> 18394579596403092954ull constexpr std::uint64_t kOca_rows[] = { - 16885932052227463976ull, - 1764667717626824743ull, - 4322727655245959010ull, - 1395244849641228558ull, - 2140358424951924404ull, - 9699116680553236170ull, - 7946281078129361061ull, - 7217030725170179233ull, - 13130200612041399664ull, - 5287199831653622788ull, - 3506631918949547901ull, - 4465527976539731669ull, - 5688012065610201252ull, - 15517873125024367971ull, - 5873090822233458790ull, - 8742784406691043605ull, - 3606250785412036839ull, - 8169195814552631507ull, - 14272420827081783476ull, - 16588921592304671538ull, - 3210670836326271236ull, - 2166897132651157340ull, - 8856912385722987180ull, - 8380781416426648794ull, - 6191340745574236276ull, - 6226269427383285383ull, - 15465297086043070479ull, - 10592838278007696860ull, - 15117205657257447887ull, - 312036520819661464ull, - 15975581040180058350ull, - 15465526605242472570ull, + 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, }; // 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 @@ -836,23 +1007,37 @@ constexpr std::uint64_t kOca_rows[] = { // 18038646180534657621ull -> 12870434045680047141ull // 7648413869989003340ull -> 5966302497310935881ull // 17778673258699498459ull -> 5749065519581012093ull +// expectation corrected (V19-FIX, kOca_reads, 12 of 12 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: +// 5684519712303414667ull -> 3105752370505168555ull +// 15423667651443634118ull -> 13318576850138903270ull +// 4216971117943841170ull -> 4943429593008048082ull +// 1090467243908742462ull -> 7900987300136689534ull +// 9157803620682564923ull -> 16889527581053605563ull +// 10534790258471791480ull -> 8561664608116353912ull +// 11014335311344812052ull -> 10377670066927504148ull +// 14978113568142998395ull -> 14341448323725690491ull +// 10741037411738045890ull -> 9080486838152177794ull +// 12870434045680047141ull -> 3016108360062111013ull +// 5966302497310935881ull -> 14134368882450491401ull +// 5749065519581012093ull -> 12755516166577050941ull constexpr std::uint64_t kOca_reads[] = { - 5684519712303414667ull, - 15423667651443634118ull, - 4216971117943841170ull, - 1090467243908742462ull, - 9157803620682564923ull, - 10534790258471791480ull, - 11014335311344812052ull, - 14978113568142998395ull, - 10741037411738045890ull, - 12870434045680047141ull, - 5966302497310935881ull, - 5749065519581012093ull, + 3105752370505168555ull, + 13318576850138903270ull, + 4943429593008048082ull, + 7900987300136689534ull, + 16889527581053605563ull, + 8561664608116353912ull, + 10377670066927504148ull, + 14341448323725690491ull, + 9080486838152177794ull, + 3016108360062111013ull, + 14134368882450491401ull, + 12755516166577050941ull, }; // 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). -constexpr std::uint64_t kOca_final = 15465526605242472570ull; +// 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; constexpr Trade kOca_trades[] = { {1736121960000LL, 1736122020000LL, 100.75, 100.75, 2, 0}, {1736122500000LL, 1736122980000LL, 100.5, 101, 2, 0}, @@ -926,39 +1111,72 @@ constexpr Trade kOca_trades[] = { // 13204409897368598050ull -> 13079457144551358245ull // 5202308151562800438ull -> 10683567794089412281ull // 5214787630894246342ull -> 14699793179829041832ull +// expectation corrected (V19-FIX, kReissue_rows, 32 of 32 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: +// 10502385223359640218ull -> 11371589716169913082ull +// 16623997955197906952ull -> 11912421295804525448ull +// 811252746116859749ull -> 16162904808422149189ull +// 10758794706905446193ull -> 1884050390405525809ull +// 15617716314233334546ull -> 1630941943368694258ull +// 6767772143179858797ull -> 1646315788396378413ull +// 5521983648121747859ull -> 3202536020749475123ull +// 15617777410714397671ull -> 16739146824408715591ull +// 18135165643667734470ull -> 12465965272475823462ull +// 7866918066084324772ull -> 12416019024705234276ull +// 13083169176416003415ull -> 8019931705879186711ull +// 975673565472503276ull -> 10511748891040354060ull +// 3561629021139748812ull -> 7873647981647748428ull +// 2811479979004047652ull -> 13583506357494126788ull +// 16463592783525505269ull -> 6827456748487052757ull +// 12265787473190619113ull -> 4860432548138722281ull +// 12140961588712596768ull -> 10318257384229209728ull +// 10187321206192027662ull -> 13270746490627165070ull +// 3504288509090403179ull -> 15647754736958033867ull +// 15037639390730560913ull -> 15257413490644512113ull +// 7303096138953671912ull -> 3422115173668721064ull +// 9693853816007893072ull -> 11954333827210587376ull +// 8169735607747040047ull -> 16717809947662223375ull +// 2591689537386467449ull -> 16431286081784348505ull +// 14207231819871013441ull -> 14674774925924029409ull +// 5330583192549066300ull -> 8888789072620710620ull +// 13611713462682254439ull -> 7677274857978420999ull +// 10373123901283573478ull -> 8389711543599766598ull +// 16143045497135443280ull -> 11866583660361571056ull +// 13079457144551358245ull -> 8782474388336378853ull +// 10683567794089412281ull -> 12830062157342008889ull +// 14699793179829041832ull -> 11660101618206053704ull constexpr std::uint64_t kReissue_rows[] = { - 10502385223359640218ull, - 16623997955197906952ull, - 811252746116859749ull, - 10758794706905446193ull, - 15617716314233334546ull, - 6767772143179858797ull, - 5521983648121747859ull, - 15617777410714397671ull, - 18135165643667734470ull, - 7866918066084324772ull, - 13083169176416003415ull, - 975673565472503276ull, - 3561629021139748812ull, - 2811479979004047652ull, - 16463592783525505269ull, - 12265787473190619113ull, - 12140961588712596768ull, - 10187321206192027662ull, - 3504288509090403179ull, - 15037639390730560913ull, - 7303096138953671912ull, - 9693853816007893072ull, - 8169735607747040047ull, - 2591689537386467449ull, - 14207231819871013441ull, - 5330583192549066300ull, - 13611713462682254439ull, - 10373123901283573478ull, - 16143045497135443280ull, - 13079457144551358245ull, - 10683567794089412281ull, - 14699793179829041832ull, + 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, }; // 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 @@ -1006,33 +1224,57 @@ constexpr std::uint64_t kReissue_rows[] = { // 17779211270428359923ull -> 12085354800256371026ull // 13487320763960909382ull -> 13244348858005104150ull // 3448145313609564269ull -> 1758314424234447340ull +// expectation corrected (V19-FIX, kReissue_reads, 22 of 22 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 +// 12590785953099354599ull -> 14952922796737447431ull +// 17377990171183053043ull -> 350142526787915315ull +// 16572248048415135888ull -> 9287618455889763760ull +// 7437751090472884240ull -> 8049051401679342672ull +// 1419423412163814619ull -> 7334056035772970331ull +// 7256904348220848392ull -> 16777030856659722344ull +// 7147605502070139471ull -> 12861194332908904879ull +// 15244420174526167777ull -> 7839065249474270945ull +// 4351182266551369470ull -> 2385401613699651006ull +// 11321314328408210670ull -> 3151001040546167918ull +// 16652192784271152720ull -> 6768266663339224688ull +// 2529807158326617239ull -> 211423388548179255ull +// 8285441707971576996ull -> 10932414881536382884ull +// 7222939095386434042ull -> 15509429580401113050ull +// 8143676236938658752ull -> 272542696402235168ull +// 10250074498143150374ull -> 12650608718358639686ull +// 15356263522172424512ull -> 16954864996066484000ull +// 5156781438342475486ull -> 2072712234614561342ull +// 12085354800256371026ull -> 15829080338314944658ull +// 13244348858005104150ull -> 2272828286921366934ull +// 1758314424234447340ull -> 16041091954465179468ull constexpr std::uint64_t kReissue_reads[] = { - 14558537749344697025ull, - 12590785953099354599ull, - 17377990171183053043ull, - 16572248048415135888ull, - 7437751090472884240ull, - 1419423412163814619ull, - 7256904348220848392ull, - 7147605502070139471ull, - 15244420174526167777ull, - 4351182266551369470ull, - 11321314328408210670ull, - 16652192784271152720ull, - 2529807158326617239ull, - 8285441707971576996ull, - 7222939095386434042ull, - 8143676236938658752ull, - 10250074498143150374ull, - 15356263522172424512ull, - 5156781438342475486ull, - 12085354800256371026ull, - 13244348858005104150ull, - 1758314424234447340ull, + 9397758857253294369ull, + 14952922796737447431ull, + 350142526787915315ull, + 9287618455889763760ull, + 8049051401679342672ull, + 7334056035772970331ull, + 16777030856659722344ull, + 12861194332908904879ull, + 7839065249474270945ull, + 2385401613699651006ull, + 3151001040546167918ull, + 6768266663339224688ull, + 211423388548179255ull, + 10932414881536382884ull, + 15509429580401113050ull, + 272542696402235168ull, + 12650608718358639686ull, + 16954864996066484000ull, + 2072712234614561342ull, + 15829080338314944658ull, + 2272828286921366934ull, + 16041091954465179468ull, }; // 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). -constexpr std::uint64_t kReissue_final = 14699793179829041832ull; +// 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; constexpr Trade kReissue_trades[] = { {1736121660000LL, 1736123040000LL, 100.75, 99.75, 2, 0}, {1736123100000LL, 1736123460000LL, 100.75, 101, 2, 1}, @@ -1104,39 +1346,72 @@ constexpr Trade kReissue_trades[] = { // 1992576013327968808ull -> 15862752879538973131ull // 8467178688432171320ull -> 9351225767820645871ull // 11930736200467274982ull -> 11018457759974752048ull +// expectation corrected (V19-FIX, kTrail_rows, 32 of 32 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: +// 10502385223359640218ull -> 11371589716169913082ull +// 11059645349770920252ull -> 15827480248058921916ull +// 12090607753233108862ull -> 9179483745920742590ull +// 4332510127470754579ull -> 6778316597629456467ull +// 2269674153403661467ull -> 5172328729500611739ull +// 1708485212322368238ull -> 538732652451172654ull +// 15066207823399735690ull -> 6878958150024727018ull +// 17463983471023957816ull -> 8673040670875734808ull +// 10987770612284218667ull -> 1224102971077480171ull +// 8613262403787993426ull -> 4557268190807619538ull +// 10910001697921811365ull -> 9136468491734931429ull +// 13468567961207850309ull -> 6184955078515723301ull +// 17114810053420357407ull -> 12362884735948053375ull +// 13615766252800931030ull -> 15431697961609933942ull +// 11483795889704580492ull -> 1411625524442821644ull +// 3924269618188270202ull -> 12832334111417447482ull +// 4243281343999459289ull -> 5598316292658291609ull +// 9461891796031610162ull -> 1094550630882239090ull +// 6122795060570411598ull -> 9032605003773573902ull +// 18091606639727212491ull -> 655750707938363819ull +// 1843709319281047669ull -> 11787846682663979157ull +// 14954610474048933113ull -> 4271553443345200153ull +// 14845114560708251965ull -> 13505617900634614461ull +// 1504103451232493045ull -> 14516777496449283573ull +// 562770761915769535ull -> 8741802295233492767ull +// 848928317146655751ull -> 7191771264792402823ull +// 6260033633149799734ull -> 407930561309081398ull +// 6655917714355362997ull -> 10139811205313313301ull +// 28864162607152614ull -> 9692372728761498854ull +// 15862752879538973131ull -> 17629340520421506187ull +// 9351225767820645871ull -> 12781001967037913295ull +// 11018457759974752048ull -> 4953750954703410960ull constexpr std::uint64_t kTrail_rows[] = { - 10502385223359640218ull, - 11059645349770920252ull, - 12090607753233108862ull, - 4332510127470754579ull, - 2269674153403661467ull, - 1708485212322368238ull, - 15066207823399735690ull, - 17463983471023957816ull, - 10987770612284218667ull, - 8613262403787993426ull, - 10910001697921811365ull, - 13468567961207850309ull, - 17114810053420357407ull, - 13615766252800931030ull, - 11483795889704580492ull, - 3924269618188270202ull, - 4243281343999459289ull, - 9461891796031610162ull, - 6122795060570411598ull, - 18091606639727212491ull, - 1843709319281047669ull, - 14954610474048933113ull, - 14845114560708251965ull, - 1504103451232493045ull, - 562770761915769535ull, - 848928317146655751ull, - 6260033633149799734ull, - 6655917714355362997ull, - 28864162607152614ull, - 15862752879538973131ull, - 9351225767820645871ull, - 11018457759974752048ull, + 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, }; // 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 @@ -1144,13 +1419,17 @@ constexpr std::uint64_t kTrail_rows[] = { // expectation corrected (v19-E, kTrail_reads, 2 of 2 values), 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): // 12411711249896042591ull -> 14558537749344697025ull // 791379823776991059ull -> 12264623362903320190ull +// 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 constexpr std::uint64_t kTrail_reads[] = { - 14558537749344697025ull, - 12264623362903320190ull, + 9397758857253294369ull, + 8570208265068282686ull, }; // 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). -constexpr std::uint64_t kTrail_final = 11018457759974752048ull; +// 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; 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 7af87af5..83348973 100644 --- a/tests/test_adapter_report_relower.cpp +++ b/tests/test_adapter_report_relower.cpp @@ -308,31 +308,56 @@ constexpr Point kOrdinary_curve[] = { // 16479222579093225873ull -> 17070412854470623372ull // 7003778984678674920ull -> 5698741489304391014ull // 13107512069304394357ull -> 8249112969542764486ull +// expectation corrected (V19-FIX, kOrdinary_hashes, 24 of 24 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 curves, rows and marks did not move; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree: +// 2926214571128404135ull -> 10420073189004151623ull +// 2460311144695717203ull -> 13145042526267837939ull +// 8827033979564876247ull -> 2622782777793463991ull +// 12198137721016629270ull -> 17996446729054892726ull +// 8010034670511875064ull -> 3969181056128787992ull +// 14775446960658454710ull -> 16947117433974038ull +// 12026164355176752283ull -> 423735939073054907ull +// 17208159137580538219ull -> 18056612604689961163ull +// 5096664486441078049ull -> 12445011709498043361ull +// 16022104096870556683ull -> 16382730376298748267ull +// 4836076347901812102ull -> 12718112296054108390ull +// 8937674376376720246ull -> 913929622865489206ull +// 2703551404259163957ull -> 13218761675008378229ull +// 3061368442138030587ull -> 10388057217060506235ull +// 10178081078865716434ull -> 4373273430774112306ull +// 8206897629920030398ull -> 11946915320520331422ull +// 6525045815118222249ull -> 2472677882334572745ull +// 15184055765275961048ull -> 4709222664471492344ull +// 5759341035588050840ull -> 5785035152764109816ull +// 3484237427845144905ull -> 15834701310167426825ull +// 6855851226167457247ull -> 18416054639860847807ull +// 17070412854470623372ull -> 9654818568811123148ull +// 5698741489304391014ull -> 7514399512975361670ull +// 8249112969542764486ull -> 375912627396258150ull constexpr std::uint64_t kOrdinary_hashes[] = { - 2926214571128404135ull, - 2460311144695717203ull, - 8827033979564876247ull, - 12198137721016629270ull, - 8010034670511875064ull, - 14775446960658454710ull, - 12026164355176752283ull, - 17208159137580538219ull, - 5096664486441078049ull, - 16022104096870556683ull, - 4836076347901812102ull, - 8937674376376720246ull, - 2703551404259163957ull, - 3061368442138030587ull, - 10178081078865716434ull, - 8206897629920030398ull, - 6525045815118222249ull, - 15184055765275961048ull, - 5759341035588050840ull, - 3484237427845144905ull, - 6855851226167457247ull, - 17070412854470623372ull, - 5698741489304391014ull, - 8249112969542764486ull, + 10420073189004151623ull, + 13145042526267837939ull, + 2622782777793463991ull, + 17996446729054892726ull, + 3969181056128787992ull, + 16947117433974038ull, + 423735939073054907ull, + 18056612604689961163ull, + 12445011709498043361ull, + 16382730376298748267ull, + 12718112296054108390ull, + 913929622865489206ull, + 13218761675008378229ull, + 10388057217060506235ull, + 4373273430774112306ull, + 11946915320520331422ull, + 2472677882334572745ull, + 4709222664471492344ull, + 5785035152764109816ull, + 15834701310167426825ull, + 18416054639860847807ull, + 9654818568811123148ull, + 7514399512975361670ull, + 375912627396258150ull, }; constexpr Row kOrdinary_rows[] = { {1736121780000LL, 1736122140000LL, 101.5, 100.5, 2, -6, 4, 0.5, 5, 1, 0}, @@ -420,31 +445,56 @@ constexpr Point kCalcOnOrderFills_curve[] = { // 15121847794654015719ull -> 12518209463246424427ull // 15174618554357284082ull -> 2102874898517593241ull // 11557602030571190083ull -> 7311762479881515305ull +// expectation corrected (V19-FIX, kCalcOnOrderFills_hashes, 24 of 24 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 curves, rows and marks did not move; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree: +// 5145607761876851736ull -> 13492117694829868536ull +// 6152128856392094028ull -> 12939649384498536620ull +// 13682746943137549482ull -> 18283200234405460746ull +// 4503732430497534158ull -> 17715026773738590286ull +// 11859342531076363326ull -> 16652393141204816958ull +// 14194996582993046802ull -> 10681558687161056850ull +// 5247200169972194989ull -> 12828138156283959725ull +// 16195574192086600895ull -> 13337213902076129151ull +// 13971810610233433902ull -> 2897666538282385134ull +// 1413803196547873442ull -> 4846519892874967202ull +// 1952174628368416920ull -> 4182996206551988888ull +// 16821279773722983085ull -> 3828630408241748525ull +// 3730293276333437051ull -> 6013095397646776827ull +// 5931646053601729348ull -> 1201561623500117444ull +// 6622293103480280585ull -> 16339691134963647305ull +// 10117757211028084286ull -> 1360168785605943262ull +// 61715527004884585ull -> 10446097969456318121ull +// 4117698574911413933ull -> 2226082304527453549ull +// 7257550778589677645ull -> 5506525745160110349ull +// 336153049891645020ull -> 16314828352609565244ull +// 10004434679436164789ull -> 14800719633294876469ull +// 12518209463246424427ull -> 1572183010617309931ull +// 2102874898517593241ull -> 6980933373957006777ull +// 7311762479881515305ull -> 3071340150260882121ull constexpr std::uint64_t kCalcOnOrderFills_hashes[] = { - 5145607761876851736ull, - 6152128856392094028ull, - 13682746943137549482ull, - 4503732430497534158ull, - 11859342531076363326ull, - 14194996582993046802ull, - 5247200169972194989ull, - 16195574192086600895ull, - 13971810610233433902ull, - 1413803196547873442ull, - 1952174628368416920ull, - 16821279773722983085ull, - 3730293276333437051ull, - 5931646053601729348ull, - 6622293103480280585ull, - 10117757211028084286ull, - 61715527004884585ull, - 4117698574911413933ull, - 7257550778589677645ull, - 336153049891645020ull, - 10004434679436164789ull, - 12518209463246424427ull, - 2102874898517593241ull, - 7311762479881515305ull, + 13492117694829868536ull, + 12939649384498536620ull, + 18283200234405460746ull, + 17715026773738590286ull, + 16652393141204816958ull, + 10681558687161056850ull, + 12828138156283959725ull, + 13337213902076129151ull, + 2897666538282385134ull, + 4846519892874967202ull, + 4182996206551988888ull, + 3828630408241748525ull, + 6013095397646776827ull, + 1201561623500117444ull, + 16339691134963647305ull, + 1360168785605943262ull, + 10446097969456318121ull, + 2226082304527453549ull, + 5506525745160110349ull, + 16314828352609565244ull, + 14800719633294876469ull, + 1572183010617309931ull, + 6980933373957006777ull, + 3071340150260882121ull, }; constexpr Row kCalcOnOrderFills_rows[] = { {1736121780000LL, 1736122080000LL, 101.5, 100.5, 2, -6, 4, 0.5, 4.5, 1, 0}, @@ -532,31 +582,56 @@ constexpr Point kSuppressedTail_curve[] = { // 16479222579093225873ull -> 17070412854470623372ull // 7003778984678674920ull -> 5698741489304391014ull // 4761381105021282633ull -> 1169937375292956878ull +// expectation corrected (V19-FIX, kSuppressedTail_hashes, 24 of 24 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 curves, rows and marks did not move; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree: +// 2926214571128404135ull -> 10420073189004151623ull +// 2460311144695717203ull -> 13145042526267837939ull +// 8827033979564876247ull -> 2622782777793463991ull +// 12198137721016629270ull -> 17996446729054892726ull +// 8010034670511875064ull -> 3969181056128787992ull +// 14775446960658454710ull -> 16947117433974038ull +// 12026164355176752283ull -> 423735939073054907ull +// 17208159137580538219ull -> 18056612604689961163ull +// 5096664486441078049ull -> 12445011709498043361ull +// 16022104096870556683ull -> 16382730376298748267ull +// 4836076347901812102ull -> 12718112296054108390ull +// 8937674376376720246ull -> 913929622865489206ull +// 2703551404259163957ull -> 13218761675008378229ull +// 3061368442138030587ull -> 10388057217060506235ull +// 10178081078865716434ull -> 4373273430774112306ull +// 8206897629920030398ull -> 11946915320520331422ull +// 6525045815118222249ull -> 2472677882334572745ull +// 15184055765275961048ull -> 4709222664471492344ull +// 5759341035588050840ull -> 5785035152764109816ull +// 3484237427845144905ull -> 15834701310167426825ull +// 6855851226167457247ull -> 18416054639860847807ull +// 17070412854470623372ull -> 9654818568811123148ull +// 5698741489304391014ull -> 7514399512975361670ull +// 1169937375292956878ull -> 7096719386424781198ull constexpr std::uint64_t kSuppressedTail_hashes[] = { - 2926214571128404135ull, - 2460311144695717203ull, - 8827033979564876247ull, - 12198137721016629270ull, - 8010034670511875064ull, - 14775446960658454710ull, - 12026164355176752283ull, - 17208159137580538219ull, - 5096664486441078049ull, - 16022104096870556683ull, - 4836076347901812102ull, - 8937674376376720246ull, - 2703551404259163957ull, - 3061368442138030587ull, - 10178081078865716434ull, - 8206897629920030398ull, - 6525045815118222249ull, - 15184055765275961048ull, - 5759341035588050840ull, - 3484237427845144905ull, - 6855851226167457247ull, - 17070412854470623372ull, - 5698741489304391014ull, - 1169937375292956878ull, + 10420073189004151623ull, + 13145042526267837939ull, + 2622782777793463991ull, + 17996446729054892726ull, + 3969181056128787992ull, + 16947117433974038ull, + 423735939073054907ull, + 18056612604689961163ull, + 12445011709498043361ull, + 16382730376298748267ull, + 12718112296054108390ull, + 913929622865489206ull, + 13218761675008378229ull, + 10388057217060506235ull, + 4373273430774112306ull, + 11946915320520331422ull, + 2472677882334572745ull, + 4709222664471492344ull, + 5785035152764109816ull, + 15834701310167426825ull, + 18416054639860847807ull, + 9654818568811123148ull, + 7514399512975361670ull, + 7096719386424781198ull, }; 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.cpp b/tests/test_publication_witness.cpp index 63f035ae..81b5170f 100644 --- a/tests/test_publication_witness.cpp +++ b/tests/test_publication_witness.cpp @@ -58,6 +58,14 @@ // the adapter's day ledger that sums it, the recorded broker-state hash rows // and the final hash move with them. Every count and the other 153 runs keep // their digest, and the rule fires in exactly those three runs (its census). +// Re-harvested once more for R5 lane V19-FIX's hash step: the source layer's +// fold (pineforge-source-adapter/v4) no longer folds the dead +// PineExecutionAdapter::path_order_, a script cancel retires every leg of +// each exit it withdraws and K1 lets a withdrawn leg go, so all 156 runs pin +// 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. #include #include @@ -750,8 +758,9 @@ void harvest() { std::printf("// R5 lane D2-D: the values tests/test_publication_witness.cpp pins, one\n" "// 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) and\n" - "// once for K-ULP3's whole-lot close: see the provenance note in the test.\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" "// 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 aea967d7..bb55d210 100644 --- a/tests/test_publication_witness_pinned.inc +++ b/tests/test_publication_witness_pinned.inc @@ -1,164 +1,165 @@ // R5 lane D2-D: the values tests/test_publication_witness.cpp pins, one // 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) and -// once for K-ULP3's whole-lot close: see the provenance note in the test. +// 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. // Generated -- never edit a row by hand. constexpr Pinned kPinned[] = { - {"Config01", 0x5694a8532bcd421dull, 144, 44, 144}, - {"ConfigFlags101", 0xba5e734f412ab41cull, 145, 45, 144}, - {"Config01M", 0xab6acb1a64dd57cbull, 144, 42, 144}, - {"ConfigFlags101M", 0x2ff5cd76e80e67deull, 145, 45, 144}, - {"Config02", 0x77088e2f22fde440ull, 48, 11, 48}, - {"ConfigFlags102", 0xa6eadfd1bf49a77aull, 144, 52, 144}, - {"Config02M", 0x7cf2779415b94164ull, 48, 11, 48}, - {"ConfigFlags102M", 0x058b509c4da54c03ull, 144, 52, 144}, - {"Config03", 0xb531a7e913be7754ull, 144, 50, 144}, - {"ConfigFlags103", 0x4390169f9487551aull, 184, 65, 144}, - {"Config03M", 0x1ad51675e11578daull, 144, 48, 144}, - {"ConfigFlags103M", 0xfc0680619a86a9a2ull, 184, 66, 144}, - {"Config04", 0xa3abb573f86a6c7aull, 144, 21, 144}, - {"ConfigFlags104", 0x641eb1a06cae9793ull, 48, 12, 48}, - {"Config04M", 0xfe582fedf876cc0full, 144, 21, 144}, - {"ConfigFlags104M", 0xcd566667a9112841ull, 48, 12, 48}, - {"Config05", 0xb6864e4b03d54a51ull, 48, 14, 48}, - {"ConfigFlags105", 0x371fd89994f2ab10ull, 48, 13, 48}, - {"Config05M", 0x1257375ae7e0f818ull, 48, 14, 48}, - {"ConfigFlags105M", 0xe9be856ad85a8e11ull, 48, 13, 48}, - {"Config06", 0xfeb9c182e1d6f1b8ull, 48, 5, 48}, - {"ConfigFlags106", 0xc2eef7f89ab61bbcull, 48, 6, 48}, - {"Config06M", 0xa692e0ba56102dbbull, 48, 5, 48}, - {"ConfigFlags106M", 0x2d7aa7817c1519b2ull, 48, 6, 48}, - {"Config07", 0xe3f9f8fdeebad630ull, 144, 48, 144}, - {"ConfigFlags107", 0x9d5bfb628fc30b25ull, 48, 20, 48}, - {"Config07M", 0x5cc45a9442b36103ull, 144, 48, 144}, - {"ConfigFlags107M", 0x17ae2bc753952274ull, 48, 21, 48}, - {"Config08", 0xed38fd67726171dcull, 48, 12, 48}, - {"ConfigFlags108", 0xad39ceff34e0e8b7ull, 48, 15, 48}, - {"Config08M", 0x50dab05d22c0ec90ull, 48, 12, 48}, - {"ConfigFlags108M", 0xd9ed682d185119d6ull, 48, 15, 48}, - {"Config09", 0xd6c2b8fe02bc6edfull, 144, 66, 144}, - {"ConfigFlags109", 0x2099340d88029cd4ull, 144, 42, 144}, - {"Config09M", 0xd3c3e2b318cd4816ull, 144, 66, 144}, - {"ConfigFlags109M", 0x13c56eeacc2c48c6ull, 144, 42, 144}, - {"Config10", 0x0365e68183f007bfull, 48, 18, 48}, - {"ConfigFlags110", 0x1db8da6353c0f901ull, 48, 11, 48}, - {"Config10M", 0x81f661a2ddec01ccull, 48, 18, 48}, - {"ConfigFlags110M", 0xaea43984cce032cfull, 48, 11, 48}, - {"Config11", 0xe0528d258ebc79d5ull, 144, 60, 144}, - {"ConfigFlags111", 0x6243eb3f6d5d9257ull, 48, 19, 48}, - {"Config11M", 0xa1227b8646e825c9ull, 144, 60, 144}, - {"ConfigFlags111M", 0xb1387af2738b084cull, 48, 19, 48}, - {"Config12", 0xe8a0e8837c842021ull, 144, 48, 144}, - {"ConfigFlags112", 0x93833c36f672043eull, 144, 18, 144}, - {"Config12M", 0x85aa8c167955881cull, 144, 48, 144}, - {"ConfigFlags112M", 0x4d8225c5ade268f6ull, 144, 18, 144}, - {"Config13", 0x07d49de545c61ea9ull, 48, 19, 48}, - {"ConfigFlags113", 0x58ff774c2c115e4dull, 48, 19, 48}, - {"Config13M", 0xaa3612b637b7f11cull, 48, 19, 48}, - {"ConfigFlags113M", 0x799338f5f6cff4deull, 48, 19, 48}, - {"Config14", 0xaa3565a7aba93b42ull, 144, 40, 144}, - {"ConfigFlags114", 0x2e8574947fc54802ull, 144, 60, 144}, - {"Config14M", 0xd873f8f2ab14eef7ull, 144, 40, 144}, - {"ConfigFlags114M", 0xcc4081478d527b9dull, 144, 60, 144}, - {"Config15", 0x214c2764fc5d6840ull, 144, 48, 144}, - {"ConfigFlags115", 0x5299993d2c9d2342ull, 48, 6, 48}, - {"Config15M", 0x5447d69b23a1ee6cull, 144, 48, 144}, - {"ConfigFlags115M", 0xfb28b13fea101881ull, 48, 6, 48}, - {"Config16", 0xd08cd80d7707847full, 144, 54, 144}, - {"ConfigFlags116", 0x1a2f6f437c6a25aaull, 48, 19, 48}, - {"Config16M", 0xae1748395550acb2ull, 144, 54, 144}, - {"ConfigFlags116M", 0x30e76fcc910d1bd7ull, 48, 19, 48}, - {"CloseAllThenEntry", 0x1229fa22516f9324ull, 40, 17, 40}, - {"CloseAllThenEntryM", 0x9c59fb96310637b4ull, 40, 17, 40}, - {"Reversals", 0xbf402522e71eaf8eull, 40, 16, 40}, - {"ReversalsM", 0x31cf8b2793b1eb3cull, 40, 16, 40}, - {"SameBarBatch", 0xda4dde8cb3db6817ull, 36, 25, 36}, - {"SameBarBatchM", 0x76bf18436639b1a7ull, 36, 25, 36}, - {"CloseOrderAdds", 0xbd84425d0e877e9aull, 36, 22, 36}, - {"CloseOrderAddsM", 0xec542d1724bd8088ull, 36, 22, 36}, - {"DelayedStops", 0xa5b4c051fd5c1b95ull, 66, 20, 40}, - {"DelayedStopsM", 0x488432e35e988731ull, 66, 20, 40}, - {"Storm01", 0x1237a9af7cc64b5eull, 43, 13, 43}, - {"Storm01M", 0xcf918999a5bd2890ull, 43, 14, 43}, - {"Storm02", 0xbd810d10ae373105ull, 63, 12, 42}, - {"Storm02M", 0x9d5fc26418cc7a53ull, 63, 12, 42}, - {"Storm03", 0xb6b747d28109bac6ull, 41, 17, 41}, - {"Storm03M", 0xa9e5b72d562ad20eull, 41, 17, 41}, - {"Storm04", 0x9a23cb72e605d60aull, 54, 20, 54}, - {"Storm04M", 0xecb5e8f81ad1152cull, 54, 20, 54}, - {"Storm05", 0xaf3973baa80bbc22ull, 81, 17, 53}, - {"Storm05M", 0xc4ab81865ec98adaull, 82, 17, 53}, - {"Storm06", 0x033fe39d5879c2eeull, 40, 11, 40}, - {"Storm06M", 0xbb923093b0d4e790ull, 40, 11, 40}, - {"Storm07", 0x6a321c7b259d179bull, 55, 15, 55}, - {"Storm07M", 0x21dd7a0810cab240ull, 55, 15, 55}, - {"Storm08", 0x73442c764bd92cf3ull, 81, 14, 58}, - {"Storm08M", 0xb1013c6b40cb1c3eull, 81, 14, 58}, - {"Storm09", 0x0862ebe8c8671a4cull, 48, 21, 48}, - {"Storm09M", 0x3e36305c3bd6c50cull, 48, 20, 48}, - {"Storm10", 0x67c65cf79fb4fd04ull, 65, 16, 57}, - {"Storm10M", 0xa525ed40fa8dcb77ull, 66, 16, 57}, - {"Storm11", 0x41ae23254ac41a25ull, 59, 24, 59}, - {"Storm11M", 0x2465ebe5a898b583ull, 59, 24, 59}, - {"Storm12", 0x2669c1b328c20e7bull, 86, 20, 56}, - {"Storm12M", 0xa93279e98ab0599bull, 86, 20, 56}, - {"Storm13", 0x119fd78a3c0faef8ull, 46, 23, 46}, - {"Storm13M", 0x26477261fefd8adeull, 46, 23, 46}, - {"Storm14", 0xa5c8636392b9e2e7ull, 46, 18, 46}, - {"Storm14M", 0xc154ab3bdaf98c91ull, 46, 18, 46}, - {"Storm15", 0x1af1c82aaec76a59ull, 43, 13, 43}, - {"Storm15M", 0xc263f4dfa70fa547ull, 43, 14, 43}, - {"Storm16", 0x36e8854ede6193ebull, 58, 18, 58}, - {"Storm16M", 0x9e95f57c34f1034bull, 58, 18, 58}, - {"Storm17", 0x5386bd65c0f62e05ull, 82, 21, 54}, - {"Storm17M", 0x2db9219d5073e5a8ull, 77, 17, 54}, - {"Storm18", 0x419a4fae817d937aull, 80, 25, 49}, - {"Storm18M", 0x821abf7a7b76eebaull, 80, 25, 49}, - {"Storm19", 0x7a6e676b4a275e0bull, 86, 22, 49}, - {"Storm19M", 0xe74e494dc47e836aull, 86, 22, 49}, - {"Storm20", 0xbe18e76544edfad4ull, 45, 12, 42}, - {"Storm20M", 0x39a6a6ccf1ab0246ull, 45, 12, 42}, - {"Storm21", 0xf21f2b2f39179b1bull, 47, 14, 47}, - {"Storm21M", 0xcde399fe29958b1full, 47, 14, 47}, - {"Storm22", 0xccf024b5aecab024ull, 76, 19, 50}, - {"Storm22M", 0xfc7f155f59078d85ull, 76, 19, 50}, - {"Storm23", 0x65469d0b86034e49ull, 89, 27, 54}, - {"Storm23M", 0x1c47e407439ec4beull, 89, 27, 54}, - {"Storm24", 0x6b4986ce4fdb2197ull, 54, 25, 54}, - {"Storm24M", 0x945a3cb6f3be7c1aull, 54, 25, 54}, - {"Legs01", 0x9dc9571ed22946a1ull, 30, 6, 30}, - {"Legs01M", 0x419fce857522490full, 30, 6, 30}, - {"LegsStops01", 0xaa76be4dd2157297ull, 30, 6, 30}, - {"LegsStops01M", 0xbe63aa44e4df19b8ull, 30, 6, 30}, - {"Legs02", 0xeb57971412ebf99aull, 30, 6, 30}, - {"Legs02M", 0x1df1cf69d98e3031ull, 30, 6, 30}, - {"LegsStops02", 0x1d9557d00456049cull, 30, 6, 30}, - {"LegsStops02M", 0x2d85eb58b1083c1full, 30, 6, 30}, - {"Legs03", 0x98038c5239227f41ull, 30, 6, 30}, - {"Legs03M", 0x1b6393ae55cfd47dull, 30, 6, 30}, - {"LegsStops03", 0x702733cf7c20baa0ull, 30, 6, 30}, - {"LegsStops03M", 0xe7b4ac34ff2a1baaull, 30, 6, 30}, - {"Legs04", 0x59bc7f53183fc225ull, 30, 6, 30}, - {"Legs04M", 0xad7a80b4e97710e8ull, 30, 6, 30}, - {"LegsStops04", 0x31c9958de0855479ull, 30, 6, 30}, - {"LegsStops04M", 0x2bab2bf509e0f519ull, 30, 6, 30}, - {"Legs05", 0x38e4d6e884458ab5ull, 30, 6, 30}, - {"Legs05M", 0x8006d578a0acc9c9ull, 30, 6, 30}, - {"LegsStops05", 0x9fef88607df091c7ull, 30, 6, 30}, - {"LegsStops05M", 0x27bfc45211d22e02ull, 30, 6, 30}, - {"Legs06", 0x772acf570f1a6201ull, 30, 6, 30}, - {"Legs06M", 0x6ec48ee06581fca1ull, 30, 6, 30}, - {"LegsStops06", 0xc98b8aa15882f1a3ull, 30, 6, 30}, - {"LegsStops06M", 0x48ce620d473579f1ull, 30, 6, 30}, - {"TwoEntries", 0xc66c8d5388de53aaull, 30, 2, 30}, - {"TwoEntriesM", 0xc0b317263a05cb28ull, 30, 2, 30}, - {"PyramidLots", 0x41b3d70e195acaeeull, 30, 2, 30}, - {"PyramidLotsM", 0x6f6963832b98e407ull, 30, 2, 30}, - {"LegBesideClose", 0xbcd40de5458f923eull, 30, 2, 30}, - {"LegBesideCloseM", 0x714e15386491094bull, 30, 2, 30}, - {"TwoLegsOnly", 0xee7018ce2387f18dull, 30, 2, 30}, - {"TwoLegsOnlyM", 0x0afb7f0d83dd4c53ull, 30, 2, 30}, - {"LegsApart", 0xfd497440bea641a7ull, 30, 2, 30}, - {"LegsApartM", 0xef6125f6d36672adull, 30, 2, 30}, + {"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, 50, 144}, + {"ConfigFlags103", 0xcf60de51b3acfcc3ull, 184, 65, 144}, + {"Config03M", 0x4511d5cd864e706full, 144, 48, 144}, + {"ConfigFlags103M", 0xdd7937eb65073a91ull, 184, 66, 144}, + {"Config04", 0xa0a03448564c9be0ull, 144, 21, 144}, + {"ConfigFlags104", 0xaf23be135bf66e22ull, 48, 12, 48}, + {"Config04M", 0xe0f138c9c848b160ull, 144, 21, 144}, + {"ConfigFlags104M", 0xbf637ed723b215beull, 48, 12, 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", 0x2a4b7804a0a3e27eull, 48, 19, 48}, + {"Config13M", 0x5ba51d9728c119b8ull, 48, 19, 48}, + {"ConfigFlags113M", 0x88c14690db445933ull, 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", 0x75c523aefe67f41aull, 43, 13, 43}, + {"Storm01M", 0x5ea8235c86419d28ull, 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, 20, 54}, + {"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}, + {"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}, + {"Storm12", 0x8b151167a2310837ull, 86, 20, 56}, + {"Storm12M", 0xc49a83bf5709682bull, 86, 20, 56}, + {"Storm13", 0x327d8e77ac1de02cull, 46, 23, 46}, + {"Storm13M", 0x6d1a8b0e3f3199b3ull, 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, 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}, + {"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}, + {"Storm23", 0xb91e295d6da6778cull, 89, 27, 54}, + {"Storm23M", 0x78f7d0134456384full, 89, 27, 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}, }; From 07fc9183a77b4b1e710e3cb59c88d794bf4086c2 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Fri, 25 Sep 2026 22:01:13 +0800 Subject: [PATCH 13/24] Docs: a cancelled exit stays cancelled, and cancelling and recording cost what is live; OL14 says what is tested (R5 lane V19-FIX) - design OL14: the revival rule as the seven lab tv tapes and test_adapter_margin_revival_cancel hold it -- live, dormant and parked (gapped-stop) cancels, the untaped spellings, the no-grid rows -- citing retire_cancelled_exits. - design section 3.8 (F9): row A completed by V19-FIX for the constant-level cancel/re-place, new row D (the kernel's cohort rosters on a Pine host), and the measured before/after, the scaling rows and their mutants. - native-engine.md (the continuation fold): a roster costs what its host keeps on it; the Pine adapter takes an unbindable origin off. - abi-stability.md: the V19-FIX hash step, what moved and why, trades only on the taped revival shapes. - design P4: the hash-state content pin re-taken (path_order_ left the fold). - Every citation of the four changed sources carried from 91d65ad6 through an exact line map (INT24's int24_exact_reanchor.py): 266 moved, anchors 1681/1681, doc lint clean. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit b40509ae3a6142496c1a9627c5725dc49979c474) --- docs/adr/0001-kernel-adapter-boundary.md | 64 +++--- docs/design/native-feature-parity.md | 265 +++++++++++++---------- docs/pages/abi-stability.md | 20 ++ docs/pages/native-engine.md | 10 +- 4 files changed, 215 insertions(+), 144 deletions(-) diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index ea68c846..52577e9f 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -194,37 +194,37 @@ 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:694`) for TV sizing and fill spelling; - `validate_execution_precommit` (`pine_strategy_host.cpp:719`), whose - `validate_precommit` (`pine_adapter.cpp:12809`) returns `AdmitWithHostMargin` - (`pine_adapter.cpp:13054`) to own the opening margin decision; `owns_lot_excursions() = true` - (`pine_strategy_host.hpp:284-285`) with `closed_lot_excursion` (`pine_strategy_host.cpp:804`), so + `resolve_execution_terms` (`pine_strategy_host.cpp:693`) for TV sizing and fill spelling; + `validate_execution_precommit` (`pine_strategy_host.cpp:718`), whose + `validate_precommit` (`pine_adapter.cpp:13017`) returns `AdmitWithHostMargin` + (`pine_adapter.cpp:13262`) to own the opening margin decision; `owns_lot_excursions() = true` + (`pine_strategy_host.hpp:284-285`) with `closed_lot_excursion` (`pine_strategy_host.cpp:803`), so MFE/MAE are measured on TV tick-quantized prices; and `on_native_tick` - (`pine_strategy_host.cpp:381`) / `on_native_applied` (`pine_strategy_host.cpp:477`) for + (`pine_strategy_host.cpp:380`) / `on_native_applied` (`pine_strategy_host.cpp:476`) for `calc_on_order_fills` re-entry. `process_orders_on_close` becomes - `NativeCloseExecution::AfterCalculation` in the projected spec (`pine_adapter.cpp:2259-2260`); + `NativeCloseExecution::AfterCalculation` in the projected spec (`pine_adapter.cpp:2301-2302`); 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:1785-1816); the retained parent-before-child ordering of live handles — an + (`sha256:27e64b2cd0bbe283a8c83056538d66cd374e53cad072772c775a8529624768ac` pine_adapter.hpp:1788-1819); 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:1071-1150`). Kernel priority is acceptance/incarnation order. + `pine_adapter.cpp:1080-1159`). 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 (`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:9381-9386`); the half-tick - trigger threshold (`sha256:bbd5b742d2109e4b01f97a199f3883c4037918f4e0e6a0cf8cbd99b98f823b0c` pine_adapter.cpp:332-344) and raw-vs-booked fill spelling, behind the - adapter's terms seam (`resolve_terms` `pine_adapter.cpp:11446`). + extremes while the raw running best is retained (`has_trail_request` `pine_adapter.cpp:9574-9579`); the half-tick + trigger threshold (`sha256:bbd5b742d2109e4b01f97a199f3883c4037918f4e0e6a0cf8cbd99b98f823b0c` pine_adapter.cpp:341-353) and raw-vs-booked fill spelling, behind the + adapter's terms seam (`resolve_terms` `pine_adapter.cpp:11654`). - **Money arithmetic.** Ten-significant-digit half-up money (`source_money_round` - `pine_adapter.cpp:445-451`, twin `tv_money_round` `pine_policy_support.hpp:9-15`). + `pine_adapter.cpp:454-460`, twin `tv_money_round` `pine_policy_support.hpp:9-15`). - **Close reservations.** `strategy.close` callsite batching, two-call provenance and the entry-id - ledger (`close_logical_units_` `pine_adapter.hpp:2011-2022`); the POOC reservation-growth population predicate + ledger (`close_logical_units_` `pine_adapter.hpp:2018-2029`); the POOC reservation-growth population predicate (`sha256:f524cf2b9e21e231672451db30ab94184ba3125d8ad6e4f9f614a0f4c538919e` src/compat/pine/reservation_expansion.cpp:9-20). - **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:13844`) for the scheduling, - `resolve_margin_requirement` (`pine_adapter.cpp:13880`) for the ten-significant-digit money, + with TradingView's: `margin_check_allowed` (`pine_adapter.cpp:14052`) for the scheduling, + `resolve_margin_requirement` (`pine_adapter.cpp:14088`) 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 @@ -235,7 +235,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:16578`), the pre-open admission slice and + (`non_pooc_commissioned_short` `pine_adapter.cpp:16787`), the pre-open admission slice 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`). @@ -243,14 +243,14 @@ kernel and own these quirks, each at its site: projects `NativeCalculationTrigger::BarCloseAndFills` with TradingView's guard literal as `max_recalculations_per_point`. What stays are the specifics COOF adds on top — the language-state snapshot/restore around a recalculation, the waypoint-only refill deferral - (`next_source_path_waypoint` `pine_adapter.cpp:8072`), the first-open execution chain and its + (`next_source_path_waypoint` `pine_adapter.cpp:8265`), the first-open execution chain and its own loop guard (`kFirstOpenLoopGuard` `pine_scheduler_native.cpp:681`), and the two fills Pine refuses to - recalculate on (`suppress_grouped_stop_recalc` `pine_adapter.cpp:5431`). + recalculate on (`suppress_grouped_stop_recalc` `pine_adapter.cpp:5624`). - **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_` `pine_strategy_host.hpp:857`), which `scheduler_update_session_state` - (`pine_strategy_host.cpp:1794`) selects from the kernel's session-day facts before each source + (`pine_strategy_host.cpp:1793`) 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 `BacktestEngine` member (`engine.hpp:378`) only this host writes; the live-tail / probe-suppress overrides (`set_realtime_tail` `pine_strategy_host.hpp:446`, @@ -292,8 +292,8 @@ 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:445-451`). `strategy.close` batching and the entry-id ledger - are **gone** from the kernel — the ledger is adapter state (`close_logical_units_` `pine_adapter.hpp:2011-2022`) and + 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:2018-2029`) 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: @@ -303,7 +303,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:566`) and the kernel derives the pair — the intrabar-fill excursion + `pine_strategy_host.cpp:565`) 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 @@ -548,14 +548,14 @@ 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:333`), `source_level_on_price_grid` (`:319`), `nearest_tick` (`:257`) / `source_bar_fill_tick` (`:278`) / `directional_tick` (`:302`), 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:13642`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:14697`), `chart_day_key` (`pine_adapter.cpp:13539`), `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:13001-13002`): 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:2301-2303`) | 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:2328-2336`, `margin.maintenance_long` `pine_adapter.cpp:2343`) | 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` | -| `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:2275-2282) | 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:2266-2268`) | 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:1613`): 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` | +| `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:13850`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:14905`), `chart_day_key` (`pine_adapter.cpp:13747`), `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:323`) and `configure_native` (`pine_strategy_host.cpp:324`) 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:13209-13210`): 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:2343-2345`) | 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:2370-2378`, `margin.maintenance_long` `pine_adapter.cpp:2385`) | 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` | +| `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:2317-2324) | 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:2308-2310`) | 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:1612`): 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` | | `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` | @@ -642,7 +642,7 @@ 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:445-451`) | +| `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`) | ### B. Live kernel mechanisms with a calibrated number — each passes the amended rule 2 diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 255f76c3..3fb5f827 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -47,20 +47,20 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:2678-2689; `entry()` pine_adapter.cpp:5082, `order()` pine_adapter.cpp:10270) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5672-5719; `submit` native_host.hpp:1193 | 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:8133 → `Flatten{}` pine_adapter.cpp:8162-8163; `Reduce{ExplicitUnits}` pine_adapter.cpp:7280-7289; `ReverseTo` pine_adapter.cpp:6114-6115) | **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 | — | -| OL3 | Amend / cancel one working order (`submit_or_replace` pine_adapter.cpp:2648-3707 → `host.replace` pine_adapter.cpp:2664-2744) | **yes** — `replace` native_host.hpp:1198-1200, `cancel` native_host.hpp:1229; `ReplacedEvent` native_order.hpp:1013-1027; result and event types `SubmitStatus` native_order.hpp:835-861, `ReplacedEvent` native_order.hpp:997-1063 | 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:4515-4561, `cancel_all` pine_adapter.cpp:17806-17821; `live_by_source_key_` pine_adapter.hpp:1983; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:3857-3922) | **partial** — single-handle `cancel` native_host.hpp:1229; no enumeration (`WorkingRequestCore::live()` is consumer-internal native_order.hpp:1673); labels exist but are unindexed (`Request::label` native_order.hpp:485) **Closed:** `cancel_all` native_host.hpp:1247, `cancel_where(comment)` native_host.hpp:1252 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1252 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1243 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:3982; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:6965-7316) | **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 | — | -| 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:5303-5338, `cohort_for` pine_adapter.cpp:2442; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7263-7280) | **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:1262, `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:2687-2693) | **yes** — `Group` / `GroupEffect` native_order.hpp:466-473, `CancelReason::Group` native_order.hpp:948-953, `ReservationReducedEvent` native_order.hpp:1172-1182 (deferred form native_order.hpp:1172-1182) | K | — | F:A12 O:A6 O:A7 S:O7 | — | +| OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:2720-2731; `entry()` pine_adapter.cpp:5272, `order()` pine_adapter.cpp:10463) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5672-5719; `submit` native_host.hpp:1193 | 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:8326 → `Flatten{}` pine_adapter.cpp:8355-8356; `Reduce{ExplicitUnits}` pine_adapter.cpp:7473-7482; `ReverseTo` pine_adapter.cpp:6307-6308) | **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 | — | +| OL3 | Amend / cancel one working order (`submit_or_replace` pine_adapter.cpp:2690-3897 → `host.replace` pine_adapter.cpp:2706-2786) | **yes** — `replace` native_host.hpp:1198-1200, `cancel` native_host.hpp:1229; `ReplacedEvent` native_order.hpp:1013-1027; result and event types `SubmitStatus` native_order.hpp:835-861, `ReplacedEvent` native_order.hpp:997-1063 | 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:4705-4751, `cancel_all` pine_adapter.cpp:18015-18030; `live_by_source_key_` pine_adapter.hpp:1990; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4047-4112) | **partial** — single-handle `cancel` native_host.hpp:1229; no enumeration (`WorkingRequestCore::live()` is consumer-internal native_order.hpp:1673); labels exist but are unindexed (`Request::label` native_order.hpp:485) **Closed:** `cancel_all` native_host.hpp:1247, `cancel_where(comment)` native_host.hpp:1252 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1252 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1243 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:4172; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:7158-7509) | **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 | — | +| 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:5496-5531, `cohort_for` pine_adapter.cpp:2484; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7456-7473) | **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:1262, `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:2729-2735) | **yes** — `Group` / `GroupEffect` native_order.hpp:466-473, `CancelReason::Group` native_order.hpp:948-953, `ReservationReducedEvent` native_order.hpp:1172-1182 (deferred form native_order.hpp:1172-1182) | 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:1084-1085 | 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:12779; lowering `submit_one` pine_adapter.cpp:8830-8842, `BindCohort` pine_adapter.cpp:9331-9337, `submit_leg` pine_adapter.cpp:9355-9379; 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:1852); 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:8435-8438; `materialize_relative_exits` pine_adapter.cpp:17096-17137, pine_adapter.hpp:1812-1824) | **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:5598-5632. `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:999 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:18857-18886) | **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:1243 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:1985-2003); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1116-1195); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3261-3428, `materialize_snapshot` pine_adapter.cpp:10670-10694), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1580-1603, `CloseCallsiteState` pine_adapter.hpp:1674-1689; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15406, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15640) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:660-662 | 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: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:855-887) | n/a | A | — | S:R4 O:A10 | TV half of OL9. **V19D-P1 (R5 lane B-ADAPTER):** a margin-call revival never restores an exit the script withdrew: `strategy.cancel` / `cancel_all` retire every leg of a dormant exit's lifecycle (`retire_cancelled_dormant_exits` pine_adapter.cpp:1795) and the revival skips it (`revive_brackets_after_margin` pine_adapter.cpp:1856), as the lab tv tapes badapter-v19dp1-control / -cancel (NYSE:F 15m) decide it — replayed by `tests/test_adapter_margin_revival_cancel.cpp` | +| OL10 | TP/SL bracket per entry, armed before the entry fills, OCA between legs (`exit()` pine_adapter.cpp:12987; lowering `submit_one` pine_adapter.cpp:9023-9035, `BindCohort` pine_adapter.cpp:9524-9530, `submit_leg` pine_adapter.cpp:9548-9572; 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:1852); 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:8628-8631; `materialize_relative_exits` pine_adapter.cpp:17305-17346, pine_adapter.hpp:1815-1827) | **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:5598-5632. `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:999 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:19066-19095) | **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:1243 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:1992-2010); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1125-1204); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3451-3618, `materialize_snapshot` pine_adapter.cpp:10863-10887), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1579-1602, `CloseCallsiteState` pine_adapter.hpp:1673-1688; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15614, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15848) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:660-662 | 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:1019-1026; `select_exit_activation` exit_activation.cpp:18-88, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:864-896) | 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:1827); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:1901). 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,71 +68,71 @@ 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:5684-5706; `NativeTrailState` native_host.hpp:657-662, `trail_state` native_host.hpp:1072 | 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:11554-11566) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4438-4443) **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:11467-11604) stays A. | -| TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:3814-3860) | **partial** — the read exists `trail_state` native_host.hpp:1072; `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:9381-9386, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11467-11604, retained best `retained_trail_best` pine_adapter.hpp:287-289, operand / restart rules `source_trail_points` pine_adapter.cpp:8405-8631, `trail_one_shot` pine_adapter.cpp:9470-9599 | 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:11762-11774) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4438-4443) **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:11675-11812) stays A. | +| TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:4004-4050) | **partial** — the read exists `trail_state` native_host.hpp:1072; `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:9574-9579, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11675-11812, retained best `retained_trail_best` pine_adapter.hpp:287-289, operand / restart rules `source_trail_points` pine_adapter.cpp:8598-8824, `trail_one_shot` pine_adapter.cpp:9663-9792 | 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:5692-5696, pine_adapter.cpp:5692) | **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:10705-11882) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:932-936, invoked native_execution_consumer.cpp:4909-4927, guarded native_execution_consumer.cpp:4909-4943 | 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:2536-2591) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4950-4952 **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:2488, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:453 (→ SZ10) | -| SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:2497-2499) | **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:2549, `typed_entry_units` pine_adapter.cpp:2567; `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:2528; `percent_commission_live_equity` pine_adapter.cpp:4121-4161) | **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:2454-2469, `PineSizingSnapshot` pine_adapter.hpp:147-154; fill-time exceptions `default_sized` pine_adapter.cpp:6435-6461) | **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:5885-5889, pine_adapter.cpp:5885) | **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:10898-12090) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:932-936, invoked native_execution_consumer.cpp:4909-4927, guarded native_execution_consumer.cpp:4909-4943 | 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:2578-2633) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4950-4952 **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:2530, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:462 (→ SZ10) | +| SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:2539-2541) | **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:2591, `typed_entry_units` pine_adapter.cpp:2609; `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:2570; `percent_commission_live_equity` pine_adapter.cpp:4311-4351) | **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:2496-2511, `PineSizingSnapshot` pine_adapter.hpp:147-154; fill-time exceptions `default_sized` pine_adapter.cpp:6628-6654) | **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:1391-1408; host-sized terms snap (`ExecutionGridPolicy` native_order.hpp:71-85); exact grid validation `execution_terms_grid_representable` native_execution_consumer.cpp:4966-4969; 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:12253-12281, `compute_exit_reservation` pine_adapter.cpp:4204-4325) | **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:4256-4579, `reserved_exit_qty` pine_adapter.cpp:8757-9067) | **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:444-451, `source_money_floor_lot` pine_adapter.cpp:453; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:4236-4251) | 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:6496), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7703), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12510), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13106, pine_adapter.cpp:13159) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15454) | 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:2536), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2549, `typed_entry_units` pine_adapter.cpp:2567), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18452) | 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:6965-7316; ledger `close_batch_bar_` pine_adapter.hpp:2026-2037; 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:1074-1186) | n/a | A | — | S:O10 | (single-source) R5-4 confirms S's own split: concept → SZ9, predicate A. | +| SZ8 | Fractional reduce of the position / a cohort (`quantize_close_units` pine_adapter.cpp:12461-12489, `compute_exit_reservation` pine_adapter.cpp:4394-4515) | **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:4446-4769, `reserved_exit_qty` pine_adapter.cpp:8950-9260) | **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:4426-4441) | 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:6689), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7896), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12718), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13314, pine_adapter.cpp:13367) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15662) | 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:2578), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2591, `typed_entry_units` pine_adapter.cpp:2609), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18661) | 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:7158-7509; ledger `close_batch_bar_` pine_adapter.hpp:2033-2044; 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:1083-1195) | 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:2302) | **yes** — `allowed_open_directions` native_run_spec.hpp:596 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2934-2942 | 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:2948-2951 | 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:13001-13004) is TV. | -| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:595 → `MaxOpenLots` native_execution_consumer.cpp:2953-2956 | 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:5970-6016, cap left unset `max_open_lots` pine_adapter.cpp:2301-2303) is TV. | -| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13055-13120 and leaves the kernel gate unset pine_adapter.cpp:13054-13056) | **yes** — `initial_margin_fraction` native_run_spec.hpp:597; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2971 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:2817 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. | -| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13710-13711, `margin_pct` pine_adapter.cpp:13118) | **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:13810-13988, `submit_margin_call_units` pine_adapter.cpp:13949-13995, `schedule_margin_call_path` pine_adapter.cpp:17959-18036) | **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:2930 **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:13741-13777) | **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:982 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:2344) | **yes** — `allowed_open_directions` native_run_spec.hpp:596 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2934-2942 | 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:2948-2951 | 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:13209-13212) is TV. | +| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:595 → `MaxOpenLots` native_execution_consumer.cpp:2953-2956 | 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:6163-6209, cap left unset `max_open_lots` pine_adapter.cpp:2343-2345) is TV. | +| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13263-13328 and leaves the kernel gate unset pine_adapter.cpp:13262-13264) | **yes** — `initial_margin_fraction` native_run_spec.hpp:597; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2971 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:2817 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. | +| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13918-13919, `margin_pct` pine_adapter.cpp:13326) | **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:14018-14196, `submit_margin_call_units` pine_adapter.cpp:14157-14203, `schedule_margin_call_path` pine_adapter.cpp:18168-18245) | **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:2930 **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:13949-13985) | **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:982 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:179-200) | **no** **Closed:** `native_liquidation_price()` native_host.hpp:1300 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:16057-16073, `apply_fx_opening_margin_slice` pine_adapter.cpp:18037-18065) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1182 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:16548-16587) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1281 **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:13090-13093) | **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:13671-13685, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14697-14727) | **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:14740-14974) | **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:13481-13560) | **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:1303 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:13699, 4× shortfall `4.0 * minimum` pine_adapter.cpp:13758, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14466-14506, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:13990-14117, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:13990) | n/a | A | — | F:D2 F:G4 O:D4 S:M4 | R5-1 | -| 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:3610-3677, `record_market_review` pine_adapter.cpp:14978-15036) | n/a | A | — | S:R2 | (single-source) | +| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:16266-16282, `apply_fx_opening_margin_slice` pine_adapter.cpp:18246-18274) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1182 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:16757-16796) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1281 **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:13298-13301) | **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:13879-13893, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14905-14935) | **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:14948-15182) | **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:13689-13768) | **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:1303 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:13907, 4× shortfall `4.0 * minimum` pine_adapter.cpp:13966, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14674-14714, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:14198-14325, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:14198) | n/a | A | — | F:D2 F:G4 O:D4 S:M4 | R5-1 | +| 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:3800-3867, `record_market_review` pine_adapter.cpp:15186-15244) | 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 native_execution_consumer.cpp:7191-7212 (call site `sha256:fc29ccd415732193f24f2370c6aca8d6f8910bca5b95315e63e88f2befe8200b` native_execution_consumer.cpp:7202) | K | — | F:E1 O:F1 | — | -| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2259-2260) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7511-7515 | K | — | F:E2 O:F6 S:C1 | — | -| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:15893) | **yes** — `on_native_bar_open` native_host.hpp:877 runs before the open match native_execution_consumer.cpp:7053-7057; `execute_current` legal native_host.hpp:1084 | K | — | F:E3 O:F2 | hazard → CT4 | +| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2301-2302) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7511-7515 | K | — | F:E2 O:F6 S:C1 | — | +| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16101) | **yes** — `on_native_bar_open` native_host.hpp:877 runs before the open match native_execution_consumer.cpp:7053-7057; `execute_current` legal native_host.hpp:1084 | 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:6996 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7047 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1053 is the lookahead-free bar so far at the cursor, 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:681; `begin_coof_recalc` pine_adapter.cpp:5400, `flush_coof_tail` pine_adapter.cpp:16035, `end_coof_recalc` pine_adapter.cpp:5421) | **partial** — `on_native_applied` native_host.hpp:925 fires mid-path after each fill native_execution_consumer.cpp:6907, native_execution_consumer.cpp:6886-6916; commands are legal there `commands_allowed` native_execution_consumer.cpp:1397-1405; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5598-5632, `remaining_path_delivery` native_order.cpp:2038-2046. 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:681; `begin_coof_recalc` pine_adapter.cpp:5593, `flush_coof_tail` pine_adapter.cpp:16244, `end_coof_recalc` pine_adapter.cpp:5614) | **partial** — `on_native_applied` native_host.hpp:925 fires mid-path after each fill native_execution_consumer.cpp:6907, native_execution_consumer.cpp:6886-6916; commands are legal there `commands_allowed` native_execution_consumer.cpp:1397-1405; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5598-5632, `remaining_path_delivery` native_order.cpp:2038-2046. 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:7135-7139; 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:284-286. | | 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:7505-7622 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:915 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:2389-2445) | **yes** — `IntrabarPath` native_run_spec.hpp:362-402, delivery native_execution_consumer.cpp:7449-7640; `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:2431-2487) | **yes** — `IntrabarPath` native_run_spec.hpp:362-402, delivery native_execution_consumer.cpp:7449-7640; `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:233-319) 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:5547), 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:681, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1655-1662 | 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:5740), 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:681, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1654-1661 | 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:1513-1516, pumped from the scheduler `scheduler_prepare_security_sequence` pine_strategy_host.cpp:1421-1577; 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:1514 **Closed:** `NativeRunSpec::subscriptions` (`NativeTimeframeSubscription` native_run_spec.hpp:485), or `declare_timeframe_subscriptions` native_host.hpp:1116 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:873 and `native_series_bar(index)` native_host.hpp:1099 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:1428-1444 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, sole caller pine_strategy_host.cpp:1546; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1446-1451 **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:8259 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). | +| HT1 | HTF series of the same symbol (`request.security`): evaluators registered `scheduler_configure_security_evaluators` pine_strategy_host.cpp:1512-1515, pumped from the scheduler `scheduler_prepare_security_sequence` pine_strategy_host.cpp:1420-1576; 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:1513 **Closed:** `NativeRunSpec::subscriptions` (`NativeTimeframeSubscription` native_run_spec.hpp:485), or `declare_timeframe_subscriptions` native_host.hpp:1116 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:873 and `native_series_bar(index)` native_host.hpp:1099 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:1428-1444 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, sole caller pine_strategy_host.cpp:1545; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1446-1451 **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:8259 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). | | 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:915 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. | @@ -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:5672-5719, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:6255-6263; `Limit::fill_through` native_order.hpp:239-244 enforced native_execution_consumer.cpp:5056-5069, re-checked after host terms native_execution_consumer.cpp:5056-5076 | 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:4585-4586, clamp native_execution_consumer.cpp:4585 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:11721-11730) is A | -| FP3 | Commission: percent / cash per unit / cash per execution (`fee_kind_for` pine_adapter.cpp:463-470) | **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:256-259, `source_bar_fill` pine_adapter.cpp:11737-11746, `directional_tick` pine_adapter.cpp:301-305) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4047; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6337-6338; 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:332-380) is A on top. | +| FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:583 applied once native_execution_consumer.cpp:4585-4586, clamp native_execution_consumer.cpp:4585 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:11929-11938) 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:11945-11954, `directional_tick` pine_adapter.cpp:310-314) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4047; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6337-6338; 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:1182 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:8689). **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:339-387 (used `source_trigger_threshold` pine_adapter.cpp:8822-8827, `exit_limit_trigger` pine_adapter.cpp:9356-9377), `source_bar_fill_tick` pine_adapter.cpp:277-290, raw-vs-booked spelling pine_adapter.cpp:11220-11326, margin-call fill pricing pine_adapter.cpp:13786-13795 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:11446) | 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:9015-9020, `exit_limit_trigger` pine_adapter.cpp:9549-9570), `source_bar_fill_tick` pine_adapter.cpp:286-299, raw-vs-booked spelling pine_adapter.cpp:11428-11534, margin-call fill pricing pine_adapter.cpp:13994-14003 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:11654) | A | — | F:G4 O:H5 S:F3 | R5-1, R5-3 | ### 1.8 Excursion, metrics, report, hash @@ -159,11 +159,11 @@ 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:1713) | **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:1438) 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:1712) | **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:1437) stay A, measured in §3.7 part 2. | | 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:9263, 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:1350, native_execution_consumer.cpp:10662; 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:1955 **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` in batch and across a stream's warmup and realtime legs. 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. What a recorded row costs — quadratic on the Pine adapter, O(closed rows) per row in the kernel — is measured and ruled in §3.8 | +| 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:1954 **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` in batch and across a stream's warmup and realtime legs. 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. What a recorded row costs — quadratic on the Pine adapter, O(closed rows) per row in the kernel — is measured and ruled in §3.8 | | 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:2195-2196, `chart_timezone` pine_adapter.cpp:2236-2238) | **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:117-118 | K | — | F:J2 O:G-5 | — | +| OT2 | Symbol info, sessions, calendars, timezones (`timezone` pine_adapter.cpp:2237-2238, `chart_timezone` pine_adapter.cpp:2278-2280) | **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:117-118 | 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:2157, `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:546 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 57 `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 symbols are a second, disjoint inventory beside the 57 codegen-facing ones (`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:155 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:7255), 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:1794 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:155 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:7255), 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:1793 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:287-289) | 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 | --- @@ -193,7 +193,7 @@ 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:2605, `sha256:ddfd09da8827c35417d2ff71e63c7fb94e625439fb0ef2b42189833ea0dc3892` pine_adapter.cpp:14124-14126, `sha256:560e106c433fafa69344365c8f83e8e9b5a33ffcf4544447fbddd2cd47b4a60c` pine_adapter.cpp:14344-14345, `sha256:05c2a313dfe73ccea95254a9a4ea41f08ec5b675eded99c5cdcf40887d90e176` pine_adapter.cpp:14903-14904) 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 (pine_adapter.cpp:2647, `sha256:ddfd09da8827c35417d2ff71e63c7fb94e625439fb0ef2b42189833ea0dc3892` pine_adapter.cpp:14332-14334, `sha256:560e106c433fafa69344365c8f83e8e9b5a33ffcf4544447fbddd2cd47b4a60c` pine_adapter.cpp:14552-14553, `sha256:05c2a313dfe73ccea95254a9a4ea41f08ec5b675eded99c5cdcf40887d90e176` pine_adapter.cpp:15111-15112) and tests, 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 | | Size | M | M | L | M (R5-11) | @@ -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:291-294 | 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:900-902 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1833-1836; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1866, `:8750`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2249-2250. 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:445-451) | 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:900-902 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1833-1836; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1866, `:8750`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2291-2292. 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:6408 | 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:18857-18886 | 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 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:566) 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:19066-19095 | 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:565) 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:1271 | 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 | @@ -283,7 +283,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 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:1334-1418 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. +3. **Untouched:** `engine_security.cpp`, the Pine scheduler sequence pine_strategy_host.cpp:1333-1417 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. 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:1446); `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). @@ -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:2918-2985) 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:2924-2991) 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). @@ -315,11 +315,11 @@ Union of F §2.vi (9), O §2.vi (5 + 1) and S §2.6 (7), deduplicated. S had no |---|---|---|---|---| | 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:3027-3029, `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:1600-1601 | F O | +| 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:1599-1600 | 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 | -| 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:445-451 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:566; the ledger `close_logical_units_` pine_adapter.hpp:2011-2022; `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:565; the ledger `close_logical_units_` pine_adapter.hpp:2018-2029; `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:4fa627f4c74950d70948186329259be7ebb753c8c50d91c16b838ba01c540a6a` c_abi.cpp:289-929, `sha256:4b7e398c78b276b34c4247e8271242b48aa195cea29126f4e809073f2665e1ea` check_c_abi_runtime.py:19-79, c_abi.cpp:848, `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:902 | 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 | | 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 | @@ -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:2141-2278) 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:1638-1642). 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:e3aa487ce8d69db9054576f75839345c0acaab55c3fc0a374c2f9c8c071559ec` AGENTS.md:132-145). A lane that cannot be shown neutral by construction does not merge. +- **(a) NEUTRALITY.** Every new behaviour is opt-in by a spec field or a new request kind. The adapter's `project()` (pine_adapter.cpp:2187-2320) 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:1638-1642). 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:e3aa487ce8d69db9054576f75839345c0acaab55c3fc0a374c2f9c8c071559ec` AGENTS.md:132-145). A lane that cannot be shown neutral by construction does not merge. - **(b) TWIN.** One chosen probe per lane runs through the adapter AND a hand-written `NativeStrategyHost` on the same bars and spec. Trade lists are diffed **by identity** (entry / exit time, price, qty, pnl, commission), never by trade number; then event, ownership and hash detail (S's D → E order). A difference is allowed only where a named TV-quirk row of §1 is active, and each one is itemized. Because 430 test files 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:1278). 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. @@ -403,11 +403,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:2298, 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:2340, 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:5603-5607) and `drain_applied_notifications` (native_execution_consumer.cpp:6264-6277) 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:1334-1418 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:1333-1417 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) | @@ -494,7 +494,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:982-1004) — after `prepare_native_begin` has already +(pine_strategy_host.cpp:981-1003) — after `prepare_native_begin` has already projected the spec (pine_strategy_host.cpp:244) 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,12 +516,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:2302); 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:13001-13002; 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:16548, `update_risk_state` pine_adapter.cpp:13642-13669): 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:12994), 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:2344); 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:13209-13210; 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:16757, `update_risk_state` pine_adapter.cpp:13850-13877): 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:13202), 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_intraday_loss` | day-open equity at the day's first bar (`intraday_start_equity` pine_adapter.cpp:16051); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16163), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:14727-14736, `submit_intraday_loss_close` pine_adapter.cpp:14697-14725), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:16905); 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:5751, `:7410`, `:8134`, `:8262`, `:11224`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:17804-17807) | `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:13633-13635); 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:13539-13560, `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:16260); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16372), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:14935-14944, `submit_intraday_loss_close` pine_adapter.cpp:14905-14933), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17114); 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:5944, `:7603`, `:8327`, `:8455`, `:11432`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:18013-18016) | `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:13841-13843); 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:13747-13768, `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, @@ -541,8 +541,8 @@ kernel half pinned; corpus byte-identity not applicable (no `src/` or kernel seeded in an uncommitted, env-gated experiment. **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:868-889, and corpus 442d497 +stands (`project()` at pine_strategy_host.cpp:323, `configure_native` at :324, +the `set_pine_risk_*` setters at `sha256:3c56d0cbb6e9a64ea2f797abe9ca619c9590c4a656b16845cfede5f5880a5b9d` pine_strategy_host.cpp:867-888, 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 @@ -711,9 +711,9 @@ 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:333), `source_level_on_price_grid` (:319) and the -`nearest_tick` / `source_bar_fill_tick` / `directional_tick` spellings (`sha256:1ca6bb18dab0492e61bbf746b913361cf1c5961fdeba7b30bbcb5c92733f3085` pine_adapter.cpp:253, -:274, `sha256:8d5d9ec456f65dcd58c7452d13772cf4387209d88b719ba9374946ddd2f2ebbf` pine_adapter.cpp:298) are a *different* rule — a half-tick threshold per order kind with +(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 the fill booked elsewhere (E7) — which is why re-lowering moves outcomes instead of preserving them. @@ -819,8 +819,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`, :667), 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:886) | 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:459), after every applied execution once terminal (`record_applied_range_end`, :666), 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:885) | 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 @@ -870,7 +870,7 @@ untouched; no epoch moves. | 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 kernel books a fill at its interval index, which on the no-path aggregated route names the INPUT bar the script bucket opens on (390 for the 26th 15m bar of a 1m feed). Only the magnifier re-stamped its rows, so on a plain aggregated chart `held` read −364, the held-two-bars exit never fired (1 open trade where `ab9714be` closes 2), and every `Trade` / `PyramidEntry` / `pf_trade_t` bar index and `avg_bars_in_*` followed | **Adapter.** `PineStrategyHost::on_native_applied` pine_strategy_host.cpp:477 re-stamps the lot a fill opened and the rows it closed with the chart bar on every aggregated chart (`aggregates_input_bars` pine_strategy_host.cpp:138), as it always did under the magnifier. Every adapter test of "did this lot open on the current bar" reads the chart bar there too — the extreme samples (`on_native_bar` pine_strategy_host.cpp:413, `on_native_tick` pine_strategy_host.cpp:381), the slippage mask, the same-bar masks of `closed_lot_excursion` pine_strategy_host.cpp:804, the `calc_on_order_fills` sample in `PineScheduler::recalculate` pine_scheduler_native.cpp:579 and the KI-62 same-bar add cover in `PineExecutionAdapter::on_applied` pine_adapter.cpp:16645 — so nothing but the index moves. A stream's quiet carried interval, matched at its own open while the context still names the bar before it, is booked on the bar it opens into (`PineScheduler::source_bar_index_for` pine_scheduler_native.cpp:161) | `tests/test_aggregated_path_regressions.cpp` rows 1-2 (the audit's heldbars and next-open probes, `ab9714be`'s rows on the chart, plain aggregated and magnified paths), row 4 (the chart run equals the plain aggregated run in every field, eight combinations of `process_orders_on_close`, `calc_on_order_fills`, slippage and pyramiding) and the KI-62 add-on row; lane E27's bar-index row of `tests/test_pooc_fill_stamp_aggregation.cpp` flipped | +| 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 kernel books a fill at its interval index, which on the no-path aggregated route names the INPUT bar the script bucket opens on (390 for the 26th 15m bar of a 1m feed). Only the magnifier re-stamped its rows, so on a plain aggregated chart `held` read −364, the held-two-bars exit never fired (1 open trade where `ab9714be` closes 2), and every `Trade` / `PyramidEntry` / `pf_trade_t` bar index and `avg_bars_in_*` followed | **Adapter.** `PineStrategyHost::on_native_applied` pine_strategy_host.cpp:476 re-stamps the lot a fill opened and the rows it closed with the chart bar on every aggregated chart (`aggregates_input_bars` pine_strategy_host.cpp:138), as it always did under the magnifier. Every adapter test of "did this lot open on the current bar" reads the chart bar there too — the extreme samples (`on_native_bar` pine_strategy_host.cpp:412, `on_native_tick` pine_strategy_host.cpp:380), the slippage mask, the same-bar masks of `closed_lot_excursion` pine_strategy_host.cpp:803, the `calc_on_order_fills` sample in `PineScheduler::recalculate` pine_scheduler_native.cpp:579 and the KI-62 same-bar add cover in `PineExecutionAdapter::on_applied` pine_adapter.cpp:16854 — so nothing but the index moves. A stream's quiet carried interval, matched at its own open while the context still names the bar before it, is booked on the bar it opens into (`PineScheduler::source_bar_index_for` pine_scheduler_native.cpp:161) | `tests/test_aggregated_path_regressions.cpp` rows 1-2 (the audit's heldbars and next-open probes, `ab9714be`'s rows on the chart, plain aggregated and magnified paths), row 4 (the chart run equals the plain aggregated run in every field, eight combinations of `process_orders_on_close`, `calc_on_order_fills`, slippage and pyramiding) and the KI-62 add-on row; lane E27's bar-index row of `tests/test_pooc_fill_stamp_aggregation.cpp` flipped | | 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 | | 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) | @@ -903,20 +903,21 @@ row C). | # | cost | measured on `fd785928` | ruling | |---|---|---|---| -| **A** | the Pine adapter's recording fold: O(retained history) per row | ×3.92 to ×4.02 per doubling — 9.26 s at 800 bars of order-and-cancel, 19.5 s at 2,688 bars of re-issue — against at most 0.015 s with recording off | **open**: blocked on storage and a write barrier outside lane F9's files; no epoch needed; value witness pinned. **Resolved** by R5 lane V19-E (`pineforge-source-adapter/v4`): retired rows are erased and the logs fold as running digests, so a row costs what is live | +| **A** | the Pine adapter's recording fold: O(retained history) per row | ×3.92 to ×4.02 per doubling — 9.26 s at 800 bars of order-and-cancel, 19.5 s at 2,688 bars of re-issue — against at most 0.015 s with recording off | **open**: blocked on storage and a write barrier outside lane F9's files; no epoch needed; value witness pinned. **Resolved** by R5 lane V19-E (`pineforge-source-adapter/v4`): retired rows are erased and the logs fold as running digests, so a row costs what is live; **completed** by R5 lane V19-FIX for the one shape V19-E still held, an exit cancelled and re-placed every bar at one level | | **B** | the kernel recorder's closed-row walk: O(closed rows) per row | ×2.61 rising to ×3.64 per doubling with trades (0.80 s at 24,000 bars and 1,200 closed rows) against ×1.94 to ×2.08 without | **ruled**: retained, cost documented. **Resolved** by the v19 value epoch (R5 lane V19-A): each closed row folds once, into a running digest | | **C** | the terminal continuation capture (E24 STOP1) | 0.048 to 0.052 s, 23 to 24 % of the 43,008-bar gated replay | **revised** by R5 lane PERF-P1 (supervisor ruling): latched as a view, folded on first read; no value moves. **Eager again** since R5 lane V19-A: the v19 fold is the live state alone, so a view saves nothing | +| **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 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:288) — one per accepted request, cleared only by -`reset_for_run` pine_adapter.cpp:2047 — and reflected the whole admission +pine_state_hash.cpp:287) — one per accepted request, cleared only by +`reset_for_run` pine_adapter.cpp:2092 — 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 @@ -959,10 +960,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:8546). Full-table scans - rewrite rows as well (`suspend_brackets_for_reversal` pine_adapter.cpp:1670, - `revive_brackets_after_margin` pine_adapter.cpp:1856, - `preserved_by_close_all` pine_adapter.cpp:11255), and + (`has_full_entry_bracket` pine_adapter.cpp:8739). Full-table scans + rewrite rows as well (`suspend_brackets_for_reversal` pine_adapter.cpp:1679, + `revive_brackets_after_margin` pine_adapter.cpp:1901, + `preserved_by_close_all` pine_adapter.cpp:11463), 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. @@ -1034,6 +1035,50 @@ rows and cost ×18.6. The tombstone audit counts that scan as the existence test it is: an erased row is a hit only when no retained row answers the test first. +**Completed by R5 lane V19-FIX (audit X2, X9), inside the v19 epoch.** Two +reads still cost what the run had done (macOS arm64, Release, process CPU). +K1 held a leg lifecycle of the current position cycle for the margin revival, +and V19-D released one only once a re-issue at another stop superseded it, so +an exit the script cancelled and re-placed every bar at ONE level kept a row +per bar for the whole cycle, each walked by the sweep at every bar open: on +`91d65ad6` the audit's `probe_recancel_constant` cost ×4.90, ×4.79, ×4.65 per +doubling from 4,000 to 32,000 bars (11.2 s at 32,000), and +`strategy.cancel_all()` with an exit at `position_avg_price * 0.95` ×3.87 to +×4.26 up to 16,000. The same hold let the revival read a cancelled exit as a +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:1827), 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:2913). 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:3344); 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 +ended with one per opening. On spark (aarch64, GCC 13, one pinned X925 core) +the same three probes read ×4.18, ×4.13, ×4.97 / ×4.32, ×4.44, ×5.06 / ×2.39, +×2.68, ×3.02 before and ×1.99 to ×2.02 after. The price is one roster removal +per closed opening: the 100 public slots (magnifier on) run +0.54 % more +instructions at the median and +1.57 % at most (slots 007 and 030, the latter +with 23,164 trades). The +dead `PineExecutionAdapter::path_order_`, +written and folded but never read, left the fold in the same step. +`tests/test_adapter_live_state_scaling.cpp` holds all three shapes at a small +leg calibrated to at least 0.1 s CPU and four times its bars, below ×5 and +within 64 retained rows and 8 roster members; on the base they fail at ×25.6 +(the constant level, 16,000 to 64,000 bars without recording), ×27.4 (the +cancel_all shape) and ×6.9 with 8,000 roster members (the flip), and three +mutants of the product fail it too (K1 holding a withdrawn leg again: ×25.9 +and 32,000 rows; no roster removal: ×6.96 and 8,000 members; the removal kept +but a walk over every origin the run accepted at every bar: ×12.0). + + **The witness, pinned now.** `tests/test_adapter_recording_hash_witness.cpp` records every row, a read after every command and the final scalar of six scenarios — submit, replace, cancel, OCA, re-issue with a late bracket, trail — @@ -1070,7 +1115,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` :510, `exit_comment` :635, `close_cause` :649-654, `exit_from_bracket` :1277 and `entry_incarnation` :1458-1466. +writes are `close_cause` pine_strategy_host.cpp:488, `exit_bar_index` :509, `exit_comment` :634, `close_cause` :648-653, `exit_from_bracket` :1276 and `entry_incarnation` :1457-1465. 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 @@ -1102,8 +1147,8 @@ the bars cost 3.8 times the CPU with a round trip every ten bars script point, not after the run's teardown, so that it is one value with recording on or off (`broker_state_hash_projection` pine_strategy_host.cpp:175-183). `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:358-366 takes that snapshot at the last batch bar +(`last_batch` pine_strategy_host.cpp:462-472), 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 @@ -1159,13 +1204,13 @@ 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:955-959); 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:12916-12983, pine_adapter.cpp:13030); its chronology lives at `sha256:bb89768c9a7b2dc9f6de5943c04de8103ed107c21bb6e184446de0e6a84cf4c2` pine_adapter.cpp:13589-13666 and its excursion sample differs by chronology (`sha256:764a1a69f2940863b4f77cbf9d20267a19ec38e86720796c7081a59ac7245427` pine_strategy_host.cpp:729-759) | 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:2296-2336) | 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:5547) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `sha256:86617ca43bb2847380f1845b07cc9ecd27059814ee80fecf80649a9393dfe8c8` native_execution_consumer.cpp:3465-3474 and `drain_applied_notifications` (native_execution_consumer.cpp:6264-6277); 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:15893-15921) | 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:1600-1619) | 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:332-380) 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:1334-1418; 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 | +| 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:13124-13191, pine_adapter.cpp:13238); its chronology lives at `sha256:bb89768c9a7b2dc9f6de5943c04de8103ed107c21bb6e184446de0e6a84cf4c2` pine_adapter.cpp:13797-13874 and its excursion sample differs by chronology (`sha256:764a1a69f2940863b4f77cbf9d20267a19ec38e86720796c7081a59ac7245427` pine_strategy_host.cpp:728-758) | 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:2338-2378) | 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:5740) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `sha256:86617ca43bb2847380f1845b07cc9ecd27059814ee80fecf80649a9393dfe8c8` native_execution_consumer.cpp:3465-3474 and `drain_applied_notifications` (native_execution_consumer.cpp:6264-6277); 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:16101-16130) | 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:1599-1618) | 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:1333-1417; 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 | | 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 | @@ -1178,11 +1223,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:07645b60312b8fe0c03aed82d9b6365d6cd2a4cc4f322bf815c914401a0d9773` pine_state_hash.cpp:207-673); 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 (`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 | | 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:423-437) | 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:824-825 → pine_adapter.cpp:17343 (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 `sha256:81dee89712544bf03ed75e83b6eb379d3707a6c920a59e368d0574d6a5b928ba` pine_strategy_host.cpp:823-824 → pine_adapter.cpp:17552 (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) @@ -1207,7 +1252,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:16057-16073) 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:2586) 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:16266-16282) 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:2628) 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. | @@ -1230,7 +1275,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:7179, native_execution_consumer.cpp:7179, native_execution_consumer.cpp:7179) 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:2298), 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:2340), 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:1010). 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:5047-5089), `engine_run.cpp`, `engine_report.cpp`, `engine_state_hash.cpp`, `native_run_spec.{hpp,cpp}`. @@ -1283,7 +1328,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:12912-12933). New hashed `RequestDefinition::origin { Host, KernelLiquidation, KernelRisk }`, new `CancelReason::Superseded` on re-price, `MarginCallEvent` joins `CommandEvent` (17 → 18, native_order.hpp:1278). O's alternative, a checkpoint on the excursion walk (`sha256:ed6f4ff668195caf3c953c8ef5dd448c6010949353f7b9ef44d50b764f7488db` native_execution_consumer.cpp:1895-1908), 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:13120-13141). New hashed `RequestDefinition::origin { Host, KernelLiquidation, KernelRisk }`, new `CancelReason::Superseded` on re-price, `MarginCallEvent` joins `CommandEvent` (17 → 18, native_order.hpp:1278). O's alternative, a checkpoint on the excursion walk (`sha256:ed6f4ff668195caf3c953c8ef5dd448c6010949353f7b9ef44d50b764f7488db` native_execution_consumer.cpp:1895-1908), 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:4649-4662 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). @@ -1306,7 +1351,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:1616-1617) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6969-6981) 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:3478-3487). `born_on_remaining_path` (native_execution_consumer.cpp:5603-5607) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1655-1662) is calibrated against it. +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:3478-3487). `born_on_remaining_path` (native_execution_consumer.cpp:5603-5607) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1654-1661) 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:1071-1078, whose body is native_execution_consumer.cpp:6819-6826). `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:2544); 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). @@ -1393,7 +1438,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:1309) (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. - 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:13465-13563); 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:13673-13771); 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:4649-4662 and the per-point walk), `native_order.hpp`, the state hash. ### A.9 Single-source proposals not adopted diff --git a/docs/pages/abi-stability.md b/docs/pages/abi-stability.md index 98a18160..a7c7f9ed 100644 --- a/docs/pages/abi-stability.md +++ b/docs/pages/abi-stability.md @@ -304,6 +304,26 @@ consumed input prefix fold as counts and running digests, each element once; the auxiliary request.security feed folds as the digest taken when it is set. The values the tree pins were re-pinned once more, each marked "expectation corrected ... because v19-E folds live adapter state". +R5 lane V19-FIX moves every Pine broker-state, recorded per-bar, +host-extension and continuation value once more, inside the unreleased v19 +epoch and with the source extension still v4, because two of those reads +still cost what the run had done. An exit the script cancelled and re-placed +every bar at one level kept its lifecycle for a revival that never reads a +cancelled exit, one row per bar the whole position cycle long: a strategy.cancel +or strategy.cancel_all now retires every leg of each exit it withdraws, live +or dormant, and the adapter lets such a row go. And every opening stayed on +its id's kernel cohort roster, which the continuation folds member by member: +an origin whose request chain no longer works and that holds no open lot now +leaves the roster at the next bar open. The dead +`PineExecutionAdapter::path_order_` leaves the extension's fold with them (the +leg order a run declares folds in the spec digest, `NativeRunSpec::path_order`). +Trades move only where a margin call used to revive an exit the script had +cancelled while it was live, or while the adapter had it parked for a +margin-call slice, as the lab tv tapes v19fix-cancel-before-pair, +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, ...)". 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/native-engine.md b/docs/pages/native-engine.md index dd3dd97c..f05cd510 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -1434,7 +1434,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:13844-13868`), so the adapter admits +(`margin_check_allowed` `pine_adapter.cpp:14052-14076`), 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 @@ -2192,7 +2192,13 @@ Both digests fold **state**, not history (the v19 value epoch, R5 lane V19-A: two driver-point instants the FX-roll check reads. The acknowledgement and the event high waters are readback bookkeeping and fold nowhere. So equal state answers an equal value, two command histories that reach one state answer one value, and a read costs - the live state whatever the run's length. The fold takes one + the live state whatever the run's length. A cohort roster is live state as + its host keeps it: every member folds at every read, so an origin the host + leaves on its roster after nothing can bind it any more — its request chain + no longer works and no lot it opened is still open — costs every later read. + The Pine adapter takes each such origin off its roster (`cohort_remove`) at + the next bar open (R5 lane V19-FIX), so a Pine run's rosters hold the + openings that still work or hold a lot, however many the run has made. The fold takes one multiply-xorshift per 64-bit word; a string folds as its length, then its bytes eight at a time. `native_run_spec_digest(spec)` keeps its byte-wise FNV-1a and its values. From 709526beb3850a0ed22869621144ceb82bac5690 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Fri, 25 Sep 2026 19:52:38 +0800 Subject: [PATCH 14/24] K-IDX: book aggregated coordinates in script bars ADR-0001 rule 2 ruling: generic kernel coordinates use script-bar interval_index, with input_interval_index retained beside it for hosts that need input cadence. Remove Pine index restamping and repin the v19 coordinate witnesses once. (cherry picked from commit e362bc4b89964a8fc7ced24f5fea0465b93120c3) INT25 merge: test_native_calendar_hash_witness takes this lane's re-pinned rows, except ny_daily_over_hourly, which keeps KERNEL-EDGE's row (13 calculated bars, 52 driver rows, 2 trades: the final calendar-closed daily bucket) until the INT25 v19 hash re-pin harvests its digest over both lanes. The pages merge on digit-normalised text; the pine-to-native decision row merges word by word (C-SURFACE-1's POLICY_SIZE clause, KERNEL-EDGE's eligible-slot clause and this lane's script-bar index sentence all kept). --- docs/adr/0001-kernel-adapter-boundary.md | 43 ++-- docs/design/native-feature-parity.md | 154 +++++++------ docs/pages/live-surface.md | 2 +- docs/pages/native-engine.md | 20 +- docs/pages/pine-to-native.md | 14 +- include/pineforge/market_driver.hpp | 8 +- src/native_execution_consumer.cpp | 34 ++- src/native_execution_consumer.hpp | 5 +- src/source/pine_adapter.cpp | 6 +- src/source/pine_scheduler_native.cpp | 7 +- src/source/pine_strategy_host.cpp | 53 +---- tests/native_event_rows_fixture.hpp | 1 + tests/native_order_full_fold.hpp | 1 + tests/test_adapter_in_place_reads.cpp | 1 + tests/test_adapter_live_state_equivalence.cpp | 1 + ...st_adapter_live_state_equivalence_data.hpp | 218 +++++++++--------- .../test_native_calendar_driver_contract.cpp | 1 + tests/test_native_calendar_hash_witness.cpp | 25 +- tests/test_native_execution_terms.cpp | 1 + tests/test_native_handle_stable_replace.cpp | 1 + tests/test_native_host_reads.cpp | 1 + tests/test_native_in_place_reads.cpp | 1 + tests/test_native_lean_path.cpp | 64 ++--- .../test_native_market_vertical_contract.cpp | 1 + tests/test_native_point_checks.cpp | 196 ++++++++-------- tests/test_native_resting_replay_contract.cpp | 2 + 26 files changed, 448 insertions(+), 413 deletions(-) diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index 52577e9f..4a3e94db 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -77,10 +77,21 @@ 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:7053-7055`) — + (`native_host.hpp:877`, `on_native_bar_open` `native_execution_consumer.cpp:7066-7068`) — `on_native_recalculate` for every calculation of the run (`native_host.hpp:900`), `on_native_sub_bar` (`native_host.hpp:915`) and post-fill `on_native_applied` (`native_host.hpp:925`). `on_bar` is `final` (`native_host.hpp:845`). +- **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 + script coordinate books `PyramidEntry::entry_bar_index`, trade-row indices and + the inclusive bar metrics (`engine_orders.cpp:113-114`, `engine_metrics.cpp:156`). + `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:1030-1044`) in the v19 continuation. The Pine + adapter keeps its chart-source sampling index as a source-layer fact and does + not rewrite the kernel's lot or row indices (`pine_strategy_host.cpp:478`). - **Requests.** `submit` / `replace` / `cancel` / `submit_market` / `replace_market` (`native_host.hpp:1207-1212`), plus `cancel_all` (`native_host.hpp:1247`) and `cancel_where` (`native_host.hpp:1252`). A request carries one of five triggers — Market, Limit (with a @@ -98,7 +109,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:934`) returns no units is a `TermsUnresolved` rejection - (`TermsUnresolved` `native_execution_consumer.cpp:4950`). + (`TermsUnresolved` `native_execution_consumer.cpp:4963`). - **Observing executions.** Implement `on_native_applied` (`native_host.hpp:925`) — notifications drain FIFO after the outer callback returns — and/or poll `native_events(after_ordinal)` (`native_host.hpp:1309`). Do *not* implement @@ -109,7 +120,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:4923`) | + | Failure model | `NativeFailureCode` (`native_host.hpp:63-77`); `Failed` is latched, nothing resumes. A throwing callback latches `CallbackException` (`CallbackException` `native_execution_consumer.cpp:4936`) | | Cohorts | `cohort_open` (`native_host.hpp:1262`) / `cohort_add` (`:1265`) / `cohort_remove` (`:1268`) — the handle a `BindCohort` owner names | | Mid-callback execution | `current_execution_point` (`native_host.hpp:1067`), `inspect_current_execution` (`native_host.hpp:1079`) / `execute_current` (`native_host.hpp:1084`) | | 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:963`), `margin_check_allowed` (`native_host.hpp:975`), `resolve_margin_call_units` (`native_host.hpp:982`) — and `validate_execution_precommit` (`native_host.hpp:947`), whose `AdmitWithHostMargin` verdict hands the opening check to the host | @@ -137,7 +148,7 @@ summary of it. (`sha256:1eb25aef104b9f79988d37cffef5383089cac9098a163627c4709370ca830540` engine.hpp:1699), 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:71`), not a feed API; read + (`engine.hpp:1765`). `market_driver.hpp` is driver *types* (`NativeCoordinate` `market_driver.hpp:72`), not a feed API; read `examples/native/native_market_strategy.cpp` for a host that drives both a batch and a stream. - **Data / HTF.** The magnifier (`magnifier.hpp`) reconstructs intrabar fills. `request.security`-style series **are** reachable from a bare host, as declared series of the @@ -194,13 +205,13 @@ 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:693`) for TV sizing and fill spelling; - `validate_execution_precommit` (`pine_strategy_host.cpp:718`), whose + `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:13017`) returns `AdmitWithHostMargin` (`pine_adapter.cpp:13262`) to own the opening margin decision; `owns_lot_excursions() = true` - (`pine_strategy_host.hpp:284-285`) with `closed_lot_excursion` (`pine_strategy_host.cpp:803`), so + (`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` - (`pine_strategy_host.cpp:380`) / `on_native_applied` (`pine_strategy_host.cpp:476`) for + (`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:2301-2302`); calc cadence and language publication are `PineScheduler`'s (`pine_scheduler.hpp:20`). @@ -244,13 +255,13 @@ kernel and own these quirks, each at its site: `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:8265`), the first-open execution chain and its - own loop guard (`kFirstOpenLoopGuard` `pine_scheduler_native.cpp:681`), and the two fills Pine refuses to + 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:5624`). - **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_` `pine_strategy_host.hpp:857`), which `scheduler_update_session_state` - (`pine_strategy_host.cpp:1793`) selects from the kernel's session-day facts before each source + (`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 `BacktestEngine` member (`engine.hpp:378`) only this host writes; the live-tail / probe-suppress overrides (`set_realtime_tail` `pine_strategy_host.hpp:446`, @@ -303,7 +314,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:565`) and the kernel derives the pair — the intrabar-fill excursion + `pine_strategy_host.cpp:548`) 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 @@ -549,13 +560,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: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:13850`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:14905`), `chart_day_key` (`pine_adapter.cpp:13747`), `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:323`) and `configure_native` (`pine_strategy_host.cpp:324`) 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:13850`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:14905`), `chart_day_key` (`pine_adapter.cpp:13747`), `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:13209-13210`): 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:2343-2345`) | 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:2370-2378`, `margin.maintenance_long` `pine_adapter.cpp:2385`) | 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` | | `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:2317-2324) | 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:2308-2310`) | 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:1612`): 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 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` | | `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` | @@ -601,7 +612,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:582`) | **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:7632-7644`); 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:1076-1081`) 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: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:1076-1081`) sees the same values, so no hash contract moves. | | `pf_native_lot_excursion_v1` (`native_c_api.h:1867`) | **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) @@ -659,7 +670,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: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` | | `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:8589-8592` (lookahead) and `clear_if_gapped` `:8583-8586` (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: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:2226`, `::gaps` `include/pineforge/native_c_api.h:2230` | 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` | @@ -676,7 +687,7 @@ The table records, at the tree the doc wave read, what each block described and | `present_session_day` `src/native_execution_consumer.cpp:7255`, presenting `NativeDecisionContext::in_session` `include/pineforge/market_driver.hpp:155` 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:7864-7880` | 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:7841-7862` | 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:6592` | 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::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 diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 3fb5f827..31c6f3d9 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -68,7 +68,7 @@ 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:5684-5706; `NativeTrailState` native_host.hpp:657-662, `trail_state` native_host.hpp:1072 | 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:11762-11774) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4438-4443) **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:11675-11812) stays A. | +| TR3 | Zero-distance trail, "ride the best, exit on any adverse tick" (adapter substitutes `tick * 0.5` pine_adapter.cpp:11762-11774) | **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:11675-11812) stays A. | | TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:4004-4050) | **partial** — the read exists `trail_state` native_host.hpp:1072; `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:9574-9579, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11675-11812, retained best `retained_trail_best` pine_adapter.hpp:287-289, operand / restart rules `source_trail_points` pine_adapter.cpp:8598-8824, `trail_one_shot` pine_adapter.cpp:9663-9792 | n/a | A | — | F:B3 S:T2 | R5-1 | @@ -78,7 +78,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa |---|---|---|---|---|---|---| | SZ1 | Explicit units (`Transact{signed}` after a grid floor pine_adapter.cpp:5885-5889, pine_adapter.cpp:5885) | **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:10898-12090) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:932-936, invoked native_execution_consumer.cpp:4909-4927, guarded native_execution_consumer.cpp:4909-4943 | 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:2578-2633) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4950-4952 **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:2530, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:462 (→ SZ10) | +| SZ3 | Percent-of-equity sizing (`default_sizing_units` pine_adapter.cpp:2578-2633) | **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:2530, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:462 (→ SZ10) | | SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:2539-2541) | **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:2591, `typed_entry_units` pine_adapter.cpp:2609; `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:2570; `percent_commission_live_equity` pine_adapter.cpp:4311-4351) | **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:2496-2511, `PineSizingSnapshot` pine_adapter.hpp:147-154; fill-time exceptions `default_sized` pine_adapter.cpp:6628-6654) | **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`. | @@ -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:2344) | **yes** — `allowed_open_directions` native_run_spec.hpp:596 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2934-2942 | 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:2948-2951 | 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:13209-13212) is TV. | -| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:595 → `MaxOpenLots` native_execution_consumer.cpp:2953-2956 | 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:6163-6209, cap left unset `max_open_lots` pine_adapter.cpp:2343-2345) is TV. | -| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13263-13328 and leaves the kernel gate unset pine_adapter.cpp:13262-13264) | **yes** — `initial_margin_fraction` native_run_spec.hpp:597; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2971 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:2817 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. | +| MG1 | Allowed opening directions (`allowed_open_directions` pine_adapter.cpp:2344) | **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:13209-13212) 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:6163-6209, cap left unset `max_open_lots` pine_adapter.cpp:2343-2345) is TV. | +| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13263-13328 and leaves the kernel gate unset pine_adapter.cpp:13262-13264) | **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. | | MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13918-13919, `margin_pct` pine_adapter.cpp:13326) | **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:14018-14196, `submit_margin_call_units` pine_adapter.cpp:14157-14203, `schedule_margin_call_path` pine_adapter.cpp:18168-18245) | **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:2930 **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:14018-14196, `submit_margin_call_units` pine_adapter.cpp:14157-14203, `schedule_margin_call_path` pine_adapter.cpp:18168-18245) | **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:13949-13985) | **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:982 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:179-200) | **no** **Closed:** `native_liquidation_price()` native_host.hpp:1300 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:16266-16282, `apply_fx_opening_margin_slice` pine_adapter.cpp:18246-18274) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1182 but no margin re-check. **Closed by audit lane N6:** `NativeMarginCheckKind::FxRoll` — a step of the declared curve is a check point of its own, measured at the unchanged price before the first point the new rate converts (`tests/test_native_margin_fx_roll.cpp`). The adapter refuses the point by kind, for a measured reason since R5 lane F7: TradingView's rollover is a broker-open checkpoint at the open price, now at any positive margin (`apply_fx_open_margin_slice`, on the one TradingView money / units rule every other checkpoint uses), while the roll point measures at the remaining path's adverse mark and rests the slice there — a carried 1x long on a step bar O 100 L 97 books 0.4116 @ 97 on the adapter's run shape and 0.4036 @ 99, measured a bar early, on a strict calendar, against TradingView's 0.3996 @ 100 (`tests/test_native_margin_hooks_twin.cpp` MG-FX). Both N6 follow-ups are closed (§4.4). **Completed by R5 follow-up lane E3:** the conversion INSTANT is now generic too — every check kind converts at its own cursor's rate (`account_currency_fx_at(cursor.point.effective_time_ms)`) for the requirement, the equity and the level, in batch and stream, so a step landing between a fill's cursor and the clock the engine presents no longer breaches at the fill (`tests/test_native_margin_fx_clock.cpp`; N6's local clock swap inside `fx_roll_margin_check` is removed as redundant). **Extended by R5 follow-up lane E21** to the opening gate's placement site (MG4), and the rate-explicit equity E3 restated beside the engine's accessor is now that accessor's one implementation, `BacktestEngine::marked_equity_at` engine.hpp:729 | K | L4 → N6 → E3 → E21 | O:D8 | (single-source) | @@ -115,40 +115,40 @@ 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:7191-7212 (call site `sha256:fc29ccd415732193f24f2370c6aca8d6f8910bca5b95315e63e88f2befe8200b` native_execution_consumer.cpp:7202) | K | — | F:E1 O:F1 | — | -| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2301-2302) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7511-7515 | K | — | F:E2 O:F6 S:C1 | — | -| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16101) | **yes** — `on_native_bar_open` native_host.hpp:877 runs before the open match native_execution_consumer.cpp:7053-7057; `execute_current` legal native_host.hpp:1084 | 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:6996 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7047 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1053 is the lookahead-free bar so far at the cursor, 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:881, dispatched native_execution_consumer.cpp:7204-7225 (call site `sha256:fc29ccd415732193f24f2370c6aca8d6f8910bca5b95315e63e88f2befe8200b` native_execution_consumer.cpp:7215) | K | — | F:E1 O:F1 | — | +| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2301-2302) | **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:16101) | **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:1084 | 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:1053 is the lookahead-free bar so far at the cursor, 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:681; `begin_coof_recalc` pine_adapter.cpp:5593, `flush_coof_tail` pine_adapter.cpp:16244, `end_coof_recalc` pine_adapter.cpp:5614) | **partial** — `on_native_applied` native_host.hpp:925 fires mid-path after each fill native_execution_consumer.cpp:6907, native_execution_consumer.cpp:6886-6916; commands are legal there `commands_allowed` native_execution_consumer.cpp:1397-1405; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5598-5632, `remaining_path_delivery` native_order.cpp:2038-2046. 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:7135-7139; 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:284-286. | +| 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:5593, `flush_coof_tail` pine_adapter.cpp:16244, `end_coof_recalc` pine_adapter.cpp:5614) | **partial** — `on_native_applied` native_host.hpp:925 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:1397-1405; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5598-5632, `remaining_path_delivery` native_order.cpp:2038-2046. 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:284-286. | | 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:7505-7622 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:915 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:2431-2487) | **yes** — `IntrabarPath` native_run_spec.hpp:362-402, delivery native_execution_consumer.cpp:7449-7640; `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:233-319) 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:5740), 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:681, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1654-1661 | 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:5740), 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:1654-1661 | 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:1512-1515, pumped from the scheduler `scheduler_prepare_security_sequence` pine_strategy_host.cpp:1420-1576; 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:1513 **Closed:** `NativeRunSpec::subscriptions` (`NativeTimeframeSubscription` native_run_spec.hpp:485), or `declare_timeframe_subscriptions` native_host.hpp:1116 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:873 and `native_series_bar(index)` native_host.hpp:1099 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:1428-1444 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, sole caller pine_strategy_host.cpp:1545; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1446-1451 **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:8259 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). | +| 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:1420-1576; 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:1116 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:873 and `native_series_bar(index)` native_host.hpp:1099 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:1428-1444 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:1446-1451 **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). | | 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:915 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:7864-7881, which feeds the same class `TimeframeAggregator` native_execution_consumer.cpp:7871, 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:7864-7881, 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 | ### 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:5672-5719, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:6255-6263; `Limit::fill_through` native_order.hpp:239-244 enforced native_execution_consumer.cpp:5056-5069, re-checked after host terms native_execution_consumer.cpp:5056-5076 | 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:4585-4586, clamp native_execution_consumer.cpp:4585 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:11929-11938) 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:5672-5719, `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:11929-11938) 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:11945-11954, `directional_tick` pine_adapter.cpp:310-314) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4047; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6337-6338; 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:11945-11954, `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:1182 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:8689). **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:1182 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:9015-9020, `exit_limit_trigger` pine_adapter.cpp:9549-9570), `source_bar_fill_tick` pine_adapter.cpp:286-299, raw-vs-booked spelling pine_adapter.cpp:11428-11534, margin-call fill pricing pine_adapter.cpp:13994-14003 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:11654) | A | — | F:G4 O:H5 S:F3 | R5-1, R5-3 | ### 1.8 Excursion, metrics, report, hash @@ -159,11 +159,11 @@ 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:1712) | **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:1437) 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 — and the short-seed swaps (`project_short_seed_report_rows` pine_strategy_host.cpp:1408) stay A, measured in §3.7 part 2. | | 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:9263, 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:1350, native_execution_consumer.cpp:10662; 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:1954 **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` in batch and across a stream's warmup and realtime legs. 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. What a recorded row costs — quadratic on the Pine adapter, O(closed rows) per row in the kernel — is measured and ruled in §3.8 | +| 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:1350, 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` in batch and across a stream's warmup and realtime legs. 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. What a recorded row costs — quadratic on the Pine adapter, O(closed rows) per row in the kernel — is measured and ruled in §3.8 | | 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:2237-2238, `chart_timezone` pine_adapter.cpp:2278-2280) | **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:117-118 | K | — | F:J2 O:G-5 | — | +| OT2 | Symbol info, sessions, calendars, timezones (`timezone` pine_adapter.cpp:2237-2238, `chart_timezone` pine_adapter.cpp:2278-2280) | **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:2157, `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:546 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 57 `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 symbols are a second, disjoint inventory beside the 57 codegen-facing ones (`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:155 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:7255), 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:1793 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: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:287-289) | 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,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:291-294 | 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:900-902 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1833-1836; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1866, `:8750`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2291-2292. 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:900-902 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1833-1836; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1869, `:8767`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2291-2292. 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:6408 | 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 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:19066-19095 | 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:565) 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 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:1271 | 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 | @@ -228,7 +228,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa 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 -typedef struct pf_native_decision_v1 { uint64_t ordinal; int32_t interval_index; int64_t effective_time_ms, +typedef struct pf_native_decision_v1 { uint64_t ordinal; int32_t interval_index; /* SCRIPT-bar index */ int64_t effective_time_ms, script_bar_open_ms, sub_bar_open_ms; uint8_t provenance, path_phase; double price; } pf_native_decision_v1; typedef struct pf_native_request_v1 { uint32_t struct_size; uint32_t intent; double intent_value; uint32_t side; /* Flatten/Reduce/Transact/ReverseTo/Sized */ @@ -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:2948-2958 | F O | +| 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 | | Legality | The existing rule (`commands_allowed` native_execution_consumer.cpp:1397-1404): 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 | @@ -276,8 +276,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-670, `contribute_input` native_execution_consumer.cpp:7998, `seal_script` native_execution_consumer.cpp:7965) 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:7085), 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-670, `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 | | 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 | @@ -285,7 +285,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 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:1333-1417 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. 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:1446); `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: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). 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". @@ -315,11 +315,11 @@ Union of F §2.vi (9), O §2.vi (5 + 1) and S §2.6 (7), deduplicated. S had no |---|---|---|---|---| | 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:3027-3029, `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:1599-1600 | F O | +| 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 | | 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 | -| 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:565; the ledger `close_logical_units_` pine_adapter.hpp:2018-2029; `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:2018-2029; `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:4fa627f4c74950d70948186329259be7ebb753c8c50d91c16b838ba01c540a6a` c_abi.cpp:289-929, `sha256:4b7e398c78b276b34c4247e8271242b48aa195cea29126f4e809073f2665e1ea` check_c_abi_runtime.py:19-79, c_abi.cpp:848, `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:902 | 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 | | 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 | @@ -406,7 +406,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:2340, 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:5603-5607) and `drain_applied_notifications` (native_execution_consumer.cpp:6264-6277) 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: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) | | 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:1333-1417 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) | @@ -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:4950-4952). 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 (`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. | | 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) | @@ -495,7 +495,7 @@ inside the per-bar body (corpus 442d497, `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:981-1003) — after `prepare_native_begin` has already -projected the spec (pine_strategy_host.cpp:244) and `configure_native` has +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 @@ -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:3288-3477 +`sha256:8c97d3e1d9d6f036a9be0b50a3a43188f1e2eee23da0384fbed242829108c3c3` native_run_spec.hpp:239-302 and `sha256:efad6b9c7bcd3f7d0e459d434ef088a61b18c546d53cfb107f481a227fbe532b` native_execution_consumer.cpp:3301-3490 (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 | @@ -541,8 +541,8 @@ kernel half pinned; corpus byte-identity not applicable (no `src/` or kernel seeded in an uncommitted, env-gated experiment. **The P6 decision: native-only.** Re-verified on main `b8e7976e`: the ordering -stands (`project()` at pine_strategy_host.cpp:323, `configure_native` at :324, -the `set_pine_risk_*` setters at `sha256:3c56d0cbb6e9a64ea2f797abe9ca619c9590c4a656b16845cfede5f5880a5b9d` pine_strategy_host.cpp:867-888, and corpus 442d497 +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 `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 @@ -819,8 +819,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:459), after every applied execution once terminal (`record_applied_range_end`, :666), 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:885) | 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`, :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 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 @@ -862,29 +862,31 @@ no baseline rows. 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`). -**Ruling (supervisor, ADR-0001 rule 2):** all three are TradingView -presentation conventions of the adapter and are fixed in the source layer. -The kernel's interval index, its calculation instant (the instant its FX -lookup, decision floor and ordering read) and every hash recipe are -untouched; no epoch moves. +**Ruling (supervisor, ADR-0001 rule 2):** Option A. The kernel owns the +public coordinate in script-bar space for every host. `NativeCoordinate::interval_index` +and every lot, trade-row and metric index are sequential SCRIPT-bar indices; +`NativeCoordinate::input_interval_index` keeps the input slot beside them for +a host that needs input cadence. The Pine adapter's index re-stamp is removed; +its chart-bar sampling remains an explicit source-layer policy. The coordinate +and its named input companion are folded by the v19 continuation recipe, so +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 kernel books a fill at its interval index, which on the no-path aggregated route names the INPUT bar the script bucket opens on (390 for the 26th 15m bar of a 1m feed). Only the magnifier re-stamped its rows, so on a plain aggregated chart `held` read −364, the held-two-bars exit never fired (1 open trade where `ab9714be` closes 2), and every `Trade` / `PyramidEntry` / `pf_trade_t` bar index and `avg_bars_in_*` followed | **Adapter.** `PineStrategyHost::on_native_applied` pine_strategy_host.cpp:476 re-stamps the lot a fill opened and the rows it closed with the chart bar on every aggregated chart (`aggregates_input_bars` pine_strategy_host.cpp:138), as it always did under the magnifier. Every adapter test of "did this lot open on the current bar" reads the chart bar there too — the extreme samples (`on_native_bar` pine_strategy_host.cpp:412, `on_native_tick` pine_strategy_host.cpp:380), the slippage mask, the same-bar masks of `closed_lot_excursion` pine_strategy_host.cpp:803, the `calc_on_order_fills` sample in `PineScheduler::recalculate` pine_scheduler_native.cpp:579 and the KI-62 same-bar add cover in `PineExecutionAdapter::on_applied` pine_adapter.cpp:16854 — so nothing but the index moves. A stream's quiet carried interval, matched at its own open while the context still names the bar before it, is booked on the bar it opens into (`PineScheduler::source_bar_index_for` pine_scheduler_native.cpp:161) | `tests/test_aggregated_path_regressions.cpp` rows 1-2 (the audit's heldbars and next-open probes, `ab9714be`'s rows on the chart, plain aggregated and magnified paths), row 4 (the chart run equals the plain aggregated run in every field, eight combinations of `process_orders_on_close`, `calc_on_order_fills`, slippage and pyramiding) and the KI-62 add-on row; lane E27's bar-index row of `tests/test_pooc_fill_stamp_aggregation.cpp` flipped | +| 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 | | 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, three entry-bar tests (the - slippage mask, the same-bar masks of `closed_lot_excursion`, the - `calc_on_order_fills` extreme sample) compare a lot's re-stamped chart-bar - index with the kernel's input-bar index, as they did before this lane (an - entry on chart bar 26 closed on input bar 435). Changing them moves - magnified outcomes and needs its own measurement against a tape. -- The per-bar broker hash of an aggregated run moves with the re-stamped lot - and row values; the recipe does not, and no pinned hash covers such a run. - The magnifier re-stamp has always moved it the same way. +- 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 + lot index; the K-IDX probes cover the kernel index and input companion. +- 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 + epoch outside v19 is changed. --- @@ -1115,7 +1117,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:488, `exit_bar_index` :509, `exit_comment` :634, `close_cause` :648-653, `exit_from_bracket` :1276 and `entry_incarnation` :1457-1465. +writes are `close_cause` pine_strategy_host.cpp:488, `exit_bar_index` :826, `exit_comment` :617, `close_cause` :631-636, `exit_from_bracket` :1247 and `entry_incarnation` :1428-1436. 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 @@ -1203,14 +1205,14 @@ 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:955-959); 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:13124-13191, pine_adapter.cpp:13238); its chronology lives at `sha256:bb89768c9a7b2dc9f6de5943c04de8103ed107c21bb6e184446de0e6a84cf4c2` pine_adapter.cpp:13797-13874 and its excursion sample differs by chronology (`sha256:764a1a69f2940863b4f77cbf9d20267a19ec38e86720796c7081a59ac7245427` pine_strategy_host.cpp:728-758) | 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 | +| 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:13124-13191, pine_adapter.cpp:13238); its chronology lives at `sha256:bb89768c9a7b2dc9f6de5943c04de8103ed107c21bb6e184446de0e6a84cf4c2` pine_adapter.cpp:13797-13874 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:2338-2378) | 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:5740) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `sha256:86617ca43bb2847380f1845b07cc9ecd27059814ee80fecf80649a9393dfe8c8` native_execution_consumer.cpp:3465-3474 and `drain_applied_notifications` (native_execution_consumer.cpp:6264-6277); new behaviour only behind new opt-ins | F | +| E4 | **Calc on fill.** Kernel suffix eligibility is continuous along the segment; TV refills only at O/H/L/C waypoints (`sha256:ddad8dc42e078838c0d53fd49e50c31060221e3b82e7d5bbaa0420769daa7d31` pine_scheduler_native.cpp:585-637, `coof_next_waypoint` pine_adapter.cpp:5740) | 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 | | E5 | **Open-bar lookahead.** The adapter reads the full bar in `on_bar_open` (margin path scheduling uses H/L, pine_adapter.cpp:16101-16130) | 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:1599-1618) | 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 (`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:1333-1417; full corpus diff; supplied-bar provenance preserved, never silently replaced by chart aggregation (S); differences on early-close sessions are recorded, not "fixed" (F). The Pine scheduler keeps one more `TimeframeAggregator` pass, before the run (`PineScheduler::run_begin`, audit M26): the script-bar count it yields (`terminal_source_bar`, the probe-tail suppression) and the per-input completion flags it hands `on_native_input` are needed before the first input arrives, while the kernel seals script intervals only as inputs arrive and offers no pre-run interval query (`NativeExecutionConsumer::script_interval_at` is consumer-private). Retained by R5 lane F7: replacing it needs that kernel capability | F O S | +| E8 | **HTF.** The evaluator machinery carries TV publication semantics (lookahead, boundary deferral, early close, `syminfo.type` branching engine_security.cpp:67-73). The feed store is TV-calibrated too: "W" / "M" from native daily bars, native stamps as the period partition (`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 | | 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 | @@ -1227,7 +1229,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 (`sha256:de7e074b6a82f71018015f842ca51f4cba526d5b258a4c9b4d6c914ee12c0541` `sha256:b263f6923ef8095700dc254259434739b63a9fb043773b8139c77ef5e1df91b9` native-engine.md:423-437) | 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:823-824 → pine_adapter.cpp:17552 (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 `sha256:81dee89712544bf03ed75e83b6eb379d3707a6c920a59e368d0574d6a5b928ba` pine_strategy_host.cpp:806-807 → pine_adapter.cpp:17552 (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) @@ -1274,7 +1276,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:7179, native_execution_consumer.cpp:7179, native_execution_consumer.cpp:7179) 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: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). - `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:2340), 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:1010). 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). @@ -1301,7 +1303,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:2940-2986). 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 `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). - `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. @@ -1328,7 +1330,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:13120-13141). New hashed `RequestDefinition::origin { Host, KernelLiquidation, KernelRisk }`, new `CancelReason::Superseded` on re-price, `MarginCallEvent` joins `CommandEvent` (17 → 18, native_order.hpp:1278). O's alternative, a checkpoint on the excursion walk (`sha256:ed6f4ff668195caf3c953c8ef5dd448c6010949353f7b9ef44d50b764f7488db` native_execution_consumer.cpp:1895-1908), 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:13120-13141). New hashed `RequestDefinition::origin { Host, KernelLiquidation, KernelRisk }`, new `CancelReason::Superseded` on re-price, `MarginCallEvent` joins `CommandEvent` (17 → 18, native_order.hpp:1278). 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). - 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:4649-4662 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). @@ -1350,10 +1352,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:1616-1617) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6969-6981) 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:3478-3487). `born_on_remaining_path` (native_execution_consumer.cpp:5603-5607) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1654-1661) 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:1071-1078, whose body is native_execution_consumer.cpp:6819-6826). `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:2544); 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:1616-1617) → 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:1654-1661) 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:1071-1078, 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. 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) @@ -1391,7 +1393,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 (`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:3534-3539) +// 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) // 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; @@ -1419,7 +1421,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:3936-3958, `execute_current` path native_execution_consumer.cpp:3756-3766), `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:3936-3958, `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). ### A.8 L9 — risk limits (F P3 + O G7 + S P5) diff --git a/docs/pages/live-surface.md b/docs/pages/live-surface.md index 49a301d7..fe4b7450 100644 --- a/docs/pages/live-surface.md +++ b/docs/pages/live-surface.md @@ -68,7 +68,7 @@ configuration**, not one-shot: it stays in effect until a caller passes day), pinned to agree with the `i+1` lookahead on every interior bar of the 24x7 lanes. Since R5 lane F5 that is the kernel's answer, not the host's: the tail bar reads `NativeDecisionContext::closes_session_day_open_ended` - (`market_driver.hpp:158`), the same fact as `closes_session_day` on every + (`market_driver.hpp:164`), the same fact as `closes_session_day` on every bar except a batch's final one, which it reads off the calendar instead of the run end. Historical bars read early closes correctly from the session day alone, with no holiday calendar; a *forming* tail bar diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index f05cd510..aa2edccf 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -622,11 +622,11 @@ 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:5603-5607`). Requests accepted before the +`native_execution_consumer.cpp:5616-5620`). 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:7053-7055`). **Lookahead warning:** the +pass (`native_execution_consumer.cpp:7066-7068`). **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:6945`), the complete bar unless the spec asks for `NativeOpenBarView::OpenOnly` — so its @@ -1813,7 +1813,7 @@ says otherwise, and the C spellings are in | `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` | | `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:91`) 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` | | `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` | | `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` | @@ -1844,11 +1844,13 @@ rather than relabelled. Failure copy and move do not allocate. `last_error()` is presentation text beside it, never the authority. **Callback contexts.** The bar callbacks receive -`NativeDecisionContext` (`market_driver.hpp:114`): the point's -`NativeCoordinate` (`market_driver.hpp:71` — ordinal, the interval stamps, -the effective time, the `NativePathPhase` and the `NativeCompletionKind`), +`NativeDecisionContext` (`market_driver.hpp:120`): the point's +`NativeCoordinate` (`market_driver.hpp:72` — ordinal, the script-bar +`interval_index`, the named input-slot `input_interval_index`, the interval +stamps, the effective time, the `NativePathPhase` and the +`NativeCompletionKind`), the decision floor, both calendar intervals, and the script bar's -session-day facts (R5 lane F5): `in_session` (`market_driver.hpp:155`), +session-day facts (R5 lane F5): `in_session` (`market_driver.hpp:161`), `opens_session_day`, `closes_session_day` and `closes_session_day_open_ended`. They read the run's own calendar and session day — the cycle that rolls at the session's first window start, @@ -1983,7 +1985,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:7789`), so +(`record_script_report_point`, `native_execution_consumer.cpp:7802`), so ```text broker_state_hash_len == equity_curve_len == script_bars_processed @@ -2783,7 +2785,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:7871`). What it does +(`TimeframeAggregator` `native_execution_consumer.cpp:7884`). 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 ce5607d6..d875350d 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -125,8 +125,8 @@ rather than defaulted. | `commission_type` | `NativeFeeKind` native_run_spec.hpp:19 | `fee_kind` pineforge.h:538 | `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:538 | `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:538 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:3945-3946. TradingView's round-then-slip order is adapter-only. | -| `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:539 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:7511-7512, 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:541 | `native_open_lots_strategy.cpp` | Caps surviving+new physical lots `sha256:87bd0ff848901478840870d95eb356b35d7d368555ce8eadee8571b8e17f86ec` native_execution_consumer.cpp:2118-2121. Pine's per-cycle *entry count* is a TradingView rule, ruled **adapter-policy** in ADR-0001's table row `max_open_lots`. | +| `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:539 | `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:541 | `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`. | | `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:2102 | `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:2106 | `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:2276 | `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. | @@ -291,7 +291,7 @@ is `strategy_native_submit_v1` native_c_api.h:2678 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:539 | `native_risk_limits_strategy.cpp` | The default. | -| `strategy.direction.long` | `NativeOpenDirections::Long` native_run_spec.hpp:74 | `allowed_open_directions` pineforge.h:539 | `tests/test_native_run_spec.cpp` | An opening the direction forbids is refused with `OpeningDirection` native_execution_consumer.cpp:2941. | +| `strategy.direction.long` | `NativeOpenDirections::Long` native_run_spec.hpp:74 | `allowed_open_directions` pineforge.h:539 | `tests/test_native_run_spec.cpp` | An opening the direction forbids is refused with `OpeningDirection` native_execution_consumer.cpp:2954. | | `strategy.direction.short` | `NativeOpenDirections::Short` native_run_spec.hpp:75 | `allowed_open_directions` pineforge.h:539 | `tests/test_native_run_spec.cpp` | Same, the other way. | | `strategy.commission.percent` | `NativeFeeKind::Percent` native_run_spec.hpp:20 | `fee_kind` pineforge.h:538 | `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:538 | `tests/test_native_selected_requests.cpp` | Account currency per unit. | @@ -396,7 +396,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:539 | `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:2941. | +| `strategy.risk.allow_entry_in()` | `allowed_open_directions` native_run_spec.hpp:596 | `allowed_open_directions` pineforge.h:539 | `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:540 | `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:2312 | `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:2315 | `native_risk_limits_strategy.cpp` | Percent is of the day's opening equity. Blocks to the end of its own day. | @@ -455,10 +455,10 @@ openings TradingView takes. | a fill, as it happens | `on_native_applied` native_host.hpp:925 | `on_applied` native_c_api.h:2464 | `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:5361. | | raw input before aggregation | `on_native_input` native_host.hpp:863 | `on_input` native_c_api.h:2459 | `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:915 | `on_sub_bar` native_c_api.h:2500 | `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:114 with `NativeCoordinate` market_driver.hpp:71 | `pf_native_decision_v1` native_c_api.h:1454 | `hello_kernel.cpp` | Copied onto the callback stack; mutating it changes nothing. In C++ `script_interval` market_driver.hpp:118 and `input_interval` market_driver.hpp:117 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 session-day facts are the three rows below. | -| `session.ismarket` | `in_session` market_driver.hpp:155 | `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. | +| `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.isfirstbar` | `opens_session_day` market_driver.hpp:156 | `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:157, `closes_session_day_open_ended` market_driver.hpp:158 | `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"). | +| `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:1067 | `price` native_c_api.h:1533 | `native_selected_strategy.cpp` | The readiness preview `inspect_current_execution` native_host.hpp:1079 is an observation, never an apply token; `execute_current` native_host.hpp:1084 is the apply. | ## Position, account and report {#pine_to_native_map_report} diff --git a/include/pineforge/market_driver.hpp b/include/pineforge/market_driver.hpp index c2bf07f0..8f9005b7 100644 --- a/include/pineforge/market_driver.hpp +++ b/include/pineforge/market_driver.hpp @@ -62,7 +62,8 @@ enum class NativeCompletionKind : std::uint8_t { }; /// Everything the kernel knows about WHERE a point is, as one owning value: its -/// event ordinal, the interval index, the nominal and scheduled-eligible opens, +/// event ordinal, the script interval index, the named input interval index, +/// the nominal and scheduled-eligible opens, /// the last traded close, the next period and next input opens, the effective time /// the account converts and hashes at, the source price time, and the three /// classifications above. A host reads it through NativeDecisionContext; mutating @@ -71,6 +72,11 @@ enum class NativeCompletionKind : std::uint8_t { struct NativeCoordinate { uint64_t ordinal = 0; int interval_index = 0; + // The script-bar coordinate is the public index above. Aggregated runs + // retain the input slot that supplied this point here for hosts that need + // to inspect input cadence; it occupies the former alignment padding and + // does not change the v19 C++ layout. + int input_interval_index = 0; int64_t open_ms = 0; int64_t eligible_open_ms = 0; int64_t last_traded_close_ms = 0; diff --git a/src/native_execution_consumer.cpp b/src/native_execution_consumer.cpp index 77c24a02..409eb4bf 100644 --- a/src/native_execution_consumer.cpp +++ b/src/native_execution_consumer.cpp @@ -1051,6 +1051,7 @@ template void hash_coordinate(F& f, const NativeCoordinate& c) noexcept { f.u(c.ordinal); f.i(c.interval_index); + f.i(c.input_interval_index); f.i(c.open_ms); f.i(c.eligible_open_ms); f.i(c.last_traded_close_ms); @@ -1589,6 +1590,7 @@ uint64_t NativeExecutionConsumer::continuation_hash() const noexcept { f.b(processing_input_); f.u(static_cast(input_mode_)); f.i(next_interval_index_); + f.i(next_script_index_); // Declared higher-timeframe series carry their own delivery cursors, and // a stream's warmup boundary is where its live phase starts -- neither is // recoverable from the input count alone. Folded only for a run that @@ -1730,6 +1732,7 @@ uint64_t NativeExecutionConsumer::continuation_hash() const noexcept { f.i(script_.first_open_ms); f.i(script_.first_source_time_ms); f.i(script_.latest_close_ms); + f.i(script_.script_index); f.i(script_.first_index); f.i(script_.last_index); f.b(script_.modeled_ohlc); @@ -2264,6 +2267,7 @@ bool NativeExecutionConsumer::begin_ready(BacktestEngine& engine, NativeRunPhase consuming_request_ = false; draining_notifications_ = false; next_interval_index_ = 0; + next_script_index_ = 0; current_input_open_.reset(); observed_input_cursor_.reset(); next_tradable_synthesis_cursor_.reset(); @@ -2524,11 +2528,12 @@ void NativeExecutionConsumer::raise_floor(int64_t t) { NativeCoordinate NativeExecutionConsumer::coordinate_from( const native_calendar::NativeInterval& interval, - int index, int64_t effective, + int script_index, int input_index, int64_t effective, NativePriceProvenance provenance, NativePathPhase phase) const { NativeCoordinate c; - c.interval_index = index; + c.interval_index = script_index; + c.input_interval_index = input_index; c.open_ms = interval.open_ms; c.eligible_open_ms = interval.eligible_open_ms; c.last_traded_close_ms = interval.last_traded_close_ms; @@ -2541,6 +2546,14 @@ NativeCoordinate NativeExecutionConsumer::coordinate_from( return c; } +int NativeExecutionConsumer::script_index_for_input( + const native_calendar::NativeInterval& interval) const noexcept { + const auto script = script_interval_at(interval.open_ms); + if (script && script_.has_data && script_.key == script->open_ms) + return script_.script_index; + return next_script_index_; +} + void NativeExecutionConsumer::record_driver(const NativeDriverPoint& point) { // Every driver point is a point the journal window may close at under the // stress switch (set_retire_every_point); by default it closes at @@ -7965,7 +7978,8 @@ bool NativeExecutionConsumer::seal_stale_script(BacktestEngine& engine, void NativeExecutionConsumer::seal_script(BacktestEngine& engine, NativeCompletionKind kind) { if (!script_.has_data || script_.sealed) return; NativeCoordinate base; - base.interval_index = script_.first_index; + base.interval_index = script_.script_index; + base.input_interval_index = script_.first_index; base.open_ms = script_.interval.open_ms; base.eligible_open_ms = script_.interval.eligible_open_ms; base.last_traded_close_ms = script_.interval.last_traded_close_ms; @@ -7983,6 +7997,7 @@ void NativeExecutionConsumer::seal_script(BacktestEngine& engine, NativeCompleti } script_.sealed = true; script_.has_data = false; + ++next_script_index_; } bool NativeExecutionConsumer::final_script_session_closed() const noexcept { @@ -8022,6 +8037,7 @@ bool NativeExecutionConsumer::contribute_input( script_.first_open_ms = interval.open_ms; script_.first_source_time_ms = source_open; script_.latest_close_ms = source_close; + script_.script_index = next_script_index_; script_.first_index = index; script_.last_index = index; script_.sealed = false; @@ -8056,7 +8072,8 @@ bool NativeExecutionConsumer::contribute_input( script_ = ScriptBucket{}; } engine.current_bar_ = bar; - engine.bar_index_ = index; + engine.bar_index_ = script_.has_data + ? script_.script_index : std::max(0, next_script_index_ - 1); next_interval_index_ = index + 1; if (!failed() && kind != InputContribution::QuietCarried) ++engine.diag_input_bars_processed_; @@ -9393,7 +9410,8 @@ bool NativeExecutionConsumer::emit_quiet_carried_open( if (!has_last_price_) return true; if (next_tradable_synthesis_cursor_ == interval.open_ms) return true; NativeDriverPoint point; - point.coordinate = coordinate_from(interval, next_interval_index_, + point.coordinate = coordinate_from(interval, script_index_for_input(interval), + next_interval_index_, interval.eligible_open_ms, NativePriceProvenance::CarriedOpen, NativePathPhase::Open); @@ -9421,7 +9439,8 @@ bool NativeExecutionConsumer::finalize_observed_tick_slot( const bool equal = pairing_.pairing == native_calendar::TimeframePairing::Passthrough; if (equal) { NativeCoordinate calc; - calc.interval_index = next_interval_index_; + calc.interval_index = script_index_for_input(interval); + calc.input_interval_index = next_interval_index_; calc.open_ms = interval.open_ms; calc.eligible_open_ms = interval.eligible_open_ms; calc.last_traded_close_ms = interval.last_traded_close_ms; @@ -9515,7 +9534,8 @@ bool NativeExecutionConsumer::deliver_tick(BacktestEngine& engine, const TradeTi return false; } NativeDriverPoint point; - point.coordinate = coordinate_from(*interval, next_interval_index_, tick.timestamp, + point.coordinate = coordinate_from(*interval, script_index_for_input(*interval), + next_interval_index_, tick.timestamp, NativePriceProvenance::ObservedPrint, NativePathPhase::None); point.coordinate.ordinal = take_ordinal(engine); diff --git a/src/native_execution_consumer.hpp b/src/native_execution_consumer.hpp index bf9b2396..0c869d4d 100644 --- a/src/native_execution_consumer.hpp +++ b/src/native_execution_consumer.hpp @@ -661,6 +661,7 @@ class NativeExecutionConsumer final : public IExecutionConsumer { int64_t first_open_ms = 0; int64_t first_source_time_ms = 0; int64_t latest_close_ms = 0; + int script_index = 0; int first_index = 0; int last_index = 0; bool sealed = false; @@ -946,6 +947,7 @@ class NativeExecutionConsumer final : public IExecutionConsumer { bool preserve_status = false); bool preflight_intrabar_path(BacktestEngine& engine); void pump_batch(BacktestEngine& engine, const Bar* bars, int n); + int script_index_for_input(const native_calendar::NativeInterval& interval) const noexcept; bool consume_confirmed_input(BacktestEngine& engine, const Bar& bar, int index, bool last); bool contribute_input(BacktestEngine& engine, const Bar& bar, const native_calendar::NativeInterval& interval, @@ -1329,7 +1331,7 @@ class NativeExecutionConsumer final : public IExecutionConsumer { NativeEventRetention retention() const noexcept; void retire_journal() noexcept; NativeCoordinate coordinate_from(const native_calendar::NativeInterval& interval, - int index, int64_t effective, + int script_index, int input_index, int64_t effective, NativePriceProvenance provenance, NativePathPhase phase) const; bool preflight_ticks(BacktestEngine& engine, const TradeTick* ticks, int n); @@ -1462,6 +1464,7 @@ class NativeExecutionConsumer final : public IExecutionConsumer { bool processing_input_ = false; InputMode input_mode_ = InputMode::Unselected; int next_interval_index_ = 0; + int next_script_index_ = 0; std::optional current_input_open_; std::optional observed_input_cursor_; std::optional next_tradable_synthesis_cursor_; diff --git a/src/source/pine_adapter.cpp b/src/source/pine_adapter.cpp index c6b62ffa..6f1e0a27 100644 --- a/src/source/pine_adapter.cpp +++ b/src/source/pine_adapter.cpp @@ -18260,9 +18260,9 @@ void PineExecutionAdapter::on_applied(const native_order::ExecutionAppliedEvent& auto* pine = pine_view_of(&require_host()); const auto cohort = cohorts_by_id_.find(placement_snapshot->from_entry); if (pine && cohort != cohorts_by_id_.end()) { - // The lots carry the chart bar wherever the host re-stamps them - // (PineStrategyHost::on_native_applied): under the magnifier and, - // since lane F1, on every aggregated chart. + // The kernel books lot rows in script-bar space. This owner-side + // cohort rule intentionally compares the explicit chart source + // index to keep Pine's sampling cadence separate from that book. const auto state = detail::run_state(*pine); const int ratio = state.spec && !state.spec->timeframe_undetected ? tf_ratio(state.spec->input_tf, state.spec->script_tf) : 1; diff --git a/src/source/pine_scheduler_native.cpp b/src/source/pine_scheduler_native.cpp index aa920aa9..ce9e2679 100644 --- a/src/source/pine_scheduler_native.cpp +++ b/src/source/pine_scheduler_native.cpp @@ -638,10 +638,9 @@ void PineScheduler::recalculate(const native_order::ExecutionAppliedEvent& event 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 carry the chart bar (the host re-stamps - // them, lane F1) where the kernel left its interval index, the input bar - // the bucket opens on, in bar_index_: the entry-bar mask reads the chart - // bar there. Under the magnifier it keeps the index it has always read. + // 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_) diff --git a/src/source/pine_strategy_host.cpp b/src/source/pine_strategy_host.cpp index 8e2a1f49..11fd37df 100644 --- a/src/source/pine_strategy_host.cpp +++ b/src/source/pine_strategy_host.cpp @@ -119,9 +119,10 @@ 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 INPUT bar a script bucket opens -// on (390 for the 26th 15m bar of a 1m feed), where a Pine script counts chart -// bars, as ab9714be's aggregation loop did (lane F1). +// 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 @@ -384,8 +385,9 @@ void source::PineStrategyHost::on_native_tick( 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 lots carry - // chart-bar indices wherever on_native_applied re-stamps them. + // 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) @@ -418,9 +420,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(); - // The lots carry chart-bar indices wherever on_native_applied re-stamps - // them, so the entry-bar tests below read the chart bar there. Under the - // magnifier the slippage mask keeps the kernel index it has always read. + // 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) @@ -489,26 +490,8 @@ void source::PineStrategyHost::on_native_applied( } } if (source_prepare_failed_) return; - // Pine counts chart bars. Under the bar magnifier and on an aggregated - // chart the kernel books a fill at an input bar's index, so the lot the - // fill opened and the rows it closed carry the chart bar instead, as - // ab9714be's aggregation loops booked them (lane F1 extended this from - // the magnifier to every aggregated chart). - const bool aggregated = detail::run_aggregates_input_bars(detail::run_consumer(*this), &adapter_, adapter_.run_counter_); - if (scheduler_.bar_magnifier_enabled() || aggregated) { - const int source_index = scheduler_.source_bar_index_for(context); - for (auto& lot : pyramid_entries_) { - if (lot.entry_incarnation == event.handle().incarnation - && event.opened_units != 0.0) { - lot.entry_bar_index = source_index; - } - } - for (std::size_t i = 0; i < event.closed_trade_count; ++i) { - const std::size_t index = event.first_trade_index + i; - if (index >= trades_.size()) continue; - trades_[index].exit_bar_index = source_index; - } - } + const bool aggregated = detail::run_aggregates_input_bars( + detail::run_consumer(*this), &adapter_, adapter_.run_counter_); // TradingView dates every fill of a chart bar at the bar's open, a // process_orders_on_close fill included (the e25-f-islastbar tape, lane // E27). The chart timeframe's zero-width interval gives exactly that: @@ -840,19 +823,7 @@ ClosedLotExcursion source::PineStrategyHost::owner_lot_excursion( fill_fav = 0.0; } ClosedLotExcursion owned; - // Whether the lot opened on the bar this fill closes it on. A plain - // aggregated chart's lot carries its chart bar (on_native_applied - // re-stamps it, lane F1) while the kernel states the exit at its own - // interval index, the input bar the bucket opens on; the exit's chart bar - // is the one the adapter opened last. Under the magnifier the test keeps - // the kernel index it has always read. - int exit_bar_index = facts.exit_bar_index; - if (!scheduler_.bar_magnifier_enabled() && detail::run_aggregates_input_bars(detail::run_consumer(*this), &adapter_, adapter_.run_counter_)) { - NativeDecisionContext current{}; - current.script_bar_open_ms = adapter_.last_broker_open_ms_; - exit_bar_index = scheduler_.source_bar_index_for(current); - } - const bool same_bar = facts.entry_bar_index == exit_bar_index; + 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 diff --git a/tests/native_event_rows_fixture.hpp b/tests/native_event_rows_fixture.hpp index 5a194bb2..8f287ea9 100644 --- a/tests/native_event_rows_fixture.hpp +++ b/tests/native_event_rows_fixture.hpp @@ -119,6 +119,7 @@ inline void fold_row(Fold& f, const pineforge::NativeMarketEvent& row) { f.u(static_cast(c.effective_time_ms)); f.u(static_cast(c.open_ms)); f.u(static_cast(c.interval_index)); + f.u(static_cast(c.input_interval_index)); f.u(static_cast(c.provenance)); f.u(static_cast(c.path_phase)); f.d(row.driver->raw_price); diff --git a/tests/native_order_full_fold.hpp b/tests/native_order_full_fold.hpp index f5ac3f4a..0be96fcd 100644 --- a/tests/native_order_full_fold.hpp +++ b/tests/native_order_full_fold.hpp @@ -97,6 +97,7 @@ void fold_vector(Fold& f, const std::vector& values) { inline void fold(Fold& f, const pineforge::NativeCoordinate& c) { f.u(c.ordinal); f.i(c.interval_index); + f.i(c.input_interval_index); f.i(c.open_ms); f.i(c.eligible_open_ms); f.i(c.last_traded_close_ms); diff --git a/tests/test_adapter_in_place_reads.cpp b/tests/test_adapter_in_place_reads.cpp index ec7a81b7..e899c332 100644 --- a/tests/test_adapter_in_place_reads.cpp +++ b/tests/test_adapter_in_place_reads.cpp @@ -70,6 +70,7 @@ std::uint64_t point_digest(const NativeCurrentPointView& point) { std::uint64_t h = 1469598103934665603ull; h = fold(h, d.coordinate.ordinal); h = fold(h, static_cast(d.coordinate.interval_index)); + h = fold(h, static_cast(d.coordinate.input_interval_index)); h = fold(h, static_cast(d.coordinate.effective_time_ms)); h = fold(h, static_cast(d.coordinate.provenance)); h = fold(h, static_cast(d.coordinate.path_phase)); diff --git a/tests/test_adapter_live_state_equivalence.cpp b/tests/test_adapter_live_state_equivalence.cpp index 9fec15e6..46e7906a 100644 --- a/tests/test_adapter_live_state_equivalence.cpp +++ b/tests/test_adapter_live_state_equivalence.cpp @@ -503,6 +503,7 @@ class ScriptHost final : public source::PineStrategyHost { fold_definition(f, event.definition); f.u(static_cast(event.reason)); f.i(event.cursor.point.interval_index); + f.i(event.cursor.point.input_interval_index); } else if constexpr (std::is_same_v) { fold_definition(f, event.definition); f.d(event.raw_price); f.d(event.resolved_price); f.d(event.closed_units); diff --git a/tests/test_adapter_live_state_equivalence_data.hpp b/tests/test_adapter_live_state_equivalence_data.hpp index 581ffef8..89cc3e9b 100644 --- a/tests/test_adapter_live_state_equivalence_data.hpp +++ b/tests/test_adapter_live_state_equivalence_data.hpp @@ -17,113 +17,115 @@ // label are unchanged. #pragma once +// K-IDX Option A re-pins the v19 transcript digests once for the script/input coordinate fold. + constexpr std::uint64_t kTranscriptDigests[] = { - 12303832286788334682ull, // reversals seed 104729, 131 trades, 396 commands, 420 rows placed; expectation corrected: 18131238421695920397 -> 12303832286788334682, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 3937583110602879389ull, // reversals seed 209458, 126 trades, 382 commands, 409 rows placed; expectation corrected: 16602191927869292264 -> 3937583110602879389, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 14034630447232634498ull, // reversals seed 314187, 208 trades, 684 commands, 653 rows placed; expectation corrected: 231611374984470899 -> 14034630447232634498, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 9043206432920183632ull, // reversals seed 418916, 105 trades, 378 commands, 361 rows placed; expectation corrected: 9115028885544041886 -> 9043206432920183632, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 201369272286234124ull, // reversals seed 523645, 104 trades, 417 commands, 357 rows placed; expectation corrected: 13664841540924280471 -> 201369272286234124, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 16050586294934063210ull, // reversals seed 628374, 138 trades, 410 commands, 393 rows placed; expectation corrected: 1083209463485241499 -> 16050586294934063210, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 1738999436590496385ull, // reversals seed 733103, 139 trades, 386 commands, 404 rows placed; expectation corrected: 5873438457293216917 -> 1738999436590496385, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 17418030836045893247ull, // reversals seed 837832, 91 trades, 401 commands, 332 rows placed; expectation corrected: 6414222399873604849 -> 17418030836045893247, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 11982413728412398791ull, // reversals seed 942561, 98 trades, 408 commands, 356 rows placed; expectation corrected: 11813731838878840527 -> 11982413728412398791, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 14895943053844515952ull, // reversals seed 1047290, 134 trades, 402 commands, 393 rows placed; expectation corrected: 136176368523764018 -> 14895943053844515952, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 15845567806271913335ull, // reversals seed 1152019, 148 trades, 386 commands, 415 rows placed; expectation corrected: 12865468784526995650 -> 15845567806271913335, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 18388647233060516099ull, // reversals seed 1256748, 118 trades, 402 commands, 362 rows placed; expectation corrected: 14176719241415607847 -> 18388647233060516099, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 15062472968639535903ull, // reversals seed 1361477, 105 trades, 413 commands, 349 rows placed; expectation corrected: 10742650442107119928 -> 15062472968639535903, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 17388918647310229967ull, // reversals seed 1466206, 151 trades, 372 commands, 393 rows placed; expectation corrected: 17224679935795555118 -> 17388918647310229967, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 17595837658874606436ull, // reversals seed 1570935, 77 trades, 357 commands, 297 rows placed; expectation corrected: 3080867217436444086 -> 17595837658874606436, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 8598226526726960377ull, // reversals seed 1675664, 132 trades, 397 commands, 406 rows placed; expectation corrected: 7852991775336859993 -> 8598226526726960377, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 1495070233845853338ull, // reversals seed 1780393, 197 trades, 704 commands, 645 rows placed; expectation corrected: 10966002126385880448 -> 1495070233845853338, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 9534990178667899707ull, // reversals seed 1885122, 113 trades, 393 commands, 388 rows placed; expectation corrected: 2096380577187654713 -> 9534990178667899707, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 2893794458913785470ull, // reversals seed 1989851, 180 trades, 430 commands, 448 rows placed; expectation corrected: 10146443718068128379 -> 2893794458913785470, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 6120235323624511436ull, // reversals seed 2094580, 92 trades, 384 commands, 340 rows placed; expectation corrected: 690888026844062800 -> 6120235323624511436, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 10066963827350094002ull, // reversals seed 2199309, 114 trades, 375 commands, 341 rows placed; expectation corrected: 8740380424255135244 -> 10066963827350094002, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 7982803305843256609ull, // reversals seed 2304038, 149 trades, 412 commands, 418 rows placed; expectation corrected: 7498458600957068498 -> 7982803305843256609, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 12871145682704028584ull, // reversals seed 2408767, 139 trades, 415 commands, 404 rows placed; expectation corrected: 12308474164129257233 -> 12871145682704028584, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 4597224690432296124ull, // reversals seed 2513496, 110 trades, 390 commands, 394 rows placed; expectation corrected: 11136018481283522759 -> 4597224690432296124, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 1610718425964291349ull, // reversals seed 2618225, 105 trades, 411 commands, 371 rows placed; expectation corrected: 12982972984933103033 -> 1610718425964291349, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 16169955000323314060ull, // reversals seed 2722954, 153 trades, 390 commands, 423 rows placed; expectation corrected: 94027180956824322 -> 16169955000323314060, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 1991085622905519718ull, // reversals seed 2827683, 72 trades, 339 commands, 277 rows placed; expectation corrected: 2586989227520445085 -> 1991085622905519718, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 4724224977341801620ull, // reversals seed 2932412, 96 trades, 360 commands, 318 rows placed; expectation corrected: 2806156414591446904 -> 4724224977341801620, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 1346366755523289949ull, // reversals seed 3037141, 114 trades, 405 commands, 377 rows placed; expectation corrected: 7681409411584951106 -> 1346366755523289949, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 16138775703366814640ull, // reversals seed 3141870, 66 trades, 332 commands, 248 rows placed; expectation corrected: 2136408739223833888 -> 16138775703366814640, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 15380097802441451398ull, // reversals seed 3246599, 135 trades, 415 commands, 422 rows placed, error: native current evaluated allowance mismatch; expectation corrected: 2701569228821610452 -> 15380097802441451398, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 1508117546357947136ull, // reversals seed 3351328, 108 trades, 414 commands, 345 rows placed; expectation corrected: 14833285915703769820 -> 1508117546357947136, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 13522312330303729782ull, // reversals seed 3456057, 88 trades, 373 commands, 334 rows placed; expectation corrected: 14316999025162314930 -> 13522312330303729782, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 2033024822879045743ull, // reversals seed 3560786, 147 trades, 413 commands, 444 rows placed; expectation corrected: 2893747481137195000 -> 2033024822879045743, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 15120456852687109028ull, // reversals seed 3665515, 133 trades, 394 commands, 397 rows placed; expectation corrected: 15784844343272452693 -> 15120456852687109028, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 18416122899997081013ull, // reversals seed 3770244, 53 trades, 339 commands, 240 rows placed; expectation corrected: 11905467654337006332 -> 18416122899997081013, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 1437776089797098821ull, // brackets seed 104730, 82 trades, 521 commands, 568 rows placed; expectation corrected: 8643457388998818943 -> 1437776089797098821, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 3732927941814356572ull, // brackets seed 209459, 77 trades, 487 commands, 551 rows placed; expectation corrected: 902038679086949503 -> 3732927941814356572, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 7855481804606579091ull, // brackets seed 314188, 147 trades, 866 commands, 1036 rows placed; expectation corrected: 11047047301388273120 -> 7855481804606579091, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 3737662390130165609ull, // brackets seed 418917, 94 trades, 557 commands, 632 rows placed; expectation corrected: 7275918846800733080 -> 3737662390130165609, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 621130846286431956ull, // brackets seed 523646, 71 trades, 517 commands, 451 rows placed; expectation corrected: 13653297247282953934 -> 621130846286431956, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 2076570233399109199ull, // brackets seed 628375, 100 trades, 536 commands, 662 rows placed; expectation corrected: 14781820592778371195 -> 2076570233399109199, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 473230311357296324ull, // brackets seed 733104, 91 trades, 534 commands, 668 rows placed; expectation corrected: 13220710615158200543 -> 473230311357296324, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 5785846610847751202ull, // brackets seed 837833, 88 trades, 482 commands, 547 rows placed; expectation corrected: 6924339124515036478 -> 5785846610847751202, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 10678325805503261473ull, // brackets seed 942562, 85 trades, 490 commands, 457 rows placed; expectation corrected: 447890157475899310 -> 10678325805503261473, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 18055465226417158208ull, // brackets seed 1047291, 86 trades, 468 commands, 526 rows placed; expectation corrected: 9260549732165415681 -> 18055465226417158208, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 3033931888211305544ull, // brackets seed 1152020, 80 trades, 487 commands, 637 rows placed; expectation corrected: 17137747942738656854 -> 3033931888211305544, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 15849491031983505500ull, // brackets seed 1256749, 88 trades, 511 commands, 566 rows placed; expectation corrected: 3581838780665307396 -> 15849491031983505500, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 18155599677917613577ull, // brackets seed 1361478, 81 trades, 540 commands, 556 rows placed; expectation corrected: 528981059190897533 -> 18155599677917613577, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 6438015927310529334ull, // brackets seed 1466207, 72 trades, 568 commands, 577 rows placed; expectation corrected: 15723147559873484934 -> 6438015927310529334, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 3046148180358104867ull, // brackets seed 1570936, 69 trades, 475 commands, 535 rows placed; expectation corrected: 14563285821087943352 -> 3046148180358104867, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 15751662755974333447ull, // brackets seed 1675665, 87 trades, 520 commands, 602 rows placed; expectation corrected: 2009672890833994838 -> 15751662755974333447, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 11565075807030336076ull, // brackets seed 1780394, 95 trades, 697 commands, 696 rows placed; expectation corrected: 13953397684263873356 -> 11565075807030336076, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 2015779222221407240ull, // brackets seed 1885123, 90 trades, 542 commands, 587 rows placed; expectation corrected: 5643943905039461363 -> 2015779222221407240, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 18424043409043824193ull, // brackets seed 1989852, 88 trades, 514 commands, 572 rows placed; expectation corrected: 6446158334200606271 -> 18424043409043824193, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 11372035822747614056ull, // brackets seed 2094581, 85 trades, 496 commands, 575 rows placed; expectation corrected: 16741159572911773919 -> 11372035822747614056, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 10055754095368358239ull, // brackets seed 2199310, 79 trades, 446 commands, 504 rows placed; expectation corrected: 5157791750556136276 -> 10055754095368358239, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 1455827488198344686ull, // brackets seed 2304039, 92 trades, 524 commands, 594 rows placed; expectation corrected: 5026426233064573920 -> 1455827488198344686, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 11979119011511214626ull, // brackets seed 2408768, 94 trades, 559 commands, 694 rows placed; expectation corrected: 8372938499495766236 -> 11979119011511214626, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 1774065971302770478ull, // brackets seed 2513497, 86 trades, 524 commands, 508 rows placed; expectation corrected: 9983745863048731897 -> 1774065971302770478, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 10830766203810780391ull, // brackets seed 2618226, 77 trades, 512 commands, 581 rows placed; expectation corrected: 3116574853050987014 -> 10830766203810780391, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 1754765538538121333ull, // brackets seed 2722955, 85 trades, 503 commands, 661 rows placed; expectation corrected: 4449959286697503302 -> 1754765538538121333, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 13625602724086270269ull, // brackets seed 2827684, 74 trades, 506 commands, 570 rows placed; expectation corrected: 13513587848084238028 -> 13625602724086270269, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 4716911134077760843ull, // brackets seed 2932413, 75 trades, 469 commands, 515 rows placed; expectation corrected: 3820103226027217069 -> 4716911134077760843, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 6708847418396898846ull, // brackets seed 3037142, 72 trades, 476 commands, 521 rows placed; expectation corrected: 17433781471451414265 -> 6708847418396898846, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 17719975631366854649ull, // brackets seed 3141871, 101 trades, 537 commands, 653 rows placed; expectation corrected: 14256578495702109160 -> 17719975631366854649, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 1791382993905034368ull, // brackets seed 3246600, 132 trades, 700 commands, 868 rows placed; expectation corrected: 9523999177047936085 -> 1791382993905034368, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 14331750725646803575ull, // brackets seed 3351329, 85 trades, 504 commands, 520 rows placed; expectation corrected: 15396286219775825004 -> 14331750725646803575, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 7444517548780988427ull, // brackets seed 3456058, 80 trades, 530 commands, 529 rows placed; expectation corrected: 8068183468371318007 -> 7444517548780988427, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 12745284405112381541ull, // brackets seed 3560787, 107 trades, 521 commands, 634 rows placed; expectation corrected: 5081162457764673072 -> 12745284405112381541, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 11302959202215131177ull, // brackets seed 3665516, 86 trades, 536 commands, 566 rows placed; expectation corrected: 8044542629328363745 -> 11302959202215131177, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 5122510577651000745ull, // brackets seed 3770245, 78 trades, 503 commands, 560 rows placed; expectation corrected: 6571220633977791831 -> 5122510577651000745, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 18021051505772238050ull, // chains seed 104731, 59 trades, 301 commands, 286 rows placed; expectation corrected: 17534408793192022275 -> 18021051505772238050, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 13132700140669805294ull, // chains seed 209460, 85 trades, 390 commands, 393 rows placed; expectation corrected: 14469238841030339065 -> 13132700140669805294, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 10251523807061229809ull, // chains seed 314189, 107 trades, 526 commands, 446 rows placed; expectation corrected: 6026647085473896911 -> 10251523807061229809, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 3484742836604887248ull, // chains seed 418918, 45 trades, 312 commands, 299 rows placed; expectation corrected: 12157747221315758367 -> 3484742836604887248, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 165784926829741990ull, // chains seed 523647, 61 trades, 322 commands, 329 rows placed; expectation corrected: 14170042428379127405 -> 165784926829741990, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 4985407749752396416ull, // chains seed 628376, 95 trades, 364 commands, 391 rows placed; expectation corrected: 18014569555818259412 -> 4985407749752396416, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 7441640167844508013ull, // chains seed 733105, 88 trades, 351 commands, 319 rows placed; expectation corrected: 11589106698728586749 -> 7441640167844508013, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 10108332716913767868ull, // chains seed 837834, 64 trades, 359 commands, 358 rows placed; expectation corrected: 17846092370411210811 -> 10108332716913767868, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 7072159996087394048ull, // chains seed 942563, 59 trades, 341 commands, 322 rows placed; expectation corrected: 3127678506736305496 -> 7072159996087394048, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 2647666278133997762ull, // chains seed 1047292, 64 trades, 343 commands, 344 rows placed; expectation corrected: 15644106941906269656 -> 2647666278133997762, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 15606848329850485008ull, // chains seed 1152021, 70 trades, 342 commands, 418 rows placed; expectation corrected: 14577691908362409842 -> 15606848329850485008, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 9428420285779825982ull, // chains seed 1256750, 65 trades, 319 commands, 302 rows placed; expectation corrected: 4548286382166158553 -> 9428420285779825982, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 1636016389546528478ull, // chains seed 1361479, 64 trades, 312 commands, 323 rows placed; expectation corrected: 6855323446875479305 -> 1636016389546528478, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 13767422546817055791ull, // chains seed 1466208, 73 trades, 382 commands, 345 rows placed; expectation corrected: 9906891830010102846 -> 13767422546817055791, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 12920035860098219755ull, // chains seed 1570937, 90 trades, 366 commands, 419 rows placed; expectation corrected: 17052664136311653837 -> 12920035860098219755, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 2115012775096152354ull, // chains seed 1675666, 64 trades, 321 commands, 389 rows placed; expectation corrected: 5485644523587285976 -> 2115012775096152354, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 12765469987058303812ull, // chains seed 1780395, 82 trades, 480 commands, 461 rows placed; expectation corrected: 1238640065168520020 -> 12765469987058303812, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 5285579298017076157ull, // chains seed 1885124, 66 trades, 360 commands, 304 rows placed; expectation corrected: 9459986323782860188 -> 5285579298017076157, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 8033691744657508662ull, // chains seed 1989853, 73 trades, 353 commands, 373 rows placed; expectation corrected: 13738959655178528135 -> 8033691744657508662, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 6926029722960384835ull, // chains seed 2094582, 75 trades, 321 commands, 365 rows placed; expectation corrected: 17037346536051525295 -> 6926029722960384835, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 18184542950344059007ull, // chains seed 2199311, 56 trades, 327 commands, 306 rows placed; expectation corrected: 9379311852477391176 -> 18184542950344059007, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 6167262563711331013ull, // chains seed 2304040, 81 trades, 342 commands, 354 rows placed; expectation corrected: 5995610355164878802 -> 6167262563711331013, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 14095125304507107324ull, // chains seed 2408769, 67 trades, 347 commands, 374 rows placed; expectation corrected: 15872066828034705283 -> 14095125304507107324, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 15218329632646994483ull, // chains seed 2513498, 66 trades, 336 commands, 338 rows placed; expectation corrected: 12752724610839071627 -> 15218329632646994483, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 14263574384454909329ull, // chains seed 2618227, 65 trades, 336 commands, 355 rows placed; expectation corrected: 16284581732957401088 -> 14263574384454909329, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 18162713119022504249ull, // chains seed 2722956, 87 trades, 363 commands, 366 rows placed; expectation corrected: 203152656759302621 -> 18162713119022504249, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 17764036597638082006ull, // chains seed 2827685, 57 trades, 337 commands, 325 rows placed; expectation corrected: 17443267043941096516 -> 17764036597638082006, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 10884597100613452852ull, // chains seed 2932414, 65 trades, 350 commands, 331 rows placed; expectation corrected: 11267177131992033103 -> 10884597100613452852, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 8164707371872766708ull, // chains seed 3037143, 81 trades, 324 commands, 358 rows placed; expectation corrected: 13133225908980478723 -> 8164707371872766708, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 2150437808164321484ull, // chains seed 3141872, 82 trades, 355 commands, 349 rows placed; expectation corrected: 12851275957818104891 -> 2150437808164321484, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 16141338846980328349ull, // chains seed 3246601, 100 trades, 485 commands, 473 rows placed; expectation corrected: 14888901744694749090 -> 16141338846980328349, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 10570656349418425385ull, // chains seed 3351330, 60 trades, 311 commands, 317 rows placed; expectation corrected: 15503283238765826799 -> 10570656349418425385, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 4168066107549506279ull, // chains seed 3456059, 59 trades, 292 commands, 298 rows placed; expectation corrected: 10149016591456765905 -> 4168066107549506279, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 12441518321478214338ull, // chains seed 3560788, 73 trades, 359 commands, 338 rows placed; expectation corrected: 9067931166147488615 -> 12441518321478214338, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 3084783482698433102ull, // chains seed 3665517, 70 trades, 347 commands, 326 rows placed; expectation corrected: 14197586845906529491 -> 3084783482698433102, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) - 10771028305569526917ull, // chains seed 3770246, 76 trades, 362 commands, 298 rows placed; expectation corrected: 7655485237433838066 -> 10771028305569526917, because v19-D carries re-issued exits' bindings (no CloseBoundEvent) + 12303832286788334682ull, // reversals seed 104729, 131 trades, 396 commands, 420 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 + 201369272286234124ull, // reversals seed 523645, 104 trades, 417 commands, 357 rows placed + 1718744356722272312ull, // reversals seed 628374, 138 trades, 410 commands, 393 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 + 15845567806271913335ull, // reversals seed 1152019, 148 trades, 386 commands, 415 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 + 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 + 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 + 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 + 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 + 11999293368236543422ull, // reversals seed 3770244, 53 trades, 339 commands, 240 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 + 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 + 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 + 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 + 15751662755974333447ull, // brackets seed 1675665, 87 trades, 520 commands, 602 rows placed + 11565075807030336076ull, // brackets seed 1780394, 95 trades, 697 commands, 696 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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_native_calendar_driver_contract.cpp b/tests/test_native_calendar_driver_contract.cpp index 7d36894e..2d96cc9e 100644 --- a/tests/test_native_calendar_driver_contract.cpp +++ b/tests/test_native_calendar_driver_contract.cpp @@ -294,6 +294,7 @@ std::string coordinate_json(const NativeCoordinate& c) { std::ostringstream out; out << "{\"ordinal\":" << json_u64(c.ordinal) << ",\"intervalIndex\":" << c.interval_index + << ",\"inputIntervalIndex\":" << c.input_interval_index << ",\"openMs\":" << json_i64(c.open_ms) << ",\"eligibleOpenMs\":" << json_i64(c.eligible_open_ms) << ",\"lastTradedCloseMs\":" << json_i64(c.last_traded_close_ms) diff --git a/tests/test_native_calendar_hash_witness.cpp b/tests/test_native_calendar_hash_witness.cpp index 9f90d93d..1d4071ae 100644 --- a/tests/test_native_calendar_hash_witness.cpp +++ b/tests/test_native_calendar_hash_witness.cpp @@ -105,6 +105,7 @@ struct Fold { void coordinate(const NativeCoordinate& c) { u64(c.ordinal); i64(c.interval_index); + i64(c.input_interval_index); i64(c.open_ms); i64(c.eligible_open_ms); i64(c.last_traded_close_ms); @@ -562,23 +563,23 @@ struct Pinned { }; #ifndef PINEFORGE_K1_HARVEST -// Observed on engine main fc7aad62, before lane PERF-K1. +// K-IDX Option A re-pins the v19 coordinate witness once. const Pinned kPinned[] = { // PINNED-BEGIN - {"utc_year_end", 0x12a983768e1c150aULL, 1200, 1200, 1200, 0, 0, 4800, 112, 16}, - {"ny_masked_dst", 0xf7c3d866b679a148ULL, 1400, 1400, 1400, 0, 0, 5600, 168, 24}, - {"tokyo_lunch", 0x6169cc1a0c1c9798ULL, 1200, 1200, 1200, 0, 0, 4800, 101, 14}, - {"london_dst", 0x36decf92d3c95856ULL, 400, 400, 400, 0, 0, 1600, 80, 11}, - {"posix_overnight", 0x82af75049101b94aULL, 140, 140, 140, 0, 0, 560, 67, 9}, - {"fixed_offset", 0x7be69391f0425aadULL, 900, 900, 900, 0, 0, 3600, 136, 19}, + {"utc_year_end", 0xb941690ef38812a2ULL, 1200, 1200, 1200, 0, 0, 4800, 112, 16}, + {"ny_masked_dst", 0xa01f619809a8a8bcULL, 1400, 1400, 1400, 0, 0, 5600, 168, 24}, + {"tokyo_lunch", 0x2ec04b88881e49acULL, 1200, 1200, 1200, 0, 0, 4800, 101, 14}, + {"london_dst", 0xfa652480d4640d2eULL, 400, 400, 400, 0, 0, 1600, 80, 11}, + {"posix_overnight", 0xc07057fe81564f56ULL, 140, 140, 140, 0, 0, 560, 67, 9}, + {"fixed_offset", 0x484ca66d57dc239dULL, 900, 900, 900, 0, 0, 3600, 136, 19}, // expectation corrected (KERNEL-EDGE, v19): the calendar-closed final // daily bucket is calculated at batch end, including its last trade. {"ny_daily_over_hourly", 0xfa85a7f9a92aea39ULL, 13, 13, 300, 0, 0, 52, 14, 2}, - {"ny_hourly_series", 0x9237cf98641532f4ULL, 234, 234, 234, 21, 0, 936, 42, 6}, - {"tolerant_aggregating", 0x0110dff138377266ULL, 200, 200, 600, 0, 0, 800, 63, 9}, - {"tolerant_stream_night", 0x7de1da848e8cb622ULL, 34, 34, 34, 0, 0, 136, 21, 3}, - {"canonical_stream_night", 0xbdbc12b421f53424ULL, 110, 110, 110, 0, 0, 440, 42, 6}, - {"tick_stream", 0xbb0a3a50f13c6aa2ULL, 14, 10, 50, 0, 60, 103, 10, 1}, + {"ny_hourly_series", 0x284026ee6aa32186ULL, 234, 234, 234, 21, 0, 936, 42, 6}, + {"tolerant_aggregating", 0xfa838ffaa5666c82ULL, 200, 200, 600, 0, 0, 800, 63, 9}, + {"tolerant_stream_night", 0x8aa785e43e2916d0ULL, 34, 34, 34, 0, 0, 136, 21, 3}, + {"canonical_stream_night", 0x5f0cc60fc521a1a2ULL, 110, 110, 110, 0, 0, 440, 42, 6}, + {"tick_stream", 0x24a2c575b74ac52aULL, 14, 10, 50, 0, 60, 103, 10, 1}, // PINNED-END }; #endif diff --git a/tests/test_native_execution_terms.cpp b/tests/test_native_execution_terms.cpp index ef0f54b0..58c4ef30 100644 --- a/tests/test_native_execution_terms.cpp +++ b/tests/test_native_execution_terms.cpp @@ -50,6 +50,7 @@ void bit_helper_controls() { bool same_coordinate_bits(const NativeCoordinate& a, const NativeCoordinate& b) { return a.ordinal == b.ordinal && a.interval_index == b.interval_index + && a.input_interval_index == b.input_interval_index && a.open_ms == b.open_ms && a.eligible_open_ms == b.eligible_open_ms && a.last_traded_close_ms == b.last_traded_close_ms diff --git a/tests/test_native_handle_stable_replace.cpp b/tests/test_native_handle_stable_replace.cpp index a058444a..413386c2 100644 --- a/tests/test_native_handle_stable_replace.cpp +++ b/tests/test_native_handle_stable_replace.cpp @@ -545,6 +545,7 @@ void read_journal(const ReissueHost& 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.input_interval_index); f.i(c.point.effective_time_ms); f.e(c.point.path_phase); f.d(c.t); diff --git a/tests/test_native_host_reads.cpp b/tests/test_native_host_reads.cpp index 91c7e7f3..087b9fee 100644 --- a/tests/test_native_host_reads.cpp +++ b/tests/test_native_host_reads.cpp @@ -129,6 +129,7 @@ bool same_bits(double a, double b) { return std::memcmp(&a, &b, sizeof a) == 0; bool same_coordinate(const NativeCoordinate& a, const NativeCoordinate& b) { return a.ordinal == b.ordinal && a.interval_index == b.interval_index + && a.input_interval_index == b.input_interval_index && a.open_ms == b.open_ms && a.eligible_open_ms == b.eligible_open_ms && a.last_traded_close_ms == b.last_traded_close_ms && a.next_period_open_ms == b.next_period_open_ms diff --git a/tests/test_native_in_place_reads.cpp b/tests/test_native_in_place_reads.cpp index aa64e1d2..28e09d0b 100644 --- a/tests/test_native_in_place_reads.cpp +++ b/tests/test_native_in_place_reads.cpp @@ -56,6 +56,7 @@ std::uint64_t point_digest(const NativeCurrentPointView& point) { const NativeDecisionContext& d = point.decision; h = k3_book::fnv_u64(h, d.coordinate.ordinal); h = k3_book::fnv_u64(h, static_cast(d.coordinate.interval_index)); + h = k3_book::fnv_u64(h, static_cast(d.coordinate.input_interval_index)); h = k3_book::fnv_u64(h, static_cast(d.coordinate.open_ms)); h = k3_book::fnv_u64(h, static_cast(d.coordinate.effective_time_ms)); h = k3_book::fnv_u64(h, static_cast(d.coordinate.source_price_time_ms)); diff --git a/tests/test_native_lean_path.cpp b/tests/test_native_lean_path.cpp index b76acd4d..ebe5c2fa 100644 --- a/tests/test_native_lean_path.cpp +++ b/tests/test_native_lean_path.cpp @@ -82,7 +82,8 @@ struct Fnv { }; void fold_coordinate(Fnv& f, const NativeCoordinate& c) { - f.v(c.ordinal); f.v(c.interval_index); f.v(c.open_ms); f.v(c.eligible_open_ms); + f.v(c.ordinal); f.v(c.interval_index); f.v(c.input_interval_index); + f.v(c.open_ms); f.v(c.eligible_open_ms); f.v(c.last_traded_close_ms); f.v(c.next_period_open_ms); f.v(c.next_input_open_ms); f.v(c.effective_time_ms); f.v(c.source_price_time_ms); f.v(static_cast(c.provenance)); f.v(static_cast(c.path_phase)); @@ -590,37 +591,38 @@ struct Pin { // expectation corrected (6 values), because v19-B folds the FX-roll check's last two driver-point instants into the continuation under a staged FX curve, now that no driver log is kept -- margin_fx_roll's fold moved only through the continuation and broker-state values it reads; with that one fold masked the rows are main's again; every hook's call count and the trades did not move; harvested with -DPF_L1_HARVEST and PF_LEAN_PATH_DUMP=1 against main 3eb2cb84 (reproduces every old pin), this tree and V19-B's tip bc749095 (byte-identical rows, identical hook counts): // margin_fx_roll/canonical: fold 2789533630001178538ULL -> 14184613073071204061ULL, continuation 17178754079582823760ULL -> 1188032297556811944ULL, broker 209420367109385368ULL -> 5438877000245170716ULL // margin_fx_roll/tolerant: fold 14338402113188947682ULL -> 6141208888695716657ULL, continuation 1793934981347633737ULL -> 13394277230948639587ULL, broker 12065124535081589535ULL -> 15614434516016031356ULL +// K-IDX Option A re-pins the v19 coordinate and input-coordinate folds once. constexpr Pin kPins[] = { - {"batch/canonical", 8417350598053238959ULL, 3930831923810011709ULL, 11999400313963825447ULL, 6ULL}, - {"batch/tolerant", 14844934503495743671ULL, 13486072370782740075ULL, 714139398657209591ULL, 7ULL}, - {"margin/canonical", 17458675826522343092ULL, 2991704189571748528ULL, 13780954738578495924ULL, 0ULL}, - {"margin/tolerant", 15357271826299544132ULL, 7845006491016447433ULL, 14848973100437479489ULL, 4ULL}, - {"margin_calculation/canonical", 12225698990055261516ULL, 17852094020611711180ULL, 13636565062500767501ULL, 4ULL}, - {"margin_calculation/tolerant", 15618343851819169665ULL, 12657325099533710982ULL, 15810633237895378470ULL, 1ULL}, - {"margin_fx_roll/canonical", 14184613073071204061ULL, 1188032297556811944ULL, 5438877000245170716ULL, 0ULL}, - {"margin_fx_roll/tolerant", 6141208888695716657ULL, 13394277230948639587ULL, 15614434516016031356ULL, 5ULL}, - {"every_point/canonical", 7562764044102581192ULL, 3737699987008739230ULL, 4383124431781758246ULL, 9ULL}, - {"every_point/tolerant", 14141982756948460013ULL, 9846509637745922907ULL, 14678512838653533407ULL, 8ULL}, - {"synthesized/canonical", 2376681450298217164ULL, 8919327222620353451ULL, 3600925079421309162ULL, 9ULL}, - {"synthesized/tolerant", 4035245647427445567ULL, 2693662612566588702ULL, 119147027709136062ULL, 13ULL}, - {"synthesized_every_point/canonical", 13963831207202754247ULL, 1411759699604227935ULL, 8220237254062191568ULL, 10ULL}, - {"synthesized_every_point/tolerant", 4089704344096729236ULL, 12877338782012258332ULL, 445075360625743117ULL, 9ULL}, - {"volume_weighted/canonical", 17081627527962347612ULL, 10482737734476373322ULL, 8103680506999774488ULL, 6ULL}, - {"volume_weighted/tolerant", 6813541438777655137ULL, 4189454090113514211ULL, 11870207615699990715ULL, 7ULL}, - {"lower/canonical", 4641827469802785645ULL, 376923421924023966ULL, 2995238052981659152ULL, 6ULL}, - {"lower/tolerant", 8536129199662207228ULL, 9174537903413103601ULL, 5095969895919361099ULL, 8ULL}, - {"aggregated/canonical", 8850485432917411920ULL, 4961257679657254387ULL, 1799761924515711285ULL, 2ULL}, - {"aggregated/tolerant", 15875277156285188567ULL, 8914616736812346008ULL, 15133071495860523233ULL, 0ULL}, - {"subscribed/canonical", 390894263916183541ULL, 12462316195371213713ULL, 7973845562128009868ULL, 10ULL}, - {"subscribed/tolerant", 7361324576483339450ULL, 9264427645489656194ULL, 2831646342730737553ULL, 10ULL}, - {"anchored/canonical", 17197439533615939981ULL, 5136987709397563060ULL, 6929047812020472439ULL, 7ULL}, - {"anchored/tolerant", 8397173971337408442ULL, 4669775647537569183ULL, 14274470403008160849ULL, 9ULL}, - {"risk/canonical", 5095627687336300866ULL, 2785871838757152508ULL, 8320168402934656314ULL, 4ULL}, - {"risk/tolerant", 13227263656148245620ULL, 10822680993924448723ULL, 16354953049491256245ULL, 3ULL}, - {"ticks/canonical", 9487218993916862366ULL, 10310962926004479498ULL, 11647353557686017076ULL, 10ULL}, - {"ticks/tolerant", 2625227532442913688ULL, 4061674630377631542ULL, 3207582933107796684ULL, 5ULL}, - {"stream_bars/canonical", 7718834609732373965ULL, 5464448038614246844ULL, 18150893572169595191ULL, 8ULL}, - {"stream_bars/tolerant", 12737355743496944695ULL, 5118755846516500329ULL, 64347809724535028ULL, 11ULL}, + {"batch/canonical", 17117662984031888860ULL, 17780913817525129464ULL, 2068800667747861245ULL, 6ULL}, + {"batch/tolerant", 7159587779173285726ULL, 7106716373028264855ULL, 979090476763592675ULL, 7ULL}, + {"margin/canonical", 15249851553654389325ULL, 15704940793391485011ULL, 2547355677285145902ULL, 0ULL}, + {"margin/tolerant", 12102871564077229025ULL, 13946083138301348255ULL, 7326839878467814545ULL, 4ULL}, + {"margin_calculation/canonical", 1763085069446177047ULL, 15220880405934724875ULL, 4672034550176332852ULL, 4ULL}, + {"margin_calculation/tolerant", 7785445382470718173ULL, 10902805914158279185ULL, 7522617531502372512ULL, 1ULL}, + {"margin_fx_roll/canonical", 16450529000057841155ULL, 13234685573085315365ULL, 6826822976864983079ULL, 0ULL}, + {"margin_fx_roll/tolerant", 12588418184566633375ULL, 2680628296643020618ULL, 2266859119956126744ULL, 5ULL}, + {"every_point/canonical", 10723811937494878803ULL, 4491187202482505889ULL, 11471363269271551172ULL, 9ULL}, + {"every_point/tolerant", 14679941050025003028ULL, 6574169214787712326ULL, 8952049994176171904ULL, 8ULL}, + {"synthesized/canonical", 6703115050510603746ULL, 535930656237195088ULL, 13434174737889010896ULL, 9ULL}, + {"synthesized/tolerant", 7780316339256592953ULL, 16964107334250799118ULL, 14901055835935455905ULL, 13ULL}, + {"synthesized_every_point/canonical", 9594243673180640049ULL, 178344748142130161ULL, 13681213038111961167ULL, 10ULL}, + {"synthesized_every_point/tolerant", 156957421120367645ULL, 10081957441325511405ULL, 7599415587229105335ULL, 9ULL}, + {"volume_weighted/canonical", 3445637731839009940ULL, 10263238676578182104ULL, 10666024533260247959ULL, 6ULL}, + {"volume_weighted/tolerant", 16738158308980766384ULL, 15126045393606532712ULL, 3816479336606218160ULL, 7ULL}, + {"lower/canonical", 16457686788008518293ULL, 11357011057993872829ULL, 6569078669742286805ULL, 6ULL}, + {"lower/tolerant", 14962791123507705639ULL, 16635376154655679104ULL, 3158821142657662539ULL, 8ULL}, + {"aggregated/canonical", 6196042442387675404ULL, 9322310899630531166ULL, 522501888358458445ULL, 2ULL}, + {"aggregated/tolerant", 16129624093897181696ULL, 5050819594448951837ULL, 8156482062715705622ULL, 0ULL}, + {"subscribed/canonical", 9354174218907517759ULL, 10755432213466760236ULL, 5910993035115172010ULL, 10ULL}, + {"subscribed/tolerant", 1683434358667330146ULL, 17773566295771860004ULL, 1407911890106154906ULL, 10ULL}, + {"anchored/canonical", 5612726195706972414ULL, 12008230362801453845ULL, 16162400829133148863ULL, 7ULL}, + {"anchored/tolerant", 16074674844732067849ULL, 17818307101311643851ULL, 16691127111077318243ULL, 9ULL}, + {"risk/canonical", 6787970345605368602ULL, 12195288227410166402ULL, 8314083083443106458ULL, 4ULL}, + {"risk/tolerant", 10695848571822133801ULL, 3646571594064542412ULL, 17592057317824588303ULL, 3ULL}, + {"ticks/canonical", 14757823880729403954ULL, 4258680971556760714ULL, 13717708224844676838ULL, 10ULL}, + {"ticks/tolerant", 7992845295774848222ULL, 15797592512778199126ULL, 2657993891625051114ULL, 5ULL}, + {"stream_bars/canonical", 11953185860877643017ULL, 10711069245673339558ULL, 8891908041682500906ULL, 8ULL}, + {"stream_bars/tolerant", 11934789529351532745ULL, 13544793672722675478ULL, 9748293825336591548ULL, 11ULL}, }; struct Scenario { diff --git a/tests/test_native_market_vertical_contract.cpp b/tests/test_native_market_vertical_contract.cpp index 1f5d3b72..a76bd855 100644 --- a/tests/test_native_market_vertical_contract.cpp +++ b/tests/test_native_market_vertical_contract.cpp @@ -294,6 +294,7 @@ std::string terms_coordinate_json(const pineforge::NativeCoordinate& point) { std::ostringstream out; out << "{\"ordinal\":" << json_u64(point.ordinal) << ",\"intervalIndex\":" << point.interval_index + << ",\"inputIntervalIndex\":" << point.input_interval_index << ",\"openMs\":" << json_i64(point.open_ms) << ",\"eligibleOpenMs\":" << json_i64(point.eligible_open_ms) << ",\"lastTradedCloseMs\":" << json_i64(point.last_traded_close_ms) diff --git a/tests/test_native_point_checks.cpp b/tests/test_native_point_checks.cpp index 843b9e3b..5b5cf110 100644 --- a/tests/test_native_point_checks.cpp +++ b/tests/test_native_point_checks.cpp @@ -370,103 +370,105 @@ struct AbortPin { // ticks/quiet/on_native_applied#3: outcome 355827270336707373ULL -> 17168120311484485675ULL // ticks/quiet/on_native_tick#1: outcome 8520651042860323138ULL -> 17346655304003294423ULL // ticks/quiet/on_native_tick#3: outcome 10915210028839708168ULL -> 9104465125362721709ULL +// K-IDX (Option A) re-pins the v19 continuation witnesses once: the +// script-space coordinate and named input coordinate are now folded. constexpr AbortPin kAbortPins[] = { - {"batch/error/on_native_input#1", 6393539083915214569ULL}, - {"batch/error/on_native_input#3", 12506965903089361792ULL}, - {"batch/error/on_native_bar_open#1", 6393539083915214569ULL}, - {"batch/error/on_native_bar_open#3", 12506965903089361792ULL}, - {"batch/error/on_native_bar#1", 2421246306453684171ULL}, - {"batch/error/on_native_bar#3", 1057123432612222053ULL}, - {"batch/error/on_native_recalculate#1", 2421246306453684171ULL}, - {"batch/error/on_native_recalculate#3", 1057123432612222053ULL}, - {"batch/error/on_native_applied#1", 5339722451315730454ULL}, - {"batch/error/on_native_applied#3", 1462432089140135432ULL}, - {"batch/error/on_native_tick#1", 14678299568377943283ULL}, - {"batch/error/on_native_tick#3", 14678299568377943283ULL}, - {"batch/quiet/on_native_input#1", 17464479845175602237ULL}, - {"batch/quiet/on_native_input#3", 2814996419086419291ULL}, - {"batch/quiet/on_native_bar_open#1", 17464479845175602237ULL}, - {"batch/quiet/on_native_bar_open#3", 2814996419086419291ULL}, - {"batch/quiet/on_native_bar#1", 8097870827278573096ULL}, - {"batch/quiet/on_native_bar#3", 8283080006278880170ULL}, - {"batch/quiet/on_native_recalculate#1", 8097870827278573096ULL}, - {"batch/quiet/on_native_recalculate#3", 8283080006278880170ULL}, - {"batch/quiet/on_native_applied#1", 7077975767914576205ULL}, - {"batch/quiet/on_native_applied#3", 18015243225726523608ULL}, - {"batch/quiet/on_native_tick#1", 18134814941465191729ULL}, - {"batch/quiet/on_native_tick#3", 18134814941465191729ULL}, - {"fills/error/on_native_input#1", 17367621735129790840ULL}, - {"fills/error/on_native_input#3", 6438362476430832568ULL}, - {"fills/error/on_native_bar_open#1", 17367621735129790840ULL}, - {"fills/error/on_native_bar_open#3", 6438362476430832568ULL}, - {"fills/error/on_native_bar#1", 8524839639049974501ULL}, - {"fills/error/on_native_bar#3", 1218036926535466279ULL}, - {"fills/error/on_native_recalculate#1", 8524839639049974501ULL}, - {"fills/error/on_native_recalculate#3", 1218036926535466279ULL}, - {"fills/error/on_native_applied#1", 16321821413986659647ULL}, - {"fills/error/on_native_applied#3", 4158348421283583503ULL}, - {"fills/error/on_native_tick#1", 7005226954935592032ULL}, - {"fills/error/on_native_tick#3", 7005226954935592032ULL}, - {"fills/quiet/on_native_input#1", 12196372944623838614ULL}, - {"fills/quiet/on_native_input#3", 17546638886976472941ULL}, - {"fills/quiet/on_native_bar_open#1", 12196372944623838614ULL}, - {"fills/quiet/on_native_bar_open#3", 17546638886976472941ULL}, - {"fills/quiet/on_native_bar#1", 14651796593170546901ULL}, - {"fills/quiet/on_native_bar#3", 7653501279252472751ULL}, - {"fills/quiet/on_native_recalculate#1", 14651796593170546901ULL}, - {"fills/quiet/on_native_recalculate#3", 7653501279252472751ULL}, - {"fills/quiet/on_native_applied#1", 11885385292447102941ULL}, - {"fills/quiet/on_native_applied#3", 2766548411790701755ULL}, - {"fills/quiet/on_native_tick#1", 9309647628106394874ULL}, - {"fills/quiet/on_native_tick#3", 9309647628106394874ULL}, - {"stream/error/on_native_input#1", 6393539083915214569ULL}, - {"stream/error/on_native_input#3", 12506965903089361792ULL}, - {"stream/error/on_native_bar_open#1", 6393539083915214569ULL}, - {"stream/error/on_native_bar_open#3", 12506965903089361792ULL}, - {"stream/error/on_native_bar#1", 2421246306453684171ULL}, - {"stream/error/on_native_bar#3", 1057123432612222053ULL}, - {"stream/error/on_native_recalculate#1", 2421246306453684171ULL}, - {"stream/error/on_native_recalculate#3", 1057123432612222053ULL}, - {"stream/error/on_native_applied#1", 5339722451315730454ULL}, - {"stream/error/on_native_applied#3", 1519443849186252878ULL}, - {"stream/error/on_native_tick#1", 3038203095228297971ULL}, - {"stream/error/on_native_tick#3", 3038203095228297971ULL}, - {"stream/quiet/on_native_input#1", 17464479845175602237ULL}, - {"stream/quiet/on_native_input#3", 2814996419086419291ULL}, - {"stream/quiet/on_native_bar_open#1", 17464479845175602237ULL}, - {"stream/quiet/on_native_bar_open#3", 2814996419086419291ULL}, - {"stream/quiet/on_native_bar#1", 8097870827278573096ULL}, - {"stream/quiet/on_native_bar#3", 8283080006278880170ULL}, - {"stream/quiet/on_native_recalculate#1", 8097870827278573096ULL}, - {"stream/quiet/on_native_recalculate#3", 8283080006278880170ULL}, - {"stream/quiet/on_native_applied#1", 7077975767914576205ULL}, - {"stream/quiet/on_native_applied#3", 7687428441803015905ULL}, - {"stream/quiet/on_native_tick#1", 17056314215193477229ULL}, - {"stream/quiet/on_native_tick#3", 17056314215193477229ULL}, - {"ticks/error/on_native_input#1", 6393539083915214569ULL}, - {"ticks/error/on_native_input#3", 12695806500583197548ULL}, - {"ticks/error/on_native_bar_open#1", 6393539083915214569ULL}, - {"ticks/error/on_native_bar_open#3", 12695806500583197548ULL}, - {"ticks/error/on_native_bar#1", 2421246306453684171ULL}, - {"ticks/error/on_native_bar#3", 17972753170949413387ULL}, - {"ticks/error/on_native_recalculate#1", 2421246306453684171ULL}, - {"ticks/error/on_native_recalculate#3", 17972753170949413387ULL}, - {"ticks/error/on_native_applied#1", 5339722451315730454ULL}, - {"ticks/error/on_native_applied#3", 2877234677515189003ULL}, - {"ticks/error/on_native_tick#1", 755879435947172223ULL}, - {"ticks/error/on_native_tick#3", 3586158540922747583ULL}, - {"ticks/quiet/on_native_input#1", 17464479845175602237ULL}, - {"ticks/quiet/on_native_input#3", 16192858612202111790ULL}, - {"ticks/quiet/on_native_bar_open#1", 17464479845175602237ULL}, - {"ticks/quiet/on_native_bar_open#3", 16192858612202111790ULL}, - {"ticks/quiet/on_native_bar#1", 8097870827278573096ULL}, - {"ticks/quiet/on_native_bar#3", 2497908218272532287ULL}, - {"ticks/quiet/on_native_recalculate#1", 8097870827278573096ULL}, - {"ticks/quiet/on_native_recalculate#3", 2497908218272532287ULL}, - {"ticks/quiet/on_native_applied#1", 7077975767914576205ULL}, - {"ticks/quiet/on_native_applied#3", 17168120311484485675ULL}, - {"ticks/quiet/on_native_tick#1", 17346655304003294423ULL}, - {"ticks/quiet/on_native_tick#3", 9104465125362721709ULL}, + {"batch/error/on_native_input#1", 3560323808188698715ULL}, + {"batch/error/on_native_input#3", 3242237994065292507ULL}, + {"batch/error/on_native_bar_open#1", 3560323808188698715ULL}, + {"batch/error/on_native_bar_open#3", 3242237994065292507ULL}, + {"batch/error/on_native_bar#1", 18274505253103044355ULL}, + {"batch/error/on_native_bar#3", 7128076562107178007ULL}, + {"batch/error/on_native_recalculate#1", 18274505253103044355ULL}, + {"batch/error/on_native_recalculate#3", 7128076562107178007ULL}, + {"batch/error/on_native_applied#1", 5962112538319194182ULL}, + {"batch/error/on_native_applied#3", 16761785072693880059ULL}, + {"batch/error/on_native_tick#1", 1064083451052185069ULL}, + {"batch/error/on_native_tick#3", 1064083451052185069ULL}, + {"batch/quiet/on_native_input#1", 15855411169808214460ULL}, + {"batch/quiet/on_native_input#3", 18330722405539334964ULL}, + {"batch/quiet/on_native_bar_open#1", 15855411169808214460ULL}, + {"batch/quiet/on_native_bar_open#3", 18330722405539334964ULL}, + {"batch/quiet/on_native_bar#1", 15244034026730534949ULL}, + {"batch/quiet/on_native_bar#3", 15522275516133939991ULL}, + {"batch/quiet/on_native_recalculate#1", 15244034026730534949ULL}, + {"batch/quiet/on_native_recalculate#3", 15522275516133939991ULL}, + {"batch/quiet/on_native_applied#1", 6325881143001360144ULL}, + {"batch/quiet/on_native_applied#3", 927968700415268790ULL}, + {"batch/quiet/on_native_tick#1", 12893583462929514494ULL}, + {"batch/quiet/on_native_tick#3", 12893583462929514494ULL}, + {"fills/error/on_native_input#1", 9374070082406354326ULL}, + {"fills/error/on_native_input#3", 16134613282448265837ULL}, + {"fills/error/on_native_bar_open#1", 9374070082406354326ULL}, + {"fills/error/on_native_bar_open#3", 16134613282448265837ULL}, + {"fills/error/on_native_bar#1", 9660291061496025695ULL}, + {"fills/error/on_native_bar#3", 3200898139300934636ULL}, + {"fills/error/on_native_recalculate#1", 9660291061496025695ULL}, + {"fills/error/on_native_recalculate#3", 3200898139300934636ULL}, + {"fills/error/on_native_applied#1", 1873181469488193620ULL}, + {"fills/error/on_native_applied#3", 18364593085729179025ULL}, + {"fills/error/on_native_tick#1", 11599053408032152495ULL}, + {"fills/error/on_native_tick#3", 11599053408032152495ULL}, + {"fills/quiet/on_native_input#1", 512806339783699407ULL}, + {"fills/quiet/on_native_input#3", 14585734011108505323ULL}, + {"fills/quiet/on_native_bar_open#1", 512806339783699407ULL}, + {"fills/quiet/on_native_bar_open#3", 14585734011108505323ULL}, + {"fills/quiet/on_native_bar#1", 15746413390901049547ULL}, + {"fills/quiet/on_native_bar#3", 10179067546779602528ULL}, + {"fills/quiet/on_native_recalculate#1", 15746413390901049547ULL}, + {"fills/quiet/on_native_recalculate#3", 10179067546779602528ULL}, + {"fills/quiet/on_native_applied#1", 9247821118085001503ULL}, + {"fills/quiet/on_native_applied#3", 2125704302211299148ULL}, + {"fills/quiet/on_native_tick#1", 3967827899961809128ULL}, + {"fills/quiet/on_native_tick#3", 3967827899961809128ULL}, + {"stream/error/on_native_input#1", 3560323808188698715ULL}, + {"stream/error/on_native_input#3", 3242237994065292507ULL}, + {"stream/error/on_native_bar_open#1", 3560323808188698715ULL}, + {"stream/error/on_native_bar_open#3", 3242237994065292507ULL}, + {"stream/error/on_native_bar#1", 18274505253103044355ULL}, + {"stream/error/on_native_bar#3", 7128076562107178007ULL}, + {"stream/error/on_native_recalculate#1", 18274505253103044355ULL}, + {"stream/error/on_native_recalculate#3", 7128076562107178007ULL}, + {"stream/error/on_native_applied#1", 5962112538319194182ULL}, + {"stream/error/on_native_applied#3", 18140500761115835465ULL}, + {"stream/error/on_native_tick#1", 5948355266559318192ULL}, + {"stream/error/on_native_tick#3", 5948355266559318192ULL}, + {"stream/quiet/on_native_input#1", 15855411169808214460ULL}, + {"stream/quiet/on_native_input#3", 18330722405539334964ULL}, + {"stream/quiet/on_native_bar_open#1", 15855411169808214460ULL}, + {"stream/quiet/on_native_bar_open#3", 18330722405539334964ULL}, + {"stream/quiet/on_native_bar#1", 15244034026730534949ULL}, + {"stream/quiet/on_native_bar#3", 15522275516133939991ULL}, + {"stream/quiet/on_native_recalculate#1", 15244034026730534949ULL}, + {"stream/quiet/on_native_recalculate#3", 15522275516133939991ULL}, + {"stream/quiet/on_native_applied#1", 6325881143001360144ULL}, + {"stream/quiet/on_native_applied#3", 12530950112278122443ULL}, + {"stream/quiet/on_native_tick#1", 9176002025615057620ULL}, + {"stream/quiet/on_native_tick#3", 9176002025615057620ULL}, + {"ticks/error/on_native_input#1", 3560323808188698715ULL}, + {"ticks/error/on_native_input#3", 10706023567362351269ULL}, + {"ticks/error/on_native_bar_open#1", 3560323808188698715ULL}, + {"ticks/error/on_native_bar_open#3", 10706023567362351269ULL}, + {"ticks/error/on_native_bar#1", 18274505253103044355ULL}, + {"ticks/error/on_native_bar#3", 2851122174747762788ULL}, + {"ticks/error/on_native_recalculate#1", 18274505253103044355ULL}, + {"ticks/error/on_native_recalculate#3", 2851122174747762788ULL}, + {"ticks/error/on_native_applied#1", 5962112538319194182ULL}, + {"ticks/error/on_native_applied#3", 14982033343797752634ULL}, + {"ticks/error/on_native_tick#1", 6633416296699794458ULL}, + {"ticks/error/on_native_tick#3", 8401220712745923109ULL}, + {"ticks/quiet/on_native_input#1", 15855411169808214460ULL}, + {"ticks/quiet/on_native_input#3", 14133373906829835866ULL}, + {"ticks/quiet/on_native_bar_open#1", 15855411169808214460ULL}, + {"ticks/quiet/on_native_bar_open#3", 14133373906829835866ULL}, + {"ticks/quiet/on_native_bar#1", 15244034026730534949ULL}, + {"ticks/quiet/on_native_bar#3", 16656575874195564660ULL}, + {"ticks/quiet/on_native_recalculate#1", 15244034026730534949ULL}, + {"ticks/quiet/on_native_recalculate#3", 16656575874195564660ULL}, + {"ticks/quiet/on_native_applied#1", 6325881143001360144ULL}, + {"ticks/quiet/on_native_applied#3", 17359368186260815726ULL}, + {"ticks/quiet/on_native_tick#1", 17287525999126084928ULL}, + {"ticks/quiet/on_native_tick#3", 9804941054249243457ULL}, }; void aborts_are_caught_where_they_were() { @@ -565,6 +567,7 @@ void sizing_price_is_the_array_walk() { // Field by field: the coordinate has padding, which a byte compare would read. bool same_coordinate(const NativeCoordinate& a, const NativeCoordinate& b) { return a.ordinal == b.ordinal && a.interval_index == b.interval_index + && a.input_interval_index == b.input_interval_index && a.open_ms == b.open_ms && a.eligible_open_ms == b.eligible_open_ms && a.last_traded_close_ms == b.last_traded_close_ms && a.next_period_open_ms == b.next_period_open_ms @@ -615,6 +618,7 @@ void frames_are_the_aggregates() { decision.coordinate.source_price_time_ms = static_cast(rng()); decision.coordinate.ordinal = rng(); decision.coordinate.interval_index = static_cast(rng() % 100000); + decision.coordinate.input_interval_index = static_cast(rng() % 100000); decision.coordinate.provenance = static_cast(rng() % 8); decision.coordinate.path_phase = static_cast(rng() % 5); decision.decision_floor_ms = static_cast(rng()); diff --git a/tests/test_native_resting_replay_contract.cpp b/tests/test_native_resting_replay_contract.cpp index fbadf718..8df6cc39 100644 --- a/tests/test_native_resting_replay_contract.cpp +++ b/tests/test_native_resting_replay_contract.cpp @@ -150,6 +150,7 @@ bool equal_cursor_bits(const no::MatchCursor& a, const no::MatchCursor& b) { const auto& x = a.point; const auto& y = b.point; return bits_eq(a.t, b.t) && x.ordinal == y.ordinal && x.interval_index == y.interval_index + && x.input_interval_index == y.input_interval_index && x.open_ms == y.open_ms && x.eligible_open_ms == y.eligible_open_ms && x.last_traded_close_ms == y.last_traded_close_ms && x.next_period_open_ms == y.next_period_open_ms @@ -304,6 +305,7 @@ void same_event_id(const no::EventId& a, const no::EventId& b) { void same_coord(const NativeCoordinate& a, const NativeCoordinate& b) { same_u64(a.ordinal, b.ordinal); CHECK(a.interval_index == b.interval_index); + CHECK(a.input_interval_index == b.input_interval_index); same_i64(a.open_ms, b.open_ms); same_i64(a.eligible_open_ms, b.eligible_open_ms); same_i64(a.last_traded_close_ms, b.last_traded_close_ms); From 3ed0276fa643609c2fefbd3aff4c960b7edcfb43 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 00:18:59 +0800 Subject: [PATCH 15/24] K-IDX: preserve Pine input projections and re-pin v19 witnesses ADR-0001 rule 2, Option A: NativeCoordinate::interval_index, lots, trade rows and bar metrics stay in script-bar space. NativeCoordinate::input_interval_index remains the named input coordinate for hosts that need input cadence. Pine source projection state reads that explicit input coordinate; the kernel report and lot coordinates are never re-stamped. Behavior fix: test_adapter_brackets_relower keeps Pine's pending-order created_bar and scheduler source chart policy on the input coordinate under aggregation and magnifier. The adapter no longer uses the kernel script coordinate for those source-only comparisons. One v19 hash/witness re-pin, caused only by the coordinate fold; trade/event counts, driver points and behavior remain unchanged: test_native_continuation_view; test_adapter_continuation_view; test_adapter_host_view_memo; test_native_match_hash_witness; test_native_intrabar_lower_lookup; test_native_batch_log_presize; test_native_event_retention; test_native_match_band_witness. No PF_API export or frozen C layout changed. (cherry picked from commit 6df7850af41b7f63f49a28915d3c53f7356fa234) INT25 merge: test_adapter_continuation_view and test_adapter_host_view_memo_data keep V19-FIX's pins in their conflict blocks (this lane's header notes kept); both lanes moved the same values, so the INT25 v19 hash re-pin harvests them once over both. The pages merge on digit-normalised text and are re-anchored once for the lane: 64 citations carried through the exact line map from 07fc9183 or 6df7850a; the five citations on the word-merged pine-to-native decision row checked by hand. --- docs/adr/0001-kernel-adapter-boundary.md | 56 ++--- docs/design/native-feature-parity.md | 220 ++++++++++---------- docs/pages/native-engine.md | 8 +- docs/pages/pine-to-native.md | 6 +- include/pineforge/source/pine_adapter.hpp | 5 + src/source/pine_adapter.cpp | 67 +++--- src/source/pine_scheduler_native.cpp | 2 +- tests/test_adapter_continuation_view.cpp | 1 + tests/test_adapter_host_view_memo_data.hpp | 1 + tests/test_native_batch_log_presize.cpp | 26 +-- tests/test_native_continuation_view.cpp | 147 ++++++------- tests/test_native_event_retention.cpp | 7 +- tests/test_native_intrabar_lower_lookup.cpp | 7 +- tests/test_native_match_band_witness.cpp | 13 +- tests/test_native_match_hash_witness.cpp | 11 +- 15 files changed, 305 insertions(+), 272 deletions(-) diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index 4a3e94db..49cba961 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -89,9 +89,11 @@ 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:1030-1044`) in the v19 continuation. The Pine - adapter keeps its chart-source sampling index as a source-layer fact and does - not rewrite the kernel's lot or row indices (`pine_strategy_host.cpp:478`). + (`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 + does not rewrite the kernel's lot or row indices. - **Requests.** `submit` / `replace` / `cancel` / `submit_market` / `replace_market` (`native_host.hpp:1207-1212`), plus `cancel_all` (`native_host.hpp:1247`) and `cancel_where` (`native_host.hpp:1252`). A request carries one of five triggers — Market, Limit (with a @@ -207,35 +209,35 @@ 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:13017`) returns `AdmitWithHostMargin` - (`pine_adapter.cpp:13262`) to own the opening margin decision; `owns_lot_excursions() = true` + `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` (`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:2301-2302`); + `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:1788-1819); the retained parent-before-child ordering of live handles — an + (`sha256:27e64b2cd0bbe283a8c83056538d66cd374e53cad072772c775a8529624768ac` pine_adapter.hpp:1793-1824); 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:1080-1159`). Kernel priority is acceptance/incarnation order. + `pine_adapter.cpp:1097-1176`). 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 (`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:9574-9579`); the half-tick + 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 - adapter's terms seam (`resolve_terms` `pine_adapter.cpp:11654`). + 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`). - **Close reservations.** `strategy.close` callsite batching, two-call provenance and the entry-id - ledger (`close_logical_units_` `pine_adapter.hpp:2018-2029`); the POOC reservation-growth population predicate + 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). - **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:14052`) for the scheduling, - `resolve_margin_requirement` (`pine_adapter.cpp:14088`) 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 @@ -246,7 +248,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:16787`), the pre-open admission slice and + (`non_pooc_commissioned_short` `pine_adapter.cpp:16804`), the pre-open admission slice 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`). @@ -254,9 +256,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:8265`), the first-open execution chain and its + (`next_source_path_waypoint` `pine_adapter.cpp:8282`), 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:5624`). + 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_` @@ -304,7 +306,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:2018-2029`) and + 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`); what enables the generic model is the presence of `NativeRunSpec::margin`. So is the string-sentinel decoding of adapter-written comments: @@ -560,12 +562,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:13850`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:14905`), `chart_day_key` (`pine_adapter.cpp:13747`), `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:13209-13210`): 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:2343-2345`) | 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:2370-2378`, `margin.maintenance_long` `pine_adapter.cpp:2385`) | 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` | -| `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:2317-2324) | 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:2308-2310`) | design rows CT4 and E5: the open-only view is opt-in because the adapter needs the full bar | `examples/native/native_calc_on_fills_strategy.cpp`, `tests/test_native_calc_timing.cpp` | +| `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp: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` | +| `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` | | `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` | @@ -612,7 +614,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:582`) | **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:1076-1081`) 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: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:1867`) | **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) @@ -684,9 +686,9 @@ 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:7255`, presenting `NativeDecisionContext::in_session` `include/pineforge/market_driver.hpp:155` 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:7864-7880` | 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:7841-7862` | 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) | +| `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) | The rows above account for every site the two audits named, and none of them is justified by diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 31c6f3d9..6634a277 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -47,115 +47,115 @@ 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:2720-2731; `entry()` pine_adapter.cpp:5272, `order()` pine_adapter.cpp:10463) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5672-5719; `submit` native_host.hpp:1193 | 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:8326 → `Flatten{}` pine_adapter.cpp:8355-8356; `Reduce{ExplicitUnits}` pine_adapter.cpp:7473-7482; `ReverseTo` pine_adapter.cpp:6307-6308) | **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 | — | -| OL3 | Amend / cancel one working order (`submit_or_replace` pine_adapter.cpp:2690-3897 → `host.replace` pine_adapter.cpp:2706-2786) | **yes** — `replace` native_host.hpp:1198-1200, `cancel` native_host.hpp:1229; `ReplacedEvent` native_order.hpp:1013-1027; result and event types `SubmitStatus` native_order.hpp:835-861, `ReplacedEvent` native_order.hpp:997-1063 | 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:4705-4751, `cancel_all` pine_adapter.cpp:18015-18030; `live_by_source_key_` pine_adapter.hpp:1990; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4047-4112) | **partial** — single-handle `cancel` native_host.hpp:1229; no enumeration (`WorkingRequestCore::live()` is consumer-internal native_order.hpp:1673); labels exist but are unindexed (`Request::label` native_order.hpp:485) **Closed:** `cancel_all` native_host.hpp:1247, `cancel_where(comment)` native_host.hpp:1252 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1252 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1243 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:4172; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:7158-7509) | **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 | — | -| 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:5496-5531, `cohort_for` pine_adapter.cpp:2484; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7456-7473) | **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:1262, `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:2729-2735) | **yes** — `Group` / `GroupEffect` native_order.hpp:466-473, `CancelReason::Group` native_order.hpp:948-953, `ReservationReducedEvent` native_order.hpp:1172-1182 (deferred form native_order.hpp:1172-1182) | K | — | F:A12 O:A6 O:A7 S:O7 | — | +| 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:1193 | 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 | — | +| 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:1198-1200, `cancel` native_host.hpp:1229; `ReplacedEvent` native_order.hpp:1013-1027; result and event types `SubmitStatus` native_order.hpp:835-861, `ReplacedEvent` native_order.hpp:997-1063 | 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:1229; no enumeration (`WorkingRequestCore::live()` is consumer-internal native_order.hpp:1673); labels exist but are unindexed (`Request::label` native_order.hpp:485) **Closed:** `cancel_all` native_host.hpp:1247, `cancel_where(comment)` native_host.hpp:1252 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1252 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1243 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 | — | +| 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:1262, `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:1172-1182 (deferred form native_order.hpp:1172-1182) | 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:1084-1085 | 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:12987; lowering `submit_one` pine_adapter.cpp:9023-9035, `BindCohort` pine_adapter.cpp:9524-9530, `submit_leg` pine_adapter.cpp:9548-9572; 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:1852); 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:8628-8631; `materialize_relative_exits` pine_adapter.cpp:17305-17346, pine_adapter.hpp:1815-1827) | **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:5598-5632. `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:999 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:19066-19095) | **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:1243 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:1992-2010); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1125-1204); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3451-3618, `materialize_snapshot` pine_adapter.cpp:10863-10887), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1579-1602, `CloseCallsiteState` pine_adapter.hpp:1673-1688; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15614, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15848) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:660-662 | 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:1019-1026; `select_exit_activation` exit_activation.cpp:18-88, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:864-896) | 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:1827); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:1901). 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:1852); 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:999 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:1243 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:660-662 | 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 | ### 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:5684-5706; `NativeTrailState` native_host.hpp:657-662, `trail_state` native_host.hpp:1072 | 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:5697-5719; `NativeTrailState` native_host.hpp:657-662, `trail_state` native_host.hpp:1072 | 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:11762-11774) | **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:11675-11812) stays A. | -| TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:4004-4050) | **partial** — the read exists `trail_state` native_host.hpp:1072; `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:9574-9579, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11675-11812, retained best `retained_trail_best` pine_adapter.hpp:287-289, operand / restart rules `source_trail_points` pine_adapter.cpp:8598-8824, `trail_one_shot` pine_adapter.cpp:9663-9792 | 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: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. | +| 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:1072; `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 | ### 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:5885-5889, pine_adapter.cpp:5885) | **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:10898-12090) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:932-936, invoked native_execution_consumer.cpp:4909-4927, guarded native_execution_consumer.cpp:4909-4943 | 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:2578-2633) | **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:2530, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:462 (→ SZ10) | -| SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:2539-2541) | **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:2591, `typed_entry_units` pine_adapter.cpp:2609; `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:2570; `percent_commission_live_equity` pine_adapter.cpp:4311-4351) | **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:2496-2511, `PineSizingSnapshot` pine_adapter.hpp:147-154; fill-time exceptions `default_sized` pine_adapter.cpp:6628-6654) | **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:1391-1408; host-sized terms snap (`ExecutionGridPolicy` native_order.hpp:71-85); exact grid validation `execution_terms_grid_representable` native_execution_consumer.cpp:4966-4969; 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:12461-12489, `compute_exit_reservation` pine_adapter.cpp:4394-4515) | **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:4446-4769, `reserved_exit_qty` pine_adapter.cpp:8950-9260) | **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:4426-4441) | 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:6689), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7896), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12718), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13314, pine_adapter.cpp:13367) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15662) | 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:2578), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2591, `typed_entry_units` pine_adapter.cpp:2609), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18661) | 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:7158-7509; ledger `close_batch_bar_` pine_adapter.hpp:2033-2044; 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:1083-1195) | 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: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:932-936, 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. | +| 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. | +| 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:1391-1408; 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. | +| 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 | +| 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. | ### 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:2344) | **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:13209-13212) 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:6163-6209, cap left unset `max_open_lots` pine_adapter.cpp:2343-2345) is TV. | -| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13263-13328 and leaves the kernel gate unset pine_adapter.cpp:13262-13264) | **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. | -| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13918-13919, `margin_pct` pine_adapter.cpp:13326) | **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:14018-14196, `submit_margin_call_units` pine_adapter.cpp:14157-14203, `schedule_margin_call_path` pine_adapter.cpp:18168-18245) | **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:13949-13985) | **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:982 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:179-200) | **no** **Closed:** `native_liquidation_price()` native_host.hpp:1300 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:16266-16282, `apply_fx_opening_margin_slice` pine_adapter.cpp:18246-18274) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1182 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:16757-16796) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1281 **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:13298-13301) | **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:13879-13893, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14905-14935) | **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:14948-15182) | **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:13689-13768) | **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:1303 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:13907, 4× shortfall `4.0 * minimum` pine_adapter.cpp:13966, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14674-14714, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:14198-14325, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:14198) | n/a | A | — | F:D2 F:G4 O:D4 S:M4 | R5-1 | -| 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:3800-3867, `record_market_review` pine_adapter.cpp:15186-15244) | n/a | A | — | S:R2 | (single-source) | +| 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. | +| 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:982 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:1300 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:1182 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:1281 **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:1303 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 | +| 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:881, dispatched native_execution_consumer.cpp:7204-7225 (call site `sha256:fc29ccd415732193f24f2370c6aca8d6f8910bca5b95315e63e88f2befe8200b` native_execution_consumer.cpp:7215) | K | — | F:E1 O:F1 | — | -| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2301-2302) | **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:16101) | **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:1084 | 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: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:1084 | 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:1053 is the lookahead-free bar so far at the cursor, 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:5593, `flush_coof_tail` pine_adapter.cpp:16244, `end_coof_recalc` pine_adapter.cpp:5614) | **partial** — `on_native_applied` native_host.hpp:925 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:1397-1405; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5598-5632, `remaining_path_delivery` native_order.cpp:2038-2046. 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:284-286. | -| 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:7505-7622 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:915 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:2431-2487) | **yes** — `IntrabarPath` native_run_spec.hpp:362-402, delivery native_execution_consumer.cpp:7449-7640; `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:233-319) 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:5740), 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:1654-1661 | 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: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:925 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:2038-2046. 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:915 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. | +| 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 | ### 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:1420-1576; 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:1116 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:873 and `native_series_bar(index)` native_host.hpp:1099 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:1428-1444 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:1446-1451 **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). | +| 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:1116 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:873 and `native_series_bar(index)` native_host.hpp:1099 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). | | 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:915 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:7864-7881, 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: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 | ### 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:5672-5719, `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:11929-11938) 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 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 | | 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:11945-11954, `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: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:1182 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:9015-9020, `exit_limit_trigger` pine_adapter.cpp:9549-9570), `source_bar_fill_tick` pine_adapter.cpp:286-299, raw-vs-booked spelling pine_adapter.cpp:11428-11534, margin-call fill pricing pine_adapter.cpp:13994-14003 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:11654) | 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 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 | ### 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:2600-2613, closing-row fold `max_runup` engine_orders.cpp:157-171; `owns_lot_excursions` native_host.hpp:1008-1012 | 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:1008-1012 | 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). | | 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) | @@ -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:2237-2238, `chart_timezone` pine_adapter.cpp:2278-2280) | **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: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:2157, `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:546 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 57 `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 symbols are a second, disjoint inventory beside the 57 codegen-facing ones (`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:287-289) | 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 | +| 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 | --- @@ -193,7 +193,7 @@ 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:2647, `sha256:ddfd09da8827c35417d2ff71e63c7fb94e625439fb0ef2b42189833ea0dc3892` pine_adapter.cpp:14332-14334, `sha256:560e106c433fafa69344365c8f83e8e9b5a33ffcf4544447fbddd2cd47b4a60c` pine_adapter.cpp:14552-14553, `sha256:05c2a313dfe73ccea95254a9a4ea41f08ec5b675eded99c5cdcf40887d90e176` pine_adapter.cpp:15111-15112) 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 (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 | | 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 | | Size | M | M | L | M (R5-11) | @@ -202,15 +202,15 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | # | Item | Where / what it is used for | Class | Plan | Src | |---|---|---|---|---|---| -| a | `source::StrategyOverrides` opaque pointer | Forward declarations in the kernel headers (since removed: no kernel header names the adapter type now); parameter of `run_rich` (execution_consumer.hpp:46-55, native_execution_consumer.hpp: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:291-294 | 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: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:900-902 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1833-1836; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1869, `:8767`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2291-2292. 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: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) | | 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:19066-19095 | 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: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 | | 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:1271 | 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) | @@ -263,7 +263,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 `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 | -| Legality | The existing rule (`commands_allowed` native_execution_consumer.cpp:1397-1404): inside a callback, or between realtime inputs | F | +| 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 | @@ -276,14 +276,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-670, `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 | +| 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 | | 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 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:1333-1417 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. +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. 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). @@ -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:2924-2991) 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:2926-2993) 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). @@ -314,12 +314,12 @@ 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:3027-3029, `sha256:4c401fc6bd6ae89db330fea9f609b75539ff3db1f8060b0fdb0541cbbaf478ee` native_run_spec.hpp:19-177, `sha256:f4041a4df37f679d9055e856e1cc247a9ce5e1e80f01a49f9cdd822631f352c6` native_run_spec.hpp:174-175 | O S | +| 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 | | 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 | -| 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:2018-2029; `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: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:4fa627f4c74950d70948186329259be7ebb753c8c50d91c16b838ba01c540a6a` c_abi.cpp:289-929, `sha256:4b7e398c78b276b34c4247e8271242b48aa195cea29126f4e809073f2665e1ea` check_c_abi_runtime.py:19-79, c_abi.cpp:848, `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:902 | 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 | | 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 | @@ -336,7 +336,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:9133-9173 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:9150-9190 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:1053; and `set_native_security_feed` documented as the pre-run door for a series' authoritative bars, refused in-run | --- @@ -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:2187-2320) 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:1638-1642). 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:e3aa487ce8d69db9054576f75839345c0acaab55c3fc0a374c2f9c8c071559ec` AGENTS.md:132-145). A lane that cannot be shown neutral by construction does not merge. +- **(a) NEUTRALITY.** Every new behaviour is opt-in by a spec field or a new request kind. The adapter's `project()` (pine_adapter.cpp: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:e3aa487ce8d69db9054576f75839345c0acaab55c3fc0a374c2f9c8c071559ec` AGENTS.md:132-145). A lane that cannot be shown neutral by construction does not merge. - **(b) TWIN.** One chosen probe per lane runs through the adapter AND a hand-written `NativeStrategyHost` on the same bars and spec. Trade lists are diffed **by identity** (entry / exit time, price, qty, pnl, commission), never by trade number; then event, ownership and hash detail (S's D → E order). A difference is allowed only where a named TV-quirk row of §1 is active, and each one is itemized. Because 430 test files 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:1278). 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. @@ -403,11 +403,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:2340, 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: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) | -| 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:1333-1417 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 (`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) | @@ -494,7 +494,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:981-1003) — after `prepare_native_begin` has already +(pine_strategy_host.cpp:952-974) — 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 @@ -516,12 +516,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:2344); 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:13209-13210; 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:16757, `update_risk_state` pine_adapter.cpp:13850-13877): 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:13202), 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: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_intraday_loss` | day-open equity at the day's first bar (`intraday_start_equity` pine_adapter.cpp:16260); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16372), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:14935-14944, `submit_intraday_loss_close` pine_adapter.cpp:14905-14933), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17114); 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:5944, `:7603`, `:8327`, `:8455`, `:11432`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:18013-18016) | `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:13841-13843); 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:13747-13768, `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: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, @@ -915,7 +915,7 @@ row C). row, every placement snapshot the run had accepted (a sorted walk of every key, since replaced by the walk of the retained rows, `placement_` pine_state_hash.cpp:287) — one per accepted request, cleared only by -`reset_for_run` pine_adapter.cpp:2092 — and reflected the whole admission +`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 re-folded the consumed source prefix too (`consumed` @@ -962,10 +962,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:8739). Full-table scans - rewrite rows as well (`suspend_brackets_for_reversal` pine_adapter.cpp:1679, - `revive_brackets_after_margin` pine_adapter.cpp:1901, - `preserved_by_close_all` pine_adapter.cpp:11463), and + (`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 `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. @@ -1050,16 +1050,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:1827), the revival skips a withdrawn +(`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:2913). 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:3344); 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 @@ -1117,7 +1117,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:488, `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` :826, `exit_comment` :617, `close_cause` :631-636, `exit_from_bracket` :1247 and `entry_incarnation` :1428-1436. 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 @@ -1148,9 +1148,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:175-183). `capture_script_continuation_hash` -pine_strategy_host.cpp:358-366 takes that snapshot at the last batch bar -(`last_batch` pine_strategy_host.cpp:462-472), and taking it is one full +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 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 @@ -1206,10 +1206,10 @@ 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:13124-13191, pine_adapter.cpp:13238); its chronology lives at `sha256:bb89768c9a7b2dc9f6de5943c04de8103ed107c21bb6e184446de0e6a84cf4c2` pine_adapter.cpp:13797-13874 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:2338-2378) | 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:5740) | 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 | -| E5 | **Open-bar lookahead.** The adapter reads the full bar in `on_bar_open` (margin path scheduling uses H/L, pine_adapter.cpp:16101-16130) | 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`; `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 | +| 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 | @@ -1229,7 +1229,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 (`sha256:de7e074b6a82f71018015f842ca51f4cba526d5b258a4c9b4d6c914ee12c0541` `sha256:b263f6923ef8095700dc254259434739b63a9fb043773b8139c77ef5e1df91b9` native-engine.md:423-437) | 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:17552 (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 `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 | ### 4.3 Open design questions (supervisor level) @@ -1254,7 +1254,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:16266-16282) 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:2628) 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. | @@ -1277,10 +1277,10 @@ 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: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). -- `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:2340), 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:1010). 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:5047-5089), `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:5060-5102), `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) @@ -1330,9 +1330,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:13120-13141). New hashed `RequestDefinition::origin { Host, KernelLiquidation, KernelRisk }`, new `CancelReason::Superseded` on re-price, `MarginCallEvent` joins `CommandEvent` (17 → 18, native_order.hpp:1278). 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:1278). 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). - 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:4649-4662 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: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). ### A.4 L5 — calculation timing (F P4 + O G8, G9 + S P6) @@ -1353,8 +1353,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:1616-1617) → 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:1654-1661) 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:1071-1078, 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 (`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. 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). @@ -1421,7 +1421,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:3936-3958, `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 engine.hpp:1255, engine.hpp:1265, `sha256:e0b3be5fe845e3a132c5b49967946649a4c2b32a67f9f240c44bd553a6a99b69` engine.hpp:1304 are the reference arithmetic (§2.ii e). ### A.8 L9 — risk limits (F P3 + O G7 + S P5) @@ -1440,8 +1440,8 @@ std::optional risk; // NativeRunSpec - Breach: `MatchRejectReason::RiskLimit` for openings (O; F: `RiskHalted`), an optional kernel-issued flatten through A.3's kernel-originated request (`origin = KernelRisk`, F), and a `NativeRiskEvent` in `native_events` (native_host.hpp:1309) (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. - 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:13673-13771); 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:4649-4662 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: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. ### A.9 Single-source proposals not adopted diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index aa2edccf..d28c76e0 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -458,7 +458,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:9133-9173`), which admits the begin, checks the +(`native_execution_consumer.cpp:9150-9190`), 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 @@ -628,7 +628,7 @@ segment, and discrete points, keep the ordinary birth gate above. `on_native_bar_open` fires at the modeled opening, before that point's matching pass (`native_execution_consumer.cpp:7066-7068`). **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:6945`), the +`engine.current_bar_ = open_view` (`native_execution_consumer.cpp:6958`), 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 @@ -1434,7 +1434,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:14052-14076`), 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 @@ -2849,7 +2849,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:283-286`). This is a + with `calc_on_order_fills`, `pine_strategy_host.cpp:284-287`). 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` diff --git a/docs/pages/pine-to-native.md b/docs/pages/pine-to-native.md index d875350d..c3f0b84d 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -124,7 +124,7 @@ rather than defaulted. | `currency` | `currency` native_run_spec.hpp:570 | `currency` pineforge.h:535 | `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:538 | `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:538 | `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:538 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:3945-3946. TradingView's round-then-slip order is adapter-only. | +| `slippage` | `slippage_ticks` native_run_spec.hpp:583 | `slippage_ticks` pineforge.h:538 | `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:539 | `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:541 | `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`. | | `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:2102 | `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. | @@ -452,12 +452,12 @@ openings TradingView takes. | `barstate.isrealtime` | `NativeStateView::phase` native_host.hpp:285 with `NativeRunPhase` native_host.hpp:41 | `pf_native_state_v1` native_c_api.h:1754 | `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:2461 | `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:1053 | `strategy_native_partial_bar_v1` native_c_api.h:2971 | `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:925 | `on_applied` native_c_api.h:2464 | `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:5361. | +| a fill, as it happens | `on_native_applied` native_host.hpp:925 | `on_applied` native_c_api.h:2464 | `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:2459 | `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:915 | `on_sub_bar` native_c_api.h:2500 | `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.isfirstbar` | `opens_session_day` market_driver.hpp:156 | `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.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:1067 | `price` native_c_api.h:1533 | `native_selected_strategy.cpp` | The readiness preview `inspect_current_execution` native_host.hpp:1079 is an observation, never an apply token; `execute_current` native_host.hpp:1084 is the apply. | diff --git a/include/pineforge/source/pine_adapter.hpp b/include/pineforge/source/pine_adapter.hpp index b34dc796..57364c2a 100644 --- a/include/pineforge/source/pine_adapter.hpp +++ b/include/pineforge/source/pine_adapter.hpp @@ -1282,6 +1282,11 @@ class PineExecutionAdapter { compat::pine::CapAttachment attachment = compat::pine::CapAttachment::None); void bind(NativeStrategyHost& host) noexcept; + // Pine source state counts chart/source bars. The kernel coordinate is + // script-bar space, so projection rows use this explicit source index and + // never rewrite a kernel lot or trade row. + int projection_bar_index(const NativeDecisionContext&) const noexcept; + int projection_bar_index(const NativeCoordinate&) const noexcept; void reset_for_run(); void set_configuration(const PineStrategyConfig& config) noexcept; void set_staged_configuration(const StagedConfiguration& staged); diff --git a/src/source/pine_adapter.cpp b/src/source/pine_adapter.cpp index 6f1e0a27..0201f9e4 100644 --- a/src/source/pine_adapter.cpp +++ b/src/source/pine_adapter.cpp @@ -842,6 +842,23 @@ PineStrategyHost* PineExecutionAdapter::pine_view_of(NativeStrategyHost* host) c return pine_view_; } +int PineExecutionAdapter::projection_bar_index( + const NativeDecisionContext& context) const noexcept { + // Pine's pending projection historically keyed this source-side field to + // the input slot the kernel presented (including magnifier sub-bars). + // Keep that policy on the named input coordinate while the kernel's public + // interval_index remains script-bar space. + return context.coordinate.input_interval_index; +} + +int PineExecutionAdapter::projection_bar_index( + const NativeCoordinate& coordinate) const noexcept { + NativeDecisionContext context; + context.coordinate = coordinate; + context.script_bar_open_ms = last_broker_open_ms_; + return projection_bar_index(context); +} + namespace { [[noreturn]] void refuse_unbound_adapter() { throw std::logic_error("Pine execution adapter is not bound to a native host"); @@ -3728,7 +3745,7 @@ std::optional PineExecutionAdapter::submit_or_repla // compares created_bar with the current bar. Submitting the staged leg // when its parent entry applies must not re-stamp the bar. if (!snapshot.projection_created_bar_pinned) { - snapshot.projection_created_bar = point->decision.coordinate.interval_index; + snapshot.projection_created_bar = projection_bar_index(point->decision); } snapshot.placement_script_open_ms = point->decision.script_bar_open_ms; snapshot.placement_sub_open_ms = point->decision.sub_bar_open_ms; @@ -5641,7 +5658,7 @@ bool PineExecutionAdapter::suppress_grouped_stop_recalc( if (filled == placement_.end()) return false; const auto eligible = [&](const PlacementSnapshot& row) { return row.family == PineOrderFamily::ExitStop - && row.projection_created_bar < context.coordinate.interval_index + && row.projection_created_bar < projection_bar_index(context) && row.from_entry == filled->second.from_entry && !row.from_entry.empty() && std::isfinite(row.requested_qty) && row.requested_qty > 0.0 && row.oca_name.empty() && std::isnan(row.exit_levels.trail_points) @@ -6055,7 +6072,7 @@ void PineExecutionAdapter::entry(const SourceId& id, bool is_long, double limit_ shadow.command_sequence = ++source_command_sequence_; shadow.source_sequence = ++source_sequence_; shadow.projection_created_bar = source_point - ? source_point->decision.coordinate.interval_index : -1; + ? projection_bar_index(source_point->decision) : -1; shadow.sizing = sizing_snapshot(); source_shadow_pending_.push_back({std::move(shadow), id}); return; @@ -7011,7 +7028,7 @@ void PineExecutionAdapter::entry(const SourceId& id, bool is_long, double limit_ } snapshot.retained_parent_topology = true; if (const auto point = detail::callback_point(require_host())) { - snapshot.projection_created_bar = point->decision.coordinate.interval_index; + snapshot.projection_created_bar = projection_bar_index(point->decision); snapshot.projection_position_side = static_cast(PositionSide::FLAT); } snapshot.source_sequence = source_sequence_ + 1; @@ -9059,7 +9076,7 @@ void PineExecutionAdapter::exit(const SourceId& exit_id, const SourceId& from_en snapshot.placement_cycle = current_position_cycle_; if (source_point) { snapshot.projection_created_bar = - source_point->decision.coordinate.interval_index; + projection_bar_index(source_point->decision); snapshot.projection_position_side = physical.signed_units > 0.0 ? static_cast(PositionSide::LONG) : (physical.signed_units < 0.0 @@ -9178,7 +9195,7 @@ void PineExecutionAdapter::exit(const SourceId& exit_id, const SourceId& from_en throw std::overflow_error("source delayed market epoch exhausted"); if (const auto point = detail::callback_point(require_host())) { snapshot.projection_created_bar = - point->decision.coordinate.interval_index; + projection_bar_index(point->decision); snapshot.placement_script_open_ms = point->decision.script_bar_open_ms; snapshot.placement_sub_open_ms = point->decision.sub_bar_open_ms; @@ -9289,7 +9306,7 @@ void PineExecutionAdapter::exit(const SourceId& exit_id, const SourceId& from_en snapshot.defer_until_post_parent_calculation = true; if (const auto point = detail::callback_point(require_host())) { snapshot.projection_created_bar = - point->decision.coordinate.interval_index; + projection_bar_index(point->decision); snapshot.projection_position_side = static_cast(PositionSide::FLAT); } @@ -9374,7 +9391,7 @@ void PineExecutionAdapter::exit(const SourceId& exit_id, const SourceId& from_en retire(predecessor); } if (const auto point = detail::callback_point(require_host())) { - snapshot.projection_created_bar = point->decision.coordinate.interval_index; + snapshot.projection_created_bar = projection_bar_index(point->decision); snapshot.projection_created_bar_pinned = true; snapshot.projection_position_side = static_cast(PositionSide::FLAT); } @@ -9839,7 +9856,7 @@ void PineExecutionAdapter::exit(const SourceId& exit_id, const SourceId& from_en snapshot.command_sequence = command_sequence; snapshot.source_sequence = ++source_sequence_; snapshot.projection_created_bar = detail::callback_point(require_host()) - ? detail::callback_point(require_host())->decision.coordinate.interval_index : -1; + ? projection_bar_index(detail::callback_point(require_host())->decision) : -1; snapshot.projection_position_side = physical.signed_units > 0.0 ? static_cast(PositionSide::LONG) : (physical.signed_units < 0.0 @@ -11622,7 +11639,7 @@ void PineExecutionAdapter::order(const SourceId& id, bool is_long, double qty, if (broker_open_epoch_ == std::numeric_limits::max()) throw std::overflow_error("source delayed market epoch exhausted"); if (const auto point = detail::callback_point(require_host())) { - snapshot.projection_created_bar = point->decision.coordinate.interval_index; + snapshot.projection_created_bar = projection_bar_index(point->decision); snapshot.placement_script_open_ms = point->decision.script_bar_open_ms; snapshot.placement_sub_open_ms = point->decision.sub_bar_open_ms; } @@ -12674,7 +12691,7 @@ native_order::ExecutionTerms PineExecutionAdapter::resolve_terms( == static_cast(PositionSide::FLAT) && !source.projection_after_close && source.projection_predecessor == 0 && facts.position.signed_units == 0.0 - && source.projection_created_bar == facts.cursor.point.interval_index + && source.projection_created_bar == projection_bar_index(facts.cursor.point) && !source.birth.from_fill() && source.oca_name.empty() && config_.pyramiding >= 0 && config_.pyramiding <= 1 && config_.commission_value == 0.0 && config_.slippage >= 0 @@ -12987,7 +13004,7 @@ bool PineExecutionAdapter::carried_long_money_precedes_priced_exit( exit = ⋚ } if (!exit || exit->from_entry.empty() || exit->legs.dormant() - || exit->projection_created_bar >= view.cursor.point.interval_index + || 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) @@ -13281,7 +13298,7 @@ NativePrecommitVerdict PineExecutionAdapter::validate_precommit(const NativePrec && source.family == PineOrderFamily::Entry && std::holds_alternative(view.definition->request.trigger) && source.is_long && source.opening - && source.projection_created_bar == view.cursor.point.interval_index + && source.projection_created_bar == projection_bar_index(view.cursor.point) && source.projection_position_side == static_cast(PositionSide::FLAT) && !source.projection_after_close && !source.birth.from_fill() && source.projection_predecessor == 0 && !source.replaced_opening @@ -13342,7 +13359,7 @@ NativePrecommitVerdict PineExecutionAdapter::validate_precommit(const NativePrec == static_cast(PositionSide::FLAT) && !source.projection_after_close && source.projection_predecessor == 0 && source.oca_type == 0 && source.oca_name.empty() - && view.cursor.point.interval_index == source.projection_created_bar + 1 + && projection_bar_index(view.cursor.point) == source.projection_created_bar + 1 && live_handles_.size() == 1 && config_.margin_long == 100.0 && config_.margin_short == 100.0 && staged_.quantity_grid && *staged_.quantity_grid > 0.0 @@ -13389,7 +13406,7 @@ NativePrecommitVerdict PineExecutionAdapter::validate_precommit(const NativePrec : (finite_positive(source.sizing.equity) ? source.sizing.equity : view.account.marked_equity); const bool pooc_slipped_signal = config_.process_orders_on_close - && source.projection_created_bar == view.cursor.point.interval_index + && source.projection_created_bar == projection_bar_index(view.cursor.point) && source.projection_position_side == static_cast(PositionSide::FLAT) && physical.signed_units == 0.0 @@ -14581,7 +14598,7 @@ bool PineExecutionAdapter::market_orders_pending_at_close( || row.family == PineOrderFamily::CloseAll; if (!market_family) continue; if (std::isfinite(row.exit_levels.limit) || std::isfinite(row.exit_levels.stop)) continue; - if (row.projection_created_bar != context.coordinate.interval_index) continue; + if (row.projection_created_bar != projection_bar_index(context)) continue; return true; } return false; @@ -15149,7 +15166,7 @@ void PineExecutionAdapter::observe_intraday_cap_noop( snapshot.projection_position_side = is_long ? static_cast(PositionSide::LONG) : static_cast(PositionSide::SHORT); - snapshot.projection_created_bar = context.coordinate.interval_index; + snapshot.projection_created_bar = projection_bar_index(context); snapshot.source_sequence = source_sequence_; const auto clock = cap_clock(context); const auto calculation = cap_calculation(context); @@ -15257,7 +15274,7 @@ void PineExecutionAdapter::refresh_pending_sizing_after_margin( auto found = placement_.find(handle.incarnation); if (found == placement_.end()) continue; auto& snapshot = found->second; - if (snapshot.projection_created_bar != context.coordinate.interval_index + if (snapshot.projection_created_bar != projection_bar_index(context) || !snapshot.market_admission.observation()) { continue; } @@ -15980,7 +15997,7 @@ void PineExecutionAdapter::apply_terminal_explicit_market_policy( if (found == placement_.end()) continue; const auto& row = found->second; if (!row.opening || row.family != PineOrderFamily::Entry - || row.projection_created_bar != context.coordinate.interval_index + || row.projection_created_bar != projection_bar_index(context) || !finite_positive(row.requested_qty) || finite_positive(row.exit_levels.limit) || finite_positive(row.exit_levels.stop) @@ -16307,14 +16324,14 @@ void PineExecutionAdapter::on_bar_open(const Bar& bar, const NativeDecisionConte if (pending.opening && (pending.family == PineOrderFamily::Entry || pending.family == PineOrderFamily::Order) - && pending.projection_created_bar < context.coordinate.interval_index + && pending.projection_created_bar < projection_bar_index(context) && pending.is_long != (opening_position.signed_units > 0.0)) { opposite_entry_waits = true; } const bool market_close = pending.family == PineOrderFamily::Close || pending.family == PineOrderFamily::CloseAll; if (!market_close - || pending.projection_created_bar >= context.coordinate.interval_index + || pending.projection_created_bar >= projection_bar_index(context) || finite_positive(pending.exit_levels.limit) || finite_positive(pending.exit_levels.stop) || finite_positive(pending.exit_levels.trail_offset)) { @@ -16465,7 +16482,7 @@ void PineExecutionAdapter::flush_pooc_marketable_limit_entry_fills( const auto& row = found->second; if (row.family != PineOrderFamily::Entry || !row.opening) continue; if (row.birth.from_fill()) continue; - if (row.projection_created_bar != context.coordinate.interval_index) continue; + 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) @@ -16594,7 +16611,7 @@ void PineExecutionAdapter::flush_pooc_marketable_exit_fills( if (row.family != PineOrderFamily::ExitLimit && row.family != PineOrderFamily::ExitStop) continue; if (row.birth.from_fill()) continue; - if (row.projection_created_bar != context.coordinate.interval_index) continue; + if (row.projection_created_bar != projection_bar_index(context)) continue; const bool stop_leg = row.family == PineOrderFamily::ExitStop; const double level = stop_leg ? row.exit_levels.stop : row.exit_levels.limit; if (!finite_positive(level)) continue; @@ -16940,7 +16957,7 @@ void PineExecutionAdapter::on_applied(const native_order::ExecutionAppliedEvent& continue; } const bool resting_limit = - pending.projection_created_bar < context.coordinate.interval_index + pending.projection_created_bar < projection_bar_index(context) && finite_positive(pending.exit_levels.limit) && !finite_positive(pending.exit_levels.stop); const bool coqueued_within_cap = @@ -18105,7 +18122,7 @@ void PineExecutionAdapter::on_applied(const native_order::ExecutionAppliedEvent& continue; } if (row.projection_created_bar < 0 - || row.projection_created_bar > context.coordinate.interval_index) { + || row.projection_created_bar > projection_bar_index(context)) { continue; } // ab9714be pine_fills.cpp:7671-7674 removes an exit diff --git a/src/source/pine_scheduler_native.cpp b/src/source/pine_scheduler_native.cpp index ce9e2679..991571ba 100644 --- a/src/source/pine_scheduler_native.cpp +++ b/src/source/pine_scheduler_native.cpp @@ -494,7 +494,7 @@ void PineScheduler::bar(const Bar& value, const NativeDecisionContext& context, if (!retained_.bar_magnifier) host.scheduler_publish_security_boundary(); } - const int chart_index = context.coordinate.interval_index; + const int chart_index = context.coordinate.input_interval_index; if (uses_aux_security_feed_) host.scheduler_feed_aux_security(chart_index); if (coof) { restore_coof_script_state(host); diff --git a/tests/test_adapter_continuation_view.cpp b/tests/test_adapter_continuation_view.cpp index abd411d9..915ab72d 100644 --- a/tests/test_adapter_continuation_view.cpp +++ b/tests/test_adapter_continuation_view.cpp @@ -307,6 +307,7 @@ void emit(const char* name, const std::vector& reads) { std::printf("};\n"); } #else +// K-IDX follow-up: re-pins the v19 coordinate/input-coordinate hash witness. // ── Pinned data (see the provenance note at the top of this file) ─────── // P1_PINNED_DATA_BEGIN // expectation corrected (k_chart, 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): diff --git a/tests/test_adapter_host_view_memo_data.hpp b/tests/test_adapter_host_view_memo_data.hpp index b949adf0..fbde5705 100644 --- a/tests/test_adapter_host_view_memo_data.hpp +++ b/tests/test_adapter_host_view_memo_data.hpp @@ -1,3 +1,4 @@ +// K-IDX follow-up: re-pins v19 broker-state hashes after the coordinate fold. // Harvested by test_adapter_host_view_memo at the lane's base (fc7aad62) with // PF_HOST_VIEW_MEMO_DUMP=1. // expectation corrected: every run's broker-state hash moved once, because v19 diff --git a/tests/test_native_batch_log_presize.cpp b/tests/test_native_batch_log_presize.cpp index d58da379..0643dc78 100644 --- a/tests/test_native_batch_log_presize.cpp +++ b/tests/test_native_batch_log_presize.cpp @@ -422,30 +422,30 @@ struct Pinned { // k24-bracket-stream: continuation 1886979334084681129ull -> 12255391317148595120ull, broker 15380761345244795255ull -> 2630080329691288926ull const Pinned kPinned[] = { {"k24-idle-raw", {Mode::Idle, true, Path::None, false, false, false}, 3000, - {0, 0x14650fb0739d0383ull, 12000ull, 0ull, 7064720261992521123ull, 12470590214657723820ull}}, + {0, 0x14650fb0739d0383ull, 12000ull, 0ull, 15485047192637436775ull, 1933090248169669312ull}}, {"k24-market-raw", {Mode::Market, true, Path::None, false, false, false}, 3000, - {149, 0xffd16e3a42465223ull, 12000ull, 748ull, 2807188055616340417ull, 3421832422185525800ull}}, + {149, 0xffd16e3a42465223ull, 12000ull, 748ull, 15718429073431250401ull, 11927358717542670328ull}}, {"k24-reissue-raw", {Mode::Reissue, true, Path::None, false, false, false}, 3000, - {0, 0x14650fb0739d0383ull, 12000ull, 11996ull, 138574470264575484ull, 14041681273026148620ull}}, + {0, 0x14650fb0739d0383ull, 12000ull, 11996ull, 10152750287253952536ull, 13027128603243255810ull}}, {"k24-bracket-raw", {Mode::Bracket, true, Path::None, false, false, false}, 3000, - {499, 0x48a8aa3386174115ull, 12000ull, 4494ull, 1047605911260740056ull, 8878901545577481248ull}}, + {499, 0x48a8aa3386174115ull, 12000ull, 4494ull, 9506515407418689311ull, 2934912856132406640ull}}, {"k24-burst-raw", {Mode::Burst, true, Path::None, false, false, false}, 3000, - {0, 0x14650fb0739d0383ull, 12000ull, 200ull, 6235366456615652126ull, 16639775381939621756ull}}, + {0, 0x14650fb0739d0383ull, 12000ull, 200ull, 13168892258055562412ull, 4125448607067718245ull}}, {"k24-bracket-synth", {Mode::Bracket, true, Path::Synthesized, false, false, false}, 3000, - {499, 0xab07e4f5c19135baull, 15000ull, 4494ull, 7762032925305995059ull, 9762893318044456255ull}}, + {499, 0xab07e4f5c19135baull, 15000ull, 4494ull, 103001943591208460ull, 18101407798730592483ull}}, {"k24-bracket-synth-volume", {Mode::Bracket, true, Path::SynthesizedVolume, false, false, false}, 3000, - {499, 0xab07e4f5c19135baull, 15000ull, 4494ull, 5528847174455583299ull, 8396626339752131633ull}}, + {499, 0xab07e4f5c19135baull, 15000ull, 4494ull, 14348902017929607696ull, 3509548862157985120ull}}, {"k24-market-after-calc", {Mode::Market, true, Path::None, true, false, false}, 3000, - {150, 0xe4072fd20cf282f3ull, 15000ull, 750ull, 14566427108214292414ull, 752215177105200869ull}}, + {150, 0xe4072fd20cf282f3ull, 15000ull, 750ull, 15568230906391022010ull, 11458602482202484137ull}}, {"k24-bracket-aggregated", {Mode::Bracket, true, Path::None, false, true, false}, 3000, - {99, 0x4967ca06278a2d63ull, 2400ull, 845ull, 7891820420429602791ull, 7321557278893600124ull}}, + {99, 0x4967ca06278a2d63ull, 2400ull, 845ull, 12529398627084087264ull, 6200526233732972992ull}}, {"k24-reissue-canonical", {Mode::Reissue, false, Path::None, false, false, false}, 240, - {0, 0x14650fb0739d0383ull, 960ull, 956ull, 3609935701573586264ull, 10739619068307988300ull}}, + {0, 0x14650fb0739d0383ull, 960ull, 956ull, 5551559802372503035ull, 8520370268542286847ull}}, {"k24-bracket-canonical", {Mode::Bracket, false, Path::None, false, false, false}, 240, - {39, 0x5a55f2e6071c7686ull, 960ull, 354ull, 15506983471220809652ull, 13995092185068427530ull}}, + {39, 0x5a55f2e6071c7686ull, 960ull, 354ull, 8530355310871831166ull, 15387242329923191668ull}}, {"k24-bracket-stream", {Mode::Bracket, false, Path::None, false, false, true}, 240, - {39, 0x5a55f2e6071c7686ull, 960ull, 354ull, 12255391317148595120ull, 2630080329691288926ull}}, + {39, 0x5a55f2e6071c7686ull, 960ull, 354ull, 2837811687237632977ull, 14310862830168009855ull}}, }; void the_pump_feeds_the_same_run() { @@ -479,6 +479,8 @@ void the_pump_feeds_the_same_run() { int main() { the_pump_feeds_the_same_run(); #ifndef PINEFORGE_K24_HARVEST +// K-IDX follow-up: re-pins the v19 coordinate/input-coordinate hash witness. + a_structural_batch_asks_for_its_driver_log_once(); a_longer_batch_holds_only_the_points_it_recorded(); #endif diff --git a/tests/test_native_continuation_view.cpp b/tests/test_native_continuation_view.cpp index fc19476e..1eae447b 100644 --- a/tests/test_native_continuation_view.cpp +++ b/tests/test_native_continuation_view.cpp @@ -294,6 +294,7 @@ void emit(const char* name, const std::vector& reads) { std::printf("};\n"); } #else +// K-IDX follow-up: re-pins the v19 coordinate/input-coordinate hash witness. // ── Pinned data (see the provenance note at the top of this file) ─────── // P1_PINNED_DATA_BEGIN // expectation corrected (k_batch, 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): @@ -306,14 +307,14 @@ void emit(const char* name, const std::vector& reads) { // 599364476702305352ull -> 15506168331272403514ull // 15819661924728693342ull -> 7430567613918907405ull constexpr std::uint64_t k_batch[] = { - 3874706988099123825ull, - 15367836576748901385ull, - 15981468109940909710ull, - 1574352814059675780ull, - 15523529820165440383ull, - 7430567613918907405ull, - 15506168331272403514ull, - 7430567613918907405ull, + 10282206035073575595ull, + 8117105284874503380ull, + 8848398979768703329ull, + 4739742555599573399ull, + 1352511348581510494ull, + 18082735004372283363ull, + 17267207790075143748ull, + 18082735004372283363ull, }; // expectation corrected (k_kernel_recorded, 9 of 9 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): // 5614733036278399145ull -> 11312775735583565012ull @@ -326,15 +327,15 @@ constexpr std::uint64_t k_batch[] = { // 8855897628712277785ull -> 519597647876169671ull // 1549062683499419271ull -> 3213001586562170957ull constexpr std::uint64_t k_kernel_recorded[] = { - 11312775735583565012ull, - 3844111693087381867ull, - 13662970706453162980ull, - 8477441320119823741ull, - 15507625209878714049ull, - 16045639815131724759ull, - 3213001586562170957ull, - 519597647876169671ull, - 3213001586562170957ull, + 4079040699515909993ull, + 8123678674598245289ull, + 12215185157324931552ull, + 17262264338911052226ull, + 12668708195714482216ull, + 10044851812123539492ull, + 616537690244417935ull, + 14571142726868795127ull, + 616537690244417935ull, }; // expectation corrected (k_aborted, 4 of 4 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): // 16067046753788442231ull -> 14046605782901906062ull @@ -342,10 +343,10 @@ constexpr std::uint64_t k_kernel_recorded[] = { // 7079798733135204626ull -> 1295792807353581922ull // 9514302941284171977ull -> 16254129914904921141ull constexpr std::uint64_t k_aborted[] = { - 14046605782901906062ull, - 16254129914904921141ull, - 1295792807353581922ull, - 16254129914904921141ull, + 1685959831200146148ull, + 2777208674932158338ull, + 7516043338442549931ull, + 2777208674932158338ull, }; // expectation corrected (k_stream, 28 of 28 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): // 16275711105144533401ull -> 15937882828208296805ull @@ -377,34 +378,34 @@ constexpr std::uint64_t k_aborted[] = { // 11284100477980189823ull -> 213879106190249824ull // 5069404992856400929ull -> 11817631875067567722ull constexpr std::uint64_t k_stream[] = { - 15937882828208296805ull, - 11432425108529535082ull, - 16673418515298261931ull, - 5285458039005469452ull, - 18056941387629449960ull, - 2894462375496212427ull, - 14624429446677514850ull, - 2112674620346426867ull, - 2562611748235069338ull, - 5998374266004261071ull, - 13898182161176238454ull, - 14276627538157298320ull, - 17794722356796227927ull, - 8310987042865327773ull, - 12354683111785255661ull, - 3313601104948653732ull, - 3725435773589134000ull, - 3725435773589134000ull, - 10233812493322420892ull, - 8747004096016098601ull, - 761589802742250794ull, - 4152804575051141009ull, - 10104621249650088517ull, - 11817631875067567722ull, - 13519590318807349176ull, - 11817631875067567722ull, - 213879106190249824ull, - 11817631875067567722ull, + 9776873597807195832ull, + 11986069253580236917ull, + 17162291834872339403ull, + 13786842037793151004ull, + 8447372653719566623ull, + 17626625212449675552ull, + 8229491038000045297ull, + 10072266054313648201ull, + 3197159470215357145ull, + 10620998501987806702ull, + 12624016652927674722ull, + 16556044180419270596ull, + 16518141588396736915ull, + 13799343351947421097ull, + 16028032644108071858ull, + 16971605674914249940ull, + 2093050977296283999ull, + 2093050977296283999ull, + 2581144724447198593ull, + 10907065441645508020ull, + 8461248817837216019ull, + 16002953856556132057ull, + 14714968428239803997ull, + 1342907117657859650ull, + 10370341518995788703ull, + 1342907117657859650ull, + 15555197184326437069ull, + 1342907117657859650ull, }; // expectation corrected (k_reused, 17 of 17 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): // 16707671882135373688ull -> 3874706988099123825ull @@ -425,23 +426,23 @@ constexpr std::uint64_t k_stream[] = { // 599364476702305352ull -> 15506168331272403514ull // 15819661924728693342ull -> 7430567613918907405ull constexpr std::uint64_t k_reused[] = { - 3874706988099123825ull, - 15367836576748901385ull, - 15981468109940909710ull, - 1574352814059675780ull, - 15523529820165440383ull, - 9335691473124626521ull, - 15291595157536991983ull, - 11095210267434077328ull, - 7430567613918907405ull, - 15506168331272403514ull, - 7430567613918907405ull, - 9335691473124626521ull, - 15291595157536991983ull, - 11095210267434077328ull, - 7430567613918907405ull, - 15506168331272403514ull, - 7430567613918907405ull, + 10282206035073575595ull, + 8117105284874503380ull, + 8848398979768703329ull, + 4739742555599573399ull, + 1352511348581510494ull, + 6763619293117683629ull, + 2018836319815576259ull, + 3214984387103191743ull, + 18082735004372283363ull, + 17267207790075143748ull, + 18082735004372283363ull, + 6763619293117683629ull, + 2018836319815576259ull, + 3214984387103191743ull, + 18082735004372283363ull, + 17267207790075143748ull, + 18082735004372283363ull, }; // expectation corrected (k_refused, 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): // 16363529955321928359ull -> 9631061672234698124ull @@ -452,13 +453,13 @@ constexpr std::uint64_t k_reused[] = { // 599364476702305352ull -> 15506168331272403514ull // 15819661924728693342ull -> 7430567613918907405ull constexpr std::uint64_t k_refused[] = { - 9631061672234698124ull, - 7430567613918907405ull, - 9631061672234698124ull, - 7430567613918907405ull, - 7430567613918907405ull, - 15506168331272403514ull, - 7430567613918907405ull, + 2939658939248907784ull, + 18082735004372283363ull, + 2939658939248907784ull, + 18082735004372283363ull, + 18082735004372283363ull, + 17267207790075143748ull, + 18082735004372283363ull, }; // P1_PINNED_DATA_END diff --git a/tests/test_native_event_retention.cpp b/tests/test_native_event_retention.cpp index 3eb18af9..d5584d18 100644 --- a/tests/test_native_event_retention.cpp +++ b/tests/test_native_event_retention.cpp @@ -140,13 +140,14 @@ constexpr int kBooks = 3; // window, when every run kept everything -- by the same fold // (V19-B-scratch/failbefore/event_rows_harvest.cpp, event_rows_harvest_base.txt). const event_rows::Record kFullRecord[kBooks] = { - {889u, 564u, 240u, 85u, 0x7fefdaa9e896c070ull}, - {7928u, 6910u, 160u, 858u, 0x55453199a45ef807ull}, - {1162u, 833u, 160u, 169u, 0x57f5593ec230c07eull}, + {889u, 564u, 240u, 85u, 0x1f9ed57a1e3b3980ull}, + {7928u, 6910u, 160u, 858u, 0x6dcae2e18cbfc033ull}, + {1162u, 833u, 160u, 169u, 0x53aa0ab250ce515eull}, }; enum class Reader { None, Poll }; +// K-IDX follow-up: re-pins v19 event-record digests after the coordinate fold. // The book, a chosen retention, and optionally a reader that polls // native_events() above its own cursor at every script bar, acknowledging // what it read until bar `acknowledge_until`. diff --git a/tests/test_native_intrabar_lower_lookup.cpp b/tests/test_native_intrabar_lower_lookup.cpp index 129dd4bd..a09cd87d 100644 --- a/tests/test_native_intrabar_lower_lookup.cpp +++ b/tests/test_native_intrabar_lower_lookup.cpp @@ -331,11 +331,11 @@ struct Pinned { // 1957262624383604956ull -> 4486098428384108224ull const Pinned kPinned[] = { {Feed::Tiling, false, "k24-lower-tiling", - {39, 0xd2ac6e3a775d5206ull, 4800ull, 12678158763721467250ull, 82279288603700875ull}}, + {39, 0xd2ac6e3a775d5206ull, 4800ull, 13919656791735621389ull, 141878967005553741ull}}, {Feed::Ragged, false, "k24-lower-ragged", - {39, 0xd1675820a42f7e8cull, 2520ull, 27817212454474162ull, 11026420420329913868ull}}, + {39, 0xd1675820a42f7e8cull, 2520ull, 402609933629186166ull, 16362418212088187476ull}}, {Feed::Tiling, true, "k24-lower-raw", - {39, 0xd1675820a42f7e8cull, 960ull, 10331922125992720379ull, 4486098428384108224ull}}, + {39, 0xd1675820a42f7e8cull, 960ull, 3468495986851218144ull, 6617510645695267944ull}}, }; void the_selected_sub_bars_feed_the_same_run() { @@ -372,6 +372,7 @@ void the_selected_sub_bars_feed_the_same_run() { int main() { the_selected_sub_bars_feed_the_same_run(); #ifndef PINEFORGE_K24_HARVEST +// K-IDX follow-up: re-pins the v19 coordinate/input-coordinate hash witness. the_lower_feed_lookup_is_linear_in_the_run(); #endif if (failures == 0) std::printf("test_native_intrabar_lower_lookup: ok\n"); diff --git a/tests/test_native_match_band_witness.cpp b/tests/test_native_match_band_witness.cpp index 139d618d..e88c7eae 100644 --- a/tests/test_native_match_band_witness.cpp +++ b/tests/test_native_match_band_witness.cpp @@ -95,6 +95,7 @@ std::uint64_t trace_digest(const Outcome& outcome) { } #ifndef PINEFORGE_L5_HARVEST +// K-IDX follow-up: re-pins the v19 coordinate/input-coordinate hash witness. // expectation corrected (18 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 trace digest folds the continuation read at every bar and every applied fill; trades, trade digest, events, event digest and the three counters did not move; re-harvested the same way on INT19's tree; V19-A's tip (6211dc94) gives the same rows: // row 0: trace 0x5c761f86ff8d6a59ull -> 0x8852dd52b2c3124dull, continuation 0x35be49c9dc0b1619ull -> 0x68316c350be98cfbull, broker 0x14820045351f6701ull -> 0x90e94b9aec984bc5ull // row 1: trace 0x003b6725f65cb1faull -> 0xb5706e0ff07be4bdull, continuation 0x9d7830a827dd26b0ull -> 0x7f333242de11f484ull, broker 0xd7654c2d887c1b39ull -> 0x6be1f81fcd8e9742ull @@ -110,12 +111,12 @@ std::uint64_t trace_digest(const Outcome& outcome) { // row 4: trace 0xae4b8cb389aa3517ull -> 0x4b4999f299d0cdf3ull, continuation 0x9dee6504b9ff28e3ull -> 0xaa0755381473b53cull, broker 0x4c877db8ad75552aull -> 0x967cdded2c9e5e51ull // row 5: trace 0xa2e515d9e6a92028ull -> 0x26b2fd0950cfaa95ull, continuation 0xde4f0d477a50d037ull -> 0xcde555440af68284ull, broker 0xad01cca35d41947cull -> 0x927c73b7c3989d27ull const Pin kPins[kScenarios] = { - {0xdd9e87bdc6914dd1ull, 0x80ae17b22e291018ull, 0xc4976ba64466ea54ull, 343, 0xfc50af70e6fbd178ull, 3189, 0x50636c7134e712ddull, 681, 181, 470}, - {0x9357ba93afee02cbull, 0xfd9e640c63f97913ull, 0x497fbb874818970eull, 419, 0x035061384ce4a851ull, 5591, 0xef4765aaa4df6c96ull, 1315, 905, 598}, - {0xea689c70123a3583ull, 0x891d3ed17fa5cf91ull, 0x92215cce5130a7faull, 391, 0xad63f1fa3d34f58bull, 3234, 0xc28c69a705a28b32ull, 774, 123, 528}, - {0xbd398693fbb24cb2ull, 0xad5d0efcb9c5967cull, 0x27238a870ac857fcull, 794, 0x553432ba5afb0756ull, 8826, 0xc007afd9f1d0e5a0ull, 2211, 1214, 1050}, - {0x4b4999f299d0cdf3ull, 0xaa0755381473b53cull, 0x967cdded2c9e5e51ull, 366, 0x07b8afde66beb2bcull, 3116, 0xe793f91e55f5fa44ull, 666, 158, 500}, - {0x26b2fd0950cfaa95ull, 0xcde555440af68284ull, 0x927c73b7c3989d27ull, 630, 0x419d9f6ff7266f85ull, 4772, 0xd9400e11c835ee59ull, 1231, 161, 846}, + {0x68912227e638409cull, 0xca8da297a9d36a42ull, 0x45ca10800a39237cull, 343, 0xfc50af70e6fbd178ull, 3189, 0x50636c7134e712ddull, 681, 181, 470}, + {0x25a6f03e405389b1ull, 0x87ef6a5457e2a044ull, 0xb928bebcf8b7b3f0ull, 419, 0x035061384ce4a851ull, 5591, 0xef4765aaa4df6c96ull, 1315, 905, 598}, + {0xab7e6b982584d43full, 0xb463b4435067c9a9ull, 0xe1302517ccf5a37eull, 391, 0xad63f1fa3d34f58bull, 3234, 0xc28c69a705a28b32ull, 774, 123, 528}, + {0xe8010d4ed5bb803eull, 0x630b685c961a963cull, 0xc981f558a7c4a8fcull, 794, 0x553432ba5afb0756ull, 8826, 0xc007afd9f1d0e5a0ull, 2211, 1214, 1050}, + {0x9f93851aa161ad64ull, 0x115caaaa6e7fec40ull, 0x27313426de630a99ull, 366, 0x07b8afde66beb2bcull, 3116, 0xe793f91e55f5fa44ull, 666, 158, 500}, + {0xfabf941108307d2cull, 0x66c6139cb1e3ef13ull, 0x71698a08ab391603ull, 630, 0x419d9f6ff7266f85ull, 4772, 0xd9400e11c835ee59ull, 1231, 161, 846}, }; #endif diff --git a/tests/test_native_match_hash_witness.cpp b/tests/test_native_match_hash_witness.cpp index 40f5b31c..2976aab5 100644 --- a/tests/test_native_match_hash_witness.cpp +++ b/tests/test_native_match_hash_witness.cpp @@ -87,6 +87,7 @@ std::uint64_t trace_digest(const Outcome& outcome) { } #ifndef PINEFORGE_K3_HARVEST +// K-IDX follow-up: re-pins the v19 coordinate/input-coordinate hash witness. // expectation corrected: the three hash columns of every scenario moved once, // because v19 folds the continuation over live state word-wise // (native-consumer/v9) and the broker-state hash folds a running closed-row @@ -109,11 +110,11 @@ std::uint64_t trace_digest(const Outcome& outcome) { // scenario 3: trace 0xbe543a454300ded7ull -> 0x359cc22165da6d96ull, continuation 0xdd2049d30f042dacull -> 0x01e03db6f203e11eull, broker 0x23cdee2a26ae76fcull -> 0x071b47fad3d98bf7ull // scenario 4: trace 0xe947a05c6802cc68ull -> 0xad4443d75ee5d3bcull, continuation 0xa1b0cd08a0745aceull -> 0xc8d5da5d64e610d6ull, broker 0xd229c4059b4b3e02ull -> 0x874121590b093b20ull const Pin kPins[kScenarios] = { - {0x136dcf16c6fa5351ull, 0x4691676bcf0aa127ull, 0x329da07804463d91ull, 309, 0xd46895e4cb220464ull, 4636, 0x2f871fcdc8768b61ull, 1080, 647, 446}, - {0x4e8e642c6c4f5571ull, 0x4be23aedc760df3bull, 0x1abd121fb23e8f41ull, 249, 0x7cc7976babca3c43ull, 2681, 0x7d16f612ee716310ull, 589, 142, 352}, - {0x39e317e0536c5304ull, 0xe9e168e352f9f8d4ull, 0xda88af88ef6027d6ull, 1342, 0x58b908db693694bcull, 16128, 0x6366cacce74b3948ull, 4400, 1950, 1779}, - {0x359cc22165da6d96ull, 0x01e03db6f203e11eull, 0x071b47fad3d98bf7ull, 430, 0x76f5a9cacb8e69deull, 3362, 0xce2d56d15c6cf10cull, 891, 102, 549}, - {0xad4443d75ee5d3bcull, 0xc8d5da5d64e610d6ull, 0x874121590b093b20ull, 502, 0x696bcd30b1e40f3bull, 4081, 0x572e1013d4fadde4ull, 1165, 94, 665}, + {0xa37d3f4c2f65a80dull, 0x40151bfbedfb2642ull, 0x4bd66a9c1fb260f4ull, 309, 0xd46895e4cb220464ull, 4636, 0x2f871fcdc8768b61ull, 1080, 647, 446}, + {0x8f0581e371bf7980ull, 0x599b9bdd575b3b54ull, 0x6948570858eeee2dull, 249, 0x7cc7976babca3c43ull, 2681, 0x7d16f612ee716310ull, 589, 142, 352}, + {0xf5b5613a3f58e1f5ull, 0x818556a05ad62374ull, 0x51985203758a2339ull, 1342, 0x58b908db693694bcull, 16128, 0x6366cacce74b3948ull, 4400, 1950, 1779}, + {0xcb145539f0f10e88ull, 0x011b0b80b2505b9cull, 0xaa5261315d1cd0e7ull, 430, 0x76f5a9cacb8e69deull, 3362, 0xce2d56d15c6cf10cull, 891, 102, 549}, + {0x826a0a26736b1485ull, 0x957b12374f396702ull, 0x615f80420a9a6d24ull, 502, 0x696bcd30b1e40f3bull, 4081, 0x572e1013d4fadde4ull, 1165, 94, 665}, }; #endif From ae8a074d5233d7acb3efa3cef94b79b9c2db9b7b Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Fri, 25 Sep 2026 19:19:41 +0800 Subject: [PATCH 16/24] docs: make native migration examples and comments executable truth (cherry picked from commit 66f95745cc243fe3b56fced95c4798368337c5b2) --- README.md | 3 +- docs/adr/0001-kernel-adapter-boundary.md | 2 +- docs/design/native-feature-parity.md | 8 +- docs/pages/index.md | 2 +- docs/pages/native-engine.md | 32 ++-- docs/pages/pine-to-native.md | 37 ++++- include/pineforge/engine.hpp | 8 +- include/pineforge/native_c_api.h | 4 +- include/pineforge/native_host.hpp | 8 +- include/pineforge/native_run_spec.hpp | 2 +- include/pineforge/pineforge.h | 76 +++++----- scripts/check_dangling_comment_names.py | 146 +++++++++++++++++++ scripts/check_doc_lint.py | 6 +- scripts/ci_preflight.py | 2 + scripts/ci_verify.py | 9 +- scripts/cxxlex.py | 70 +++++++++ scripts/test_check_dangling_comment_names.py | 33 +++++ scripts/test_check_doc_lint.py | 13 +- src/c_abi.cpp | 2 +- src/engine_internal.hpp | 2 +- src/native_execution_consumer.cpp | 4 +- src/source/pine_adapter.cpp | 10 +- src/source/pine_reissue_binding.hpp | 3 +- tests/CMakeLists.txt | 24 +++ tests/extract_native_engine_complete_host.py | 61 ++++++++ tests/test_pine_to_native_doc_rows.cpp | 37 ----- tests/test_pine_to_native_worked.cpp | 91 +++++++++++- 27 files changed, 564 insertions(+), 131 deletions(-) create mode 100644 scripts/check_dangling_comment_names.py create mode 100644 scripts/cxxlex.py create mode 100644 scripts/test_check_dangling_comment_names.py create mode 100644 tests/extract_native_engine_complete_host.py delete mode 100644 tests/test_pine_to_native_doc_rows.cpp diff --git a/README.md b/README.md index e9a207ad..10289a67 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,8 @@ report. Each is a CTest row: `ctest --test-dir build -R example_`. Hand the runtime a callback table and drive the kernel from any language with a C FFI — no C++ in your own code. The complete file is [`examples/native/hello_kernel_c.c`](examples/native/hello_kernel_c.c); the -43 `strategy_native_*` functions are declared in +41 `strategy_native_*` functions (plus two `strategy_configure_native_ext_*` +functions) are declared in [`include/pineforge/native_c_api.h`](include/pineforge/native_c_api.h) and summarised in [Driving the kernel from C](#driving-the-kernel-from-c) below. diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index 49cba961..0108fdf3 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -666,7 +666,7 @@ The table records, at the tree the doc wave read, what each block described and | 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 holiday session walk `set_native_periods` `src/timeframe.cpp:1621` (the `TradingView` comment `sha256:01f0fe296ca8c2a1e93cad96b9a6cc25331fb326a6a63a2cbd894bb11710fc7e` :1470) | 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 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` | | `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` | diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 6634a277..14bae3c1 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 `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:1350, 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` in batch and across a stream's warmup and realtime legs. 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. What a recorded row costs — quadratic on the Pine adapter, O(closed rows) per row in the kernel — is measured and ruled in §3.8 | +| 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 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 @@ -175,7 +175,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | 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:2157, `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:546 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 57 `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 symbols are a second, disjoint inventory beside the 57 codegen-facing ones (`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. | +| 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 43 native C 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 | @@ -906,7 +906,7 @@ row C). | # | cost | measured on `fd785928` | ruling | |---|---|---|---| | **A** | the Pine adapter's recording fold: O(retained history) per row | ×3.92 to ×4.02 per doubling — 9.26 s at 800 bars of order-and-cancel, 19.5 s at 2,688 bars of re-issue — against at most 0.015 s with recording off | **open**: blocked on storage and a write barrier outside lane F9's files; no epoch needed; value witness pinned. **Resolved** by R5 lane V19-E (`pineforge-source-adapter/v4`): retired rows are erased and the logs fold as running digests, so a row costs what is live; **completed** by R5 lane V19-FIX for the one shape V19-E still held, an exit cancelled and re-placed every bar at one level | -| **B** | the kernel recorder's closed-row walk: O(closed rows) per row | ×2.61 rising to ×3.64 per doubling with trades (0.80 s at 24,000 bars and 1,200 closed rows) against ×1.94 to ×2.08 without | **ruled**: retained, cost documented. **Resolved** by the v19 value epoch (R5 lane V19-A): each closed row folds once, into a running digest | +| **B — historical** | the pre-v19 kernel recorder's closed-row walk: O(closed rows) per row | ×2.61 rising to ×3.64 per doubling with trades (0.80 s at 24,000 bars and 1,200 closed rows) against ×1.94 to ×2.08 without | **historical ruling, superseded** by the v19 value epoch (R5 lane V19-A): each closed row now folds once, into a running digest | | **C** | the terminal continuation capture (E24 STOP1) | 0.048 to 0.052 s, 23 to 24 % of the 43,008-bar gated replay | **revised** by R5 lane PERF-P1 (supervisor ruling): latched as a view, folded on first read; no value moves. **Eager again** since R5 lane V19-A: the v19 fold is the live state alone, so a view saves nothing | | **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 | @@ -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:1616-1617) → 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:1618-1619) → 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/index.md b/docs/pages/index.md index 0ee956d3..d594aa80 100644 --- a/docs/pages/index.md +++ b/docs/pages/index.md @@ -163,7 +163,7 @@ declarations plus 43 native-host declarations) and the toolchain ## A 30-second taste -This snippet calls the eight per-strategy exports generated by PineForge codegen. +This snippet calls four of the eight per-strategy exports generated by PineForge codegen. Compile it as part of the generated strategy module and link that module with `libpineforge`; these symbols are not provided by the archive on its own. diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index d28c76e0..2fc509f1 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -935,7 +935,9 @@ trigger alternative, and a retained best must still produce a representable level for the successor's offset; otherwise the replacement is rejected and the predecessor stays live. -Two more options change what a replacement consumes, never what it matches. +Two more options change what a replacement keeps. `keep_handle` can also +change matching when a re-priced parent has waiting children: they retain +their owner and may fill, whereas a plain replace ends that ownership. `keep_handle` re-prices the request in place: the successor keeps the predecessor's handle, its place in the chain (no chain-root pair is folded again), its children and cohort members, and a trail's arm ordinal, and takes @@ -1898,7 +1900,7 @@ spec digest folds a retention other than the default. | retention | what the kernel keeps | memory | |---|---|---| -| `Window` (the default) | the command journal, only until the host has read it; no driver point, no account row | O(live) | +| `Window` (the default) | the command journal, only until the host has read it; no driver point, no account row | O(live + replaces) | | `Commands` | the whole command journal and every account row; no driver point | O(run) | | `Full` | the whole command journal, every driver point and every account row — the record every run kept before the field existed | O(run) | @@ -2890,8 +2892,10 @@ native contract is legacy and cannot take `--native-config`. | Submit/replace/cancel outside the allowed phase | throws; not a fill | command only from callbacks or between realtime inputs | | Cooperative abort | `Failed` (`Aborted`) | reuse with `configure_native` using the same session key and a higher run number | -`last_run_status()` is not failure authority. After `Failed`, later `run` / -`stream_*` / `configure_native` refuse (`native host already failed`). +`last_run_status()` is not failure authority. After a cooperative `Aborted` +failure, `configure_native` can reuse the host with the same session key and +a higher run number. Other failures remain latched: subsequent `run`, +`stream_*` and `configure_native` calls refuse (`native host already failed`). ## C++ example @@ -2948,6 +2952,7 @@ pineforge::NativeRunSpec make_spec() { spec.fee_value = 6.0; spec.close_execution = pineforge::NativeCloseExecution::NextEligiblePoint; spec.allowed_open_directions = pineforge::NativeOpenDirections::Both; + spec.event_retention = pineforge::NativeEventRetention::Full; return spec; } @@ -3244,10 +3249,9 @@ its `resolve_execution_terms` override; and its own placement-time money-band and affordability gates, which consume a quantity before any request exists and so cannot be a kernel decision. The percentage fee reserve is the kernel's (`reserve_percent_fee`). The generic conversion is available through -`native_sized_units()`, while source paths that keep `HostSized{Open}` restate -their source-specific quotient and floor in `resolve_execution_terms`; this -paragraph does not claim that every source conversion is implemented at one -call site. The two +`native_sized_units()`. Source paths that keep `HostSized{Open}` retain their +source-specific floor in `resolve_execution_terms`, while the cash or equity +quotient comes from the kernel's sizing query. The two lot floors stay the adapter's because neither is the kernel's `SnapToGrid` on every input — the cash floor keeps a quotient a millionth of a lot under a boundary raw, and the percent floor has no on-grid tolerance, so an exact lot @@ -3802,11 +3806,11 @@ by anything but an abort, answer `PF_NATIVE_E_STATE` with cooperative abort latches only while a run holds its spec, and the Failed state keeps it, so no public call reaches it. -`pf_native_run_spec_ext_v1`'s third published length, +`pf_native_run_spec_ext_v1`'s third of five published lengths, `PF_NATIVE_RUN_SPEC_EXT_V1_POLICY_SIZE`, ends the tail after the base layout (`PF_NATIVE_RUN_SPEC_EXT_V1_BASE_SIZE`) and the risk tail (`PF_NATIVE_RUN_SPEC_EXT_V1_RISK_SIZE`); the runtime accepts it and the other -three. That tail appends what the header used to list as unrepresentable — the retained intrabar path +four. That tail appends what the header used to list as unrepresentable — the retained intrabar path (`PF_NATIVE_SPEC_EXT_INTRABAR`), the four feed-shape and presentation policies (`PF_NATIVE_SPEC_EXT_FEED_POLICY`: slot labels, feed tolerance, the forced path order, abort reporting), and the margin model's remaining knobs @@ -3821,12 +3825,14 @@ The intrabar block is also what makes `on_sub_bar` reachable: only has sub-bars of its own. A `PF_NATIVE_SLOT_LABEL_FEED_TOLERANT` run keeps the caller's own labels and delivers none. -`pf_native_request_v1` likewise has four published lengths now — base, plus +`pf_native_request_v1` likewise has five published lengths now — base, plus the anchored-leg tail (`PF_NATIVE_REQUEST_V1_ANCHOR_SIZE`), plus the sizing detail (`PF_NATIVE_REQUEST_V1_SIZING_SIZE`): `size_price` (`SizePrice`: `RESOLVED`, `SIGNAL`, `SIGNAL_ON_TICK`) and `reduce_basis` (`ScopeBasis`: -`AT_MATCH`, `AT_ACCEPTANCE`), plus the trail seed: `trail_best_seed` and -`trail_has_best_seed` (`Trail::best_seed`, where the running best starts). +`AT_MATCH`, `AT_ACCEPTANCE`), plus the trail seed +(`PF_NATIVE_REQUEST_V1_SEED_SIZE`): `trail_best_seed` and +`trail_has_best_seed` (`Trail::best_seed`, where the running best starts), +and the current arm-relation tail. `pf_native_working_v1` is the first **readout** with an additive tail: `trail_has_arm_price`, the request's own flag read back, so a trail with no diff --git a/docs/pages/pine-to-native.md b/docs/pages/pine-to-native.md index c3f0b84d..c9b0b697 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -315,8 +315,8 @@ 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 | `arm_scope` / `arm_first_match` native_c_api.h:2176-2177 | `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` native_c_api.h:2111 | `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_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:2093-2100 | `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:2094-2098 | `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:2116 | `native_trail_risk_strategy.cpp` | The absolute level the trail arms at. Read the live projection with `trail_state` native_host.hpp:1072 (C: `strategy_native_trail_state_v1` native_c_api.h:2986). 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, @@ -493,18 +493,40 @@ host on the default reads them truthfully without recording a curve. prints the difference; recording books no cash and places no order, so the two runs close the same trades for the same money. +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:1309. 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 +positions; `strategy.equity` adds that gross amount to realized equity. A +native open lot's `unrealized_pnl` and `native_marked_equity(mark)` deduct its +remaining entry commission. To recover the Pine figures from those native +values, add the open lots' `entry_commission` once, at the same mark. + +The report's trade statistics (`pf_trade_stats_t` in the all, long and short +blocks) count **report-only range-end rows** when +`report_open_position_at_end` is enabled. Protected `BacktestEngine` closed +trade accessors count booked closes only. To compare any of the C trade +statistics below with Pine's closed-trade statistics, aggregate C +`pf_trade_t` rows with `open_at_end == 0`; the direct values and the +`gross_loss` sign conversion apply to the report as printed only when +range-end reporting is off. + | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | | `strategy.account_currency` | `currency` native_run_spec.hpp:570 | `currency` pineforge.h:535 | `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:1182. 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:537 | `hello_kernel.cpp` | The value you declared, unchanged by the run. | -| `strategy.equity` | `native_marked_equity` native_host.hpp:1281 | `strategy_native_marked_equity_v1` native_c_api.h:3011 | `native_open_lots_strategy.cpp` | Pine marks at the current `close`; the native call takes the mark explicitly, so the caller says what "now" means. It equals the balance plus the open lots' `unrealized_pnl` native_host.hpp:341, which the example asserts at every calculation. | +| `strategy.equity` | `current_equity() + open_profit(mark)` engine.hpp:970-1024; or `native_marked_equity(mark)` native_host.hpp:1281 plus open entry commissions | `strategy_native_marked_equity_v1` native_c_api.h:3011 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.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` | The protected C++ accessor is signed (negative for losses), while the C report stores `gross_loss` as a positive magnitude. Convert C to the protected sign with `-gross_loss`; then `netprofit == grossprofit - C.gross_loss`. | +| `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:3011 less the balance | `native_open_lots_strategy.cpp` | Takes the price to mark at, like every other native mark. | +| `strategy.openprofit` | `open_profit` `sha256:0490e46ca24b81c82c9549ce38af81be1ea888a60277263108cd26bf3f01e0c7` engine.hpp:1024 | `strategy_native_marked_equity_v1` native_c_api.h:3011 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.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. | @@ -927,7 +949,10 @@ them (`tests/test_pine_to_native_worked.cpp`), and runs them on sixteen as `CancelReason::OwnerGone`, because the lots they close are gone; the row's commission is 0.1 % of both notionals, 5.2423. The row computes every one of those numbers from the Pine block at the top of this section, so a block that stops -configuring, or stops charging what the Pine declares, fails it. +configuring, or stops charging what the Pine declares, fails it. Two further +tapes drive the same extracted host: one arms the trail at +`101 + 30 × 0.25 = 108.5` and closes at its best minus eight ticks; the other +closes the stop at `101 - 20 × 0.25 = 96`. ### What a Pine author gets wrong the first time diff --git a/include/pineforge/engine.hpp b/include/pineforge/engine.hpp index e4c98e10..d1e9987e 100644 --- a/include/pineforge/engine.hpp +++ b/include/pineforge/engine.hpp @@ -225,9 +225,9 @@ struct TradeC { double pnl; double pnl_pct; int is_long; - // Max Adverse/Favorable Excursion expressed as $ move per unit qty. - // max_runup is peak favorable move (price travel in direction of trade). - // max_drawdown is peak adverse move (price travel against trade). + // Max Adverse/Favorable Excursion for the whole trade in account currency, + // net of entry fees. max_runup is the peak favorable amount; + // max_drawdown is the peak adverse amount. double max_runup; double max_drawdown; double qty; @@ -2113,7 +2113,7 @@ class BacktestEngine { // * different driving -- run(), stream_begin(warmup=1)+push and // stream_begin(warmup=all) over the same bars booking the same trades // record different rows from index 0; only the length identity above - // survives. Two streams share exactly their common Warmup prefix. + // survives. Without subscriptions, streams share their Warmup prefix. // * the broker half alone IS driving-mode invariant: factor the // continuation out with broker_state_hash_from_execution_hash(fixed) // and the remaining fold is identical at every bar in every driving. diff --git a/include/pineforge/native_c_api.h b/include/pineforge/native_c_api.h index 586cca39..534637d1 100644 --- a/include/pineforge/native_c_api.h +++ b/include/pineforge/native_c_api.h @@ -2396,12 +2396,12 @@ typedef struct pf_native_run_spec_ext_v1 { ((uint32_t)offsetof(pf_native_run_spec_ext_v1, risk_has_max_drawdown)) /** Byte length of #pf_native_run_spec_ext_v1 with L9's risk tail but without - * N8's intrabar / policy tail — the second of its four published layouts. */ + * N8's intrabar / policy tail — the second of its five published layouts. */ #define PF_NATIVE_RUN_SPEC_EXT_V1_RISK_SIZE \ ((uint32_t)offsetof(pf_native_run_spec_ext_v1, intrabar_kind)) /** Byte length of #pf_native_run_spec_ext_v1 with N8's intrabar / policy tail - * but before the `auxiliary_*` tail was appended — the third of its four + * but before the `auxiliary_*` tail was appended — the third of its five * published layouts. It is the offset of the first auxiliary field, for the * same reason #PF_NATIVE_RUN_SPEC_EXT_V1_BASE_SIZE is an offset. */ #define PF_NATIVE_RUN_SPEC_EXT_V1_POLICY_SIZE \ diff --git a/include/pineforge/native_host.hpp b/include/pineforge/native_host.hpp index 0e2812c3..132e4882 100644 --- a/include/pineforge/native_host.hpp +++ b/include/pineforge/native_host.hpp @@ -23,8 +23,8 @@ inline namespace engine_script_run_v19 { /// Unconfigured is a fresh host, Ready a staged spec, Running a consumed begin /// (with NativeRunPhase saying which driving), Completed a finished run whose /// lots and live requests stay visible but not actionable, and Failed a durable -/// first failure: discard the host, replay on a fresh instance, never reconfigure -/// in place. Pinned by tests/test_native_host_repairs.cpp. +/// first failure. Cooperative Aborted permits same-key, higher-run reuse; +/// other failures need a new host. Pinned by tests/test_native_host_repairs.cpp. enum class NativeLifecycleKind : std::uint8_t { Unconfigured = 0, Ready = 1, @@ -58,8 +58,8 @@ enum class NativeCompletion : std::uint8_t { /// driver refused, UnsupportedSource a source-only setter or command on a bare /// host, CallbackException a host callback that threw or a C callback that /// returned non-zero, and Aborted a cooperative abort. The rest are internal -/// exhaustion states with no rollback promise. Set once and latched: later run / -/// stream_* / configure_native calls refuse. +/// exhaustion states with no rollback promise. Later run/stream_* calls refuse; +/// only Aborted permits configure_native with the same key and higher run. enum class NativeFailureCode : std::uint16_t { None = 0, InvalidSpecification = 1, diff --git a/include/pineforge/native_run_spec.hpp b/include/pineforge/native_run_spec.hpp index b6d8a483..84516b90 100644 --- a/include/pineforge/native_run_spec.hpp +++ b/include/pineforge/native_run_spec.hpp @@ -528,7 +528,7 @@ struct NativeAuxiliaryFeed { /// every script-bar end. The kernel never drops an event its own live state /// still reads -- a deferred group-adjustment chain, an applied notification /// not yet delivered -- and keeps no driver point and no account observation. -/// Memory is O(live), not O(run). +/// Memory is O(live + replaces): plain replaces retain a small chain index. /// /// Commands keeps the whole command journal and every account observation, /// and no driver point. Full keeps everything: the command journal, every diff --git a/include/pineforge/pineforge.h b/include/pineforge/pineforge.h index dc5fb2ad..92424195 100644 --- a/include/pineforge/pineforge.h +++ b/include/pineforge/pineforge.h @@ -200,8 +200,8 @@ typedef struct pf_trade_s { * the previous short form (entry/exit-1)*100 was wrong on * large moves. Sign always matches pnl. */ int is_long; /**< 1 if long, 0 if short. */ - double max_runup; /**< Peak favorable price travel during the trade ($/unit qty). */ - double max_drawdown; /**< Peak adverse price travel during the trade ($/unit qty). */ + double max_runup; /**< Peak favorable whole-trade excursion, account currency, net of entry fees. */ + double max_drawdown; /**< Peak adverse whole-trade excursion, account currency, net of entry fees. */ double qty; /**< Filled quantity. */ double commission; /**< Entry+exit commission actually deducted from pnl * (account currency). pnl is already net of this. */ @@ -446,24 +446,24 @@ typedef struct pf_report_s { * Loss-side fields are positive magnitudes. Undefined values are NaN * (see per-field docs). */ pf_metrics_t metrics; - /* Per-script-bar equity curve. time_ms is the script-bar OPEN + /* Per-script-bar equity curve (bare host: KernelRecorded); time_ms is the script-bar OPEN * timestamp; equity = initial_capital + net_profit + open_profit at - * bar close. Heap-allocated; freed by report_free. len == + * bar close. Free via #report_free or #strategy_native_report_free_v1. len == * script_bars_processed, EXCEPT after a mid-run error (check * strategy_get_last_error): an exception can truncate the curve, and * metrics then describe the truncated prefix. NOTE int64_t length * (ctypes: c_int64). */ pf_equity_point_t* equity_curve; int64_t equity_curve_len; - /* Per-script-bar broker-state hash, filled when - * #strategy_set_broker_state_hash_recording is on; freed by - * #report_free. NULL / 0-length when recording was off (default) or no - * script bars were dispatched. When populated, len == - * script_bars_processed, and the last element equals - * #strategy_broker_state_hash's value at the end of the run for a - * compiled Pine strategy and a bare native host alike: the recorder - * latches the continuation at each report point, and the scalar read - * after the run folds the last latch. ABI v4. */ + /* Per-script-bar broker-state hash when recording is on and report + * points exist (KernelRecorded for a bare native host); freed by + * #report_free or #strategy_native_report_free_v1. NULL / 0-length when off or no points were recorded. + * On a completed run, len == script_bars_processed and the last row + * equals #strategy_broker_state_hash after the run for Pine and bare + * native hosts alike: each point latches the continuation, and the + * scalar folds the last latch. A Failed or Aborted run can have a + * shorter recorded prefix; its last row need not equal the scalar. + * ABI v4. */ uint64_t* broker_state_hash; int64_t broker_state_hash_len; } pf_report_t; @@ -756,11 +756,11 @@ PF_API uint64_t strategy_closed_trade_entry_incarnation( * lifecycle uses close-only strategy calculation (the Pine strategy default) * while resting broker orders are evaluated on every normalized trade. * - * This compiled-strategy C entry point rejects calc_on_order_fills, historical - * probe/tail overrides, timestamped FX and auxiliary/native security feeds. - * The hand-written `NativeStrategyHost` stream API has its own generic - * contract and accepts the native FX curve, auxiliary feeds and calculation - * trigger where its run specification permits them. + * A generated Pine strategy using this entry point rejects calc_on_order_fills, + * historical probe/tail overrides, timestamped FX and auxiliary/native + * security feeds. A C native host can also use this entry point and accepts + * the native FX curve, auxiliary feeds and calculation trigger where its + * run specification permits them. * @return 0 on success, -1 on failure. Inspect #strategy_get_last_error. */ PF_API int strategy_stream_begin(pf_strategy_t s, const pf_bar_t* warmup_bars, @@ -965,8 +965,8 @@ PF_API void strategy_set_probe_suppress_tail_logic(pf_strategy_t s, int on); PF_API void strategy_set_path_order(pf_strategy_t s, int mode); /** The dual-entry-stop arbitration decided on the LAST bar the most recent * run() dispatched: a flat position resting exactly one long stop-only - * ENTRY and one short stop-only ENTRY, both touched on that bar - * (`dual_entry_stop_path_winner`, internal). Values mirror + * ENTRY and one short stop-only ENTRY, both touched on that bar. + * Values mirror * `internal::DualEntryStopPathWinner`'s enumerator order: * - `0` None -- no such pair was arbitrated on that bar (not flat, no * matching pair, or neither/only one side touched). @@ -997,8 +997,8 @@ PF_API int strategy_last_bar_dual_entry_path(pf_strategy_t s); * * When @p on is non-zero, every subsequent run() appends * #strategy_broker_state_hash's value to pf_report_t::broker_state_hash - * immediately after each script bar is dispatched, so the array's length - * matches pf_report_t::script_bars_processed. Cleared (recorded array + * after each report point (KernelRecorded for bare hosts); completed runs + * match pf_report_t::script_bars_processed, failed ones may not. Cleared (recorded array * emptied, not the flag itself) at the start of every run(); the flag is * persistent configuration, like #strategy_set_realtime_tail, and stays * set until a caller passes @p on == 0. @@ -1099,16 +1099,16 @@ typedef enum pf_fill_qty_partition_e { * over-cap add); or what one of the two MARKET reversal kernels * opens -- a same-bar-market member against an opposite live position * opens the remainder `sbmt_tx_qty - min(sbmt_tx_qty, live qty)` - * (`apply_same_bar_market_tx_reversal`), and the exact SHORT-seed + * (the same-bar market reversal path), and the exact SHORT-seed * collision's final short re-opens the residual * `pyramid_entries[0].qty - pyramid_entries[1].qty` after closing both - * lots (`short_seed_collision_final_short_is_live`). Both kernels are + * 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 `use_default_stop_placement_qty` says the + * 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 (`calc_qty`). + * - `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 * predicate opens no leg of its own: the order's @@ -1119,17 +1119,17 @@ typedef enum pf_fill_qty_partition_e { * same-cycle frozen explicit-FIXED transaction the close consumes exactly, * a finalized flat MARKET pair against an opposite position, or one of the * two reversal kernels above opening nothing. Where the order was created - * FLAT the engine's close-only branch is `close_opposite_then_enter`: a + * FLAT the engine's close-only branch can open a remainder: a * transaction larger than the live position still opens the remainder, so * a consumer compares @p qty with the live position. A replaced - * default-percent short (`replaced_percent_short_market_is_live`) is - * dispatched `close_opposite_then_enter` with its `frozen_default_qty`: + * default-percent short is dispatched + * with its `frozen_default_qty`: * @p qty is that transaction, @p close_only 0. The probe answers for the * order filling against the CURRENT book and position; fills that an * earlier order in the same pass would make first are not simulated. * NOT folded into @p qty: the deferred-flip - * carry (`tv_carry_qty`, added by `enter_market_from_flat` for a priced - * entry firing from FLAT whose placement side is the opposite of the + * carry (`tv_carry_qty`, added on a priced entry from FLAT whose + * placement side is the opposite of the * requested side) -- read `tv_carry_qty` / `created_position_side` from * the mirror. Returns 0 on success; 1 -- with @p qty NaN, @p close_only 0, * @p partition -1 (#PF_FILL_QTY_PARTITION_EXIT) -- when the order is an EXIT (its fill quantity is @@ -1143,10 +1143,10 @@ PF_API int strategy_pending_order_fill_qty(pf_strategy_t s, int index, double fi * (`profit_ticks` / `loss_ticks` / `trail_points`) resolve now (ABI v4, * task 8): entries, plain orders and exits with an empty `from_entry` * always; an exit bound to a `from_entry` only once that id has filled in - * the CURRENT position cycle -- the gate the engine's own - * `materialize_relative_exit_prices_for_live_position` and eligibility - * pass share. 0 otherwise; -1 when @p s is NULL or @p index is out of - * range. */ + * the CURRENT position cycle -- the same condition the level-resolution + * and eligibility passes use. + * 0 otherwise; -1 when @p s is NULL or + * @p index is out of range. */ PF_API int strategy_pending_order_level_resolved(pf_strategy_t s, int index); /** The price levels the @p index-th resting order would fire at, as the * engine's fill path resolves them (ABI v4, task 8). A leg the order @@ -1247,12 +1247,12 @@ typedef enum pf_close_cause_e { * max-intraday-loss (4) and filled-order-cap (5) rows get their value (the * adapter classifies its own bracket rows by order family, so it clears the * kernel's `2` on them first); then the row's `exit_from_bracket` flag -- true - * only for a REAL `strategy.exit` leg, either an `OrderType::EXIT` fill + * only for a REAL `strategy.exit` leg, either an exit-order fill * whose id does NOT carry the internal `"__close__"` prefix that a deferred * `strategy.close`/`close_all` order is also given (that path reuses the - * same `OrderType::EXIT` fill machinery), or a whole-position bracket + * same exit fill machinery), or a whole-position bracket * revived and fired at the margin-call event price - * (`revive_position_brackets_after_margin_call_partial`) -- -> 2; + * (through the margin-call bracket revival) -- -> 2; * otherwise 1. * A Pine/source run's values are unchanged. A bare native host that declares * a kernel margin model now reads `3` for its own liquidation rows (and `4` diff --git a/scripts/check_dangling_comment_names.py b/scripts/check_dangling_comment_names.py new file mode 100644 index 00000000..d927ee9a --- /dev/null +++ b/scripts/check_dangling_comment_names.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +"""Reject code-shaped names in installed/kernel comments that name no code. + +This is a lexical guard, not a C++ name resolver. It checks the identifier +parts of qualified names against comment-free source and allows explicit +historical wording. The small exemption set below contains documented prose +labels and intentionally incomplete field prefixes, not callable symbols. +""" +from __future__ import annotations + +import argparse +from pathlib import Path +import re +import sys + +from cxxlex import lex_lines + +ROOT = Path(__file__).resolve().parent.parent +SOURCE_SUFFIXES = {".h", ".hpp", ".c", ".cpp", ".cc", ".inc"} +IDENT = re.compile(r"[A-Za-z_][A-Za-z0-9_]*") +CITED = re.compile( + r"`([^`]+)`|([A-Za-z_][A-Za-z0-9_:]*)\s*\(|" + r"([A-Za-z_][A-Za-z0-9_]*(?:::[A-Za-z_~][A-Za-z0-9_]*)+)|" + r"\b([a-z][a-z0-9]*(?:_[a-z0-9]+){2,})\b") +HISTORY = re.compile(r"\b(?:deleted|removed|retired|gone|historical|formerly|" + r"deprecated|superseded|no longer|before v[0-9]+|" + r"[0-9a-f]{8})\b", re.I) +# These are prose labels or examples, never promised declarations. A new +# exemption requires the reviewer to name why it is not an API/code name. +PROSE_LABELS = { + "RTH_open", "last_bar_time", "last_traded_close", "next_period_open", + "mean_sub_bar_volume", "source_price_time", "precommit_pod", + "validation_typed_matrix", "pf_native_c_api", "pf_native_c_status", + "pf_native_c_types", "pf_native_c_enums", "currency_fx", "k_best", + "source_price_time", "Ambient", # Ambient*Scope is a wildcard pattern. +} + + +def source_paths(root: Path): + # Deliberately stay out of corpus and benchmark asset trees. + for folder in ("src", "include"): + base = root / folder + if base.exists(): + yield from (p for p in base.rglob("*") + if p.is_file() and p.suffix in SOURCE_SUFFIXES) + + +def comment_paths(root: Path): + include = root / "include/pineforge" + if include.exists(): + yield from (p for p in include.rglob("*") + if p.is_file() and p.suffix in SOURCE_SUFFIXES + and "source" not in p.relative_to(include).parts) + source = root / "src" + if source.exists(): + yield from (p for p in source.iterdir() + if p.is_file() and p.suffix in SOURCE_SUFFIXES) + + +def findings(root: Path) -> list[str]: + code_ids: set[str] = set() + artefact_names: set[str] = set() + for path in source_paths(root): + artefact_names.add(path.stem) + for code, _, literal in lex_lines(path.read_text(encoding="utf-8", errors="replace")): + code_ids.update(IDENT.findall(code)) + code_ids.update(IDENT.findall(literal)) + for folder in ("tests", "examples"): + base = root / folder + if base.exists(): + artefact_names.update(p.stem for p in base.rglob("*") + if p.is_file() and p.suffix in SOURCE_SUFFIXES) + scripts = root / "scripts" + if scripts.exists(): + artefact_names.update(p.stem for p in scripts.glob("*.py")) + artefact_names.update(p.stem for p in scripts.glob("*.sh")) + missing: list[tuple[Path, int, str]] = [] + for path in comment_paths(root): + relative = path.relative_to(root) + previous_comment = "" + for line_no, (_, comment, _) in enumerate( + lex_lines(path.read_text(encoding="utf-8", errors="replace")), 1): + historical = HISTORY.search(comment) or ( + previous_comment and HISTORY.search(previous_comment)) + previous_comment = comment + if not comment or historical: + continue + found_here: set[str] = set() + for match in CITED.finditer(comment): + if re.match(r"\.(?:cpp|hpp|h|c|cc)\b", comment[match.end():]): + continue # a file citation, judged by documentation gates + chunk = next(group for group in match.groups() if group) + if "*" in chunk: + continue # an intentionally incomplete name pattern + names = IDENT.findall(chunk) + for name in names: + if (name in found_here or name in code_ids + or name in artefact_names or name in PROSE_LABELS): + continue + if len(name) < 6 or name.startswith("_") or name.endswith("_"): + continue + if name.isupper(): + continue # prose enum examples and macro families + if "_" not in name and "::" not in chunk: + continue + found_here.add(name) + missing.append((relative, line_no, name)) + + # A few kernel comments cite a test helper or fixture field. Resolve only + # the still-missing names there, so the guard does not lex the whole test + # corpus on every ci_verify source-check invocation. + needed = {name for _, _, name in missing} + external_ids: set[str] = set() + for folder in ("tests", "examples"): + base = root / folder + if not base.exists() or not needed: + continue + paths = (p for p in base.rglob("*") + if p.is_file() and p.suffix in SOURCE_SUFFIXES) + for path in sorted(paths, key=lambda p: ("fixtures" in p.parts, str(p))): + if not needed: + break + raw = path.read_bytes() + if not any(name.encode() in raw for name in needed): + continue + for code, _, literal in lex_lines(raw.decode("utf-8", errors="replace")): + external_ids.update(IDENT.findall(code)) + external_ids.update(IDENT.findall(literal)) + needed.difference_update(external_ids) + return [f"{relative}:{line_no}: dangling comment name {name}" + for relative, line_no, name in missing if name not in external_ids] + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--root", type=Path, default=ROOT) + args = parser.parse_args() + result = findings(args.root) + for item in result: + print(item, file=sys.stderr) + print(f"dangling comment names: {len(result)} findings") + return 1 if result else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/check_doc_lint.py b/scripts/check_doc_lint.py index d8309ca8..505822fa 100644 --- a/scripts/check_doc_lint.py +++ b/scripts/check_doc_lint.py @@ -157,7 +157,7 @@ COUNT_PHRASINGS = ( ('native-c-api', re.compile(r'\b' + _N + r'\s+(?:additive|further)\s+(?:PF_API\s+)?' r'(?:symbols|functions)\b', re.I), None), - ('native-c-api', re.compile(r'\b' + _N + r'\s+strategy_native_\*\s+(?:symbols|functions)\b', + ('native-prefix', re.compile(r'\b' + _N + r'\s+strategy_native_\*\s+(?:symbols|functions)\b', re.I), None), ('native-c-api', re.compile(r'native_c_api\.h>?\s*(?:\([^)]*\))?\s*[—–]+\s*' + _N + r'\b', re.I), None), @@ -191,6 +191,7 @@ PARTIAL_COUNT_RE = re.compile(r'\b(?:new|more|another|further|extra|other)\s*$', re.I) COUNT_SOURCES = { 'native-c-api': 'include/pineforge/native_c_api.h', + 'native-prefix': 'include/pineforge/native_c_api.h', 'pineforge-h': 'include/pineforge/pineforge.h', 'runtime': 'scripts/check_c_abi_runtime.py', 'pf-api-total': 'include/pineforge/native_c_api.h + include/pineforge/pineforge.h', @@ -301,6 +302,9 @@ def read(rel: str) -> str | None: public = read('include/pineforge/pineforge.h') if native is not None: counts['native-c-api'] = len(re.findall(r'^PF_API\b', native, re.MULTILINE)) + counts['native-prefix'] = len(re.findall( + r'^PF_API[^\n]*\bstrategy_native_[A-Za-z0-9_]+\s*\(', + native, re.MULTILINE)) if public is not None: counts['pineforge-h'] = len(re.findall(r'^PF_API\b', public, re.MULTILINE)) if native is not None and public is not None: diff --git a/scripts/ci_preflight.py b/scripts/ci_preflight.py index d1c85fea..966049f3 100644 --- a/scripts/ci_preflight.py +++ b/scripts/ci_preflight.py @@ -188,6 +188,8 @@ def check_commands(source: Path) -> list[tuple]: ('native-tempdir-tests', [sys.executable, str(source / 'scripts/check_native_cpp_versions.py'), '--self-test-tempdir']), + ('dangling-comment-names-tests', + [sys.executable, str(source / 'scripts/test_check_dangling_comment_names.py')]), ('doc-anchors-tests', [sys.executable, str(source / 'scripts/test_check_doc_anchors.py')]), ('doc-anchors', diff --git a/scripts/ci_verify.py b/scripts/ci_verify.py index 001a221d..7b62035b 100644 --- a/scripts/ci_verify.py +++ b/scripts/ci_verify.py @@ -233,7 +233,9 @@ # +2 K-ULP4 test_native_unrepresentable_refusal, test_native_quantity_tolerance # All four register in release too. 276 registered, 275 run: the WebSocket row # still skips on a system libcurl. -KERNEL_MIN_TESTS = 275 +# DOC-TRUTH-4 adds the extracted Complete host (test_native_engine_complete_host): +# 277 registered, 276 run. +KERNEL_MIN_TESTS = 276 # 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 @@ -340,8 +342,8 @@ # 676 = those 674 plus the two R5 lane K-ULP4 rows KERNEL_MIN_TESTS lists # above, which register here too (ctest -N on the lane's tree); its C-language # checks are rows inside test_native_c_api. No release row skips, so 676 -# registered is 676 run. -RELEASE_MIN_TESTS = 676 +# registered is 676 run. DOC-TRUTH-4 adds the Complete host in release: 677. +RELEASE_MIN_TESTS = 677 # PR-only registration floors. These are the complete CTest populations at # 91d65ad6 (INT24); the ordinary full-run floors above remain unchanged. # An excluded run must still discover at least this many rows before -LE. @@ -376,6 +378,7 @@ ('source-guard-native-c-surface', ['scripts/check_native_c_api_surface.py']), ('source-guard-aggregate-versions', ['scripts/check_aggregate_cpp_versions.py']), ('source-guard-adapter-spec-shadowing', ['scripts/check_adapter_spec_shadowing.py']), + ('source-guard-dangling-comment-names', ['scripts/check_dangling_comment_names.py']), ) NATIVE_INCLUDE_INDEPENDENCE_PROFILES = frozenset(('release', 'native', 'kernel')) # The kernel-only archive must name no TradingView vocabulary outside ADR-0001's diff --git a/scripts/cxxlex.py b/scripts/cxxlex.py new file mode 100644 index 00000000..c59e7f44 --- /dev/null +++ b/scripts/cxxlex.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Split C/C++ lines into code, comment and literal text for source guards. +Handles // and /* */ comments, "..." strings, '.' chars, raw strings R"d(...)d", +digit separators (1'000), backslash-newline continuation of // comments.""" +IDENT = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_") + +def lex_lines(text): + lines = text.split("\n") + out = [] + state = "N" # N normal, LC line comment, BC block comment, S string, C char, R raw + raw_end = "" + for line in lines: + code, com, lit = [], [], [] + i, n = 0, len(line) + if state == "LC": # continued line comment (previous ended with backslash) + com.append(line); i = n + while i < n: + c = line[i] + if state == "N": + if line.startswith("//", i): + state = "LC"; com.append(line[i:]); i = n; break + if line.startswith("/*", i): + state = "BC"; com.append("/*"); i += 2; continue + if c == '"': + # raw string? + k = i - 1 + while k >= 0 and line[k] in IDENT: k -= 1 + prefix = line[k+1:i] + if prefix in ("R", "u8R", "uR", "UR", "LR"): + p = line.find("(", i+1) + delim = line[i+1:p] if p >= 0 else "" + raw_end = ")" + delim + '"' + state = "R"; i = p + 1 if p >= 0 else n; continue + state = "S"; i += 1; continue + if c == "'": + k = i - 1 + while k >= 0 and line[k] in IDENT: k -= 1 + if line[k+1:i][:1].isdigit(): + code.append(c); i += 1; continue # digit separator (1'000) + state = "C"; i += 1; continue + code.append(c); i += 1; continue + if state == "BC": + j = line.find("*/", i) + if j < 0: + com.append(line[i:]); i = n; break + com.append(line[i:j+2]); i = j + 2; state = "N"; continue + if state == "S": + if c == "\\": + lit.append(line[i:i+2]); i += 2; continue + if c == '"': + state = "N"; lit.append(" "); i += 1; continue + lit.append(c); i += 1; continue + if state == "C": + if c == "\\": + i += 2; continue + if c == "'": + state = "N"; i += 1; continue + i += 1; continue + if state == "R": + j = line.find(raw_end, i) + if j < 0: + lit.append(line[i:]); i = n; break + lit.append(line[i:j]); i = j + len(raw_end); state = "N"; continue + # end of line + if state == "LC": + state = "LC" if line.endswith("\\") else "N" + if state in ("S", "C"): + state = "N" # unterminated on this line: be forgiving + out.append(("".join(code), " ".join(com), "".join(lit))) + return out diff --git a/scripts/test_check_dangling_comment_names.py b/scripts/test_check_dangling_comment_names.py new file mode 100644 index 00000000..38e4caff --- /dev/null +++ b/scripts/test_check_dangling_comment_names.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +"""Must-fail mutations for the dangling comment-name guard.""" +from pathlib import Path +import tempfile + +from check_dangling_comment_names import findings + + +def main() -> int: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + header = root / "include/pineforge/sample.hpp" + header.parent.mkdir(parents=True) + header.write_text("// phantom_broker_step() settles this request.\n" + "int live_broker_step();\n", encoding="utf-8") + bad = findings(root) + assert any("phantom_broker_step" in item for item in bad), bad + header.write_text("// OrderType::EXIT is the fill branch.\n", + encoding="utf-8") + bad = findings(root) + assert any("OrderType" in item for item in bad), bad + header.write_text("// live_broker_step() settles this request.\n" + "int live_broker_step();\n", encoding="utf-8") + assert findings(root) == [], findings(root) + header.write_text("// historical: phantom_broker_step() was removed.\n", + encoding="utf-8") + assert findings(root) == [], findings(root) + print("dangling comment-name guard: must-fail mutation detected") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/test_check_doc_lint.py b/scripts/test_check_doc_lint.py index 3d30f68b..fe3be2af 100644 --- a/scripts/test_check_doc_lint.py +++ b/scripts/test_check_doc_lint.py @@ -95,7 +95,7 @@ def tree(**files: str): PF_API int strategy_native_a_v1(void); PF_API int strategy_native_b_v1(void); /* PF_API in a comment is not a declaration */ -PF_API void strategy_native_c_v1(void); +PF_API void strategy_configure_native_ext_v1(void); """ PINEFORGE_H = """#pragma once PF_API int pf_abi_version(void); @@ -248,6 +248,14 @@ def test_the_total_across_two_headers(self) -> None: self.assertEqual(code, 1, out) self.assertIn('derives 5', out) + def test_native_prefix_is_not_the_whole_header(self) -> None: + with tree(**counted('The 2 `strategy_native_*` functions are declared there.\n')) as t: + self.assertEqual(t.run()[0], 0) + with tree(**counted('The 3 `strategy_native_*` functions are declared there.\n')) as t: + code, out = t.run() + self.assertEqual(code, 1, out) + self.assertIn('derives 2', out) + def test_the_public_and_runtime_counts(self) -> None: body = 'It declares exactly 65 public `PF_API` functions: 57 runtime implementations.\n' with tree(**counted(body)) as t: @@ -471,7 +479,8 @@ class Derivation(unittest.TestCase): def test_counts_read_their_sources(self) -> None: with tree(**counted('x\n')) as t: counts = guard.derived_counts(t.root) - self.assertEqual(counts, {'native-c-api': 3, 'pineforge-h': 2, 'runtime': 1, + self.assertEqual(counts, {'native-c-api': 3, 'native-prefix': 2, + 'pineforge-h': 2, 'runtime': 1, 'pf-api-total': 5, 'kernel-floor': 202, 'release-floor': 572, 'examples': 3, 'examples-cpp': 1, 'examples-c': 2}) diff --git a/src/c_abi.cpp b/src/c_abi.cpp index fa0470c1..b32e5d55 100644 --- a/src/c_abi.cpp +++ b/src/c_abi.cpp @@ -310,7 +310,7 @@ extern "C" { * failed closed on the identity gate and the verifier ladder rejected the * TV-identical trim candidate for ena-grid (XAUUSD 1D), even though the * engine's per-lot range-end rows themselves matched TV (xau-grid 6/6, - * silicon 9/9 rows). The row is built by build_close_trade from the open + * silicon 9/9 rows). The row is built from the open * pyramid lot, so it carries that lot's entry_incarnation like any other * close. No struct changed by this accessor (PF_ABI_VERSION is 4 since the native lowering). */ PF_API uint64_t strategy_closed_trade_entry_incarnation( diff --git a/src/engine_internal.hpp b/src/engine_internal.hpp index 650d54b4..ddb8852f 100644 --- a/src/engine_internal.hpp +++ b/src/engine_internal.hpp @@ -82,7 +82,7 @@ bool first_touch_position(const Bar& bar, double level, double* out_pos); // design-stop-tick-rounding: the path helpers below also come in a form whose // LEG ORDER (O->H->L->C vs O->L->H->C) is chosen by the caller. The engine -// walks the tick-quantized twin of a bar (BacktestEngine::broker_trigger_bar) +// walks the tick-quantized twin of a bar // with the RAW bar's order, so a path coordinate produced here — the entry // cursor a same-bar bracket resumes from, a sibling / opposing-order // tie-break — lives in the same coordinate system as the matcher's own walk diff --git a/src/native_execution_consumer.cpp b/src/native_execution_consumer.cpp index 409eb4bf..640346ad 100644 --- a/src/native_execution_consumer.cpp +++ b/src/native_execution_consumer.cpp @@ -613,8 +613,8 @@ std::optional representable_units(double units, // fee reserve. // // NativeRunSpec::fee_value is a PERCENT for NativeFeeKind::Percent -// (native_run_spec.hpp:560): the charge is fee_value / 100 of the account -// notional (engine.hpp calc_commission). The reserve is the exact inverse of +// (native_run_spec.hpp:589): the charge is fee_value / 100 of the account +// notional. The reserve is the exact inverse of // that charge, so it divides by 1 + fee_value / 100 and a 0.1 % fee reserves // 0.1 %, not 10 %. std::optional sized_basis_units(const native_order::Sized& sized, double price, diff --git a/src/source/pine_adapter.cpp b/src/source/pine_adapter.cpp index 0201f9e4..a818e11e 100644 --- a/src/source/pine_adapter.cpp +++ b/src/source/pine_adapter.cpp @@ -2888,9 +2888,9 @@ bool PineExecutionAdapter::lifecycle_readable(const PlacementSnapshot& row) cons // and the pair hold rewrite lifecycles, and exit() asks whether an // opened origin's leg was consumed; // K2 a current-cycle row naming a revival candidate as its predecessor, -// K3 the first current-cycle leg re-issuing a revival candidate at a later -// bar at another stop: the revival's superseded test asks whether ANY -// such re-issue exists, and a row's family, ids, placement bar and stop +// K3 the first later-bar current-cycle exit of the same family, source id +// and from-entry id with different stop bits: the revival's superseded +// test asks whether ANY such re-issue exists, and a row's family, ids, placement bar and stop // never change, so the lowest-incarnation one answers for every later // one (pinning them all kept two rows per exit per bar through a flat // cycle that re-issues its exits every bar, each walked at every bar @@ -2910,8 +2910,8 @@ bool PineExecutionAdapter::lifecycle_readable(const PlacementSnapshot& row) cons // // R5 lane V19-D: nor does K1 keep a leg of the current cycle bound to no // askable origin once the revival's superseded test answers for it -- some -// row of the cycle names it (or its target) as its predecessor, or re-issues -// it at a later bar at another stop. K2 and K3 keep one such row for as long +// row of the cycle names it (or its target) as its predecessor, or a later-bar +// exit of the same family and ids has a different stop. K2 and K3 keep one such row for as long // as the leg is a candidate, so a leg superseded now stays superseded for the // rest of its cycle: the revival never picks it again, and the suspension and // the pair hold only rewrite its own lifecycle, which nothing else reads. A diff --git a/src/source/pine_reissue_binding.hpp b/src/source/pine_reissue_binding.hpp index 9d71402a..1fb51aff 100644 --- a/src/source/pine_reissue_binding.hpp +++ b/src/source/pine_reissue_binding.hpp @@ -7,7 +7,8 @@ // exit leg bound to the position keeps its binding across the re-issue and the // kernel records no CloseBoundEvent for it at its next point (the kernel binds // it exactly as a plain replace would whenever the position has moved). The -// adapter witness (tests/test_adapter_reissue_binding.cpp) runs every script +// adapter witness (tests/test_adapter_live_state_equivalence.cpp, +// --reissue-binding) runs every script // twice -- carrying, and with every re-issue a plain replace as before V19-D -- // and holds the two runs' transcripts equal bar for bar, CloseBoundEvents and // the ordinals they shift aside. The shipped library never clears the switch. diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 87662d8a..d98f8f1e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -2257,6 +2257,30 @@ if(PINEFORGE_ENABLE_COVERAGE) endif() add_test(NAME test_pine_to_native_worked COMMAND test_pine_to_native_worked) +# DOC-TRUTH-4: compile the guide's Complete host block verbatim. The CTest +# wrapper also checks that the built source still matches the live page. +set(_pf_ne_page ${PROJECT_SOURCE_DIR}/docs/pages/native-engine.md) +set(_pf_ne_complete ${CMAKE_CURRENT_BINARY_DIR}/native_engine_complete_host.cpp) +add_custom_command( + OUTPUT ${_pf_ne_complete} + COMMAND ${Python3_EXECUTABLE} -B + ${CMAKE_CURRENT_SOURCE_DIR}/extract_native_engine_complete_host.py + --page ${_pf_ne_page} --out ${_pf_ne_complete} + DEPENDS ${_pf_ne_page} ${CMAKE_CURRENT_SOURCE_DIR}/extract_native_engine_complete_host.py + COMMENT "Extracting Complete host from docs/pages/native-engine.md" + VERBATIM) +add_executable(test_native_engine_complete_host ${_pf_ne_complete}) +target_link_libraries(test_native_engine_complete_host PRIVATE PineForge::kernel) +target_compile_options(test_native_engine_complete_host PRIVATE -ffp-contract=off -UNDEBUG) +if(PINEFORGE_ENABLE_COVERAGE) + target_compile_options(test_native_engine_complete_host PRIVATE ${_pf_cov_test_flags}) +endif() +add_test(NAME test_native_engine_complete_host + COMMAND ${Python3_EXECUTABLE} -B + ${CMAKE_CURRENT_SOURCE_DIR}/extract_native_engine_complete_host.py + --page ${_pf_ne_page} --out ${_pf_ne_complete} + --run $) + # Independent native vertical contract fixture identity. file(SHA256 "${CMAKE_CURRENT_SOURCE_DIR}/test_native_market_vertical_contract.cpp" PINEFORGE_NATIVE_MARKET_VERTICAL_CONTRACT_SHA256) diff --git a/tests/extract_native_engine_complete_host.py b/tests/extract_native_engine_complete_host.py new file mode 100644 index 00000000..74ab7d2d --- /dev/null +++ b/tests/extract_native_engine_complete_host.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +"""Build and run the Complete host directly from native-engine.md. + +The CTest command compares the generated translation unit with the live page +before executing it, so ctest cannot silently run a binary from an older page. +""" +from __future__ import annotations + +import argparse +from pathlib import Path +import subprocess +import sys + + +def extracted(page: Path) -> str: + lines = page.read_text(encoding="utf-8").splitlines() + headings = [i for i, line in enumerate(lines) if line == "## C++ example"] + if len(headings) != 1: + raise ValueError(f"expected one C++ example section, found {len(headings)}") + start = headings[0] + end = next((i for i in range(start + 1, len(lines)) + if lines[i].startswith("## ")), len(lines)) + intros = [i for i in range(start + 1, end) + if lines[i].startswith("Complete host:")] + if len(intros) != 1: + raise ValueError(f"expected one Complete host introduction, found {len(intros)}") + fences = [i for i in range(intros[0] + 1, end) if lines[i] == "```cpp"] + if len(fences) != 1: + raise ValueError(f"expected one C++ block after Complete host, found {len(fences)}") + first = fences[0] + 1 + last = next((i for i in range(first, end) if lines[i] == "```"), None) + if last is None: + raise ValueError("Complete host block has no closing fence") + body = "\n".join(lines[first:last]) + "\n" + if "int main()" not in body or "native_events(0)" not in body: + raise ValueError("Complete host no longer has its runnable event check") + return f'#line {first + 1} "docs/pages/native-engine.md"\n' + body + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--page", type=Path, required=True) + parser.add_argument("--out", type=Path, required=True) + parser.add_argument("--run", type=Path) + args = parser.parse_args() + try: + source = extracted(args.page) + if args.run is None: + args.out.parent.mkdir(parents=True, exist_ok=True) + args.out.write_text(source, encoding="utf-8") + return 0 + if not args.out.exists() or args.out.read_text(encoding="utf-8") != source: + raise ValueError("Complete host page changed after build; rebuild its target") + return subprocess.run([str(args.run)], timeout=120, check=False).returncode + except (OSError, ValueError, subprocess.TimeoutExpired) as error: + print(f"Complete host extraction: {error}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_pine_to_native_doc_rows.cpp b/tests/test_pine_to_native_doc_rows.cpp deleted file mode 100644 index 28a5a520..00000000 --- a/tests/test_pine_to_native_doc_rows.cpp +++ /dev/null @@ -1,37 +0,0 @@ -// Documentation witness for D-A, D-K and D-L. -// -// expectation corrected: the old migration rows treated trail_points as the -// ride offset, treated C loss fields as signed, and counted report range-end -// rows as Pine closed trades. These assertions pin the corrected translations -// against the public types without changing engine behaviour. -#include -#include - -#include -#include - -int main() { - constexpr double entry = 101.0; - constexpr double tick = 0.25; - constexpr double arm_ticks = 30.0; - constexpr double offset_ticks = 8.0; - const double arm = entry + arm_ticks * tick; - const double ride = offset_ticks * tick; - const double stop = arm - ride; - assert(std::fabs(arm - 108.5) < 1e-12); - assert(std::fabs(ride - 2.0) < 1e-12); - assert(std::fabs(stop - 106.5) < 1e-12); - - pf_trade_stats_t c_stats{}; - c_stats.gross_loss = 10.0; // C report: positive magnitude. - const double protected_signed_loss = -c_stats.gross_loss; - assert(protected_signed_loss == -10.0); - - pf_trade_t rows[2]{}; - rows[0].open_at_end = 0; - rows[1].open_at_end = 1; // report-only range-end row. - int pine_closed = 0; - for (const auto& row : rows) pine_closed += row.open_at_end == 0; - assert(pine_closed == 1); - return 0; -} diff --git a/tests/test_pine_to_native_worked.cpp b/tests/test_pine_to_native_worked.cpp index 8e4e91ae..8a87bb5e 100644 --- a/tests/test_pine_to_native_worked.cpp +++ b/tests/test_pine_to_native_worked.cpp @@ -16,9 +16,10 @@ // * no request is refused, at submission or at a match; // * the entry is Sized by cash with the fee reserved, so it buys // default_qty_value / (price * (1 + commission_value / 100)) units; -// * the take-profit leg ("tp", +profit ticks) closes it, and its two -// siblings ("sl", "trail") are withdrawn with it: the lots they close are -// gone, so the kernel cancels them as CancelReason::OwnerGone; +// * three tapes close by take-profit, trail and stop respectively; the +// take-profit's siblings are withdrawn as CancelReason::OwnerGone; +// * armed levels use the Pine block's profit, loss, trail_points and +// trail_offset values as tick counts on the declared price grid; // * the row's commission is commission_value percent of both notionals. // // The binary re-reads the page and refuses to run when the page changed @@ -99,6 +100,87 @@ std::vector tape() { return bars; } +std::vector variant_tape(bool trailing) { + std::vector bars = tape(); + bars.resize(5); // Keep the same first hourly close and entry signal. + const auto add = [&bars](double open, double high, double low, double close) { + bars.push_back({open, high, low, close, 1000.0, + bars.back().timestamp + 15LL * 60 * 1000}); + }; + if (trailing) { + add(101.0, 105.0, 100.75, 105.0); + add(105.0, 109.5, 105.0, 109.5); + add(109.5, 110.5, 109.5, 110.5); + add(110.5, 110.5, 107.0, 107.0); + add(107.0, 107.0, 107.0, 107.0); + } else { + add(101.0, 101.5, 95.5, 96.0); + add(96.0, 96.0, 96.0, 96.0); + } + return bars; +} + +void check_variant(const pineforge::NativeRunSpec& spec, bool trailing) { + WorkedMigration host; + const auto setup = host.configure_native(spec); + check(setup.status == pineforge::NativeSetupStatus::Applied, + "variant configures the page's extracted spec"); + if (setup.status != pineforge::NativeSetupStatus::Applied) return; + const auto bars = variant_tape(trailing); + host.run(bars.data(), static_cast(bars.size())); + check(host.native_state().kind == pineforge::NativeLifecycleKind::Completed, + "variant run completes"); + + std::optional take_profit, stop_loss, trail_arm, trail_offset; + int rejected = 0; + namespace no = pineforge::native_order; + for (const auto& event : host.native_events(0)) { + if (!event.command) continue; + rejected += std::holds_alternative(*event.command) + || std::holds_alternative(*event.command); + if (const auto* armed = std::get_if(&*event.command)) { + if (!armed->definition) continue; + const auto& trigger = armed->definition->request.trigger; + if (const auto* limit = std::get_if(&trigger)) + take_profit = limit->price; + if (const auto* stop = std::get_if(&trigger)) + stop_loss = stop->price; + if (const auto* trail = std::get_if(&trigger)) { + trail_arm = trail->arm_price; + trail_offset = trail->offset; + } + } + } + const double fill = kSignalClose; + check(rejected == 0, "variant has no refused request"); + check(take_profit && near(*take_profit, + fill + PF_P2N_PINE_PROFIT_TICKS * spec.price_tick), + "take-profit arms at fill plus Pine profit ticks"); + check(stop_loss && near(*stop_loss, + fill - PF_P2N_PINE_LOSS_TICKS * spec.price_tick), + "stop arms at fill minus Pine loss ticks"); + check(trail_arm && near(*trail_arm, + fill + PF_P2N_PINE_TRAIL_POINTS_TICKS * spec.price_tick), + "trail arms at fill plus Pine trail_points ticks"); + check(trail_offset && near(*trail_offset, + PF_P2N_PINE_TRAIL_OFFSET_TICKS * spec.price_tick), + "trail rides Pine trail_offset ticks"); + check(host.trade_count() == 1, "variant closes exactly one trade"); + if (host.trade_count() != 1) return; + const auto& trade = host.get_trade(0); + const double expected_exit = trailing + ? 110.5 - PF_P2N_PINE_TRAIL_OFFSET_TICKS * spec.price_tick + : fill - PF_P2N_PINE_LOSS_TICKS * spec.price_tick; + std::printf("%s tape: entry=%.4f exit=%.4f exit_id=%s; armed tp=%.4f sl=%.4f trail=%.4f offset=%.4f\n", + trailing ? "trail" : "stop", trade.entry_price, trade.exit_price, + trade.exit_id.c_str(), take_profit.value_or(-1), stop_loss.value_or(-1), + trail_arm.value_or(-1), trail_offset.value_or(-1)); + check(trade.exit_id == (trailing ? "trail" : "sl") + && near(trade.exit_price, expected_exit), + trailing ? "trail closes at running best minus Pine offset ticks" + : "stop closes at fill minus Pine loss ticks"); +} + } // namespace int main() { @@ -181,6 +263,9 @@ int main() { "commission == commission_value percent of both notionals"); } + check_variant(spec, true); + check_variant(spec, false); + std::printf("pine-to-native worked migration: %s\n", failures == 0 ? "every check passed" : "checks failed"); return failures == 0 ? 0 : 1; From 4c029334e7c95d40f72d6ce8dd424cccb5b937bf Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Fri, 25 Sep 2026 22:02:17 +0800 Subject: [PATCH 17/24] docs: finish native contract truth and report caveats (cherry picked from commit 21bdc2e8f68b081322f0517bccd0c81176a6b3c7) INT25 merge, DOC-TRUTH-4 over wave G's code lanes (both picks): - strategy_configure_native_v1 (pineforge.h, native-engine.md) keeps C-SURFACE-1's refusal contract (a Running handle is refused at the C boundary without mutation, a Completed or aborted handle may be reused) and adds this lane's facts: an aborted handle is reused with the same session key and a higher run number, and a handle whose configure call failed remains Failed. - The run-spec extension has six published layouts since K-ULP4: README keeps K-ULP4's six-layout sentence, and this lane's "five" in the RISK_SIZE / POLICY_SIZE comments (native_c_api.h) and in native-engine.md's POLICY_SIZE paragraph ("the other four") read six / "the other five". - The design page's row 8 keeps C-SURFACE-1's text; its c_abi.cpp:289-929 content pin is re-hashed over both lanes' edits (the window moved only by this lane's one-line comment fix at :313). - The floors add the extracted Complete host (kernel 276, release 677). The pages are re-anchored once for the lane (9 citations through the exact line map from 3ed0276f or 21bdc2e8, the floors' citations set by hand). --- CONTRIBUTING.md | 4 +- README.md | 6 +- docs/adr/0001-kernel-adapter-boundary.md | 8 +- docs/design/native-feature-parity.md | 22 +++--- docs/pages/contributing-llm.md | 6 +- docs/pages/index.md | 5 +- docs/pages/live-surface.md | 37 +++++---- docs/pages/native-engine.md | 24 +++--- docs/pages/pine-to-native.md | 84 ++++++++++----------- docs/pages/report-schema.md | 15 ++-- include/pineforge/engine.hpp | 11 +-- include/pineforge/native_c_api.h | 4 +- include/pineforge/pineforge.h | 16 ++-- tests/test_native_handle_stable_replace.cpp | 5 +- 14 files changed, 133 insertions(+), 114 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8b3e827e..9c461f3b 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:236 and `RELEASE_MIN_TESTS` -ci_verify.py:344. +floor — `KERNEL_MIN_TESTS` ci_verify.py:238 and `RELEASE_MIN_TESTS` +ci_verify.py:346. 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 10289a67..32e940bd 100644 --- a/README.md +++ b/README.md @@ -450,12 +450,12 @@ inventories are pinned by `scripts/check_c_abi_runtime.py`: | `strategy_set_account_currency_fx_series` | Effective-time quote-to-account FX | | `strategy_get_last_error` | The latest runtime error | | `pf_version_get` / `pf_version_string` / `pf_abi_version` | Runtime version, version string, struct-layout version (`PF_ABI_VERSION == 4`) | -| `strategy_execution_contract` / `strategy_configure_native_v1` / `strategy_configure_native_fx_curve_v1` / `_fx_curve_ext_v1` | Query Legacy vs NativeMarketV1; apply the versioned native run specification (a refused one returns `-1` and leaves the handle Failed); stage or clear an immutable native FX curve, `_fx_curve_ext_v1` also writing the typed refusal (`pf_native_fx_curve_error_e`) and the offending point's index | +| `strategy_execution_contract` / `strategy_configure_native_v1` / `strategy_configure_native_fx_curve_v1` / `_fx_curve_ext_v1` | Query Legacy vs NativeMarketV1; apply the versioned native run specification (an invalid or refused base call returns `-1` and leaves the handle Failed; a cooperative `Aborted` handle may be reused with the same key and a higher run number); stage or clear an immutable native FX curve, `_fx_curve_ext_v1` also writing the typed refusal (`pf_native_fx_curve_error_e`) and the offending point's index | | `strategy_request_abort` / `strategy_last_run_status` | Cooperative abort of a run in progress; `0`=completed, `1`=aborted | | `strategy_set_realtime_tail` | Live-runtime surface (ABI v4): the array's last bar is a still-forming tail — `barstate.islast=false`, `last_bar_index`/`last_bar_time` frozen at the horizon bar, no range-end row | | `strategy_set_probe_suppress_tail_logic` | ABI v4: the last bar runs only the broker's pre-`on_bar` steps (pending-order settlement, intraday-cap/loss checks) and returns — no `on_bar`, no margin-call / POOC second pass / bracket-reissue processing (the range-end row is `strategy_set_realtime_tail`'s to skip; the flags are independent) | | `strategy_set_path_order` / `strategy_last_bar_dual_entry_path` | ABI v4: force the intrabar O→H/L→C leg order (`AUTO`/`HIGH_FIRST`/`LOW_FIRST`) for path-dependent fill probing; read which side won a same-bar dual-entry-stop arbitration | -| `strategy_set_broker_state_hash_recording` / `strategy_broker_state_hash` | ABI v4: toggle a 64-bit broker-state hash appended per script bar to `pf_report_t::broker_state_hash`; read the final state's hash | +| `strategy_set_broker_state_hash_recording` / `strategy_broker_state_hash` | ABI v4: toggle a 64-bit broker-state hash appended at report points to `pf_report_t::broker_state_hash` (a bare host uses `KernelRecorded`); read the final state's hash | | `strategy_pending_orders_len` / `strategy_pending_order_get` / `strategy_pending_order_layout` | ABI v4: the resting pending-order book after the most recent run — count, a POD snapshot per order (`pf_pending_order_v1_t`), and the snapshot's self-describing field layout | | `strategy_pending_order_fill_qty` / `_level_resolved` / `_effective_levels` / `strategy_trail_best_price` | ABI v4: engine-computed values for a resting order — the quantity it would open if filled at a given price, whether its relative offsets resolve yet, its resolved stop/limit/trail-activation levels, and the live position's trail extreme | | `strategy_position_avg_price` / `strategy_position_cycle_seq` / `strategy_position_size` | ABI v4: the live position's volume-weighted average entry price, its cycle id, and its script-facing signed size | @@ -524,7 +524,7 @@ needs no new symbol: the `strategy_stream_*` family takes these handles unchanged. Worked example: [`examples/native/hello_kernel_c.c`](examples/native/hello_kernel_c.c); reference: [`docs/pages/native-engine.md`](docs/pages/native-engine.md). -POD types `pf_bar_t`, `pf_trade_tick_t`, `pf_trade_t`, `pf_report_t`, `pf_security_diag_t`, `pf_trace_entry_t`, `pf_version_t`, `pf_trade_stats_t`, `pf_equity_stats_t`, `pf_metrics_t`, `pf_equity_point_t`, `pf_pending_order_v1_t`, `pf_field_desc_t` and the `pf_magnifier_distribution_t` enum complete the surface. ABI v2 added computed trading metrics and a per-bar equity curve; ABI v3 added `pf_trade_t::open_at_end`, TradingView's range-end close of a position still open after the last bar; ABI v4 added the live-runtime accessors above plus `pf_report_t::broker_state_hash` / `broker_state_hash_len` (a per-script-bar broker-state hash array, appended after `equity_curve_len`, NULL/0-length unless `strategy_set_broker_state_hash_recording` is on) and the `pf_pending_order_v1_t` generated POD mirror of the engine's resting-order record. Check `pf_abi_version()` before running: the report struct is caller-allocated. +POD types `pf_bar_t`, `pf_trade_tick_t`, `pf_trade_t`, `pf_report_t`, `pf_security_diag_t`, `pf_trace_entry_t`, `pf_version_t`, `pf_trade_stats_t`, `pf_equity_stats_t`, `pf_metrics_t`, `pf_equity_point_t`, `pf_pending_order_v1_t`, `pf_field_desc_t` and the `pf_magnifier_distribution_t` enum complete the surface. ABI v2 added computed trading metrics and a per-bar equity curve; ABI v3 added `pf_trade_t::open_at_end`, TradingView's range-end close of a position still open after the last bar; ABI v4 added the live-runtime accessors above plus `pf_report_t::broker_state_hash` / `broker_state_hash_len` (a per-report-point broker-state hash array, appended after `equity_curve_len`, NULL/0-length unless recording is on and a report point exists) and the `pf_pending_order_v1_t` generated POD mirror of the engine's resting-order record. Check `pf_abi_version()` before running: the report struct is caller-allocated. Full flag semantics, string lifetimes and the three L0 evidence lanes behind the ABI v4 live surface: [`docs/pages/live-surface.md`](docs/pages/live-surface.md). diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index 0108fdf3..d2599168 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -346,7 +346,7 @@ citations, 264 are in `src/source/` and its headers; 6 sit in kernel files 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:629`), which is why a C host frees its report + 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 @@ -609,10 +609,10 @@ number names its symbol on this tree. | `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:1681`) 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:965`) 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:1059`) | **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_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_fx_curve_v1` (`pineforge.h:582`) | **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_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:1867`) | **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`. | diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 14bae3c1..c78e809d 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:1852); 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:999 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:1243 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:660-662 | 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: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 | ### 1.2 Trailing @@ -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 `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:1350, 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 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: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. | | 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 @@ -173,9 +173,9 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | 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 | — | | 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:2157, `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:546 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 43 native C 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. | +| 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) | +| 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 | @@ -213,7 +213,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | 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 | | 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:1271 | 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: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) | @@ -267,7 +267,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 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 | -| 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:546 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:907) | F S | Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the epoch batch. @@ -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:2926-2993) 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:2934-3002) 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). @@ -320,7 +320,7 @@ Union of F §2.vi (9), O §2.vi (5 + 1) and S §2.6 (7), deduplicated. S had no | 5 | "`engine_aux_security.cpp` includes `source/pine_strategy_host.hpp`" (ADR L129-132) | False at `73817c1`: its includes are `engine_internal.hpp`, `ta.hpp` and std headers (*fresh* re-read). 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 | | 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:4fa627f4c74950d70948186329259be7ebb753c8c50d91c16b838ba01c540a6a` c_abi.cpp:289-929, `sha256:4b7e398c78b276b34c4247e8271242b48aa195cea29126f4e809073f2665e1ea` check_c_abi_runtime.py:19-79, c_abi.cpp:848, `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:902 | F O S | +| 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:848, `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:902 | 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 | | 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) | @@ -1227,7 +1227,7 @@ unchanged after it. | 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 | | 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:423-437) | 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 (`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 | | 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 | @@ -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:1618-1619) → 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: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). 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 68f1b26f..a58a8d2b 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:236 and - `RELEASE_MIN_TESTS` ci_verify.py:344. Adding rows means raising the floor + *ran* against a floor: `KERNEL_MIN_TESTS` ci_verify.py:238 and + `RELEASE_MIN_TESTS` ci_verify.py:346. 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:236, `RELEASE_MIN_TESTS` ci_verify.py:344). It +(`KERNEL_MIN_TESTS` ci_verify.py:238, `RELEASE_MIN_TESTS` ci_verify.py:346). 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/index.md b/docs/pages/index.md index d594aa80..6d115a50 100644 --- a/docs/pages/index.md +++ b/docs/pages/index.md @@ -152,10 +152,11 @@ The public C surface is **109 `PF_API` declarations** across two headers: | Diagnostics | `strategy_get_last_error` | #strategy_get_last_error | | Version | `pf_version_get`, `pf_abi_version`, `pf_version_string` | @ref pf_version | | Types | `pf_bar_t`, `pf_trade_tick_t`, `pf_trade_t`, `pf_report_t`, metrics, diagnostics, trace, equity, version, and `pf_magnifier_distribution_t` | @ref pf_types | -| Native kernel host (C) | `strategy_native_host_create_v1`, `strategy_native_run_v1`, the submit / replace / cancel family, the position, working-book, open-lot, event and state reads, the cohort and subscription calls, and `strategy_configure_native_ext_v1` | `native_c_api.h` | +| Native kernel host (C) | `strategy_native_host_create_v1`, `strategy_native_run_v1`, the submit / replace / cancel family, the position, working-book, open-lot, event and state reads, the cohort and subscription calls, and `strategy_configure_native_ext_v1` / `_result_v1` | `native_c_api.h` | A built PineForge strategy `.so` exports 109 `PF_API` symbols (66 compiled-strategy -declarations plus 43 native-host declarations) and the toolchain +declarations plus 41 `strategy_native_*` declarations and two +`strategy_configure_native_ext_*` declarations) and the toolchain `std::piecewise_construct` symbol; no project-internal C++ symbol is exported — see **[ABI stability](@ref abi_stability)** for the full guarantee. diff --git a/docs/pages/live-surface.md b/docs/pages/live-surface.md index fe4b7450..8e1a25d1 100644 --- a/docs/pages/live-surface.md +++ b/docs/pages/live-surface.md @@ -28,7 +28,7 @@ these recomputation controls. | `strategy_set_probe_suppress_tail_logic` | Run only the broker's pre-`on_bar` steps on the last bar and stop (§3.2 below). | Off (`on == 0`). | No | | `strategy_set_path_order` | Force the intrabar leg order used by every OHLC-path helper (§3.3 below). | `0` AUTO (the unchanged TV-emulator "nearer-to-open" rule). | No | | `strategy_last_bar_dual_entry_path` | Read which side won a same-bar dual-entry-stop arbitration on the last dispatched bar (`0` none, `1` long-first, `2` short-first, `-1` if `s` is `NULL`). | N/A — read-only per-bar snapshot. | No | -| `strategy_set_broker_state_hash_recording` | Toggle a 64-bit broker-state hash appended to `pf_report_t::broker_state_hash` after every dispatched script bar (§3.4 below). | Off (`on == 0`); `broker_state_hash` stays `NULL`/0-length. | No | +| `strategy_set_broker_state_hash_recording` | Toggle a 64-bit broker-state hash appended to `pf_report_t::broker_state_hash` after each report point (§3.4 below). A bare native host gets points under `KernelRecorded`. | Off (`on == 0`); `broker_state_hash` stays `NULL`/0-length. | No | | `strategy_broker_state_hash` | The final broker-state hash of the most recent `run()`, whether or not recording was on. `0` if `s` is `NULL`. | N/A — read-only. | No | | `strategy_pending_orders_len` | Count of orders resting in the pending-order book after the most recent `run()` — the book in force for the next bar. `0` if `s` is `NULL`. | N/A — read-only. | No | | `strategy_pending_order_get` | Copy the *i*-th resting order into a `pf_pending_order_v1_t` snapshot (see [The pending-order mirror](#live_surface_pending_mirror)). | N/A — read-only. | No | @@ -223,15 +223,19 @@ before this flag existed. ### §3.4 — `strategy_set_broker_state_hash_recording(s, on)` -When `on` is non-zero, every subsequent `run()` appends -`strategy_broker_state_hash`'s value to `pf_report_t::broker_state_hash` -immediately after each script bar is dispatched, so the array's length -matches `pf_report_t::script_bars_processed`. Cleared (the recorded array -emptied, not the flag) at the start of every `run()`; the flag is -persistent configuration, like `strategy_set_realtime_tail`. Also covers a -stream's warmup `run()` and every script bar the realtime tick stream -dispatches afterward — set this **before** `strategy_stream_begin` to also -record the warmup leg. +When `on` is non-zero, every subsequent report point appends +`strategy_broker_state_hash`'s value to `pf_report_t::broker_state_hash`. +For a bare native host, report points come from +`NativeReportPolicy::KernelRecorded`; a host using `HostRecorded` must append +its own report rows and does not get kernel rows. On a completed run the +recorded length matches the number of report points, and under +`KernelRecorded` it matches `pf_report_t::script_bars_processed`. A Failed or +Aborted run can expose a shorter prefix, whose last row need not equal the +final scalar. Cleared (the recorded array emptied, not the flag) at the start +of every `run()`; the flag is persistent configuration, like +`strategy_set_realtime_tail`. Also covers a stream's warmup `run()` and every +report point the realtime tick stream dispatches afterward — set this +**before** `strategy_stream_begin` to also record the warmup leg. Default off (`on == 0`): `pf_report_t::broker_state_hash` is `NULL`/ 0-length and every historical run stays byte-identical to before this flag @@ -283,16 +287,17 @@ ABI v4 appends two fields to `pf_report_t`, directly after `equity_curve_len` (append-only, per the struct's stability guarantee): ```c -uint64_t* broker_state_hash; /* per-script-bar hash, ABI v4 */ +uint64_t* broker_state_hash; /* per-report-point hash, ABI v4 */ int64_t broker_state_hash_len; /* length of broker_state_hash */ ``` `NULL` / 0-length when `strategy_set_broker_state_hash_recording` was off -(the default) or no script bars were dispatched. When populated, -`broker_state_hash_len == script_bars_processed` and the last element -equals `strategy_broker_state_hash`'s value at the end of the run. Heap -array, freed by `report_free` — same ownership rule as `trades` / -`equity_curve` / `trace`. +(the default), no report points were recorded, or a bare host kept +`HostRecorded`. When populated on a completed `KernelRecorded` run, +`broker_state_hash_len == script_bars_processed` and the last element equals +`strategy_broker_state_hash`'s value at the end of the run. A Failed or +Aborted run can have a shorter prefix. Heap array, freed by `report_free` — +same ownership rule as `trades` / `equity_curve` / `trace`. ## The pending-order mirror {#live_surface_pending_mirror} diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 2fc509f1..4561c242 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -115,7 +115,9 @@ Configure: setup. - `Completed` → same session key, `run_number` above high-water → `Ready`. Prior-run records stay inspectable until the next successful begin. -- `Running` or `Failed` → refused; `Failed` keeps the first failure. +- `Running` → refused. A non-aborted `Failed` handle keeps its first failure + and refuses configure; an `Aborted` handle may be reused with the same + session key and a higher run number. Begin (`run` / `stream_begin`) is allowed only from `Ready`. Success consumes the run number, projects the spec onto execution storage, **resets once** @@ -1981,7 +1983,7 @@ drawdown/run-up walk, and metrics computed over a real series. The per-bar **broker-state hash** is a row of that same report, so `KernelRecorded` records it too. It stays behind the recording switch it always had — `set_broker_state_hash_recording(true)` -(`engine.hpp:2126`; C: `strategy_set_broker_state_hash_recording`), off by +(`engine.hpp:2127`; C: `strategy_set_broker_state_hash_recording`), off by default, set while no run is active — because each row is a full `broker_state_hash()` over the lots and the closed rows (since v19 a row costs the live state, not the run's length: the closed rows enter through a running @@ -1993,8 +1995,10 @@ one row follows each point, after the extremes that point just folded broker_state_hash_len == equity_curve_len == script_bars_processed ``` -in batch and across a stream's warmup and realtime legs alike. That length -identity is the only part of the array that holds across drivings. +on a completed run in batch and across a stream's warmup and realtime legs +alike. A Failed or Aborted run may expose only a recorded prefix, whose last +row need not equal the final scalar. That length identity is the only part of +the array that holds across drivings. **What a row is, and what it is for.** A row is the run's *continuation identity* at that bar, not its trade outcome. `broker_state_hash()` is @@ -2110,13 +2114,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:1271`) the C one, with the +`strategy_closed_trade_close_cause` (`pineforge.h:1273`) 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:1194-1224`), which index exactly the +`_exit_id` / `_exit_comment` (`pineforge.h:1196-1226`), 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. @@ -3783,7 +3787,9 @@ refuses a Running handle without changing its state, including when a callback calls it. It also refuses a Completed host's `on_hash_extension` callback during a read-only hash query. A Completed handle or one Failed by a cooperative abort may be configured for another run; the kernel judges reuse -constraints and can fail a refused reuse with `PF_NATIVE_FAILURE_CONTRACT`. +constraints and can fail a refused reuse with `PF_NATIVE_FAILURE_CONTRACT`. An +Aborted handle is reusable with the same session key and a higher run number; a +handle whose configure call failed remains Failed and cannot be configured again. The call has no typed out-parameters. `strategy_configure_native_ext_result_v1` is the typed spelling of @@ -3806,11 +3812,11 @@ by anything but an abort, answer `PF_NATIVE_E_STATE` with cooperative abort latches only while a run holds its spec, and the Failed state keeps it, so no public call reaches it. -`pf_native_run_spec_ext_v1`'s third of five published lengths, +`pf_native_run_spec_ext_v1`'s third of six published lengths, `PF_NATIVE_RUN_SPEC_EXT_V1_POLICY_SIZE`, ends the tail after the base layout (`PF_NATIVE_RUN_SPEC_EXT_V1_BASE_SIZE`) and the risk tail (`PF_NATIVE_RUN_SPEC_EXT_V1_RISK_SIZE`); the runtime accepts it and the other -four. That tail appends what the header used to list as unrepresentable — the retained intrabar path +five. That tail appends what the header used to list as unrepresentable — the retained intrabar path (`PF_NATIVE_SPEC_EXT_INTRABAR`), the four feed-shape and presentation policies (`PF_NATIVE_SPEC_EXT_FEED_POLICY`: slot labels, feed tolerance, the forced path order, abort reporting), and the margin model's remaining knobs diff --git a/docs/pages/pine-to-native.md b/docs/pages/pine-to-native.md index c9b0b697..0dd0e0bc 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -112,21 +112,21 @@ 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:1176); a C host sends `pf_native_run_spec_v1` -(pineforge.h:546) plus `pf_native_run_spec_ext_v1` (native_c_api.h:2265) +(pineforge.h:548) plus `pf_native_run_spec_ext_v1` (native_c_api.h:2265) through `strategy_configure_native_ext_v1` (native_c_api.h:3107). 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:546 | `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:537 | `hello_kernel.cpp` | Required, positive. | -| `currency` | `currency` native_run_spec.hpp:570 | `currency` pineforge.h:535 | `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:538 | `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:538 | `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:538 | `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:539 | `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:541 | `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`. | +| `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. | +| `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. | +| `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`. | | `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:2102 | `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:2106 | `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:2276 | `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. | @@ -135,7 +135,7 @@ rather than defaulted. | `margin_short` | `NativeMarginModel::initial_short` native_run_spec.hpp:242 | `margin_initial_short` native_c_api.h:2290 | `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:2329 | `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:646 configure a *compiled Pine strategy*, never a native host. | +| `use_bar_magnifier` | `IntrabarPath` native_run_spec.hpp:378 | `intrabar_kind` native_c_api.h:2329 | `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`. | @@ -261,7 +261,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: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.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:688 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:690 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` @@ -290,12 +290,12 @@ is `strategy_native_submit_v1` native_c_api.h:2678 over a | `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.direction.all` | `NativeOpenDirections::Both` native_run_spec.hpp:76 | `allowed_open_directions` pineforge.h:539 | `native_risk_limits_strategy.cpp` | The default. | -| `strategy.direction.long` | `NativeOpenDirections::Long` native_run_spec.hpp:74 | `allowed_open_directions` pineforge.h:539 | `tests/test_native_run_spec.cpp` | An opening the direction forbids is refused with `OpeningDirection` native_execution_consumer.cpp:2954. | -| `strategy.direction.short` | `NativeOpenDirections::Short` native_run_spec.hpp:75 | `allowed_open_directions` pineforge.h:539 | `tests/test_native_run_spec.cpp` | Same, the other way. | -| `strategy.commission.percent` | `NativeFeeKind::Percent` native_run_spec.hpp:20 | `fee_kind` pineforge.h:538 | `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:538 | `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:538 | `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: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.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. | Two more order concepts Pine has no word for, both generic: `ReverseTo` native_order.hpp:60 is one exact signed target reached in one @@ -315,8 +315,8 @@ 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:2093-2100 | `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:2094-2098 | `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_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:2110-2117 | `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:2111-2115 | `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:2116 | `native_trail_risk_strategy.cpp` | The absolute level the trail arms at. Read the live projection with `trail_state` native_host.hpp:1072 (C: `strategy_native_trail_state_v1` native_c_api.h:2986). 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, @@ -396,8 +396,8 @@ 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:539 | `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:540 | `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.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:2312 | `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:2315 | `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:2317 | `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. | @@ -497,7 +497,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:1309. A C caller that omits the +`native_acknowledge_events` native_host.hpp:1324. 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 @@ -517,8 +517,8 @@ range-end reporting is off. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `strategy.account_currency` | `currency` native_run_spec.hpp:570 | `currency` pineforge.h:535 | `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:1182. 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:537 | `hello_kernel.cpp` | The value you declared, unchanged by the run. | +| `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:1182. 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:1281 plus open entry commissions | `strategy_native_marked_equity_v1` native_c_api.h:3011 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. | @@ -548,8 +548,8 @@ range-end reporting is off. | `strategy.position_avg_price` | `NativePhysicalPosition::average_price` native_host.hpp:298 | `strategy_native_position_v1` native_c_api.h:2791 | `native_selected_strategy.cpp` | Volume-weighted over the open lots. | | `strategy.position_entry_name` | `native_open_lots` native_host.hpp:1277 then the last row's `entry_label` native_host.hpp:333 | `strategy_native_open_lot_get_v1` native_c_api.h:2832 | `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:1300 | `strategy_native_liquidation_price_v1` native_c_api.h:3019 | `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:582 | `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:1182 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:582 | `tests/test_native_fx_curve.cpp` | Same ruling. | +| `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:1182 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} @@ -603,22 +603,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:1189, `strategy_closed_trade_exit_id` pineforge.h:1202, -`strategy_closed_trade_exit_comment` pineforge.h:1204 and -`strategy_closed_trade_close_cause` pineforge.h:1271, which index exactly the +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 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:1189 | `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:1189 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:1730) before the close. | +| `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:1730) 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:1202 | `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:1204 | `native_margin_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_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` | | @@ -633,7 +633,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:1271) distinguishes a script +`strategy_closed_trade_close_cause` pineforge.h:1273) 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 @@ -653,7 +653,7 @@ classifies its own rows by order family instead. Which leg closed a row is its | `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.currency_rate()` | none — no FX data feed | `strategy_configure_native_fx_curve_v1` pineforge.h:582 | `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: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. | | `request.financial()` | none — no fundamentals feed | — | — | Same. | @@ -735,7 +735,7 @@ 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:2454 table handed to `strategy_native_host_create_v1` native_c_api.h:2637; the run is -`pf_native_run_spec_v1` pineforge.h:546 plus `pf_native_run_spec_ext_v1` +`pf_native_run_spec_v1` pineforge.h:548 plus `pf_native_run_spec_ext_v1` native_c_api.h:2265 through `strategy_configure_native_ext_v1` native_c_api.h:3107, and `strategy_native_run_v1` native_c_api.h:2650 drives the bars into a `pf_report_t` pineforge.h:469. @@ -1022,12 +1022,12 @@ PINEFORGE_EXPORT_NATIVE_STRATEGY(MyHost); ``` `PINEFORGE_EXPORT_NATIVE_STRATEGY` native_module.hpp:271 defines -`strategy_create` `sha256:bdc1d58cf27e0aaacfa19c793899d70108d6ac472ca4f29373d6d279a2f20f25` pineforge.h:616, `strategy_free`, `strategy_set_input` -pineforge.h:678, `strategy_set_override` pineforge.h:688, -`strategy_set_magnifier_volume_weighted` pineforge.h:696, `run_backtest` -pineforge.h:629, `run_backtest_full` pineforge.h:646 and `report_free` -pineforge.h:660. It adds **no** new C symbol: every remaining runtime export — -`strategy_configure_native_v1` pineforge.h:546, the `strategy_stream_*` family +`strategy_create` `sha256:bdc1d58cf27e0aaacfa19c793899d70108d6ac472ca4f29373d6d279a2f20f25` pineforge.h:618, `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 lives in the engine, and the generated `strategy_create` references `pf_abi_version()` so a static link keeps that object. diff --git a/docs/pages/report-schema.md b/docs/pages/report-schema.md index 88c19005..3101c240 100644 --- a/docs/pages/report-schema.md +++ b/docs/pages/report-schema.md @@ -54,8 +54,9 @@ typedef struct pf_report_s { pf_equity_point_t* equity_curve; int64_t equity_curve_len; /* NOTE: int64, not int */ - /* Per-script-bar broker-state hash (ABI v4; NULL / 0-length unless - * strategy_set_broker_state_hash_recording is on) */ + /* Per-report-point broker-state hash (ABI v4; a bare native host records + * it under NativeReportPolicy::KernelRecorded; NULL / 0-length when the + * switch is off or no report point exists) */ uint64_t* broker_state_hash; int64_t broker_state_hash_len; } pf_report_t; @@ -243,10 +244,12 @@ typedef struct pf_equity_point_s { } pf_equity_point_t; ``` -`equity_curve_len` equals `script_bars_processed` on a clean run (an -exception mid-run can truncate the curve — check -`strategy_get_last_error`). The array is heap-allocated and freed by -#report_free. Note the length field is `int64_t`, not `int`. +`equity_curve_len` equals `script_bars_processed` on a completed +`KernelRecorded` run (an exception, cooperative abort or other failure +mid-run can truncate the recorded prefix — check `strategy_get_last_error`). A +bare native host using the default `HostRecorded` policy has no kernel-owned +curve. The array is heap-allocated and freed by #report_free. Note the length +field is `int64_t`, not `int`. ## Lifetime and ownership diff --git a/include/pineforge/engine.hpp b/include/pineforge/engine.hpp index d1e9987e..9b881be0 100644 --- a/include/pineforge/engine.hpp +++ b/include/pineforge/engine.hpp @@ -2079,16 +2079,17 @@ class BacktestEngine { // realtime tick-driven bar after strategy_stream_begin. int64_t script_bars_processed() const { return diag_script_bars_processed_; } - // ABI v4 live-runtime surface (task 6): when on, every script bar's - // report point appends broker_state_hash() to broker_state_hashes_ + // ABI v4 live-runtime surface (task 6): when on, every report point + // appends broker_state_hash() to broker_state_hashes_ // immediately after that bar's record_equity_point() call -- under // KernelRecorded the execution consumer's record_script_report_point, // which every script calculation it delivers reaches (confirmed, // intrabar and aggregated bars, in a batch run and across a stream's // warmup and realtime legs); a host that records or marks its own report - // points appends its own row at each -- so the recorded array's length - // matches script_bars_processed and pf_report_t::broker_state_hash_len - // 1:1 -- including on strategy_stream_fill_report, whose report is the + // points appends its own row at each -- so a completed run's recorded + // array has one row per report point (and, under KernelRecorded, matches + // script_bars_processed and pf_report_t::broker_state_hash_len 1:1) -- + // including on strategy_stream_fill_report, whose report is the // cumulative warmup + realtime run. Default off: broker_state_hashes_ // stays empty, fill_report emits a null/zero-length array, and every // historical run stays byte-identical to before this flag existed. diff --git a/include/pineforge/native_c_api.h b/include/pineforge/native_c_api.h index 534637d1..cfd81be7 100644 --- a/include/pineforge/native_c_api.h +++ b/include/pineforge/native_c_api.h @@ -2396,12 +2396,12 @@ typedef struct pf_native_run_spec_ext_v1 { ((uint32_t)offsetof(pf_native_run_spec_ext_v1, risk_has_max_drawdown)) /** Byte length of #pf_native_run_spec_ext_v1 with L9's risk tail but without - * N8's intrabar / policy tail — the second of its five published layouts. */ + * N8's intrabar / policy tail — the second of its six published layouts. */ #define PF_NATIVE_RUN_SPEC_EXT_V1_RISK_SIZE \ ((uint32_t)offsetof(pf_native_run_spec_ext_v1, intrabar_kind)) /** Byte length of #pf_native_run_spec_ext_v1 with N8's intrabar / policy tail - * but before the `auxiliary_*` tail was appended — the third of its five + * but before the `auxiliary_*` tail was appended — the third of its six * published layouts. It is the offset of the first auxiliary field, for the * same reason #PF_NATIVE_RUN_SPEC_EXT_V1_BASE_SIZE is an offset. */ #define PF_NATIVE_RUN_SPEC_EXT_V1_POLICY_SIZE \ diff --git a/include/pineforge/pineforge.h b/include/pineforge/pineforge.h index 92424195..fcb818fd 100644 --- a/include/pineforge/pineforge.h +++ b/include/pineforge/pineforge.h @@ -448,11 +448,11 @@ typedef struct pf_report_s { pf_metrics_t metrics; /* Per-script-bar equity curve (bare host: KernelRecorded); time_ms is the script-bar OPEN * timestamp; equity = initial_capital + net_profit + open_profit at - * bar close. Free via #report_free or #strategy_native_report_free_v1. len == - * script_bars_processed, EXCEPT after a mid-run error (check - * strategy_get_last_error): an exception can truncate the curve, and - * metrics then describe the truncated prefix. NOTE int64_t length - * (ctypes: c_int64). */ + * bar close. Free via #report_free or #strategy_native_report_free_v1. + * On a completed KernelRecorded run len == script_bars_processed; a + * Failed or Aborted run can expose a shorter prefix (check + * strategy_get_last_error), and metrics describe that prefix. NOTE int64_t + * length (ctypes: c_int64). */ pf_equity_point_t* equity_curve; int64_t equity_curve_len; /* Per-script-bar broker-state hash when recording is on and report @@ -521,7 +521,9 @@ typedef enum pf_native_spec_optional_e { * the kernel's Contract rule. A Running handle is refused at the C boundary * without changing its native state, including from a callback. A Completed * handle or one Failed by a cooperative abort may be configured for another - * run outside callbacks; the kernel judges reuse constraints. A Completed + * run outside callbacks; the kernel judges reuse constraints (an aborted + * handle is reused with the same session key and a higher run number), and a + * handle whose configure call failed remains Failed. A Completed * host's `on_hash_extension` callback cannot reconfigure it during a read-only hash query. * This call has no typed out-parameters. The * extended path validates before configuring: #strategy_configure_native_ext_v1 @@ -993,7 +995,7 @@ PF_API void strategy_set_path_order(pf_strategy_t s, int mode); * value; it is a silent no-op under the COOF scheduler, mirroring * #strategy_set_probe_suppress_tail_logic's dispatch-path-scope caveat. */ PF_API int strategy_last_bar_dual_entry_path(pf_strategy_t s); -/** Toggle per-script-bar broker-state hash recording (spec §3.4, ABI v4). +/** Toggle per-report-point broker-state hash recording (spec §3.4, ABI v4). * * When @p on is non-zero, every subsequent run() appends * #strategy_broker_state_hash's value to pf_report_t::broker_state_hash diff --git a/tests/test_native_handle_stable_replace.cpp b/tests/test_native_handle_stable_replace.cpp index 413386c2..bfd8e804 100644 --- a/tests/test_native_handle_stable_replace.cpp +++ b/tests/test_native_handle_stable_replace.cpp @@ -1,7 +1,8 @@ // R5 lane V19-D witness: a replace that keeps the handle // (ReplaceOptions::keep_handle) or carries a close's book binding -// (ReplaceOptions::keep_binding) changes what the run records, never what it -// matches, nor any ordinal of its timeline. +// (ReplaceOptions::keep_binding) changes what the run records and, when a +// re-priced parent has waiting children, whether those children remain +// matchable; outside that ownership case it preserves the fill timeline. // // 1. The differential. One seeded bare host plays randomized re-issue scripts // on gapped tapes -- exits of every closing intent (explicit units, From 9ccc4e55d7aed4916f9abf07c5648a07fb7ca320 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 03:03:35 +0800 Subject: [PATCH 18/24] INT25 (i): current_partial_bar() says what a bare host gets -- the bar through the last path point consumed (audit N3) AUDIT4's N3: the partial-bar sentences promised "close at the cursor", and the acid check F8e refuted it. The kernel folds a discrete point (the open of the bar or of a sub-bar, a DistributionSamples sample, an observed print) before its callbacks run, but a segment's destination only after the segment has been matched (note_partial_point after match_segment, src/native_execution_consumer.cpp emit_segment and the intrabar sampler). So a fill inside a segment -- its on_native_applied and its OrderFill recalculation -- reads a bar that still ends at the segment's origin and does not hold the fill price: TP-E1 at 4990 on the High -> Low segment of O 4992 H 4992.5 L 4987 reads O 4992 H 4992.5 L 4992 C 4992.5; the liquidation at 5088.53 on the Low -> High segment of the 10:00 spike reads O 5062 H 5062 L 5061 C 5061. Comment-only and doc-only; no behaviour moves: - native_host.hpp current_partial_bar() and on_native_recalculate's note; - native_c_api.h strategy_native_partial_bar_v1; - native-engine.md: the open-bar warning (at the bar open, the open alone), "The bar so far, and the open-bar view", the host-reads table row and the C read-back paragraph; the design page's CT4 row; README's C read row. Citations the longer comments moved are carried through the exact line map. F8e, copied from $EV/exec/AUDIT4-opus-acid-test.cpp into INT25-scratch/acid/acid_int25.cpp and rewritten to the corrected sentence (the original is untouched), prints PASS in all four runs (bk, bp, sk, sp). Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 2 +- docs/adr/0001-kernel-adapter-boundary.md | 48 +++++------ docs/design/native-feature-parity.md | 60 ++++++------- docs/pages/native-engine.md | 67 ++++++++------- docs/pages/pine-to-native.md | 102 +++++++++++------------ include/pineforge/native_c_api.h | 10 ++- include/pineforge/native_host.hpp | 28 ++++--- 7 files changed, 168 insertions(+), 149 deletions(-) diff --git a/README.md b/README.md index 32e940bd..78843df1 100644 --- a/README.md +++ b/README.md @@ -482,7 +482,7 @@ strategy. They are additive; no symbol, struct or behaviour above changes, and | `strategy_native_events_v1` / `_state_v1` | Poll the recorded event history by ordinal; read the lifecycle and its typed failure | | `strategy_native_acknowledge_events_v1` / `strategy_native_event_window_v1` | Under the `WINDOW` event retention: say which events the host has read, so the kernel drops those command events at the next script-bar boundary; read the oldest ordinal a poll can still return | | `strategy_native_timeframe_bar_interval_v1` | From inside `on_timeframe_bar`, the delivered bucket's own calendar interval (`pf_native_timeframe_interval_v1`: the C++ `NativeTimeframeBarContext::interval`); `PF_NATIVE_E_STATE` anywhere else | -| `strategy_native_partial_bar_v1` / `_series_bar_v1` / `_trail_state_v1` / `_liquidation_price_v1` | The four optional reads — the bar so far at the cursor, a declared higher-timeframe series' latest bucket, a live trail's projection, the solved liquidation level. Each answers `PF_NATIVE_ABSENT` where the C++ `std::optional` is empty | +| `strategy_native_partial_bar_v1` / `_series_bar_v1` / `_trail_state_v1` / `_liquidation_price_v1` | The four optional reads — the bar so far through the last path point consumed, a declared higher-timeframe series' latest bucket, a live trail's projection, the solved liquidation level. Each answers `PF_NATIVE_ABSENT` where the C++ `std::optional` is empty | | `strategy_native_risk_state_v1` / `_marked_equity_v1` / `_recalculations_v1` / `_continuation_hash_v1` | The generic risk ledger, marked equity at a mark, the driven/suppressed recalculation counters, and the run's continuation identity | | `strategy_native_margin_call_v1` | One margin call's whole economics by its event ordinal (`pf_native_margin_call_v1`: mark, units, the position before and after, the surviving book's equity and requirement, the re-solved liquidation price) | | `strategy_native_sized_units_v1` | The units a `PF_NATIVE_INTENT_SIZED` request resolves to under the run's spec — the kernel's own sizing function, as a pure query before submitting | diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index d2599168..c41769e6 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -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:1176`), then feed. + continues live. Configure with `configure_native(spec)` (`native_host.hpp:1184`), 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`), `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`) — - `on_native_recalculate` for every calculation of the run (`native_host.hpp:900`), - `on_native_sub_bar` (`native_host.hpp:915`) and post-fill - `on_native_applied` (`native_host.hpp:925`). `on_bar` is `final` (`native_host.hpp:845`). + `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`). - **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:1207-1212`), plus `cancel_all` (`native_host.hpp:1247`) and `cancel_where` - (`native_host.hpp:1252`). A request carries one of five triggers — Market, Limit (with a + (`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 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:934`) returns no + unresolved `HostSized` whose `resolve_execution_terms` (`native_host.hpp:935`) returns no units is a `TermsUnresolved` rejection (`TermsUnresolved` `native_execution_consumer.cpp:4963`). -- **Observing executions.** Implement `on_native_applied` (`native_host.hpp:925`) — notifications +- **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:1309`). 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`: @@ -123,12 +123,12 @@ summary of it. |---|---| | 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`) | - | Cohorts | `cohort_open` (`native_host.hpp:1262`) / `cohort_add` (`:1265`) / `cohort_remove` (`:1268`) — the handle a `BindCohort` owner names | - | Mid-callback execution | `current_execution_point` (`native_host.hpp:1067`), `inspect_current_execution` (`native_host.hpp:1079`) / `execute_current` (`native_host.hpp:1084`) | - | 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:963`), `margin_check_allowed` (`native_host.hpp:975`), `resolve_margin_call_units` (`native_host.hpp:982`) — and `validate_execution_precommit` (`native_host.hpp:947`), 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:1303`); the breach appends a `NativeRiskEvent` to the command history | - | State reads | `physical_position()` (`native_host.hpp:1272`) / `native_marked_equity(mark)` (`native_host.hpp:1281`) / `native_open_lots(mark)` (`native_host.hpp:1277`), `trail_state(handle)` (`native_host.hpp:1072`), 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:1053`) is the lookahead-free bar so far, and `NativeOpenBarView::OpenOnly` (`native_run_spec.hpp:109`) masks that one callback | + | 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 | - **The run spec is generic, and no longer narrow.** `NativeRunSpec` (`native_run_spec.hpp:553`) owns instrument and clock facts, fees (`NativeFeeKind` `native_run_spec.hpp:19`), a scalar account FX, close timing, the quantity @@ -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:1116`) from inside `on_native_run_begin`, with + (`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:1099`) reading them back, and `NativeAuxiliaryFeed` + + (`native_host.hpp:1107`) 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:1182`). + `configure_native_fx_curve` (`native_host.hpp:1190`). - **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 at `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:1350`) 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 @@ -593,7 +593,7 @@ number names its symbol on this tree. | C++ hook | C route | witness in `tests/test_native_c_api.c` | |---|---|---| | `WaitForApplied::first_match` / `WaitForApplied::scope` | `arm_first_match` (`native_c_api.h:2176`), `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:3047`), reading a SIZED request's own sizing block | the sizing-query scenario | +| `native_sized_units` | `strategy_native_sized_units_v1` (`native_c_api.h:3051`), reading a SIZED request's own sizing block | the sizing-query scenario | | `resolve_execution_terms`, price half | `on_execution_terms` (`native_c_api.h:2562`): 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:2571`): 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:2578`) | the anchored-level scenario | @@ -603,9 +603,9 @@ number names its symbol on this tree. | surface | ruling | reason | |---|---|---| -| `cancel_all` (`native_host.hpp:1247`) / `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:2744`). | -| `cohort_add` (`native_host.hpp:1265`) / `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:1294`) / `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:3019`). | +| `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:2744`). | +| `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:3023`). | | `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:1681`) 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. | @@ -756,7 +756,7 @@ no new kernel policy. 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. 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:1010`; the market-if-touched + 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 "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 c78e809d..8f71379b 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -47,18 +47,18 @@ 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:1193 | 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: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 | — | -| 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:1198-1200, `cancel` native_host.hpp:1229; `ReplacedEvent` native_order.hpp:1013-1027; result and event types `SubmitStatus` native_order.hpp:835-861, `ReplacedEvent` native_order.hpp:997-1063 | 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:1229; no enumeration (`WorkingRequestCore::live()` is consumer-internal native_order.hpp:1673); labels exist but are unindexed (`Request::label` native_order.hpp:485) **Closed:** `cancel_all` native_host.hpp:1247, `cancel_where(comment)` native_host.hpp:1252 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1252 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1243 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:1206-1208, `cancel` native_host.hpp:1237; `ReplacedEvent` native_order.hpp:1013-1027; result and event types `SubmitStatus` native_order.hpp:835-861, `ReplacedEvent` native_order.hpp:997-1063 | 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:1673); 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 | — | -| 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:1262, `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: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:1172-1182 (deferred form native_order.hpp:1172-1182) | 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:1084-1085 | 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: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:1852); 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:999 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:1243 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: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 | @@ -66,10 +66,10 @@ 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:1072 | 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:5697-5719; `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. | -| 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:1072; `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. | +| 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 | ### 1.3 Sizing bases and reservations @@ -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:932-936, 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 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. | | 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. | @@ -100,14 +100,14 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | 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. | | 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:982 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:1300 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:1182 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:1281 **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 | +| 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) | +| 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:1303 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 | +| 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 | | 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) | @@ -117,12 +117,12 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa |---|---|---|---|---|---|---| | 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 | — | | 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:1084 | 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:1053 is the lookahead-free bar so far at the cursor, 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. | +| 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:925 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:2038-2046. 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:2038-2046. 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:915 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: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. | | 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 | @@ -131,9 +131,9 @@ 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:1116 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:873 and `native_series_bar(index)` native_host.hpp:1099 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: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). | -| 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:915 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: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 | @@ -148,21 +148,21 @@ 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: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:1182 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: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 | ### 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:1008-1012 | 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). | | 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. | | 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:1350, 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 | — | +| 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. | | 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 | — | @@ -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:2934-3002) 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: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). - 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). @@ -332,12 +332,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:925), a recalculation hook (`on_native_recalculate` native_host.hpp:900) and a sub-bar hook (`on_native_sub_bar` native_host.hpp:915). 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: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 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) | -| 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:1053; and `set_native_security_feed` documented as the pre-run door for a series' authoritative bars, refused in-run | +| 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 | --- @@ -1238,7 +1238,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:1287); between realtime inputs there is none — reject or fall back to `AtMatch`? | F: reject with `InvalidQuantityBasis` | +| 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` | | 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 | @@ -1279,7 +1279,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: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). - `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:1010). 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). +- 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}`. ### A.2 L3 — sizing, fractional reduce, sibling reservation (F P1, P7 + O G2 + S P2, P3) @@ -1438,7 +1438,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:1309) (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 (`sha256:af190cea22828f37fb9b832bcb29c9d50029da09280a8ddd3fc11149729a7142` native_calendar.hpp:363-366), 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/native-engine.md b/docs/pages/native-engine.md index 4561c242..eafe4297 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -27,26 +27,26 @@ default that is the established behaviour. The surface is **not** close-only. | `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:900`) | 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:915`) | after each retained lower-timeframe sub-bar's whole path | *Sub-bars* | -| `on_native_applied` (`native_host.hpp:925`) | after each applied execution — the calculate-on-fill point | *Native requests* | -| `on_native_margin_call` (`native_host.hpp:988`) | right after the `on_native_applied` of a kernel liquidation's own fill | *Margin and liquidation* | +| `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* | **The answering hooks** — each is consulted, and each has a default that is -the kernel's own answer: `resolve_execution_terms` (`native_host.hpp:934`), -`validate_execution_precommit` (`native_host.hpp:947`), -`resolve_margin_requirement` (`native_host.hpp:963`), `margin_check_allowed` -(`native_host.hpp:975`), `resolve_margin_call_units` (`native_host.hpp:982`), -`resolve_anchored_level` (`native_host.hpp:999`), `owns_lot_excursions` / -`closed_lot_excursion` (`native_host.hpp:1017`) and the hash seam +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 `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:1034`), and one that keeps the default +(`native_host.hpp:1035`), and one that keeps the default `validate_execution_precommit` with `declare_native_precommit_hook(false)` -(`native_host.hpp:1042`). The kernel then makes no bar-open call, and neither +(`native_host.hpp:1043`). 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, @@ -634,9 +634,9 @@ pass (`native_execution_consumer.cpp:7066-7068`). **Lookahead warning:** 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:1053`; C: -`strategy_native_partial_bar_v1`), the lookahead-free bar so far at this -cursor, or declares `NativeOpenBarView::OpenOnly`, which masks this one +`current_partial_bar()` (`native_host.hpp:1061`; C: +`strategy_native_partial_bar_v1`), the lookahead-free bar so far (at the bar +open, the open alone), or declares `NativeOpenBarView::OpenOnly`, which masks this one callback's bar down to its open. Both are under *The bar so far, and the open-bar view* below. @@ -1664,14 +1664,21 @@ and a request born there follows the ordinary birth rule. std::optional current_partial_bar() const; // non-virtual ``` -answers the lookahead-free bar so far at the current cursor: the open of this -script bar's first modeled point, the running high/low, and the close at the -cursor. `volume` accrues only activity actually consumed — the completed -lower-timeframe sub-bars of an intrabar path, or the observed prints of a -stream — and stays 0 for a modeled path that carries no intrabar volume of its -own. It is valid in the bar-open, applied, tick, sub-bar and recalculation -callbacks, and is `nullopt` outside a path walk, including in the bar's own -close calculation, where the host already holds the complete bar. +answers the lookahead-free bar so far: the open of this script bar's first +modeled point, the running high/low, and the close at the last path point the +walk has consumed. A discrete point — the open of the bar or of a sub-bar, a +sample of a `DistributionSamples` magnifier, an observed print of a stream — is +folded before its callbacks run, so there the bar so far closes at the cursor. +A fill inside a segment (a trigger crossed between two path points) happens +before the walk reaches the segment's destination, so that fill's +`on_native_applied` and its `OrderFill` recalculation read a bar that still +ends at the segment's origin: it never runs ahead of the cursor, and it does +not yet hold the fill price. `volume` accrues only activity actually consumed — +the completed lower-timeframe sub-bars of an intrabar path, or the observed +prints of a stream — and stays 0 for a modeled path that carries no intrabar +volume of its own. It is valid in the bar-open, applied, tick, sub-bar and +recalculation callbacks, and is `nullopt` outside a path walk, including in the +bar's own close calculation, where the host already holds the complete bar. This matters because the mid-bar callbacks are handed the **complete** script bar: `on_native_bar_open` receives the whole bar by default, and so does every @@ -1822,11 +1829,11 @@ says otherwise, and the C spellings are in | `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` | | `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 at this cursor | `strategy_native_partial_bar_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:1335`) | 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:1340`) | 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 | 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 | @@ -1883,8 +1890,8 @@ decision context plus the print's sequence, where zero keeps the public **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:1265`) enrolls one accepted opening's handle, -`cohort_remove` (`native_host.hpp:1268`) takes it back off, and a request with +(`native_host.hpp:1273`) enrolls one accepted opening's handle, +`cohort_remove` (`native_host.hpp:1276`) 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 @@ -3699,8 +3706,8 @@ such field matches. **Reading the kernel back.** Eight accessors answer what the C++ host reads off itself. Four of them are `std::optional` in C++, and `PF_NATIVE_ABSENT` (1, a non-negative *outcome*, not an error) is the C spelling of that empty: -`strategy_native_partial_bar_v1` (`current_partial_bar()` — the bar so far at -this cursor, absent in the bar's own close calculation, where the callback +`strategy_native_partial_bar_v1` (`current_partial_bar()` — the bar so far +through the last path point consumed, absent in the bar's own close calculation, where the callback already holds the complete bar), `_series_bar_v1` (`native_series_bar()` — the latest completed bucket of a declared subscription, absent before its first delivery and on every bar a `PF_NATIVE_GAPS_CLEAR` series publishes nothing on), diff --git a/docs/pages/pine-to-native.md b/docs/pages/pine-to-native.md index 0dd0e0bc..d5e1bb22 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -70,7 +70,7 @@ gain: (native_order.hpp:997), `ExecutionAppliedEvent` (native_order.hpp:1136), `MatchRejectedEvent` (native_order.hpp:1098), `CancelledEvent` (native_order.hpp:1039) — readable in order from `native_events` - (native_host.hpp:1309). + (native_host.hpp:1317). - **Any language.** The same kernel drives from C through a callback table (`pf_native_callbacks_v1` native_c_api.h:2454), so a host in Rust, Go, Python or Zig needs no C++. - **One binary, no transpiler.** Link `libpineforge`, ship an executable or a @@ -111,9 +111,9 @@ 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:1176); a C host sends `pf_native_run_spec_v1` +(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:2265) -through `strategy_configure_native_ext_v1` (native_c_api.h:3107). Setup is +through `strategy_configure_native_ext_v1` (native_c_api.h:3111). Setup is atomic: `Ready` → begin → `Completed`, and an incomplete value is refused rather than defaulted. @@ -265,14 +265,14 @@ 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:934), which sees the kernel-resolved quantity as the facts' +(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:2546 for a close. ## Orders {#pine_to_native_map_orders} -Everything here is one call: `submit` native_host.hpp:1193, or `submit_market` -native_host.hpp:1207 for the market-only shorthand, which *refuses* non-market +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:2678 over a @@ -285,8 +285,8 @@ is `strategy_native_submit_v1` native_c_api.h:2678 over a | `strategy.exit()` | `submit_bracket` native_toolkit.hpp:160 over a `BracketSpec` native_toolkit.hpp:37 | `strategy_native_submit_v1` native_c_api.h:2678 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:1155. 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:1258 with `NativeRequestField::Label` native_host.hpp:679 | `strategy_native_cancel_where_v1` native_c_api.h:2765 | `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:1229 is the single-request form for a host that kept the handle. | -| `strategy.cancel_all()` | `cancel_all` native_host.hpp:1247 | `strategy_native_cancel_all_v1` native_c_api.h:2744 | `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:1243 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:2765 | `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:2744 | `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. | @@ -302,7 +302,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:1200, which emits +quantity. Amending a live order is `replace` native_host.hpp:1208, which emits `ReplacedEvent` native_order.hpp:1013 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 @@ -317,7 +317,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:2110-2117 | `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:2111-2115 | `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:2116 | `native_trail_risk_strategy.cpp` | The absolute level the trail arms at. Read the live projection with `trail_state` native_host.hpp:1072 (C: `strategy_native_trail_state_v1` native_c_api.h:2986). 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:2116 | `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:2990). 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 @@ -339,11 +339,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:999, and carries the installed level + `resolve_anchored_level` native_host.hpp:1000, and carries the installed level in `ArmedEvent` native_order.hpp:1216. - **Invisible until armed.** `NativeArmVisibility::PendingUntilArmed` native_order.hpp:394 keeps the legs out of `native_working_requests` - native_host.hpp:1243 until the fill, like Pine's pending exit and like a + native_host.hpp:1251 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: @@ -401,7 +401,7 @@ block is the `PF_NATIVE_SPEC_EXT_RISK` native_c_api.h:735 tail. | `strategy.risk.max_drawdown()` | `NativeRiskLimits::max_drawdown` native_run_spec.hpp:311 | `risk_max_drawdown` native_c_api.h:2312 | `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:2315 | `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:2317 | `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:2319 | `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:1303. | +| `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:2319 | `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 @@ -427,12 +427,12 @@ liquidation level, and the ticket the forced close is booked under. | which equity it is tested against | `NativeMarginEquityBasis` native_run_spec.hpp:172 | `margin_equity_basis` native_c_api.h:2349 | `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:2350 | `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:2351 | `native_margin_strategy.cpp` | -| the host's own admission verdict | `validate_execution_precommit` native_host.hpp:947 | — C hosts gate through `on_precommit` native_c_api.h:2571 | `native_margin_strategy.cpp` | -| the host's own requirement | `resolve_margin_requirement` native_host.hpp:963 | `on_margin_requirement` native_c_api.h:2508 | `native_margin_strategy.cpp` | -| which check points run at all | `margin_check_allowed` native_host.hpp:975 | `on_margin_check` native_c_api.h:2517 | `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:2571 | `native_margin_strategy.cpp` | +| the host's own requirement | `resolve_margin_requirement` native_host.hpp:964 | `on_margin_requirement` native_c_api.h:2508 | `native_margin_strategy.cpp` | +| which check points run at all | `margin_check_allowed` native_host.hpp:976 | `on_margin_check` native_c_api.h:2517 | `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:982 | `on_margin_call_units` native_c_api.h:2524 | `native_margin_strategy.cpp` | -| the liquidation event | `MarginCallEvent` native_order.hpp:1232 delivered to `on_native_margin_call` native_host.hpp:988 | `on_margin_call` native_c_api.h:2472 | `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:2524 | `native_margin_strategy.cpp` | +| the liquidation event | `MarginCallEvent` native_order.hpp:1232 delivered to `on_native_margin_call` native_host.hpp:989 | `on_margin_call` native_c_api.h:2472 | `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 @@ -446,29 +446,29 @@ openings TradingView takes. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| script body at bar close | `on_native_bar` native_host.hpp:881 | `on_bar` native_c_api.h:2462 | `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:900, whose default forwards here. | -| every calculation, tagged with its reason | `on_native_recalculate` native_host.hpp:900 with `NativeCalculationReason` native_host.hpp:813 | `on_recalculate` native_c_api.h:2490 | `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. | +| script body at bar close | `on_native_bar` native_host.hpp:881 | `on_bar` native_c_api.h:2462 | `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:2490 | `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:2490 | `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:1754 | `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:2461 | `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:1053 | `strategy_native_partial_bar_v1` native_c_api.h:2971 | `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:925 | `on_applied` native_c_api.h:2464 | `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. | +| the bar so far, mid-bar | `current_partial_bar` native_host.hpp:1061 | `strategy_native_partial_bar_v1` native_c_api.h:2975 | `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:2464 | `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:2459 | `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:915 | `on_sub_bar` native_c_api.h:2500 | `tests/test_native_calc_timing.cpp` | `NativeCalculationReason::SubBar` is reserved and never delivered to the recalculate hook — a sub-bar has its own callback. | +| a lower-timeframe sub-bar | `on_native_sub_bar` native_host.hpp:916 | `on_sub_bar` native_c_api.h:2500 | `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.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:1067 | `price` native_c_api.h:1533 | `native_selected_strategy.cpp` | The readiness preview `inspect_current_execution` native_host.hpp:1079 is an observation, never an apply token; `execute_current` native_host.hpp:1084 is the apply. | +| 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. | ## 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:1272, - `native_marked_equity` native_host.hpp:1281, `native_open_lots` - native_host.hpp:1277, `native_liquidation_price` native_host.hpp:1300. +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. 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` @@ -497,7 +497,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:1324. 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 @@ -517,16 +517,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:1182. 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.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:1281 plus open entry commissions | `strategy_native_marked_equity_v1` native_c_api.h:3011 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:1289 plus open entry commissions | `strategy_native_marked_equity_v1` native_c_api.h:3015 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.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` `sha256:0490e46ca24b81c82c9549ce38af81be1ea888a60277263108cd26bf3f01e0c7` engine.hpp:1024 | `strategy_native_marked_equity_v1` native_c_api.h:3011 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` `sha256:0490e46ca24b81c82c9549ce38af81be1ea888a60277263108cd26bf3f01e0c7` engine.hpp:1024 | `strategy_native_marked_equity_v1` native_c_api.h:3015 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.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. | @@ -546,15 +546,15 @@ range-end reporting is off. | `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:2791 | `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:2791 | `native_selected_strategy.cpp` | Volume-weighted over the open lots. | -| `strategy.position_entry_name` | `native_open_lots` native_host.hpp:1277 then the last row's `entry_label` native_host.hpp:333 | `strategy_native_open_lot_get_v1` native_c_api.h:2832 | `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:1300 | `strategy_native_liquidation_price_v1` native_c_api.h:3019 | `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:1182 stages an immutable timestamped curve; codegen emits the Pine call as the identity, because no fixed table reproduces TradingView's series. | +| `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:2832 | `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:3023 | `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. | @anchor pine_to_native_map_trades ## Open trades {#pine_to_native_map_open_trades} -`native_open_lots(mark)` native_host.hpp:1277 answers one `NativeOpenLot` +`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:2824 then @@ -572,7 +572,7 @@ native_order.hpp:445 names. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `strategy.opentrades` | `native_open_lots` native_host.hpp:1277 | `strategy_native_open_lot_count_v1` native_c_api.h:2824 | `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:2824 | `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:1730 | `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:1730 | `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:1730 | `native_open_lots_strategy.cpp` | Script-bar index of the opening fill. | @@ -591,7 +591,7 @@ native_order.hpp:445 names. 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:1010) keeps its own sampler, so for that +(`owns_lot_excursions` native_host.hpp:1011) keeps its own sampler, so for that run the two excursion fields fold `mark` alone. ## Closed trades {#pine_to_native_map_closed_trades} @@ -647,8 +647,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:1116 inside `on_native_run_begin` native_host.hpp:860 | `strategy_native_declare_subscriptions_v1` native_c_api.h:2896 | `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:1099. `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:3169 | `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:915 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:2896 | `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:3173 | `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. | @@ -715,7 +715,7 @@ class HelloKernel : public pineforge::NativeStrategyHost { The host is a class; the run is one `NativeRunSpec` native_run_spec.hpp:553 naming the clock, instrument, account and fees; `configure_native` -native_host.hpp:1176 applies it and `run` engine.hpp:1709 drives the bars. +native_host.hpp:1184 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: @@ -737,7 +737,7 @@ The host is a `pf_native_callbacks_v1` native_c_api.h:2454 table handed to `strategy_native_host_create_v1` native_c_api.h:2637; the run is `pf_native_run_spec_v1` pineforge.h:548 plus `pf_native_run_spec_ext_v1` native_c_api.h:2265 through `strategy_configure_native_ext_v1` -native_c_api.h:3107, and `strategy_native_run_v1` native_c_api.h:2650 drives +native_c_api.h:3111, and `strategy_native_run_v1` native_c_api.h:2650 drives the bars into a `pf_report_t` pineforge.h:469. Three rules make the C door behave like the C++ one: @@ -755,14 +755,14 @@ Three rules make the C door behave like the C++ one: 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:1079 (its preview has no size-prefixed POD; +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:947 is -`on_precommit`, `resolve_anchored_level` native_host.hpp:999 `on_anchored_level`, +has a C route: `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:934 `on_execution_terms`, `native_sized_units` -native_host.hpp:1294 `strategy_native_sized_units_v1` and the +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 lists every public member with either its C spelling or that reason, and @@ -856,7 +856,7 @@ void on_native_run_begin() override { } ``` -`native_series_bar(0)` native_host.hpp:1099 then answers the latest completed +`native_series_bar(0)` native_host.hpp:1107 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. @@ -958,13 +958,13 @@ 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:1176 refuses an incomplete value + them, and `configure_native` native_host.hpp:1184 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:1309. + `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. @@ -1054,8 +1054,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:1309, ownership (which opening a close consumed), and the - continuation hash `native_continuation_hash` native_host.hpp:1350. + 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 diff --git a/include/pineforge/native_c_api.h b/include/pineforge/native_c_api.h index cfd81be7..f3c242e1 100644 --- a/include/pineforge/native_c_api.h +++ b/include/pineforge/native_c_api.h @@ -2960,11 +2960,15 @@ PF_API int strategy_native_declare_auxiliary_feed_v1(pf_strategy_t s, const char const pf_bar_t* bars, int32_t n, uint32_t* error, uint32_t* field); -/** The bar so far at the current cursor — `current_partial_bar()`. +/** The bar so far — `current_partial_bar()`. * * Open of the script bar's first modeled point, running high/low, close at - * the cursor; volume is the activity actually consumed so far. Valid in the - * bar-open, applied, tick, sub-bar and recalculation callbacks. + * the last path point the walk has consumed: the cursor itself at a discrete + * point (the open of the bar or of a sub-bar, a distribution sample, an + * observed print), and the segment's origin at a fill inside a segment, + * whose `on_applied` and ORDER_FILL recalculation read the bar before the + * fill price is folded in. Volume is the activity actually consumed so far. + * Valid in the bar-open, applied, tick, sub-bar and recalculation callbacks. * @return PF_NATIVE_OK when @p out was written, #PF_NATIVE_ABSENT outside a * path walk — including in the bar's own close calculation, where the * callback already holds the complete bar — leaving @p out untouched. */ diff --git a/include/pineforge/native_host.hpp b/include/pineforge/native_host.hpp index 132e4882..b8d63749 100644 --- a/include/pineforge/native_host.hpp +++ b/include/pineforge/native_host.hpp @@ -895,7 +895,8 @@ class NativeStrategyHost : public BacktestEngine { /// `bar` is the bar the calculation is about: the script bar under /// delivery in batch, the print's value bar for a stream Tick. It is the /// COMPLETE script bar even mid-path; current_partial_bar() is the - /// lookahead-free bar so far at this cursor. Commands and + /// lookahead-free bar so far, through the last path point consumed (at an + /// OrderFill inside a segment, the segment's origin). Commands and /// execute_current are legal here exactly as in on_native_applied. virtual void on_native_recalculate(const Bar& bar, const NativeDecisionContext& ctx, NativeCalculationReason reason, @@ -1041,15 +1042,22 @@ class NativeStrategyHost : public BacktestEngine { /// declares it. void declare_native_precommit_hook(bool implemented); - /// The bar so far at the current cursor, folded from the modeled points - /// this script bar has already presented: open of its first point, - /// running high/low, close at the cursor. Volume is the activity actually - /// consumed so far — the completed lower-timeframe sub-bars of an - /// intrabar path, or the prints of an observed stream — and stays 0 for a - /// modeled path with no intrabar volume of its own. Valid in the bar-open, - /// applied, tick, sub-bar and recalculation callbacks; nullopt outside a - /// path walk, including in the bar's own close calculation, where the host - /// already holds the complete bar. + /// The bar so far, folded from the path points this script bar has + /// already consumed: open of its first point, running high/low, close at + /// the last point consumed. A discrete point — the open of the bar or of a + /// sub-bar, a distribution sample, an observed print — is folded before + /// its callbacks run, so there the bar closes at the cursor. A fill inside + /// a segment (a trigger crossed between two path points) comes before the + /// walk reaches the segment's destination: its on_native_applied and its + /// OrderFill recalculation read a bar that still ends at the segment's + /// origin and does not hold the fill price. It never runs ahead of the + /// cursor. Volume is the activity actually consumed so far — the completed + /// lower-timeframe sub-bars of an intrabar path, or the prints of an + /// observed stream — and stays 0 for a modeled path with no intrabar + /// volume of its own. Valid in the bar-open, applied, tick, sub-bar and + /// recalculation callbacks; nullopt outside a path walk, including in the + /// bar's own close calculation, where the host already holds the complete + /// bar. std::optional current_partial_bar() const; /// How many recalculations the kernel has driven this run, and how many it /// suppressed because a point had already spent its From b4b9a367644f23ab3bc30921708677d965437bc9 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 03:19:16 +0800 Subject: [PATCH 19/24] INT25 (iii): promote-baseline admits a squash merge by tree equality, and reads the merge gate on the PR head The owner squash-merges PRs, so the PR head is never an ancestor of main and the "Verify exact-head" step skipped every such merge. The step now takes the merge commit from github.event.pull_request.merge_commit_sha (dispatch: inputs.merge_commit, plus a new required pr_head input for the verified head) and promotes only when - the merge commit is on origin/, - the PR head (fetched by sha when it is on no branch) exists, - tree(merge commit) == tree(PR head), and - tree(origin/) == tree(merge commit) (the base has not moved on). Every other outcome writes ok=false and exits 0 before any promotion step. pineforge/verify + pineforge/parity are still read on the PR head, newest status per context. baseline-promote.mjs receives the merge commit as --merge-commit and the verified head as --head-sha / --ci-head; its exit-2 notice now names the squash case. AGENTS.md and docs/ci.md say the same: the exact-tree guard and the statuses on the PR head, and that a squash still defers while the campaign tool requires the merge commit to be the gated head itself. The design page's content pin on that AGENTS.md section is re-hashed over its new extent (AGENTS.md:132-148). scripts/ci_preflight.py pins the new guard (merge_commit_sha, the pr_head input, ancestry of the merge commit only, both tree comparisons, four skips that exit, the three mjs arguments); scripts/test_ci_preflight.py adds ten mutations, each of which the contract refuses. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/promote-baseline.yml | 76 +++++++++++++++++--------- AGENTS.md | 9 ++- docs/ci.md | 8 ++- docs/design/native-feature-parity.md | 2 +- scripts/ci_preflight.py | 21 +++++++ scripts/test_ci_preflight.py | 15 +++++ 6 files changed, 100 insertions(+), 31 deletions(-) diff --git a/.github/workflows/promote-baseline.yml b/.github/workflows/promote-baseline.yml index 523b0d22..177feefb 100644 --- a/.github/workflows/promote-baseline.yml +++ b/.github/workflows/promote-baseline.yml @@ -6,12 +6,15 @@ # # 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-head (the gated commit's tree is -# now on the base branch), both maintainer verification statuses are green on -# that commit, and a PASS verdict binds -# the full (engine, codegen) pair. Otherwise it exits green without promoting. +# 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. # -# The gate logic is exact-head + pineforge/verify + pineforge/parity + +# The gate logic is exact-tree + pineforge/verify + pineforge/parity + # baseline-promote.mjs. The plane is now 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 @@ -55,12 +58,16 @@ on: 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. + # head was orphaned): pass the exact commit now on the base branch and the + # PR head the maintainers verified. 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)" + required: true pr_number: description: "the PR number" required: true @@ -97,26 +104,40 @@ jobs: with: fetch-depth: 0 - # Exact-head is the safety property: the snapshot describes the gated - # commit's tree, so we promote only if that tree is what is now on the - # base branch. A squash/rebase that rewrote the tree, or a base that - # moved on, is skipped — those must be re-gated. - - name: Verify exact-head + # 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 env: - HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.merge_commit }} + 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 }} BASE_REF: ${{ github.event.pull_request.base.ref || 'main' }} run: | set -euo pipefail git fetch --quiet origin "$BASE_REF" - if ! git merge-base --is-ancestor "$HEAD_SHA" "origin/$BASE_REF"; then - echo "::notice::head $HEAD_SHA is not on origin/$BASE_REF — not an exact-head merge; skipping" + 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 - base_tree=$(git rev-parse "origin/$BASE_REF^{tree}") + # 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 [ "$base_tree" != "$head_tree" ]; then - echo "::notice::base tree != head tree — the base diverged from the gated tree; re-gate. skipping" + 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 "ok=false" >> "$GITHUB_OUTPUT"; exit 0 fi echo "ok=true" >> "$GITHUB_OUTPUT" @@ -126,7 +147,7 @@ jobs: if: steps.exacthead.outputs.ok == 'true' env: GH_TOKEN: ${{ github.token }} - HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.merge_commit }} + HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.pr_head }} run: | set -euo pipefail # GitHub lists newest statuses first. Keep the first occurrence of @@ -216,7 +237,8 @@ jobs: # No metadata server on a GitHub runner; mint GCS tokens from the # federated gcloud credential explicitly. PINEFORGE_GCP_TOKEN_CMD: gcloud auth print-access-token - HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.merge_commit }} + 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 }} 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 }} @@ -226,18 +248,22 @@ jobs: set +e node .workflow/campaign/src/baseline-promote.mjs \ --repo "${{ steps.axis.outputs.repo }}" \ - --merge-commit "$HEAD_SHA" --head-sha "$HEAD_SHA" --ci-head "$HEAD_SHA" --ci-green \ + --merge-commit "$MERGE_SHA" --head-sha "$HEAD_SHA" --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 # advance -- a cross-cutting engine+codegen change (gated as a composite - # the single axis cannot complete), an ungated merge, or a non-exact-head - # merge. 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, 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. if [ "$code" = "2" ]; then - echo "::notice::baseline not promoted (deferred): no PASS verdict binds this merge as a single-axis advance. 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): 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." exit 0 fi exit "$code" diff --git a/AGENTS.md b/AGENTS.md index 9497ac86..b350b068 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -140,6 +140,9 @@ 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-head guard, both -required statuses on that head, and the campaign verdict pass. A squash or -rebase that rewrites the SHA defers promotion and needs new verification. +`.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. diff --git a/docs/ci.md b/docs/ci.md index 47b038d7..c1ae9903 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -533,8 +533,12 @@ 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 exact merged PR head, as well -as its existing exact-head and campaign verdict guards. +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. 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/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 8f71379b..100ca8a6 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -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:e3aa487ce8d69db9054576f75839345c0acaab55c3fc0a374c2f9c8c071559ec` AGENTS.md:132-145). A lane that cannot be shown neutral by construction does not merge. +- **(a) NEUTRALITY.** Every new behaviour is opt-in by a spec field or a new request kind. The adapter's `project()` (pine_adapter.cpp: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. - **(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:1278). 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. diff --git a/scripts/ci_preflight.py b/scripts/ci_preflight.py index 966049f3..8ef88cfd 100644 --- a/scripts/ci_preflight.py +++ b/scripts/ci_preflight.py @@ -102,6 +102,27 @@ def ci_workflow_findings(ci: str, native: str, promote: str, cmake: str) -> list or '.["pineforge/parity"] == "success"' not in promote or '/check-runs' in promote): findings.append('baseline promotion must 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. + 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 }}' + 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 '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): + 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) names = re.findall(r'^ (test_[A-Za-z0-9_]+)$', blocks[0], re.MULTILINE) if len(blocks) == 1 else [] diff --git a/scripts/test_ci_preflight.py b/scripts/test_ci_preflight.py index 9f0ac389..047c0bbf 100644 --- a/scripts/test_ci_preflight.py +++ b/scripts/test_ci_preflight.py @@ -41,6 +41,21 @@ def test_ci_contract_pins_parallel_pr_light_and_full_events(self): (2, '.["pineforge/parity"] == "success"', 'true'), (2, 'if has($s.context) then . else .[$s.context] = $s.state end', '.[$s.context] = $s.state'), + # Squash merges: the merge commit is merge_commit_sha, admitted by + # tree equality with the verified PR head, never by its ancestry. + (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 }}', + 'HEAD_SHA: ${{ github.event.pull_request.merge_commit_sha || github.event.inputs.merge_commit }}'), + (2, ' pr_head:\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-commit "$MERGE_SHA"', '--merge-commit "$HEAD_SHA"'), + (2, '--ci-head "$HEAD_SHA"', '--ci-head "$MERGE_SHA"'), (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 cb6fce27f9638b1f3be147b50adcbd9ba95ec0e0 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 03:58:05 +0800 Subject: [PATCH 20/24] INT25 (ii): CI fits CI-LITE's rule for wave G -- slow rows re-measured, an hour for a full sanitizers run, floors recounted Slow rows. The rows wave G enlarged or added were re-measured in a full sanitizers and Debug run of this tree on the maintainers' x86-64 build host (lab remote rj-20260925t192212-92d7b5, NTU2, 12 parallel CTest jobs): RATIO-HARDEN's legs (test_native_state_continuation 34.7 s sanitizers / 18.9 s Debug, test_adapter_lookup_index_scaling 24.3 / 3.0, test_native_match_rescan_ scaling 14.7 / 8.4, the continuation-tail, intrabar-lookup, journal-window and bare-host rows under 4.1 / 3.4), V19-FIX's scaling rows (inside the labelled test_adapter_live_state_scaling, 52.6 / 8.1) and the new rows of KERNEL-EDGE, K-ULP4, C-SURFACE-1 and DOC-TRUTH-4 (each under 1 s). No row outside the 27 crosses >60 s under sanitizers or >30 s in Debug, so the label list and its pinned digest stand; docs/ci.md records the measurement. Bound. ci_verify.py's ctest stage keeps 30 minutes for every run but a full sanitizers run (no --exclude-label: push to main, manual dispatch, the maintainers' verification), which gets an hour (ctest_timeout(), SANITIZERS_FULL_CTEST_TIMEOUT); main's own full set ran out of 30 minutes twice. The PR set keeps 1800 s. ci.yml's sanitizers job allows 120 minutes so the hour after a ~30-minute build is the binding limit. test_ci_verify pins the bound per profile and exclusion; three mutants (the exclusion ignored, the hour back to 1800, the stage hard-coded) each fail it. Floors, recounted with ctest -N per profile (ci_verify's own configure argv): release 672 -> 677, debug 653 -> 658, sanitizers 653 -> 658, native 662 -> 667, kernel 272 -> 277 registered (276 run with the WebSocket row skipping on a system libcurl). The five rows, in every profile: C-SURFACE-1 test_native_c_api_int23_header, KERNEL-EDGE test_native_kernel_edge, K-ULP4 test_native_unrepresentable_refusal and test_native_quantity_tolerance, DOC-TRUTH-4 test_native_engine_complete_host. RELEASE_MIN_TESTS 677 and KERNEL_MIN_TESTS 276 (set by the picks) get one measured comment naming those lanes; EXCLUDED_REGISTERED_MIN becomes {debug 658, sanitizers 658, native 667}. The floor citations in CONTRIBUTING.md and contributing-llm.md follow the constants to ci_verify.py:241 and :349. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/ci.yml | 4 ++- CONTRIBUTING.md | 4 +-- docs/ci.md | 22 +++++++++--- docs/pages/contributing-llm.md | 6 ++-- scripts/ci_verify.py | 63 ++++++++++++++++++++++------------ scripts/test_ci_verify.py | 20 ++++++++++- 6 files changed, 86 insertions(+), 33 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 03272cc6..107cfe83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -143,7 +143,9 @@ jobs: # PINEFORGE_ENABLE_SANITIZERS). sanitizers: runs-on: ubuntu-24.04 - timeout-minutes: 90 + # 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 env: ASAN_OPTIONS: detect_leaks=1:halt_on_error=1:abort_on_error=1 UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9c461f3b..d21d14ce 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:238 and `RELEASE_MIN_TESTS` -ci_verify.py:346. +floor — `KERNEL_MIN_TESTS` ci_verify.py:241 and `RELEASE_MIN_TESTS` +ci_verify.py:349. 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 c1ae9903..7b7b59d8 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 the INT24 base are 653 -for Debug and sanitizers and 662 for native. The existing full-run release and -kernel floors remain 672 and 271 rows that ran; full runs do not exclude a -label. +missing label fails. The PR registration floors at INT25 are 658 for Debug +and sanitizers and 667 for native (653 and 662 at the INT24 base, plus wave G's +five rows). The full-run release and kernel floors are 677 and 276 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`) @@ -524,9 +524,21 @@ the 27 CTest rows labelled `slow` in `tests/CMakeLists.txt`. These rows 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. +INT25 re-measured the rows wave G enlarged or added (RATIO-HARDEN's timing +legs, V19-FIX's scaling rows, and the new KERNEL-EDGE, K-ULP4, C-SURFACE-1 and +DOC-TRUTH-4 rows) in a full sanitizers and Debug run on the maintainers' x86-64 +verification host (twelve parallel CTest jobs): none crosses either threshold, +so the 27 labelled rows stand. 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. A push to `main` and a manual dispatch of `ci.yml` run every profile without -the exclusion. Standalone manual dispatch of `native-live.yml` is also full. +the exclusion. Their sanitizers CTest stage gets an hour (`ci_verify.py` +bounds CTest at 30 minutes otherwise, the PR set included), because main's full +set ran out of 30 minutes twice, and the sanitizers job allows 120 minutes for +that hour after its build. Standalone manual dispatch of `native-live.yml` is also full. The maintainers run the full `ci_verify.py` profiles on their x86-64 build hosts before the PR merge gate and post `pineforge/verify` to the PR head. Their parity verdict posts `pineforge/parity`, reporting no regression or that diff --git a/docs/pages/contributing-llm.md b/docs/pages/contributing-llm.md index a58a8d2b..59d831c3 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:238 and - `RELEASE_MIN_TESTS` ci_verify.py:346. Adding rows means raising the floor + *ran* against a floor: `KERNEL_MIN_TESTS` ci_verify.py:241 and + `RELEASE_MIN_TESTS` ci_verify.py:349. 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:238, `RELEASE_MIN_TESTS` ci_verify.py:346). It +(`KERNEL_MIN_TESTS` ci_verify.py:241, `RELEASE_MIN_TESTS` ci_verify.py:349). 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 7b62035b..24ccc5af 100644 --- a/scripts/ci_verify.py +++ b/scripts/ci_verify.py @@ -226,15 +226,18 @@ # dual-entry witness reach the source layer (see RELEASE_MIN_TESTS), and # B-C-SURFACE and B-DOCS add no row. 272 registered, 271 run: the WebSocket # row still skips on a system libcurl. -# C-SURFACE-1 adds the INT23 frozen-header decision-tail witness. -# Wave G KERNEL-EDGE adds the source-free test_native_kernel_edge row. -# 275 run = those 271, C-SURFACE-1's and KERNEL-EDGE's rows, and the two -# source-free TUs of R5 lane K-ULP4: -# +2 K-ULP4 test_native_unrepresentable_refusal, test_native_quantity_tolerance -# All four register in release too. 276 registered, 275 run: the WebSocket row -# still skips on a system libcurl. -# DOC-TRUTH-4 adds the extracted Complete host (test_native_engine_complete_host): -# 277 registered, 276 run. +# 276 run = those 271 plus the five source-free TUs of wave G (INT25) -- +# measured on the integrated tree (ctest -N), not summed from the lanes' own +# bases: +# +1 C-SURFACE-1 test_native_c_api_int23_header (the INT23 frozen-header +# decision-tail witness) +# +1 KERNEL-EDGE test_native_kernel_edge +# +2 K-ULP4 test_native_unrepresentable_refusal, +# test_native_quantity_tolerance +# +1 DOC-TRUTH-4 test_native_engine_complete_host (the guide's Complete host, +# extracted from the page and run) +# All five register in release too; RATIO-HARDEN, V19-FIX and K-IDX add no row. +# 277 registered, 276 run: the WebSocket row still skips on a system libcurl. KERNEL_MIN_TESTS = 276 # 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 @@ -336,18 +339,35 @@ # test_adapter_margin_revival_cancel # +1 INT24 test_adapter_dual_entry_tie (the ruling on B-ADAPTER's # finding 1) -# B-C-SURFACE's witnesses are rows inside test_native_c_api. C-SURFACE-1 adds -# the INT23 frozen-header decision-tail row. Wave G KERNEL-EDGE adds the same -# test_native_kernel_edge row in release: 674. -# 676 = those 674 plus the two R5 lane K-ULP4 rows KERNEL_MIN_TESTS lists -# above, which register here too (ctest -N on the lane's tree); its C-language -# checks are rows inside test_native_c_api. No release row skips, so 676 -# registered is 676 run. DOC-TRUTH-4 adds the Complete host in release: 677. +# B-C-SURFACE's witnesses are rows inside test_native_c_api. +# 677 = those 672 plus the five wave-G (INT25) rows KERNEL_MIN_TESTS lists +# above -- C-SURFACE-1 +1, KERNEL-EDGE +1, K-ULP4 +2, DOC-TRUTH-4 +1 -- which +# register here too, counted with ctest -N on the integrated tree; no wave-G +# lane adds a source-bound row (K-ULP4's C checks and V19-FIX's scaling rows +# are rows inside existing TUs). No release row skips, so 677 registered is 677 +# run. RELEASE_MIN_TESTS = 677 -# PR-only registration floors. These are the complete CTest populations at -# 91d65ad6 (INT24); the ordinary full-run floors above remain unchanged. -# An excluded run must still discover at least this many rows before -LE. -EXCLUDED_REGISTERED_MIN = {'debug': 653, 'sanitizers': 653, 'native': 662} +# 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 five rows (C-SURFACE-1 +1, +# KERNEL-EDGE +1, K-ULP4 +2, DOC-TRUTH-4 +1) in each. An excluded run must +# still discover at least this many rows before -LE. +EXCLUDED_REGISTERED_MIN = {'debug': 658, 'sanitizers': 658, 'native': 667} +# 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 +# run that excludes a label (the PR set) and every other profile keep 30 +# minutes. +CTEST_TIMEOUT = 1800 +SANITIZERS_FULL_CTEST_TIMEOUT = 3600 + + +def ctest_timeout(cfg: 'VerifyConfig') -> int: + if cfg.profile.sanitizers and not cfg.exclude_label: + return SANITIZERS_FULL_CTEST_TIMEOUT + return CTEST_TIMEOUT + + # CTest's closing summary: '100% tests passed out of N' when nothing failed, # '97% tests passed, 3 tests failed out of N' otherwise. N includes a skipped # row (counted as passed) and a row CTest could not start (counted as @@ -1471,7 +1491,8 @@ def run(self) -> int: ctest += ['-LE', self.cfg.exclude_label] if ctest_supports_junit(self.cfg.runner): ctest += ['--output-junit', str(self.cfg.build_dir / 'ctest-junit.xml')] - ran = self.invoke('ctest', ctest, extra_env=self.sanitizer_env(), timeout=1800) + ran = self.invoke('ctest', ctest, extra_env=self.sanitizer_env(), + timeout=ctest_timeout(self.cfg)) self.enforce_test_floor(ran, registered=registered, selected=selected) installed = self.invoke( diff --git a/scripts/test_ci_verify.py b/scripts/test_ci_verify.py index 97f65f89..5822a3d3 100644 --- a/scripts/test_ci_verify.py +++ b/scripts/test_ci_verify.py @@ -125,12 +125,16 @@ def __init__(self, build_dir: Path, source: Path, profile: str = 'release', **ex self.profile = profile self.exits = exits self.calls: list[list[str]] = [] + # Every invocation's bound, beside the calls fixture (which existing + # tests assert verbatim). + self.timeouts: list[tuple[list[str], int]] = [] self.cxx = shutil.which('c++') or '/usr/bin/c++' self.secret_seen_in_argv = False def __call__(self, argv, *, extra_env=None, timeout=600, combine_stderr=True, stream_output=False) -> Completed: argv = list(map(str, argv)) + self.timeouts.append((argv, timeout)) # Discovery invocations are asserted through Driver stages. Keep the # existing execution-call fixture stable for pre-existing tests. if argv[0] != 'ctest' or '-N' not in argv: @@ -1322,7 +1326,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': 653, 'sanitizers': 653, 'native': 662}) + {'debug': 658, 'sanitizers': 658, 'native': 667}) for profile, registered in ci_verify.EXCLUDED_REGISTERED_MIN.items(): with self.subTest(profile=profile): code, summary, scripted, _ = self.run_profile( @@ -1336,6 +1340,20 @@ def test_pr_exclusion_proves_registered_minus_labelled_equals_ran(self): self.assertIn('ctest-list-all', stage_names(summary)) self.assertIn('ctest-list-selected', stage_names(summary)) + def test_full_sanitizers_ctest_runs_an_hour_and_every_other_run_half(self): + # Main's full sanitizers set outran 30 minutes twice; the PR set and + # every other profile keep the old bound. + for profile, extra, bound in (('sanitizers', [], 3600), + ('sanitizers', ['--exclude-label', 'slow'], 1800), + ('debug', [], 1800), ('debug', ['--exclude-label', 'slow'], 1800), + ('release', [], 1800), ('kernel', [], 1800)): + with self.subTest(profile=profile, extra=extra): + code, summary, scripted, _ = self.run_profile(profile, extra=extra) + self.assertEqual(code, 0, summary['failures']) + bounds = [timeout for argv, timeout in scripted.timeouts + if argv[0] == 'ctest' and '--output-on-failure' in argv] + self.assertEqual(bounds, [bound]) + def test_ctest_discovery_count_rejects_missing_or_ambiguous_summary(self): self.assertEqual(ci_verify.ctest_list_count(b'Test #1: a\nTotal Tests: 1\n'), 1) self.assertIsNone(ci_verify.ctest_list_count(b'No tests were found\n')) From 18ec002590d427ed0f6028c73e1e71145bd481a5 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 02:31:57 +0800 Subject: [PATCH 21/24] Kernel: an OCA-Reduce deduction binary64 cannot take off its recipient is absorbed, and the run goes on (R5 lane K-ULP5) Ruling (supervisor, owner-level, on K-ULP4's finding 2): a sibling reservation deduction that is unrepresentable -- at most half an ulp of the sibling's remaining units, so fl(units - d) is the units again -- is ABSORBED as a no-op. The sibling's units stay as they are, the exact binary64 result of the subtraction; the member's fill stands; the absorption is an observable event; generic, no knob. Recorded as the ADR-0001 rule-2 row "an OCA-Reduce sibling's deduction binary64 cannot take". A member's fill in a GroupEffect::Reduce group is deducted in four places of the request core (src/native_order.cpp), each of which answered CoreFailure::UnrepresentableReservation and failed the run (code 6, discriminator 7) after the fill was booked; each now absorbs, on the staged and the direct path alike: - prepare_group_effect / apply_group_effect, a sibling with remaining units: a ReservationReducedEvent with actual_deduction 0 and after == before; the row is not touched (C: RESERVATION_REDUCED, closed_units 0). - the same, a sibling whose units are deferred: a fill the pending total cannot move is a DeferredGroupAdjustmentEvent with deferred_delta 0 whose pending_after is its pending_before; it joins no chain (C: DEFERRED_GROUP, closed_units 0). A total below half an ulp of a later fill rounds into it (pending_sum), which stopped the run too; collect_pending_chain re-sums the same way. Only a pending total that overflows binary64 still fails the run with discriminator 7. - prepare_terms / apply_terms through effective_host_units, and prepare_owner_applied / apply_owner_applied: a pending total the resolved (or the owner's opened) units absorb leaves those units standing, recorded as effective_deduction 0 beside a positive pending_total. A deduction binary64 can take is taken exactly as before, bit for bit: no completed run changes. inspect_current_execution no longer throws "native host-sized deduction is not representable" for such a request. Tests: test_native_group_absorption (new, kernel-only): p5b, each site, pending-rounds, the overflow control and a seeded battery of 3 x 1,000 OCA-Reduce runs at ulp scale, staged and direct (bit-equal, hashes included) -- 0 stopped; on f1af50dc 914 stopped (all code 6 / disc 7) and 970 of 177,824 checks fail. check_reservation_absorbed in test_native_c_api.c (8 failures on f1af50dc). Rows that pinned the old contract move to the new rule, each with its reason: G5 in test_native_resting_acceptance and test_native_resting_replay_contract (the overflow row stays a failure), checked_reservation_arithmetic in test_native_order_resting_core, A-T4d in test_native_execution_terms, numeric_absorption_and_chain_authentication in test_native_order_terms_core. Floors: kernel 274 (275 registered), release 675. Header docs for the four events, CoreFailure::UnrepresentableReservation and effective_host_units; native-settlement.md and native-engine.md drop K-ULP4's "one case still stops the run" and say what a host gets; every citation the lane moved is re-anchored from f1af50dc (anchors 1678/1678). Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 1d7639c80de4250babe30935cb1fd4904a57c657) INT25 merge: picked after the integration items (the lane was accepted at 03:55, after picks 6-8 and items (i)-(iii) had landed; it sits on K-ULP4, which it follows). The floors add its row to the wave-G comment (kernel 277 run / 278 registered, release 678, PR registration floors 659/659/668) and its new row test_native_group_absorption is labelled slow: 70.35 s under sanitizers (17.37 s Debug) in the lane's own lab remote run on NTU2 (rj-20260925t190356-3de52e / rj-20260925t190357-b83ced), over CI-LITE's 60 s rule -- 28 labelled rows, the preflight digest re-pinned. The pages merge on digit-normalised text and are re-anchored once at the lane's second pick. --- CONTRIBUTING.md | 4 +- docs/adr/0001-kernel-adapter-boundary.md | 33 +- docs/ci.md | 12 +- docs/design/native-feature-parity.md | 22 +- docs/native-settlement.md | 35 +- docs/pages/contributing-llm.md | 8 +- docs/pages/native-engine.md | 39 +- docs/pages/pine-to-native.md | 158 +-- include/pineforge/native_c_api.h | 4 +- include/pineforge/native_order.hpp | 35 +- scripts/ci_preflight.py | 4 +- scripts/ci_verify.py | 31 +- scripts/test_ci_verify.py | 2 +- src/native_order.cpp | 121 +- tests/CMakeLists.txt | 9 + tests/test_native_c_api.c | 233 ++++ tests/test_native_execution_terms.cpp | 61 +- tests/test_native_group_absorption.cpp | 1014 +++++++++++++++++ tests/test_native_order_resting_core.cpp | 81 +- tests/test_native_order_terms_core.cpp | 80 +- tests/test_native_resting_acceptance.cpp | 76 +- tests/test_native_resting_replay_contract.cpp | 62 +- 22 files changed, 1820 insertions(+), 304 deletions(-) create mode 100644 tests/test_native_group_absorption.cpp diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d21d14ce..194ba34d 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:241 and `RELEASE_MIN_TESTS` -ci_verify.py:349. +floor — `KERNEL_MIN_TESTS` ci_verify.py:242 and `RELEASE_MIN_TESTS` +ci_verify.py:350. 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 c41769e6..8f96c92b 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -337,11 +337,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:2637`), runs a batch - (`strategy_native_run_v1` `native_c_api.h:2650`), and **submits, replaces, cancels and - executes** orders — `strategy_native_submit_v1` (`native_c_api.h:2678`), `_replace_v1` - (`native_c_api.h:2700`), `_cancel_v1` (`native_c_api.h:2740`), `_cancel_all_v1` (`native_c_api.h:2744`), `_cancel_where_v1` (`native_c_api.h:2765`), - `_execute_current_v1` (`native_c_api.h:2786`) — 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 @@ -464,6 +464,7 @@ see a mechanism, so these rows are held by reading, not by the checker): | the event record a run keeps — `NativeRunSpec::event_retention` (`include/pineforge/native_run_spec.hpp`), the journal window (`WorkingRequestCore::retire_history`, `include/pineforge/native_order.hpp`; `NativeExecutionConsumer::retire_journal`, `src/native_execution_consumer.cpp`) and the reader's acknowledgement (`NativeStrategyHost::native_acknowledge_events`, `native_event_window_start`, `include/pineforge/native_host.hpp`) | the host declares a retention in its spec and acknowledges what it has read; the Pine adapter declares `Window` in `PineExecutionAdapter::project()` and acknowledges its receipt cursor in `observe_terminal_receipts` (and "nothing read yet" at its run begin); the kernel retires at every script-bar boundary | **a reporting policy with a reader acknowledgement** (R5 lane V19-B). Rule 2 (a), mechanism: what a run keeps of its own event record, and a reader telling the kernel what it has consumed, the same for every host; the kernel never retires an event its own live state reads (a queued applied notification, a live deferred group-adjustment chain), and what the journal used to answer beyond that is state (next row). Rule 2 (b), knob: a spec field with no TradingView meaning -- `Window`, `Commands`, `Full` -- folded into the spec digest only off the default; a C caller that does not send the word keeps `Full`, the record its layout was published with. No trade, and no Pine value, depends on it: the adapter reads above its own cursor only. Pinned by `tests/test_native_event_retention.cpp` (kernel-only: `Full` row for row against the pre-window record, the window and the acknowledgement's edges, and the same books booking the same trades with the window closing at every driver point) and `check_event_retention` of `tests/test_native_c_api.c`. | | 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`. | 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 (→ @@ -592,21 +593,21 @@ 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:2176`), `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:3051`), reading a SIZED request's own sizing block | the sizing-query scenario | -| `resolve_execution_terms`, price half | `on_execution_terms` (`native_c_api.h:2562`): 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:2571`): 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:2578`) | the anchored-level scenario | -| `hash_host_extension` | `on_hash_extension` (`native_c_api.h:2589`): 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: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:2744`). | +| `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`). | | `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:3023`). | -| `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:1681`) 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: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`). | +| `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`. | @@ -615,7 +616,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:1867`) | **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) @@ -673,7 +674,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: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` | | `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:2226`, `::gaps` `include/pineforge/native_c_api.h:2230` | 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: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/ci.md b/docs/ci.md index 7b7b59d8..c0495e46 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -100,9 +100,9 @@ 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 658 for Debug -and sanitizers and 667 for native (653 and 662 at the INT24 base, plus wave G's -five rows). The full-run release and kernel floors are 677 and 276 rows that +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 ran; full runs do not exclude a label. Preflight also runs the detached-comment census of the kernel compile closure @@ -520,7 +520,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 27 CTest rows labelled `slow` in `tests/CMakeLists.txt`. These rows were +the 28 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. @@ -528,7 +528,9 @@ INT25 re-measured the rows wave G enlarged or added (RATIO-HARDEN's timing legs, V19-FIX's scaling rows, and the new KERNEL-EDGE, K-ULP4, C-SURFACE-1 and DOC-TRUTH-4 rows) in a full sanitizers and Debug run on the maintainers' x86-64 verification host (twelve parallel CTest jobs): none crosses either threshold, -so the 27 labelled rows stand. The largest unlabelled rows there were +so the 27 labelled rows stand. K-ULP5's `test_native_group_absorption`, +picked after that run, took 70.4 s under sanitizers (17.4 s Debug) in its own +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 diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 100ca8a6..f603019f 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:1852, *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:1883, *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). | @@ -49,14 +49,14 @@ 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 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 | — | -| 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:1013-1027; result and event types `SubmitStatus` native_order.hpp:835-861, `ReplacedEvent` native_order.hpp:997-1063 | 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:1673); 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: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:1016-1030; result and event types `SubmitStatus` native_order.hpp:835-861, `ReplacedEvent` native_order.hpp:1000-1066 | 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:1702); 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 | — | | 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:1172-1182 (deferred form native_order.hpp:1172-1182) | K | — | F:A12 O:A6 O:A7 S:O7 | — | +| 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:1190-1200 (deferred form native_order.hpp:1190-1200) | 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:1852); 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:1883); 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. | @@ -82,7 +82,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | 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:1391-1408; 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:1420-1437; 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. | | 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 | @@ -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:2038-2046. 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: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. | @@ -347,7 +347,7 @@ the campaign's own anchor audit found that the first thing to rot: - **(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. - **(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:1278). Every new test unit compiles first against the frozen previous-epoch header closure (fail-before, `CLAUDE.md`). +- **(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:1307). 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. ### 3.2 Lanes: scope, size, dependencies, epochs @@ -1285,7 +1285,7 @@ NativeMetricsView native_metrics() const; // S: net_pro ### A.2 L3 — sizing, fractional reduce, sibling reservation (F P1, P7 + O G2 + S P2, P3) ```cpp -// native_order.hpp — native_order_v6; OrderIntent grows 5 -> 6 (static_assert native_order.hpp:1358) +// native_order.hpp — native_order_v6; OrderIntent grows 5 -> 6 (static_assert native_order.hpp:1387) struct CashValue { double cash = 0.0; }; // account currency struct EquityFraction { double fraction = 0.0; }; // of marked equity at the sizing point (0.10 = 10 %) using SizeBasis = std::variant; @@ -1330,7 +1330,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:1278). 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:1307). 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). - 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). @@ -1405,7 +1405,7 @@ BracketReceipt submit_bracket(NativeStrategyHost&, const BracketSpec&); // emi template class OrderBook; // id -> handle: submit-or-replace, cancel-by-id ``` -- A leg with owner `WaitForApplied` / `BindOpening` and a non-absolute anchor gets its level when the owner's fill arms it (`ArmedEvent` native_order.hpp:1216-1223 already exists) (O). Fallback with no kernel change: the toolkit materializes relative levels from the host's `on_native_applied` (F). +- A leg with owner `WaitForApplied` / `BindOpening` and a non-absolute anchor gets its level when the owner's fill arms it (`ArmedEvent` native_order.hpp:1245-1252 already exists) (O). Fallback with no kernel change: the toolkit materializes relative levels from the host's `on_native_applied` (F). - Gap lane N13 (after R4d): two facts of the arm were missing and are now opt-in fields appended to `WaitForApplied`, each default byte-identical and folded only when set. `NativeArmFirstMatch { AtArmPrint, AfterArmPrint }`: whether the armed leg is a candidate at its owner's fill print or, like a request born in the owner's fill callback, only after it. `NativeArmScope { OwnerLot, Book }`: whether an armed closing leg closes the lot its owner opened or is bound at the arm to the whole position (a `BookClose`), under which it may be `HostSized`. Ruling: generic — both are broker models of a contingent child, neither names a source language — and they are what makes an anchored child the very request a host would submit from `on_native_applied`. The Pine adapter's relative `strategy.exit` legs use both; R4d's owner-lot spelling left a later same-id add open where the source leg closes the book (`rel-pyramid-set-once`, tests/test_adapter_brackets_relower.cpp). - Not adopted (R5-2): S's kernel bracket object, its C symbols `strategy_native_bracket_*` and its TU list (`engine_orders.cpp`, `engine_execution.cpp`). - TUs: `native_order.{hpp,cpp}` (arming), `native_matching.hpp` (`checked_trail_stop`), `native_execution_consumer.cpp` (`prepare_owner_applied` path, command surface). diff --git a/docs/native-settlement.md b/docs/native-settlement.md index 2d08ec3b..8b05da12 100644 --- a/docs/native-settlement.md +++ b/docs/native-settlement.md @@ -107,11 +107,11 @@ residual. A selection closed by its whole sum was already consumed whole, and `Flatten` needs no quantity at all. What a native host gets, then, from a quantity request on the exact settlement: -exactly one terminal outcome, and no run stopped for its quantity (one group -case is left, below). A close that spans lots and ends inside one is one fill -charged its request (K-ULP2); a close whose binary64 FIFO sum reaches its -request at a lot closes that lot whole (K-ULP3); a `Transact` that crosses the -book is one fill charged its units (K-ULP1, in +exactly one terminal outcome, and no run stopped for its quantity -- a +sibling's share of it included (below). A close that spans lots and ends +inside one is one fill charged its request (K-ULP2); a close whose binary64 +FIFO sum reaches its request at a lot closes that lot whole (K-ULP3); a +`Transact` that crosses the book is one fill charged its units (K-ULP1, in `docs/pages/native-engine.md`); a request the settlement cannot book exactly is refused, typed, as above -- the settlement's own refusal shows beforehand in `inspect_current_execution` as a `settlement_readiness` of @@ -139,15 +139,22 @@ lot, or found nothing to close, and the book's own sizes were `OffGrid`. A that is no boundary of its scope: a later lot's size would be taken FIFO from the head lot and split it off the grid. -One case still stops the run, as it did before K-ULP4, and it is the request -core's, not the settlement's: in an OCA group whose effect is -`GroupEffect::Reduce`, a member's fill smaller than half an ulp of a sibling's -remaining units cannot be deducted from them, and after the fill is booked the -run fails with code 6, discriminator 7 (`CoreFailure::UnrepresentableReservation`). -It takes a dust-sized fill -- the close of a dust lot -- beside a far larger -resting sibling. A sibling's fill also lowers a member's remaining units -without regard to the quantity grid; the grid's re-check above holds only the -`Reduce` it admitted as a boundary. +A sibling's share of a fill does not stop the run either, and it is the +request core's, not the settlement's. In an OCA group whose effect is +`GroupEffect::Reduce` a member's fill is deducted from every live sibling -- +from its remaining units, or, while they are not known, into its pending +total, which its terms or its owner's fill later takes off the units they bind +-- and a deduction too small to move what it is taken from (`fl(units - d)` is +the units again: at most half an ulp of them) is absorbed. The sibling keeps +its units, the exact binary64 result of the subtraction; the member's fill +stands; and the event that records the deduction took nothing (R5 lane K-ULP5; +`docs/pages/native-engine.md`, "What a host gets from a quantity"). It takes a +dust-sized fill -- the close of a dust lot -- beside a far larger sibling, and +until K-ULP5 it failed the run after the fill was booked, with code 6, +discriminator 7 (`CoreFailure::UnrepresentableReservation`); only a pending +total that overflows binary64 still does. A sibling's fill also lowers a +member's remaining units without regard to the quantity grid; the grid's +re-check above holds only the `Reduce` it admitted as a boundary. ## Quantity tolerance diff --git a/docs/pages/contributing-llm.md b/docs/pages/contributing-llm.md index 59d831c3..63fee8cb 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:241 and - `RELEASE_MIN_TESTS` ci_verify.py:349. Adding rows means raising the floor + *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 in the same commit. ## The recipe for a lane @@ -241,7 +241,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:2639. +`strategy_native_*` surface `sha256:1a4d41202e1b32a33ab75c2e42a36285ccb1d061087a8db1c19162b244c5b29f` native_c_api.h:2641. **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 @@ -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:241, `RELEASE_MIN_TESTS` ci_verify.py:349). It +(`KERNEL_MIN_TESTS` ci_verify.py:242, `RELEASE_MIN_TESTS` ci_verify.py:350). 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 eafe4297..4023b2ba 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -488,7 +488,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:2728-2778`), 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` @@ -2884,7 +2884,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 (`sha256:2e963d6ab1630db1535bd944dc7406ba649e9d14a589065db25347569fbad150` native_c_api.h:37-39) take a `pf_strategy_t` from `strategy_native_host_create_v1` unchanged — and -`strategy_native_submit_v1` (`native_c_api.h:2678`) 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 @@ -3098,9 +3098,9 @@ a 2^-49-unit remainder that a second fill opened as a dust lot. ### What a host gets from a quantity Every quantity request ends in exactly one terminal outcome, and no ordinary -quantity stops the run (one OCA-group case is left, last below). The FIFO -rules are the settlement's (`docs/native-settlement.md`, "Physical lots and -quantities"); what a host sees: +quantity stops the run, nor does a sibling's share of it (last below). The +FIFO rules are the settlement's (`docs/native-settlement.md`, "Physical lots +and quantities"); what a host sees: - A close that spans lots and ends inside one is one terminal fill whose `closed_units` and `filled_working` are its request, even where the rows, @@ -3137,12 +3137,29 @@ quantities"); what a host sees: meet the refusal above. A C host sets it in the `quantity_tolerance` tail of `pf_native_run_spec_ext_v1`. The Pine adapter does not declare it and keeps its own `1e-10` rule (`docs/native-settlement.md`, "Quantity tolerance"). -- Still a run failure, as before K-ULP4: in an OCA group with - `GroupEffect::Reduce`, a member's fill smaller than half an ulp of a - sibling's remaining units cannot be deducted from them, and after the fill - is booked the run fails with code 6, discriminator 7 - (`CoreFailure::UnrepresentableReservation`) -- a dust-sized fill beside a far - larger resting sibling. +- A deduction an OCA-Reduce sibling cannot take is absorbed, and the run goes + on (R5 lane K-ULP5). In a group with `GroupEffect::Reduce` a member's fill is + deducted from every live sibling: from its remaining units (a + `ReservationReducedEvent`; C `PF_NATIVE_EVENT_RESERVATION_REDUCED`), or, + while its units are not known, into its pending total (a + `DeferredGroupAdjustmentEvent`; C `PF_NATIVE_EVENT_DEFERRED_GROUP`), which + its terms (`TermsResolvedEvent`) or its owner's fill (`QuantityBoundEvent`) + later takes off the units they bind. A deduction too small to move what it + is taken from -- `fl(units - d)` is the units again, at most half an ulp of + them -- leaves the sibling's units, or its pending total, as they were, the + exact binary64 result; the member's fill stands, and the event says it took + nothing: `actual_deduction` 0 with `after` equal to `before`, or + `deferred_delta` 0 with `pending_after` equal to `pending_before` (it joins + no chain), or `effective_deduction` 0 beside a positive `pending_total`; a C + row's `closed_units` is 0. After open 0.5, open 1000.1 and a bound close of + 1000, one group of a resting `Reduce 1000`, a `Reduce 0.3` and a + `ScopeFraction 1`: the fraction closes 2.2759572004815709e-14, below half an + ulp of the resting member's 999.7, which keeps 999.70000000000005. A pending + total below half an ulp of a later fill rounds into it, as binary64 addition + does. Until K-ULP5 each of these failed the run after the fill was booked, + with code 6, discriminator 7 (`CoreFailure::UnrepresentableReservation`); a + pending total that overflows binary64 -- fills near the largest finite + double -- still does. ### Sizing without a host override diff --git a/docs/pages/pine-to-native.md b/docs/pages/pine-to-native.md index d5e1bb22..131fb4d5 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -67,12 +67,12 @@ gain: and nothing comes from a chart. - **Typed orders and typed outcomes.** A request is a value (`Request` native_order.hpp:483); its fate is a typed event — `AcceptedEvent` - (native_order.hpp:997), `ExecutionAppliedEvent` (native_order.hpp:1136), - `MatchRejectedEvent` (native_order.hpp:1098), `CancelledEvent` - (native_order.hpp:1039) — readable in order from `native_events` + (native_order.hpp:1000), `ExecutionAppliedEvent` (native_order.hpp:1145), + `MatchRejectedEvent` (native_order.hpp:1107), `CancelledEvent` + (native_order.hpp:1042) — 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:2454), 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++. - **One binary, no transpiler.** Link `libpineforge`, ship an executable or a loadable module. @@ -112,8 +112,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:2265) -through `strategy_configure_native_ext_v1` (native_c_api.h:3111). 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. @@ -127,15 +127,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 `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`. | -| `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:2102 | `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:2106 | `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:2276 | `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:2276 | `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:2286 | `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:2290 | `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: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. | | `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:2329 | `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: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`. | @@ -267,7 +267,7 @@ 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:2546 for a close. +the same question through `on_close_units` native_c_api.h:2548 for a close. ## Orders {#pine_to_native_map_orders} @@ -275,18 +275,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:2678 over a -`pf_native_request_v1` native_c_api.h:2093. +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:2678 | `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:2678 | `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:2678 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:1155. 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.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: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:1164. 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:2765 | `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:2744 | `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: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.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. | @@ -303,7 +303,7 @@ 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 -`ReplacedEvent` native_order.hpp:1013 and can carry the predecessor's live +`ReplacedEvent` native_order.hpp:1016 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 spelling is `strategy_native_replace_v1`, or `strategy_native_replace_ext_v1` @@ -317,7 +317,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:2110-2117 | `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:2111-2115 | `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:2116 | `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:2990). 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: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. | `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 @@ -340,7 +340,7 @@ once. Natively they are separate and each is nameable: `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 - in `ArmedEvent` native_order.hpp:1216. + in `ArmedEvent` native_order.hpp:1245. - **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 @@ -372,10 +372,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:2678 with +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:454, and its `first_match` and `scope` are the request's `arm_first_match` / `arm_scope` -native_c_api.h:2176-2177, 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 @@ -398,10 +398,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:2312 | `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:2315 | `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:2317 | `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:2319 | `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: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 @@ -420,19 +420,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:2286 | `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:2291 | `native_margin_strategy.cpp` | -| how much a liquidation closes | `NativeLiquidationSizing` native_run_spec.hpp:140 | `margin_sizing` native_c_api.h:2281 | `native_margin_strategy.cpp` | -| when the requirement is tested | `NativeLiquidationCheck` native_run_spec.hpp:158 | `margin_check` native_c_api.h:2282 | `native_margin_strategy.cpp` | -| which equity it is tested against | `NativeMarginEquityBasis` native_run_spec.hpp:172 | `margin_equity_basis` native_c_api.h:2349 | `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:2350 | `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:2351 | `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:2571 | `native_margin_strategy.cpp` | -| the host's own requirement | `resolve_margin_requirement` native_host.hpp:964 | `on_margin_requirement` native_c_api.h:2508 | `native_margin_strategy.cpp` | -| which check points run at all | `margin_check_allowed` native_host.hpp:976 | `on_margin_check` native_c_api.h:2517 | `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: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` | | 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:2524 | `native_margin_strategy.cpp` | -| the liquidation event | `MarginCallEvent` native_order.hpp:1232 delivered to `on_native_margin_call` native_host.hpp:989 | `on_margin_call` native_c_api.h:2472 | `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:2526 | `native_margin_strategy.cpp` | +| the liquidation event | `MarginCallEvent` native_order.hpp:1261 delivered to `on_native_margin_call` native_host.hpp:989 | `on_margin_call` native_c_api.h:2474 | `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 @@ -446,15 +446,15 @@ openings TradingView takes. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| script body at bar close | `on_native_bar` native_host.hpp:881 | `on_bar` native_c_api.h:2462 | `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:2490 | `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:2490 | `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:1754 | `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:2461 | `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:2975 | `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:2464 | `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:2459 | `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:2500 | `tests/test_native_calc_timing.cpp` | `NativeCalculationReason::SubBar` is reserved and never delivered to the recalculate hook — a sub-bar has its own callback. | +| 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. | +| `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. | | `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.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. | @@ -544,10 +544,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:2791 | `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:2791 | `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:2832 | `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:3023 | `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: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.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. | @@ -557,9 +557,9 @@ 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:2824 then -`strategy_native_open_lot_get_v1` native_c_api.h:2832 into a -`pf_native_open_lot_v1` native_c_api.h:1730, 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 @@ -572,19 +572,19 @@ 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:2824 | `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:1730 | `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:1730 | `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:1730 | `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:1730 | `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:1730 | `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:1730 | `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:1730 | `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:1730 | `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: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.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:1730 | `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:1730 | `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. | @@ -613,7 +613,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: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:1730) 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: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_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` | | @@ -647,8 +647,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:2896 | `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:3173 | `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: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. | | `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. | @@ -733,21 +733,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:2454 table handed to -`strategy_native_host_create_v1` native_c_api.h:2637; 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:2265 through `strategy_configure_native_ext_v1` -native_c_api.h:3111, and `strategy_native_run_v1` native_c_api.h:2650 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 make the C door behave like the C++ one: 1. **Every struct is tagged and size-prefixed.** Set `struct_size` - native_c_api.h:2094 and `version` native_c_api.h:2095; 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:2877. + `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. diff --git a/include/pineforge/native_c_api.h b/include/pineforge/native_c_api.h index f3c242e1..0d6d81d6 100644 --- a/include/pineforge/native_c_api.h +++ b/include/pineforge/native_c_api.h @@ -1566,7 +1566,9 @@ typedef struct pf_native_applied_v1 { * bound to, `cycle_after` its cycle, cursor fields. * - RESERVATION_REDUCED / DEFERRED_GROUP: `incarnation` is the recipient, * `reason` its #pf_native_group_effect_t, `closed_units` the deduction - * (applied, or deferred). + * (applied, or deferred); 0 when it was absorbed -- too small to move the + * recipient's remaining units, or its pending total, in binary64, which + * stay as they were (R5 lane K-ULP5). * - QUANTITY_BOUND: `incarnation`, `opened_units` = the source units. * - TERMS_RESOLVED: `raw_price`, `resolved_price` (= `price`), cursor fields. * - MARGIN_CALL: `reason` is the #pf_native_side_t of the liquidated diff --git a/include/pineforge/native_order.hpp b/include/pineforge/native_order.hpp index 415d9db7..83f66d25 100644 --- a/include/pineforge/native_order.hpp +++ b/include/pineforge/native_order.hpp @@ -974,6 +974,9 @@ enum class CoreFailure : std::uint8_t { StaleHandle = 4, UnsupportedTransition = 5, ConflictingReceipt = 6, + /// A group deduction's pending total that overflows binary64. A deduction + /// binary64 cannot take off its recipient is absorbed instead (R5 lane + /// K-ULP5); it failed the run with this discriminator before. UnrepresentableReservation = 7, InvalidScope = 8, InvalidProposal = 9, @@ -1076,6 +1079,12 @@ struct NoEffectEvent { const Birth& birth() const noexcept { return definition->birth; } }; +/// One candidate's execution terms, recorded. For a request whose units were +/// deferred it also binds them: it spends the pending group chain +/// (`prior_adjustment_ids`, `pending_total`), takes `effective_deduction` off the +/// resolved units and leaves `remaining_after`. That deduction is 0 with a +/// positive `pending_total` when binary64 cannot take the total off the units -- +/// absorbed, and the units stand (R5 lane K-ULP5). struct TermsResolvedEvent { uint64_t ordinal = 0; DefinitionRef definition; @@ -1169,6 +1178,15 @@ struct ExecutionAppliedEvent { } }; +/// A group effect's deduction from a live sibling whose remaining units are +/// known (GroupEffect::Reduce): `requested_delta` is the member fill's +/// `filled_working`, `actual_deduction` the part taken -- capped at `before`, +/// which then cancels the sibling -- and `after` is fl(before - +/// actual_deduction). A deduction binary64 cannot take off `before` (at most +/// half an ulp of it: fl(before - d) is `before`) is absorbed (R5 lane K-ULP5): +/// `actual_deduction` is 0 and `after` equals `before`, the exact binary64 +/// result, and the member's fill stands. A deduction that is taken is positive, +/// so an `actual_deduction` of 0 names an absorbed one. struct ReservationReducedEvent { uint64_t ordinal = 0; DefinitionRef definition; @@ -1181,6 +1199,14 @@ struct ReservationReducedEvent { RemainingProjection after = RemainingProjectionUnits{}; }; +/// A group effect's deduction deferred into a sibling whose units are not known +/// yet: its pending total grows by `deferred_delta`, in commit order, and the +/// receipts chain back through `previous_pending_receipt` until the terms or +/// the owner fill that binds the sibling's units takes the total off them. A +/// fill the total cannot move (fl(total + fill) is the total) is absorbed (R5 +/// lane K-ULP5): `deferred_delta` is 0, `pending_after` is `pending_before` and +/// the event joins no chain. A total below half an ulp of a later fill rounds +/// into it, as binary64 addition does. struct DeferredGroupAdjustmentEvent { uint64_t ordinal = 0; DefinitionRef definition; @@ -1195,7 +1221,10 @@ struct DeferredGroupAdjustmentEvent { /// A Reduce{OwnerOpenedUnits} was bound, at its owner's fill, to what that fill /// opened. It is how a bracket child's size becomes a number, and it precedes the -/// ArmedEvent of the same drain. +/// ArmedEvent of the same drain. `effective_deduction` is the part of the child's +/// pending group total taken off `source_units`; it is 0 with a positive +/// `pending_total` when binary64 cannot take that total off them -- absorbed, and +/// the owner's units stand (R5 lane K-ULP5). struct QuantityBoundEvent { uint64_t ordinal = 0; DefinitionRef definition; @@ -1840,7 +1869,9 @@ class WorkingRequestCore { const EvaluationContext& context, const TargetObservation& observation); /// Pure arithmetic over the cached pending total. Outputs are assigned only - /// after every validation and subtraction succeeds. + /// after every validation and subtraction succeeds. A total binary64 cannot + /// take off the resolved units is absorbed: the deduction is 0 and the + /// units stand (R5 lane K-ULP5). static bool effective_host_units(const PendingAdjustments& pending, double resolved_units, double* deduction, diff --git a/scripts/ci_preflight.py b/scripts/ci_preflight.py index 8ef88cfd..e948da8a 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 = '90f8932f921579c55561e1af962610e0e311d1f93f28722a4c5931d7b6f7fb53' +PR_SLOW_ROWS_SHA256 = 'c1abd9bdd0c540398eb5e0870a0f652b12743d653eafb5c4ee9ee94ca6dc4cf0' def _jobs(workflow: str) -> dict[str, str]: @@ -127,7 +127,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) != 27 or len(set(names)) != 27 + if (len(blocks) != 1 or len(names) != 28 or len(set(names)) != 28 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 24ccc5af..61adeaf7 100644 --- a/scripts/ci_verify.py +++ b/scripts/ci_verify.py @@ -226,7 +226,7 @@ # dual-entry witness reach the source layer (see RELEASE_MIN_TESTS), and # B-C-SURFACE and B-DOCS add no row. 272 registered, 271 run: the WebSocket # row still skips on a system libcurl. -# 276 run = those 271 plus the five source-free TUs of wave G (INT25) -- +# 277 run = those 271 plus the six source-free TUs of wave G (INT25) -- # measured on the integrated tree (ctest -N), not summed from the lanes' own # bases: # +1 C-SURFACE-1 test_native_c_api_int23_header (the INT23 frozen-header @@ -234,11 +234,12 @@ # +1 KERNEL-EDGE test_native_kernel_edge # +2 K-ULP4 test_native_unrepresentable_refusal, # test_native_quantity_tolerance +# +1 K-ULP5 test_native_group_absorption # +1 DOC-TRUTH-4 test_native_engine_complete_host (the guide's Complete host, # extracted from the page and run) -# All five register in release too; RATIO-HARDEN, V19-FIX and K-IDX add no row. -# 277 registered, 276 run: the WebSocket row still skips on a system libcurl. -KERNEL_MIN_TESTS = 276 +# 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 # 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 @@ -340,19 +341,19 @@ # +1 INT24 test_adapter_dual_entry_tie (the ruling on B-ADAPTER's # finding 1) # B-C-SURFACE's witnesses are rows inside test_native_c_api. -# 677 = those 672 plus the five wave-G (INT25) rows KERNEL_MIN_TESTS lists -# above -- C-SURFACE-1 +1, KERNEL-EDGE +1, K-ULP4 +2, DOC-TRUTH-4 +1 -- which -# register here too, counted with ctest -N on the integrated tree; no wave-G -# lane adds a source-bound row (K-ULP4's C checks and V19-FIX's scaling rows -# are rows inside existing TUs). No release row skips, so 677 registered is 677 -# run. -RELEASE_MIN_TESTS = 677 +# 678 = those 672 plus the six wave-G (INT25) rows KERNEL_MIN_TESTS lists +# above -- C-SURFACE-1 +1, KERNEL-EDGE +1, K-ULP4 +2, K-ULP5 +1, DOC-TRUTH-4 +1 +# -- which register here too, counted with ctest -N on the integrated tree; no +# 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 # 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 five rows (C-SURFACE-1 +1, -# KERNEL-EDGE +1, K-ULP4 +2, DOC-TRUTH-4 +1) in each. An excluded run must -# still discover at least this many rows before -LE. -EXCLUDED_REGISTERED_MIN = {'debug': 658, 'sanitizers': 658, 'native': 667} +# 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} # 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 5822a3d3..c335c666 100644 --- a/scripts/test_ci_verify.py +++ b/scripts/test_ci_verify.py @@ -1326,7 +1326,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': 658, 'sanitizers': 658, 'native': 667}) + {'debug': 659, 'sanitizers': 659, 'native': 668}) 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_order.cpp b/src/native_order.cpp index 275ee510..888d2d60 100644 --- a/src/native_order.cpp +++ b/src/native_order.cpp @@ -166,6 +166,36 @@ bool checked_sub_cap(double before, double deduct, double* after, bool* exhauste return true; } +// A group deduction binary64 cannot take off what it is taken from: `deduct` +// is below `before`, and fl(before - deduct) is `before` again -- at most half +// an ulp of it (R5 lane K-ULP5). The deduction is absorbed: the units stay as +// they are, which is the exact binary64 result of the subtraction, and the +// event that records it deducts nothing. +bool absorbed_deduction(double before, double deduct) noexcept { + return std::isfinite(before) && std::isfinite(deduct) && deduct > 0.0 && deduct < before + && before - deduct == before; +} + +// A deferred recipient's pending total is the binary64 sum of the fills +// deducted into it, in commit order. A delta the total absorbs is not added +// (absorbed_pending_delta); a total below half an ulp of a later delta rounds +// into it, as the addition does; only a sum that overflows is unrepresentable +// (R5 lane K-ULP5; until then both absorbing sums stopped the run too). +bool pending_sum(double total, double delta, double* out) noexcept { + if (!std::isfinite(total) || !std::isfinite(delta) || total < 0.0 || !(delta > 0.0)) { + return false; + } + const double sum = total + delta; + if (!std::isfinite(sum) || !(sum > total)) return false; + *out = sum; + return true; +} + +bool absorbed_pending_delta(double total, double delta) noexcept { + return std::isfinite(total) && std::isfinite(delta) && total > 0.0 && delta > 0.0 + && total + delta == total; +} + TriggerState trigger_state_from(const Trigger& trigger) { if (std::holds_alternative(trigger)) return MarketReady{}; if (std::holds_alternative(trigger)) return LimitReady{}; @@ -1345,7 +1375,8 @@ bool WorkingRequestCore::collect_pending_chain(const PendingAdjustments& pending std::reverse(ids->begin(), ids->end()); // Reproduce the exact accumulation order used when each receipt was // committed. Reassociating these additions tail-first changes binary64 - // totals for valid chains such as 0.1, 0.2, 0.3. + // totals for valid chains such as 0.1, 0.2, 0.3. An absorbed delta joined + // no chain, so every link here moved the total (pending_sum). double sum = 0.0; for (std::size_t i = 0; i < ids->size(); ++i) { const CommandEvent* event = event_at((*ids)[i]); @@ -1358,7 +1389,7 @@ bool WorkingRequestCore::collect_pending_chain(const PendingAdjustments& pending if (!before || before->total != sum || before->count != i || before->tail_receipt != (*ids)[i - 1]) return false; } - if (!checked_add_positive(sum, payload->deferred_delta, &sum)) return false; + if (!pending_sum(sum, payload->deferred_delta, &sum)) return false; if (payload->pending_after.total != sum || payload->pending_after.count != i + 1 || payload->pending_after.tail_receipt != (*ids)[i]) return false; } @@ -2876,11 +2907,14 @@ bool WorkingRequestCore::effective_host_units(const PendingAdjustments& pending, if (!std::isfinite(pending_total) || pending_total < 0.0) return false; } - const double computed_deduction = std::min(pending_total, resolved_units); + double computed_deduction = std::min(pending_total, resolved_units); double computed_after = resolved_units; bool computed_exhausted = computed_deduction == resolved_units; if (resolved_units == 0.0 || computed_exhausted) { computed_after = 0.0; + } else if (absorbed_deduction(resolved_units, computed_deduction)) { + // Absorbed (K-ULP5): the resolved units stand and nothing is deducted. + computed_deduction = 0.0; } else if (computed_deduction > 0.0) { if (!checked_sub_cap(resolved_units, computed_deduction, &computed_after, &computed_exhausted)) { @@ -4923,12 +4957,19 @@ Preparation WorkingRequestCore::prepare_group_effect( const uint64_t ordinal = usable_ordinal(next_timeline_ordinal); PendingDeferred after; PendingAdjustments before = live.pending; + // A fill the pending total absorbs (K-ULP5) joins no chain: the total, + // its count and its tail stay, and the event defers nothing. + bool absorbed = false; if (const auto* pending = std::get_if(&live.pending)) { - if (!checked_add_positive(pending->total, payload->filled_working, &after.total)) { + if (absorbed_pending_delta(pending->total, payload->filled_working)) { + absorbed = true; + after = *pending; + } else if (!pending_sum(pending->total, payload->filled_working, &after.total)) { return PreparationError{CoreFailure::UnrepresentableReservation, applied, recipient}; + } else { + after.count = pending->count + 1; + after.tail_receipt = EventId{identity_, ordinal}; } - after.count = pending->count + 1; - after.tail_receipt = EventId{identity_, ordinal}; } else { after.total = payload->filled_working; after.count = 1; @@ -4943,18 +4984,20 @@ Preparation WorkingRequestCore::prepare_group_effect( deferred.cause = applied; deferred.recipient = recipient; deferred.effect = GroupEffect::Reduce; - deferred.deferred_delta = payload->filled_working; + deferred.deferred_delta = absorbed ? 0.0 : payload->filled_working; deferred.pending_before = before; deferred.pending_after = after; if (const auto* pending = std::get_if(&before)) { - deferred.previous_pending_receipt = pending->tail_receipt; + if (!absorbed) deferred.previous_pending_receipt = pending->tail_receipt; } - LiveRequest updated = live; - updated.pending = after; plan.events.emplace_back(std::move(deferred)); - plan.live_change = kLiveUpdate; - plan.live_index = live_index; - plan.live_row = std::move(updated); + if (!absorbed) { + LiveRequest updated = live; + updated.pending = after; + plan.live_change = kLiveUpdate; + plan.live_index = live_index; + plan.live_row = std::move(updated); + } plan.receipt_outcome = ordinal; return finish_mutation(std::move(plan)); } @@ -4971,9 +5014,13 @@ Preparation WorkingRequestCore::prepare_group_effect( const double before_q = working_units(live.remaining); const double deduct = std::min(payload->filled_working, before_q); - double after_q = 0.0; + double after_q = before_q; bool exhausted = false; - if (!checked_sub_cap(before_q, deduct, &after_q, &exhausted)) { + // A deduction binary64 cannot take off the remaining units is absorbed + // (K-ULP5): the units stand, the event records a deduction of 0, and the + // row is not touched. + const bool absorbed = absorbed_deduction(before_q, deduct); + if (!absorbed && !checked_sub_cap(before_q, deduct, &after_q, &exhausted)) { return PreparationError{CoreFailure::UnrepresentableReservation, applied, recipient}; } const uint64_t ordinal = usable_ordinal(next_timeline_ordinal); @@ -4984,7 +5031,7 @@ Preparation WorkingRequestCore::prepare_group_effect( reduced.recipient = recipient; reduced.effect = GroupEffect::Reduce; reduced.requested_delta = payload->filled_working; - reduced.actual_deduction = deduct; + reduced.actual_deduction = absorbed ? 0.0 : deduct; reduced.before = RemainingUnits{before_q}; reduced.after = RemainingProjectionUnits{after_q}; plan.events.emplace_back(std::move(reduced)); @@ -5000,7 +5047,7 @@ Preparation WorkingRequestCore::prepare_group_effect( plan.events.emplace_back(make_cancelled(cancel_ord, snapshot, CancelReason::Group, applied)); plan.live_change = kLiveErase; plan.live_index = live_index; - } else { + } else if (!absorbed) { LiveRequest updated = live; updated.remaining = RemainingUnits{after_q}; plan.live_change = kLiveUpdate; @@ -5080,12 +5127,18 @@ Preparation WorkingRequestCore::apply_group_effect( const uint64_t ordinal = usable_ordinal(next_timeline_ordinal); PendingDeferred after; PendingAdjustments before = live.pending; + // prepare_group_effect, step for step: an absorbed fill joins no chain. + bool absorbed = false; if (const auto* pending = std::get_if(&live.pending)) { - if (!checked_add_positive(pending->total, filled_working, &after.total)) { + if (absorbed_pending_delta(pending->total, filled_working)) { + absorbed = true; + after = *pending; + } else if (!pending_sum(pending->total, filled_working, &after.total)) { return PreparationError{CoreFailure::UnrepresentableReservation, applied, recipient}; + } else { + after.count = pending->count + 1; + after.tail_receipt = EventId{identity_, ordinal}; } - after.count = pending->count + 1; - after.tail_receipt = EventId{identity_, ordinal}; } else { after.total = filled_working; after.count = 1; @@ -5100,16 +5153,16 @@ Preparation WorkingRequestCore::apply_group_effect( deferred.cause = applied; deferred.recipient = recipient; deferred.effect = GroupEffect::Reduce; - deferred.deferred_delta = filled_working; + deferred.deferred_delta = absorbed ? 0.0 : filled_working; deferred.pending_before = before; deferred.pending_after = after; if (const auto* pending = std::get_if(&before)) { - deferred.previous_pending_receipt = pending->tail_receipt; + if (!absorbed) deferred.previous_pending_receipt = pending->tail_receipt; } seal_direct(1, false, true, false); const std::size_t first = history_end(); append_direct(std::move(deferred), false); - live.pending = after; + if (!absorbed) live.pending = after; receipt(ordinal); return Installed{finish_direct(first)}; } @@ -5123,9 +5176,10 @@ Preparation WorkingRequestCore::apply_group_effect( const double before_q = working_units(live.remaining); const double deduct = std::min(filled_working, before_q); - double after_q = 0.0; + double after_q = before_q; bool exhausted = false; - if (!checked_sub_cap(before_q, deduct, &after_q, &exhausted)) { + const bool absorbed = absorbed_deduction(before_q, deduct); + if (!absorbed && !checked_sub_cap(before_q, deduct, &after_q, &exhausted)) { return PreparationError{CoreFailure::UnrepresentableReservation, applied, recipient}; } const uint64_t ordinal = usable_ordinal(next_timeline_ordinal); @@ -5136,7 +5190,7 @@ Preparation WorkingRequestCore::apply_group_effect( reduced.recipient = recipient; reduced.effect = GroupEffect::Reduce; reduced.requested_delta = filled_working; - reduced.actual_deduction = deduct; + reduced.actual_deduction = absorbed ? 0.0 : deduct; reduced.before = RemainingUnits{before_q}; reduced.after = RemainingProjectionUnits{after_q}; if (exhausted) { @@ -5160,7 +5214,7 @@ Preparation WorkingRequestCore::apply_group_effect( seal_direct(1, false, true, false); const std::size_t first = history_end(); append_direct(std::move(reduced), false); - live.remaining = RemainingUnits{after_q}; + if (!absorbed) live.remaining = RemainingUnits{after_q}; receipt(ordinal); return Installed{finish_direct(first)}; } @@ -5286,11 +5340,14 @@ Preparation WorkingRequestCore::prepare_owner_applied( if (!collect_pending_chain(live.pending, child, &ids, &pending_total)) { return PreparationError{CoreFailure::ConflictingReceipt, applied, child}; } - const double deduct = std::min(pending_total, source); + double deduct = std::min(pending_total, source); double after = source; bool exhausted = deduct == source; if (deduct > 0.0 && !exhausted) { - if (!checked_sub_cap(source, deduct, &after, &exhausted)) { + if (absorbed_deduction(source, deduct)) { + // Absorbed (K-ULP5): the owner's units stand, nothing deducted. + deduct = 0.0; + } else if (!checked_sub_cap(source, deduct, &after, &exhausted)) { return PreparationError{CoreFailure::UnrepresentableReservation, applied, child}; } } else if (exhausted) { @@ -5476,11 +5533,13 @@ Preparation WorkingRequestCore::apply_owner_applied( if (!collect_pending_chain(live.pending, child, &ids, &pending_total)) { return PreparationError{CoreFailure::ConflictingReceipt, applied, child}; } - const double deduct = std::min(pending_total, source); + double deduct = std::min(pending_total, source); double after = source; bool exhausted = deduct == source; if (deduct > 0.0 && !exhausted) { - if (!checked_sub_cap(source, deduct, &after, &exhausted)) { + if (absorbed_deduction(source, deduct)) { + deduct = 0.0; // absorbed, as prepare_owner_applied + } else if (!checked_sub_cap(source, deduct, &after, &exhausted)) { return PreparationError{CoreFailure::UnrepresentableReservation, applied, child}; } } else if (exhausted) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d98f8f1e..5002790e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1184,6 +1184,14 @@ list(APPEND TEST_SOURCES test_native_unrepresentable_refusal) # absent, pinned from 91d65ad6), and the C extension's tail. Source-free, so it # also runs in the kernel-only profile. list(APPEND TEST_SOURCES test_native_quantity_tolerance) +# R5 lane K-ULP5: an OCA-Reduce deduction binary64 cannot take off its +# recipient -- off its remaining units, into its pending total, at its terms or +# at its owner's fill -- is absorbed and the run goes on (it stopped with code 6 +# / discriminator 7 on f1af50dc): K-ULP4's p5b probe, each of the four sites, +# a pending total that rounds into a later fill, the overflow that still stops, +# 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 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 @@ -2446,6 +2454,7 @@ set(PINEFORGE_PR_SLOW_TESTS test_native_definition_index test_native_direct_mutation test_native_event_retention + test_native_group_absorption test_native_handle_stable_replace test_native_host_reads test_native_lookup_memos diff --git a/tests/test_native_c_api.c b/tests/test_native_c_api.c index ca7cc99b..29b9c6c5 100644 --- a/tests/test_native_c_api.c +++ b/tests/test_native_c_api.c @@ -1458,6 +1458,238 @@ static void check_unrepresentable_quantity(void) { "a negative tolerance named another field"); } +/* ── R5 lane K-ULP5: an OCA-Reduce deduction binary64 cannot take ─── + * + * K-ULP4's p5b probe through the C surface. A book {0.5, fl(1000.1 - 1000)} + * and three members of one OCA-Reduce group: a resting Reduce 1000 (a sell + * limit at 150 the run never reaches), a market Reduce 0.3 and a market + * ScopeFraction 1. The 0.3 fill lowers the resting member to fl(1000 - 0.3) + * and becomes the fraction's pending deduction; the fraction then closes + * 2.2759572004815709e-14, below half an ulp of 999.7. That deduction is + * absorbed -- a RESERVATION_REDUCED row whose closed_units is 0, the resting + * member's remaining units unchanged -- and the run completes (it failed with + * code 6 before the lane). The pending case: a bracket child waiting on an + * owner that never fills holds 2^60 pending, and a fill of 1 is absorbed into + * it -- a DEFERRED_GROUP row whose closed_units is 0. */ +typedef struct kulp5_state { + pf_strategy_t host; + int bar; + int error; + int pending_case; + uint64_t open1000; + int64_t cycle; + uint64_t resting; + uint64_t fraction; + uint64_t owner; + uint64_t child; +} kulp5_state; + +static uint64_t kulp5_submit(kulp5_state* state, const pf_native_request_v1* request) { + uint64_t incarnation = 0; + uint32_t reject = 0; + if (strategy_native_submit_v1(state->host, request, &incarnation, &reject) != PF_NATIVE_OK + && state->error == 0) { + state->error = 1; + } + return incarnation; +} + +static pf_native_request_v1 kulp5_request(uint32_t intent, uint32_t reduce_size, double value, + int64_t cohort) { + pf_native_request_v1 request = blank_request(); + request.intent = intent; + request.reduce_size = reduce_size; + request.intent_value = value; + if (cohort != 0) { + request.group_kind = PF_NATIVE_GROUP_MEMBER; + request.group_effect = PF_NATIVE_GROUP_REDUCE; + request.group_id = 9; + request.group_cohort = cohort; + } + request.label = "k-ulp5"; + request.comment = ""; + return request; +} + +static int kulp5_on_bar(void* user, const pf_bar_t* bar, const pf_native_decision_v1* at) { + kulp5_state* state = (kulp5_state*)user; + const int b = state->bar++; + pf_native_request_v1 request; + (void)bar; + (void)at; + if (state->pending_case) { + if (b == 0) { + request = kulp5_request(PF_NATIVE_INTENT_TRANSACT, 0u, 1.0, 0); + request.trigger = PF_NATIVE_TRIGGER_STOP; + request.p1 = 200.0; + state->owner = kulp5_submit(state, &request); + request = kulp5_request(PF_NATIVE_INTENT_REDUCE, PF_NATIVE_REDUCE_OWNER_OPENED, 0.0, 2); + request.owner = PF_NATIVE_OWNER_WAIT_FOR_APPLIED; + request.owner_n = 1u; + request.owner_incarnations = &state->owner; + state->child = kulp5_submit(state, &request); + request = kulp5_request(PF_NATIVE_INTENT_TRANSACT, 0u, 0x1p60, 1); + (void)kulp5_submit(state, &request); + } else if (b == 2) { + request = kulp5_request(PF_NATIVE_INTENT_FLATTEN, 0u, 0.0, 0); + (void)kulp5_submit(state, &request); + } else if (b == 4) { + request = kulp5_request(PF_NATIVE_INTENT_TRANSACT, 0u, 1.0, 3); + (void)kulp5_submit(state, &request); + } + return 0; + } + if (b == 0) { + request = kulp5_request(PF_NATIVE_INTENT_TRANSACT, 0u, 0.5, 0); + (void)kulp5_submit(state, &request); + } else if (b == 2) { + request = kulp5_request(PF_NATIVE_INTENT_TRANSACT, 0u, 1000.1, 0); + state->open1000 = kulp5_submit(state, &request); + } else if (b == 4) { + request = kulp5_request(PF_NATIVE_INTENT_REDUCE, PF_NATIVE_REDUCE_EXPLICIT_UNITS, 1000.0, 0); + request.owner = PF_NATIVE_OWNER_BIND_OPENING; + request.owner_n = 1u; + request.owner_incarnations = &state->open1000; + request.owner_cycle = state->cycle; + (void)kulp5_submit(state, &request); + } else if (b == 6) { + request = kulp5_request(PF_NATIVE_INTENT_REDUCE, PF_NATIVE_REDUCE_EXPLICIT_UNITS, 1000.0, 3); + request.trigger = PF_NATIVE_TRIGGER_LIMIT; + request.p1 = 150.0; + state->resting = kulp5_submit(state, &request); + request = kulp5_request(PF_NATIVE_INTENT_REDUCE, PF_NATIVE_REDUCE_EXPLICIT_UNITS, 0.3, 1); + (void)kulp5_submit(state, &request); + request = kulp5_request(PF_NATIVE_INTENT_REDUCE, PF_NATIVE_REDUCE_SCOPE_FRACTION, 1.0, 2); + state->fraction = kulp5_submit(state, &request); + } + return 0; +} + +static int kulp5_on_applied(void* user, const pf_native_applied_v1* applied, + const pf_native_decision_v1* at) { + kulp5_state* state = (kulp5_state*)user; + (void)at; + if (applied->incarnation == state->open1000) state->cycle = applied->cycle_after; + return 0; +} + +/* Runs one case; answers the lifecycle and the rows. */ +static uint32_t kulp5_run(kulp5_state* state, int pending_case, pf_native_event_v1* rows, + int capacity, int* got) { + pf_native_callbacks_v1 table; + pf_native_run_spec_v1 spec = twin_spec(); + pf_native_run_spec_ext_v1 ext; + pf_native_state_v1 run_state; + pf_bar_t bars[12]; + int i; + + memset(state, 0, sizeof(*state)); + state->pending_case = pending_case; + table = blank_callbacks(state); + table.on_bar = kulp5_on_bar; + table.on_applied = kulp5_on_applied; + state->host = strategy_native_host_create_v1(&table); + CHECK(state->host != NULL, "K-ULP5 host create failed"); + *got = 0; + if (!state->host) return 0u; + spec.session_key = pending_case ? "native-c-api-k-ulp5-pending" : "native-c-api-k-ulp5-p5b"; + spec.initial_capital = 1e9; + spec.fee_kind = PF_NATIVE_FEE_CASH_PER_EXECUTION; + memset(&ext, 0, sizeof(ext)); + ext.struct_size = (uint32_t)sizeof(ext); + ext.version = PF_NATIVE_API_VERSION; + ext.present_mask = PF_NATIVE_SPEC_EXT_EVENT_RETENTION; + ext.event_retention = PF_NATIVE_EVENT_RETENTION_FULL; + CHECK_EQ_INT(strategy_configure_native_ext_v1(state->host, &spec, &ext), PF_NATIVE_OK, + "the K-ULP5 spec was refused"); + for (i = 0; i < 12; ++i) { + bars[i].open = bars[i].high = bars[i].low = bars[i].close = 100.0; + bars[i].volume = 5.0; + bars[i].timestamp = (int64_t)i * 300000; + } + CHECK_EQ_INT(strategy_native_run_v1(state->host, bars, 12, NULL), PF_NATIVE_OK, + "the K-ULP5 run stopped"); + CHECK_EQ_INT(state->error, 0, "a K-ULP5 submit was refused"); + memset(&run_state, 0, sizeof(run_state)); + run_state.struct_size = (uint32_t)sizeof(run_state); + run_state.version = PF_NATIVE_API_VERSION; + CHECK_EQ_INT(strategy_native_state_v1(state->host, &run_state), PF_NATIVE_OK, + "K-ULP5 state read"); + memset(rows, 0, sizeof(*rows) * (size_t)capacity); + for (i = 0; i < capacity; ++i) { + rows[i].struct_size = (uint32_t)sizeof(rows[i]); + rows[i].version = PF_NATIVE_API_VERSION; + } + *got = strategy_native_events_v1(state->host, 0, rows, capacity); + return run_state.lifecycle; +} + +static void check_reservation_absorbed(void) { + static pf_native_event_v1 rows[256]; + kulp5_state state; + pf_native_working_v1 working; + const double resting_units = 1000.0 - 0.3; + int got = 0; + int i, n; + int reduced = 0, absorbed = 0, deferred = 0; + double first_reduction = -1.0; + uint32_t lifecycle; + + lifecycle = kulp5_run(&state, 0, rows, 256, &got); + CHECK_EQ_INT(lifecycle, PF_NATIVE_LIFECYCLE_COMPLETED, "the absorbed deduction stopped the run"); + for (i = 0; i < got; ++i) { + if (rows[i].kind == PF_NATIVE_EVENT_RESERVATION_REDUCED + && rows[i].incarnation == state.resting) { + CHECK_EQ_INT(rows[i].reason, PF_NATIVE_GROUP_REDUCE, "a reduction named another effect"); + if (reduced == 0) first_reduction = rows[i].closed_units; + if (rows[i].closed_units == 0.0) ++absorbed; + ++reduced; + } + if (rows[i].kind == PF_NATIVE_EVENT_DEFERRED_GROUP && rows[i].incarnation == state.fraction) { + CHECK(rows[i].closed_units == 0.3, "the fraction deferred another deduction"); + ++deferred; + } + } + CHECK_EQ_INT(reduced, 2, "the resting member saw another number of reductions"); + CHECK(first_reduction == 0.3, "the 0.3 fill was not deducted"); + CHECK_EQ_INT(absorbed, 1, "the dust fill's deduction was not the absorbed row"); + CHECK_EQ_INT(deferred, 1, "the fraction saw another number of deferred deductions"); + n = strategy_native_working_len_v1(state.host); + reduced = 0; + for (i = 0; i < n; ++i) { + memset(&working, 0, sizeof(working)); + working.struct_size = (uint32_t)sizeof(working); + working.version = PF_NATIVE_API_VERSION; + if (strategy_native_working_get_v1(state.host, i, &working) != PF_NATIVE_OK) continue; + if (working.incarnation != state.resting) continue; + CHECK_EQ_INT(working.remaining_kind, PF_NATIVE_REMAINING_UNITS, + "the resting member lost its units"); + CHECK(working.remaining_units == resting_units, + "the absorbed deduction moved the resting member's units"); + ++reduced; + } + CHECK_EQ_INT(reduced, 1, "the resting member is no longer working"); + CHECK_EQ_INT(strategy_native_open_lot_count_v1(state.host, NAN), 2, + "the fraction's fill did not stand"); + strategy_native_host_free(state.host); + + lifecycle = kulp5_run(&state, 1, rows, 256, &got); + CHECK_EQ_INT(lifecycle, PF_NATIVE_LIFECYCLE_COMPLETED, "the absorbed pending fill stopped the run"); + deferred = 0; + absorbed = 0; + for (i = 0; i < got; ++i) { + if (rows[i].kind != PF_NATIVE_EVENT_DEFERRED_GROUP || rows[i].incarnation != state.child) { + continue; + } + if (deferred == 0) CHECK(rows[i].closed_units == 0x1p60, "the first delta was not deferred"); + if (rows[i].closed_units == 0.0) ++absorbed; + ++deferred; + } + CHECK_EQ_INT(deferred, 2, "the child saw another number of deferred deductions"); + CHECK_EQ_INT(absorbed, 1, "the fill of 1 was not the absorbed row"); + strategy_native_host_free(state.host); +} + /* ── L9's risk limits, read back through the C event history ────── */ /* One opening per calculation, with a limit of one applied fill per day: the @@ -8913,6 +9145,7 @@ int pf_native_c_api_checks(void) { check_event_polling(); check_event_retention(); check_unrepresentable_quantity(); + check_reservation_absorbed(); check_risk_event(); check_absent_accessors(); check_live_accessors(); diff --git a/tests/test_native_execution_terms.cpp b/tests/test_native_execution_terms.cpp index 58c4ef30..66742230 100644 --- a/tests/test_native_execution_terms.cpp +++ b/tests/test_native_execution_terms.cpp @@ -591,7 +591,16 @@ void host_price_cannot_breach_active_limit() { CHECK(events(host).empty()); } -void a_t4d_authenticated_unrepresentable_deduction() { +// A host-sized opening with 0.1 pending resolves 1e16 units, and fl(1e16 - 0.1) +// is 1e16. Since R5 lane K-ULP5 that deduction is absorbed: the authentic +// deferred receipt is spent by a TermsResolvedEvent whose pending_total is 0.1 +// and whose effective_deduction is 0, the 1e16 units stand, and the run goes on +// -- the 1e16 opening beside the 0.1 lot then meets the settlement's own typed +// refusal (K-ULP4: the opening absorbs the book). Before K-ULP5 the queued path +// failed the run with UnrepresentableReservation at prepare_terms, and the +// current path's preview threw "native host-sized deduction is not +// representable" before execution failed the same way. +void a_t4d_authenticated_absorbed_deduction() { auto configure_terms = [](TermsHost& host) { host.resolver = [](const NativeExecutionTermsFacts& facts) { if (std::holds_alternative(facts.definition->request.intent)) { @@ -603,8 +612,8 @@ void a_t4d_authenticated_unrepresentable_deduction() { }; }; - // The queued path obtains its error only through prepare_terms after an - // authentic deferred-group receipt has been installed. + // The queued path reaches prepare_terms after an authentic deferred-group + // receipt has been installed. TermsHost queued; configure_terms(queued); no::RequestHandle queued_b; @@ -618,20 +627,27 @@ void a_t4d_authenticated_unrepresentable_deduction() { queued_b = put(h, b); }; run(queued, spec("terms-numeric-queued"), {100}); - CHECK(queued.native_state().kind == NativeLifecycleKind::Failed); - CHECK(queued.native_state().failure.code == NativeFailureCode::SettlementFailure); - CHECK(queued.native_state().failure.discriminator - == static_cast(no::CoreFailure::UnrepresentableReservation)); + completed(queued); const auto deferred = last_event(queued); REQUIRE(deferred); CHECK(deferred->recipient == queued_b); CHECK(bits(deferred->pending_after.total) == bits(0.1)); - CHECK(events(queued).empty()); + const auto resolved = last_event(queued); + REQUIRE(resolved); + CHECK(resolved->definition && resolved->definition->handle == queued_b); + CHECK(bits(resolved->pending_total) == bits(0.1)); + CHECK(resolved->prior_adjustment_ids.size() == 1); + CHECK(resolved->effective_deduction == 0.0); + CHECK(std::get(resolved->remaining_after).q == 1e16); + const auto refused = last_event(queued); + REQUIRE(refused); + CHECK(refused->handle() == queued_b); + CHECK(refused->reason == no::MatchRejectReason::UnrepresentableQuantity); CHECK(events(queued).size() == 1); CHECK(accounts(queued) == 1); - // The current preview throws unlatched, then execution follows the same - // hard-failure mapping and leaves the pending chain intact. + // The current preview answers without throwing, and execution follows the + // same absorption. TermsHost current; configure_terms(current); bool reached = false; @@ -651,30 +667,31 @@ void a_t4d_authenticated_unrepresentable_deduction() { bool preview_threw = false; try { (void)h.inspect_current_execution(command(b_target)); - } catch (const std::overflow_error& error) { - preview_threw = std::string(error.what()) - == "native host-sized deduction is not representable"; + } catch (const std::exception&) { + preview_threw = true; } - CHECK(preview_threw); + CHECK(!preview_threw); CHECK(h.native_state().kind == NativeLifecycleKind::Running); CHECK(h.native_continuation_hash() == hash); CHECK(h.validator_calls == 1); bool execute_threw = false; try { (void)h.execute_current(command(b_target)); - } catch (const std::runtime_error& error) { - execute_threw = std::string(error.what()) == "native current execution failed"; + } catch (const std::exception&) { + execute_threw = true; } - CHECK(execute_threw); - CHECK(h.native_state().kind == NativeLifecycleKind::Failed); - CHECK(h.native_state().failure.code == NativeFailureCode::SettlementFailure); - CHECK(h.native_state().failure.discriminator - == static_cast(no::CoreFailure::UnrepresentableReservation)); + CHECK(!execute_threw); + CHECK(h.native_state().kind == NativeLifecycleKind::Running); + const auto terms = last_event(h); + REQUIRE(terms); + CHECK(bits(terms->pending_total) == bits(0.1) && terms->effective_deduction == 0.0); + CHECK(std::get(terms->remaining_after).q == 1e16); CHECK(h.validator_calls == 1); reached = true; }; run(current, spec("terms-numeric-current"), {100}); CHECK(reached); + completed(current); } void a_t5_terms_rejections_retain_attempted_terms() { @@ -1989,7 +2006,7 @@ int main() { test("host NaN attempted terms", host_nan_price_is_rejected_without_a_receipt); test("A-T11d host-sized NaN rows", a_t11d_host_sized_nan_rows); test("host price limit fence", host_price_cannot_breach_active_limit); - test("A-T4d authenticated unrepresentable deduction", a_t4d_authenticated_unrepresentable_deduction); + test("A-T4d authenticated absorbed deduction", a_t4d_authenticated_absorbed_deduction); test("A-T5 terms rejections retain attempts", a_t5_terms_rejections_retain_attempted_terms); test("A-T5 bound host-sized rematch units", a_t5_bound_host_sized_rematch_rejects_units); test("explicit reduction grid policy both ways", explicit_reduction_grid_policy_both_ways); diff --git a/tests/test_native_group_absorption.cpp b/tests/test_native_group_absorption.cpp new file mode 100644 index 00000000..9896e9b6 --- /dev/null +++ b/tests/test_native_group_absorption.cpp @@ -0,0 +1,1014 @@ +// R5 lane K-ULP5: an OCA-Reduce deduction binary64 cannot take off its +// recipient is absorbed, and the run goes on. +// +// A fill of one member of an OCA group whose effect is GroupEffect::Reduce is +// deducted from every live sibling (WorkingRequestCore::prepare_group_effect / +// apply_group_effect): from the sibling's remaining units, or -- while those +// units are not known yet -- into its pending total, which its terms +// (prepare_terms / apply_terms) or its owner's fill (prepare_owner_applied / +// apply_owner_applied) later takes off the units they bind. A deduction too +// small to move what it is taken from -- fl(units - d) is the units again, at +// most half an ulp of them -- and a delta too small to move a pending total +// could not be recorded: each of the four sites answered +// CoreFailure::UnrepresentableReservation and the run stopped after the +// member's fill was booked (lifecycle Failed, code 6, discriminator 7), on +// ordinary decimal quantities (K-ULP4's third-review probe p5b). Now such a +// deduction is ABSORBED: the recipient's units (or its pending total) stay as +// they are -- the exact binary64 result of taking a sub-ulp amount off them -- +// the member's fill stands, and the event that records the deduction says it +// took nothing: +// +// units a ReservationReducedEvent with actual_deduction 0 and +// after == before (C: RESERVATION_REDUCED, closed_units 0) +// pending a DeferredGroupAdjustmentEvent with deferred_delta 0 whose +// pending_after is its pending_before, joined to no chain +// (C: DEFERRED_GROUP, closed_units 0) +// terms, bound a TermsResolvedEvent / QuantityBoundEvent whose +// pending_total is positive and effective_deduction 0: the +// resolved (or the owner's opened) units stand +// +// A pending total below half an ulp of a later delta rounds into it, as +// binary64 addition does (pending-rounds); it stopped the run too. A pending +// total that overflows (two fills of DBL_MAX) is no sub-ulp deduction and +// still stops the run with discriminator 7 (overflow, the control). +// +// p5b {0.5, fl(1000.1 - 1000)}: a resting Reduce 1000, a Reduce +// 0.3 and a ScopeFraction 1 in one group; the fraction closes +// 2.2759572004815709e-14, below half an ulp of the resting +// member's 999.7 -- absorbed; a later opening fills +// units-* a 2^60 recipient beside fills of 1 (absorbed) and 256 +// (deducted), long and short +// pending a bracket child waiting on its parent holds 2^60 pending; +// a fill of 1 is absorbed; the parent's fill binds the chain +// pending-rounds a pending 2^-60 then a fill of 1: the total is 1 +// terms, -current a host-sized opening with 0.1 pending resolves 1e16: the +// 1e16 stand; through execute_current, whose preview no +// longer throws +// bound a bracket child with 0.5 pending binds to an owner's 1e16 +// overflow the control above +// battery seeded random OCA-Reduce groups beside decimal books and +// their dust: resting recipients of large and decimal units, +// deferred fractions, bracket children and host-sized +// openings; market reduces of the book's own lots, fractions, +// openings, host-sized openings and bracket owners. Every run +// completes, and every deduction is either taken (after is +// fl(before - d)) or absorbed exactly where binary64 cannot +// take it +// Every case runs on the request core's staged and direct paths, which must +// agree bit for bit, hashes included. +// +// Fail-before: compiled against f1af50dc (K-ULP4) the TU builds and fails 970 +// of 177,824 checks: p5b, units-*, pending, pending-rounds, terms, +// terms-current and bound stop with code 6, discriminator 7 (terms-current's +// preview throws "native host-sized deduction is not representable" first), +// and the battery stops 321, 290 and 303 of its 1,000 runs per seed, each with +// code 6, discriminator 7. Source-free: the kernel-only profile registers the +// row. +#include "../src/engine_internal.hpp" +#include "../src/native_execution_consumer.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace pineforge; +namespace no = pineforge::native_order; + +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) + +constexpr double kP60 = 0x1p60; +constexpr double kNever = 1e6; // a sell limit (and a buy stop) the run never reaches + +// ---- A scripted host: each bar may run one callback --------------------------- + +struct ScriptHost; +using Script = std::map>; + +struct ScriptHost final : public NativeStrategyHost { + explicit ScriptHost(bool direct) { + 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); + } + // A HostSized request is answered the units its label names. + no::ExecutionTerms resolve_execution_terms( + const NativeExecutionTermsFacts& facts) const override { + if (std::holds_alternative(facts.definition->request.intent)) { + const auto it = host_units.find(facts.definition->request.label); + if (it != host_units.end()) { + return {facts.default_resolved_price, it->second, no::OpeningShape::Transact}; + } + } + return {facts.default_resolved_price, std::nullopt, no::OpeningShape::Transact}; + } + std::int64_t cycle() const { return position_cycle_seq_; } + // 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; + return true; + } + std::vector book() const { + std::vector out; + for (const auto& lot : native_open_lots(std::numeric_limits::quiet_NaN())) { + out.push_back(lot.signed_units); + } + return out; + } + std::optional remaining_of(const std::string& name) const { + const auto it = h.find(name); + if (it == h.end()) return std::nullopt; + for (const auto& row : native_working_requests()) { + if (row.definition && row.definition->handle == it->second) return row.remaining; + } + return std::nullopt; + } + + Script script; + std::map host_units; + std::map h; + int bar = 0; + int refused = 0; + // execute_current's run, for the -current case. + bool preview_threw = false; + int current_kind = -1; +}; + +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 frac(double fraction) { + no::Request r; + r.intent = no::Reduce{no::ScopeFraction{fraction}}; + return r; +} +no::Request owner_opened(const no::RequestHandle& parent) { + no::Request r; + r.intent = no::Reduce{no::OwnerOpenedUnits{}}; + r.owner = no::WaitForApplied{parent}; + return r; +} +no::Request host_open() { + no::Request r; + r.intent = no::HostSized{no::HostSizedKind::Open, no::Side::Long}; + return r; +} +no::Request flatten() { no::Request r; r.intent = no::Flatten{}; return r; } +no::Request in_group(no::Request r, std::uint64_t group, std::int64_t cohort) { + r.group = no::Member{group, cohort, no::GroupEffect::Reduce}; + return r; +} + +std::uint64_t bits(double value) { + std::uint64_t out = 0; + std::memcpy(&out, &value, sizeof out); + return out; +} + +// ---- One run and what it came to ---------------------------------------------- + +struct Counts { + int reductions = 0, absorbed_units = 0; + int deferred = 0, absorbed_pending = 0, rounded_pending = 0; + int terms_with_pending = 0, absorbed_terms = 0; + int bound_with_pending = 0, absorbed_bound = 0; + void add(const Counts& o) { + reductions += o.reductions; absorbed_units += o.absorbed_units; + deferred += o.deferred; absorbed_pending += o.absorbed_pending; + rounded_pending += o.rounded_pending; + terms_with_pending += o.terms_with_pending; absorbed_terms += o.absorbed_terms; + bound_with_pending += o.bound_with_pending; absorbed_bound += o.absorbed_bound; + } +}; + +struct Outcome { + bool completed = false; + unsigned code = 0, discriminator = 0; + std::string transcript; // every command event, the fields a deduction writes + std::uint64_t continuation = 0, broker = 0; + std::vector book; + Counts counts; + std::unique_ptr host; +}; + +double filled_of(const ScriptHost& host, const no::EventId& cause) { + for (const auto& row : host.native_events(0)) { + if (!row.command) continue; + if (const auto* a = std::get_if(&*row.command)) { + if (a->ordinal == cause.ordinal) return a->filled_working; + } + } + return std::numeric_limits::quiet_NaN(); +} + +std::string fmt(const char* format, double a, double b = 0.0, double c = 0.0, double d = 0.0) { + char buffer[256]; + std::snprintf(buffer, sizeof buffer, format, a, b, c, d); + return buffer; +} + +// Reads every event of a finished run; checks each deduction against binary64 +// and counts the absorbed ones. +void read_events(Outcome& out) { + const ScriptHost& host = *out.host; + for (const auto& row : host.native_events(0)) { + if (!row.command) continue; + const auto& command = *row.command; + out.transcript += std::to_string(command.index()) + ":"; + if (const auto* a = std::get_if(&command)) { + out.transcript += fmt("A %a %a %a;", a->closed_units, a->opened_units, a->filled_working); + } else if (const auto* r = std::get_if(&command)) { + const double before = r->before.q; + const auto* after = std::get_if(&r->after); + out.transcript += fmt("R %a %a %a %a;", r->requested_delta, r->actual_deduction, before, + after ? after->q : -1.0); + ++out.counts.reductions; + CHECK(after != nullptr); + if (!after) continue; + const double d = std::min(r->requested_delta, before); + CHECK(r->requested_delta == filled_of(host, r->cause)); + if (r->actual_deduction == 0.0) { + // Absorbed: binary64 cannot take d off `before`. + ++out.counts.absorbed_units; + CHECK(d > 0.0 && d < before && before - d == before); + CHECK(after->q == before); + } else { + CHECK(r->actual_deduction == d); + CHECK(d == before ? after->q == 0.0 : after->q == before - d); + CHECK(after->q < before); + } + } else if (const auto* g = std::get_if(&command)) { + out.transcript += fmt("D %a %a;", g->deferred_delta, g->pending_after.total); + ++out.counts.deferred; + const double fill = filled_of(host, g->cause); + const auto* before = std::get_if(&g->pending_before); + if (g->deferred_delta == 0.0) { + // Absorbed: the pending total cannot move by this fill. + ++out.counts.absorbed_pending; + CHECK(before != nullptr); + if (!before) continue; + CHECK(before->total + fill == before->total); + CHECK(g->pending_after.total == before->total); + CHECK(g->pending_after.count == before->count); + CHECK(g->pending_after.tail_receipt == before->tail_receipt); + CHECK(!g->previous_pending_receipt); + } else { + CHECK(g->deferred_delta == fill); + CHECK(g->pending_after.tail_receipt.ordinal == g->ordinal); + if (before) { + CHECK(g->pending_after.total == before->total + fill); + CHECK(g->pending_after.total > before->total); + CHECK(g->pending_after.count == before->count + 1); + CHECK(g->previous_pending_receipt + && *g->previous_pending_receipt == before->tail_receipt); + if (g->pending_after.total == fill) ++out.counts.rounded_pending; + } else { + CHECK(g->pending_after.total == fill && g->pending_after.count == 1); + } + } + } else if (const auto* t = std::get_if(&command)) { + const auto* after = std::get_if(&t->remaining_after); + out.transcript += fmt("T %a %a %a;", t->pending_total, t->effective_deduction, + after ? after->q : -1.0); + if (t->pending_total > 0.0 && t->input.terms.units && after) { + ++out.counts.terms_with_pending; + const double units = *t->input.terms.units; + const double d = std::min(t->pending_total, units); + if (t->effective_deduction == 0.0 && units > 0.0) { + ++out.counts.absorbed_terms; + CHECK(d < units && units - d == units); + CHECK(after->q == units); + } else { + CHECK(t->effective_deduction == d); + CHECK(d == units ? after->q == 0.0 : after->q == units - d); + } + } + } else if (const auto* q = std::get_if(&command)) { + const auto* after = std::get_if(&q->remaining); + out.transcript += fmt("Q %a %a %a %a;", q->source_units, q->pending_total, + q->effective_deduction, after ? after->q : -1.0); + if (q->pending_total > 0.0 && after) { + ++out.counts.bound_with_pending; + const double d = std::min(q->pending_total, q->source_units); + if (q->effective_deduction == 0.0) { + ++out.counts.absorbed_bound; + CHECK(d < q->source_units && q->source_units - d == q->source_units); + CHECK(after->q == q->source_units); + } else { + CHECK(q->effective_deduction == d); + CHECK(d == q->source_units ? after->q == 0.0 + : after->q == q->source_units - d); + } + } + } else if (const auto* m = std::get_if(&command)) { + out.transcript += "M" + std::to_string(static_cast(m->reason)) + ";"; + } else if (const auto* c = std::get_if(&command)) { + out.transcript += "C" + std::to_string(static_cast(c->reason)) + ";"; + } else { + out.transcript += ";"; + } + } +} + +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; +} + +NativeRunSpec spec_for(const std::string& name) { + NativeRunSpec s; + s.event_retention = NativeEventRetention::Full; + s.identity = {"k-ulp5-" + name, 1}; + s.input_tf = "1"; + s.script_tf = "1"; + s.tickerid = "K-ULP5:ABSORB"; + 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; +} + +Outcome run_one(const std::string& name, const Script& script, const std::vector& bars, + bool direct, const std::map& host_units = {}) { + Outcome out; + out.host = std::make_unique(direct); + out.host->script = script; + out.host->host_units = host_units; + CHECK(out.host->configure_native(spec_for(name)).status == NativeSetupStatus::Applied); + out.host->run(bars.data(), static_cast(bars.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); + read_events(out); + out.continuation = out.host->native_continuation_hash(); + out.broker = out.host->broker_state_hash(); + out.book = out.host->book(); + return out; +} + +// Both paths, which must agree; answers the staged run. +Outcome run_both(const std::string& name, const Script& script, const std::vector& bars, + const std::map& host_units = {}) { + scenario = name; + Outcome staged = run_one(name, script, bars, false, host_units); + const Outcome direct = run_one(name, script, bars, true, host_units); + CHECK(staged.completed == direct.completed); + CHECK(staged.code == direct.code && staged.discriminator == direct.discriminator); + CHECK(staged.transcript == direct.transcript); + CHECK(staged.continuation == direct.continuation); + CHECK(staged.broker == direct.broker); + CHECK(staged.book == direct.book); + std::printf("%-16s completed=%d code=%u disc=%u absorbed: units=%d pending=%d terms=%d " + "bound=%d rounded=%d\n", + name.c_str(), staged.completed ? 1 : 0, staged.code, staged.discriminator, + staged.counts.absorbed_units, staged.counts.absorbed_pending, + staged.counts.absorbed_terms, staged.counts.absorbed_bound, + staged.counts.rounded_pending); + return staged; +} + +template +std::vector events_of(const ScriptHost& 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; +} + +std::vector fills_of(const ScriptHost& host, const std::string& name) { + std::vector out; + const auto it = host.h.find(name); + if (it == host.h.end()) return out; + for (const auto& a : events_of(host)) { + if (a.handle() == it->second) out.push_back(a); + } + return out; +} + +bool remaining_is(const ScriptHost& host, const std::string& name, double units) { + const auto remaining = host.remaining_of(name); + if (!remaining) return false; + const auto* q = std::get_if(&*remaining); + return q && q->q == units; +} + +// ---- Directed cases ----------------------------------------------------------- + +void p5b() { + Script s; + s[0] = [](ScriptHost& p) { p.sub("open0.5", tx(0.5)); }; + s[2] = [](ScriptHost& p) { p.sub("open1000.1", tx(1000.1)); }; + s[4] = [](ScriptHost& p) { + auto r = red(1000.0); + r.owner = no::BindOpening{p.h["open1000.1"], p.cycle()}; + p.sub("bound-1000", r); + }; + s[6] = [](ScriptHost& p) { + auto resting = in_group(red(1000.0), 9, 3); + resting.trigger = no::Limit{150.0}; + p.sub("oca-resting-1000", resting); + p.sub("oca-reduce-0.3", in_group(red(0.3), 9, 1)); + p.sub("oca-fraction", in_group(frac(1.0), 9, 2)); + }; + s[8] = [](ScriptHost& p) { p.sub("after", tx(1.0)); }; + const Outcome o = run_both("p5b", s, flat_bars(12)); + const ScriptHost& host = *o.host; + CHECK(o.completed); + CHECK(o.code == 0 && o.discriminator == 0); + CHECK(host.refused == 0); + + const double lot1 = 0.5 - 0.3; // the 0.5 lot after the 0.3 close + const double lot2 = 1000.1 - 1000.0; // 0.10000000000002274 + const double scope = lot1 + lot2; // what the fraction resolves + const double dust = scope - 0.3; // its units net of the 0.3 pending + const double resting = 1000.0 - 0.3; // 999.70000000000005 + CHECK(bits(dust) == bits(0x1.9ap-46)); // 2.2759572004815709e-14 + CHECK(dust < (std::nextafter(resting, 1e9) - resting) / 2); + + const auto reduced = events_of(host); + CHECK(reduced.size() == 2); + if (reduced.size() == 2) { + CHECK(reduced[0].recipient == host.h.at("oca-resting-1000")); + CHECK(reduced[0].requested_delta == 0.3 && reduced[0].actual_deduction == 0.3); + CHECK(reduced[0].before.q == 1000.0); + CHECK(std::get(reduced[0].after).q == resting); + // The absorbed one: requested the fraction's fill, took nothing. + CHECK(reduced[1].recipient == host.h.at("oca-resting-1000")); + CHECK(reduced[1].requested_delta == dust); + CHECK(reduced[1].actual_deduction == 0.0); + CHECK(reduced[1].before.q == resting); + CHECK(std::get(reduced[1].after).q == resting); + } + const auto deferred = events_of(host); + CHECK(deferred.size() == 1); + if (deferred.size() == 1) { + CHECK(deferred[0].recipient == host.h.at("oca-fraction")); + CHECK(deferred[0].deferred_delta == 0.3); + } + // The member's fill stands. + const auto fraction = fills_of(host, "oca-fraction"); + CHECK(fraction.size() == 1); + if (fraction.size() == 1) { + CHECK(fraction[0].closed_units == dust && fraction[0].terminal); + } + // The resting member keeps its units, and the run goes on: the later + // opening fills. + CHECK(remaining_is(host, "oca-resting-1000", resting)); + CHECK(fills_of(host, "after").size() == 1); + CHECK(o.book == (std::vector{lot1 - dust, lot2, 1.0})); + CHECK(o.counts.absorbed_units == 1); +} + +void units(double sign) { + Script s; + s[0] = [sign](ScriptHost& p) { + auto recipient = in_group(tx(sign * kP60), 7, 2); + recipient.trigger = no::Limit{sign > 0 ? 1.0 : 200.0}; + p.sub("recipient", recipient); + p.sub("fill-1", in_group(tx(sign * 1.0), 7, 1)); + }; + s[2] = [sign](ScriptHost& p) { p.sub("fill-256", in_group(tx(sign * 256.0), 7, 3)); }; + s[4] = [sign](ScriptHost& p) { p.sub("fill-1b", in_group(tx(sign * 1.0), 7, 4)); }; + s[6] = [sign](ScriptHost& p) { p.sub("after", tx(sign * 2.0)); }; + const Outcome o = run_both(sign > 0 ? "units-long" : "units-short", s, flat_bars(10)); + const ScriptHost& host = *o.host; + CHECK(o.completed); + const auto reduced = events_of(host); + CHECK(reduced.size() == 3); + if (reduced.size() == 3) { + // fl(2^60 - 1) is 2^60: absorbed. + CHECK(reduced[0].requested_delta == 1.0 && reduced[0].actual_deduction == 0.0); + CHECK(reduced[0].before.q == kP60); + CHECK(std::get(reduced[0].after).q == kP60); + // 256 is one ulp of 2^60: deducted exactly. + CHECK(reduced[1].requested_delta == 256.0 && reduced[1].actual_deduction == 256.0); + CHECK(std::get(reduced[1].after).q == kP60 - 256.0); + // fl(2^60 - 256 - 1) is 2^60 - 256: absorbed again. + CHECK(reduced[2].requested_delta == 1.0 && reduced[2].actual_deduction == 0.0); + CHECK(reduced[2].before.q == kP60 - 256.0); + CHECK(std::get(reduced[2].after).q == kP60 - 256.0); + } + CHECK(remaining_is(host, "recipient", kP60 - 256.0)); + CHECK(fills_of(host, "fill-1").size() == 1 && fills_of(host, "fill-1b").size() == 1); + CHECK(fills_of(host, "after").size() == 1); + CHECK(o.book == (std::vector{sign * 1.0, sign * 256.0, sign * 1.0, sign * 2.0})); + CHECK(events_of(host).empty()); +} + +// Bars at 100 but `trigger_bar`, where the price is 250: a buy stop at 200 +// fills there. +std::vector bars_with_rise(int n, int trigger_bar) { + auto bars = flat_bars(n); + const double p = 250.0; + bars[trigger_bar] = Bar{p, p, p, p, 10.0, bars[trigger_bar].timestamp}; + return bars; +} + +void pending() { + Script s; + s[0] = [](ScriptHost& p) { + auto parent = tx(1.0); + parent.trigger = no::Stop{200.0}; + p.sub("parent", parent); + p.sub("child", in_group(owner_opened(p.h["parent"]), 7, 2)); + p.sub("fill-2^60", in_group(tx(kP60), 7, 1)); + }; + s[2] = [](ScriptHost& p) { p.sub("flat-a", flatten()); }; + s[4] = [](ScriptHost& p) { p.sub("fill-1", in_group(tx(1.0), 7, 3)); }; + s[6] = [](ScriptHost& p) { p.sub("flat-b", flatten()); }; + const Outcome o = run_both("pending", s, bars_with_rise(12, 9)); + const ScriptHost& host = *o.host; + CHECK(o.completed); + const auto deferred = events_of(host); + CHECK(deferred.size() == 2); + if (deferred.size() == 2) { + CHECK(deferred[0].deferred_delta == kP60); + CHECK(deferred[0].pending_after.total == kP60 && deferred[0].pending_after.count == 1); + const no::EventId head{deferred[0].pending_after.tail_receipt}; + CHECK(head.ordinal == deferred[0].ordinal); + // fl(2^60 + 1) is 2^60: absorbed, joined to no chain. + CHECK(deferred[1].deferred_delta == 0.0); + const auto* before = std::get_if(&deferred[1].pending_before); + CHECK(before && before->total == kP60 && before->count == 1 && before->tail_receipt == head); + CHECK(deferred[1].pending_after.total == kP60 && deferred[1].pending_after.count == 1); + CHECK(deferred[1].pending_after.tail_receipt == head); + CHECK(!deferred[1].previous_pending_receipt); + } + CHECK(fills_of(host, "fill-1").size() == 1); + // The parent's fill binds the chain it left: 2^60 pending exhausts the + // child's 1 unit. + CHECK(fills_of(host, "parent").size() == 1); + const auto bound = events_of(host); + CHECK(bound.size() == 1); + if (bound.size() == 1) { + CHECK(bound[0].source_units == 1.0 && bound[0].pending_total == kP60); + CHECK(bound[0].prior_adjustment_ids.size() == 1); + CHECK(bound[0].effective_deduction == 1.0); + } + const auto cancelled = events_of(host); + CHECK(cancelled.size() == 1 && cancelled[0].reason == no::CancelReason::Group); + CHECK(o.counts.absorbed_pending == 1); +} + +void pending_rounds() { + Script s; + s[0] = [](ScriptHost& p) { + auto parent = tx(3.0); + parent.trigger = no::Stop{200.0}; + p.sub("parent", parent); + p.sub("child", in_group(owner_opened(p.h["parent"]), 7, 2)); + p.sub("fill-2^-60", in_group(tx(0x1p-60), 7, 1)); + }; + s[2] = [](ScriptHost& p) { p.sub("flat-a", flatten()); }; + s[4] = [](ScriptHost& p) { p.sub("fill-1", in_group(tx(1.0), 7, 3)); }; + s[6] = [](ScriptHost& p) { p.sub("flat-b", flatten()); }; + const Outcome o = run_both("pending-rounds", s, bars_with_rise(12, 9)); + const ScriptHost& host = *o.host; + CHECK(o.completed); + const auto deferred = events_of(host); + CHECK(deferred.size() == 2); + if (deferred.size() == 2) { + CHECK(deferred[0].deferred_delta == 0x1p-60); + // fl(2^-60 + 1) is 1: the earlier total rounds into the fill. + CHECK(deferred[1].deferred_delta == 1.0); + CHECK(deferred[1].pending_after.total == 1.0 && deferred[1].pending_after.count == 2); + } + const auto bound = events_of(host); + CHECK(bound.size() == 1); + if (bound.size() == 1) { + CHECK(bound[0].source_units == 3.0 && bound[0].pending_total == 1.0); + CHECK(bound[0].prior_adjustment_ids.size() == 2); + CHECK(bound[0].effective_deduction == 1.0); + CHECK(std::get(bound[0].remaining).q == 2.0); + } + // The bound child is a market close of 2 of the parent's 3. + CHECK(fills_of(host, "child").size() == 1); + CHECK(o.book == (std::vector{1.0})); + CHECK(o.counts.rounded_pending == 1); +} + +void terms() { + Script s; + s[0] = [](ScriptHost& p) { p.sub("lot", tx(0.1)); }; + s[2] = [](ScriptHost& p) { + p.sub("close-0.1", in_group(red(0.1), 41, 1)); + p.sub("host-open", in_group(host_open(), 41, 2)); + }; + const Outcome o = run_both("terms", s, flat_bars(6), {{"host-open", 1e16}}); + const ScriptHost& host = *o.host; + CHECK(o.completed); + const auto resolved = events_of(host); + CHECK(resolved.size() == 1); + if (resolved.size() == 1) { + // fl(1e16 - 0.1) is 1e16: absorbed; the host's units stand. + CHECK(resolved[0].pending_total == 0.1); + CHECK(resolved[0].effective_deduction == 0.0); + CHECK(resolved[0].prior_adjustment_ids.size() == 1); + CHECK(std::get(resolved[0].remaining_after).q == 1e16); + } + const auto opened = fills_of(host, "host-open"); + CHECK(opened.size() == 1 && opened[0].opened_units == 1e16); + CHECK(o.book == (std::vector{1e16})); + CHECK(o.counts.absorbed_terms == 1); +} + +void terms_current() { + Script s; + s[0] = [](ScriptHost& p) { p.sub("lot", tx(0.1)); }; + s[2] = [](ScriptHost& p) { + p.sub("close-0.1", in_group(red(0.1), 42, 1)); + p.sub("host-open", in_group(host_open(), 42, 2)); + (void)p.execute_current(NativeCurrentExecution{p.h["close-0.1"]}); + try { + (void)p.inspect_current_execution(NativeCurrentExecution{p.h["host-open"]}); + } catch (const std::exception&) { + p.preview_threw = true; + } + try { + p.current_kind = static_cast( + p.execute_current(NativeCurrentExecution{p.h["host-open"]}).index()); + } catch (const std::exception&) { + p.current_kind = -2; + } + }; + const Outcome o = run_both("terms-current", s, flat_bars(6), {{"host-open", 1e16}}); + const ScriptHost& host = *o.host; + CHECK(o.completed); + CHECK(!host.preview_threw); + CHECK(host.current_kind >= 0); + const auto resolved = events_of(host); + CHECK(resolved.size() == 1); + if (resolved.size() == 1) { + CHECK(resolved[0].pending_total == 0.1 && resolved[0].effective_deduction == 0.0); + } + CHECK(fills_of(host, "host-open").size() == 1); + CHECK(o.book == (std::vector{1e16})); +} + +void bound() { + Script s; + s[0] = [](ScriptHost& p) { p.sub("lot", tx(0.5)); }; + s[2] = [](ScriptHost& p) { + auto parent = tx(1e16); + parent.trigger = no::Stop{200.0}; + p.sub("parent", parent); + auto child = in_group(owner_opened(p.h["parent"]), 8, 2); + child.trigger = no::Limit{kNever}; + p.sub("child", child); + p.sub("close-0.5", in_group(red(0.5), 8, 1)); + }; + const Outcome o = run_both("bound", s, bars_with_rise(12, 9)); + const ScriptHost& host = *o.host; + CHECK(o.completed); + const auto bound_events = events_of(host); + CHECK(bound_events.size() == 1); + if (bound_events.size() == 1) { + // fl(1e16 - 0.5) is 1e16: absorbed; the owner's units stand. + CHECK(bound_events[0].source_units == 1e16); + CHECK(bound_events[0].pending_total == 0.5); + CHECK(bound_events[0].effective_deduction == 0.0); + CHECK(std::get(bound_events[0].remaining).q == 1e16); + } + CHECK(remaining_is(host, "child", 1e16)); + CHECK(o.book == (std::vector{1e16})); + CHECK(o.counts.absorbed_bound == 1); +} + +// Control: DBL_MAX pending plus DBL_MAX overflows binary64. That is no +// sub-ulp deduction, and the run still stops with discriminator 7. +void overflow() { + const double maxv = std::numeric_limits::max(); + Script s; + s[0] = [](ScriptHost& p) { + auto parent = tx(1.0); + parent.trigger = no::Stop{0.02}; + p.sub("parent", parent); + p.sub("child", in_group(owner_opened(p.h["parent"]), 7, 2)); + }; + s[1] = [maxv](ScriptHost& p) { p.sub("fill-max-a", in_group(tx(maxv), 7, 1)); }; + s[3] = [](ScriptHost& p) { p.sub("flat", flatten()); }; + s[5] = [maxv](ScriptHost& p) { p.sub("fill-max-b", in_group(tx(maxv), 7, 3)); }; + const Outcome o = run_both("overflow", s, flat_bars(10, 0.01)); + CHECK(!o.completed); + CHECK(o.code == static_cast(NativeFailureCode::SettlementFailure)); + CHECK(o.discriminator + == static_cast(no::CoreFailure::UnrepresentableReservation)); + CHECK(fills_of(*o.host, "fill-max-b").size() == 1); +} + +// ---- The seeded battery ------------------------------------------------------- + +// splitmix64: the same stream on every platform. +struct Rng { + std::uint64_t s; + std::uint64_t next() { + std::uint64_t z = (s += 0x9e3779b97f4a7c15ULL); + z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9ULL; + z = (z ^ (z >> 27)) * 0x94d049bb133111ebULL; + return z ^ (z >> 31); + } + int below(int n) { return static_cast(next() % static_cast(n)); } + template T pick(const std::vector& v) { return v[static_cast(below(static_cast(v.size())))]; } +}; + +// Recipient sizes: large (far below which a decimal fill is sub-ulp) and +// ordinary. +double big_units(Rng& r) { + switch (r.below(4)) { + case 0: return std::ldexp(1.0, 20 + r.below(43)); + case 1: { // 10^k by exact multiplication (5^k fits the significand) + double p = 1000.0; + for (int k = r.below(15); k > 0; --k) p *= 10.0; + return p; + } + case 2: return r.pick(std::vector{1000.0, 999.7, 12345.678, 1e15 + 0.5, 4503599627370497.0}); + default: return r.pick(std::vector{3.7, 12.5, 0.9, 250.25}); + } +} + +double decimal_units(Rng& r) { return (1 + r.below(40)) / 10.0; } + +// One scenario: a decimal book, its residues, and one OCA-Reduce group. +Script battery_script(std::uint64_t seed, std::map* host_units) { + Rng r{seed}; + Script s; + // The book: one to three decimal lots, and a big lot a bound close cuts + // to its decimal residue (p5b's fl(1000.1 - 1000)). + std::vector opens; + const int lots = 1 + r.below(3); + for (int i = 0; i < lots; ++i) opens.push_back(decimal_units(r)); + const bool residue = r.below(2) == 0; + const double big_lot = r.pick(std::vector{1000.1, 39.1, 1.3, 106.0 / 84.5}); + s[0] = [opens, residue, big_lot](ScriptHost& p) { + for (std::size_t i = 0; i < opens.size(); ++i) p.sub("open" + std::to_string(i), tx(opens[i])); + if (residue) p.sub("big", tx(big_lot)); + }; + if (residue) { + s[2] = [big_lot](ScriptHost& p) { + if (!p.h.count("big")) return; + auto cut = red(std::floor(big_lot)); + cut.owner = no::BindOpening{p.h["big"], p.cycle()}; + p.sub("cut", cut); + }; + } + // The group at bar 4: recipients first (they rest), then fillers, in a + // shuffled order. + struct Member { int kind; double value; }; + std::vector members; + const int recipients = 1 + r.below(3); + for (int i = 0; i < recipients; ++i) members.push_back({r.below(5), big_units(r)}); + const int fillers = 1 + r.below(3); + for (int i = 0; i < fillers; ++i) members.push_back({10 + r.below(6), 0.0}); + for (std::size_t i = members.size(); i > 1; --i) { + const std::size_t j = static_cast(r.below(static_cast(i))); + std::swap(members[i - 1], members[j]); + } + const std::uint64_t group = 1 + r.below(1000); + std::vector fractions{1.0, 0.5, 0.3, 0.9999999999999999, 0.25}; + std::vector fracs; + std::vector picks; + std::vector host_answers; + for (std::size_t i = 0; i < members.size(); ++i) { + fracs.push_back(r.pick(fractions)); + picks.push_back(r.below(8)); + host_answers.push_back(big_units(r)); + } + for (std::size_t i = 0; i < members.size(); ++i) { + (*host_units)["m" + std::to_string(i)] = host_answers[i]; + } + s[4] = [members, group, fracs, picks](ScriptHost& p) { + const auto book = p.book(); + for (std::size_t i = 0; i < members.size(); ++i) { + const std::string name = "m" + std::to_string(i); + const std::int64_t cohort = static_cast(i) + 1; + const auto& m = members[i]; + switch (m.kind) { + case 0: { // resting Reduce of large or decimal units + auto q = in_group(red(m.value), group, cohort); + q.trigger = no::Limit{kNever}; + p.sub(name, q); + break; + } + case 1: { // resting Transact that never fills + auto q = in_group(tx(m.value), group, cohort); + q.trigger = no::Limit{1.0}; + p.sub(name, q); + break; + } + case 2: { // resting fraction: deferred + auto q = in_group(frac(fracs[i]), group, cohort); + q.trigger = no::Limit{kNever}; + p.sub(name, q); + break; + } + case 3: { // bracket child of an owner that never fills: unbound + auto parent = tx(m.value); + parent.trigger = no::Stop{kNever}; + if (p.sub(name + "-owner", parent)) { + p.sub(name, in_group(owner_opened(p.h[name + "-owner"]), group, cohort)); + } + break; + } + case 4: { // resting host-sized opening + auto q = in_group(host_open(), group, cohort); + q.trigger = no::Limit{1.0}; + p.sub(name, q); + break; + } + case 10: { // market Reduce of one of the book's own lots, or a decimal + double q = picks[i] % 2 == 0 ? 0.1 : 0.3; + if (!book.empty() && picks[i] < 6) { + q = std::abs(book[static_cast(picks[i]) % book.size()]); + } + p.sub(name, in_group(red(q), group, cohort)); + break; + } + case 11: // market fraction + p.sub(name, in_group(frac(fracs[i]), group, cohort)); + break; + case 12: // market opening of a decimal or a dust quantity + p.sub(name, in_group(tx(picks[i] < 4 ? 0.1 * (1 + picks[i]) + : std::ldexp(1.0, -40 - 3 * picks[i])), + group, cohort)); + break; + case 13: // market host-sized opening + p.sub(name, in_group(host_open(), group, cohort)); + break; + case 14: { // a market owner and its bracket child, the child a member; + // an owner far larger than the dust deferred into its + // child absorbs it when it binds + const double owner_units = picks[i] < 4 + ? 0.1 * (1 + picks[i]) + : std::vector{1e6, 0x1p30, 12345.678, 1e9}[picks[i] - 4]; + if (p.sub(name + "-owner", tx(owner_units))) { + auto child = in_group(owner_opened(p.h[name + "-owner"]), group, cohort); + child.trigger = no::Limit{kNever}; + p.sub(name, child); + } + break; + } + default: { // a bound close of the residue lot, when there is one + if (p.h.count("big")) { + auto q = in_group(frac(1.0), group, cohort); + q.owner = no::BindOpening{p.h["big"], p.cycle()}; + p.sub(name, q); + } else { + p.sub(name, in_group(frac(1.0), group, cohort)); + } + break; + } + } + } + }; + // Later fillers of the same group: the dust the book now holds, fractions, + // the smallest lot ahead of a large owner whose bracket child it defers + // into, and -- after a Flatten -- a dust opening beside the survivors. + const int later = r.below(4); + std::vector later_kinds; + for (int i = 0; i < later; ++i) later_kinds.push_back(r.below(5)); + s[6] = [later_kinds, group](ScriptHost& p) { + for (std::size_t i = 0; i < later_kinds.size(); ++i) { + const std::string name = "late" + std::to_string(i); + const std::int64_t cohort = 100 + static_cast(i); + const auto book = p.book(); + switch (later_kinds[i]) { + case 0: + if (!book.empty()) p.sub(name, in_group(red(book.front()), group, cohort)); + break; + case 1: + if (!book.empty()) p.sub(name, in_group(red(book.back()), group, cohort)); + break; + case 2: p.sub(name, in_group(frac(1.0), group, cohort)); break; + case 3: p.sub(name, in_group(frac(0.5), group, cohort)); break; + default: { + if (book.empty()) break; + double smallest = std::abs(book.front()); + for (double lot : book) smallest = std::min(smallest, std::abs(lot)); + p.sub(name, in_group(red(smallest), group, cohort)); + if (p.sub(name + "-owner", tx(1e6))) { + auto child = in_group(owner_opened(p.h[name + "-owner"]), group, cohort + 50); + child.trigger = no::Limit{kNever}; + p.sub(name + "-child", child); + } + break; + } + } + } + }; + if (r.below(2) == 0) { + const int e = 30 + r.below(30); + s[8] = [](ScriptHost& p) { p.sub("flat", flatten()); }; + s[10] = [e, group](ScriptHost& p) { p.sub("dust", in_group(tx(std::ldexp(1.0, -e)), group, 200)); }; + } + return s; +} + +Counts battery(std::uint64_t seed, int runs) { + Counts total; + int completed = 0; + int stopped = 0; + for (int i = 0; i < runs; ++i) { + std::map host_units; + const Script s = battery_script(seed * 1000003ULL + static_cast(i), &host_units); + const std::string name = "battery-" + std::to_string(seed) + "-" + std::to_string(i); + scenario = name; + const Outcome staged = run_one(name, s, flat_bars(14), false, host_units); + const Outcome direct = run_one(name, s, flat_bars(14), true, host_units); + CHECK(staged.completed == direct.completed); + CHECK(staged.transcript == direct.transcript); + CHECK(staged.continuation == direct.continuation && staged.broker == direct.broker); + if (staged.completed) { + ++completed; + } else { + ++stopped; + std::printf(" %s STOPPED code=%u disc=%u\n", name.c_str(), staged.code, + staged.discriminator); + } + CHECK(staged.completed); + total.add(staged.counts); + } + scenario = "battery-" + std::to_string(seed); + std::printf("battery seed %llu: runs=%d completed=%d STOPPED=%d reductions=%d absorbed=%d " + "deferred=%d absorbed=%d rounded=%d terms=%d absorbed=%d bound=%d absorbed=%d\n", + static_cast(seed), runs, completed, stopped, + total.reductions, total.absorbed_units, total.deferred, total.absorbed_pending, + total.rounded_pending, total.terms_with_pending, total.absorbed_terms, + total.bound_with_pending, total.absorbed_bound); + CHECK(stopped == 0); + // Every seed reaches the two absorptions a resting sibling meets. + CHECK(total.absorbed_units > 0); + CHECK(total.absorbed_pending > 0); + return total; +} + +} // namespace + +int main() { + p5b(); + units(1.0); + units(-1.0); + pending(); + pending_rounds(); + terms(); + terms_current(); + bound(); + overflow(); + Counts all; + for (std::uint64_t seed : {11ULL, 22ULL, 33ULL}) all.add(battery(seed, 1000)); + // Across the seeds it reaches every site: the terms and the bind too. + scenario = "battery"; + CHECK(all.absorbed_terms > 0); + CHECK(all.absorbed_bound > 0); + CHECK(all.rounded_pending > 0); + std::printf("%d checks, %d failures\n", checks, failures); + return failures == 0 ? 0 : 1; +} diff --git a/tests/test_native_order_resting_core.cpp b/tests/test_native_order_resting_core.cpp index b9304e83..61392c5f 100644 --- a/tests/test_native_order_resting_core.cpp +++ b/tests/test_native_order_resting_core.cpp @@ -485,7 +485,14 @@ EventId fill_group_source(WorkingRequestCore& core, RequestHandle handle, double return EventId{kRun, std::get(core.history().back()).ordinal}; } -void checked_reservation_failure() { +// A group deduction's arithmetic. DBL_MAX pending plus DBL_MAX overflows +// binary64 and is still refused with UnrepresentableReservation: no sub-ulp +// deduction, which R5 lane K-ULP5's ruling does not absorb. The two sub-ulp +// deductions -- 1 into a 2^60 pending total, 1 off 2^60 remaining units -- +// were refused the same way before K-ULP5; now each is absorbed, on the staged +// and the direct path alike: one event that deducts 0, the recipient as it +// was, and a receipt, so a second preparation is AlreadyApplied. +void checked_reservation_arithmetic() { const double maxv = std::numeric_limits::max(); const double p60 = 0x1p60; { @@ -522,7 +529,7 @@ void checked_reservation_failure() { .total == maxv); } - { + for (const bool direct : {false, true}) { WorkingRequestCore core(kRun); uint64_t inc = 1; uint64_t ord = 1; @@ -535,20 +542,44 @@ void checked_reservation_failure() { const auto waiting = core.submit(child, 1, inc, ord); const EventId cause = fill_group_source(core, *parent.handle, p60, inc, ord); commit_mutation(core, core.prepare_group_effect(cause, *waiting.handle, ord), ord); + const auto chain = std::get( + core.find_live(*waiting.handle)->pending); Request filler2{Transact{1.0}, "f2", ""}; filler2.group = Member{4, 1, GroupEffect::Reduce}; const auto parent2 = core.submit(filler2, 1, inc, ord); const EventId cause2 = fill_group_source(core, *parent2.handle, 1.0, inc, ord); - auto second = core.prepare_group_effect(cause2, *waiting.handle, ord); - CHECK(std::holds_alternative(second)); - CHECK(std::get(second).code - == pineforge::native_order::CoreFailure::UnrepresentableReservation); - CHECK(std::get( - core.find_live(*waiting.handle)->pending) - .total - == p60); + const std::size_t hist = core.history().size(); + if (direct) { + auto second = core.apply_group_effect(cause2, *waiting.handle, ord); + CHECK(std::holds_alternative(second)); + if (std::holds_alternative(second)) { + CHECK(std::get(second).events.count == 1); + ord += std::get(second).events.count; + } + } else { + CHECK(commit_mutation(core, core.prepare_group_effect(cause2, *waiting.handle, ord), ord) + == 1); + } + // fl(2^60 + 1) is 2^60: the fill joins no chain and defers nothing. + CHECK(core.history().size() == hist + 1); + const auto& absorbed = std::get(core.history().back()); + CHECK(absorbed.cause == cause2 && absorbed.recipient == *waiting.handle); + CHECK(absorbed.deferred_delta == 0.0); + const auto* before = + std::get_if(&absorbed.pending_before); + CHECK(before && before->total == p60 && before->count == 1 + && before->tail_receipt == chain.tail_receipt); + CHECK(absorbed.pending_after.total == p60 && absorbed.pending_after.count == 1 + && absorbed.pending_after.tail_receipt == chain.tail_receipt); + CHECK(!absorbed.previous_pending_receipt); + const auto after = std::get( + core.find_live(*waiting.handle)->pending); + CHECK(after.total == p60 && after.count == 1 && after.tail_receipt == chain.tail_receipt); + auto again = core.prepare_group_effect(cause2, *waiting.handle, ord); + CHECK(std::holds_alternative(again) + && std::get(again).reason == NoChangeReason::AlreadyApplied); } - { + for (const bool direct : {false, true}) { WorkingRequestCore core(kRun); uint64_t inc = 1; uint64_t ord = 1; @@ -563,12 +594,28 @@ void checked_reservation_failure() { const auto parent = core.submit(filler, 1, inc, ord); const EventId cause = fill_group_source(core, *parent.handle, 1.0, inc, ord); const std::size_t hist = core.history().size(); - auto reduce = core.prepare_group_effect(cause, *dest.handle, ord); - CHECK(std::holds_alternative(reduce)); - CHECK(std::get(reduce).code - == pineforge::native_order::CoreFailure::UnrepresentableReservation); - CHECK(core.history().size() == hist); + if (direct) { + auto reduce = core.apply_group_effect(cause, *dest.handle, ord); + CHECK(std::holds_alternative(reduce)); + if (std::holds_alternative(reduce)) { + CHECK(std::get(reduce).events.count == 1); + ord += std::get(reduce).events.count; + } + } else { + CHECK(commit_mutation(core, core.prepare_group_effect(cause, *dest.handle, ord), ord) + == 1); + } + // fl(2^60 - 1) is 2^60: requested 1, deducted 0, the units stand. + CHECK(core.history().size() == hist + 1); + const auto& reduced = std::get(core.history().back()); + CHECK(reduced.cause == cause && reduced.recipient == *dest.handle); + CHECK(reduced.requested_delta == 1.0 && reduced.actual_deduction == 0.0); + CHECK(reduced.before.q == p60); + CHECK(std::get(reduced.after).q == p60); CHECK(std::get(core.find_live(*dest.handle)->remaining).q == p60); + auto again = core.prepare_group_effect(cause, *dest.handle, ord); + CHECK(std::holds_alternative(again) + && std::get(again).reason == NoChangeReason::AlreadyApplied); } } @@ -1037,7 +1084,7 @@ int main() { partial_retain_and_target_exhaust(); group_reduce_defer_and_quantity_bound(); owner_applied_wait_through_close_only(); - checked_reservation_failure(); + checked_reservation_arithmetic(); stale_prepare_and_parent_terminal(); stop_activation_and_receipt_dedup(); wrong_core_token_and_move_invalidation(); diff --git a/tests/test_native_order_terms_core.cpp b/tests/test_native_order_terms_core.cpp index fadb4bcc..3f6e885b 100644 --- a/tests/test_native_order_terms_core.cpp +++ b/tests/test_native_order_terms_core.cpp @@ -470,14 +470,29 @@ void authenticated_deduction_and_terminals() { CHECK(!exhausted_group.fixture.core.find_live(exhausted_group.b)); } -void numeric_failure_and_chain_authentication() { - PendingGroup unrepresentable; - const auto* live = unrepresentable.fixture.core.find_live(unrepresentable.b); +// 0.1 pending against 1e16 resolved units: fl(1e16 - 0.1) is 1e16. Since R5 +// lane K-ULP5 the deduction is absorbed -- effective_host_units answers a +// deduction of 0 and the units standing, and the receipt spends the chain with +// effective_deduction 0, on the staged and the direct path alike. Before +// K-ULP5 effective_host_units refused it and prepare_terms answered +// UnrepresentableReservation, leaving the request as it was. Invalid inputs +// are still refused with the outputs untouched. +void numeric_absorption_and_chain_authentication() { + PendingGroup absorbed; + const auto* live = absorbed.fixture.core.find_live(absorbed.b); REQUIRE(live); double deduction = 17.0; double after = 19.0; bool exhausted = true; - CHECK(!no::WorkingRequestCore::effective_host_units(live->pending, 1e16, + CHECK(no::WorkingRequestCore::effective_host_units(live->pending, 1e16, + &deduction, &after, &exhausted)); + CHECK(deduction == 0.0 && after == 1e16 && !exhausted); + deduction = 17.0; + after = 19.0; + exhausted = true; + CHECK(!no::WorkingRequestCore::effective_host_units( + live->pending, std::numeric_limits::quiet_NaN(), &deduction, &after, &exhausted)); + CHECK(!no::WorkingRequestCore::effective_host_units(live->pending, -1.0, &deduction, &after, &exhausted)); CHECK(deduction == 17.0 && after == 19.0 && exhausted); CHECK(no::WorkingRequestCore::effective_host_units(live->pending, 0.0, @@ -487,28 +502,39 @@ void numeric_failure_and_chain_authentication() { &deduction, &after, &exhausted)); CHECK(deduction == 0.0 && after == 0.5 && !exhausted); - const auto history_size = unrepresentable.fixture.core.history().size(); - const auto ordinal = unrepresentable.fixture.ordinal; - const auto pending = live->pending; - const auto remaining = live->remaining; - const auto allowance = live->allowance; - const auto failed = unrepresentable.fixture.core.prepare_terms( - unrepresentable.b, unrepresentable.b_context, terms(100.0, 1e16), - unrepresentable.fixture.ordinal); - check_error(failed, no::CoreFailure::UnrepresentableReservation); - const auto& error = std::get(failed); - CHECK(error.cause.run == unrepresentable.fixture.run && error.cause.ordinal == 0); - CHECK(unrepresentable.fixture.core.history().size() == history_size); - CHECK(unrepresentable.fixture.ordinal == ordinal); - const auto* after_failure = unrepresentable.fixture.core.find_live(unrepresentable.b); - REQUIRE(after_failure); - CHECK(after_failure->pending.index() == pending.index()); - CHECK(after_failure->remaining.index() == remaining.index()); - CHECK(after_failure->allowance.index() == allowance.index()); - - const auto invalid = unrepresentable.fixture.core.prepare_terms( - unrepresentable.b, unrepresentable.b_context, - terms(100.0, std::numeric_limits::quiet_NaN()), unrepresentable.fixture.ordinal); + for (const bool direct : {false, true}) { + PendingGroup group; + const auto history_size = group.fixture.core.history().size(); + if (direct) { + auto installed = group.fixture.core.apply_terms( + group.b, group.b_context, terms(100.0, 1e16), group.fixture.ordinal); + REQUIRE(std::holds_alternative(installed)); + CHECK(std::get(installed).events.count == 1); + group.fixture.ordinal += std::get(installed).events.count; + } else { + const auto installed = group.fixture.install_mutation(group.fixture.core.prepare_terms( + group.b, group.b_context, terms(100.0, 1e16), group.fixture.ordinal)); + CHECK(installed.events.count == 1); + } + CHECK(group.fixture.core.history().size() == history_size + 1); + const auto& receipt = + std::get(group.fixture.core.history().back()); + CHECK(receipt.pending_total == 0.1); + CHECK(receipt.effective_deduction == 0.0); + CHECK(receipt.prior_adjustment_ids.size() == 1); + REQUIRE(std::holds_alternative(receipt.remaining_after)); + CHECK(std::get(receipt.remaining_after).q == 1e16); + const auto* bound = group.fixture.core.find_live(group.b); + REQUIRE(bound); + REQUIRE(std::holds_alternative(bound->remaining)); + CHECK(std::get(bound->remaining).q == 1e16); + CHECK(std::holds_alternative(bound->pending)); + } + + PendingGroup unresolved; + const auto invalid = unresolved.fixture.core.prepare_terms( + unresolved.b, unresolved.b_context, + terms(100.0, std::numeric_limits::quiet_NaN()), unresolved.fixture.ordinal); check_error(invalid, no::CoreFailure::InvalidProposal); PendingGroup corrupt; @@ -663,7 +689,7 @@ int main() { deferred_allowance_and_binding(); zero_paths_and_rejections(); authenticated_deduction_and_terminals(); - numeric_failure_and_chain_authentication(); + numeric_absorption_and_chain_authentication(); plan_keyed_accounting_and_shape_validation(); } catch (const std::exception& error) { ++failures; diff --git a/tests/test_native_resting_acceptance.cpp b/tests/test_native_resting_acceptance.cpp index 5adea9f2..a4dd62e7 100644 --- a/tests/test_native_resting_acceptance.cpp +++ b/tests/test_native_resting_acceptance.cpp @@ -361,7 +361,13 @@ void check_group_failure(Host& h, const no::RequestHandle& filler, CHECK(account_seen); } -void group_subtraction_failure(double sign) { +// A one-unit fill cannot move a 2^60-unit sibling's remaining units +// (fl(2^60 - 1) is 2^60). Since R5 lane K-ULP5 that deduction is absorbed: a +// ReservationReducedEvent that requests the fill and deducts 0, before and after +// both 2^60, and the run goes on. The request core answered +// UnrepresentableReservation after the fill was booked, and the run failed with +// SettlementFailure / discriminator 7, before. +void group_subtraction_absorbed(double sign) { Host h; start(h,sign > 0 ? "G5-subtract-long" : "G5-subtract-short",1,2); auto waiting = tx(sign * 0x1p60,"large-recipient"); waiting.trigger = no::Limit{sign > 0 ? 1.0 : 200.0}; @@ -369,16 +375,39 @@ void group_subtraction_failure(double sign) { const auto recipient = put(h,waiting); auto emit = tx(sign,"committed-filler"); emit.group = no::Member{7,1,no::GroupEffect::Reduce}; const auto filler = put(h,emit); - CHECK(!h.input(0,100)); check_group_failure(h,filler,recipient); + CHECK(h.input(0,100)); CHECK(h.native_state().kind == NativeLifecycleKind::Running); REQUIRE(fills(h,filler).size() == 1); + CHECK(fills(h,filler)[0].terminal); CHECK(fills(h,filler)[0].filled_working == 1 && fills(h,filler)[0].current_ticket == 2); - CHECK(events(h).empty()); + const auto reduced = events(h); REQUIRE(reduced.size() == 1); + CHECK(reduced[0].recipient == recipient); + CHECK(reduced[0].cause.ordinal == fills(h,filler)[0].ordinal); + CHECK(reduced[0].requested_delta == 1 && reduced[0].actual_deduction == 0); + CHECK(reduced[0].before.q == 0x1p60); + CHECK(std::get(reduced[0].after).q == 0x1p60); CHECK(events(h).empty()); + CHECK(fills(h,recipient).empty()); + bool kept = false; // the recipient keeps its units + for (const auto& row : h.native_working_requests()) { + if (!row.definition || row.definition->handle != recipient) continue; + const auto* units = std::get_if(&row.remaining); + kept = units && units->q == 0x1p60; + } + CHECK(kept); near(h.physical_position().signed_units,sign); near(h.native_marked_equity(100),9998); - failed_is_permanent(h,1,100); + // The run goes on: the next request fills and the stream completes. + const auto next = put(h,tx(sign,"next")); + CHECK(h.input(1,100)); CHECK(fills(h,next).size() == 1); + finish(h); } -void pending_addition_failure(bool overflow) { +// 2^60 pending and a second fill of 1: fl(2^60 + 1) is 2^60. Since R5 lane +// K-ULP5 that fill is absorbed -- a DeferredGroupAdjustmentEvent that defers 0 +// and leaves the pending total, its count and its tail as they were -- and the +// run goes on; it failed with discriminator 7 before. DBL_MAX pending plus +// DBL_MAX overflows binary64 instead: no sub-ulp deduction, which K-ULP5's +// ruling does not absorb, so that sum still fails the run. +void pending_addition(bool overflow) { Host h; const double first = overflow ? std::numeric_limits::max() : 0x1p60; const double second = overflow ? std::numeric_limits::max() : 1.0; @@ -401,16 +430,41 @@ void pending_addition_failure(bool overflow) { near(h.physical_position().signed_units,0); emit = tx(second,"second-delta"); emit.group = no::Member{7,1,no::GroupEffect::Reduce}; const auto second_filler = put(h,emit); - CHECK(!h.input(2,price)); check_group_failure(h,second_filler,recipient); - const auto after = events(h); REQUIRE(after.size() == 1); + if (overflow) { + CHECK(!h.input(2,price)); check_group_failure(h,second_filler,recipient); + const auto after = events(h); REQUIRE(after.size() == 1); + CHECK(after[0].ordinal == prior[0].ordinal && after[0].cause == prior[0].cause); + CHECK(events(h).empty()); + CHECK(events(h).empty()); + CHECK(events(h).empty()); + CHECK(fills(h,parent).empty()); + CHECK(h.physical_position().signed_units == second); + CHECK(fills(h,second_filler)[0].filled_working == second); + failed_is_permanent(h,3,price); + return; + } + CHECK(h.input(2,price)); CHECK(h.native_state().kind == NativeLifecycleKind::Running); + const auto after = events(h); REQUIRE(after.size() == 2); CHECK(after[0].ordinal == prior[0].ordinal && after[0].cause == prior[0].cause); + REQUIRE(fills(h,second_filler).size() == 1); + CHECK(fills(h,second_filler)[0].terminal); + CHECK(after[1].recipient == recipient); + CHECK(after[1].cause.ordinal == fills(h,second_filler)[0].ordinal); + CHECK(after[1].deferred_delta == 0); + const auto* before = std::get_if(&after[1].pending_before); + REQUIRE(before); + CHECK(before->total == first && before->count == 1); + CHECK(before->tail_receipt.ordinal == prior[0].ordinal); + CHECK(after[1].pending_after.total == first && after[1].pending_after.count == 1); + CHECK(after[1].pending_after.tail_receipt == before->tail_receipt); + CHECK(!after[1].previous_pending_receipt); CHECK(events(h).empty()); CHECK(events(h).empty()); CHECK(events(h).empty()); CHECK(fills(h,parent).empty()); CHECK(h.physical_position().signed_units == second); CHECK(fills(h,second_filler)[0].filled_working == second); - failed_is_permanent(h,3,price); + finish(h); } void run_case(const char* name, const std::function& body) { @@ -434,10 +488,10 @@ int main() { run_case(sign>0?"C9 long transaction first":"C9 short transaction first",[&]{flat_incarnation_order(sign,true);}); run_case(sign>0?"C9 long close first":"C9 short close first",[&]{flat_incarnation_order(sign,false);}); run_case(sign>0?"C8 long precommit arithmetic refusal":"C8 short precommit arithmetic refusal",[&]{capacity_progress_failure(sign);}); - run_case(sign>0?"G5 long postcommit subtraction failure":"G5 short postcommit subtraction failure",[&]{group_subtraction_failure(sign);}); + run_case(sign>0?"G5 long postcommit subtraction absorbed":"G5 short postcommit subtraction absorbed",[&]{group_subtraction_absorbed(sign);}); } - run_case("G5 pending 2^60 plus 1",[]{pending_addition_failure(false);}); - run_case("G5 pending DBL_MAX plus DBL_MAX",[]{pending_addition_failure(true);}); + run_case("G5 pending 2^60 plus 1 absorbed",[]{pending_addition(false);}); + run_case("G5 pending DBL_MAX plus DBL_MAX overflow",[]{pending_addition(true);}); std::printf("%s native resting acceptance: %d cases, %d checks, %d failures\n", failures?"FAIL":"PASS",cases,checks,failures); return failures?1:0; diff --git a/tests/test_native_resting_replay_contract.cpp b/tests/test_native_resting_replay_contract.cpp index 8df6cc39..b0a64633 100644 --- a/tests/test_native_resting_replay_contract.cpp +++ b/tests/test_native_resting_replay_contract.cpp @@ -874,25 +874,6 @@ void units_after(const no::ExecutionAppliedEvent& e, double before, double after same_d(std::get(e.remaining_after).q, after); } -void failed_is_permanent(Host& h, int64_t next_offset) { - REQUIRE(h.native_state().kind == NativeLifecycleKind::Failed); - const auto before = h.native_state().failure; - const auto hash = h.native_continuation_hash(); - const auto count = h.native_events(0).size(); - const auto position = h.physical_position(); - CHECK(!h.input(next_offset, 100)); - CHECK(!h.stream_advance_time(T + next_offset + 100)); - CHECK(!h.stream_end(false)); - bool threw = false; - try { (void)h.submit(tx(1)); } catch (const std::exception&) { threw = true; } - CHECK(threw); - CHECK(h.native_state().kind == NativeLifecycleKind::Failed); - same_failure(h.native_state().failure, before); - CHECK(h.native_events(0).size() == count); - CHECK(h.native_continuation_hash() == hash); - same_d(h.physical_position().signed_units, position.signed_units); -} - void replay_partial_budget(double sign) { Host a, b; const char* key = sign > 0 ? "P2-budget-L" : "P2-budget-S"; @@ -1285,7 +1266,13 @@ void replay_failed_capacity(double sign) { CHECK(hb.incarnation == ha.incarnation); } -void replay_failed_group(double sign) { +// A one-unit fill cannot move a 2^60-unit sibling's remaining units +// (fl(2^60 - 1) is 2^60). Since R5 lane K-ULP5 that deduction is absorbed -- a +// ReservationReducedEvent that requests the fill and deducts 0, the recipient's +// units 2^60 before and after -- and the run goes on; the request core answered +// UnrepresentableReservation and the run failed with discriminator 7 before. +// Both replays absorb identically. +void replay_absorbed_group(double sign) { Host a, b; const char* key = sign > 0 ? "P2-G5-L" : "P2-G5-S"; start(a, key, 1, 2); start(b, key, 1, 2); @@ -1296,22 +1283,31 @@ void replay_failed_group(double sign) { auto emit = tx(sign, "filler"); emit.group = no::Member{7, 1, no::GroupEffect::Reduce}; const auto fa = put(a, emit), fb = put(b, emit); same_hosts("accepted", a, b); - CHECK(!a.input(0, 100)); CHECK(!b.input(0, 100)); - same_hosts("failed", a, b); + CHECK(a.input(0, 100)); CHECK(b.input(0, 100)); + same_hosts("absorbed", a, b); + CHECK(a.native_state().kind == NativeLifecycleKind::Running); REQUIRE(fills(a, fa).size() == 1); CHECK(fills(a, fa)[0].terminal); CHECK(fills(a, ra).empty()); - CHECK(events_of(a).empty()); - const auto failure = a.native_state().failure; - CHECK(failure.discriminator == static_cast(no::CoreFailure::UnrepresentableReservation)); - CHECK(native_failure_has_cause(failure.context) && native_failure_has_recipient(failure.context)); - CHECK(failure.context.cause.ordinal == fills(a, fa)[0].ordinal); - CHECK(failure.context.recipient.incarnation == ra.incarnation); + const auto reduced = events_of(a); + REQUIRE(reduced.size() == 1); + CHECK(reduced[0].recipient == ra); + CHECK(reduced[0].cause.ordinal == fills(a, fa)[0].ordinal); + same_d(reduced[0].requested_delta, 1); + same_d(reduced[0].actual_deduction, 0); + same_d(reduced[0].before.q, 0x1p60); + same_remaining_proj(reduced[0].after, no::RemainingProjectionUnits{0x1p60}); + CHECK(cancellations(a, ra).empty()); same_d(a.physical_position().signed_units, sign); - failed_is_permanent(a, 1); - failed_is_permanent(b, 1); - same_hosts("refused", a, b); + // The run goes on: the same next fill on both, and both complete. + const auto na = put(a, tx(sign, "next")), nb = put(b, tx(sign, "next")); + CHECK(a.input(1, 100)); CHECK(b.input(1, 100)); + CHECK(fills(a, na).size() == 1); + same_hosts("continued", a, b); + finish(a); finish(b); + same_hosts("complete", a, b); CHECK(fb.incarnation == fa.incarnation && rb.incarnation == ra.incarnation); + CHECK(nb.incarnation == na.incarnation); } void perturb_one_field(double sign) { @@ -1806,8 +1802,8 @@ int main() { [&] { replay_confirmed_bar(sign); }); run_case(sign > 0 ? "refused capacity prefix long" : "refused capacity prefix short", [&] { replay_failed_capacity(sign); }); - run_case(sign > 0 ? "failed group prefix long" : "failed group prefix short", - [&] { replay_failed_group(sign); }); + run_case(sign > 0 ? "absorbed group prefix long" : "absorbed group prefix short", + [&] { replay_absorbed_group(sign); }); run_case(sign > 0 ? "perturb fields long" : "perturb fields short", [&] { perturb_one_field(sign); }); run_case(sign > 0 ? "perturb owner long" : "perturb owner short", From 3e2157116301b89f3fc94c4928877f9d8c6c619f Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 03:03:44 +0800 Subject: [PATCH 22/24] Tests, docs: the absorption's C reach stated, each deduction taken on its own, and the checks that could pass for the wrong reason tightened (R5 lane K-ULP5, review) An independent review of 1d7639c8 found no defect in the kernel change and these tightenings, each applied here: - native-engine.md said "a C row's closed_units is 0" after listing all four markers; C's TERMS_RESOLVED and QUANTITY_BOUND rows carry no deduction. The page now says a C host reads the first two as RESERVATION_REDUCED / DEFERRED_GROUP rows with closed_units 0, and the terms/bind absorption as the units that stand beside the DEFERRED_GROUP rows it received. - "effective_deduction 0 beside a positive pending_total" also describes a host that answers 0 units; the marker now requires units above zero (page and TermsResolvedEvent doc). - Each deduction is taken on its own: a hundred fills of 1 against 2^60 are a hundred absorbed deductions and leave it 2^60, where one deduction of 100 leaves fl(2^60 - 100) = 2^60 - 128. Stated in native-settlement.md and pinned by the new units-repeated row (the hundred share one cohort, so they reduce the recipient and not each other). - CoreFailure::UnrepresentableReservation's doc names the defensive non-finite guards beside the overflow. - test_native_c_api.c: the lot count could not fail for "the fraction's fill did not stand" (two lots either way); it now checks the fraction's one APPLIED row of the dust and the head lot's units. The -current rows now assert what execute_current answered: the host-sized fill (test_native_group_absorption) and the typed UnrepresentableQuantity refusal (A-T4d in test_native_execution_terms). - native-feature-parity.md OL7 cited ReservationReducedEvent's lines for the deferred form too (carried from the base); it now cites DeferredGroupAdjustmentEvent. Citations re-anchored from 1d7639c8 (anchors 1678/1678). Fail-before on f1af50dc: test_native_group_absorption fails 975 of 177,837 checks (914 of 3,000 battery runs stop, all code 6 / disc 7); the C rows fail 8. On this commit: 206,377 checks, 0 failures; C ok. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 2a03c6580c74906c585618b050c3c32cff999827) INT25 merge: the pages merge on digit-normalised text; the lane is re-anchored once here (4 citations into native_c_api.h carried through the exact line map from c3a45b50). Every anchor cites the text one parent cited (verify_carry: 1675 exact, 6 = the floor lines). --- docs/design/native-feature-parity.md | 20 +++++----- docs/native-settlement.md | 5 ++- docs/pages/native-engine.md | 11 +++++- docs/pages/pine-to-native.md | 22 +++++------ include/pineforge/native_order.hpp | 7 ++-- tests/test_native_c_api.c | 23 +++++++++++- tests/test_native_execution_terms.cpp | 7 +++- tests/test_native_group_absorption.cpp | 51 +++++++++++++++++++++++--- 8 files changed, 110 insertions(+), 36 deletions(-) diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index f603019f..bc98a62f 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:1883, *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:1884, *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). | @@ -49,14 +49,14 @@ 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 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 | — | -| 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:1016-1030; result and event types `SubmitStatus` native_order.hpp:835-861, `ReplacedEvent` native_order.hpp:1000-1066 | 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:1702); 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: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 | — | | 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:1190-1200 (deferred form native_order.hpp:1190-1200) | K | — | F:A12 O:A6 O:A7 S:O7 | — | +| 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:1883); 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: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. | @@ -82,7 +82,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | 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:1420-1437; 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: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. | | 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 | @@ -347,7 +347,7 @@ the campaign's own anchor audit found that the first thing to rot: - **(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. - **(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:1307). Every new test unit compiles first against the frozen previous-epoch header closure (fail-before, `CLAUDE.md`). +- **(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. ### 3.2 Lanes: scope, size, dependencies, epochs @@ -1285,7 +1285,7 @@ NativeMetricsView native_metrics() const; // S: net_pro ### A.2 L3 — sizing, fractional reduce, sibling reservation (F P1, P7 + O G2 + S P2, P3) ```cpp -// native_order.hpp — native_order_v6; OrderIntent grows 5 -> 6 (static_assert native_order.hpp:1387) +// native_order.hpp — native_order_v6; OrderIntent grows 5 -> 6 (static_assert native_order.hpp:1388) struct CashValue { double cash = 0.0; }; // account currency struct EquityFraction { double fraction = 0.0; }; // of marked equity at the sizing point (0.10 = 10 %) using SizeBasis = std::variant; @@ -1330,7 +1330,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:1307). 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 (`sha256:ed6f4ff668195caf3c953c8ef5dd448c6010949353f7b9ef44d50b764f7488db` native_execution_consumer.cpp:1898-1911), 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). @@ -1405,7 +1405,7 @@ BracketReceipt submit_bracket(NativeStrategyHost&, const BracketSpec&); // emi template class OrderBook; // id -> handle: submit-or-replace, cancel-by-id ``` -- A leg with owner `WaitForApplied` / `BindOpening` and a non-absolute anchor gets its level when the owner's fill arms it (`ArmedEvent` native_order.hpp:1245-1252 already exists) (O). Fallback with no kernel change: the toolkit materializes relative levels from the host's `on_native_applied` (F). +- A leg with owner `WaitForApplied` / `BindOpening` and a non-absolute anchor gets its level when the owner's fill arms it (`ArmedEvent` native_order.hpp:1246-1253 already exists) (O). Fallback with no kernel change: the toolkit materializes relative levels from the host's `on_native_applied` (F). - Gap lane N13 (after R4d): two facts of the arm were missing and are now opt-in fields appended to `WaitForApplied`, each default byte-identical and folded only when set. `NativeArmFirstMatch { AtArmPrint, AfterArmPrint }`: whether the armed leg is a candidate at its owner's fill print or, like a request born in the owner's fill callback, only after it. `NativeArmScope { OwnerLot, Book }`: whether an armed closing leg closes the lot its owner opened or is bound at the arm to the whole position (a `BookClose`), under which it may be `HostSized`. Ruling: generic — both are broker models of a contingent child, neither names a source language — and they are what makes an anchored child the very request a host would submit from `on_native_applied`. The Pine adapter's relative `strategy.exit` legs use both; R4d's owner-lot spelling left a later same-id add open where the source leg closes the book (`rel-pyramid-set-once`, tests/test_adapter_brackets_relower.cpp). - Not adopted (R5-2): S's kernel bracket object, its C symbols `strategy_native_bracket_*` and its TU list (`engine_orders.cpp`, `engine_execution.cpp`). - TUs: `native_order.{hpp,cpp}` (arming), `native_matching.hpp` (`checked_trail_stop`), `native_execution_consumer.cpp` (`prepare_owner_applied` path, command surface). diff --git a/docs/native-settlement.md b/docs/native-settlement.md index 8b05da12..8ce5193a 100644 --- a/docs/native-settlement.md +++ b/docs/native-settlement.md @@ -148,7 +148,10 @@ total, which its terms or its owner's fill later takes off the units they bind the units again: at most half an ulp of them) is absorbed. The sibling keeps its units, the exact binary64 result of the subtraction; the member's fill stands; and the event that records the deduction took nothing (R5 lane K-ULP5; -`docs/pages/native-engine.md`, "What a host gets from a quantity"). It takes a +`docs/pages/native-engine.md`, "What a host gets from a quantity"). Each +deduction is taken on its own: a hundred fills of 1 beside a 2^60 sibling are +a hundred absorbed deductions and leave it 2^60, where one deduction of 100 +would leave `fl(2^60 - 100)`, 2^60 - 128. It takes a dust-sized fill -- the close of a dust lot -- beside a far larger sibling, and until K-ULP5 it failed the run after the fill was booked, with code 6, discriminator 7 (`CoreFailure::UnrepresentableReservation`); only a pending diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 4023b2ba..25b6028c 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -3150,8 +3150,15 @@ and quantities"); what a host sees: exact binary64 result; the member's fill stands, and the event says it took nothing: `actual_deduction` 0 with `after` equal to `before`, or `deferred_delta` 0 with `pending_after` equal to `pending_before` (it joins - no chain), or `effective_deduction` 0 beside a positive `pending_total`; a C - row's `closed_units` is 0. After open 0.5, open 1000.1 and a bound close of + no chain), or `effective_deduction` 0 beside a positive `pending_total` and + units above zero that stand. A C host reads the first two as + `PF_NATIVE_EVENT_RESERVATION_REDUCED` and `PF_NATIVE_EVENT_DEFERRED_GROUP` + rows whose `closed_units` is 0; its `TERMS_RESOLVED` and `QUANTITY_BOUND` + rows carry no deduction, so there it reads the absorption as the units that + stand -- the fill, or the working row's remaining units -- beside the + `DEFERRED_GROUP` rows it received. Each deduction is taken on its own, so + fills absorbed one by one can leave a sibling above what one deduction of + their sum would. After open 0.5, open 1000.1 and a bound close of 1000, one group of a resting `Reduce 1000`, a `Reduce 0.3` and a `ScopeFraction 1`: the fraction closes 2.2759572004815709e-14, below half an ulp of the resting member's 999.7, which keeps 999.70000000000005. A pending diff --git a/docs/pages/pine-to-native.md b/docs/pages/pine-to-native.md index 131fb4d5..5ff1c047 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -67,9 +67,9 @@ gain: and nothing comes from a chart. - **Typed orders and typed outcomes.** A request is a value (`Request` native_order.hpp:483); its fate is a typed event — `AcceptedEvent` - (native_order.hpp:1000), `ExecutionAppliedEvent` (native_order.hpp:1145), - `MatchRejectedEvent` (native_order.hpp:1107), `CancelledEvent` - (native_order.hpp:1042) — readable in order from `native_events` + (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). - **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++. @@ -283,7 +283,7 @@ is `strategy_native_submit_v1` native_c_api.h:2680 over a | `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: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:1164. 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()` | `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. | @@ -303,7 +303,7 @@ 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 -`ReplacedEvent` native_order.hpp:1016 and can carry the predecessor's live +`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 spelling is `strategy_native_replace_v1`, or `strategy_native_replace_ext_v1` @@ -315,8 +315,8 @@ 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:2110-2117 | `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:2111-2115 | `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_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. | `limit=` and `stop=` are listed once here rather than once per command, @@ -340,7 +340,7 @@ once. Natively they are separate and each is nameable: `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 - in `ArmedEvent` native_order.hpp:1245. + 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 @@ -432,7 +432,7 @@ liquidation level, and the ticket the forced close is booked under. | 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` | | 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:1261 delivered to `on_native_margin_call` native_host.hpp:989 | `on_margin_call` native_c_api.h:2474 | `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 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 @@ -519,14 +519,14 @@ range-end reporting is off. | --- | --- | --- | --- | --- | | `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:3015 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: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.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.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` `sha256:0490e46ca24b81c82c9549ce38af81be1ea888a60277263108cd26bf3f01e0c7` engine.hpp:1024 | `strategy_native_marked_equity_v1` native_c_api.h:3015 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` `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.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. | diff --git a/include/pineforge/native_order.hpp b/include/pineforge/native_order.hpp index 83f66d25..db7e3c17 100644 --- a/include/pineforge/native_order.hpp +++ b/include/pineforge/native_order.hpp @@ -974,7 +974,8 @@ enum class CoreFailure : std::uint8_t { StaleHandle = 4, UnsupportedTransition = 5, ConflictingReceipt = 6, - /// A group deduction's pending total that overflows binary64. A deduction + /// A group deduction's pending total that overflows binary64 (or a + /// non-finite reservation, which a valid run does not reach). A deduction /// binary64 cannot take off its recipient is absorbed instead (R5 lane /// K-ULP5); it failed the run with this discriminator before. UnrepresentableReservation = 7, @@ -1083,8 +1084,8 @@ struct NoEffectEvent { /// deferred it also binds them: it spends the pending group chain /// (`prior_adjustment_ids`, `pending_total`), takes `effective_deduction` off the /// resolved units and leaves `remaining_after`. That deduction is 0 with a -/// positive `pending_total` when binary64 cannot take the total off the units -- -/// absorbed, and the units stand (R5 lane K-ULP5). +/// positive `pending_total` and positive resolved units when binary64 cannot +/// take the total off them -- absorbed, and the units stand (R5 lane K-ULP5). struct TermsResolvedEvent { uint64_t ordinal = 0; DefinitionRef definition; diff --git a/tests/test_native_c_api.c b/tests/test_native_c_api.c index 29b9c6c5..7fd413df 100644 --- a/tests/test_native_c_api.c +++ b/tests/test_native_c_api.c @@ -1628,10 +1628,13 @@ static void check_reservation_absorbed(void) { static pf_native_event_v1 rows[256]; kulp5_state state; pf_native_working_v1 working; + pf_native_open_lot_v1 lot; const double resting_units = 1000.0 - 0.3; + const double head = 0.5 - 0.3; /* the 0.5 lot after the 0.3 close */ + const double dust = (head + (1000.1 - 1000.0)) - 0.3; /* the fraction's units net of 0.3 */ int got = 0; int i, n; - int reduced = 0, absorbed = 0, deferred = 0; + int reduced = 0, absorbed = 0, deferred = 0, applied = 0; double first_reduction = -1.0; uint32_t lifecycle; @@ -1669,8 +1672,24 @@ static void check_reservation_absorbed(void) { ++reduced; } CHECK_EQ_INT(reduced, 1, "the resting member is no longer working"); + /* The fraction's fill stands: one APPLIED row closing the dust, taken off + * the head lot. */ + applied = 0; + for (i = 0; i < got; ++i) { + if (rows[i].kind == PF_NATIVE_EVENT_APPLIED && rows[i].incarnation == state.fraction) { + CHECK(rows[i].closed_units == dust, "the fraction closed another quantity"); + ++applied; + } + } + CHECK_EQ_INT(applied, 1, "the fraction's fill did not stand"); CHECK_EQ_INT(strategy_native_open_lot_count_v1(state.host, NAN), 2, - "the fraction's fill did not stand"); + "the book holds another number of lots"); + memset(&lot, 0, sizeof(lot)); + lot.struct_size = (uint32_t)sizeof(lot); + lot.version = PF_NATIVE_API_VERSION; + CHECK_EQ_INT(strategy_native_open_lot_get_v1(state.host, 0, &lot), PF_NATIVE_OK, + "the head lot could not be read"); + CHECK(lot.signed_units == head - dust, "the dust was not taken off the head lot"); strategy_native_host_free(state.host); lifecycle = kulp5_run(&state, 1, rows, 256, &got); diff --git a/tests/test_native_execution_terms.cpp b/tests/test_native_execution_terms.cpp index 66742230..56b7b1d9 100644 --- a/tests/test_native_execution_terms.cpp +++ b/tests/test_native_execution_terms.cpp @@ -675,12 +675,17 @@ void a_t4d_authenticated_absorbed_deduction() { CHECK(h.native_continuation_hash() == hash); CHECK(h.validator_calls == 1); bool execute_threw = false; + bool refused_typed = false; try { - (void)h.execute_current(command(b_target)); + const auto result = h.execute_current(command(b_target)); + const auto* refused = std::get_if(&result); + refused_typed = refused + && refused->reason == no::MatchRejectReason::UnrepresentableQuantity; } catch (const std::exception&) { execute_threw = true; } CHECK(!execute_threw); + CHECK(refused_typed); CHECK(h.native_state().kind == NativeLifecycleKind::Running); const auto terms = last_event(h); REQUIRE(terms); diff --git a/tests/test_native_group_absorption.cpp b/tests/test_native_group_absorption.cpp index 9896e9b6..3177485e 100644 --- a/tests/test_native_group_absorption.cpp +++ b/tests/test_native_group_absorption.cpp @@ -37,7 +37,8 @@ // 2.2759572004815709e-14, below half an ulp of the resting // member's 999.7 -- absorbed; a later opening fills // units-* a 2^60 recipient beside fills of 1 (absorbed) and 256 -// (deducted), long and short +// (deducted), long and short; a hundred fills of 1, each +// absorbed on its own, then one of 100 (2^60 - 128) // pending a bracket child waiting on its parent holds 2^60 pending; // a fill of 1 is absorbed; the parent's fill binds the chain // pending-rounds a pending 2^-60 then a fill of 1: the total is 1 @@ -57,8 +58,8 @@ // Every case runs on the request core's staged and direct paths, which must // agree bit for bit, hashes included. // -// Fail-before: compiled against f1af50dc (K-ULP4) the TU builds and fails 970 -// of 177,824 checks: p5b, units-*, pending, pending-rounds, terms, +// Fail-before: compiled against f1af50dc (K-ULP4) the TU builds and fails 975 +// of 177,837 checks: p5b, units-*, pending, pending-rounds, terms, // terms-current and bound stop with code 6, discriminator 7 (terms-current's // preview throws "native host-sized deduction is not representable" first), // and the battery stops 321, 290 and 303 of its 1,000 runs per seed, each with @@ -544,6 +545,43 @@ void units(double sign) { CHECK(events_of(host).empty()); } +// Each deduction is taken on its own: a hundred fills of 1 against 2^60 are a +// hundred absorbed deductions and leave it 2^60; one fill of 100 then takes it +// to fl(2^60 - 100), which is 2^60 - 128. The hundred share one cohort, so +// they reduce the recipient and not each other. +void units_repeated() { + Script s; + s[0] = [](ScriptHost& p) { + auto recipient = in_group(tx(kP60), 7, 1); + recipient.trigger = no::Limit{1.0}; + p.sub("recipient", recipient); + for (int i = 0; i < 100; ++i) { + p.sub("fill-" + std::to_string(i), in_group(tx(1.0), 7, 2)); + } + }; + s[2] = [](ScriptHost& p) { p.sub("fill-100", in_group(tx(100.0), 7, 200)); }; + const Outcome o = run_both("units-repeated", s, flat_bars(6)); + const ScriptHost& host = *o.host; + CHECK(o.completed); + const auto reduced = events_of(host); + CHECK(reduced.size() == 101); + int absorbed = 0; + for (std::size_t i = 0; i + 1 < reduced.size(); ++i) { + if (reduced[i].requested_delta == 1.0 && reduced[i].actual_deduction == 0.0 + && reduced[i].before.q == kP60 + && std::get(reduced[i].after).q == kP60) { + ++absorbed; + } + } + CHECK(absorbed == 100); + if (reduced.size() == 101) { + CHECK(reduced[100].requested_delta == 100.0 && reduced[100].actual_deduction == 100.0); + CHECK(std::get(reduced[100].after).q == kP60 - 128.0); + } + CHECK(remaining_is(host, "recipient", kP60 - 128.0)); + CHECK(o.counts.absorbed_units == 100); +} + // Bars at 100 but `trigger_bar`, where the price is 250: a buy stop at 200 // fills there. std::vector bars_with_rise(int n, int trigger_bar) { @@ -674,8 +712,8 @@ void terms_current() { p.preview_threw = true; } try { - p.current_kind = static_cast( - p.execute_current(NativeCurrentExecution{p.h["host-open"]}).index()); + const auto result = p.execute_current(NativeCurrentExecution{p.h["host-open"]}); + p.current_kind = std::holds_alternative(result) ? 1 : 0; } catch (const std::exception&) { p.current_kind = -2; } @@ -684,7 +722,7 @@ void terms_current() { const ScriptHost& host = *o.host; CHECK(o.completed); CHECK(!host.preview_threw); - CHECK(host.current_kind >= 0); + CHECK(host.current_kind == 1); // execute_current answered the host-sized fill const auto resolved = events_of(host); CHECK(resolved.size() == 1); if (resolved.size() == 1) { @@ -996,6 +1034,7 @@ int main() { p5b(); units(1.0); units(-1.0); + units_repeated(); pending(); pending_rounds(); terms(); From 16496aea7053a453ba955e26a7c188925a7a35ef Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 04:45:14 +0800 Subject: [PATCH 23/24] INT25 merge: two row-8 citations C-SURFACE-1's page kept over its own c_abi.cpp insertions docs/design/native-feature-parity.md row 8 cites c_abi.cpp:848 (the end of strategy_set_native_security_feed, "? 0 : -1;") and c_abi.cpp:902 ("cpp.identity.run_number = spec->run_number;"). C-SURFACE-1 inserted lines above both in src/c_abi.cpp and moved the row's hashed ranges, but left these two symbol-less single-line citations at their base digits, so on this tree they cited `});` and `if (!host) return -1;`. The anchor gate only checks that a symbol-less line is code, and the pick's exact carry accepted the lane's own copy of the file unjudged. They now cite the same text: c_abi.cpp:862 and c_abi.cpp:929. A scan of every page for anchors that kept base digits while the base text moved (stale_scan.py, against 1a0e7ea1) finds no other. Found by the independent review of the integration. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/design/native-feature-parity.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index bc98a62f..499faefb 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -320,7 +320,7 @@ Union of F §2.vi (9), O §2.vi (5 + 1) and S §2.6 (7), deduplicated. S had no | 5 | "`engine_aux_security.cpp` includes `source/pine_strategy_host.hpp`" (ADR L129-132) | False at `73817c1`: its includes are `engine_internal.hpp`, `ta.hpp` and std headers (*fresh* re-read). 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 | | 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:848, `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:902 | F O S | +| 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 | | 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) | From 2a75925cc905acd7d0b31baa9f512e25a4578bc4 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 04:11:57 +0800 Subject: [PATCH 24/24] INT25 v19 hash re-pin V19-FIX, K-IDX and KERNEL-EDGE each moved v19 hash values in their own named commits, each against a tree without the others, and their pins met in three witnesses; the picks kept one side in each conflict. This commit re-harvests every witness pin once, on the integrated tree (after the K-ULP5 picks, which moved none: all ten harvests are byte-identical before and after them), inside the unreleased v19 epoch; no epoch token moves. How: each pinned TU compiled with its own harvest switch against this tree's library, with its ctest target's exact compile and link lines (harvest_int25: V19-FIX's harvest3 plus PINEFORGE_K1_HARVEST for the calendar witness), then repin_int25 (V19-FIX's repin_int21 plus the calendar rows), which STOPs on any non-hash change: a moved trade, trade digest, net profit, error, count, bar, driver row or other non-hash field aborts the run (both refusals checked by mutating a harvested trade count and a calendar trade count). Seven of the ten harvested witnesses reproduce their pins exactly (report_relower, recording_hash_witness, receipts_in_place, quiet_bar, command_allocations, lookup_index_witness, publication_witness); every other witness row passes unchanged in the full suite. A second harvest after writing reproduces every pin: 0 moved. Moved (hash values only; the notes above each table list them again): test_adapter_continuation_view k_chart (7 of 7): 14621553252499744072 -> 5575980828577033360 1560069015253079270 -> 17778912298194596574 8181232920474966978 -> 16463834114134690168 8150352125352055815 -> 8941018855024971093 4370795498617211977 -> 2898399952537902865 10361542891827113526 -> 8964175343669296125 4370795498617211977 -> 2898399952537902865 test_adapter_continuation_view k_magnifier (7 of 7): 1310098789583311035 -> 4430826578646987126 9252967425603149690 -> 14738067743722080804 9717019523752667192 -> 2077753189921542836 14284557384012157011 -> 2917741806179085516 15984354419168920510 -> 13581436790443683042 16607038826027722339 -> 14737345821341801219 15984354419168920510 -> 13581436790443683042 test_adapter_continuation_view k_aggregated (5 of 5): 7408940017323371281 -> 16542564830293500828 11387000028137044299 -> 13031512803108236715 4995545673543620333 -> 6906214955679447270 9893483468656556008 -> 10543459672655655434 4995545673543620333 -> 6906214955679447270 test_adapter_continuation_view k_aggregated_magnifier (5 of 5): 2376229218543853263 -> 14031106247497767919 301774797075971540 -> 10607093485200722581 6106527879897255797 -> 22607172627576334 14842205570428090150 -> 12265832737220737843 6106527879897255797 -> 22607172627576334 test_adapter_continuation_view k_coof (10 of 10): 10706142720152936082 -> 16820608513549156800 14042469651802437968 -> 9462991605602344175 1992762524201176782 -> 12220673217690837314 9972018108613272455 -> 1856715264914641232 9011525226083087013 -> 12811765853164186626 7755254022538140640 -> 9115044940243703863 16056359684121154720 -> 9198850018143016781 10916717879615654635 -> 784137025905909089 17916071608255404005 -> 11981249830754474578 10916717879615654635 -> 784137025905909089 test_adapter_continuation_view k_recording (71 of 72): 14621553252499744072 -> 5575980828577033360 16947653718640119089 -> 16309388099732889217 8181232920474966978 -> 16463834114134690168 384032772146433484 -> 14697449118452563264 4370795498617211977 -> 2898399952537902865 10361542891827113526 -> 8964175343669296125 4370795498617211977 -> 2898399952537902865 11635388043626139808 -> 11912929120179152844 5137642655964282706 -> 7111499071038382551 1067158101705749258 -> 14395892391241930212 17589216349291364682 -> 9034635987304089269 13103826456263923669 -> 7745035282290493167 14125475791418217750 -> 9653978480792136139 5496734832958426517 -> 6575784925310671623 3300864403891436803 -> 16756306850270906652 14334667028075329258 -> 6791844355462743637 19027534220486873 -> 9752175874850418240 17400684420983527587 -> 5581794927458177019 16843748232757858737 -> 16367531041072987671 5910420545552211065 -> 5499206076623092635 4037494890945270795 -> 16891823664600062531 13685270228904493022 -> 15306266440153708710 12610051923577992823 -> 8382791724770590634 13419685112760992640 -> 4926336483410460532 6561776234336247499 -> 12823090459845782834 11286535916465691925 -> 10217177223946990874 9286401377316929631 -> 12680312482263936655 11941619648187175172 -> 15032085204869760060 2781110012097249044 -> 7696336452231268012 5152088055254729287 -> 6819477884530924764 14925422330047382771 -> 13932522791731114426 18068979986855895194 -> 15842817164955438950 3293728938989802792 -> 4250764768037495411 3040154494312885091 -> 1760469320874157114 3914527074597397115 -> 7565330933885892856 6028800054409062637 -> 3348650657119335616 17539544361470737737 -> 16026642042438818681 17497902664474484401 -> 15336121108426360137 16821147754446235025 -> 12610087835245307199 3501965129295230921 -> 3169655936610324835 17136543747664596622 -> 18005345939863936504 6796102544471338071 -> 10374887129263366256 10258488113034267824 -> 3316605285128650350 15437545425183691961 -> 10411230090089985874 10208872586329130020 -> 2321647345879300563 14368668855499836453 -> 9282855342318040804 628171839165946395 -> 8782939554936041465 10702221356985455012 -> 9270809468617755726 2314157047087909764 -> 9705564861311910669 12920471494293590838 -> 1984027035687621850 8752737450779041921 -> 5309816640440296119 7994102271719463356 -> 61906512444861614 4741946402210887143 -> 18408253739502061131 739366574823717504 -> 2900636347809437670 3655914840365203865 -> 8590510826456084479 7448010022416009296 -> 16739036358771975930 11266041923012349372 -> 3846505090147632664 16061447354858643806 -> 13346518438036570112 6641998937765131086 -> 3939181335635768762 15745847663605277053 -> 1996675722330704212 2232785799096296677 -> 271113896427528122 12057790076704791676 -> 1511893874670543988 14126847163984952264 -> 15973548105797383183 6703125855558665936 -> 4392899708297163730 750370809058189391 -> 11467883812317724901 727242285392145435 -> 11051413718335140252 9029503590661437192 -> 903533060614176790 6205861443650071106 -> 14466209299323094098 5548300568428407565 -> 4010960734010157804 4191735526950747220 -> 5172512134487820157 4370795498617211977 -> 2898399952537902865 test_adapter_continuation_view k_stream (54 of 54): 16510252855932086823 -> 16458853630937199693 14048324983519873828 -> 10534014544629119321 17804079663198632649 -> 15852567477617018112 7665081442761407167 -> 8395254103899656747 1510562666313522433 -> 9137153746257222586 16253369998080909938 -> 4321243615010347791 15976513930905472129 -> 2464381025353933752 3846751227738950664 -> 4335032133758631355 15450124388103584652 -> 3418642895666580588 2225665425051551577 -> 12043850726481513634 15615312821753524918 -> 15778603109418817912 11958084298410326056 -> 7422979898823356625 7978187604899367877 -> 8082887755714141501 876738446875159256 -> 18272796975887582060 3498362567022607364 -> 4620349392656757831 7053403205296459880 -> 11062470475981718812 11858356529053237 -> 11681022048666484124 5137924053122990913 -> 1110410394930231379 1455779883697863925 -> 8353289200898942510 1450972605221627516 -> 5464661787014092283 6748150359618158603 -> 18042314908697358845 17100694157255179539 -> 17913569146247237990 16383805058165184155 -> 14252084119842247317 14458762612387161134 -> 13951402111806527659 7578874640041129857 -> 14984524306684102418 1091721997205765161 -> 4929388494852177725 4972829120680344696 -> 8427843384471180011 11905106519757767589 -> 12312569890916686665 14990642672847258155 -> 7806471840435792838 18051484560037243770 -> 10841176947711978698 7846354589731725875 -> 14141658027994245481 1584652476433904101 -> 15796138674937755121 12788838416146221578 -> 4930499034900633496 859043901073302035 -> 11894749826123257324 5668633829937549152 -> 331885767850924737 7252967951263295973 -> 6209022505833017115 13226339972060618816 -> 13004202639896233962 1981332788226950674 -> 1029059189446661931 17570152268719247518 -> 9179914352160302837 808203791867106338 -> 9236479431486641300 8732491230892848194 -> 5209128789544115679 960688443443831230 -> 5582126543454279956 8967860587877234272 -> 14370442711223214011 12765059320032069741 -> 16435999169585034112 3138802922902822053 -> 2858404675926398753 6653748162035956560 -> 2863207613356019619 12107272552955766436 -> 4197841241754323017 11629978161529728893 -> 1651019462700250297 12532033173457401429 -> 9342015881796079139 8444087394523764362 -> 971242319250035563 13899343461558324542 -> 6367780246848246992 8444087394523764362 -> 971242319250035563 14079671624423886806 -> 6931684686417541656 8444087394523764362 -> 971242319250035563 test_adapter_continuation_view k_stream_aborted (35 of 35): 16510252855932086823 -> 16458853630937199693 14048324983519873828 -> 10534014544629119321 17804079663198632649 -> 15852567477617018112 7665081442761407167 -> 8395254103899656747 1510562666313522433 -> 9137153746257222586 16253369998080909938 -> 4321243615010347791 15976513930905472129 -> 2464381025353933752 3846751227738950664 -> 4335032133758631355 15450124388103584652 -> 3418642895666580588 2225665425051551577 -> 12043850726481513634 15615312821753524918 -> 15778603109418817912 11958084298410326056 -> 7422979898823356625 7978187604899367877 -> 8082887755714141501 876738446875159256 -> 18272796975887582060 3498362567022607364 -> 4620349392656757831 7053403205296459880 -> 11062470475981718812 11858356529053237 -> 11681022048666484124 5137924053122990913 -> 1110410394930231379 1455779883697863925 -> 8353289200898942510 1450972605221627516 -> 5464661787014092283 6748150359618158603 -> 18042314908697358845 17100694157255179539 -> 17913569146247237990 16383805058165184155 -> 14252084119842247317 14458762612387161134 -> 13951402111806527659 7578874640041129857 -> 14984524306684102418 1091721997205765161 -> 4929388494852177725 4972829120680344696 -> 8427843384471180011 11905106519757767589 -> 12312569890916686665 14990642672847258155 -> 7806471840435792838 18051484560037243770 -> 10841176947711978698 7846354589731725875 -> 14141658027994245481 1584652476433904101 -> 15796138674937755121 12788838416146221578 -> 4930499034900633496 6600350751158217016 -> 6596242839819495123 12788838416146221578 -> 4930499034900633496 test_adapter_continuation_view k_aborted (6 of 6): 14621553252499744072 -> 5575980828577033360 1560069015253079270 -> 17778912298194596574 8181232920474966978 -> 16463834114134690168 12415576349368044635 -> 12754404695042992156 14564863596030526264 -> 8825354032181240290 12415576349368044635 -> 12754404695042992156 test_adapter_continuation_view k_reused (15 of 15): 14621553252499744072 -> 5575980828577033360 1560069015253079270 -> 17778912298194596574 8181232920474966978 -> 16463834114134690168 8150352125352055815 -> 8941018855024971093 14621553252499744072 -> 5575980828577033360 1560069015253079270 -> 17778912298194596574 8181232920474966978 -> 16463834114134690168 8150352125352055815 -> 8941018855024971093 4370795498617211977 -> 2898399952537902865 10361542891827113526 -> 8964175343669296125 4370795498617211977 -> 2898399952537902865 1258618990550900905 -> 4063572268643314481 3498362567022607364 -> 4620349392656757831 9810780513184341561 -> 7040750059417794475 3498362567022607364 -> 4620349392656757831 test_adapter_continuation_view k_refused (11 of 11): 10361542891827113526 -> 8964175343669296125 4370795498617211977 -> 2898399952537902865 10361542891827113526 -> 8964175343669296125 4370795498617211977 -> 2898399952537902865 14621553252499744072 -> 5575980828577033360 1560069015253079270 -> 17778912298194596574 8181232920474966978 -> 16463834114134690168 8150352125352055815 -> 8941018855024971093 4370795498617211977 -> 2898399952537902865 10361542891827113526 -> 8964175343669296125 4370795498617211977 -> 2898399952537902865 test_adapter_host_view_memo (13 of 13 runs, broker-state hash): batch/A: 13831511007419341774 -> 496593797863776142 batch/B: 16942810041893704776 -> 5316201723780989919 stream/A: 3001610611355826173 -> 4351362905685696693 stream/B: 8241673901617223988 -> 1033336853289890500 interleaved/A: 3001610611355826173 -> 4351362905685696693 interleaved/B: 8241673901617223988 -> 1033336853289890500 round_robin/A1: 3001610611355826173 -> 4351362905685696693 round_robin/B: 8241673901617223988 -> 1033336853289890500 round_robin/A2: 3001610611355826173 -> 4351362905685696693 rebuilt/0: 13831511007419341774 -> 496593797863776142 rebuilt/1: 16942810041893704776 -> 5316201723780989919 rebuilt/2: 13831511007419341774 -> 496593797863776142 rebuilt/3: 16942810041893704776 -> 5316201723780989919 test_native_calendar_hash_witness ny_daily_over_hourly digest: 0xfa85a7f9a92aea39 -> 0xe4365dfe9a898fbd Non-hash fields: none moved -- host_view_memo keeps every run's trades, trade FNV, net profit and error; the calendar row keeps KERNEL-EDGE's 13 calculated bars, 13 opens, 300 inputs, 52 driver rows, 14 other rows and 2 trades; the continuation-view tables are continuation values only. Pine witness (ReissueReplay x64 on btcusdt_15m_7d, spark GCC 13.3, normal and stress builds): main 1a0e7ea1 10045334278681590956 -> INT25 14010942914806881059 (43008 callbacks, 43008 bars, position 1 on both); V19-FIX alone gave 9305091844641472748 and K-IDX alone 10808029678210119875. Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/test_adapter_continuation_view.cpp | 689 +++++++++++++------- tests/test_adapter_host_view_memo_data.hpp | 41 +- tests/test_native_calendar_hash_witness.cpp | 8 +- 3 files changed, 498 insertions(+), 240 deletions(-) diff --git a/tests/test_adapter_continuation_view.cpp b/tests/test_adapter_continuation_view.cpp index 915ab72d..b6c74606 100644 --- a/tests/test_adapter_continuation_view.cpp +++ b/tests/test_adapter_continuation_view.cpp @@ -338,14 +338,22 @@ void emit(const char* name, const std::vector& reads) { // 13208968445305919179ull -> 4370795498617211977ull // 10324478866479362969ull -> 10361542891827113526ull // 13208968445305919179ull -> 4370795498617211977ull +// expectation corrected (INT25, k_chart, 7 of 7 values), because the integrated tree folds V19-FIX's Pine hash inputs and K-IDX's script-bar coordinate together (each lane re-pinned on a tree without the other); harvested once on the INT25 tree with this TU's harvest switch: +// 14621553252499744072ull -> 5575980828577033360ull +// 1560069015253079270ull -> 17778912298194596574ull +// 8181232920474966978ull -> 16463834114134690168ull +// 8150352125352055815ull -> 8941018855024971093ull +// 4370795498617211977ull -> 2898399952537902865ull +// 10361542891827113526ull -> 8964175343669296125ull +// 4370795498617211977ull -> 2898399952537902865ull constexpr std::uint64_t k_chart[] = { - 14621553252499744072ull, - 1560069015253079270ull, - 8181232920474966978ull, - 8150352125352055815ull, - 4370795498617211977ull, - 10361542891827113526ull, - 4370795498617211977ull, + 5575980828577033360ull, + 17778912298194596574ull, + 16463834114134690168ull, + 8941018855024971093ull, + 2898399952537902865ull, + 8964175343669296125ull, + 2898399952537902865ull, }; // 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 @@ -375,14 +383,22 @@ constexpr std::uint64_t k_chart[] = { // 4411147221463257853ull -> 15984354419168920510ull // 11201307460949430058ull -> 16607038826027722339ull // 4411147221463257853ull -> 15984354419168920510ull +// expectation corrected (INT25, k_magnifier, 7 of 7 values), because the integrated tree folds V19-FIX's Pine hash inputs and K-IDX's script-bar coordinate together (each lane re-pinned on a tree without the other); harvested once on the INT25 tree with this TU's harvest switch: +// 1310098789583311035ull -> 4430826578646987126ull +// 9252967425603149690ull -> 14738067743722080804ull +// 9717019523752667192ull -> 2077753189921542836ull +// 14284557384012157011ull -> 2917741806179085516ull +// 15984354419168920510ull -> 13581436790443683042ull +// 16607038826027722339ull -> 14737345821341801219ull +// 15984354419168920510ull -> 13581436790443683042ull constexpr std::uint64_t k_magnifier[] = { - 1310098789583311035ull, - 9252967425603149690ull, - 9717019523752667192ull, - 14284557384012157011ull, - 15984354419168920510ull, - 16607038826027722339ull, - 15984354419168920510ull, + 4430826578646987126ull, + 14738067743722080804ull, + 2077753189921542836ull, + 2917741806179085516ull, + 13581436790443683042ull, + 14737345821341801219ull, + 13581436790443683042ull, }; // 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 @@ -399,12 +415,18 @@ constexpr std::uint64_t k_magnifier[] = { // 5992953984750558008ull -> 4995545673543620333ull // 5456021430637490595ull -> 9893483468656556008ull // 5992953984750558008ull -> 4995545673543620333ull +// expectation corrected (INT25, k_aggregated, 5 of 5 values), because the integrated tree folds V19-FIX's Pine hash inputs and K-IDX's script-bar coordinate together (each lane re-pinned on a tree without the other); harvested once on the INT25 tree with this TU's harvest switch: +// 7408940017323371281ull -> 16542564830293500828ull +// 11387000028137044299ull -> 13031512803108236715ull +// 4995545673543620333ull -> 6906214955679447270ull +// 9893483468656556008ull -> 10543459672655655434ull +// 4995545673543620333ull -> 6906214955679447270ull constexpr std::uint64_t k_aggregated[] = { - 7408940017323371281ull, - 11387000028137044299ull, - 4995545673543620333ull, - 9893483468656556008ull, - 4995545673543620333ull, + 16542564830293500828ull, + 13031512803108236715ull, + 6906214955679447270ull, + 10543459672655655434ull, + 6906214955679447270ull, }; // 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 @@ -421,12 +443,18 @@ constexpr std::uint64_t k_aggregated[] = { // 18112393076487589516ull -> 6106527879897255797ull // 13595293692156698476ull -> 14842205570428090150ull // 18112393076487589516ull -> 6106527879897255797ull +// expectation corrected (INT25, k_aggregated_magnifier, 5 of 5 values), because the integrated tree folds V19-FIX's Pine hash inputs and K-IDX's script-bar coordinate together (each lane re-pinned on a tree without the other); harvested once on the INT25 tree with this TU's harvest switch: +// 2376229218543853263ull -> 14031106247497767919ull +// 301774797075971540ull -> 10607093485200722581ull +// 6106527879897255797ull -> 22607172627576334ull +// 14842205570428090150ull -> 12265832737220737843ull +// 6106527879897255797ull -> 22607172627576334ull constexpr std::uint64_t k_aggregated_magnifier[] = { - 2376229218543853263ull, - 301774797075971540ull, - 6106527879897255797ull, - 14842205570428090150ull, - 6106527879897255797ull, + 14031106247497767919ull, + 10607093485200722581ull, + 22607172627576334ull, + 12265832737220737843ull, + 22607172627576334ull, }; // 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 @@ -464,17 +492,28 @@ constexpr std::uint64_t k_aggregated_magnifier[] = { // 5365537423784642334ull -> 10916717879615654635ull // 17202960931486753898ull -> 17916071608255404005ull // 5365537423784642334ull -> 10916717879615654635ull +// expectation corrected (INT25, k_coof, 10 of 10 values), because the integrated tree folds V19-FIX's Pine hash inputs and K-IDX's script-bar coordinate together (each lane re-pinned on a tree without the other); harvested once on the INT25 tree with this TU's harvest switch: +// 10706142720152936082ull -> 16820608513549156800ull +// 14042469651802437968ull -> 9462991605602344175ull +// 1992762524201176782ull -> 12220673217690837314ull +// 9972018108613272455ull -> 1856715264914641232ull +// 9011525226083087013ull -> 12811765853164186626ull +// 7755254022538140640ull -> 9115044940243703863ull +// 16056359684121154720ull -> 9198850018143016781ull +// 10916717879615654635ull -> 784137025905909089ull +// 17916071608255404005ull -> 11981249830754474578ull +// 10916717879615654635ull -> 784137025905909089ull constexpr std::uint64_t k_coof[] = { - 10706142720152936082ull, - 14042469651802437968ull, - 1992762524201176782ull, - 9972018108613272455ull, - 9011525226083087013ull, - 7755254022538140640ull, - 16056359684121154720ull, - 10916717879615654635ull, - 17916071608255404005ull, - 10916717879615654635ull, + 16820608513549156800ull, + 9462991605602344175ull, + 12220673217690837314ull, + 1856715264914641232ull, + 12811765853164186626ull, + 9115044940243703863ull, + 9198850018143016781ull, + 784137025905909089ull, + 11981249830754474578ull, + 784137025905909089ull, }; // 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 @@ -742,78 +781,150 @@ constexpr std::uint64_t k_coof[] = { // 11989522059105970117ull -> 5548300568428407565ull // 14967623866548664000ull -> 4191735526950747220ull // 13208968445305919179ull -> 4370795498617211977ull +// expectation corrected (INT25, k_recording, 71 of 72 values), because the integrated tree folds V19-FIX's Pine hash inputs and K-IDX's script-bar coordinate together (each lane re-pinned on a tree without the other); harvested once on the INT25 tree with this TU's harvest switch: +// 14621553252499744072ull -> 5575980828577033360ull +// 16947653718640119089ull -> 16309388099732889217ull +// 8181232920474966978ull -> 16463834114134690168ull +// 384032772146433484ull -> 14697449118452563264ull +// 4370795498617211977ull -> 2898399952537902865ull +// 10361542891827113526ull -> 8964175343669296125ull +// 4370795498617211977ull -> 2898399952537902865ull +// 11635388043626139808ull -> 11912929120179152844ull +// 5137642655964282706ull -> 7111499071038382551ull +// 1067158101705749258ull -> 14395892391241930212ull +// 17589216349291364682ull -> 9034635987304089269ull +// 13103826456263923669ull -> 7745035282290493167ull +// 14125475791418217750ull -> 9653978480792136139ull +// 5496734832958426517ull -> 6575784925310671623ull +// 3300864403891436803ull -> 16756306850270906652ull +// 14334667028075329258ull -> 6791844355462743637ull +// 19027534220486873ull -> 9752175874850418240ull +// 17400684420983527587ull -> 5581794927458177019ull +// 16843748232757858737ull -> 16367531041072987671ull +// 5910420545552211065ull -> 5499206076623092635ull +// 4037494890945270795ull -> 16891823664600062531ull +// 13685270228904493022ull -> 15306266440153708710ull +// 12610051923577992823ull -> 8382791724770590634ull +// 13419685112760992640ull -> 4926336483410460532ull +// 6561776234336247499ull -> 12823090459845782834ull +// 11286535916465691925ull -> 10217177223946990874ull +// 9286401377316929631ull -> 12680312482263936655ull +// 11941619648187175172ull -> 15032085204869760060ull +// 2781110012097249044ull -> 7696336452231268012ull +// 5152088055254729287ull -> 6819477884530924764ull +// 14925422330047382771ull -> 13932522791731114426ull +// 18068979986855895194ull -> 15842817164955438950ull +// 3293728938989802792ull -> 4250764768037495411ull +// 3040154494312885091ull -> 1760469320874157114ull +// 3914527074597397115ull -> 7565330933885892856ull +// 6028800054409062637ull -> 3348650657119335616ull +// 17539544361470737737ull -> 16026642042438818681ull +// 17497902664474484401ull -> 15336121108426360137ull +// 16821147754446235025ull -> 12610087835245307199ull +// 3501965129295230921ull -> 3169655936610324835ull +// 17136543747664596622ull -> 18005345939863936504ull +// 6796102544471338071ull -> 10374887129263366256ull +// 10258488113034267824ull -> 3316605285128650350ull +// 15437545425183691961ull -> 10411230090089985874ull +// 10208872586329130020ull -> 2321647345879300563ull +// 14368668855499836453ull -> 9282855342318040804ull +// 628171839165946395ull -> 8782939554936041465ull +// 10702221356985455012ull -> 9270809468617755726ull +// 2314157047087909764ull -> 9705564861311910669ull +// 12920471494293590838ull -> 1984027035687621850ull +// 8752737450779041921ull -> 5309816640440296119ull +// 7994102271719463356ull -> 61906512444861614ull +// 4741946402210887143ull -> 18408253739502061131ull +// 739366574823717504ull -> 2900636347809437670ull +// 3655914840365203865ull -> 8590510826456084479ull +// 7448010022416009296ull -> 16739036358771975930ull +// 11266041923012349372ull -> 3846505090147632664ull +// 16061447354858643806ull -> 13346518438036570112ull +// 6641998937765131086ull -> 3939181335635768762ull +// 15745847663605277053ull -> 1996675722330704212ull +// 2232785799096296677ull -> 271113896427528122ull +// 12057790076704791676ull -> 1511893874670543988ull +// 14126847163984952264ull -> 15973548105797383183ull +// 6703125855558665936ull -> 4392899708297163730ull +// 750370809058189391ull -> 11467883812317724901ull +// 727242285392145435ull -> 11051413718335140252ull +// 9029503590661437192ull -> 903533060614176790ull +// 6205861443650071106ull -> 14466209299323094098ull +// 5548300568428407565ull -> 4010960734010157804ull +// 4191735526950747220ull -> 5172512134487820157ull +// 4370795498617211977ull -> 2898399952537902865ull constexpr std::uint64_t k_recording[] = { - 14621553252499744072ull, - 16947653718640119089ull, - 8181232920474966978ull, - 384032772146433484ull, - 4370795498617211977ull, - 10361542891827113526ull, - 4370795498617211977ull, - 11635388043626139808ull, - 5137642655964282706ull, - 1067158101705749258ull, - 17589216349291364682ull, - 13103826456263923669ull, - 14125475791418217750ull, - 5496734832958426517ull, - 3300864403891436803ull, - 14334667028075329258ull, - 19027534220486873ull, - 17400684420983527587ull, - 16843748232757858737ull, - 5910420545552211065ull, - 4037494890945270795ull, - 13685270228904493022ull, - 12610051923577992823ull, - 13419685112760992640ull, - 6561776234336247499ull, - 11286535916465691925ull, - 9286401377316929631ull, - 11941619648187175172ull, - 2781110012097249044ull, - 5152088055254729287ull, - 14925422330047382771ull, - 18068979986855895194ull, - 3293728938989802792ull, - 3040154494312885091ull, - 3914527074597397115ull, - 6028800054409062637ull, - 17539544361470737737ull, - 17497902664474484401ull, - 16821147754446235025ull, - 3501965129295230921ull, - 17136543747664596622ull, - 6796102544471338071ull, - 10258488113034267824ull, - 15437545425183691961ull, - 10208872586329130020ull, - 14368668855499836453ull, - 628171839165946395ull, - 10702221356985455012ull, - 2314157047087909764ull, - 12920471494293590838ull, - 8752737450779041921ull, - 7994102271719463356ull, - 4741946402210887143ull, - 739366574823717504ull, - 3655914840365203865ull, - 7448010022416009296ull, - 11266041923012349372ull, - 16061447354858643806ull, - 6641998937765131086ull, - 15745847663605277053ull, - 2232785799096296677ull, - 12057790076704791676ull, - 14126847163984952264ull, - 6703125855558665936ull, - 750370809058189391ull, - 727242285392145435ull, - 9029503590661437192ull, - 6205861443650071106ull, - 5548300568428407565ull, - 4191735526950747220ull, - 4370795498617211977ull, + 5575980828577033360ull, + 16309388099732889217ull, + 16463834114134690168ull, + 14697449118452563264ull, + 2898399952537902865ull, + 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, 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): @@ -1021,61 +1132,116 @@ constexpr std::uint64_t k_recording[] = { // 10443374725851587239ull -> 8444087394523764362ull // 11033218740417797382ull -> 14079671624423886806ull // 10443374725851587239ull -> 8444087394523764362ull +// expectation corrected (INT25, k_stream, 54 of 54 values), because the integrated tree folds V19-FIX's Pine hash inputs and K-IDX's script-bar coordinate together (each lane re-pinned on a tree without the other); harvested once on the INT25 tree with this TU's harvest switch: +// 16510252855932086823ull -> 16458853630937199693ull +// 14048324983519873828ull -> 10534014544629119321ull +// 17804079663198632649ull -> 15852567477617018112ull +// 7665081442761407167ull -> 8395254103899656747ull +// 1510562666313522433ull -> 9137153746257222586ull +// 16253369998080909938ull -> 4321243615010347791ull +// 15976513930905472129ull -> 2464381025353933752ull +// 3846751227738950664ull -> 4335032133758631355ull +// 15450124388103584652ull -> 3418642895666580588ull +// 2225665425051551577ull -> 12043850726481513634ull +// 15615312821753524918ull -> 15778603109418817912ull +// 11958084298410326056ull -> 7422979898823356625ull +// 7978187604899367877ull -> 8082887755714141501ull +// 876738446875159256ull -> 18272796975887582060ull +// 3498362567022607364ull -> 4620349392656757831ull +// 7053403205296459880ull -> 11062470475981718812ull +// 11858356529053237ull -> 11681022048666484124ull +// 5137924053122990913ull -> 1110410394930231379ull +// 1455779883697863925ull -> 8353289200898942510ull +// 1450972605221627516ull -> 5464661787014092283ull +// 6748150359618158603ull -> 18042314908697358845ull +// 17100694157255179539ull -> 17913569146247237990ull +// 16383805058165184155ull -> 14252084119842247317ull +// 14458762612387161134ull -> 13951402111806527659ull +// 7578874640041129857ull -> 14984524306684102418ull +// 1091721997205765161ull -> 4929388494852177725ull +// 4972829120680344696ull -> 8427843384471180011ull +// 11905106519757767589ull -> 12312569890916686665ull +// 14990642672847258155ull -> 7806471840435792838ull +// 18051484560037243770ull -> 10841176947711978698ull +// 7846354589731725875ull -> 14141658027994245481ull +// 1584652476433904101ull -> 15796138674937755121ull +// 12788838416146221578ull -> 4930499034900633496ull +// 859043901073302035ull -> 11894749826123257324ull +// 5668633829937549152ull -> 331885767850924737ull +// 7252967951263295973ull -> 6209022505833017115ull +// 13226339972060618816ull -> 13004202639896233962ull +// 1981332788226950674ull -> 1029059189446661931ull +// 17570152268719247518ull -> 9179914352160302837ull +// 808203791867106338ull -> 9236479431486641300ull +// 8732491230892848194ull -> 5209128789544115679ull +// 960688443443831230ull -> 5582126543454279956ull +// 8967860587877234272ull -> 14370442711223214011ull +// 12765059320032069741ull -> 16435999169585034112ull +// 3138802922902822053ull -> 2858404675926398753ull +// 6653748162035956560ull -> 2863207613356019619ull +// 12107272552955766436ull -> 4197841241754323017ull +// 11629978161529728893ull -> 1651019462700250297ull +// 12532033173457401429ull -> 9342015881796079139ull +// 8444087394523764362ull -> 971242319250035563ull +// 13899343461558324542ull -> 6367780246848246992ull +// 8444087394523764362ull -> 971242319250035563ull +// 14079671624423886806ull -> 6931684686417541656ull +// 8444087394523764362ull -> 971242319250035563ull constexpr std::uint64_t k_stream[] = { - 16510252855932086823ull, - 14048324983519873828ull, - 17804079663198632649ull, - 7665081442761407167ull, - 1510562666313522433ull, - 16253369998080909938ull, - 15976513930905472129ull, - 3846751227738950664ull, - 15450124388103584652ull, - 2225665425051551577ull, - 15615312821753524918ull, - 11958084298410326056ull, - 7978187604899367877ull, - 876738446875159256ull, - 3498362567022607364ull, - 7053403205296459880ull, - 11858356529053237ull, - 5137924053122990913ull, - 1455779883697863925ull, - 1450972605221627516ull, - 6748150359618158603ull, - 17100694157255179539ull, - 16383805058165184155ull, - 14458762612387161134ull, - 7578874640041129857ull, - 1091721997205765161ull, - 4972829120680344696ull, - 11905106519757767589ull, - 14990642672847258155ull, - 18051484560037243770ull, - 7846354589731725875ull, - 1584652476433904101ull, - 12788838416146221578ull, - 859043901073302035ull, - 5668633829937549152ull, - 7252967951263295973ull, - 13226339972060618816ull, - 1981332788226950674ull, - 17570152268719247518ull, - 808203791867106338ull, - 8732491230892848194ull, - 960688443443831230ull, - 8967860587877234272ull, - 12765059320032069741ull, - 3138802922902822053ull, - 6653748162035956560ull, - 12107272552955766436ull, - 11629978161529728893ull, - 12532033173457401429ull, - 8444087394523764362ull, - 13899343461558324542ull, - 8444087394523764362ull, - 14079671624423886806ull, - 8444087394523764362ull, + 16458853630937199693ull, + 10534014544629119321ull, + 15852567477617018112ull, + 8395254103899656747ull, + 9137153746257222586ull, + 4321243615010347791ull, + 2464381025353933752ull, + 4335032133758631355ull, + 3418642895666580588ull, + 12043850726481513634ull, + 15778603109418817912ull, + 7422979898823356625ull, + 8082887755714141501ull, + 18272796975887582060ull, + 4620349392656757831ull, + 11062470475981718812ull, + 11681022048666484124ull, + 1110410394930231379ull, + 8353289200898942510ull, + 5464661787014092283ull, + 18042314908697358845ull, + 17913569146247237990ull, + 14252084119842247317ull, + 13951402111806527659ull, + 14984524306684102418ull, + 4929388494852177725ull, + 8427843384471180011ull, + 12312569890916686665ull, + 7806471840435792838ull, + 10841176947711978698ull, + 14141658027994245481ull, + 15796138674937755121ull, + 4930499034900633496ull, + 11894749826123257324ull, + 331885767850924737ull, + 6209022505833017115ull, + 13004202639896233962ull, + 1029059189446661931ull, + 9179914352160302837ull, + 9236479431486641300ull, + 5209128789544115679ull, + 5582126543454279956ull, + 14370442711223214011ull, + 16435999169585034112ull, + 2858404675926398753ull, + 2863207613356019619ull, + 4197841241754323017ull, + 1651019462700250297ull, + 9342015881796079139ull, + 971242319250035563ull, + 6367780246848246992ull, + 971242319250035563ull, + 6931684686417541656ull, + 971242319250035563ull, }; // 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 @@ -1211,42 +1377,78 @@ constexpr std::uint64_t k_stream[] = { // 7431106375660941195ull -> 12788838416146221578ull // 13404512392582016370ull -> 6600350751158217016ull // 7431106375660941195ull -> 12788838416146221578ull +// expectation corrected (INT25, k_stream_aborted, 35 of 35 values), because the integrated tree folds V19-FIX's Pine hash inputs and K-IDX's script-bar coordinate together (each lane re-pinned on a tree without the other); harvested once on the INT25 tree with this TU's harvest switch: +// 16510252855932086823ull -> 16458853630937199693ull +// 14048324983519873828ull -> 10534014544629119321ull +// 17804079663198632649ull -> 15852567477617018112ull +// 7665081442761407167ull -> 8395254103899656747ull +// 1510562666313522433ull -> 9137153746257222586ull +// 16253369998080909938ull -> 4321243615010347791ull +// 15976513930905472129ull -> 2464381025353933752ull +// 3846751227738950664ull -> 4335032133758631355ull +// 15450124388103584652ull -> 3418642895666580588ull +// 2225665425051551577ull -> 12043850726481513634ull +// 15615312821753524918ull -> 15778603109418817912ull +// 11958084298410326056ull -> 7422979898823356625ull +// 7978187604899367877ull -> 8082887755714141501ull +// 876738446875159256ull -> 18272796975887582060ull +// 3498362567022607364ull -> 4620349392656757831ull +// 7053403205296459880ull -> 11062470475981718812ull +// 11858356529053237ull -> 11681022048666484124ull +// 5137924053122990913ull -> 1110410394930231379ull +// 1455779883697863925ull -> 8353289200898942510ull +// 1450972605221627516ull -> 5464661787014092283ull +// 6748150359618158603ull -> 18042314908697358845ull +// 17100694157255179539ull -> 17913569146247237990ull +// 16383805058165184155ull -> 14252084119842247317ull +// 14458762612387161134ull -> 13951402111806527659ull +// 7578874640041129857ull -> 14984524306684102418ull +// 1091721997205765161ull -> 4929388494852177725ull +// 4972829120680344696ull -> 8427843384471180011ull +// 11905106519757767589ull -> 12312569890916686665ull +// 14990642672847258155ull -> 7806471840435792838ull +// 18051484560037243770ull -> 10841176947711978698ull +// 7846354589731725875ull -> 14141658027994245481ull +// 1584652476433904101ull -> 15796138674937755121ull +// 12788838416146221578ull -> 4930499034900633496ull +// 6600350751158217016ull -> 6596242839819495123ull +// 12788838416146221578ull -> 4930499034900633496ull constexpr std::uint64_t k_stream_aborted[] = { - 16510252855932086823ull, - 14048324983519873828ull, - 17804079663198632649ull, - 7665081442761407167ull, - 1510562666313522433ull, - 16253369998080909938ull, - 15976513930905472129ull, - 3846751227738950664ull, - 15450124388103584652ull, - 2225665425051551577ull, - 15615312821753524918ull, - 11958084298410326056ull, - 7978187604899367877ull, - 876738446875159256ull, - 3498362567022607364ull, - 7053403205296459880ull, - 11858356529053237ull, - 5137924053122990913ull, - 1455779883697863925ull, - 1450972605221627516ull, - 6748150359618158603ull, - 17100694157255179539ull, - 16383805058165184155ull, - 14458762612387161134ull, - 7578874640041129857ull, - 1091721997205765161ull, - 4972829120680344696ull, - 11905106519757767589ull, - 14990642672847258155ull, - 18051484560037243770ull, - 7846354589731725875ull, - 1584652476433904101ull, - 12788838416146221578ull, - 6600350751158217016ull, - 12788838416146221578ull, + 16458853630937199693ull, + 10534014544629119321ull, + 15852567477617018112ull, + 8395254103899656747ull, + 9137153746257222586ull, + 4321243615010347791ull, + 2464381025353933752ull, + 4335032133758631355ull, + 3418642895666580588ull, + 12043850726481513634ull, + 15778603109418817912ull, + 7422979898823356625ull, + 8082887755714141501ull, + 18272796975887582060ull, + 4620349392656757831ull, + 11062470475981718812ull, + 11681022048666484124ull, + 1110410394930231379ull, + 8353289200898942510ull, + 5464661787014092283ull, + 18042314908697358845ull, + 17913569146247237990ull, + 14252084119842247317ull, + 13951402111806527659ull, + 14984524306684102418ull, + 4929388494852177725ull, + 8427843384471180011ull, + 12312569890916686665ull, + 7806471840435792838ull, + 10841176947711978698ull, + 14141658027994245481ull, + 15796138674937755121ull, + 4930499034900633496ull, + 6596242839819495123ull, + 4930499034900633496ull, }; // 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 @@ -1272,13 +1474,20 @@ constexpr std::uint64_t k_stream_aborted[] = { // 7856356957291164459ull -> 12415576349368044635ull // 4177885075058309128ull -> 14564863596030526264ull // 7856356957291164459ull -> 12415576349368044635ull +// expectation corrected (INT25, k_aborted, 6 of 6 values), because the integrated tree folds V19-FIX's Pine hash inputs and K-IDX's script-bar coordinate together (each lane re-pinned on a tree without the other); harvested once on the INT25 tree with this TU's harvest switch: +// 14621553252499744072ull -> 5575980828577033360ull +// 1560069015253079270ull -> 17778912298194596574ull +// 8181232920474966978ull -> 16463834114134690168ull +// 12415576349368044635ull -> 12754404695042992156ull +// 14564863596030526264ull -> 8825354032181240290ull +// 12415576349368044635ull -> 12754404695042992156ull constexpr std::uint64_t k_aborted[] = { - 14621553252499744072ull, - 1560069015253079270ull, - 8181232920474966978ull, - 12415576349368044635ull, - 14564863596030526264ull, - 12415576349368044635ull, + 5575980828577033360ull, + 17778912298194596574ull, + 16463834114134690168ull, + 12754404695042992156ull, + 8825354032181240290ull, + 12754404695042992156ull, }; // 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 @@ -1336,22 +1545,38 @@ constexpr std::uint64_t k_aborted[] = { // 17064616757066975641ull -> 3498362567022607364ull // 15316717102556938983ull -> 9810780513184341561ull // 17064616757066975641ull -> 3498362567022607364ull +// expectation corrected (INT25, k_reused, 15 of 15 values), because the integrated tree folds V19-FIX's Pine hash inputs and K-IDX's script-bar coordinate together (each lane re-pinned on a tree without the other); harvested once on the INT25 tree with this TU's harvest switch: +// 14621553252499744072ull -> 5575980828577033360ull +// 1560069015253079270ull -> 17778912298194596574ull +// 8181232920474966978ull -> 16463834114134690168ull +// 8150352125352055815ull -> 8941018855024971093ull +// 14621553252499744072ull -> 5575980828577033360ull +// 1560069015253079270ull -> 17778912298194596574ull +// 8181232920474966978ull -> 16463834114134690168ull +// 8150352125352055815ull -> 8941018855024971093ull +// 4370795498617211977ull -> 2898399952537902865ull +// 10361542891827113526ull -> 8964175343669296125ull +// 4370795498617211977ull -> 2898399952537902865ull +// 1258618990550900905ull -> 4063572268643314481ull +// 3498362567022607364ull -> 4620349392656757831ull +// 9810780513184341561ull -> 7040750059417794475ull +// 3498362567022607364ull -> 4620349392656757831ull constexpr std::uint64_t k_reused[] = { - 14621553252499744072ull, - 1560069015253079270ull, - 8181232920474966978ull, - 8150352125352055815ull, - 14621553252499744072ull, - 1560069015253079270ull, - 8181232920474966978ull, - 8150352125352055815ull, - 4370795498617211977ull, - 10361542891827113526ull, - 4370795498617211977ull, - 1258618990550900905ull, - 3498362567022607364ull, - 9810780513184341561ull, - 3498362567022607364ull, + 5575980828577033360ull, + 17778912298194596574ull, + 16463834114134690168ull, + 8941018855024971093ull, + 5575980828577033360ull, + 17778912298194596574ull, + 16463834114134690168ull, + 8941018855024971093ull, + 2898399952537902865ull, + 8964175343669296125ull, + 2898399952537902865ull, + 4063572268643314481ull, + 4620349392656757831ull, + 7040750059417794475ull, + 4620349392656757831ull, }; // 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 @@ -1395,18 +1620,30 @@ constexpr std::uint64_t k_reused[] = { // 13208968445305919179ull -> 4370795498617211977ull // 10324478866479362969ull -> 10361542891827113526ull // 13208968445305919179ull -> 4370795498617211977ull +// expectation corrected (INT25, k_refused, 11 of 11 values), because the integrated tree folds V19-FIX's Pine hash inputs and K-IDX's script-bar coordinate together (each lane re-pinned on a tree without the other); harvested once on the INT25 tree with this TU's harvest switch: +// 10361542891827113526ull -> 8964175343669296125ull +// 4370795498617211977ull -> 2898399952537902865ull +// 10361542891827113526ull -> 8964175343669296125ull +// 4370795498617211977ull -> 2898399952537902865ull +// 14621553252499744072ull -> 5575980828577033360ull +// 1560069015253079270ull -> 17778912298194596574ull +// 8181232920474966978ull -> 16463834114134690168ull +// 8150352125352055815ull -> 8941018855024971093ull +// 4370795498617211977ull -> 2898399952537902865ull +// 10361542891827113526ull -> 8964175343669296125ull +// 4370795498617211977ull -> 2898399952537902865ull constexpr std::uint64_t k_refused[] = { - 10361542891827113526ull, - 4370795498617211977ull, - 10361542891827113526ull, - 4370795498617211977ull, - 14621553252499744072ull, - 1560069015253079270ull, - 8181232920474966978ull, - 8150352125352055815ull, - 4370795498617211977ull, - 10361542891827113526ull, - 4370795498617211977ull, + 8964175343669296125ull, + 2898399952537902865ull, + 8964175343669296125ull, + 2898399952537902865ull, + 5575980828577033360ull, + 17778912298194596574ull, + 16463834114134690168ull, + 8941018855024971093ull, + 2898399952537902865ull, + 8964175343669296125ull, + 2898399952537902865ull, }; // 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 fbde5705..b7db1bf8 100644 --- a/tests/test_adapter_host_view_memo_data.hpp +++ b/tests/test_adapter_host_view_memo_data.hpp @@ -90,31 +90,46 @@ // rebuilt/1: hash=17469022339767313045 -> hash=16942810041893704776 // rebuilt/2: hash=11831117278087605757 -> hash=13831511007419341774 // rebuilt/3: hash=17469022339767313045 -> hash=16942810041893704776 +// expectation corrected (INT25): every run's broker-state hash moved once more, because the integrated tree folds V19-FIX's Pine hash inputs and K-IDX's script-bar coordinate together (each lane re-pinned on a tree without the other); harvested once on the INT25 tree with this TU's harvest switch; +// trades, trade digests, net profits and errors did not move: +// batch/A: hash=13831511007419341774 -> hash=496593797863776142 +// batch/B: hash=16942810041893704776 -> hash=5316201723780989919 +// stream/A: hash=3001610611355826173 -> hash=4351362905685696693 +// stream/B: hash=8241673901617223988 -> hash=1033336853289890500 +// interleaved/A: hash=3001610611355826173 -> hash=4351362905685696693 +// interleaved/B: hash=8241673901617223988 -> hash=1033336853289890500 +// round_robin/A1: hash=3001610611355826173 -> hash=4351362905685696693 +// round_robin/B: hash=8241673901617223988 -> hash=1033336853289890500 +// round_robin/A2: hash=3001610611355826173 -> hash=4351362905685696693 +// rebuilt/0: hash=13831511007419341774 -> hash=496593797863776142 +// rebuilt/1: hash=16942810041893704776 -> hash=5316201723780989919 +// rebuilt/2: hash=13831511007419341774 -> hash=496593797863776142 +// rebuilt/3: hash=16942810041893704776 -> hash=5316201723780989919 constexpr Pinned kPinned[] = { {"batch/A", - "trades=23 fnv=e337022ac3d8b681 net=71.786509342348978 hash=13831511007419341774 error=''"}, + "trades=23 fnv=e337022ac3d8b681 net=71.786509342348978 hash=496593797863776142 error=''"}, {"batch/B", - "trades=35 fnv=9fbd9e232cb9843a net=-268.32270548365625 hash=16942810041893704776 error=''"}, + "trades=35 fnv=9fbd9e232cb9843a net=-268.32270548365625 hash=5316201723780989919 error=''"}, {"stream/A", - "trades=23 fnv=a0e7b82126f9e8de net=17.321792890020646 hash=3001610611355826173 error=''"}, + "trades=23 fnv=a0e7b82126f9e8de net=17.321792890020646 hash=4351362905685696693 error=''"}, {"stream/B", - "trades=35 fnv=3eaaaacd90998d64 net=-306.89379320283734 hash=8241673901617223988 error=''"}, + "trades=35 fnv=3eaaaacd90998d64 net=-306.89379320283734 hash=1033336853289890500 error=''"}, {"interleaved/A", - "trades=23 fnv=a0e7b82126f9e8de net=17.321792890020646 hash=3001610611355826173 error=''"}, + "trades=23 fnv=a0e7b82126f9e8de net=17.321792890020646 hash=4351362905685696693 error=''"}, {"interleaved/B", - "trades=35 fnv=3eaaaacd90998d64 net=-306.89379320283734 hash=8241673901617223988 error=''"}, + "trades=35 fnv=3eaaaacd90998d64 net=-306.89379320283734 hash=1033336853289890500 error=''"}, {"round_robin/A1", - "trades=23 fnv=a0e7b82126f9e8de net=17.321792890020646 hash=3001610611355826173 error=''"}, + "trades=23 fnv=a0e7b82126f9e8de net=17.321792890020646 hash=4351362905685696693 error=''"}, {"round_robin/B", - "trades=35 fnv=3eaaaacd90998d64 net=-306.89379320283734 hash=8241673901617223988 error=''"}, + "trades=35 fnv=3eaaaacd90998d64 net=-306.89379320283734 hash=1033336853289890500 error=''"}, {"round_robin/A2", - "trades=23 fnv=a0e7b82126f9e8de net=17.321792890020646 hash=3001610611355826173 error=''"}, + "trades=23 fnv=a0e7b82126f9e8de net=17.321792890020646 hash=4351362905685696693 error=''"}, {"rebuilt/0", - "trades=23 fnv=e337022ac3d8b681 net=71.786509342348978 hash=13831511007419341774 error=''"}, + "trades=23 fnv=e337022ac3d8b681 net=71.786509342348978 hash=496593797863776142 error=''"}, {"rebuilt/1", - "trades=35 fnv=9fbd9e232cb9843a net=-268.32270548365625 hash=16942810041893704776 error=''"}, + "trades=35 fnv=9fbd9e232cb9843a net=-268.32270548365625 hash=5316201723780989919 error=''"}, {"rebuilt/2", - "trades=23 fnv=e337022ac3d8b681 net=71.786509342348978 hash=13831511007419341774 error=''"}, + "trades=23 fnv=e337022ac3d8b681 net=71.786509342348978 hash=496593797863776142 error=''"}, {"rebuilt/3", - "trades=35 fnv=9fbd9e232cb9843a net=-268.32270548365625 hash=16942810041893704776 error=''"}, + "trades=35 fnv=9fbd9e232cb9843a net=-268.32270548365625 hash=5316201723780989919 error=''"}, }; diff --git a/tests/test_native_calendar_hash_witness.cpp b/tests/test_native_calendar_hash_witness.cpp index 1d4071ae..6c1bc946 100644 --- a/tests/test_native_calendar_hash_witness.cpp +++ b/tests/test_native_calendar_hash_witness.cpp @@ -574,7 +574,13 @@ const Pinned kPinned[] = { {"fixed_offset", 0x484ca66d57dc239dULL, 900, 900, 900, 0, 0, 3600, 136, 19}, // expectation corrected (KERNEL-EDGE, v19): the calendar-closed final // daily bucket is calculated at batch end, including its last trade. - {"ny_daily_over_hourly", 0xfa85a7f9a92aea39ULL, 13, 13, 300, 0, 0, 52, 14, 2}, + // expectation corrected (INT25): digest 0xfa85a7f9a92aea39 -> 0xe4365dfe9a898fbd, + // harvested once on the INT25 tree (PINEFORGE_K1_HARVEST): KERNEL-EDGE's + // final calendar-closed bucket and K-IDX's script-bar coordinate fold meet + // in this scenario, and each lane pinned a tree without the other. The + // calculated bars, opens, inputs, series, prints, driver and other rows + // and trades did not move. + {"ny_daily_over_hourly", 0xe4365dfe9a898fbdULL, 13, 13, 300, 0, 0, 52, 14, 2}, {"ny_hourly_series", 0x284026ee6aa32186ULL, 234, 234, 234, 21, 0, 936, 42, 6}, {"tolerant_aggregating", 0xfa838ffaa5666c82ULL, 200, 200, 600, 0, 0, 800, 63, 9}, {"tolerant_stream_night", 0x8aa785e43e2916d0ULL, 34, 34, 34, 0, 0, 136, 21, 3},