From 100a6057d1d8e69fd01407165132b622e5b4800d Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sun, 18 Oct 2026 19:35:22 +0800 Subject: [PATCH 01/35] test(cr150): add L15a view binding red witness --- tests/CMakeLists.txt | 4 + tests/test_native_bar_views_l15a.cpp | 273 +++++++++++++++++++++++++++ 2 files changed, 277 insertions(+) create mode 100644 tests/test_native_bar_views_l15a.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d11ecee74..018f4bcd3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -7087,3 +7087,7 @@ pineforge_add_unit_test(test_cr150_contract_headers) target_sources(test_cr150_contract_headers PRIVATE test_cr150_contract_headers_c.c) set_target_properties(test_cr150_contract_headers PROPERTIES C_STANDARD 11 C_STANDARD_REQUIRED ON C_EXTENSIONS OFF) + +# CR-150 L15a: append after every existing line-pinned registration. +list(APPEND TEST_SOURCES test_native_bar_views_l15a) +pineforge_add_unit_test(test_native_bar_views_l15a) diff --git a/tests/test_native_bar_views_l15a.cpp b/tests/test_native_bar_views_l15a.cpp new file mode 100644 index 000000000..eb19a2c80 --- /dev/null +++ b/tests/test_native_bar_views_l15a.cpp @@ -0,0 +1,273 @@ +// CR-150 L15a: execute the frozen V1/V2 contract over synthetic data. +// Red on ee3083d4a: the accessor, context overloads and staging members are +// declared but undefined. No compiler or runtime result is claimed locally. +#include "native_instrument_feed_fixture.hpp" +#include "../src/native_execution_consumer.hpp" + +#include +#include + +#include +#include +#include +#include +#include + +namespace { +using namespace instrument_fixture; +namespace views = pineforge::native_bar_views_v1; + +NativeInstrumentFeed tape() { + FeedBuilder builder("SYN:VIEW", "1", 101.0); + for (int i = 0; i < 6; ++i) + builder.add(monday() + i * kMinute, monday() + (i + 1) * kMinute); + builder.feed.columns.push_back({"x", {1, 2, 3, 4, 5, 6}}); + return builder.feed; +} + +NativeInstrumentFeed metadata(const NativeInstrumentFeed& feed) { + NativeInstrumentFeed result = feed; + result.bars.clear(); + result.close_ms.clear(); + return result; +} + +NativeInstrumentFeedView borrowed(const NativeInstrumentFeed& feed) { + return {{feed.bars.data(), feed.bars.size()}, feed.close_ms.data()}; +} + +NativeRunSpec spec_for(const NativeInstrumentFeed& feed) { + auto spec = continuous_spec("l15a-views", "1"); + spec.instrument_feeds.push_back(feed); + spec.subscriptions.push_back(instrument_series(feed)); + return spec; +} + +void effective_data_and_refusals() { + const auto owned = tape(); + const auto lent = metadata(owned); + const auto descriptor = borrowed(owned); + const NativeInstrumentFeedView* slots[] = {&descriptor}; + const NativeBarViews context{slots, 1}; + const auto read = native_instrument_feed_view(lent, &descriptor); + assert(read.bars.data == owned.bars.data()); + assert(read.close_ms == owned.close_ms.data()); + assert(read.bars.size == 6 && read.close_count == 6); + const auto fallback = native_instrument_feed_view(owned, nullptr); + assert(fallback.bars.data == owned.bars.data()); + + const NativeInstrumentFeedView empty{{nullptr, 0}, nullptr}; + assert(native_instrument_feed_view(owned, &empty).bars.size == 0); + auto missing_close = owned; + missing_close.close_ms.pop_back(); + assert(native_instrument_feed_view(missing_close, nullptr).close_count == 5); + assert(views::native_instrument_feeds_digest({missing_close}, {}) + == pineforge::native_run_spec_v3::native_instrument_feeds_digest({missing_close})); + + auto logical = spec_for(lent); + const auto reference = spec_for(owned); + assert(views::normalize_native_run_spec(logical, context)); + assert(views::native_instrument_feeds_digest(logical.instrument_feeds, context) + == pineforge::native_run_spec_v3::native_instrument_feeds_digest(reference.instrument_feeds)); + assert(views::native_run_spec_digest(logical, context) + == pineforge::native_run_spec_v3::native_run_spec_digest(reference)); + assert(views::validate_native_timeframe_subscriptions( + logical.subscriptions, logical.input_tf, false, logical.auxiliary_feed, + logical.instrument_feeds, context)); + + auto bad_column = logical; + bad_column.instrument_feeds[0].columns[0].values.pop_back(); + const auto column_error = views::normalize_native_run_spec(bad_column, context); + assert(column_error.error == NativeRunSpecError::InvalidInstrumentFeedColumn); + assert(column_error.field == NativeRunSpecField::InstrumentFeedColumns); + + auto bad_close = owned.close_ms; + bad_close[0] = owned.bars[0].timestamp; + const NativeInstrumentFeedView invalid{{owned.bars.data(), owned.bars.size()}, bad_close.data()}; + const NativeInstrumentFeedView* bad_slots[] = {&invalid}; + const auto close_error = views::validate_native_instrument_feeds({lent}, {bad_slots, 1}); + assert(close_error.error == NativeRunSpecError::InvalidInstrumentFeedClose); + assert(close_error.field == NativeRunSpecField::InstrumentFeedClose); + const auto conflict = views::validate_native_instrument_feeds({owned}, context); + assert(conflict.error == NativeRunSpecError::InvalidInstrumentFeedBar); + const auto mismatch = views::validate_native_instrument_feeds({}, context); + assert(mismatch.error == NativeRunSpecError::InvalidInstrumentFeedBar); + + auto empty_meta = lent; + empty_meta.columns.clear(); + const NativeInstrumentFeedView* empty_slots[] = {&empty}; + assert(views::validate_native_instrument_feeds({empty_meta}, {empty_slots, 1})); + const NativeInstrumentFeedView* mixed_slots[] = {nullptr, &descriptor}; + auto other = owned; + other.instrument = "SYN:OWNED"; + assert(views::validate_native_instrument_feeds({other, lent}, {mixed_slots, 2})); + + auto nan_twin = owned; + std::uint64_t bits = UINT64_C(0x7ff8000000000042); + std::memcpy(&nan_twin.bars[0].volume, &bits, sizeof(bits)); + const auto nan_descriptor = borrowed(nan_twin); + const NativeInstrumentFeedView* nan_slots[] = {&nan_descriptor}; + assert(views::native_instrument_feeds_digest({lent}, {nan_slots, 1}) + == pineforge::native_run_spec_v3::native_instrument_feeds_digest({owned})); +} + +class HashHost : public InstrumentHost { +public: + std::vector hashes; + void on_native_bar(const Bar& bar, const NativeDecisionContext& context) override { + InstrumentHost::on_native_bar(bar, context); + hashes.push_back(native_continuation_hash()); + } +}; + +void binding_delivery_and_detachment() { + auto owned = tape(); + const auto reference = spec_for(owned); + auto logical = spec_for(metadata(owned)); + HashHost copied; + HashHost lent; + auto& consumer = NativeExecutionConsumer::bound(lent); + { + auto descriptor = borrowed(owned); + const NativeInstrumentFeedView* slots[] = {&descriptor}; + consumer.stage_instrument_feed_views(slots, 1); + } // staging must own descriptor copies, not these stack objects. + assert(consumer.bar_views().instrument_feed_count == 0); + assert(copied.configure_native(reference).status == NativeSetupStatus::Applied); + assert(lent.configure_native(logical).status == NativeSetupStatus::Applied); + const auto active = consumer.bar_views(); + assert(active.instrument_feed_count == 1); + assert(active.instrument_feeds[0]->bars.data == owned.bars.data()); + + // Staging while Ready affects pending only; no Ready restaging. + const NativeInstrumentFeedView empty{{nullptr, 0}, nullptr}; + const NativeInstrumentFeedView* next[] = {&empty}; + consumer.stage_instrument_feed_views(next, 1); + assert(consumer.bar_views().instrument_feeds[0]->bars.size == owned.bars.size()); + consumer.stage_instrument_feed_views(nullptr, 0); + + const auto chart = continuous_quarter_chart(monday(), 1); + // The chart is a valid single 1-minute input regardless of fixture spacing. + copied.run(chart.data(), 1, "1", "1", false, 4, MagnifierDistribution::ENDPOINTS); + lent.run(chart.data(), 1, "1", "1", false, 4, MagnifierDistribution::ENDPOINTS); + assert(copied.last_error().empty() && lent.last_error().empty()); + assert(copied.deliveries.size() == lent.deliveries.size()); + assert(!lent.deliveries.empty()); + for (std::size_t i = 0; i < copied.deliveries.size(); ++i) { + assert(bars_equal(copied.deliveries[i].bar, lent.deliveries[i].bar)); + assert(copied.deliveries[i].context.interval.last_traded_close_ms + == lent.deliveries[i].context.interval.last_traded_close_ms); + } + assert(copied.hashes == lent.hashes); + assert(copied.native_continuation_hash() == lent.native_continuation_hash()); + assert(consumer.bar_views().instrument_feed_count == 0); + const auto after = lent.native_continuation_hash(); + owned.bars.clear(); + owned.bars.shrink_to_fit(); + owned.close_ms.clear(); + owned.close_ms.shrink_to_fit(); + assert(lent.native_continuation_hash() == after); +} + +class VectorSource final : public NativeLowerPathSource { + const std::vector& bars_; + int& destroyed_; + bool throws_; +public: + VectorSource(const std::vector& bars, int& destroyed, bool throws = false) + : bars_(bars), destroyed_(destroyed), throws_(throws) {} + ~VectorSource() noexcept override { ++destroyed_; } + std::size_t count() const override { return bars_.size(); } + bool for_each(Visitor visitor, void* context) const override { + if (throws_) throw std::runtime_error("l15a-walk-fault"); + for (const auto& bar : bars_) if (!visitor(context, bar)) return false; + return true; + } + NativeBarView segment(std::int64_t, std::int64_t) override { + return {bars_.data(), bars_.size()}; + } + const Bar* next_after_segment() const noexcept override { return nullptr; } +}; + +void lower_context_and_exception_atomicity() { + const auto chart = continuous_quarter_chart(monday(), 2); + auto spec = continuous_spec("l15a-lower", "15"); + IntrabarPath::lower_tf path; + path.tf = "1"; + path.bars = chart; + spec.intrabar.value = path; + auto lent = spec; + lent.intrabar = IntrabarPath{}; + path.bars.clear(); + lent.intrabar.value = path; + + int destroyed = 0; + { + VectorSource source(chart, destroyed); + assert(views::native_intrabar_path_digest(lent.intrabar, &source) + == pineforge::native_run_spec_v3::native_intrabar_path_digest(spec.intrabar)); + const NativeBarViews context{nullptr, 0, &source}; + assert(views::native_run_spec_digest(lent, context) + == pineforge::native_run_spec_v3::native_run_spec_digest(spec)); + } + assert(destroyed == 1); + HashHost host; + auto& consumer = NativeExecutionConsumer::bound(host); + consumer.stage_lower_path_source(std::make_unique(chart, destroyed, true)); + assert(consumer.bar_views().lower_path == nullptr); + bool threw = false; + try { + (void)host.configure_native(lent); + } catch (const std::runtime_error& error) { + threw = std::string(error.what()) == "l15a-walk-fault"; + } + assert(threw); + assert(host.native_state().kind == NativeLifecycleKind::Unconfigured); + assert(consumer.bar_views().lower_path == nullptr); + assert(destroyed == 2); + assert(host.configure_native(spec).status == NativeSetupStatus::Applied); + + // A null staging table is a programmer invariant; active state is intact. + bool invalid_table = false; + try { consumer.stage_instrument_feed_views(nullptr, 1); } + catch (const std::logic_error&) { invalid_table = true; } + assert(invalid_table); + assert(host.native_state().kind == NativeLifecycleKind::Ready); +} + +void refused_configure_discards_pending() { + HashHost host; + auto& consumer = NativeExecutionConsumer::bound(host); + const auto owned = tape(); + auto descriptor = borrowed(owned); + const NativeInstrumentFeedView* slots[] = {&descriptor}; + consumer.stage_instrument_feed_views(slots, 1); + auto bad = spec_for(metadata(owned)); + bad.instrument_feeds[0].columns[0].values.pop_back(); + const auto refused = host.configure_native(bad); + assert(refused.status != NativeSetupStatus::Applied); + assert(consumer.bar_views().instrument_feed_count == 0); + + HashHost retry; + auto& retry_consumer = NativeExecutionConsumer::bound(retry); + const auto logical = spec_for(metadata(owned)); + retry_consumer.stage_instrument_feed_views(slots, 1); + assert(retry.configure_native(logical).status == NativeSetupStatus::Applied); + auto malformed = make_bar(monday(), 0, 1.0); + malformed.high = malformed.low - 1.0; + retry.run(&malformed, 1, "1", "1", false, 4, MagnifierDistribution::ENDPOINTS); + assert(retry.native_state().kind == NativeLifecycleKind::Ready); + assert(retry_consumer.bar_views().instrument_feed_count == 0); + const auto valid = make_bar(monday(), 0, 1.0); + retry.run(&valid, 1, "1", "1", false, 4, MagnifierDistribution::ENDPOINTS); + assert(!retry.last_error().empty()); // detached Ready is engine_invariant. +} +} // namespace + +int main() { + effective_data_and_refusals(); + binding_delivery_and_detachment(); + lower_context_and_exception_atomicity(); + refused_configure_discards_pending(); + std::puts("test_native_bar_views_l15a: contract, identity and lifetime assertions passed"); +} From 25391023b1c4a3b6c24981b7c3677521d0121e76 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sun, 18 Oct 2026 19:47:18 +0800 Subject: [PATCH 02/35] feat(cr150): implement L15a pending bar view bindings Implement the frozen ee3083d4a V1/V2 contract and the 19:24 eager-component ruling. Preserve v3 layouts, owned fallbacks and digest bytes; add no borrowed production export or concrete lower provider. Update read-inventory claims introduced by d0fd21c10 and amended by a6abb14e8 and ee3083d4a; other documentation edits only carry source coordinates. Reword the EntryExitGuard explanation introduced by 47184e5c6e to include unconditional outer-run binding detachment. Red witness committed first as 100a6057d. Builds, compilation, tests and source guards were not run on this Mac, per CORE-TOP brief; lab verification remains pending. --- CMakeLists.txt | 2 +- docs/adr/0001-kernel-adapter-boundary.md | 14 +- docs/design/cr150-contracts.md | 24 +- docs/native-refactor-progress.md | 10 +- scripts/check_native_cpp_versions.py | 287 ++++++++++++++++++++++- src/native_bar_views.cpp | 68 ++++++ src/native_execution_consumer.cpp | 128 ++++++++-- src/native_execution_consumer.hpp | 50 +++- src/native_run_spec.cpp | 137 +++++++++-- src/source/pine_adapter.cpp | 4 +- src/source/pine_security_eval.cpp | 27 ++- src/source/pine_strategy_host.cpp | 7 +- tests/CMakeLists.txt | 4 + tests/test_native_bar_views_l15a.cpp | 39 ++- 14 files changed, 709 insertions(+), 92 deletions(-) create mode 100644 src/native_bar_views.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 1fc877960..461861fd5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -149,7 +149,7 @@ set(PINEFORGE_KERNEL_SOURCES src/timezone.cpp src/native_calendar.cpp src/native_run_spec.cpp - src/native_fx_curve.cpp + src/native_fx_curve.cpp src/native_bar_views.cpp src/selected_report_view.cpp src/selected_window_plan.cpp ) diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index 7110412c6..5c9a861a0 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -823,7 +823,7 @@ The table records, at the tree the doc wave read, what each block described and | `ta::ATR` `include/pineforge/ta.hpp:321` (the `TradingView` comment `sha256:4f6f6939aec2773684b114a13dd002eb58990d6d96c3056ad87361753faf7a0a` :333-340) | average true range over Wilder's `RMA` | Wilder's ATR is the textbook definition; the comment records which convention and the tape that pinned the recompute behaviour | a seeding/averaging field on an indicator whose definition is the convention | `tests/test_ta_rma_warmup.cpp`, `tests/test_recompute.cpp` | | `ta::StdDev` `include/pineforge/ta.hpp:430` (the `TradingView` comment `sha256:7cc1bbcd32dfe673285d51a8ca7080890ac551858099b523480d998f46408666` :420-429) | the population (biased) standard deviation | biased vs sample is a textbook choice, and the kernel states which | a per-instance switch is the *right* answer where two conventions genuinely compete, which is why `ta::EmaSeeding` exists and this one does not need it | `tests/test_ta_indicators_extras.cpp` | | `float_band_eq` `include/pineforge/ta_compare_band.hpp:38` | an absolute 1e-10 float comparison for indicator equality | an epsilon band is needed by any float comparison; the width is the calibration | a band-width field on a comparison the indicators share | `tests/test_dmi_parity.cpp`, `tests/test_ta_osc_edge.cpp` | -| the two delivery rules, `projected_first_index` `src/native_execution_consumer.cpp:9767-9770` (lookahead) and `clear_if_gapped` `:9754-9757` (gaps) | deliver a completed bucket at its **first** contributing bar (lookahead), and **clear** the series on a bar it publishes nothing on (gaps) | both are delivery rules over a bucket the aggregator already closed; neither reads a platform. They are spec fields: `NativeTimeframeSubscription::lookahead` / `::gaps` | already spelled, and both default to the rule that moves no identity | `tests/test_native_htf_subscriptions.cpp` | +| the two delivery rules, `projected_first_index` `src/native_execution_consumer.cpp:9835-9838` (lookahead) and `clear_if_gapped` `:9822-9825` (gaps) | deliver a completed bucket at its **first** contributing bar (lookahead), and **clear** the series on a bar it publishes nothing on (gaps) | both are delivery rules over a bucket the aggregator already closed; neither reads a platform. They are spec fields: `NativeTimeframeSubscription::lookahead` / `::gaps` | already spelled, and both default to the rule that moves no identity | `tests/test_native_htf_subscriptions.cpp` | | `pf_native_subscription_v1::lookahead` `include/pineforge/native_c_api.h:2271`, `::gaps` `include/pineforge/native_c_api.h:2275` | the C spelling of those two rules | since R5 lane E7 both words are typed and named after the kernel's own delivery rules (`pf_native_lookahead_e` `include/pineforge/native_c_api.h:799`, `pf_native_gaps_e` `:810`); the C header no longer names `barmerge` at all, and the migration page maps Pine's words onto them — a *pointer to the reader's vocabulary*, not a justification | — | `tests/test_native_c_api.c` | | the authoritative-feed partition `src/engine_aux_security.cpp` | a feed is the venue's own bars of one timeframe; its stamps are the period partition | already ruled: design §2.ii row s, and the residual table's own row | the policy knob is installing the feed or not | `tests/test_native_htf_subscriptions.cpp`, `tests/test_native_security_feed.cpp` | | `session_template_knows_early_close` `src/engine_security.cpp` | the instrument-class classification | already ruled: design §2.ii row t; `SymInfo::type`'s vocabulary is fixed by the frozen C ABI | — | the private test `test_native_wm_buckets` | @@ -837,11 +837,11 @@ The table records, at the tree the doc wave read, what each block described and | the indicator library -- `include/pineforge/ta.hpp`, `src/ta_moving_averages.cpp`, `src/ta_oscillators.cpp`, `src/ta_extremes_volume.cpp`, `src/ta_volatility_trend.cpp`, `src/ta_misc.cpp`, `include/pineforge/window_sum.hpp`, `include/pineforge/math.hpp` | each indicator implements its Pine `ta.*` / `math.*` definition -- warm-up and na rules, the window call-site ring, the compensated window sum, tie and epsilon rules -- and its comments record the TradingView tape or export that calibrated each number (the `ta::ATR` and standard-deviation rows above are two of them) | an indicator is a function of its inputs, the same for every venue; generated strategies call exactly these classes, and where two textbook conventions genuinely compete the choice is a per-instance option (`ta::EmaSeeding`), never a branch on the platform | a convention switch on every indicator, for a choice the script already made by calling that Pine function | the kernel-only TA suites `tests/test_ta_*.cpp` and the corpus parity gate | | the formatting library -- `include/pineforge/str_utils.hpp`, `src/str_utils.cpp` (`str_tostring`, `str_format_values`, `str_format`) | the number text of Pine's `str.tostring` / `str.format` built-ins: a number's shortest round-trip decimal spelling, scaled and rounded half-up on its digits, the default, `percent`, `volume` (K / M / B / T) and pattern modes, MessageFormat placeholders and quoting, `NaN` and `Infinity` -- the published rendering TradingView's exported tapes pin (R5 lane C6), ported from the codegen formatter (lane C6b) by R5 lane B-ENGINE; the `mintick` mode, which generated code delegates to it, is kept as it was | a formatting library is a function of its inputs, the same for every venue, as the indicator library is: it spells a number the way the source language's own `str.*` library defines, and no kernel order, fill, settlement, margin or hash code calls it -- of the thirty-five kernel translation units only `src/str_utils.cpp` names these functions | a rendering switch on functions whose definition the calling script chose by calling them | `tests/test_str_number_format.cpp` (lane C6b's 42-case edge battery, every numeric string of lane C6's TradingView exports, byte-identical output on macOS arm64 and Linux x86-64) and `tests/test_str_utils.cpp` | | the language calendar functions -- `include/pineforge/session_time.hpp`, `src/session_time.cpp` (`timeframe_time`, `local_hour` and its siblings, `session_in_market`, `session_trading_day_open_ms`, ...) | the definitions of Pine's `time()` / `time_close()` / bare-time / `session.is*` built-ins for generated code: session and timezone parsing, the invalid- and 24-hour-session spellings, `time("60")` day keying, `session.is*` on daily charts, each comment naming the tape that pinned it | the inputs are data -- a session string, a timezone, a timeframe -- so another venue is driven by its own session template; nothing branches on a platform, and a native host's calendar is the separate generic `native_calendar` | a rules switch on functions whose definition the generated script chose by calling them | `tests/test_session_time.cpp`, `tests/test_pine_time_day_stamp_grid.cpp` and the corpus parity gate | -| `present_session_day` `src/native_execution_consumer.cpp:8161`, presenting `NativeDecisionContext::in_session` `include/pineforge/market_driver.hpp:161` and the three facts after it | a script bar's session-day facts: in session on the run's calendar, read at the bar's label, or at its interval's first eligible instant when that is later (a bar opening in a declared break), never at an instant before the label; it opens / closes its session day when the bar before / after it is out of session or on another session day, that bar being the one the run holds (batch input, stream warmup) and otherwise the calendar's previous or next eligible input slot across declared breaks; the run's first bar opens its day, a batch's final bar closes it, a D/W/M bar holds whole days | the session, the timezone and the session day are the run's own calendar (`spec.session`, `spec.timezone`, `native_calendar::session_day_ordinal`), the neighbours are the run's own input, and a second venue drives it through that data alone. The reading is TradingView's session DAY, measured on its tapes by lanes E25/E26 (the private fixture set `session_islastbar`: 255/255 NYSE:F last bars, 255/255 first, 370/370 ETH last), which a bare host now reproduces from the kernel alone. Reading a bar at its own label is generic too: an instant before the label is not the bar's, whatever grid the calendar keys its interval to (R5 lane K-SESSION-WINDOWS: NASDAQ:AAPL with extended hours opens its bars on the hour, and the 16:00 bar, which the 09:30-anchored grid files under 15:30, read in session and closed the regular day in place of the 15:00 bar; TSE:7203's and CBOT:ZC1!'s 60-minute bars that open inside a break are in session on TradingView's tapes, which reading at the label alone would lose) | a switch over the run-end convention or the neighbour rule would put a hash-visible choice on the surface for what the run's own input already decides. The one host that reads its batch end differently — a live probe recomputing a batch whose last input is still forming — reads a second fact, `closes_session_day_open_ended`, not a switch; the facts are presentation and fold into no digest | the private test `test_native_session_day_facts` (kernel-only: the tapes, the audit's Tokyo `2230-0500` session, streams, a fill recalculation), `tests/test_native_session_day_bar_open.cpp` (kernel-only: TradingView's extended-hours and break-open tapes, the private fixture set `session_windows`), the C scenario of `tests/test_native_c_api.c`, the private test `test_session_day_facts_adapter` (the adapter selects them flag for flag), `tests/test_native_session_day_utc.cpp` (kernel-only: on a UTC calendar the facts read an in-session instant without resolving its interval, and match the zone path at every decision point), and `tests/test_native_session_day_zoned.cpp` (kernel-only: on a certified cycle of an exchange zone, which `native_calendar::cycle_certificate` decides and the witness checks at real transitions, the facts read an instant off its session day, and match the zone's POSIX twin at every decision point) | -| `NativeExecutionConsumer::script_bucket_completions` `src/native_execution_consumer.cpp:8918-8934` | before a run, how an input span aggregates into script bars: which input bars complete one, and which complete one on a period boundary, under the run's own timeframes, zone and session (R5 lane B-ADAPTER, item 4) | any host that aggregates a finer feed asks it before its first input; the answer is the run's own `TimeframeAggregator` over the span and spec fields -- the real-end / chart-close completion and the session-day split this table already rules generic -- and nothing branches on a platform | none: a const query with no spec field, nothing hashed and no state moved; the host decides what the answer means for it (the Pine scheduler reads it instead of running its own preview aggregator) | `tests/test_native_script_bucket_completions.cpp` (kernel-only: the Pine scheduler's preview on the K1 zones, across DST, windowed sessions and 1D/1W/1M) | -| `PhysicalExecutionContext::account_fx` `include/pineforge/execution.hpp:92`, read by `build_close_trade_with_costs` `src/engine_orders.cpp:70-87` and handed by `append_open_position_report_rows` `src/native_execution_consumer.cpp:8893-8914` | a closing or mark-to-market row converts at the account-currency rate its execution names -- a mark at a report point's instant, a fill at its own cursor -- and at the presented clock's rate when it names none, as before (R5 lane B-ADAPTER, item 3) | the rate is data a host of any venue supplies with the row it asks for; nothing branches on a platform, and the row's currency is the basis its owner's magnitudes are recorded in | none: no spec field; a caller names the rate per call, and a call naming none converts exactly as before | `tests/test_adapter_range_end_fx.cpp` (a stepped FX curve at seven instants around the range end, plain and aggregated charts) | -| `NativeExecutionConsumer::execute_current` `src/native_execution_consumer.cpp:7440` | a current execution converts at its cursor's rate, threaded through its terms facts, inspection, preview and settlement contexts, and neither writes nor restores the host's presented clock, so its own hooks and the host after it see the frame's clock; its preview `inspect_current_execution` still converts at the presented clock (R5 lane B-ADAPTER, item 5) | an execution converting at its own instant's rate is the same for every host and venue, and the host's presented clock stays its own | none: the rate is the one the run already chose (the cursor's instant), carried as data | `tests/test_native_current_execution_rate.cpp` (kernel-only) | -| `NativeExecutionConsumer::validate_current_execution` `src/native_execution_consumer.cpp:7163` | a current execution refuses a request bound to a host roster (`BindCohort`) as `UnsupportedRequest`, before its point is taken and whatever the roster holds: it admits exactly the owners the evaluation's current shape executes (`current_shape` `src/native_order.cpp:383`: `Independent`, `BindOpening`, `BindOpenings`). Since R4 slice C it had admitted `BindCohort` as well, which the evaluation then left unevaluated, so a roster holding a live lot failed the run with "native current evaluated allowance mismatch" and an empty one answered `UnreadyOwner`; its read-only preview (`inspect_current_execution`) settled what the execution could not (R5 lane PAR-ORDERS-2) | one legality rule, the evaluation's own, decides a command before any point is taken, the same for every host and for the C surface (`strategy_native_execute_current_v1` answers through this validation); a roster read at the match is not a membership fixed at the command, which is what the documented contract names (`docs/pages/native-engine.md` "Selected exposure and current execution") | none: no spec field or hook. A refused request stays queued and fills at the next match like any market close; a host that wants a cohort's lots closed at once fixes its roster first (`BindOpenings`), as the Pine adapter's immediate closes do | `tests/test_native_current_cohort_refusal.cpp` (kernel-only: a live and an empty roster refused, then filled as queued closes; a fixed roster executing at once) , `tests/test_native_cohort_sliced_close.cpp` (its preview rows: the roster close's preview is refused, the same selection's settles) and `tests/test_adapter_live_state_equivalence.cpp` (reversals seed 3246599, which the failure used to end) | +| `present_session_day` `src/native_execution_consumer.cpp:8229`, presenting `NativeDecisionContext::in_session` `include/pineforge/market_driver.hpp:161` and the three facts after it | a script bar's session-day facts: in session on the run's calendar, read at the bar's label, or at its interval's first eligible instant when that is later (a bar opening in a declared break), never at an instant before the label; it opens / closes its session day when the bar before / after it is out of session or on another session day, that bar being the one the run holds (batch input, stream warmup) and otherwise the calendar's previous or next eligible input slot across declared breaks; the run's first bar opens its day, a batch's final bar closes it, a D/W/M bar holds whole days | the session, the timezone and the session day are the run's own calendar (`spec.session`, `spec.timezone`, `native_calendar::session_day_ordinal`), the neighbours are the run's own input, and a second venue drives it through that data alone. The reading is TradingView's session DAY, measured on its tapes by lanes E25/E26 (the private fixture set `session_islastbar`: 255/255 NYSE:F last bars, 255/255 first, 370/370 ETH last), which a bare host now reproduces from the kernel alone. Reading a bar at its own label is generic too: an instant before the label is not the bar's, whatever grid the calendar keys its interval to (R5 lane K-SESSION-WINDOWS: NASDAQ:AAPL with extended hours opens its bars on the hour, and the 16:00 bar, which the 09:30-anchored grid files under 15:30, read in session and closed the regular day in place of the 15:00 bar; TSE:7203's and CBOT:ZC1!'s 60-minute bars that open inside a break are in session on TradingView's tapes, which reading at the label alone would lose) | a switch over the run-end convention or the neighbour rule would put a hash-visible choice on the surface for what the run's own input already decides. The one host that reads its batch end differently — a live probe recomputing a batch whose last input is still forming — reads a second fact, `closes_session_day_open_ended`, not a switch; the facts are presentation and fold into no digest | the private test `test_native_session_day_facts` (kernel-only: the tapes, the audit's Tokyo `2230-0500` session, streams, a fill recalculation), `tests/test_native_session_day_bar_open.cpp` (kernel-only: TradingView's extended-hours and break-open tapes, the private fixture set `session_windows`), the C scenario of `tests/test_native_c_api.c`, the private test `test_session_day_facts_adapter` (the adapter selects them flag for flag), `tests/test_native_session_day_utc.cpp` (kernel-only: on a UTC calendar the facts read an in-session instant without resolving its interval, and match the zone path at every decision point), and `tests/test_native_session_day_zoned.cpp` (kernel-only: on a certified cycle of an exchange zone, which `native_calendar::cycle_certificate` decides and the witness checks at real transitions, the facts read an instant off its session day, and match the zone's POSIX twin at every decision point) | +| `NativeExecutionConsumer::script_bucket_completions` `src/native_execution_consumer.cpp:8986-9002` | before a run, how an input span aggregates into script bars: which input bars complete one, and which complete one on a period boundary, under the run's own timeframes, zone and session (R5 lane B-ADAPTER, item 4) | any host that aggregates a finer feed asks it before its first input; the answer is the run's own `TimeframeAggregator` over the span and spec fields -- the real-end / chart-close completion and the session-day split this table already rules generic -- and nothing branches on a platform | none: a const query with no spec field, nothing hashed and no state moved; the host decides what the answer means for it (the Pine scheduler reads it instead of running its own preview aggregator) | `tests/test_native_script_bucket_completions.cpp` (kernel-only: the Pine scheduler's preview on the K1 zones, across DST, windowed sessions and 1D/1W/1M) | +| `PhysicalExecutionContext::account_fx` `include/pineforge/execution.hpp:92`, read by `build_close_trade_with_costs` `src/engine_orders.cpp:70-87` and handed by `append_open_position_report_rows` `src/native_execution_consumer.cpp:8961-8982` | a closing or mark-to-market row converts at the account-currency rate its execution names -- a mark at a report point's instant, a fill at its own cursor -- and at the presented clock's rate when it names none, as before (R5 lane B-ADAPTER, item 3) | the rate is data a host of any venue supplies with the row it asks for; nothing branches on a platform, and the row's currency is the basis its owner's magnitudes are recorded in | none: no spec field; a caller names the rate per call, and a call naming none converts exactly as before | `tests/test_adapter_range_end_fx.cpp` (a stepped FX curve at seven instants around the range end, plain and aggregated charts) | +| `NativeExecutionConsumer::execute_current` `src/native_execution_consumer.cpp:7508` | a current execution converts at its cursor's rate, threaded through its terms facts, inspection, preview and settlement contexts, and neither writes nor restores the host's presented clock, so its own hooks and the host after it see the frame's clock; its preview `inspect_current_execution` still converts at the presented clock (R5 lane B-ADAPTER, item 5) | an execution converting at its own instant's rate is the same for every host and venue, and the host's presented clock stays its own | none: the rate is the one the run already chose (the cursor's instant), carried as data | `tests/test_native_current_execution_rate.cpp` (kernel-only) | +| `NativeExecutionConsumer::validate_current_execution` `src/native_execution_consumer.cpp:7231` | a current execution refuses a request bound to a host roster (`BindCohort`) as `UnsupportedRequest`, before its point is taken and whatever the roster holds: it admits exactly the owners the evaluation's current shape executes (`current_shape` `src/native_order.cpp:383`: `Independent`, `BindOpening`, `BindOpenings`). Since R4 slice C it had admitted `BindCohort` as well, which the evaluation then left unevaluated, so a roster holding a live lot failed the run with "native current evaluated allowance mismatch" and an empty one answered `UnreadyOwner`; its read-only preview (`inspect_current_execution`) settled what the execution could not (R5 lane PAR-ORDERS-2) | one legality rule, the evaluation's own, decides a command before any point is taken, the same for every host and for the C surface (`strategy_native_execute_current_v1` answers through this validation); a roster read at the match is not a membership fixed at the command, which is what the documented contract names (`docs/pages/native-engine.md` "Selected exposure and current execution") | none: no spec field or hook. A refused request stays queued and fills at the next match like any market close; a host that wants a cohort's lots closed at once fixes its roster first (`BindOpenings`), as the Pine adapter's immediate closes do | `tests/test_native_current_cohort_refusal.cpp` (kernel-only: a live and an empty roster refused, then filled as queued closes; a fixed roster executing at once) , `tests/test_native_cohort_sliced_close.cpp` (its preview rows: the roster close's preview is refused, the same selection's settles) and `tests/test_adapter_live_state_equivalence.cpp` (reversals seed 3246599, which the failure used to end) | | the chart symbol's daily partition -- `prepare_chart_day_partition` `src/engine_aux_security.cpp:358`, kept in `chart_day_partition_` `include/pineforge/engine.hpp:1218` and presented to the language runtime through `RuntimeAmbient::day_partition` `src/runtime_ambient.hpp:38` (the TradingView comment above `NativeDayPartition` in `include/pineforge/timeframe.hpp`) | on an intraday chart whose run installed its own symbol's `D` feed, the chart-level D period follows that feed's stamps: the exchange's trade-date bars, a holiday-merged day included. It governs `time("D")`, `timeframe.change("1D")`, `ta.change(time("D"))`, the default-anchored `ta.vwap` and `crosses_boundary(DAY)`. Without the feed, on a 1D chart or on another symbol's clock every rule is the nominal session day | the stamps are the venue's own data, as for the authoritative-feed partition above: another venue is driven by installing its own daily bars, and nothing branches on a platform | the policy knob is installing the feed or not, so a "which daily partition" field would restate it. Only the Pine host builds it (`prepare_chart_day_partition` `pine_strategy_host.cpp:1646`, `:1687`) and presents it for each bar it publishes (`AmbientDayPartitionScope` `pine_strategy_host.cpp:2389`). A bare host never calls the builder, so its calendar stays nominal | the private test `test_o_close_pct_day_anchor` (the partition is built from the D feed, and none without it) and `tests/test_chart_day_memo.cpp`; the tape is `lab tv` `o-cme-dayanchor-full`, recorded at the site | | the bar-time accessors -- `_decompose_bar_time` and `_bar_hour` through `_bar_weekofyear` `include/pineforge/engine.hpp:1066-1093` | the presented bar's stamp broken into calendar fields in UTC, memoized per stamp | a function of the stamp alone, and UTC is the engine's storage timezone | none: they read no spec field and fold into no digest. They are protected `BacktestEngine` members, part of the codegen contract. Their comment claimed that generated code reads them for Pine's bare `hour` / `minute` / `dayofweek`. It does not: the codegen transpiler emits `pine_hour(current_bar_.timestamp, syminfo_.timezone)` and its siblings on the exchange timezone (`pineforge_codegen/codegen/tables.py` at the corpus's pinned commit `f3285d79`), which are the language calendar functions above. No generated strategy in `corpus/` names a `_bar_*` accessor. The comment now says so (R5 lane H-DOCGATES) | `tests/test_chart_timezone.cpp` (`_bar_hour()` is the UTC hour) and `tests/test_intraday_rollover_chart_tz_l4a.cpp` | | the conventional offset spellings of `timezone_accepted` `include/pineforge/native_calendar.hpp:208` (its comment's "TV sign" line) | `UTC+05:30` and `GMT-4` mean what they say: five and a half hours ahead of UTC, four behind (ISO 8601's sign), where a POSIX `TZ` string of the same shape means the opposite; a POSIX rule string keeps POSIX's own sign | a zone spelling is data, and the sign is the one a venue printing `UTC+8` means; nothing branches on a platform | a sign-convention switch over a spelling whose meaning is fixed | `tests/test_native_calendar.cpp` (`resolve_civil("UTC+05:30", …)` and `resolve_civil("GMT-4", …)` against their fixed instants) | @@ -941,7 +941,7 @@ questions: the mechanism, why it is generic, and why no knob is needed. | Kernel capability | Generic mechanism | Why no platform knob is added | Executed witness | |---|---|---|---| -| A daily bar the installed daily feed dates: `NativeExecutionConsumer::prepare_day_labels` `src/native_execution_consumer.cpp:2412`, called at the end of `begin_ready`, once every feed is installed (`prepare_day_labels` `src/native_execution_consumer.cpp:3029`) and read where a script interval resolves (`script_interval_resolved` `src/native_execution_consumer.cpp:2362`); a lookup by a moved label is its bar's own (`day_label_origin` `src/native_execution_consumer.cpp:2465`) | (a) The venue's own daily bars date its days. When an intraday input aggregates into `"D"` script bars and a daily feed is installed -- the feed store's (`set_native_security_feed`) or a declared series' authoritative bars -- each stamp labels the D bar holding the session instant it covers: the stamp itself inside a session, else the next session's open, the instant the store already keys the feed's bars by. Only the label moves; the bar's span, closes, successor and OHLCV stay the calendar's aggregate, and every lookup by the label -- the bar's open and close, its sub-bars on a magnified run, its execution anchors, its risk day and a C host's session day -- reads the bar's own interval and day, not the day before the stamp. A bar no stamp covers keeps the calendar's label, a bar two stamps cover takes the earlier, and a label never passes an input bar the run holds: a stamp with an input between it and its bar's calendar open labels nothing, so a label never follows its bar's first input nor precedes another bar's. A stamp at the calendar's own open (09:30 ET, 17:00 CT, 00:00 UTC, OANDA:EURUSD's 17:00 ET) changes nothing; a venue that dates its days elsewhere -- OANDA's XAUUSD at 17:00 ET in the break of its 1800-1700 session, NSE's Muhurat day at its 13:45 open -- is driven by installing its own daily bars, and the kernel holds no venue's day-stamp convention | (b) None: the policy knob is the authoritative-feed ruling's, installing the daily feed or not (`engine_aux_security.cpp`'s contract), exactly as W15's late opens are; a "day label" field would restate the stamps the run installed. No input moves without the feed, and a calendar input's labels (the venue's bars themselves) never move. Calibration: TradingView dates every order of OANDA:XAUUSD 1D, backtested with the bar magnifier, by the 17:00 ET stamp -- 22:00 UTC in EST, 21:00 UTC in EDT -- where the 1m aggregate's session opens at 18:00 (`lab tv` `xc-dmag-xau1d`, `xc-dmag-dip-xau1d` and their `-off` twins, the private fixture set `daily_magnifier`) | `tests/test_native_day_labels.cpp` (kernel-only: break stamps across a DST switch, the bar's open reading its close's interval and input slot, a magnified run's sub-bars, a declared series' authoritative bars, stamps at the open, a missing, a doubled and a late stamp, an input in the break before a stamp's bar, NSE's late open, a daily, weekly and intraday script control, a reused host), `tests/test_native_c_api.c` (a C host's session day of a dated bar), `tests/test_daily_magnifier_tapes.cpp` (the four tapes through the Pine adapter: 20/20, 18/18, 31/31, 31/31; without the daily feed every trade an hour late) | +| A daily bar the installed daily feed dates: `NativeExecutionConsumer::prepare_day_labels` `src/native_execution_consumer.cpp:2462`, called at the end of `begin_ready`, once every feed is installed (`prepare_day_labels` `src/native_execution_consumer.cpp:3097`) and read where a script interval resolves (`script_interval_resolved` `src/native_execution_consumer.cpp:2412`); a lookup by a moved label is its bar's own (`day_label_origin` `src/native_execution_consumer.cpp:2515`) | (a) The venue's own daily bars date its days. When an intraday input aggregates into `"D"` script bars and a daily feed is installed -- the feed store's (`set_native_security_feed`) or a declared series' authoritative bars -- each stamp labels the D bar holding the session instant it covers: the stamp itself inside a session, else the next session's open, the instant the store already keys the feed's bars by. Only the label moves; the bar's span, closes, successor and OHLCV stay the calendar's aggregate, and every lookup by the label -- the bar's open and close, its sub-bars on a magnified run, its execution anchors, its risk day and a C host's session day -- reads the bar's own interval and day, not the day before the stamp. A bar no stamp covers keeps the calendar's label, a bar two stamps cover takes the earlier, and a label never passes an input bar the run holds: a stamp with an input between it and its bar's calendar open labels nothing, so a label never follows its bar's first input nor precedes another bar's. A stamp at the calendar's own open (09:30 ET, 17:00 CT, 00:00 UTC, OANDA:EURUSD's 17:00 ET) changes nothing; a venue that dates its days elsewhere -- OANDA's XAUUSD at 17:00 ET in the break of its 1800-1700 session, NSE's Muhurat day at its 13:45 open -- is driven by installing its own daily bars, and the kernel holds no venue's day-stamp convention | (b) None: the policy knob is the authoritative-feed ruling's, installing the daily feed or not (`engine_aux_security.cpp`'s contract), exactly as W15's late opens are; a "day label" field would restate the stamps the run installed. No input moves without the feed, and a calendar input's labels (the venue's bars themselves) never move. Calibration: TradingView dates every order of OANDA:XAUUSD 1D, backtested with the bar magnifier, by the 17:00 ET stamp -- 22:00 UTC in EST, 21:00 UTC in EDT -- where the 1m aggregate's session opens at 18:00 (`lab tv` `xc-dmag-xau1d`, `xc-dmag-dip-xau1d` and their `-off` twins, the private fixture set `daily_magnifier`) | `tests/test_native_day_labels.cpp` (kernel-only: break stamps across a DST switch, the bar's open reading its close's interval and input slot, a magnified run's sub-bars, a declared series' authoritative bars, stamps at the open, a missing, a doubled and a late stamp, an input in the break before a stamp's bar, NSE's late open, a daily, weekly and intraday script control, a reused host), `tests/test_native_c_api.c` (a C host's session day of a dated bar), `tests/test_daily_magnifier_tapes.cpp` (the four tapes through the Pine adapter: 20/20, 18/18, 31/31, 31/31; without the daily feed every trade an hour late) | ## Boundary rules (for contributors) diff --git a/docs/design/cr150-contracts.md b/docs/design/cr150-contracts.md index 8b53dbb40..f103c497e 100644 --- a/docs/design/cr150-contracts.md +++ b/docs/design/cr150-contracts.md @@ -206,19 +206,19 @@ references; L15a must maintain these anchors and mechanically pin every current | Reader to route through the view/source | Source anchors in this tree | DESIGN lines | |---|---|---| -| Kernel instrument-feed pump: current bar, close, next open, count | `NativeExecutionConsumer::pump_instrument_subscription` `src/native_execution_consumer.cpp:9932-9983` | 357-358 | -| Host feed lookup/delivery and column reads | `installed_symbol_feed` `src/source/pine_security_eval.cpp:1059-1068`; `installed_symbol_feed` `src/source/pine_security_eval.cpp:1286-1293`; `installed_symbol_feed` `src/source/pine_security_eval.cpp:1312-1320` | 359-360 | -| Feed validation: bar values/order, close length/bounds, column length; configure and consumer entry points | `feed.bars` `src/native_run_spec.cpp:254-276`; `instrument_feed_shapes` `src/native_run_spec.cpp:730`; `validate_native_instrument_feeds` `src/native_run_spec.cpp:995-998`; `validate_native_timeframe_subscriptions` `src/native_execution_consumer.cpp:9468-9471`; `validate_native_timeframe_subscriptions` `src/native_execution_consumer.cpp:9493-9495` | 361-363 | -| Adapter projection validation: use the context normalizer; host stages after lending and before projection, using a context built from its store | `validate_native_run_spec` `src/source/pine_adapter.cpp:2997-3001` | 361-363; V1/V2 change 5 | -| Instrument-feed digest and cached/uncached continuation folds | `native_instrument_feeds_digest` `src/native_run_spec.cpp:1124-1160`; `native_instrument_feeds_digest` `src/native_execution_consumer.cpp:321-323`; `native_instrument_feeds_digest` `src/native_execution_consumer.cpp:1935-1936`; `native_run_spec_digest` `src/native_execution_consumer.cpp:12730-12735` | 364-365 | -| Staged feed counts in host state and its hash | `installed_symbol_feed` `src/source/pine_strategy_host.cpp:230-234`; `staged_.instrument_feeds` `src/source/pine_state_hash.cpp:292-299` | 366-367 | -| Refresh of symbol-data digest and host hash fold | `refresh_symbol_data_digest` `src/source/pine_security_eval.cpp:1536-1544`; `symbol_data_digest_` `src/source/pine_state_hash.cpp:1123-1129` | 367-368 | -| Restore/store upkeep and column-setter length check | `held.bars` `src/source/pine_security_eval.cpp:1091-1096`; `feed.bars` `src/source/pine_security_eval.cpp:1420-1422` | 369-370 | -| Lower-path preflight/validation | `NativeExecutionConsumer::preflight_intrabar_path` `src/native_execution_consumer.cpp:3148-3181` | 371,1221-1222 | -| Lower-path digest (count first) and continuation hash | `native_intrabar_path_digest` `src/native_run_spec.cpp:867-895`; `native_intrabar_path_digest` `src/native_execution_consumer.cpp:288` | 371,1223-1224 | -| Per-script-bar lower-path binary search/walk | `candidate` `src/native_execution_consumer.cpp:8562-8576` | 371,1225 | +| Kernel instrument-feed pump: current bar, close, next open, count | `NativeExecutionConsumer::pump_instrument_subscription` `src/native_execution_consumer.cpp:10000-10055` | 357-358 | +| Host feed lookup/delivery and column reads | `installed_symbol_feed` `src/source/pine_security_eval.cpp:1059-1068`; `installed_symbol_feed` `src/source/pine_security_eval.cpp:1291-1301`; `installed_symbol_feed` `src/source/pine_security_eval.cpp:1320-1328` | 359-360 | +| Feed validation: bar values/order, close length/bounds, column length; configure and consumer entry points | `feed.bars` `src/native_run_spec.cpp:259-290`; `instrument_feed_shapes` `src/native_run_spec.cpp:743`; `validate_native_instrument_feeds` `src/native_run_spec.cpp:1019-1022`; `validate_native_timeframe_subscriptions` `src/native_execution_consumer.cpp:9536-9539`; `validate_native_timeframe_subscriptions` `src/native_execution_consumer.cpp:9561-9563` | 361-363 | +| Adapter projection validation: use the context normalizer; host stages after lending and before projection, using a context built from its store | `normalize_projected_spec` `src/source/pine_adapter.cpp:2997-3003` | 361-363; V1/V2 change 5 | +| Instrument-feed digest and cached/uncached continuation folds | `digest_instrument_feed_views` `src/native_run_spec.cpp:1162-1205`; `native_instrument_feeds_digest` `src/native_execution_consumer.cpp:321-323`; `native_instrument_feeds_digest` `src/native_execution_consumer.cpp:2774-2775`; `native_run_spec_digest` `src/native_execution_consumer.cpp:12807-12812` | 364-365 | +| Staged feed counts in host state and its hash | `installed_symbol_feed` `src/source/pine_strategy_host.cpp:230-238`; `staged_.instrument_feeds` `src/source/pine_state_hash.cpp:292-299` | 366-367 | +| Refresh of symbol-data digest and host hash fold | `refresh_symbol_data_digest` `src/source/pine_security_eval.cpp:1545-1553`; `symbol_data_digest_` `src/source/pine_state_hash.cpp:1123-1129` | 367-368 | +| Restore/store upkeep and column-setter length check | `held.bars` `src/source/pine_security_eval.cpp:1091-1101`; `data.bars` `src/source/pine_security_eval.cpp:1428-1431` | 369-370 | +| Lower-path preflight/validation | `NativeExecutionConsumer::preflight_intrabar_path` `src/native_execution_consumer.cpp:3216-3249` | 371,1221-1222 | +| Lower-path digest (count first) and continuation hash | `native_intrabar_path_digest` `src/native_run_spec.cpp:891-919`; `native_intrabar_path_digest` `src/native_execution_consumer.cpp:288` | 371,1223-1224 | +| Per-script-bar lower-path binary search/walk | `candidate` `src/native_execution_consumer.cpp:8630-8644` | 371,1225 | | Scheduler sub-bar lookup and one-bar look-ahead | `PineScheduler::lower_path_bar_at` `src/source/pine_scheduler_native.cpp:252-264`; `next_open` `src/source/pine_scheduler_native.cpp:296-298` | 371,1226-1227 | -| Lower-path emptiness test | `inactive_sampler_path` `src/source/pine_adapter.cpp:3420-3422` | 371,1228 | +| Lower-path emptiness test | `inactive_sampler_path` `src/source/pine_adapter.cpp:3422-3424` | 371,1228 | Every count/digest must fold the same bytes/order for copied and borrowed feeds, including cached and uncached paths (2.2:372-375). Views replace storage; diff --git a/docs/native-refactor-progress.md b/docs/native-refactor-progress.md index fd9c33a3f..f55e931e5 100644 --- a/docs/native-refactor-progress.md +++ b/docs/native-refactor-progress.md @@ -173,7 +173,7 @@ them for the record only. "Linux aarch64" is the maintainers' Cortex-A725 host. | Item | What it is | Measured size | Deferred by | Prototype and evidence | Reopens when | | --- | --- | --- | --- | --- | --- | -| D2-B, the quiet-bar protocol and one receipt read per bar | PERF-D2's lane: a per-bar bar-open request, a bulk fold of quiet driver points, the adapter's quiet-open predicate with lazy open bookkeeping, and a gate before the receipt poll. D2-A landed the hook declarations and the O(1) `has_command_after` (`src/native_execution_consumer.hpp:321`). PERF-L4 gated the read inside `observe_terminal_receipts` (`src/source/pine_adapter.cpp:6446`), which still runs about three times a bar | PERF-D2, Linux aarch64 (Cortex-A725), callgrind and GBench: about ×0.88 on the 14 probes (an estimate from a measured −13 to −14 % floor bound for the bar-open request alone); after PB the receipt poll still costs about 250 instructions a bar on the no-order floor. On this tree the quiet-bar probe asks the receipt gate 40,996 times and each once-per-bar-open gate 13,248 times: 3.09 reads a bar, 82 % of them stopped at the gate (macOS arm64, AppleClang 17, Release) | "R5 PERF-D2 DONE 2026-09-24 19:56" (D2-B after V19-D); "R5 SUPERVISOR RULING 2026-09-25 00:15 Taipei": "D2-B (quiet-bar protocol) waits for PERF-D3's verdict"; TRIAGE1 §D | An unpublished prototype branch and its patches. PERF-D3's S1 (next row) prototyped the request and the lazy opening | A post-release performance epoch. Done when the receipt gate is asked as often as the once-per-bar gates | +| D2-B, the quiet-bar protocol and one receipt read per bar | PERF-D2's lane: a per-bar bar-open request, a bulk fold of quiet driver points, the adapter's quiet-open predicate with lazy open bookkeeping, and a gate before the receipt poll. D2-A landed the hook declarations and the O(1) `has_command_after` (`src/native_execution_consumer.hpp:321`). PERF-L4 gated the read inside `observe_terminal_receipts` (`src/source/pine_adapter.cpp:6448`), which still runs about three times a bar | PERF-D2, Linux aarch64 (Cortex-A725), callgrind and GBench: about ×0.88 on the 14 probes (an estimate from a measured −13 to −14 % floor bound for the bar-open request alone); after PB the receipt poll still costs about 250 instructions a bar on the no-order floor. On this tree the quiet-bar probe asks the receipt gate 40,996 times and each once-per-bar-open gate 13,248 times: 3.09 reads a bar, 82 % of them stopped at the gate (macOS arm64, AppleClang 17, Release) | "R5 PERF-D2 DONE 2026-09-24 19:56" (D2-B after V19-D); "R5 SUPERVISOR RULING 2026-09-25 00:15 Taipei": "D2-B (quiet-bar protocol) waits for PERF-D3's verdict"; TRIAGE1 §D | An unpublished prototype branch and its patches. PERF-D3's S1 (next row) prototyped the request and the lazy opening | A post-release performance epoch. Done when the receipt gate is asked as often as the once-per-bar gates | | S1a, kernel quiet-bar driving (PERF-D3) | A bar with nothing live, a flat book and a declined opening is driven in a quiet form: the kernel writes the bookkeeping its four price points would leave (ordinals, the last two driver marks, the floor, the point epochs) without walking them or calling the bar-open callback. Additive host cadence declarations; C hosts declare from their callback table; one C symbol under the PF_API checklist | PERF-D3, Linux aarch64 (Cortex-A725), GBench time and `perf stat` instructions, on the L3 tree `b33ea659`, S1a with S1b: no-order floor 0.764 / 0.660 in time (0.718 / 0.656 in instructions), 14 probes 0.958 / 0.921 (0.892 / 0.856), 100 slots 0.972 / 0.956 per slot. A bare kernel host that opts in: idle 0.545× (35 % fewer instructions a bar), market 0.81×, bracket 0.92×. Byte-identical: corpus 312/312, the 132-configuration battery with its hash columns | "FINDING PERF-D3 (filed for a post-release epoch per the owner's cap)" (ledger, 2026-09-24 16:58 UTC); "OWNER DECISION 2026-09-25 00:20 Taipei"; TRIAGE1 §D: "PERF-D3 S1/S2 (post-release epoch)" | An unpublished prototype branch (five commits on L3's tree) and its patches | The post-release epoch, with the owner calls PERF-D3 lists (the API under the existing ruling, the restated gate count) | | S1b, the Pine lazy opening | The Pine host records what its skipped opening and its uncalled input callback would have written, at the bar's first host callback. Member functions only, no layout change | Measured with S1a. A quiet bar then asks 15 gates, not 30, so the quiet-bar cost check `CHECK(asked >= 20 * o.boundary.size())` (`tests/test_adapter_quiet_bar.cpp:1054`) must be restated | As S1a | As S1a. The in-script hash witnesses caught the prototype's one miss (`awaiting_legacy_script_open_ms_`) | As S1a | | S1c, the held-position wake band | S1 for bars that hold a position with nothing live, 41 % of the median public slot's bars. The host declares a price band, and the kernel calls the opening only when the bar's range leaves it. The adapter derives the band from its margin model and declines when it cannot bound it | PERF-D3, a measurement-only upper bound without the band, which is not sound in general: 100 slots 0.776 / 0.716 per slot, 14 probes 0.830 / 0.761, cumulative against L3 with S1, S2 and D2-A | As S1a; the owner call "Fund the price band" | An unpublished measurement-only script | As S1a. Size L, parity risk high: TradingView's margin money rules | @@ -183,7 +183,7 @@ them for the record only. "Linux aarch64" is the maintainers' Cortex-A725 host. | Compact journal records and a history ring buffer (L3) | Smaller command records and a ring buffer for the request core's history. `WorkingRequestCore::history` (`include/pineforge/native_order.hpp:1709`) hands out the whole vector and the core holds `history_` (`include/pineforge/native_order.hpp:2280`) inside `native_order_v7`, so both are a layout and API change | L3, Linux aarch64 (Cortex-A725), `perf` call graphs, inclusive shares: at most 3.68 % of bracket, 11.99 % of re-issue, 4.26 % of market and 3.42 % of the 14 probes (retiring the history alone: 1.64, 4.51, 2.53 and 1.00 %); only part is realisable, since records must still be written | "R5 L3 DONE + ACCEPTED 2026-09-24 22:10 Taipei", ruling 1: "DEFERRED to the PERF-D3 structural plan"; TRIAGE1 §D: "compact journal records" | Lane L3's code, merged by INT22 (#285); its profiles are unpublished | A `native_order` layout epoch | | A checked-values handoff for executions (L3) | `execution_values` runs twice, in `check_execution` (`src/native_order.cpp:4887`) and again in `apply_execution` (`src/native_order.cpp:4910`); handing the checked values across needs a public type, an API addition with no layout change | L3, as above: 2.21 % of bracket for the two together, about 1.1 % recoverable | L3 ruling 2: "a public checked-values handoff for executions (~1.1% bracket) -> DEFERRED" | As above | An owner decision on the API addition | | A chunked `trades_` store (L3) | Closed trades in chunks rather than one vector; `trades_` (`include/pineforge/engine.hpp:546`) is protected `BacktestEngine` state, so script ABI | L3, as above: on bracket, vector growth 0.64 %, `Trade` moves 0.74 % and `record_close_trade` 0.43 % (at most about 1.4 %); at most 0.35 % on the 14 probes | L3 ruling 4: "chunked trades_ store (ABI) -> DEFERRED" | As above | An `engine_script_run` epoch that changes the engine's protected layout | -| A zoned chart-day memo (D2-D finding 4) | On a chart with a timezone, `libc_chart_day_key` (`src/source/pine_adapter.cpp:17351`) takes the process timezone lock through `ScopedTimezone` and calls `localtime_r` on every bar, because `on_bar_open` (`src/source/pine_adapter.cpp:22250`) reads `chart_day_key` (`src/source/pine_adapter.cpp:22487`) unconditionally. The non-UTC monthly Sharpe/Sortino walk, `month_key_local` (`src/engine_metrics.cpp:33`), pays one `localtime_r` per equity point. UTC charts use D2-D's memo, `civil_chart_day_key` (`src/source/pine_adapter.cpp:17335`) | Not measured on a zoned chart. The UTC path cost 134 instructions a bar before D2-D's memo and 68 after (D2-D, Linux aarch64, callgrind on the floor). PERF-K1: `ScopedTimezone` copies the zone name, which allocates for names of 16 or more characters under libstdc++ | TRIAGE1 §D: "D2-D finding 4 (the timezone `localtime_r` memo)", under the owner's cap. D2-D's report: an exact zoned memo "would need the zone's transition table, which belongs in `timezone.cpp`" | None; the UTC memo's witness is `tests/test_chart_day_memo.cpp` | A lane that owns `src/timezone.cpp` and builds an exact zone-transition memo (PERF-K1's Sitka case shows why a shortcut is not exact), byte-identical, with the zoned keys of `test_chart_day_memo` still equal to the computed ones | +| A zoned chart-day memo (D2-D finding 4) | On a chart with a timezone, `libc_chart_day_key` (`src/source/pine_adapter.cpp:17353`) takes the process timezone lock through `ScopedTimezone` and calls `localtime_r` on every bar, because `on_bar_open` (`src/source/pine_adapter.cpp:22252`) reads `chart_day_key` (`src/source/pine_adapter.cpp:22489`) unconditionally. The non-UTC monthly Sharpe/Sortino walk, `month_key_local` (`src/engine_metrics.cpp:33`), pays one `localtime_r` per equity point. UTC charts use D2-D's memo, `civil_chart_day_key` (`src/source/pine_adapter.cpp:17337`) | Not measured on a zoned chart. The UTC path cost 134 instructions a bar before D2-D's memo and 68 after (D2-D, Linux aarch64, callgrind on the floor). PERF-K1: `ScopedTimezone` copies the zone name, which allocates for names of 16 or more characters under libstdc++ | TRIAGE1 §D: "D2-D finding 4 (the timezone `localtime_r` memo)", under the owner's cap. D2-D's report: an exact zoned memo "would need the zone's transition table, which belongs in `timezone.cpp`" | None; the UTC memo's witness is `tests/test_chart_day_memo.cpp` | A lane that owns `src/timezone.cpp` and builds an exact zone-transition memo (PERF-K1's Sitka case shows why a shortcut is not exact), byte-identical, with the zoned keys of `test_chart_day_memo` still equal to the computed ones | | Thread-local reads that remain per call | D2-C moved the three scopes' writes onto the pump's block, `pump_ambient` (`src/native_execution_consumer.hpp:496`). A reader still reaches the block through `tl_runtime_ambient` (`src/ta_extremes_volume.cpp:29`): `bar_context` (`src/ta_extremes_volume.cpp:82`), read by every `ExtremeRing::update` (`src/ta_extremes_volume.cpp:117`), which backs `ta.highest`, `ta.lowest`, `ta.highestbars` and `ta.lowestbars` and, through an embedded `Highest` and `Lowest`, `ta.stoch`, `ta.wpr` and `ta.range`; and `matching_day_partition` (`src/timeframe.cpp:628`), read by the default-anchored `ta.vwap` through `session_day_index`, by `crosses_boundary` on a DAY period (`timeframe.change("D")`) and by the session-period helpers. `decompose_ms_local` (`src/session_time.cpp:250`) reads two thread-locals per call on a zoned clock, and the prepared order storage's `free_blocks` (`src/native_order.cpp:620`) one per prepared mutation. `ema_na_warmup_flag` is read once per EMA, at its first compute | One thread-local access per reader call. AUDIT4, macOS arm64, a dlopen'd tutorial-MACD probe, counted by `_tlv_get_addr` breakpoints and by descriptor patching: 1.000 a bar for one `ta::Highest` and for one default `ta::VWAP`, 1.999 for one `pine_hour` on a New York clock, 0.000 on the no-order floor and with orders. On Linux a TLS-descriptor call costs about 20 instructions: D2-C's 13 → 4 calls a bar was 260 → 80 instructions (Linux aarch64, GCC 13, callgrind). INT23: 2.00 a bar on the zoned probe 064, 0.08 on probe 008 | None for the TA and day-partition readers: "RULING: INT23 ACCEPTED" (ledger, 2026-09-24 23:30 UTC) reads "dlopen TLS 13->0/bar", and INT23's report names only the timezone and order-core residuals. This row is their record | Unpublished (AUDIT4's scratch) | A byte-identical lane that hands these readers the pump's block without a thread-local read. `ta::bar_context()` is script ABI, so its public declaration stays as it is | | "audit-off mode", "LTO", "data-oriented layout" | Named in "OWNER DECISION 2026-09-25 00:20 Taipei" among the post-release structural items: "S1 bulk quiet bars, S2 per-strategy specialization, audit-off mode, LTO+PGO, data-oriented layout". No engine or campaign document defines, designs or measures the three | None | The owner decision | None | Struck from this inventory until a design note defines one of them | @@ -194,9 +194,9 @@ them for the record only. "Linux aarch64" is the maintainers' Cortex-A725 host. K4, the batch log pre-size. Since V19-B's step 6 (`08e71bae`) a run under the default `NativeEventRetention::Window` keeps no driver point and retires its journal at every script-bar boundary, so K4 has nothing to size. -`reserve_driver_log` (`src/native_execution_consumer.cpp:3243-3248`) returns +`reserve_driver_log` (`src/native_execution_consumer.cpp:3311-3316`) returns unless the retention is Full, and `presize_logs` -(`src/native_execution_consumer.cpp:10278-10289`) sizes no journal under Window. +(`src/native_execution_consumer.cpp:10350-10361`) sizes no journal under Window. `NativeRunSpec::event_retention` (`include/pineforge/native_run_spec.hpp:714`) defaults to Window, and the Pine adapter declares `NativeEventRetention::Window` (`src/source/pine_adapter.cpp:2996`). K4 still @@ -210,7 +210,7 @@ or more, and the same run under Full holds 66,000 points in one block of **PERF-L4 reads the receipts about three times a bar, not once.** PERF-L4's brief asked it to "read the receipts ONCE per bar instead of three times". The lane gated the read instead: `observe_terminal_receipts` -(`src/source/pine_adapter.cpp:6446`) returns at its receipt gate when no +(`src/source/pine_adapter.cpp:6448`) returns at its receipt gate when no command lies above its cursor. `every_gate_is_taken_both_ways` (`tests/test_adapter_quiet_bar.cpp:1061`) prints `gate ReceiptRead asked 40996 quiet 33552 live 7444`, against `asked 13248` diff --git a/scripts/check_native_cpp_versions.py b/scripts/check_native_cpp_versions.py index 0c143da21..e166ec219 100644 --- a/scripts/check_native_cpp_versions.py +++ b/scripts/check_native_cpp_versions.py @@ -37,12 +37,194 @@ "include/pineforge/native_order_identity.hpp", "include/pineforge/native_fx_curve.hpp", "src/native_fx_curve.cpp", + 'include/pineforge/native_bar_views.hpp', + 'src/native_bar_views.cpp', + 'src/source/pine_security_eval.cpp', + 'src/source/pine_strategy_host.cpp', + 'src/source/pine_adapter.cpp', + 'src/source/pine_scheduler_native.cpp', + 'src/source/magnifier_intrabars.cpp', ) NATIVE_FX_CURVE_HEADER = "include/pineforge/native_fx_curve.hpp" NATIVE_FX_CURVE_SOURCE = "src/native_fx_curve.cpp" NATIVE_FX_CURVE_NAMESPACE = "native_fx_curve_v1" +# CR-150: approved v1 sibling bridges, as narrow as the original v3 digest +# exception. Every other definition stays owned by its existing epoch. +NATIVE_BAR_VIEWS_HEADER = 'include/pineforge/native_bar_views.hpp' +NATIVE_BAR_VIEWS_SOURCE = 'src/native_bar_views.cpp' +NATIVE_BAR_VIEWS_NAMESPACE = 'native_bar_views_v1' +SPEC_VIEWS_BRIDGE = """inline namespace native_bar_views_v1 { + +NativeRunSpecValidation normalize_native_run_spec( + NativeRunSpec& spec, const NativeBarViews& views) noexcept { + const auto judged = native_run_spec_v3::validate_run_spec_views(spec, views); + if (!judged) return judged; + if (spec.fee_value == 0.0) spec.fee_value = 0.0; + return {}; +} + +NativeRunSpecValidation validate_native_instrument_feeds( + const std::vector& feeds, const NativeBarViews& views) noexcept { + try { + return native_run_spec_v3::instrument_feed_shapes(feeds, views); + } catch (const std::bad_alloc&) { + return {NativeRunSpecError::AllocationFailure, NativeRunSpecField::InstrumentFeedTimeframe}; + } catch (...) { + return {NativeRunSpecError::CalendarFailure, NativeRunSpecField::InstrumentFeedTimeframe}; + } +} + +NativeRunSpecValidation validate_native_timeframe_subscriptions( + const std::vector& subscriptions, + const std::string& input_tf, bool timeframe_undetected, + const std::optional& auxiliary_feed, + const std::vector& feeds, const NativeBarViews& views) noexcept { + return native_run_spec_v3::validate_subscription_views( + subscriptions, input_tf, timeframe_undetected, auxiliary_feed, feeds, views); +} + +std::uint64_t native_instrument_feeds_digest( + const std::vector& feeds, const NativeBarViews& views) noexcept { + return native_run_spec_v3::digest_instrument_feed_views(feeds, views); +} + +} // inline namespace native_bar_views_v1 +""" +CONSUMER_VIEWS_BRIDGE = """inline namespace native_bar_views_v1 { + +std::uint64_t native_run_spec_digest(const NativeRunSpec& spec, const NativeBarViews& views) { + NativeExecutionConsumer::SpecBarDigests digests; + digests.intrabar = native_bar_views_v1::native_intrabar_path_digest(spec.intrabar, views.lower_path); + if (!spec.subscriptions.empty()) + digests.subscriptions = native_timeframe_subscriptions_digest(spec.subscriptions); + if (spec.auxiliary_feed) + digests.auxiliary = native_auxiliary_feed_digest(*spec.auxiliary_feed); + if (!spec.instrument_feeds.empty()) + digests.instruments = native_bar_views_v1::native_instrument_feeds_digest(spec.instrument_feeds, views); + Fnv f; + f.run_base = spec.identity.run_number; + hash_spec(f, spec, &digests); + return f.h; +} + +} // inline namespace native_bar_views_v1 +""" + +# Every .bars/.close_ms and ->bars/->close_ms access in the named read set. +# Storage writes are included deliberately. L15b/L16 update their rows when +# introducing a caller/provider; an unlisted access fails before ABI fixtures. +BAR_VIEW_READS = { + 'src/native_bar_views.cpp': ( + 'return {borrowed->bars, borrowed->close_ms, borrowed->bars.size};', + 'return {{owned.bars.data(), owned.bars.size()},', + 'owned.close_ms.data(), owned.close_ms.size()};', + 'if (lower == nullptr || !lower->bars.empty())', + ), + 'src/native_run_spec.cpp': ( + 'const auto bars = data.bars;', + 'if ((borrowed != nullptr && !feed.bars.empty())', + 'if ((borrowed != nullptr && !feed.close_ms.empty())', + '|| (data.close_count != 0 && data.close_ms == nullptr))', + 'if (data.close_ms[i] <= bars.data[i].timestamp', + '|| (i + 1 < bars.size && data.close_ms[i] > bars.data[i + 1].timestamp)) {', + 'for (std::size_t i = 0; i < feed->bars.size(); ++i) {', + 'if (!native_bar_structurally_valid(feed->bars[i])) {', + 'if (i > 0 && feed->bars[i].timestamp <= feed->bars[i - 1].timestamp) {', + '&& (spec.intrabar.lower() == nullptr || !spec.intrabar.lower()->bars.empty()))', + 'u(lower->bars.size());', + 'for (const auto& bar : lower->bars) {', + 'u(feed.bars.size());', + 'for (const auto& bar : feed.bars) {', + 'u(data.bars.size);', + 'for (std::size_t k = 0; k < data.bars.size; ++k) {', + 'const Bar& bar = data.bars.data[k];', + 'i(k < data.close_count ? data.close_ms[k] : 0);', + ), + 'src/native_execution_consumer.hpp': ( + ), + 'src/native_execution_consumer.cpp': ( + 'begin_bars_ = args.bars;', + 'descriptor->bars.data = nullptr;', + 'descriptor->close_ms = nullptr;', + 'if (feed.seconds == kSecPerDay && !feed.bars.empty()) {', + 'daily = &feed.bars;', + 'if (!intrabar_tf_ || lower.bars.size() > static_cast(std::numeric_limits::max())) {', + 'path_spec, lower.bars.empty() ? nullptr : lower.bars.data(),', + 'static_cast(lower.bars.size()), NativeInputPolicy::Batch);', + 'lower->bars.begin(), lower->bars.end(), begin,', + 'for (; candidate != lower->bars.end() && candidate->timestamp < end; ++candidate) {', + 'spec && spec->auxiliary_feed ? spec->auxiliary_feed->bars.size() : 0;', + 'const auto& declared = spec_ptr()->auxiliary_feed->bars;', + 'const std::size_t total = feed.bars.size;', + '? feed.bars.data[at].timestamp <= horizon.open_ms', + ': feed.close_ms[at] <= horizon.last_traded_close_ms;', + 'subscription.latest = feed.bars.data[at];', + 'context.interval.open_ms = feed.bars.data[at].timestamp;', + 'context.interval.eligible_open_ms = feed.bars.data[at].timestamp;', + 'context.interval.last_traded_close_ms = feed.close_ms[at];', + 'context.interval.next_period_open_ms = feed.close_ms[at];', + 'at + 1 < total ? feed.bars.data[at + 1].timestamp : feed.close_ms[at];', + 'if (!invoke_timeframe_callback(engine, feed.bars.data[at], context)) return false;', + ), + 'src/source/pine_security_eval.cpp': ( + 'feed.bars = held.bars;', + 'feed.close_ms = held.close_ms;', + 'site.close_ms = 0;', + 'site.close_ms = close_ms;', + 'while (static_cast(site.delivered) < feed.bars.size) {', + 'const bool visible = site.lookahead ? feed.bars.data[at].timestamp <= chart_bar.timestamp', + ': feed.close_ms[at] <= chart_close;', + 'run_foreign_site_bar(site, feed.bars.data[at], feed.close_ms[at]);', + 'feed.bars.assign(bars, bars + n);', + 'feed.close_ms.assign(close_ms, close_ms + n);', + 'if (n < 0 || static_cast(n) != data.bars.size || (n > 0 && values == nullptr))', + '+ " values for " + std::to_string(data.bars.size) + " bars");', + 'f.u(native_instrument_feed_view(feed, nullptr).bars.size);', + ), + 'src/source/pine_strategy_host.cpp': ( + 'if (args.n > 0 && args.bars == nullptr) {', + 'const Bar& bar = args.bars[i];', + 'const std::int64_t previous = args.bars[i - 1].timestamp;', + '&& (!std::isfinite(args.bars[args.n - 1].close)', + '|| args.bars[args.n - 1].close <= 0.0)) {', + 'lent[i].bars.swap(store[i].bars);', + 'lent[i].close_ms.swap(store[i].close_ms);', + 'store[i].bars.swap(lent[i].bars);', + 'store[i].close_ms.swap(lent[i].close_ms);', + '{feed.instrument, feed.tf, data.bars.size, feed.columns.size()});', + 'if (effective_input.empty() && args.n >= 2 && args.bars != nullptr)', + 'effective_input = detect_timeframe(args.bars, args.n);', + ), + 'src/source/pine_adapter.cpp': ( + 'if (effective_input.empty() && args.n >= 2 && args.bars != nullptr) {', + 'effective_input = detect_timeframe(args.bars, args.n);', + 'if (args.bars && args.n > 0) {', + 'path.bars = tradingview_magnifier_bars(', + 'args.bars, args.n, spec.input_tf, spec.script_tf, spec.session,', + 'const bool inactive_sampler_path = inert != nullptr && inert->bars.empty()', + ), + 'src/source/pine_scheduler_native.cpp': ( + 'if (args.bars && args.n > 0) next.bars.assign(args.bars, args.bars + args.n);', + 'input_script_completes_.assign(retained_.bars.size(), 1U);', + 'input_script_boundary_completes_.assign(retained_.bars.size(), 0U);', + 'retained_.bars.data(), retained_.bars.size(), input_script_completes_,', + 'selected_executed_source_bars(host, state.spec, window, retained_.bars);', + 'host.scheduler_prepare_script_run(retained_.bars, static_eligible,', + 'host.scheduler_prepare_security_sequence(retained_.bars);', + 'lower->bars.begin(), lower->bars.end(), context.sub_bar_open_ms,', + 'if (found == lower->bars.end() || found->timestamp != context.sub_bar_open_ms)', + 'const auto& path = spec->intrabar.lower()->bars;', + '&& context.input_index + 1 < static_cast(retained_.bars.size())) {', + 'next_input_ms = retained_.bars[static_cast(context.input_index + 1)].timestamp;', + '&& !retained_.bars.empty() && calendar_period_for(retained_.script_tf) == CalendarPeriod::DAY', + 'const auto next_stamp = session_period_open_ms(retained_.bars.back().timestamp + 86400000LL,', + ), + 'src/source/magnifier_intrabars.cpp': ( + ), +} + DRIVER_FORWARD = ( "inline namespace native_run_spec_v3 { struct NativeRunSpec; }" ) @@ -165,7 +347,10 @@ def consumer_epoch_source(files): "native_run_spec_v3 spec fold, seeded like continuation_hash()") if "native_run_spec_digest" not in files["include/pineforge/native_run_spec.hpp"]: raise ValueError("native_run_spec_digest must be declared in native_run_spec_v3") - epoch = versioned(text.replace(CONSUMER_SPEC_DIGEST, "", 1), + if text.count(CONSUMER_VIEWS_BRIDGE) != 1: + raise ValueError('context spec digest must preserve its exact v1 bridge') + epoch = versioned(text.replace(CONSUMER_SPEC_DIGEST, "", 1) + .replace(CONSUMER_VIEWS_BRIDGE, "", 1), "pineforge", "engine_script_run_v19") if "native_run_spec_digest" in epoch: raise ValueError("native_run_spec_digest does not belong to engine_script_run_v19") @@ -325,8 +510,97 @@ def authenticate_historical_host_manifests(root=ROOT, providers=PROVIDERS): return manifests +def check_native_bar_views(files): + header = clean(files[NATIVE_BAR_VIEWS_HEADER]) + value = versioned(header, 'pineforge', NATIVE_BAR_VIEWS_NAMESPACE) + same_names(header, value, TYPE_DEF, 'bar-view public type', NATIVE_BAR_VIEWS_NAMESPACE) + if namespace_functions(header) != namespace_functions(value): + raise ValueError('bar-view functions must belong to native_bar_views_v1') + require(value, ('NativeBarView', 'NativeInstrumentFeedView', + 'NativeInstrumentFeedReadView', 'NativeBarViews', 'NativeLowerPathSource'), + NATIVE_BAR_VIEWS_NAMESPACE, r'\b(?:struct|class)\s+NAME\s*\{') + shapes = ( + ('NativeBarView', 'constBar*data;std::size_tsize;'), + ('NativeInstrumentFeedView', 'NativeBarViewbars;conststd::int64_t*close_ms;'), + ('NativeInstrumentFeedReadView', + 'NativeBarViewbars;conststd::int64_t*close_ms;std::size_tclose_count;'), + ('NativeBarViews', 'constNativeInstrumentFeedView*const*instrument_feeds=nullptr;' + 'std::size_tinstrument_feed_count=0;constNativeLowerPathSource*lower_path=nullptr;' + 'NativeBarViewlower_segment{nullptr,0};'), + ) + for name, expected in shapes: + shape = body(value, r'\bstruct\s+' + name + r'\s*\{', name) + if re.sub(r'\s+', '', shape) != expected: + raise ValueError(name + ' must preserve its frozen v1 fields') + functions = ('native_instrument_feed_view', 'normalize_native_run_spec', + 'validate_native_instrument_feeds', 'validate_native_timeframe_subscriptions', + 'native_instrument_feeds_digest', 'native_run_spec_digest', + 'native_intrabar_path_digest') + require_namespace_functions(value, functions, NATIVE_BAR_VIEWS_NAMESPACE) + accessor = re.search(r'native_instrument_feed_view\s*\([^;]+;', value, re.S) + if accessor is None or '=' in accessor.group(0): + raise ValueError('bar accessor must require its explicit override argument') + for forbidden in ('stage_instrument_feed_views', 'stage_lower_path_source', + 'NativeStrategyHost', 'NativeExecutionConsumer'): + if forbidden in header: + raise ValueError('bar-view staging/query belongs to the private consumer: ' + forbidden) + source = versioned(files[NATIVE_BAR_VIEWS_SOURCE], 'pineforge', NATIVE_BAR_VIEWS_NAMESPACE) + require_namespace_functions(source, ('native_instrument_feed_view', 'native_intrabar_path_digest'), + NATIVE_BAR_VIEWS_NAMESPACE) + for path, expected in BAR_VIEW_READS.items(): + actual = tuple(line.strip() for line in clean(files[path]).splitlines() + if re.search(r'(?:\.|->)(?:bars|close_ms)\b', line)) + if actual != expected: + added = [line for line in actual if line not in expected] + missing = [line for line in expected if line not in actual] + raise ValueError(path + ': bar/close access inventory drift; added=' + + repr(added) + '; missing=' + repr(missing)) + routes = { + 'src/native_execution_consumer.cpp': ( + 'candidate, views', 'bar_views());', 'spec->instrument_feeds[subscription.instrument_feed], slot'), + 'src/source/pine_security_eval.cpp': ( + 'native_instrument_feed_view(installed_symbol_feed(site.feed), slot)', + 'native_instrument_feed_view(feed, nullptr)'), + 'src/source/pine_strategy_host.cpp': ( + 'native_instrument_feed_view(feed, slot)', 'stage_instrument_feed_views(nullptr, 0)'), + 'src/source/pine_adapter.cpp': ('normalize_projected_spec(spec)',), + } + for path, tokens in routes.items(): + for token in tokens: + if token not in clean(files[path]): + raise ValueError(path + ': missing view route: ' + token) + + +def self_test_bar_views(): + files = load(ROOT) + check_native_bar_views(files) + mutations = ( + (NATIVE_BAR_VIEWS_HEADER, 'inline namespace native_bar_views_v1', + 'inline namespace native_bar_views_v2'), + (NATIVE_BAR_VIEWS_HEADER, 'const NativeInstrumentFeedView* borrowed) noexcept;', + 'const NativeInstrumentFeedView* borrowed = nullptr) noexcept;'), + (NATIVE_BAR_VIEWS_HEADER, 'const NativeLowerPathSource* lower_path', + 'NativeLowerPathSource* lower_path'), + ('src/source/pine_adapter.cpp', 'normalize_projected_spec(spec)', + 'validate_native_run_spec(spec)'), + ('src/source/pine_security_eval.cpp', 'native_instrument_feed_view(feed, nullptr)', + 'native_instrument_feed_view(feed, nullptr).bars'), + ) + for path, before, after in mutations: + if before not in files[path]: + raise ValueError('bar-view self-test has a stale mutation: ' + before) + changed = dict(files) + changed[path] = files[path].replace(before, after, 1) + try: + check_native_bar_views(changed) + except ValueError: + continue + raise ValueError('bar-view guard accepted mutation: ' + before) + + def check_texts(files): check_native_fx_curve(files) + check_native_bar_views(files) identity = versioned(files[FILES[11]], "pineforge::native_order", "native_order_v1") require(identity, ("RunIdentity", "RequestHandle", "Birth"), "native_order_v1", r'\b(?:class|struct)\s+NAME\s*\{') @@ -557,7 +831,10 @@ def check_texts(files): 'LegacyTolerant = FeedTolerant,'): if alias not in spec: raise ValueError('native_run_spec_v3 dropped a deprecated alias: ' + alias) - spec_src = versioned(files[FILES[5]], "pineforge", "native_run_spec_v3") + if files[FILES[5]].count(SPEC_VIEWS_BRIDGE) != 1: + raise ValueError('context spec validation/digest must preserve their exact v1 bridge') + spec_src = versioned(files[FILES[5]].replace(SPEC_VIEWS_BRIDGE, '', 1), + "pineforge", "native_run_spec_v3") require_namespace_functions( spec_src, ("validate_native_run_spec", "normalize_native_run_spec", "validate_native_timeframe_subscriptions", @@ -1163,12 +1440,16 @@ def check(root=ROOT): if __name__ == "__main__": + if sys.argv[1:] == ['--self-test-bar-views']: + self_test_bar_views() + print('bar-view ownership, explicit slots and read inventory mutations ... OK') + raise SystemExit(0) if sys.argv[1:] == ['--self-test-tempdir']: self_test_tempdir() print('historical provider extraction stays outside the repository ... OK') raise SystemExit(0) if len(sys.argv) != 1: - raise SystemExit('usage: check_native_cpp_versions.py [--self-test-tempdir]') + raise SystemExit('usage: check_native_cpp_versions.py [--self-test-tempdir | --self-test-bar-views]') absent = missing_private_fixtures() if absent: # R2-F4: the text half reads only public sources, so it runs in the diff --git a/src/native_bar_views.cpp b/src/native_bar_views.cpp new file mode 100644 index 000000000..8c1dd9019 --- /dev/null +++ b/src/native_bar_views.cpp @@ -0,0 +1,68 @@ +#include +#include + +#include + +namespace pineforge { +inline namespace native_bar_views_v1 { + +NativeInstrumentFeedReadView native_instrument_feed_view( + const NativeInstrumentFeed& owned, + const NativeInstrumentFeedView* borrowed) noexcept { + if (borrowed != nullptr) + return {borrowed->bars, borrowed->close_ms, borrowed->bars.size}; + return {{owned.bars.data(), owned.bars.size()}, + owned.close_ms.data(), owned.close_ms.size()}; +} + +std::uint64_t native_intrabar_path_digest( + const IntrabarPath& path, const NativeLowerPathSource* source) { + if (source == nullptr) + return native_run_spec_v3::native_intrabar_path_digest(path); + const auto* lower = path.lower(); + if (lower == nullptr || !lower->bars.empty()) + throw coded(RunFailureCode::engine_invariant, {}, + "lower source lost its metadata"); + struct Fold { + std::uint64_t h = UINT64_C(1469598103934665603); + std::size_t seen = 0; + std::size_t count = 0; + void bytes(const void* data, std::size_t n) noexcept { + const auto* at = static_cast(data); + for (std::size_t i = 0; i < n; ++i) { + h ^= at[i]; + h *= UINT64_C(1099511628211); + } + } + void u(std::uint64_t value) noexcept { bytes(&value, sizeof(value)); } + void i(std::int64_t value) noexcept { bytes(&value, sizeof(value)); } + void d(double value) noexcept { bytes(&value, sizeof(value)); } + } fold; + fold.u(path.value.index()); + fold.u(lower->tf.size()); + fold.bytes(lower->tf.data(), lower->tf.size()); + fold.i(lower->samples); + fold.u(static_cast(lower->distribution)); + fold.u(lower->volume_weighted ? 1u : 0u); + fold.i(lower->volume_weighted_min_samples); + fold.i(lower->volume_weighted_max_samples); + fold.u(static_cast(lower->sample_eligibility)); + fold.count = source->count(); + fold.u(fold.count); + const bool complete = source->for_each( + [](void* state, const Bar& bar) noexcept { + auto& f = *static_cast(state); + if (f.seen == f.count) return false; + ++f.seen; + f.d(bar.open); f.d(bar.high); f.d(bar.low); f.d(bar.close); + f.d(bar.volume); f.i(bar.timestamp); + return true; + }, &fold); + if (!complete || fold.seen != fold.count) + throw coded(RunFailureCode::engine_invariant, {}, + "lower source count and walk disagree"); + return fold.h; +} + +} // inline namespace native_bar_views_v1 +} // namespace pineforge diff --git a/src/native_execution_consumer.cpp b/src/native_execution_consumer.cpp index e21d17b0e..8212ee702 100644 --- a/src/native_execution_consumer.cpp +++ b/src/native_execution_consumer.cpp @@ -1706,6 +1706,11 @@ bool NativeExecutionConsumer::admit_public_begin(BacktestEngine& engine, const c present_refusal(engine, not_ready_text); return false; } + if (detached_bar_views_) { + present_refusal(engine, "native Ready bar binding was detached", + RunFailureCode::engine_invariant); + return false; + } return true; } @@ -1759,6 +1764,7 @@ bool NativeExecutionConsumer::prepare_public_begin( try { host->prepare_native_begin(args); } catch (const std::exception& e) { + pending_bar_views_ = {}; preparing_begin_ = false; // A provider's begin-time validation is a public-entry refusal. It // has not started a native run or consumed an identity, so preserve a @@ -1767,6 +1773,7 @@ bool NativeExecutionConsumer::prepare_public_begin( render_exception(engine, e); return false; } catch (...) { + pending_bar_views_ = {}; preparing_begin_ = false; render_unclassified(engine, "native pre-begin provider exception"); return false; @@ -1893,6 +1900,48 @@ void NativeExecutionConsumer::refuse_source_mutation(const char* operation) { (operation ? operation : "")); } +void NativeExecutionConsumer::stage_instrument_feed_views( + const NativeInstrumentFeedView* const* views, std::size_t count) { + if (count != 0 && views == nullptr) + throw coded(RunFailureCode::engine_invariant, {}, + "nonempty feed view table is null"); + std::vector> descriptors(count); + std::vector slots(count, nullptr); + for (std::size_t i = 0; i < count; ++i) { + if (views[i] == nullptr) continue; + descriptors[i] = *views[i]; + slots[i] = &*descriptors[i]; + } + pending_bar_views_.descriptors.swap(descriptors); + pending_bar_views_.slots.swap(slots); +} + +void NativeExecutionConsumer::stage_lower_path_source( + std::unique_ptr source) noexcept { + pending_bar_views_.lower_path = std::move(source); +} + +NativeBarViews NativeExecutionConsumer::bar_views() const noexcept { + if (!std::holds_alternative(state_) && !running()) return {}; + return active_bar_views_.context(); +} + +void NativeExecutionConsumer::detach_bar_views() noexcept { + detached_bar_views_ = detached_bar_views_ || active_bar_views_.borrowed(); + // Keep only borrowed row counts in the copied descriptors. Their data + // pointers and table are inaccessible after detachment; owned counts stay + // in the retained spec. Component digests already include these counts. + for (auto& descriptor : active_bar_views_.descriptors) { + if (!descriptor) continue; + descriptor->bars.data = nullptr; + descriptor->close_ms = nullptr; + } + active_bar_views_.slots.clear(); + active_bar_views_.lower_path.reset(); + active_bar_views_.lower_segment = {nullptr, 0}; + pending_bar_views_ = {}; +} + bool NativeExecutionConsumer::stage_account_currency_fx_series( const std::vector& timestamps, const std::vector& rates) { if (timestamps.size() != rates.size()) return false; @@ -1926,13 +1975,14 @@ bool NativeExecutionConsumer::stage_account_currency_fx_series( const NativeExecutionConsumer::SpecBarDigests& NativeExecutionConsumer::spec_bar_digests( const NativeRunSpec& spec) const noexcept { if (!spec_bar_digests_) { - SpecBarDigests digests; - digests.intrabar = native_intrabar_path_digest(spec.intrabar); + SpecBarDigests digests = bound_bar_components_.value_or(SpecBarDigests{}); + if (!bound_bar_components_) + digests.intrabar = native_intrabar_path_digest(spec.intrabar); if (!spec.subscriptions.empty()) digests.subscriptions = native_timeframe_subscriptions_digest(spec.subscriptions); if (spec.auxiliary_feed) digests.auxiliary = native_auxiliary_feed_digest(*spec.auxiliary_feed); - if (!spec.instrument_feeds.empty()) + if (!bound_bar_components_ && !spec.instrument_feeds.empty()) digests.instruments = native_instrument_feeds_digest(spec.instrument_feeds); spec_bar_digests_ = digests; } @@ -2647,6 +2697,13 @@ NativeSetupResult NativeExecutionConsumer::configure(BacktestEngine& engine, NativeSetupResult NativeExecutionConsumer::configure_spec(BacktestEngine& engine, const NativeRunSpec* borrowed, NativeRunSpec* owned) { + // Every exit consumes pending, including a phase refusal or a digest + // exception. The active binding is changed only at the Applied commit. + struct DiscardPending { + BarViewBinding& pending; + ~DiscardPending() { pending = {}; } + } discard{pending_bar_views_}; + const NativeBarViews views = pending_bar_views_.context(); NativeSetupResult result; const NativeRunSpec* prior_spec = nullptr; // A refusal for the host's phase judges no field: it is WrongPhase at @@ -2682,7 +2739,7 @@ NativeSetupResult NativeExecutionConsumer::configure_spec(BacktestEngine& engine } std::optional copied; NativeRunSpec& candidate = owned != nullptr ? *owned : copied.emplace(*borrowed); - const auto validation = normalize_native_run_spec(candidate); + const auto validation = native_bar_views_v1::normalize_native_run_spec(candidate, views); result.validation = validation; if (!validation) { fail(engine, NativeFailure{NativeFailureCode::InvalidSpecification, @@ -2710,6 +2767,13 @@ NativeSetupResult NativeExecutionConsumer::configure_spec(BacktestEngine& engine return result; } } + SpecBarDigests components; + components.intrabar = native_bar_views_v1::native_intrabar_path_digest( + candidate.intrabar, views.lower_path); + if (!candidate.instrument_feeds.empty()) + components.instruments = native_bar_views_v1::native_instrument_feeds_digest( + candidate.instrument_feeds, views); + const std::size_t lower_count = views.lower_path == nullptr ? 0 : views.lower_path->count(); auto parsed_session = native_calendar::parse_session(candidate.session, candidate.timezone); if (!parsed_session) { fail(engine, NativeFailure{NativeFailureCode::Calendar, NativeFailureOperation::Configure}); @@ -2751,6 +2815,10 @@ NativeSetupResult NativeExecutionConsumer::configure_spec(BacktestEngine& engine // persists across a completed/aborted handle and is reapplied by the next // provider begin, so it must survive that provider's configure call. if (!staged_ingress_fx_) staged_fx_curve_.reset(); + active_bar_views_ = std::move(pending_bar_views_); + active_bar_views_.lower_count = lower_count; + bound_bar_components_ = components; + detached_bar_views_ = false; forget_spec_digests(); leave_running(); state_ = NativeReady{std::move(candidate)}; @@ -9465,9 +9533,9 @@ NativeSetupResult NativeExecutionConsumer::declare_timeframe_subscriptions( result.validation = {NativeRunSpecError::WrongPhase, NativeRunSpecField::None}; return result; } - result.validation = validate_native_timeframe_subscriptions( + result.validation = native_bar_views_v1::validate_native_timeframe_subscriptions( declared, running->spec.input_tf, running->spec.timeframe_undetected, - running->spec.auxiliary_feed, running->spec.instrument_feeds); + running->spec.auxiliary_feed, running->spec.instrument_feeds, bar_views()); if (!result.validation) return result; running->spec.subscriptions = std::move(declared); forget_spec_digests(); @@ -9490,9 +9558,9 @@ NativeSetupResult NativeExecutionConsumer::declare_auxiliary_feed( result.validation = {NativeRunSpecError::WrongPhase, NativeRunSpecField::None}; return result; } - result.validation = validate_native_timeframe_subscriptions( + result.validation = native_bar_views_v1::validate_native_timeframe_subscriptions( running->spec.subscriptions, running->spec.input_tf, - running->spec.timeframe_undetected, declared, running->spec.instrument_feeds); + running->spec.timeframe_undetected, declared, running->spec.instrument_feeds, bar_views()); if (!result.validation) return result; running->spec.auxiliary_feed = std::move(declared); forget_spec_digests(); @@ -9938,7 +10006,11 @@ bool NativeExecutionConsumer::pump_instrument_subscription( render(engine, "native instrument series lost its feed"); return false; } - const NativeInstrumentFeed& feed = spec->instrument_feeds[subscription.instrument_feed]; + const auto context = bar_views(); + const auto* slot = context.instrument_feed_count == 0 + ? nullptr : context.instrument_feeds[subscription.instrument_feed]; + const auto feed = native_instrument_feed_view( + spec->instrument_feeds[subscription.instrument_feed], slot); // The input's own calendar interval. A raw label partition presents every // input as a zero-length interval (timestamp_partition), which states no // close to judge a feed bar against, so the rule reads the interval the @@ -9952,29 +10024,29 @@ bool NativeExecutionConsumer::pump_instrument_subscription( horizon = *resolved; } } - const std::size_t total = feed.bars.size(); + const std::size_t total = feed.bars.size; bool delivered = false; while (subscription.instrument_cursor < total) { const std::size_t at = subscription.instrument_cursor; const bool visible = subscription.lookahead - ? feed.bars[at].timestamp <= horizon.open_ms + ? feed.bars.data[at].timestamp <= horizon.open_ms : feed.close_ms[at] <= horizon.last_traded_close_ms; if (!visible) break; ++subscription.instrument_cursor; delivered = true; // The pull accessor answers with this bar for the whole callback. - subscription.latest = feed.bars[at]; + subscription.latest = feed.bars.data[at]; NativeTimeframeBarContext context; context.subscription = subscription.index; - context.interval.open_ms = feed.bars[at].timestamp; - context.interval.eligible_open_ms = feed.bars[at].timestamp; + context.interval.open_ms = feed.bars.data[at].timestamp; + context.interval.eligible_open_ms = feed.bars.data[at].timestamp; context.interval.last_traded_close_ms = feed.close_ms[at]; context.interval.next_period_open_ms = feed.close_ms[at]; context.interval.next_input_open_ms = - at + 1 < total ? feed.bars[at + 1].timestamp : feed.close_ms[at]; + at + 1 < total ? feed.bars.data[at + 1].timestamp : feed.close_ms[at]; context.completion = NativeCompletionKind::Confirmed; context.delivered_at_ms = bar.timestamp; - if (!invoke_timeframe_callback(engine, feed.bars[at], context)) return false; + if (!invoke_timeframe_callback(engine, feed.bars.data[at], context)) return false; } // gaps: the subscription contract's rule, on the pull side. An instrument // series has no evaluator of the kernel's to clear on the push side; its @@ -10517,6 +10589,11 @@ bool NativeExecutionConsumer::stream_begin(BacktestEngine& engine, args.warmup_n = n_warmup; if (!prepare_public_begin(engine, args)) return false; if (!admit_public_begin(engine, "native stream_begin requires Ready")) return false; + if (active_bar_views_.lower_path) { + present_refusal(engine, "native lower source cannot begin a stream", + RunFailureCode::engine_invariant); + return false; + } clear_rendered(engine); engine.last_run_status_ = 0; engine.abort_requested_.store(false, std::memory_order_relaxed); @@ -12735,4 +12812,23 @@ uint64_t native_run_spec_digest(const NativeRunSpec& spec) noexcept { } } // inline namespace native_run_spec_v3 + +inline namespace native_bar_views_v1 { + +std::uint64_t native_run_spec_digest(const NativeRunSpec& spec, const NativeBarViews& views) { + NativeExecutionConsumer::SpecBarDigests digests; + digests.intrabar = native_bar_views_v1::native_intrabar_path_digest(spec.intrabar, views.lower_path); + if (!spec.subscriptions.empty()) + digests.subscriptions = native_timeframe_subscriptions_digest(spec.subscriptions); + if (spec.auxiliary_feed) + digests.auxiliary = native_auxiliary_feed_digest(*spec.auxiliary_feed); + if (!spec.instrument_feeds.empty()) + digests.instruments = native_bar_views_v1::native_instrument_feeds_digest(spec.instrument_feeds, views); + Fnv f; + f.run_base = spec.identity.run_number; + hash_spec(f, spec, &digests); + return f.h; +} + +} // inline namespace native_bar_views_v1 } // namespace pineforge diff --git a/src/native_execution_consumer.hpp b/src/native_execution_consumer.hpp index 0c17c18ae..2de9f1a63 100644 --- a/src/native_execution_consumer.hpp +++ b/src/native_execution_consumer.hpp @@ -1694,6 +1694,29 @@ class NativeExecutionConsumer final : public IExecutionConsumer { native_calendar::Timeframe script_tf_{}; std::optional intrabar_tf_; native_calendar::TimeframeCompatibility pairing_{}; + // Representation-only bindings: continuation identity folds their logical + // content through bound_bar_components_, never pointer addresses or tags. + struct BarViewBinding { + std::vector> descriptors; + std::vector slots; + std::unique_ptr lower_path; + NativeBarView lower_segment{nullptr, 0}; + std::size_t lower_count = 0; // represented by the lower component digest + NativeBarViews context() const noexcept { + return {slots.empty() ? nullptr : slots.data(), slots.size(), + lower_path.get(), lower_segment}; + } + bool borrowed() const noexcept { + if (lower_path) return true; + for (const auto* slot : slots) if (slot != nullptr) return true; + return false; + } + }; + BarViewBinding pending_bar_views_; + BarViewBinding active_bar_views_; + std::optional bound_bar_components_; + bool detached_bar_views_ = false; + void detach_bar_views() noexcept; std::optional staged_fx_curve_; bool staged_ingress_fx_ = false; bool in_callback_ = false; @@ -2262,13 +2285,14 @@ class NativeExecutionConsumer final : public IExecutionConsumer { // it is destroyed after the AttemptScope and every pump and callback frame // have unwound. The constructor records, before the entry runs anything, // whether the handle is tracked, whether this call is nested under an - // attempt, a begin, a callback, an input or a dispatch (meaningful for a - // tracked handle only), and std::uncaught_exceptions(). `allow_live` lets a + // attempt, a begin, a callback, an input or a dispatch, and (for a tracked + // handle) std::uncaught_exceptions(). `allow_live` lets a // NORMAL return that leaves the run Running keep its generation open: true // for stream_begin and the pushes and the time advance, false for the batch // runs and stream_end. The destructor is noexcept and renders, dispatches, - // allocates and moves a NativeRunSpec never. It does nothing for an untracked - // handle or a nested call. Otherwise it closes an epoch that is STILL + // allocates and moves a NativeRunSpec never. Its selected-epoch half does + // nothing for an untracked handle or nested call; outer run exits always + // detach borrowed bar bindings. Otherwise it closes an epoch that is STILL // Executing or Capturing (close_open_epoch_at_exit) and leaves a Sealed or // Results one alone, so a refused attempt that merely latched NativeFailed // before admission never rewrites an earlier generation. @@ -2286,13 +2310,15 @@ class NativeExecutionConsumer final : public IExecutionConsumer { bool allow_live) noexcept : c_(c), engine_(engine), tracked_(c.selected_epoch_tracked()), uncaught_(tracked_ ? std::uncaught_exceptions() : 0), - nested_(tracked_ && (c.in_attempt_call_ || c.preparing_begin_ || c.in_callback_ - || c.processing_input_ || c.selected_epoch_.dispatching)), + nested_(c.in_attempt_call_ || c.preparing_begin_ || c.in_callback_ + || c.processing_input_ || c.selected_epoch_.dispatching), allow_live_(allow_live) {} ~EntryExitGuard() { - if (!tracked_ || nested_) return; - c_.close_open_epoch_at_exit(engine_, std::uncaught_exceptions() > uncaught_, - allow_live_); + if (nested_) return; + if (tracked_) + c_.close_open_epoch_at_exit(engine_, std::uncaught_exceptions() > uncaught_, + allow_live_); + if (!allow_live_ || !c_.running()) c_.detach_bar_views(); } EntryExitGuard(const EntryExitGuard&) = delete; EntryExitGuard& operator=(const EntryExitGuard&) = delete; @@ -2398,6 +2424,12 @@ class NativeExecutionConsumer final : public IExecutionConsumer { bool calc_slot_at(std::int64_t at_ms, native_calc::Slot& slot) const; public: + // Projection judges the host's pending context without publishing it via + // bar_views(), which is active-only. No installed host surface changes. + NativeRunSpecValidation normalize_projected_spec(NativeRunSpec& spec) const noexcept { + return native_bar_views_v1::normalize_native_run_spec(spec, pending_bar_views_.context()); + } + // CR-150 V1/V2, CORE-TOP rev 6 amendment (10-18 19:24), changes 1-2: // staging only replaces pending descriptors/source. It never judges the // lifecycle or changes active state; only an applied configure binds and diff --git a/src/native_run_spec.cpp b/src/native_run_spec.cpp index d2b69f877..f4116977d 100644 --- a/src/native_run_spec.cpp +++ b/src/native_run_spec.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -232,7 +233,11 @@ bool valid_instrument_bar(const Bar& bar) noexcept { // The installed instrument feeds, judged without a calendar: each key's // strings and literal, one feed per key, and each feed's bars, closes and // columns. Parsing a literal may allocate; the callers convert that. -Result instrument_feed_shapes(const std::vector& feeds) { +Result instrument_feed_shapes(const std::vector& feeds, + const NativeBarViews& views) { + if (views.instrument_feed_count != 0 + && (views.instrument_feeds == nullptr || views.instrument_feed_count != feeds.size())) + return {Error::InvalidInstrumentFeedBar, Field::InstrumentFeedBars}; std::vector keys; keys.reserve(feeds.size()); for (std::size_t f = 0; f < feeds.size(); ++f) { @@ -251,18 +256,26 @@ Result instrument_feed_shapes(const std::vector& feeds) { return {Error::DuplicateInstrumentFeed, Field::InstrumentFeedTimeframe}; } keys.push_back(*parsed); - const auto& bars = feed.bars; - for (std::size_t i = 0; i < bars.size(); ++i) { - if (!valid_instrument_bar(bars[i])) + const auto* borrowed = views.instrument_feed_count == 0 + ? nullptr : views.instrument_feeds[f]; + const auto data = native_instrument_feed_view(feed, borrowed); + const auto bars = data.bars; + if ((borrowed != nullptr && !feed.bars.empty()) + || (bars.size != 0 && bars.data == nullptr)) + return {Error::InvalidInstrumentFeedBar, Field::InstrumentFeedBars}; + for (std::size_t i = 0; i < bars.size; ++i) { + if (!valid_instrument_bar(bars.data[i])) return {Error::InvalidInstrumentFeedBar, Field::InstrumentFeedBars}; - if (i > 0 && bars[i].timestamp <= bars[i - 1].timestamp) + if (i > 0 && bars.data[i].timestamp <= bars.data[i - 1].timestamp) return {Error::UnorderedInstrumentFeedBars, Field::InstrumentFeedBars}; } - if (feed.close_ms.size() != bars.size()) + if ((borrowed != nullptr && !feed.close_ms.empty()) + || data.close_count != bars.size + || (data.close_count != 0 && data.close_ms == nullptr)) return {Error::InvalidInstrumentFeedClose, Field::InstrumentFeedClose}; - for (std::size_t i = 0; i < bars.size(); ++i) { - if (feed.close_ms[i] <= bars[i].timestamp - || (i + 1 < bars.size() && feed.close_ms[i] > bars[i + 1].timestamp)) { + for (std::size_t i = 0; i < bars.size; ++i) { + if (data.close_ms[i] <= bars.data[i].timestamp + || (i + 1 < bars.size && data.close_ms[i] > bars.data[i + 1].timestamp)) { return {Error::InvalidInstrumentFeedClose, Field::InstrumentFeedClose}; } } @@ -273,7 +286,7 @@ Result instrument_feed_shapes(const std::vector& feeds) { !name) { return {Error::InvalidInstrumentFeedColumn, Field::InstrumentFeedColumns}; } - if (column.values.size() != bars.size()) + if (column.values.size() != bars.size) return {Error::InvalidInstrumentFeedColumn, Field::InstrumentFeedColumns}; for (std::size_t other = 0; other < c; ++other) { if (feed.columns[other].name == column.name) @@ -602,7 +615,7 @@ bool valid_legacy_tolerance(NativeFeedTolerance tolerance) noexcept { return (bits & ~kKnown) == 0u; } -Result validate_values(const NativeRunSpec& spec) noexcept { +Result validate_values(const NativeRunSpec& spec, const NativeBarViews& views) noexcept { const bool require_timeframes = !spec.timeframe_undetected; const struct { const std::string& value; @@ -727,7 +740,7 @@ Result validate_values(const NativeRunSpec& spec) noexcept { } Field shapes_field = Field::InstrumentFeedTimeframe; try { - if (const auto feeds = instrument_feed_shapes(spec.instrument_feeds); !feeds) + if (const auto feeds = instrument_feed_shapes(spec.instrument_feeds, views); !feeds) return feeds; shapes_field = Field::SubscriptionTimeframe; if (const auto series = @@ -741,6 +754,9 @@ Result validate_values(const NativeRunSpec& spec) noexcept { } catch (...) { return {Error::CalendarFailure, shapes_field}; } + if (views.lower_path != nullptr + && (spec.intrabar.lower() == nullptr || !spec.intrabar.lower()->bars.empty())) + return {Error::InvalidIntrabarPath, Field::IntrabarTimeframe}; if (spec.intrabar.value.index() > 2) { return {Error::InvalidIntrabarPath, Field::IntrabarTimeframe}; } @@ -782,8 +798,10 @@ Result validate_values(const NativeRunSpec& spec) noexcept { } // namespace -NativeRunSpecValidation validate_native_run_spec(const NativeRunSpec& spec) noexcept { - const auto values = validate_values(spec); +namespace { +NativeRunSpecValidation validate_run_spec_views(const NativeRunSpec& spec, + const NativeBarViews& views) noexcept { + const auto values = validate_values(spec, views); if (!values) return values; Field active_field = Field::InputTimeframe; @@ -857,6 +875,12 @@ NativeRunSpecValidation validate_native_run_spec(const NativeRunSpec& spec) noex } } +} // namespace + +NativeRunSpecValidation validate_native_run_spec(const NativeRunSpec& spec) noexcept { + return validate_run_spec_views(spec, {}); +} + NativeRunSpecValidation normalize_native_run_spec(NativeRunSpec& spec) noexcept { const auto result = validate_native_run_spec(spec); if (!result) return result; @@ -995,7 +1019,7 @@ NativeRunSpecValidation validate_native_timeframe_subscriptions( NativeRunSpecValidation validate_native_instrument_feeds( const std::vector& instrument_feeds) noexcept { try { - return instrument_feed_shapes(instrument_feeds); + return instrument_feed_shapes(instrument_feeds, {}); } catch (const std::bad_alloc&) { return {Error::AllocationFailure, Field::InstrumentFeedTimeframe}; } catch (...) { @@ -1003,12 +1027,14 @@ NativeRunSpecValidation validate_native_instrument_feeds( } } -NativeRunSpecValidation validate_native_timeframe_subscriptions( +namespace { +NativeRunSpecValidation validate_subscription_views( const std::vector& subscriptions, const std::string& input_tf, bool timeframe_undetected, const std::optional& auxiliary_feed, - const std::vector& instrument_feeds) noexcept { - const auto instruments = validate_native_instrument_feeds(instrument_feeds); + const std::vector& instrument_feeds, + const NativeBarViews& views) noexcept { + const auto instruments = native_bar_views_v1::validate_native_instrument_feeds(instrument_feeds, views); if (!instruments) return instruments; const auto feed = validate_native_auxiliary_feed(auxiliary_feed, input_tf, timeframe_undetected); @@ -1037,6 +1063,17 @@ NativeRunSpecValidation validate_native_timeframe_subscriptions( } } +} // namespace + +NativeRunSpecValidation validate_native_timeframe_subscriptions( + const std::vector& subscriptions, + const std::string& input_tf, bool timeframe_undetected, + const std::optional& auxiliary_feed, + const std::vector& instrument_feeds) noexcept { + return validate_subscription_views(subscriptions, input_tf, timeframe_undetected, + auxiliary_feed, instrument_feeds, {}); +} + NativeRunSpecValidation validate_native_auxiliary_feed( const std::optional& auxiliary_feed, const std::string& input_tf, bool timeframe_undetected) noexcept { @@ -1121,8 +1158,10 @@ std::uint64_t native_auxiliary_feed_digest(const NativeAuxiliaryFeed& feed) noex return state; } -std::uint64_t native_instrument_feeds_digest( - const std::vector& instrument_feeds) noexcept { +namespace { +std::uint64_t digest_instrument_feed_views( + const std::vector& instrument_feeds, + const NativeBarViews& views) noexcept { std::uint64_t state = 1469598103934665603ULL; const auto bytes = [&state](const void* data, std::size_t count) noexcept { const auto* values = static_cast(data); @@ -1144,15 +1183,19 @@ std::uint64_t native_instrument_feeds_digest( bytes(value.data(), value.size()); }; u(instrument_feeds.size()); - for (const auto& feed : instrument_feeds) { + for (std::size_t at = 0; at < instrument_feeds.size(); ++at) { + const auto& feed = instrument_feeds[at]; + const auto* borrowed = views.instrument_feed_count == 0 + ? nullptr : views.instrument_feeds[at]; + const auto data = native_instrument_feed_view(feed, borrowed); s(feed.instrument); s(feed.tf); - u(feed.bars.size()); - for (std::size_t k = 0; k < feed.bars.size(); ++k) { - const Bar& bar = feed.bars[k]; + u(data.bars.size); + for (std::size_t k = 0; k < data.bars.size; ++k) { + const Bar& bar = data.bars.data[k]; d(bar.open); d(bar.high); d(bar.low); d(bar.close); d(bar.volume); i(bar.timestamp); - i(k < feed.close_ms.size() ? feed.close_ms[k] : 0); + i(k < data.close_count ? data.close_ms[k] : 0); } u(feed.columns.size()); for (const auto& column : feed.columns) { @@ -1164,5 +1207,49 @@ std::uint64_t native_instrument_feeds_digest( return state; } +} // namespace + +std::uint64_t native_instrument_feeds_digest( + const std::vector& instrument_feeds) noexcept { + return digest_instrument_feed_views(instrument_feeds, {}); +} + } // inline namespace native_run_spec_v3 + +inline namespace native_bar_views_v1 { + +NativeRunSpecValidation normalize_native_run_spec( + NativeRunSpec& spec, const NativeBarViews& views) noexcept { + const auto judged = native_run_spec_v3::validate_run_spec_views(spec, views); + if (!judged) return judged; + if (spec.fee_value == 0.0) spec.fee_value = 0.0; + return {}; +} + +NativeRunSpecValidation validate_native_instrument_feeds( + const std::vector& feeds, const NativeBarViews& views) noexcept { + try { + return native_run_spec_v3::instrument_feed_shapes(feeds, views); + } catch (const std::bad_alloc&) { + return {NativeRunSpecError::AllocationFailure, NativeRunSpecField::InstrumentFeedTimeframe}; + } catch (...) { + return {NativeRunSpecError::CalendarFailure, NativeRunSpecField::InstrumentFeedTimeframe}; + } +} + +NativeRunSpecValidation validate_native_timeframe_subscriptions( + const std::vector& subscriptions, + const std::string& input_tf, bool timeframe_undetected, + const std::optional& auxiliary_feed, + const std::vector& feeds, const NativeBarViews& views) noexcept { + return native_run_spec_v3::validate_subscription_views( + subscriptions, input_tf, timeframe_undetected, auxiliary_feed, feeds, views); +} + +std::uint64_t native_instrument_feeds_digest( + const std::vector& feeds, const NativeBarViews& views) noexcept { + return native_run_spec_v3::digest_instrument_feed_views(feeds, views); +} + +} // inline namespace native_bar_views_v1 } // namespace pineforge diff --git a/src/source/pine_adapter.cpp b/src/source/pine_adapter.cpp index af99883b2..8e7ff61d9 100644 --- a/src/source/pine_adapter.cpp +++ b/src/source/pine_adapter.cpp @@ -2994,7 +2994,9 @@ NativeRunSpec PineExecutionAdapter::project(const PineStrategyConfig& config, // (observe_terminal_receipts), so it keeps only the unread window: the // kernel default, which folds nothing into the spec digest. spec.event_retention = NativeEventRetention::Window; - const auto validation = validate_native_run_spec(spec); + const auto validation = host_ != nullptr + ? NativeExecutionConsumer::bound(*host_).normalize_projected_spec(spec) + : native_bar_views_v1::normalize_native_run_spec(spec, {}); if (!validation) { throw coded(run_spec_field_failure(validation.field), "Pine adapter produced invalid native run spec field " diff --git a/src/source/pine_security_eval.cpp b/src/source/pine_security_eval.cpp index 08a930d65..e19acc887 100644 --- a/src/source/pine_security_eval.cpp +++ b/src/source/pine_security_eval.cpp @@ -1091,8 +1091,13 @@ void source::PineStrategyHost::restore_symbol_feeds() { for (std::size_t i = 0; i < symbol_feeds_.size(); ++i) { const NativeInstrumentFeed& held = spec->instrument_feeds[i]; NativeInstrumentFeed& feed = symbol_feeds_[i]; - feed.bars = held.bars; - feed.close_ms = held.close_ms; + const auto views = NativeExecutionConsumer::bound(*this).bar_views(); + const auto* slot = views.instrument_feed_count == 0 + ? nullptr : views.instrument_feeds[i]; + if (slot == nullptr) { + feed.bars = held.bars; + feed.close_ms = held.close_ms; + } for (std::size_t c = 0; c < feed.columns.size(); ++c) feed.columns[c].values = held.columns[c].values; } @@ -1283,13 +1288,16 @@ void source::PineStrategyHost::read_ahead_foreign_security_sites(const Bar& char : is_na(chart_close)) { continue; } - const NativeInstrumentFeed& feed = installed_symbol_feed(site.feed); - while (static_cast(site.delivered) < feed.bars.size()) { + const auto views = NativeExecutionConsumer::bound(*this).bar_views(); + const auto* slot = views.instrument_feed_count == 0 + ? nullptr : views.instrument_feeds[site.feed]; + const auto feed = native_instrument_feed_view(installed_symbol_feed(site.feed), slot); + while (static_cast(site.delivered) < feed.bars.size) { const auto at = static_cast(site.delivered); - const bool visible = site.lookahead ? feed.bars[at].timestamp <= chart_bar.timestamp + const bool visible = site.lookahead ? feed.bars.data[at].timestamp <= chart_bar.timestamp : feed.close_ms[at] <= chart_close; if (!visible) break; - run_foreign_site_bar(site, feed.bars[at], feed.close_ms[at]); + run_foreign_site_bar(site, feed.bars.data[at], feed.close_ms[at]); } } } @@ -1417,9 +1425,10 @@ bool source::PineStrategyHost::set_symbol_feed_column(const std::string& key, if (const std::string why = restore_symbol_feeds_reason(); !why.empty()) return refuse("restore_failed", why); NativeInstrumentFeed& feed = symbol_feeds_[static_cast(found)]; - if (n < 0 || static_cast(n) != feed.bars.size() || (n > 0 && values == nullptr)) + const auto data = native_instrument_feed_view(feed, nullptr); + if (n < 0 || static_cast(n) != data.bars.size || (n > 0 && values == nullptr)) return refuse("column_length", "column '" + name + "' has " + std::to_string(n) - + " values for " + std::to_string(feed.bars.size()) + " bars"); + + " values for " + std::to_string(data.bars.size) + " bars"); // The kernel's judgement of the name, here rather than at the next begin: // a feed of the same key without bars carries it (the count is checked // above, and no column value is refused). @@ -1539,7 +1548,7 @@ void source::PineStrategyHost::refresh_symbol_data_digest() noexcept { for (const auto& feed : symbol_feeds_) { f.s(feed.instrument); f.s(feed.tf); - f.u(feed.bars.size()); + f.u(native_instrument_feed_view(feed, nullptr).bars.size); f.u(feed.columns.size()); for (const auto& column : feed.columns) f.s(column.name); } diff --git a/src/source/pine_strategy_host.cpp b/src/source/pine_strategy_host.cpp index 3d5f7cf65..bd14070c0 100644 --- a/src/source/pine_strategy_host.cpp +++ b/src/source/pine_strategy_host.cpp @@ -230,8 +230,12 @@ StagedConfiguration source::PineStrategyHost::staged_configuration() const { staged.instrument_feeds.reserve(symbol_feeds_.size()); for (std::size_t i = 0; i < symbol_feeds_.size(); ++i) { const NativeInstrumentFeed& feed = installed_symbol_feed(i); + const auto views = NativeExecutionConsumer::bound(*this).bar_views(); + const auto* slot = views.instrument_feed_count == 0 + ? nullptr : views.instrument_feeds[i]; + const auto data = native_instrument_feed_view(feed, slot); staged.instrument_feeds.push_back( - {feed.instrument, feed.tf, feed.bars.size(), feed.columns.size()}); + {feed.instrument, feed.tf, data.bars.size, feed.columns.size()}); } return staged; } @@ -342,6 +346,7 @@ void source::PineStrategyHost::prepare_native_begin(const NativeBeginArgs& args) std::vector lent = lend_feed_bytes(symbol_feeds_); NativeRunSpec spec = [&] { try { + NativeExecutionConsumer::bound(*this).stage_instrument_feed_views(nullptr, 0); return adapter_.project(effective, staged, args, path_order, &lent); } catch (...) { return_feed_bytes(symbol_feeds_, lent); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 018f4bcd3..be69219ab 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -7091,3 +7091,7 @@ set_target_properties(test_cr150_contract_headers PROPERTIES # CR-150 L15a: append after every existing line-pinned registration. list(APPEND TEST_SOURCES test_native_bar_views_l15a) pineforge_add_unit_test(test_native_bar_views_l15a) + +add_test(NAME test_native_bar_views_guard_l15a + COMMAND ${Python3_EXECUTABLE} ${PROJECT_SOURCE_DIR}/scripts/check_native_cpp_versions.py + --self-test-bar-views) diff --git a/tests/test_native_bar_views_l15a.cpp b/tests/test_native_bar_views_l15a.cpp index eb19a2c80..b336afa23 100644 --- a/tests/test_native_bar_views_l15a.cpp +++ b/tests/test_native_bar_views_l15a.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -173,6 +174,11 @@ class VectorSource final : public NativeLowerPathSource { const std::vector& bars_; int& destroyed_; bool throws_; + bool selected_ = false; + std::int64_t begin_ = 0; + std::int64_t end_ = 0; + NativeBarView active_{nullptr, 0}; + const Bar* next_ = nullptr; public: VectorSource(const std::vector& bars, int& destroyed, bool throws = false) : bars_(bars), destroyed_(destroyed), throws_(throws) {} @@ -180,13 +186,26 @@ class VectorSource final : public NativeLowerPathSource { std::size_t count() const override { return bars_.size(); } bool for_each(Visitor visitor, void* context) const override { if (throws_) throw std::runtime_error("l15a-walk-fault"); + if (visitor == nullptr) + throw coded(RunFailureCode::engine_invariant, {}, "null visitor"); for (const auto& bar : bars_) if (!visitor(context, bar)) return false; return true; } - NativeBarView segment(std::int64_t, std::int64_t) override { - return {bars_.data(), bars_.size()}; + NativeBarView segment(std::int64_t begin, std::int64_t end) override { + if (end < begin || (selected_ && (begin < begin_ || end < end_))) + throw coded(RunFailureCode::engine_invariant, {}, "window order"); + if (selected_ && begin == begin_ && end == end_) return active_; + const auto less = [](const Bar& bar, std::int64_t time) { return bar.timestamp < time; }; + const auto first = std::lower_bound(bars_.begin(), bars_.end(), begin, less); + const auto last = std::lower_bound(first, bars_.end(), end, less); + active_ = {first == last ? nullptr : &*first, static_cast(last - first)}; + next_ = last == bars_.end() ? nullptr : &*last; + selected_ = true; + begin_ = begin; + end_ = end; + return active_; } - const Bar* next_after_segment() const noexcept override { return nullptr; } + const Bar* next_after_segment() const noexcept override { return next_; } }; void lower_context_and_exception_atomicity() { @@ -227,6 +246,16 @@ void lower_context_and_exception_atomicity() { assert(destroyed == 2); assert(host.configure_native(spec).status == NativeSetupStatus::Applied); + HashHost stream; + auto& stream_consumer = NativeExecutionConsumer::bound(stream); + stream_consumer.stage_lower_path_source(std::make_unique(chart, destroyed)); + assert(stream.configure_native(lent).status == NativeSetupStatus::Applied); + assert(stream_consumer.bar_views().lower_path != nullptr); + assert(!stream.stream_begin(chart.data(), static_cast(chart.size()), "15", "15")); + assert(stream_consumer.bar_views().lower_path == nullptr); + assert(destroyed == 3); + (void)stream.native_continuation_hash(); + // A null staging table is a programmer invariant; active state is intact. bool invalid_table = false; try { consumer.stage_instrument_feed_views(nullptr, 1); } @@ -252,6 +281,10 @@ void refused_configure_discards_pending() { auto& retry_consumer = NativeExecutionConsumer::bound(retry); const auto logical = spec_for(metadata(owned)); retry_consumer.stage_instrument_feed_views(slots, 1); + bool table_error = false; + try { retry_consumer.stage_instrument_feed_views(nullptr, 1); } + catch (const std::logic_error&) { table_error = true; } + assert(table_error); assert(retry.configure_native(logical).status == NativeSetupStatus::Applied); auto malformed = make_bar(monday(), 0, 1.0); malformed.high = malformed.low - 1.0; From 4e209344c3398d859cfae388b7a6ea56432fbee0 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sun, 18 Oct 2026 20:38:31 +0800 Subject: [PATCH 03/35] fix(cr150): list L15a kernel sources on separate lines Round 3 item 1, lab rj-20261018t115236-b4aba1: repair the shared source line introduced by 25391023b so the detached-comment reader sees separate translation units. Carry every affected root-CMake documentation coordinate by one line; tests/CMakeLists.txt and its private_fixture_reads.evidence pin stay unchanged, following the line-pin discipline of P00 fix4 (25e44916d). The documentation source claims came from 47184e5c6e and 868aa65f1; only their coordinates change. No builds, tests or guards executed on the Mac. --- CMakeLists.txt | 3 ++- docs/adr/0001-kernel-adapter-boundary.md | 6 +++--- docs/design/native-feature-parity.md | 6 +++--- docs/pages/contributing-llm.md | 2 +- docs/pages/native-engine.md | 2 +- docs/pages/pine-to-native.md | 2 +- 6 files changed, 11 insertions(+), 10 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 461861fd5..716e7651b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -149,7 +149,8 @@ set(PINEFORGE_KERNEL_SOURCES src/timezone.cpp src/native_calendar.cpp src/native_run_spec.cpp - src/native_fx_curve.cpp src/native_bar_views.cpp + src/native_fx_curve.cpp + src/native_bar_views.cpp src/selected_report_view.cpp src/selected_window_plan.cpp ) diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index 5c9a861a0..ad8a79b3c 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -37,7 +37,7 @@ Three layers, two of them meant to be optional: `timeframe`, `timezone`, `str_utils`, `c_abi`, `native_c_host`, `reservation_expansion` (`CMakeLists.txt:126`) and `pending_order_mirror` (`CMakeLists.txt:132`) units — plus the headers directly under `include/pineforge/`. That list is what `add_library` - (`CMakeLists.txt:158`) compiles into `pineforge_kernel` / `PineForge::kernel`, and it and + (`CMakeLists.txt:159`) compiles into `pineforge_kernel` / `PineForge::kernel`, and it and `PINEFORGE_SOURCE_LAYER_SOURCES` are disjoint: no file is in both, and none is outside both. **Two stems exist twice; the kernel copy is the generic half** — `ReservationExpansion` (`src/reservation_expansion.cpp:10`) and @@ -370,7 +370,7 @@ citations, 264 are in `src/source/` and its headers; 6 sit in kernel files ## Current state vs. target (read before relying on "without Pine") - **The kernel is a separately linkable artifact.** `add_library` - (`CMakeLists.txt:158`) builds `pineforge_kernel` / `PineForge::kernel` from + (`CMakeLists.txt:159`) builds `pineforge_kernel` / `PineForge::kernel` from `PINEFORGE_KERNEL_SOURCES` alone, with the same flags `pineforge` uses, in **both** configurations. `libpineforge.a` still carries the adapter beside the kernel by default; `-DPINEFORGE_BUILD_SOURCE_LAYER=OFF` (`CMakeLists.txt:44`) compiles no source-layer @@ -448,7 +448,7 @@ citations, 264 are in `src/source/` and its headers; 6 sit in kernel files `native_selected_strategy.cpp`), both of which include `native_host.hpp`. Each C host includes ``, which includes `native_c_api.h`. Every one links `PineForge::kernel` and checks its own numbers before it prints its summary line. `PINEFORGE_BUILD_EXAMPLES` - (`CMakeLists.txt:37`, default OFF) builds them (`add_subdirectory` `CMakeLists.txt:387`) and + (`CMakeLists.txt:37`, default OFF) builds them (`add_subdirectory` `CMakeLists.txt:388`) and registers each as a CTest row that runs through `examples/native/run_example.cmake`, which fails on a nonzero exit, a signal **or** a missing summary line — so a row asserts both, which a bare `PASS_REGULAR_EXPRESSION` cannot. `scripts/test_example_runner.py` proves diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 2442b75c8..d1ffdf4ad 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -185,7 +185,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa ### 2.i Kernel-only build target (L1) -- **Before L1:** one static library always compiled the source layer and installed every header, `include/pineforge/source/**` and `include/pineforge/compat/pine/**` included, and `src/compat/pine/market_admission.cpp` sat in the *main* list for historical reasons (added in an earlier commit, before the source-layer list existed — F). **Since L1:** the source layer is its own list (`PINEFORGE_SOURCE_LAYER_SOURCES` CMakeLists.txt:91, `market_admission.cpp` at :94), appended to `pineforge` only when `PINEFORGE_BUILD_SOURCE_LAYER` is ON (`add_library(pineforge STATIC` CMakeLists.txt:167-170), beside the kernel-only archive (`pineforge_kernel` CMakeLists.txt:158), whose header install excludes `source` and `compat` (`PATTERN` CMakeLists.txt:471-476). +- **Before L1:** one static library always compiled the source layer and installed every header, `include/pineforge/source/**` and `include/pineforge/compat/pine/**` included, and `src/compat/pine/market_admission.cpp` sat in the *main* list for historical reasons (added in an earlier commit, before the source-layer list existed — F). **Since L1:** the source layer is its own list (`PINEFORGE_SOURCE_LAYER_SOURCES` CMakeLists.txt:91, `market_admission.cpp` at :94), appended to `pineforge` only when `PINEFORGE_BUILD_SOURCE_LAYER` is ON (`add_library(pineforge STATIC` CMakeLists.txt:168-171), beside the kernel-only archive (`pineforge_kernel` CMakeLists.txt:159), whose header install excludes `source` and `compat` (`PATTERN` CMakeLists.txt:472-477). - **Link feasibility:** no kernel TU and no public kernel header includes `source/` or `compat/pine/` (F, O). The only code coupling is the forward-declared opaque `source::StrategyOverrides*` (§2.ii a), which needs no definition. - **Risk:** the archive member order of `libpineforge.a` changes. Harmless for static linking, but check `PINEFORGE_REQUIRE_ABI_RECEIPTS` (CMakeLists.txt:65); what the receipts pin is unverified (F). @@ -299,7 +299,7 @@ Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the **Status (L10): done.** `examples/native/` holds eighteen hosts, sixteen C++ and two C, built by `PINEFORGE_BUILD_EXAMPLES`; `include/pineforge/native_module.hpp` defines `PINEFORGE_EXPORT_NATIVE_STRATEGY(Class)`. The bullets below are the plan L10 worked from. -- Move `runner/examples/native_market_strategy.cpp` and `native_selected_strategy.cpp` to a top-level `examples/native/`, built by `PINEFORGE_BUILD_EXAMPLES` (then "none yet" and guarding nothing CMakeLists.txt:37; the examples build only under `PINEFORGE_BUILD_LIVE_RUNNER` CMakeLists.txt:508-509, with `native_market_example` runner/CMakeLists.txt:54). Link the kernel target (S), or `PineForge::pineforge` during migration (O) — no SQLite / curl / OpenSSL (runner/CMakeLists.txt:1-4). +- Move `runner/examples/native_market_strategy.cpp` and `native_selected_strategy.cpp` to a top-level `examples/native/`, built by `PINEFORGE_BUILD_EXAMPLES` (then "none yet" and guarding nothing CMakeLists.txt:37; the examples build only under `PINEFORGE_BUILD_LIVE_RUNNER` CMakeLists.txt:509-510, with `native_market_example` runner/CMakeLists.txt:54). Link the kernel target (S), or `PineForge::pineforge` during migration (O) — no SQLite / curl / OpenSSL (runner/CMakeLists.txt:1-4). - Build (1) standalone executables with a `main()` that runs batch + stream on embedded bars (the guide already contains that `main` native-engine.md:3237-3305) and (2) the same MODULE targets the runner tests load; keep the C-ABI shims so the live runner can still `dlopen` them (F, O, S). - Update the three places that pin the paths: `native_market_example` runner/CMakeLists.txt:54, `NATIVE_EXAMPLES` check_native_include_independence.py:49, the tests at `test_native_example_batch` runner/CMakeLists.txt:158. - Add a minimal "hello, kernel" host (~60 lines, no C ABI — O) and one example per v1 feature lane as it lands; each doubles as that lane's twin host (F). @@ -330,7 +330,7 @@ Union of F §2.vi (9), O §2.vi (5 + 1) and S §2.6 (7), deduplicated. S had no | 8 | "No kernel-level C API. `c_abi.cpp` exports only version/descriptor symbols" (ADR L136-138) | 80 runtime `PF_API` symbols: native configure, the 12-symbol stream family, pending-order views, broker-state hash, security feeds, FX curve, reports. The 80 compiled-strategy runtime symbols in `c_abi.cpp` are distinct from the native C host API in `native_c_host.cpp`: the latter exposes submit, replace, cancel and a callback table to a C host. | `sha256:733786ed81c9137223b98e1887e28d01afdb3c64ba5e8ca52ce5161f2caf3f75` c_abi.cpp:325-1013, `sha256:6327f1829019fdd172cd76a96e3d8c9c0af1610ad0dc9878640dda0ca21a686b` check_c_abi_runtime.py:20-81, c_abi.cpp:946, `sha256:5ff4c65a56410fe29b21cd68bc21ac87bdb34b77344e4f75f90efa8887fe48e8` c_abi.cpp:664-788, `sha256:051fe087eb9d30e9960dc4f0ab3f7cc837531ebeaa71b5871a3f1cdc22fb37c2` c_abi.cpp:566-641, `sha256:f18d46ac323234f7e3082367d30840b704b8a7548f7ae95ee8cff8714f29fd50` c_abi.cpp:545-562, `sha256:6a9ef4e688f33dc10ea8297d9eaa8144ea7923ef6ee6f8fe9ff20f6af4e9acb3` c_abi.cpp:881-917, c_abi.cpp:1013 | F O S | | 9 | "No examples. `PINEFORGE_BUILD_EXAMPLES` is 'none yet'" (ADR L139) | The option is inert, but two Pine-free `NativeStrategyHost` examples exist, are compiled by the independence checker and tested through the live runner, only under `PINEFORGE_BUILD_LIVE_RUNNER`. Wording: "no top-level examples target" | at `73817c1` (overtaken; ADR-0001, "Current state vs. target") | F O S | | 10 | Follow-ups (1)-(4) (ADR L168-170) | Add native reporting, sizing, margin, HTF — and, per R5, calc-timing, price grid and order ergonomics. Without them "usable without Pine" is a build-system claim, not a feature claim | §3.5 | F | -| 11 | Build layout | One `pineforge` library always appends the source list; a kernel-only target does not exist yet; `compat/pine/market_admission.cpp` sits in the main list, a boundary bug. **Overtaken:** `PINEFORGE_KERNEL_SOURCES` CMakeLists.txt:115 and `add_library` CMakeLists.txt:158 build `PineForge::kernel`, and the compat unit is in `PINEFORGE_SOURCE_LAYER_SOURCES` CMakeLists.txt:91 | `add_library` CMakeLists.txt:158 | S (F, O agree in §2.i) | +| 11 | Build layout | One `pineforge` library always appends the source list; a kernel-only target does not exist yet; `compat/pine/market_admission.cpp` sits in the main list, a boundary bug. **Overtaken:** `PINEFORGE_KERNEL_SOURCES` CMakeLists.txt:115 and `add_library` CMakeLists.txt:159 build `PineForge::kernel`, and the compat unit is in `PINEFORGE_SOURCE_LAYER_SOURCES` CMakeLists.txt:91 | `add_library` CMakeLists.txt:159 | S (F, O agree in §2.i) | | 12 | Host constructors | `NativeStrategyHost` is a zero-argument C++ host with the callbacks and the request API; `PineStrategyHost` is the class that takes `CapAttachment` — do not attribute it to the native constructor | `NativeStrategyHost` native_host.hpp:845, `PineStrategyHost` pine_strategy_host.hpp:287 | S (single-source) | `docs/pages/native-engine.md` fixes, all applied (F unless noted). The page is cited by diff --git a/docs/pages/contributing-llm.md b/docs/pages/contributing-llm.md index f7706c690..113801c7d 100644 --- a/docs/pages/contributing-llm.md +++ b/docs/pages/contributing-llm.md @@ -261,7 +261,7 @@ decision, and removing one is a regression: **kernel** — the Pine-agnostic half of the engine: matching, fills, sizing, margin, settlement, indicators, calendars. Buildable alone as -`PineForge::kernel` CMakeLists.txt:159 with `PINEFORGE_BUILD_SOURCE_LAYER` +`PineForge::kernel` CMakeLists.txt:160 with `PINEFORGE_BUILD_SOURCE_LAYER` CMakeLists.txt:43 off. **adapter** — `src/source/` and `src/compat/pine/`: the TradingView-parity diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index acedc6c43..b1f7523e7 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -4550,7 +4550,7 @@ Nor is there a build-level one. The two source sets are disjoint: `PINEFORGE_SOURCE_LAYER_SOURCES` (`CMakeLists.txt:91`) holds all six `src/compat/pine/` units and all twelve `src/source/` ones, and `PINEFORGE_KERNEL_SOURCES` (`CMakeLists.txt:115`) holds the kernel's own -thirty-five, which are what `add_library` (`CMakeLists.txt:158`) compiles into +thirty-five, which are what `add_library` (`CMakeLists.txt:159`) compiles into `pineforge_kernel`. `libpineforge.a` still carries both sets when `PINEFORGE_BUILD_SOURCE_LAYER` is ON, which is the default; the kernel archive exists either way. The installed-header closure is clean too, which the diff --git a/docs/pages/pine-to-native.md b/docs/pages/pine-to-native.md index e6d1f192a..021f93441 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -1025,7 +1025,7 @@ cmake -S . -B build-kernel -DPINEFORGE_BUILD_SOURCE_LAYER=OFF -DPINEFORGE_BUILD_ ``` `PINEFORGE_BUILD_SOURCE_LAYER` CMakeLists.txt:43 OFF leaves `libpineforge.a` -holding exactly the objects of `PineForge::kernel` CMakeLists.txt:159, and +holding exactly the objects of `PineForge::kernel` CMakeLists.txt:160, and every example already links that target, so a source-layer symbol reaching an example is a link error in the default build too. From ae868d625bd1d310a1061e17af83903a1bc672c9 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sun, 18 Oct 2026 20:41:44 +0800 Subject: [PATCH 04/35] docs(cr150): carry all L15a source citations to the new tree Round 3 item 2, lab rj-20261018t115236-b4aba1: carry 302 source-coordinate spans from ee3083d4a to the unchanged 25391023b engine sources, including all 288 anchors listed in lab-doc-anchors.log and 14 additional affected references. Handle basenames, full paths and shorthand lists while retaining the backticked claims and the intended occurrence of repeated symbols. Preserve citations already moved in Round 2. Doc source-claim provenance: 47184e5c6e, 65c69b0788, f48b7c8c37 and 868aa65f1. Only citation coordinates change; no policy, numeric assertion, frozen declaration or engine behavior is reworded. Repair source drift introduced by 25391023b. No builds, compilation, tests or guards executed here. CORE-TOP must rerun the two failed preflight stages and required verification on the lab. --- docs/adr/0001-kernel-adapter-boundary.md | 60 +++--- docs/design/native-feature-parity.md | 242 +++++++++++------------ docs/pages/native-engine.md | 16 +- docs/pages/pine-to-native.md | 12 +- 4 files changed, 165 insertions(+), 165 deletions(-) diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index ad8a79b3c..bca57350b 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -78,7 +78,7 @@ summary of it. (`native_host.hpp:893`); also `on_native_input` (`native_host.hpp:875`), `on_native_tick` (`native_host.hpp:878`), `on_native_timeframe_bar` (`native_host.hpp:885`), `on_native_bar_open` — at the modeled opening, before its matching pass - (`native_host.hpp:889`, `on_native_bar_open` `native_execution_consumer.cpp:7955-7957`) — + (`native_host.hpp:889`, `on_native_bar_open` `native_execution_consumer.cpp:8023-8025`) — `on_native_recalculate` for every calculation of the run (`native_host.hpp:913`), `on_native_sub_bar` (`native_host.hpp:928`) and post-fill `on_native_applied` (`native_host.hpp:938`). `on_bar` is `final` (`native_host.hpp:857`). @@ -112,7 +112,7 @@ summary of it. bare host needs no override at all, while `HostSized` hands the units to the host — and an unresolved `HostSized` whose `resolve_execution_terms` (`native_host.hpp:947`) returns no units is a `TermsUnresolved` rejection - (`TermsUnresolved` `native_execution_consumer.cpp:5769`). + (`TermsUnresolved` `native_execution_consumer.cpp:5837`). - **Observing executions.** Implement `on_native_applied` (`native_host.hpp:938`) — notifications drain FIFO after the outer callback returns — and/or poll `native_events(after_ordinal)` (`native_host.hpp:1335`). Do *not* implement @@ -123,7 +123,7 @@ summary of it. | What | Surface | |---|---| | Run state | `native_state()` → `NativeStateView` — kind, phase, `NativeCompletion` BatchComplete / StreamEnded, failure, high water (`NativeCompletion` `native_host.hpp:50`, `NativeStateView` `:282`, `NativeRunPhase` `:41`) | - | Failure model | `NativeFailureCode` (`native_host.hpp:63-77`); `Failed` is latched, nothing resumes. A throwing callback latches `CallbackException` (`CallbackException` `native_execution_consumer.cpp:5742`) | + | Failure model | `NativeFailureCode` (`native_host.hpp:63-77`); `Failed` is latched, nothing resumes. A throwing callback latches `CallbackException` (`CallbackException` `native_execution_consumer.cpp:5810`) | | Cohorts | `cohort_open` (`native_host.hpp:1282`) / `cohort_add` (`:1285`) / `cohort_remove` (`:1288`) — the handle a `BindCohort` owner names | | Mid-callback execution | `current_execution_point` (`native_host.hpp:1087`), `inspect_current_execution` (`native_host.hpp:1099`) / `execute_current` (`native_host.hpp:1104`) | | Margin seams | four, not one: the run spec's own `margin` model (`NativeMarginModel` `native_run_spec.hpp:240`) with its three hooks — `resolve_margin_requirement` (`native_host.hpp:976`), `margin_check_allowed` (`native_host.hpp:988`), `resolve_margin_call_units` (`native_host.hpp:995`) — and `validate_execution_precommit` (`native_host.hpp:960`), whose `AdmitWithHostMargin` verdict hands the opening check to the host | @@ -211,11 +211,11 @@ into kernel driving — never by special-casing the kernel *for a specific scrip kernel and own these quirks, each at its site: - **Seams the adapter overrides.** The kernel is driven, not patched. `PineStrategyHost` overrides - `resolve_execution_terms` (`pine_strategy_host.cpp:823`) for TV sizing and fill spelling; - `validate_execution_precommit` (`pine_strategy_host.cpp:848`), whose - `validate_precommit` (`pine_adapter.cpp:16371`) returns `AdmitWithHostMargin` - (`pine_adapter.cpp:16575`) to own the opening margin decision; `on_native_tick` - (`pine_strategy_host.cpp:550`) / `on_native_applied` (`pine_strategy_host.cpp:622`) for + `resolve_execution_terms` (`pine_strategy_host.cpp:828`) for TV sizing and fill spelling; + `validate_execution_precommit` (`pine_strategy_host.cpp:853`), whose + `validate_precommit` (`pine_adapter.cpp:16373`) returns `AdmitWithHostMargin` + (`pine_adapter.cpp:16577`) to own the opening margin decision; `on_native_tick` + (`pine_strategy_host.cpp:555`) / `on_native_applied` (`pine_strategy_host.cpp:627`) for `calc_on_order_fills` re-entry. It does not own lot excursions: since R5 lane H-THIN it keeps the kernel's `owns_lot_excursions()` default, so the kernel's sampler books every Pine lot's MFE/MAE, and `ab9714be`'s host model (RULING A48) is gone. R5 lane H-MEASURE had measured the @@ -237,14 +237,14 @@ kernel and own these quirks, each at its site: (`select_exit_activation` src/compat/pine/exit_activation.cpp:44, `select_exit_suspension` src/compat/pine/exit_lifecycle.cpp:11, `select_replacement_revival_definition` src/compat/pine/exit_lifecycle.cpp:51) and historical birth reach (`order_birth.cpp` `src/compat/pine/order_birth.cpp:5-14`). - **Trail and tick conventions.** The half-tick arm threshold measured against tick-quantized - extremes while the raw running best is retained (`has_trail_request` `pine_adapter.cpp:12500-12505`); the half-tick + extremes while the raw running best is retained (`has_trail_request` `pine_adapter.cpp:12502-12507`); the half-tick trigger threshold (`source_trigger_threshold` pine_adapter.cpp:384) and raw-vs-booked fill spelling, behind the - adapter's terms seam (`resolve_terms` `pine_adapter.cpp:14844`). + adapter's terms seam (`resolve_terms` `pine_adapter.cpp:14846`). - **Money arithmetic.** Ten-significant-digit half-up money (`source_money_round` `pine_adapter.cpp:558-564`, twin `tv_money_round` `pine_policy_support.hpp:9-15`). - **Quantity dust.** After every applied execution the Pine host erases any lot of at most `kQtyEpsilon` (`1e-10`) from the book it shares with the kernel, with no closing row - (`on_native_applied` `pine_strategy_host.cpp:622`), inside a live book as well as at a flat + (`on_native_applied` `pine_strategy_host.cpp:627`), inside a live book as well as at a flat reset — broader than `ab9714be`, which reset only a whole book at or under that size. Measured by R5 lane H-MEASURE (`tests/test_pine_dust_sweep_paired.cpp`): TradingView's decimal quantities leave no remnant row on a `lab tv` tape, which the Pine host books row for row @@ -260,8 +260,8 @@ kernel and own these quirks, each at its site: (`select_reservation_growth_sources` src/compat/pine/reservation_expansion.cpp:9). - **Margin.** The kernel owns the margin *mechanism* — the level solve, the check points, the kernel request, its re-pricing, the receipt — and the adapter answers its three policy hooks - with TradingView's: `margin_check_allowed` (`pine_adapter.cpp:17830`) for the scheduling, - `resolve_margin_requirement` (`pine_adapter.cpp:17899`) for the ten-significant-digit money, + with TradingView's: `margin_check_allowed` (`pine_adapter.cpp:17832`) for the scheduling, + `resolve_margin_requirement` (`pine_adapter.cpp:17901`) for the ten-significant-digit money, and `resolve_margin_call_units` for the lot-floored 4x restore. That hook answers every call on purpose (R5 lane F7): TradingView floors the restore onto the lot grid before the multiple, so on a gridded tape the kernel's own `ShortfallMultiple 4` books 5 lots where TradingView @@ -272,7 +272,7 @@ kernel and own these quirks, each at its site: margin since F7), while the roll point measures at the remaining path's adverse mark (MG-FX: 0.4116 @ 97 there against TradingView's 0.3996 @ 100). What has no kernel check point at all stays adapter-side: the `process_orders_on_close` chronology exception - (`non_pooc_commissioned_short` `pine_adapter.cpp:24165`) and + (`non_pooc_commissioned_short` `pine_adapter.cpp:24167`) and the 1x-long money call — plus the TV admission scopes (`explicit_pair_scope` `src/compat/pine/market_admission.cpp:34-47`) and the review fold they feed (`awaits_pair_review` `:68-73`, `fold_admission_history` `:83-140`). @@ -333,14 +333,14 @@ kernel and own these quirks, each at its site: projects `NativeCalculationTrigger::BarCloseAndFills` with TradingView's guard literal as `max_recalculations_per_point`. What stays are the specifics COOF adds on top — the language-state snapshot/restore around a recalculation, the waypoint-only refill deferral - (`next_source_path_waypoint` `pine_adapter.cpp:10464`), the first-open execution chain and its + (`next_source_path_waypoint` `pine_adapter.cpp:10466`), the first-open execution chain and its own loop guard (`kFirstOpenLoopGuard` `pine_scheduler_native.cpp:848`), and the two fills Pine refuses to - recalculate on (`suppress_grouped_stop_recalc` `pine_adapter.cpp:6744`). + recalculate on (`suppress_grouped_stop_recalc` `pine_adapter.cpp:6746`). - **Pine language state and harness flags.** Series, the tick-level barstate flags and position-view freezing (`PineLanguageState` `pine_language_state.hpp:12`); the session flags, `PineStrategyHost` members since R5 lane F5 (`session_ismarket_` `pine_strategy_host.hpp:1186`), which `scheduler_update_session_state` - (`pine_strategy_host.cpp:2223`) selects from the kernel's session-day facts before each source + (`pine_strategy_host.cpp:2228`) selects from the kernel's session-day facts before each source callback — `session_ismarket_` and the `_regular` pair are those facts as they are, and the plain `session.isfirstbar` / `islastbar` pair widens the regular day by the pre- and post-market bars an extended-hours chart holds (`update_extended_session_day`, Pine policy on TradingView's @@ -600,17 +600,17 @@ name is usually a plain word. The fourth audit found these gaps (AUDIT4-opus X10 | the margin model's check points on an intrabar path -- `NativeMarginCheckKind::IntrabarSample` (`include/pineforge/native_host.hpp`; C `PF_NATIVE_MARGIN_CHECK_INTRABAR_SAMPLE`), offered at every delivered sample after the script bar's first of a `lower_tf` path matched as `ContinuousSegments`, at that sample's price, immediately before it is matched (`NativeExecutionConsumer::deliver_intrabar_script`, `src/native_execution_consumer.cpp`) | every host with a margin model and such a path, through `margin_check_allowed`; the Pine adapter admits it on a magnified run's leveraged long and, outside `process_orders_on_close`, its short at any margin (R5 lane PAR-MARGIN-2) (`PineExecutionAdapter::intrabar_sample_checked`, `src/source/pine_adapter.cpp`) | **a check point of the kernel's own** (R5 lane PAR-MARGIN, item 3; H-MEASURE's acid finding A). Rule 2 (a), mechanism: a run with no whole-bar waypoint model measures its maintenance requirement where its path is -- at each delivered sample -- the rule the docs and `native_run_spec.hpp` stated and the kernel had not kept (it checked the first sample and after fills only); nothing in it knows a venue, and a host that checks somewhere else suppresses the points it does not share, as for every other kind. Rule 2 (b), knob: none -- the path is the run's own declaration, and a one-price distribution path (`synthesized`, `DistributionSamples`) is not offered the point, since a request armed at a discrete point is matched only at a later one. What moved: a margin run on a continuous intrabar path whose book crosses its line inside a bar now books that call at the crossing sample (H-MEASURE's intrabar probe: 55 points offered, a slice at 105 where none was booked). TradingView's magnified broker does the same at its own intrabars, 2 minutes on a 15-minute chart (the private fixture set `intrabar_margin`: the first call at the first lower-timeframe low that crosses on 4 of 4 tapes; a model of the check at TradingView's 2-minute intrabars books every row of all 20 magnified tapes, R5 lane PAR-MARGIN-2). No pinned value moved. Pinned by `tests/test_native_margin_intrabar_samples.cpp` (kernel-only) and the "intrabar margin on tapes" section of the private test `test_adapter_margin_schedule_differential`. | | the path a post-fill margin check measures -- `NativeExecutionConsumer::margin_segment_origin` (`src/native_execution_consumer.cpp`), the phase the `AfterApplied` re-arm hands `margin_sizing_price`: the waypoint the segment into the fill's driver point starts from, with the fill price as the point's own mark | every host with a margin model on a path walked by its modeled waypoints (no intrabar path), through the `AfterApplied` point `margin_check_allowed` admits | **a mechanism correction** (R5 lane PAR-MARGIN-2). Rule 2 (a), mechanism: a request matched at a driver point was reached on the segment into it, so the book its fill leaves still faces that point's waypoint -- a limit filled on its way down to the bar's low faces the low, a short filled on its way up faces the high -- and the scan that started strictly after the point's phase measured a path the book had not finished; the waypoints the path already passed stay out, as `fx_roll_margin_check_at`'s continuous roll measures from its segment's origin. Nothing in it knows a venue. Rule 2 (b), knob: none. What moved: a post-fill point after a fill matched on a segment now measures that segment's destination waypoint -- on a high-first bar O100 H101 L80 C81 a buy limit at 95 measures 80 where it measured the close 81, and a low-first bar or a short filled on its way up is called where nothing was (`tests/test_native_margin_post_fill_path.cpp`, kernel-only); a fill at a waypoint's own price, the open's included, measures what it did. TradingView books that call on the fill's bar (lab tv tapes `pm2-m7-lim-*` of the private fixture set `margin_entry_bar` at the low, `pm2-m7-slim-*` and `pm2-m7-s1lim-*` at the high, 12 of 12; with the adapter's admissions `pm2-m7-poocl-*` and `pm2-m7b-lim-*` too), as `ab9714be`'s entry-bar suffix did; the differential's M7-B pin moved to that answer, 10 @80. One v19 witness value moved with it: `tests/test_publication_witness.cpp` Config03, a short filled on its way up a bar, now called at that bar's high (461a919ebdcd41e9 -> 4593ef08ab33e045); the corpus is 312/312, moved 0. | | the samples of a retained lower bar -- `NativeExecutionConsumer::deliver_intrabar_script` (`src/native_execution_consumer.cpp`): a bar the direct four-sample walk declines (a leg of zero length, an open at its own low) is walked through its own four turning points, the repeated one included, where the general `ENDPOINTS` sampler filled the missing point uniformly; a bar that traded nothing at one price (volume 0, open = high = low = close) is one discrete point, walked once | every host with a `lower_tf` path; the Pine adapter's magnified path marks a chart bar's own open and close this way (R5 lane MAG-INTRABAR, `source::tradingview_magnifier_bars`, `src/source/magnifier_intrabars.cpp`) | **a mechanism correction** (R5 lane MAG-INTRABAR). Rule 2 (a), mechanism: a retained bar supplies its own four ticks, and every sample is a check point of the margin model since R5 lane PAR-MARGIN, so a sample between them measured -- and a request matched at -- a price the bar did not print at that point of its path; a bar that traded nothing is one quote with no path, and its four coincident samples were three path points, three phases and three margin checks that are not there. Nothing in it knows a venue: the rule reads the bar's own prices and volume. Rule 2 (b), knob: none. TradingView's magnified broker walks an intrabar as its four ticks whatever their values (tape `pm2-i3-pooc-mag-0402-2000` of the private fixture set `intrabar_margin`: its first call is booked at an intrabar that opens at its low). What moved: an open-at-its-low bar's second mark is its low, 60, where the sampler put 77.5, and the call armed at that open is booked there; a zero-volume one-price bar gives one mark where it gave four (`tests/test_native_intrabar_turning_points.cpp`, kernel-only), and the sampler test's lower-path expectation moved with it (`tests/test_magnifier_ordered_sampler.cpp`: a sub-bar with a zero-length leg walks its corners). No witness value moved; the corpus runs no lower path (312/312, moved 0). | -| the presented bar -- `current_bar_`, `bar_index_`, `prev_bar_timestamp_` (`include/pineforge/engine.hpp`) | the kernel at every callback point: the consumer presents the bar at the point's instant (`src/native_execution_consumer.cpp`). For the body it runs, the Pine host then presents its own script bar. `scheduler_publish_source_bar` and `scheduler_publish_suppressed_tail` write the whole `current_bar_`, stamped at the bar's label (`current_bar_ = bar` `pine_strategy_host.cpp:2360` and `:2490`). They also write the slot's `bar_index_`, restored after a temporary publication, and each published slot's `prev_bar_timestamp_`. `PineScheduler::input` holds `bar_index_` at the input's own index for one input (`InputBarIndexScope` `pine_scheduler_native.cpp:427`) | **retained, the host's view; the fourth audit's E21 check e**. Execution reads its cursor, not this bar: `execute_current` converts at its cursor's rate and neither writes nor restores the host's clock (Section B's row). The broker-state hash folds none of the three, and `scripts/broker_state_hash_waivers.txt` waives `current_bar_` (a convenience copy) and `bar_index_` (a reconstructed cursor). While the Pine body runs, the host's bar is what these read: generated code's `time` and OHLC reads, the presented-clock readers `open_trade_profit` and `active_account_currency_fx`, and `strategy_stream_state_hash`, which folds `current_bar_` and `prev_bar_timestamp_` (`src/engine_stream.cpp`) | -| the run's words on the codegen-ABI class -- `input_tf_`, `script_tf_`, `script_tf_seconds_`, `last_bar_index_`, `last_bar_time_`, `qty_step_` (`include/pineforge/engine.hpp`) | the Pine host alone. `scheduler_prepare_script_run` sets the timeframe words and the feed's last bar from its scheduler's run spec (`script_tf_seconds_` `pine_strategy_host.cpp:1484`). `apply_realtime_tail_horizon` narrows the last bar to a live probe's horizon, and `prepare_native_begin` copies the symbol's `qty_step` | **retained as the Pine host's run facts**. Generated code reads the timeframe words. The kernel reads them for the report (`src/engine_report.cpp`), the implicit chart feed and the chart-day partition (`src/engine_aux_security.cpp`), all of which a bare host reaches with them empty. A bare host's timeframes are its `NativeRunSpec`'s, which the consumer parses into its own. `last_bar_index_`, `last_bar_time_` and `qty_step_` sit in the hashed region and are waived as feed and symbol metadata; the timeframe words sit outside it. `script_tf_seconds_`'s comment used to place `script_tf_`'s writers in `engine_run.cpp`; it now names the one writer (R5 lane H-DOCGATES) | -| the security-evaluator registry the Pine host fills -- `security_eval_states_`, `security_input_tf_`, `security_next_input_ms_` (`include/pineforge/engine.hpp`) | the Pine host. `declare_security_sites_to_kernel` hands a routable run's sites to the kernel and empties the registry (`security_eval_states_` `pine_strategy_host.cpp:1744`). `scheduler_prepare_security_sequence` sets the input timeframe and resets the next-input cursor, and the auxiliary drive and the scheduler advance that cursor (`src/source/pine_aux_security.cpp`, `src/source/pine_scheduler.cpp`) | **retained by design §2.ii row m**: the kernel keeps the registry, the aggregator and one generic step, and the Pine semantics run in `src/source/pine_security_eval.cpp`. `security_next_input_ms_` is waived: the source scheduler's deferred-boundary state is hashed on the source side. `strategy_stream_state_hash` folds the registry | +| the presented bar -- `current_bar_`, `bar_index_`, `prev_bar_timestamp_` (`include/pineforge/engine.hpp`) | the kernel at every callback point: the consumer presents the bar at the point's instant (`src/native_execution_consumer.cpp`). For the body it runs, the Pine host then presents its own script bar. `scheduler_publish_source_bar` and `scheduler_publish_suppressed_tail` write the whole `current_bar_`, stamped at the bar's label (`current_bar_ = bar` `pine_strategy_host.cpp:2365` and `:2495`). They also write the slot's `bar_index_`, restored after a temporary publication, and each published slot's `prev_bar_timestamp_`. `PineScheduler::input` holds `bar_index_` at the input's own index for one input (`InputBarIndexScope` `pine_scheduler_native.cpp:427`) | **retained, the host's view; the fourth audit's E21 check e**. Execution reads its cursor, not this bar: `execute_current` converts at its cursor's rate and neither writes nor restores the host's clock (Section B's row). The broker-state hash folds none of the three, and `scripts/broker_state_hash_waivers.txt` waives `current_bar_` (a convenience copy) and `bar_index_` (a reconstructed cursor). While the Pine body runs, the host's bar is what these read: generated code's `time` and OHLC reads, the presented-clock readers `open_trade_profit` and `active_account_currency_fx`, and `strategy_stream_state_hash`, which folds `current_bar_` and `prev_bar_timestamp_` (`src/engine_stream.cpp`) | +| the run's words on the codegen-ABI class -- `input_tf_`, `script_tf_`, `script_tf_seconds_`, `last_bar_index_`, `last_bar_time_`, `qty_step_` (`include/pineforge/engine.hpp`) | the Pine host alone. `scheduler_prepare_script_run` sets the timeframe words and the feed's last bar from its scheduler's run spec (`script_tf_seconds_` `pine_strategy_host.cpp:1489`). `apply_realtime_tail_horizon` narrows the last bar to a live probe's horizon, and `prepare_native_begin` copies the symbol's `qty_step` | **retained as the Pine host's run facts**. Generated code reads the timeframe words. The kernel reads them for the report (`src/engine_report.cpp`), the implicit chart feed and the chart-day partition (`src/engine_aux_security.cpp`), all of which a bare host reaches with them empty. A bare host's timeframes are its `NativeRunSpec`'s, which the consumer parses into its own. `last_bar_index_`, `last_bar_time_` and `qty_step_` sit in the hashed region and are waived as feed and symbol metadata; the timeframe words sit outside it. `script_tf_seconds_`'s comment used to place `script_tf_`'s writers in `engine_run.cpp`; it now names the one writer (R5 lane H-DOCGATES) | +| the security-evaluator registry the Pine host fills -- `security_eval_states_`, `security_input_tf_`, `security_next_input_ms_` (`include/pineforge/engine.hpp`) | the Pine host. `declare_security_sites_to_kernel` hands a routable run's sites to the kernel and empties the registry (`security_eval_states_` `pine_strategy_host.cpp:1749`). `scheduler_prepare_security_sequence` sets the input timeframe and resets the next-input cursor, and the auxiliary drive and the scheduler advance that cursor (`src/source/pine_aux_security.cpp`, `src/source/pine_scheduler.cpp`) | **retained by design §2.ii row m**: the kernel keeps the registry, the aggregator and one generic step, and the Pine semantics run in `src/source/pine_security_eval.cpp`. `security_next_input_ms_` is waived: the source scheduler's deferred-boundary state is hashed on the source side. `strategy_stream_state_hash` folds the registry | | the auxiliary-feed seams -- `source_aux_security_feed_enabled`, `source_aux_security_input_view` (`include/pineforge/engine.hpp`, behind `PINEFORGE_HAS_AUX_SECURITY_FEED_V1`) -- and `last_error_` | the kernel declares both seams with an inert default (disabled, no bars: `src/engine_consumer.cpp`). It asks them once, in `prepare_native_security_feeds` (`src/engine_aux_security.cpp`), for the auxiliary slice it feeds the `request.security` evaluators. The Pine host overrides both from `strategy_set_aux_security_feed`'s bars (`src/source/pine_aux_security.cpp`). It writes `last_error_` when that setter refuses its input and when its run's preparation throws | **retained with the `auxiliary_feed` adapter-policy ruling above**. The Pine host drives its own auxiliary slice (lane N7's three measurements), and these seams are how that slice reaches the kernel's feed preparation. A bare host declares `NativeRunSpec::auxiliary_feed` and never meets them. `last_error_` is the handle's one error slot, which any layer that refuses a call writes | | `source_stream_entry_comment` (`include/pineforge/engine.hpp`) | nobody. The kernel's default (`src/engine_consumer.cpp`) and the Pine host's override (`src/source/pine_strategy_host.cpp`) both do nothing, so `stream_observe_entry` keeps the lot's own comment (`src/engine_stream.cpp`) | **dead, kept for the script ABI**. It is a virtual of `BacktestEngine`, which every generated strategy derives from, so deleting it moves every generated script's vtable: an `engine_script_run` epoch. It goes at the next one (`docs/native-refactor-progress.md`, "Scheduled for the next script-ABI epoch") | | the magnifier report facts -- `bar_magnifier_enabled_`, `diag_magnifier_sub_bars_processed_`, `diag_magnifier_sample_ticks_processed_` (`include/pineforge/engine.hpp`) | the kernel, from the run spec's intrabar path and its driver statistics (`src/native_execution_consumer.cpp`). The Pine host writes them again at every bar callback, from its scheduler's magnifier switch and the context's driver statistics (`on_native_bar_open`, `on_native_bar`) | **retained, report-only**: the report's `bar_magnifier_enabled` and magnifier totals (`src/engine_report.cpp`), outside the hashed region. The kernel publishes them for a bare host itself (the C-surface row "kernel-owned report facts" above), so the Pine host's second write is an adapter-thinness question, not a boundary one | | the timeframe report facts -- `diag_script_tf_ratio_`, `diag_needs_aggregation_` (`include/pineforge/engine.hpp`) | the Pine host alone. Its scheduler's run begin stores the ratio of the run spec's script and input timeframes and whether the run aggregates (`diag_script_tf_ratio_` `pine_scheduler_native.cpp:185`, in `PineScheduler::run_begin`), as the retired scheduler's run did before #254. A bare host's report reads 0 for both: the kernel does not write them | **retained, report-only** (R5 lane FIX-E1E2): the report's `script_tf_ratio` and `needs_aggregation` (`src/engine_report.cpp`), outside the hashed region. Execution never reads them | -| `broker_fill_event_seq_` (`include/pineforge/engine.hpp`, in the hashed region) | the Pine host alone, one step per applied broker instruction (`broker_fill_event_seq_` `pine_strategy_host.cpp:721`, in `on_native_applied`); the kernel only resets and folds it | **retained, hashed: the adapter's counter on the kernel's class**. Its placement facts (`signal_close_mc_fill_seq`) compare against it in fill-time gates that cross a bar; the execution authority is the kernel's own ordinal. It is folded into the broker-state hash (`src/engine_state_hash.cpp`), so moving it to the source layer's hash extension moves every run's hash: an epoch decision, not a docs change | -| `broker_state_hashes_` (`include/pineforge/engine.hpp`) | the kernel under `KernelRecorded` (`src/native_execution_consumer.cpp`), and the Pine host at each script point it marks while recording is on (`scheduler_record_broker_hash` `pine_strategy_host.cpp:2790`) | **retained, per driving mode** (design Q4): the report's per-bar hash series, waived as append-only report output that execution never consults | -| the position book's mirror -- `position_qty_`, `position_entry_price_`, `position_entry_count_`, `pyramid_entries_` (`include/pineforge/engine.hpp`, all hashed) | the kernel books and settles them. The Pine host writes them in two places. After every applied event it erases each lot at or below `internal::kQtyEpsilon` and recomputes the position's size, average price and count (`pyramid_entries_` `pine_strategy_host.cpp:660-686`, in `on_native_applied`, restating `ab9714be`'s partial-exit settlement). Before every script body it overwrites `position_entry_count_` with its own entry-slot count (`source_entry_slot_count` `pine_strategy_host.cpp:2427`) | **OPEN, two rulings nobody has made**. The dust sweep erases a lot inside a live book, which the fourth audit found with neither a row nor a tape (its item 32): lane H-THIN's A4-DUST. The count overwrite gives one hashed member two writers with two meanings: the kernel's lots in the current direction, Pine's opened entry slots. It is recorded here first and has no lane yet | -| fields of the kernel's rows the Pine host amends -- `PyramidEntry::time`, `Trade::entry_time`, `Trade::exit_time`, `Trade::close_cause`, `Trade::exit_id`, `Trade::exit_comment`, `Trade::entry_incarnation`, `Trade::exit_from_bracket`, and on a global exit's re-cut rows `Trade::qty`, `Trade::pnl`, `Trade::pnl_pct`, `Trade::commission`, `Trade::max_runup`, `Trade::max_drawdown` (`include/pineforge/engine.hpp`) | the Pine host, inside the applied notification that booked them or at a bar callback. On an aggregated or magnified chart it dates their fills at the chart bar's open (`on_native_applied`; design row AG2), the entry time of a row a later execution of the same pass booked from the lot included (R5 lane FIX-E1E2); their bar indices are the kernel's own script-bar indices, which it books for every host (design row AG1, R5 lane K-IDX), so the host writes no index. It writes no excursion of its own: since R5 lane H-THIN the kernel's sampler books every Pine lot's run-up and drawdown, as it does for any host that keeps the `owns_lot_excursions()` default. The one exception is the re-cut of a global `strategy.exit` fill. When one fill of an exit that names no entry closes the lots of two or more entry ids and leaves the book flat, the host re-cuts that fill's rows into TradingView's children (`order_global_exit_children`, the private fixture set `global_exit_children`). Under `close_entries_rule = "ANY"` it permutes the rows. Otherwise each child is a transaction of its lot's quantity settled oldest lot first, and each slice is a row carrying its share of quantity, P&L, excursions and ten-significant-digit fees; the kernel's book and fills are untouched. It states the close cause from its own labels, clearing the kernel's `Bracket` on its rows (R5 lanes L12 and F3). It relabels an intraday-loss close's `exit_id` and `exit_comment`, swaps `entry_incarnation` on the short-seed report rows (`project_short_seed_report_rows`), and sets `exit_from_bracket` by order family (`exit_from_bracket` `pine_strategy_host.cpp:1283`, in `adapter_label_bracket_trades`) | **retained: the source layer's labels and report dates on the kernel's rows**. A closed row is final for the hash only once its notification returns (`closed_rows_digest`), so the amendments made inside it are folded as the host leaves them. The hash folds a lot's time, entry bar index and two excursions (`src/engine_state_hash.cpp`), and a closed row's entry and exit times, prices, quantity and P&L (`fold_closed_row` `src/native_execution_consumer.cpp`). The labels are not folded. The kernel reads `exit_from_bracket` in one place, `closed_trade_close_cause`, after the row's recorded cause; no kernel path sets it | +| `broker_fill_event_seq_` (`include/pineforge/engine.hpp`, in the hashed region) | the Pine host alone, one step per applied broker instruction (`broker_fill_event_seq_` `pine_strategy_host.cpp:726`, in `on_native_applied`); the kernel only resets and folds it | **retained, hashed: the adapter's counter on the kernel's class**. Its placement facts (`signal_close_mc_fill_seq`) compare against it in fill-time gates that cross a bar; the execution authority is the kernel's own ordinal. It is folded into the broker-state hash (`src/engine_state_hash.cpp`), so moving it to the source layer's hash extension moves every run's hash: an epoch decision, not a docs change | +| `broker_state_hashes_` (`include/pineforge/engine.hpp`) | the kernel under `KernelRecorded` (`src/native_execution_consumer.cpp`), and the Pine host at each script point it marks while recording is on (`scheduler_record_broker_hash` `pine_strategy_host.cpp:2795`) | **retained, per driving mode** (design Q4): the report's per-bar hash series, waived as append-only report output that execution never consults | +| the position book's mirror -- `position_qty_`, `position_entry_price_`, `position_entry_count_`, `pyramid_entries_` (`include/pineforge/engine.hpp`, all hashed) | the kernel books and settles them. The Pine host writes them in two places. After every applied event it erases each lot at or below `internal::kQtyEpsilon` and recomputes the position's size, average price and count (`pyramid_entries_` `pine_strategy_host.cpp:665-691`, in `on_native_applied`, restating `ab9714be`'s partial-exit settlement). Before every script body it overwrites `position_entry_count_` with its own entry-slot count (`source_entry_slot_count` `pine_strategy_host.cpp:2432`) | **OPEN, two rulings nobody has made**. The dust sweep erases a lot inside a live book, which the fourth audit found with neither a row nor a tape (its item 32): lane H-THIN's A4-DUST. The count overwrite gives one hashed member two writers with two meanings: the kernel's lots in the current direction, Pine's opened entry slots. It is recorded here first and has no lane yet | +| fields of the kernel's rows the Pine host amends -- `PyramidEntry::time`, `Trade::entry_time`, `Trade::exit_time`, `Trade::close_cause`, `Trade::exit_id`, `Trade::exit_comment`, `Trade::entry_incarnation`, `Trade::exit_from_bracket`, and on a global exit's re-cut rows `Trade::qty`, `Trade::pnl`, `Trade::pnl_pct`, `Trade::commission`, `Trade::max_runup`, `Trade::max_drawdown` (`include/pineforge/engine.hpp`) | the Pine host, inside the applied notification that booked them or at a bar callback. On an aggregated or magnified chart it dates their fills at the chart bar's open (`on_native_applied`; design row AG2), the entry time of a row a later execution of the same pass booked from the lot included (R5 lane FIX-E1E2); their bar indices are the kernel's own script-bar indices, which it books for every host (design row AG1, R5 lane K-IDX), so the host writes no index. It writes no excursion of its own: since R5 lane H-THIN the kernel's sampler books every Pine lot's run-up and drawdown, as it does for any host that keeps the `owns_lot_excursions()` default. The one exception is the re-cut of a global `strategy.exit` fill. When one fill of an exit that names no entry closes the lots of two or more entry ids and leaves the book flat, the host re-cuts that fill's rows into TradingView's children (`order_global_exit_children`, the private fixture set `global_exit_children`). Under `close_entries_rule = "ANY"` it permutes the rows. Otherwise each child is a transaction of its lot's quantity settled oldest lot first, and each slice is a row carrying its share of quantity, P&L, excursions and ten-significant-digit fees; the kernel's book and fills are untouched. It states the close cause from its own labels, clearing the kernel's `Bracket` on its rows (R5 lanes L12 and F3). It relabels an intraday-loss close's `exit_id` and `exit_comment`, swaps `entry_incarnation` on the short-seed report rows (`project_short_seed_report_rows`), and sets `exit_from_bracket` by order family (`exit_from_bracket` `pine_strategy_host.cpp:1288`, in `adapter_label_bracket_trades`) | **retained: the source layer's labels and report dates on the kernel's rows**. A closed row is final for the hash only once its notification returns (`closed_rows_digest`), so the amendments made inside it are folded as the host leaves them. The hash folds a lot's time, entry bar index and two excursions (`src/engine_state_hash.cpp`), and a closed row's entry and exit times, prices, quantity and P&L (`fold_closed_row` `src/native_execution_consumer.cpp`). The labels are not folded. The kernel reads `exit_from_bracket` in one place, `closed_trade_close_cause`, after the row's recorded cause; no kernel path sets it | | the closed-row counters -- `win_trades_count_`, `loss_trades_count_`, `eventrades_count_`, `gross_profit_sum_`, `gross_loss_sum_` (`include/pineforge/engine.hpp`) | the kernel, once per row it books (`engine_orders.cpp`); the Pine host re-counts them over the children's rows when it re-cuts a global exit's fill into more rows than the kernel booked (`order_global_exit_children`), so `strategy.wintrades`, `strategy.losstrades` and `strategy.eventrades` count the rows the report shows | **retained: the counts follow the report's rows**. Hashed (`src/engine_state_hash.cpp`) and folded as the host leaves them; the re-cut runs inside the applied notification, before the next hash read. `net_profit_sum_` is not re-counted: the equity stays the kernel's | | the range-end report -- `range_end_trades_`, the extremes `max_equity_`, `min_equity_`, `max_drawdown_`, `max_runup_`, and `stream_warmup_mode_` (`include/pineforge/engine.hpp`) | the Pine host: `scheduler_record_range_end` produces report-only terminal rows and preserves ordinary stored marks in REALTIME; `PineStrategyHost::present_report` shapes only the caller-owned report copy through the engine's generic default-no-op presentation seam, before metrics are derived. Historical batch keeps its existing terminal re-mark and extreme fold. Complete-prefix stream reports equal batch from the first realtime script bar; an open position at zero realtime bars retains the warmup mark, and mid-bar reads are host presentation, not a pinned batch boundary. The stream hash contract changes: hashed extremes no longer fold hypothetical terminal marks in REALTIME, so stream and batch terminal hashes need not match; restart/replay binds the exact library digest | **adapter-policy, the `report_open_position_at_end` row above** (design RP5 and §3.7 part 2): report shape, not a mark-to-market row, and the kernel's run-end producer is gated out of every Pine run. The extremes are hashed; the report rows are waived as report-only | | `host_mutation_guard_inert_` (`include/pineforge/engine.hpp`) | the Pine host, once, in its constructor (`host_mutation_guard_inert_ = true` `pine_strategy_host.cpp:159`) | **retained, the host's declared choice**: an in-run call of a frozen C setter is a no-op on a Pine handle, as `ab9714be`'s legacy consumer refused it, and throws on a native host. Outside the hashed region | @@ -710,13 +710,13 @@ Every line number in this section names its symbol on this tree, which | `NativeRunSpec` field | ruling | what the adapter runs instead | measurement and ruling of record | executed native consumers | |---|---|---|---|---| | `price_grid`, `grid_rounding` | **native-only** | TradingView's per-order-kind tick rules on top of `None`: `source_trigger_threshold` (`pine_adapter.cpp:384`), `source_level_on_price_grid` (`:367`), `nearest_tick` (`:274`) / `source_bar_fill_tick` (`:295`) / `directional_tick` (`:327`), behind the terms seam | R5-3 and design risk E7 ruled `None` for the adapter before the lane ran; lanes R7 and N13 measured the alternative anyway (raw levels submitted, `QuantizeFillsAndTriggers` with `HalfUp` declared). L8b closed the first blocker (no run aborts). The second has no remedy on either side: TradingView quantizes per order kind (stop and limit legs and a trail's activation on the quantized bar; the trail stop, the running best, stop-limit entries and the `calc_on_order_fills` cursors raw), the grid is one rule for the run, and the old N13 trial moved 30 pinned checks in 4 units; this is historical evidence, not a current-head result; a per-kind mask would spell that inconsistency into the kernel. The corpus cannot arbitrate (every probe runs a 0.01 tick on an on-grid feed; 5 of 312 differ in an engine-only column). Permanent witness: `tests/test_adapter_grid_relower.cpp`, whose section 5 pins the trail stop the grid fires a bar early. Design row PG and §3.6 | `examples/native/native_price_grid_strategy.cpp`, `examples/native/native_price_grid_c.c`, the private test `test_native_price_grid` | -| `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:17481`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:20856`), `chart_day_key` (`pine_adapter.cpp:17378`), `compat::pine::IntradayCap` with `IntradayOrderBudget` (337 lines) | Structural first: Pine's risk calls are per-bar statements, so a limit reaches the adapter on script bar 0, after `project()` (`pine_strategy_host.cpp:345`) and the kernel's `configure` (`pine_strategy_host.cpp:354`) have fixed and digested the spec. In substance (lane N12, `tests/test_adapter_risk_relower.cpp`, 62 checks over nine paired scenarios): the drawdown latch samples at the close only and still admits a reversal; the loss-day streak counts trades, not days; the intraday loss closes at the path's adverse extreme, refuses every placement and withdraws the book; the fill cap charges slots, transfers quota and closes at the bar's better extreme on the chart timezone's day. With the kernel seeded on the corpus, 3 of 4 cap probes diverge (3840 of 3916, 312 of 604, 2370 of 2384 rows; re-derived by `scripts/check_seeded_risk_experiment.sh` since R5 lane H-MEASURE). `strategy.risk.allow_entry_in` is the one rule that is the kernel's already (`allowed_open_directions`). Design §3.6 | `examples/native/native_risk_limits_strategy.cpp`, `examples/native/native_trail_risk_strategy.cpp`, `tests/test_native_risk_limits.cpp`, `tests/test_native_c_api.c` | -| `max_abs_units` | **adapter-policy** | `strategy.risk.max_position_size` as a gate on the LIVE book before the fill (`max_position_size` `pine_adapter.cpp:16490-16491`): an entry is refused once the book already holds the limit | design row MG2 (R5-1): the resulting-book cap is the generic one. Measured by N12's scenario `PS` in `tests/test_adapter_risk_relower.cpp`: two-unit entries against a limit of 3 leave the adapter at 4 and the kernel cap at 2 | `tests/test_native_resting_matching_contract.cpp`, `tests/test_native_run_spec.cpp` | +| `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:17483`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:20858`), `chart_day_key` (`pine_adapter.cpp:17380`), `compat::pine::IntradayCap` with `IntradayOrderBudget` (337 lines) | Structural first: Pine's risk calls are per-bar statements, so a limit reaches the adapter on script bar 0, after `project()` (`pine_strategy_host.cpp:350`) and the kernel's `configure` (`pine_strategy_host.cpp:359`) have fixed and digested the spec. In substance (lane N12, `tests/test_adapter_risk_relower.cpp`, 62 checks over nine paired scenarios): the drawdown latch samples at the close only and still admits a reversal; the loss-day streak counts trades, not days; the intraday loss closes at the path's adverse extreme, refuses every placement and withdraws the book; the fill cap charges slots, transfers quota and closes at the bar's better extreme on the chart timezone's day. With the kernel seeded on the corpus, 3 of 4 cap probes diverge (3840 of 3916, 312 of 604, 2370 of 2384 rows; re-derived by `scripts/check_seeded_risk_experiment.sh` since R5 lane H-MEASURE). `strategy.risk.allow_entry_in` is the one rule that is the kernel's already (`allowed_open_directions`). Design §3.6 | `examples/native/native_risk_limits_strategy.cpp`, `examples/native/native_trail_risk_strategy.cpp`, `tests/test_native_risk_limits.cpp`, `tests/test_native_c_api.c` | +| `max_abs_units` | **adapter-policy** | `strategy.risk.max_position_size` as a gate on the LIVE book before the fill (`max_position_size` `pine_adapter.cpp:16492-16493`): an entry is refused once the book already holds the limit | design row MG2 (R5-1): the resulting-book cap is the generic one. Measured by N12's scenario `PS` in `tests/test_adapter_risk_relower.cpp`: two-unit entries against a limit of 3 leave the adapter at 4 and the kernel cap at 2 | `tests/test_native_resting_matching_contract.cpp`, `tests/test_native_run_spec.cpp` | | `max_open_lots` | **adapter-policy** | Pine pyramiding is a per-cycle entry count in the adapter's command policy; a resting source entry must not consume a physical-lot cap before it fills, so `project()` leaves the cap unset (`max_open_lots` `pine_adapter.cpp:2646-2648`) | design row MG3 (R5-1); the contract comment in `project()`. Measured against TradingView by R5 lane H-MEASURE (the private test `test_pyramiding_count_differential`, 15 scenarios on three `lab tv` tapes): TradingView checks an entry once, at its first eligible point, against the trades then open, and does not check a resting entry again at its fill; the kernel's cap books 14 of the 15 as TradingView does, the adapter's per-cycle count 13 (it keeps a slot a close or another entry's exit drained; since lane W8A-SIGSTATE-1's R-B a resting LIMIT entry no longer counts, and one the count admitted fills over the cap, and since lane W5B-ENG-MARGIN-RESIDUAL's SB the later market entries of a flat bar are refused at the fill once the book holds the cap, as on TradingView's tapes). A recorded divergence: the retention stands pending a re-lowering ruling. R5 lane H-THIN tried the lowering and kept the count: on the kernel's cap four corpus probes moved away from TradingView, for want of the first-eligible-point rule (H-MEASURE's P4b); it fixed the reservation divergence measured beside it (P10), so a default `strategy.exit(from_entry)` under FIFO now reserves its entry's own quantity, the oldest lots first, as TradingView does | `tests/test_native_resting_matching_contract.cpp`, `tests/test_native_margin_model.cpp` | | `initial_margin_fraction` | **adapter-policy** | TradingView's ten-significant-digit money admission against the signal-time tuple, answered as `AdmitWithHostMargin`; the `margin` model the adapter does declare is maintenance-only (`NativeMarginModel` `pine_adapter.cpp:2885-2893`, `margin.maintenance_long` `pine_adapter.cpp:2900`) | design row MG4 and the wave-4 ruling recorded in `project()`: a positive initial requirement would decline openings TradingView takes. Measured both ways by R5 lane H-MEASURE (the private test `test_adapter_margin_schedule_differential`, M11): the kernel's gate declines a 1x long whose entry fee the adapter admits (`InitialMargin`), and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times | `tests/test_native_precommit_view.cpp`, `tests/test_native_margin_model.cpp` | -| `report_open_position_at_end` | **adapter-policy** | The Pine host owns TradingView's range-end shape: batch re-marks the stored final point and re-folds extremes, while REALTIME retains ordinary marks and `PineStrategyHost::present_report` re-marks only the caller-owned copy before generic metric derivation; the stream hash excludes that terminal extreme fold (`scheduler_record_range_end` pine_strategy_host.cpp:1982): report shape, not a mark-to-market row (`KernelRecordedAtHostMarks` pine_adapter.cpp:2855) | design row RP5; the kernel reads the field under `KernelRecorded` only, which `scripts/check_adapter_spec_shadowing.py` gates | `examples/native/native_sized_report_strategy.cpp`, `tests/test_native_report_truth.cpp` | +| `report_open_position_at_end` | **adapter-policy** | The Pine host owns TradingView's range-end shape: batch re-marks the stored final point and re-folds extremes, while REALTIME retains ordinary marks and `PineStrategyHost::present_report` re-marks only the caller-owned copy before generic metric derivation; the stream hash excludes that terminal extreme fold (`scheduler_record_range_end` pine_strategy_host.cpp:1987): report shape, not a mark-to-market row (`KernelRecordedAtHostMarks` pine_adapter.cpp:2855) | design row RP5; the kernel reads the field under `KernelRecorded` only, which `scripts/check_adapter_spec_shadowing.py` gates | `examples/native/native_sized_report_strategy.cpp`, `tests/test_native_report_truth.cpp` | | `open_bar_view` | **adapter-policy** | `Complete`: TradingView's bar-open scheduling and its `calc_on_order_fills` callback read the whole script bar (`pine_adapter.cpp:2825-2827`) | design rows CT4 and E5: the open-only view is opt-in because the adapter needs the full bar | `examples/native/native_calc_on_fills_strategy.cpp`, `tests/test_native_calc_timing.cpp` | -| `subscriptions` | **adapter-hook** | `declare_timeframe_subscriptions` from the begin-time hook (`pine_strategy_host.cpp:1712`): a plain `request.security` site is a kernel subscription | lane R3b over the L6c hook: 21 of the corpus's 23 `request.security` probes run their sites on the kernel, byte-identical. The predicate (`declare_security_sites_to_kernel` `pine_strategy_host.cpp:1703`) declares every site of a run or none. A run keeps all of its sites on the source evaluator when it is a stream's warmup, runs the bar magnifier, cuts its range start to na (KI-55), projects historical lookahead, installs an auxiliary feed, aggregates its chart or has no detected timeframe. It keeps them there too when any one site uses `lookahead_on`, `ticker.heikinashi`, a lower timeframe (requested, emulated, read from the input, or as an array), a publication gate, or a calling-bar close or open latch; when a forex or CFD intraday chart without a native feed asks for `D`; when a site asks for N days, whose bars TradingView counts in the symbol's trading dates from each year's first (`tests/test_security_multi_day_buckets_tapes.cpp`), not on the kernel's calendar-day grid; when the registry's `sec_id`s are not dense from 0; and when the kernel refuses the declaration. R5 lane H-MEASURE pins each reachable condition with what it changes (`tests/test_adapter_security_route_conditions.cpp`, a site through the evaluator against the same site through a bare kernel host): on the feeds measured the magnifier and an aggregated chart change no delivered value, while the range-start cut, the projection's incomplete tail, `lookahead_on` and the OTC pins do; the four lower-timeframe flags, the publish gate and the calling-bar latches are subsumed by other clauses, and the empty or undetected timeframe checks are unreachable after begin-time validation | `examples/native/native_htf_strategy.cpp`, `tests/test_native_htf_subscriptions.cpp` | +| `subscriptions` | **adapter-hook** | `declare_timeframe_subscriptions` from the begin-time hook (`pine_strategy_host.cpp:1717`): a plain `request.security` site is a kernel subscription | lane R3b over the L6c hook: 21 of the corpus's 23 `request.security` probes run their sites on the kernel, byte-identical. The predicate (`declare_security_sites_to_kernel` `pine_strategy_host.cpp:1708`) declares every site of a run or none. A run keeps all of its sites on the source evaluator when it is a stream's warmup, runs the bar magnifier, cuts its range start to na (KI-55), projects historical lookahead, installs an auxiliary feed, aggregates its chart or has no detected timeframe. It keeps them there too when any one site uses `lookahead_on`, `ticker.heikinashi`, a lower timeframe (requested, emulated, read from the input, or as an array), a publication gate, or a calling-bar close or open latch; when a forex or CFD intraday chart without a native feed asks for `D`; when a site asks for N days, whose bars TradingView counts in the symbol's trading dates from each year's first (`tests/test_security_multi_day_buckets_tapes.cpp`), not on the kernel's calendar-day grid; when the registry's `sec_id`s are not dense from 0; and when the kernel refuses the declaration. R5 lane H-MEASURE pins each reachable condition with what it changes (`tests/test_adapter_security_route_conditions.cpp`, a site through the evaluator against the same site through a bare kernel host): on the feeds measured the magnifier and an aggregated chart change no delivered value, while the range-start cut, the projection's incomplete tail, `lookahead_on` and the OTC pins do; the four lower-timeframe flags, the publish gate and the calling-bar latches are subsumed by other clauses, and the empty or undetected timeframe checks are unreachable after begin-time validation | `examples/native/native_htf_strategy.cpp`, `tests/test_native_htf_subscriptions.cpp` | | `auxiliary_feed` | **adapter-policy** | the adapter's own auxiliary drive: the chart-slice mapping and the deferred first bucket (`src/source/pine_aux_security.cpp`) | lane N7, retained on three measurements: 0 of 312 corpus probes install an auxiliary feed; TradingView's chart slice leaves pre-range coverage inert where the kernel folds it by time, and evaluates after the bar's matching pass where the kernel delivers before it (`tests/test_native_auxiliary_feed_twin.cpp`, rows B and C). Design §2.iv | `tests/test_native_auxiliary_feed.cpp`, `tests/test_native_auxiliary_feed_stream.cpp` | | `quantity_tolerance` | **adapter-policy** | TradingView's `1e-10` quantity rules on top of the exact default: a close within `1e-10` of the held position is a `Flatten`, a batched process-orders-on-close (POOC) close ending within `internal::kQtyEpsilon` (`1e-10`) of an interior lot boundary is one selected `Flatten` of whole lots (`source_fifo_prefix_openings`); an ambiguous ordinary deferred FIFO `strategy.close` instead uses `HostSized` to authenticate the exact physical FIFO prefix sum for its `Reduce` at fill time, with that same tolerance (`source_fifo_close_units`, `src/source/pine_adapter.cpp`). Detection checks only the current prefix and prefixes reachable after already-pending same-bar closes, so non-ambiguous request shapes, hashes and readback remain unchanged. Same-bar market transactions, `immediately` closes and the ShortSeed placeholder remain out of scope, and after every fill `PineStrategyHost::on_native_applied` erases lots of at most `kQtyEpsilon` without a closing row (`src/source/pine_strategy_host.cpp`) | lane K-ULP4 (the owner-level ruling of audit X1, part b): opt-in and absent by default, so every adapter run is byte-identical by construction. The tolerance charges a snapped `Reduce` its request and books every dust lot it closes as a row, where the adapter's prefix `Flatten` is charged the lots it holds and its sweep books no row; declaring it would move the adapter's fills, ledgers and hashes, so whether the adapter can move onto it is a later lane's measurement. R5 lane H-THIN measured each of those comparisons (design §3.9b) and kept them: made exact, the sweep fails its own tape row (`test_pine_dust_sweep_paired`) | `tests/test_native_quantity_tolerance.cpp` | @@ -764,7 +764,7 @@ number names its symbol on this tree. | `strategy_configure_native_v1` (`c_abi.cpp:980`) versus `strategy_configure_native_ext_result_v1` (`native_c_host.cpp:4066`) | **retained legacy base plus typed additive route** | A base specification the kernel rejects still returns `-1` and latches `Failed`, preserving its frozen caller contract; a Ready misuse retains that Contract latch. The C boundary refuses arguments, a Running phase and a Completed hash hook without entering kernel configure, so a read cannot reconfigure its host. The extended result call validates before its first configure, writes `pf_native_spec_error_t` / `pf_native_spec_field_t`, and names reused-host session-key and run-number refusals; no old signature or layout moved. | | `strategy_configure_native_fx_curve_v1` (`pineforge.h:546`) | **typed additive route** | `strategy_configure_native_fx_curve_ext_v1` writes the C twin of `NativeFxCurveValidation` and its offending index. The old `-1` route remains unchanged, while a C caller can now distinguish a decreasing timestamp, non-positive rate, allocation failure or wrong phase. | | `on_native_timeframe_bar` (`native_host.hpp:885`) and `NativeTimeframeBarContext::interval` (`native_host.hpp:809`) | **query routed** | The callback signature stays frozen. `strategy_native_timeframe_bar_interval_v1` is valid only during that callback and copies the kernel calendar interval without deriving it from the delivered bar or delivery timestamp. | -| `magnifier_sub_bars_total` / `magnifier_sample_ticks_total` (`engine_report.cpp:75-76`) | **kernel-owned report facts** | The generic intrabar driver already increments `NativeDriverStatistics::sub_bars_processed` / `sample_ticks_processed` for every retained path (`native_execution_consumer.cpp:8681-8693`); the kernel now publishes those same counters for bare hosts. They are report fields, not new durable state: the existing `native_continuation_hash` fold of `NativeDriverStatistics` (`native_execution_consumer.cpp:1125-1130`) sees the same values, so no hash contract moves. | +| `magnifier_sub_bars_total` / `magnifier_sample_ticks_total` (`engine_report.cpp:75-76`) | **kernel-owned report facts** | The generic intrabar driver already increments `NativeDriverStatistics::sub_bars_processed` / `sample_ticks_processed` for every retained path (`native_execution_consumer.cpp:8749-8761`); the kernel now publishes those same counters for bare hosts. They are report fields, not new durable state: the existing `native_continuation_hash` fold of `NativeDriverStatistics` (`native_execution_consumer.cpp:1125-1130`) sees the same values, so no hash contract moves. | | `pf_native_lot_excursion_v1` (`native_c_api.h:1912`) | **additive fee tail** | `entry_commission` carries the C++ `ClosedLotExcursionFacts::entry_commission` in account currency. The callback table publishes a trailing layout marker so F4 callers still receive the base facts length; current callers receive the fee tail without changing `PF_NATIVE_API_VERSION`. | | `strategy_outputs_set_enabled` and the eight `strategy_outputs_*` readers (`pineforge.h`, group `pf_outputs`) | **additive runtime exports** | Recording is off by default, and nothing a run computes reads the record. The event copy is size-prefixed like `strategy_pending_order_get`, and `PF_ABI_VERSION` stays 4. The writers are C++ only in this version (the BASE-CLASS SEAMS rows of `native_c_api.h`): a C host declares no outputs, so its handle answers every reader as empty. | @@ -842,7 +842,7 @@ The table records, at the tree the doc wave read, what each block described and | `PhysicalExecutionContext::account_fx` `include/pineforge/execution.hpp:92`, read by `build_close_trade_with_costs` `src/engine_orders.cpp:70-87` and handed by `append_open_position_report_rows` `src/native_execution_consumer.cpp:8961-8982` | a closing or mark-to-market row converts at the account-currency rate its execution names -- a mark at a report point's instant, a fill at its own cursor -- and at the presented clock's rate when it names none, as before (R5 lane B-ADAPTER, item 3) | the rate is data a host of any venue supplies with the row it asks for; nothing branches on a platform, and the row's currency is the basis its owner's magnitudes are recorded in | none: no spec field; a caller names the rate per call, and a call naming none converts exactly as before | `tests/test_adapter_range_end_fx.cpp` (a stepped FX curve at seven instants around the range end, plain and aggregated charts) | | `NativeExecutionConsumer::execute_current` `src/native_execution_consumer.cpp:7508` | a current execution converts at its cursor's rate, threaded through its terms facts, inspection, preview and settlement contexts, and neither writes nor restores the host's presented clock, so its own hooks and the host after it see the frame's clock; its preview `inspect_current_execution` still converts at the presented clock (R5 lane B-ADAPTER, item 5) | an execution converting at its own instant's rate is the same for every host and venue, and the host's presented clock stays its own | none: the rate is the one the run already chose (the cursor's instant), carried as data | `tests/test_native_current_execution_rate.cpp` (kernel-only) | | `NativeExecutionConsumer::validate_current_execution` `src/native_execution_consumer.cpp:7231` | a current execution refuses a request bound to a host roster (`BindCohort`) as `UnsupportedRequest`, before its point is taken and whatever the roster holds: it admits exactly the owners the evaluation's current shape executes (`current_shape` `src/native_order.cpp:383`: `Independent`, `BindOpening`, `BindOpenings`). Since R4 slice C it had admitted `BindCohort` as well, which the evaluation then left unevaluated, so a roster holding a live lot failed the run with "native current evaluated allowance mismatch" and an empty one answered `UnreadyOwner`; its read-only preview (`inspect_current_execution`) settled what the execution could not (R5 lane PAR-ORDERS-2) | one legality rule, the evaluation's own, decides a command before any point is taken, the same for every host and for the C surface (`strategy_native_execute_current_v1` answers through this validation); a roster read at the match is not a membership fixed at the command, which is what the documented contract names (`docs/pages/native-engine.md` "Selected exposure and current execution") | none: no spec field or hook. A refused request stays queued and fills at the next match like any market close; a host that wants a cohort's lots closed at once fixes its roster first (`BindOpenings`), as the Pine adapter's immediate closes do | `tests/test_native_current_cohort_refusal.cpp` (kernel-only: a live and an empty roster refused, then filled as queued closes; a fixed roster executing at once) , `tests/test_native_cohort_sliced_close.cpp` (its preview rows: the roster close's preview is refused, the same selection's settles) and `tests/test_adapter_live_state_equivalence.cpp` (reversals seed 3246599, which the failure used to end) | -| the chart symbol's daily partition -- `prepare_chart_day_partition` `src/engine_aux_security.cpp:358`, kept in `chart_day_partition_` `include/pineforge/engine.hpp:1218` and presented to the language runtime through `RuntimeAmbient::day_partition` `src/runtime_ambient.hpp:38` (the TradingView comment above `NativeDayPartition` in `include/pineforge/timeframe.hpp`) | on an intraday chart whose run installed its own symbol's `D` feed, the chart-level D period follows that feed's stamps: the exchange's trade-date bars, a holiday-merged day included. It governs `time("D")`, `timeframe.change("1D")`, `ta.change(time("D"))`, the default-anchored `ta.vwap` and `crosses_boundary(DAY)`. Without the feed, on a 1D chart or on another symbol's clock every rule is the nominal session day | the stamps are the venue's own data, as for the authoritative-feed partition above: another venue is driven by installing its own daily bars, and nothing branches on a platform | the policy knob is installing the feed or not, so a "which daily partition" field would restate it. Only the Pine host builds it (`prepare_chart_day_partition` `pine_strategy_host.cpp:1646`, `:1687`) and presents it for each bar it publishes (`AmbientDayPartitionScope` `pine_strategy_host.cpp:2389`). A bare host never calls the builder, so its calendar stays nominal | the private test `test_o_close_pct_day_anchor` (the partition is built from the D feed, and none without it) and `tests/test_chart_day_memo.cpp`; the tape is `lab tv` `o-cme-dayanchor-full`, recorded at the site | +| the chart symbol's daily partition -- `prepare_chart_day_partition` `src/engine_aux_security.cpp:358`, kept in `chart_day_partition_` `include/pineforge/engine.hpp:1218` and presented to the language runtime through `RuntimeAmbient::day_partition` `src/runtime_ambient.hpp:38` (the TradingView comment above `NativeDayPartition` in `include/pineforge/timeframe.hpp`) | on an intraday chart whose run installed its own symbol's `D` feed, the chart-level D period follows that feed's stamps: the exchange's trade-date bars, a holiday-merged day included. It governs `time("D")`, `timeframe.change("1D")`, `ta.change(time("D"))`, the default-anchored `ta.vwap` and `crosses_boundary(DAY)`. Without the feed, on a 1D chart or on another symbol's clock every rule is the nominal session day | the stamps are the venue's own data, as for the authoritative-feed partition above: another venue is driven by installing its own daily bars, and nothing branches on a platform | the policy knob is installing the feed or not, so a "which daily partition" field would restate it. Only the Pine host builds it (`prepare_chart_day_partition` `pine_strategy_host.cpp:1651`, `:1692`) and presents it for each bar it publishes (`AmbientDayPartitionScope` `pine_strategy_host.cpp:2394`). A bare host never calls the builder, so its calendar stays nominal | the private test `test_o_close_pct_day_anchor` (the partition is built from the D feed, and none without it) and `tests/test_chart_day_memo.cpp`; the tape is `lab tv` `o-cme-dayanchor-full`, recorded at the site | | the bar-time accessors -- `_decompose_bar_time` and `_bar_hour` through `_bar_weekofyear` `include/pineforge/engine.hpp:1066-1093` | the presented bar's stamp broken into calendar fields in UTC, memoized per stamp | a function of the stamp alone, and UTC is the engine's storage timezone | none: they read no spec field and fold into no digest. They are protected `BacktestEngine` members, part of the codegen contract. Their comment claimed that generated code reads them for Pine's bare `hour` / `minute` / `dayofweek`. It does not: the codegen transpiler emits `pine_hour(current_bar_.timestamp, syminfo_.timezone)` and its siblings on the exchange timezone (`pineforge_codegen/codegen/tables.py` at the corpus's pinned commit `f3285d79`), which are the language calendar functions above. No generated strategy in `corpus/` names a `_bar_*` accessor. The comment now says so (R5 lane H-DOCGATES) | `tests/test_chart_timezone.cpp` (`_bar_hour()` is the UTC hour) and `tests/test_intraday_rollover_chart_tz_l4a.cpp` | | the conventional offset spellings of `timezone_accepted` `include/pineforge/native_calendar.hpp:208` (its comment's "TV sign" line) | `UTC+05:30` and `GMT-4` mean what they say: five and a half hours ahead of UTC, four behind (ISO 8601's sign), where a POSIX `TZ` string of the same shape means the opposite; a POSIX rule string keeps POSIX's own sign | a zone spelling is data, and the sign is the one a venue printing `UTC+8` means; nothing branches on a platform | a sign-convention switch over a spelling whose meaning is fixed | `tests/test_native_calendar.cpp` (`resolve_civil("UTC+05:30", …)` and `resolve_civil("GMT-4", …)` against their fixed instants) | | the grid and lot helpers -- `tick_grid_price` `include/pineforge/engine.hpp:799`, `price_grid_decimals` `include/pineforge/engine.hpp:833`, `level_on_price_grid` `include/pineforge/engine.hpp:843`, `apply_qty_step` `include/pineforge/engine.hpp:932`, `apply_exit_qty_step` `include/pineforge/engine.hpp:952` (their TradingView calibration comments sit above each) | nearest-tick and price-grid quantization and the lot-increment floors, each calibrated on TradingView tapes | **not a kernel mechanism**: no kernel or source-layer code calls any of them (`tests/test_f6_dead_kernel_members.cpp`, witness 2). The adapter runs its own copies of TradingView's per-order-kind tick rules (ruled native-only in the `price_grid` row above) and sizes its own lots; a native host declares a price grid and a quantity grid in its run spec | nothing to spell. They are rule 5's legacy, kept only because the twin-parity freeze (`scripts/check_twin_parity.py`, base `ab9714be`) holds the checks of the suites that reach them, and they leave with those checks | `tests/test_stop_tick_rounding_l4d.cpp`, `tests/test_level_grid_snap_l4d.cpp`, `tests/test_qty_step_epsilon_floor_l4b.cpp` | diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index d1ffdf4ad..d01bc41e7 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -47,123 +47,123 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:3434; `entry()` pine_adapter.cpp:7157, `order()` pine_adapter.cpp:14610) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:6511; `submit` native_host.hpp:1213 | K | — | F:A1 O:A1 S:O2 | — | -| OL2 | Intents: flatten, reduce by units, reduce owner-opened units, transact, reverse-to (`close_all()` pine_adapter.cpp:10541 → `Flatten{}` pine_adapter.cpp:10575; `Reduce{ExplicitUnits}` pine_adapter.cpp:9414; `ReverseTo` pine_adapter.cpp:7833) | **yes** — `OrderIntent` native_order.hpp:229, reduce sizes `ReductionSize` native_order.hpp:215, `ReverseTo` native_order.hpp:60-62, `OpeningShape` native_order.hpp:910-914, `Flatten` native_order.hpp:36 | K | — | F:A4 F:A8 O:A2 O:A3 S:O1 | — | -| OL3 | Amend / cancel one working order (`submit_or_replace` pine_adapter.cpp:4361 → `host.replace` pine_adapter.cpp:4786) | **yes** — `replace` native_host.hpp:1218-1220, `cancel` native_host.hpp:1249; `ReplacedEvent` native_order.hpp:1017-1031; result and event types `SubmitStatus` native_order.hpp:835-861, `ReplacedEvent` native_order.hpp:1001-1067 | K | — | F:A2 O:A4 S:O3 | id → handle map is host bookkeeping (F) → OL4 | -| OL4 | Cancel all / cancel by label; label → handle book (`cancel(id)` pine_adapter.cpp:14504, `cancel_all` pine_adapter.cpp:14577; `live_by_source_key_` pine_adapter.hpp:2521; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4799) | **partial** — single-handle `cancel` native_host.hpp:1249; no enumeration (`WorkingRequestCore::live()` is consumer-internal native_order.hpp:1703); labels exist but are unindexed (`Request::label` native_order.hpp:485) **Closed:** `cancel_all` native_host.hpp:1267, `cancel_where(comment)` native_host.hpp:1272 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1272 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1263 is the enumeration; the id → handle book is `native_toolkit::OrderBook`, host-side and Pine-free (`tests/test_native_order_ergonomics.cpp`, `tests/test_native_anchored_legs.cpp`). | K+A | L7 | F:A3 F:A2 O:A5 O:A14 S:O3 | **Disagree:** F partial / O no (A5) + partial (A14) / S yes (folded into O3). R5-1: cancel-all / by-label is generic → kernel. TV state inheritance across a same-id re-issue stays A. The adapter's `strategy.cancel` / `strategy.cancel_all` keep their per-handle loops, measured by R5 lane F7 (audit M27): TradingView cancels in the SOURCE book, which holds commands of the same script call that have not reached the kernel yet (and a live `strategy.close` is labelled `__close__` there), so at the same point `cancel_where(id, NativeRequestField::Label)` and `cancel_all()` withdraw 0 — the close flattens at the next open, the reversal lands — where the source cancels withdraw them (`tests/test_adapter_brackets_relower.cpp`, M27 rows); each withdrawn source handle is also retired one by one. | -| OL5 | Per-point fill capacity, partial fills, residual working quantity (Pine uses only `ImmediateRemaining` pine_adapter.cpp:5005; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:9160) | **yes** — `Capacity` / `PointBudget` native_order.hpp:379-383, `Remaining` native_order.hpp:557-579, allowances `Allowance` native_order.hpp:662-675 | K | — | O:A16 S:O4 | — | -| OL6 | Ownership: legs bound to a parent / opening(s) / cohort; close the lots of a given entry, FIFO or ANY (`owner_for_close` pine_adapter.cpp:6577, `cohort_for` pine_adapter.cpp:3010; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:9255) | **yes** — `Owner` native_order.hpp:464-479, `Authority` native_order.hpp:620-621, `CohortClose` native_order.hpp:617-619, `SelectedExposure` native_order.hpp:625-629, `cohort_open` native_host.hpp:1282, `CancelReason::OwnerGone` native_order.hpp:949 | K | — | F:A6 O:A8 O:A9 S:O5 | FIFO *ordering* is host-computed (O) | -| OL7 | OCA cancel / reduce groups (`group_for` pine_adapter.cpp:3443) | **yes** — `Group` / `GroupEffect` native_order.hpp:466-473, `CancelReason::Group` native_order.hpp:948-953, `ReservationReducedEvent` native_order.hpp:1191-1201 (deferred form `DeferredGroupAdjustmentEvent` native_order.hpp:1211-1221) | K | — | F:A12 O:A6 O:A7 S:O7 | — | +| OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:3436; `entry()` pine_adapter.cpp:7159, `order()` pine_adapter.cpp:14612) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:6579; `submit` native_host.hpp:1213 | K | — | F:A1 O:A1 S:O2 | — | +| OL2 | Intents: flatten, reduce by units, reduce owner-opened units, transact, reverse-to (`close_all()` pine_adapter.cpp:10543 → `Flatten{}` pine_adapter.cpp:10577; `Reduce{ExplicitUnits}` pine_adapter.cpp:9416; `ReverseTo` pine_adapter.cpp:7835) | **yes** — `OrderIntent` native_order.hpp:229, reduce sizes `ReductionSize` native_order.hpp:215, `ReverseTo` native_order.hpp:60-62, `OpeningShape` native_order.hpp:910-914, `Flatten` native_order.hpp:36 | K | — | F:A4 F:A8 O:A2 O:A3 S:O1 | — | +| OL3 | Amend / cancel one working order (`submit_or_replace` pine_adapter.cpp:4363 → `host.replace` pine_adapter.cpp:4788) | **yes** — `replace` native_host.hpp:1218-1220, `cancel` native_host.hpp:1249; `ReplacedEvent` native_order.hpp:1017-1031; result and event types `SubmitStatus` native_order.hpp:835-861, `ReplacedEvent` native_order.hpp:1001-1067 | K | — | F:A2 O:A4 S:O3 | id → handle map is host bookkeeping (F) → OL4 | +| OL4 | Cancel all / cancel by label; label → handle book (`cancel(id)` pine_adapter.cpp:14506, `cancel_all` pine_adapter.cpp:14579; `live_by_source_key_` pine_adapter.hpp:2521; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4801) | **partial** — single-handle `cancel` native_host.hpp:1249; no enumeration (`WorkingRequestCore::live()` is consumer-internal native_order.hpp:1703); labels exist but are unindexed (`Request::label` native_order.hpp:485) **Closed:** `cancel_all` native_host.hpp:1267, `cancel_where(comment)` native_host.hpp:1272 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1272 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1263 is the enumeration; the id → handle book is `native_toolkit::OrderBook`, host-side and Pine-free (`tests/test_native_order_ergonomics.cpp`, `tests/test_native_anchored_legs.cpp`). | K+A | L7 | F:A3 F:A2 O:A5 O:A14 S:O3 | **Disagree:** F partial / O no (A5) + partial (A14) / S yes (folded into O3). R5-1: cancel-all / by-label is generic → kernel. TV state inheritance across a same-id re-issue stays A. The adapter's `strategy.cancel` / `strategy.cancel_all` keep their per-handle loops, measured by R5 lane F7 (audit M27): TradingView cancels in the SOURCE book, which holds commands of the same script call that have not reached the kernel yet (and a live `strategy.close` is labelled `__close__` there), so at the same point `cancel_where(id, NativeRequestField::Label)` and `cancel_all()` withdraw 0 — the close flattens at the next open, the reversal lands — where the source cancels withdraw them (`tests/test_adapter_brackets_relower.cpp`, M27 rows); each withdrawn source handle is also retired one by one. | +| OL5 | Per-point fill capacity, partial fills, residual working quantity (Pine uses only `ImmediateRemaining` pine_adapter.cpp:5007; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:9162) | **yes** — `Capacity` / `PointBudget` native_order.hpp:379-383, `Remaining` native_order.hpp:557-579, allowances `Allowance` native_order.hpp:662-675 | K | — | O:A16 S:O4 | — | +| OL6 | Ownership: legs bound to a parent / opening(s) / cohort; close the lots of a given entry, FIFO or ANY (`owner_for_close` pine_adapter.cpp:6579, `cohort_for` pine_adapter.cpp:3012; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:9257) | **yes** — `Owner` native_order.hpp:464-479, `Authority` native_order.hpp:620-621, `CohortClose` native_order.hpp:617-619, `SelectedExposure` native_order.hpp:625-629, `cohort_open` native_host.hpp:1282, `CancelReason::OwnerGone` native_order.hpp:949 | K | — | F:A6 O:A8 O:A9 S:O5 | FIFO *ordering* is host-computed (O) | +| OL7 | OCA cancel / reduce groups (`group_for` pine_adapter.cpp:3445) | **yes** — `Group` / `GroupEffect` native_order.hpp:466-473, `CancelReason::Group` native_order.hpp:948-953, `ReservationReducedEvent` native_order.hpp:1191-1201 (deferred form `DeferredGroupAdjustmentEvent` native_order.hpp:1211-1221) | K | — | F:A12 O:A6 O:A7 S:O7 | — | | OL8 | Immediate execution at the current point (`immediately=true`) | **yes** — `inspect_current_execution` / `execute_current` native_host.hpp:1104-1105 | K | — | F:A9 | (single-source row; O:F2 cites the same call) | | OL9 | Birth eligibility: an order accepted at bar N cannot fill on N's already-delivered points | **yes** — `point_eligible` native_order.hpp:786-790, the generic birth record `OrderBirth` include/pineforge/order_birth.hpp:59, contract `docs/pages/native-engine.md` ("Acceptance is not a fill") | K+A | — | F:A13 O:A10 S:R4 | **Disagree:** F yes / O partial / S "no, TV quirk". R5-1: the generic core (birth cursor) is native; TV's bar-granular first-bar rule is the A half → OL14. | -| OL10 | TP/SL bracket per entry, armed before the entry fills, OCA between legs (`exit()` pine_adapter.cpp:11034; lowering `submit_one` pine_adapter.cpp:11912, `BindCohort` pine_adapter.cpp:12375, `submit_leg` pine_adapter.cpp:11668; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1940); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | -| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:11294; `materialize_relative_exits` pine_adapter.cpp:13983, pine_adapter.hpp:2259-2271) | **partial** — composable: the host computes levels in `on_native_applied` and submits; suffix eligibility makes that same-bar correct `born_on_remaining_path` native_execution_consumer.cpp:6439. `Trigger` itself is absolute-only native_order.hpp:296 **Closed:** `Request::anchor = FromOwnerFill{offset, ticks, rounding}` (`TriggerAnchor` native_order.hpp:336) defers the level to the owner's fill, the kernel materializes it once at the arm under `NativeAnchorRounding`, and `resolve_anchored_level` native_host.hpp:1012 is where a source language's own level arithmetic lives (`tests/test_native_anchored_legs.cpp`, `tests/test_native_arm_options.cpp`). | K | L7 | F:A11 O:A11 S:O6 | **Disagree:** F partial (toolkit is enough) / O no (kernel `TriggerAnchor`). R5-2: L7 delivers relative anchors; kernel anchor primary, toolkit materialization is the zero-kernel fallback. | -| OL12 | Working-order view for the host and outside observers (`strategy_pending_order_*`; `PendingIntentView` pine_adapter.hpp:1449-1484; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:27061) | **no** — base returns 0 / -1 `observe_last_bar_dual_entry_path_v1` engine_consumer.cpp:124-133; the POD is TV-shaped (`tv_carry_qty` pending_order_mirror.hpp:73, `pine_birth_reach` pending_order_mirror.hpp:135) **Closed for a bare host:** `native_working_requests()` native_host.hpp:1263 returns one owning `NativeWorkingRequest` native_host.hpp:681 per live request — definition, remaining, trigger state — with the C spelling `strategy_native_working_len_v1` / `_get_v1`. The TV-shaped POD stays frozen and stays the source layer's projection (`tests/test_native_limit_fill_through.cpp`, `tests/test_native_arm_options.cpp`, `tests/test_native_c_api.c`). | K+A | L7 (C++), L13 (C) | F:J5 O:J6 S:X4 | **Disagree:** O "partial + TV-shaped" / F, S no. A bare host reads zeros → no. R5-1: neutral view → kernel; POD shape and Pine journal retention stay A (POD frozen, §2.ii j). | -| OL13 | *(quirk)* Same-bar command batching and the seven deferred queues (`pending_bracket_legs_` pine_adapter.hpp:2515-2534); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1364); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:4192, `materialize_snapshot` pine_adapter.cpp:13957), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1357-1384, `DeferredOpenMarketableSell` pine_adapter.hpp:1845-1868, `CloseCallsiteState` pine_adapter.hpp:1983-1997; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:21615, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:21849) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:676-678 | A | — | F:A15 O:A15 S:O9 S:R3 | R5-1 names KI-62. S: the kernel exposes deterministic insertion order only. | +| OL10 | TP/SL bracket per entry, armed before the entry fills, OCA between legs (`exit()` pine_adapter.cpp:11036; lowering `submit_one` pine_adapter.cpp:11914, `BindCohort` pine_adapter.cpp:12377, `submit_leg` pine_adapter.cpp:11670; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1940); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | +| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:11296; `materialize_relative_exits` pine_adapter.cpp:13985, pine_adapter.hpp:2259-2271) | **partial** — composable: the host computes levels in `on_native_applied` and submits; suffix eligibility makes that same-bar correct `born_on_remaining_path` native_execution_consumer.cpp:6507. `Trigger` itself is absolute-only native_order.hpp:296 **Closed:** `Request::anchor = FromOwnerFill{offset, ticks, rounding}` (`TriggerAnchor` native_order.hpp:336) defers the level to the owner's fill, the kernel materializes it once at the arm under `NativeAnchorRounding`, and `resolve_anchored_level` native_host.hpp:1012 is where a source language's own level arithmetic lives (`tests/test_native_anchored_legs.cpp`, `tests/test_native_arm_options.cpp`). | K | L7 | F:A11 O:A11 S:O6 | **Disagree:** F partial (toolkit is enough) / O no (kernel `TriggerAnchor`). R5-2: L7 delivers relative anchors; kernel anchor primary, toolkit materialization is the zero-kernel fallback. | +| OL12 | Working-order view for the host and outside observers (`strategy_pending_order_*`; `PendingIntentView` pine_adapter.hpp:1449-1484; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:27063) | **no** — base returns 0 / -1 `observe_last_bar_dual_entry_path_v1` engine_consumer.cpp:124-133; the POD is TV-shaped (`tv_carry_qty` pending_order_mirror.hpp:73, `pine_birth_reach` pending_order_mirror.hpp:135) **Closed for a bare host:** `native_working_requests()` native_host.hpp:1263 returns one owning `NativeWorkingRequest` native_host.hpp:681 per live request — definition, remaining, trigger state — with the C spelling `strategy_native_working_len_v1` / `_get_v1`. The TV-shaped POD stays frozen and stays the source layer's projection (`tests/test_native_limit_fill_through.cpp`, `tests/test_native_arm_options.cpp`, `tests/test_native_c_api.c`). | K+A | L7 (C++), L13 (C) | F:J5 O:J6 S:X4 | **Disagree:** O "partial + TV-shaped" / F, S no. A bare host reads zeros → no. R5-1: neutral view → kernel; POD shape and Pine journal retention stay A (POD frozen, §2.ii j). | +| OL13 | *(quirk)* Same-bar command batching and the seven deferred queues (`pending_bracket_legs_` pine_adapter.hpp:2515-2534); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1364); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:4194, `materialize_snapshot` pine_adapter.cpp:13959), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1357-1384, `DeferredOpenMarketableSell` pine_adapter.hpp:1845-1868, `CloseCallsiteState` pine_adapter.hpp:1983-1997; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:21617, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:21851) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:676-678 | A | — | F:A15 O:A15 S:O9 S:R3 | R5-1 names KI-62. S: the kernel exposes deterministic insertion order only. | | OL14 | *(quirk)* Exit activation / suspension / revival barriers, historical birth reach (`select_exit_activation` exit_activation.cpp:44-90, enforced `leg_activation` pine_adapter.cpp:1261; `select_exit_activation` exit_activation.cpp:20-90, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:1078) | n/a | A | — | S:R4 O:A10 | TV half of OL9. **V19D-P1 (R5 lanes B-ADAPTER, V19-FIX):** a margin-call revival never restores an exit the script cancelled: `strategy.cancel` / `cancel_all` retire every leg of each exit row they name — the live ones they take off the book (caused at the Cancelled event, after anything the same callback did to the leg), the dormant ones, and the ones the adapter took off the book itself — and drop a gapped stop a declined reversal parked for the next margin-call slice (`retire_cancelled_exits` pine_adapter.cpp:2123); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:2200). What `tests/test_adapter_margin_revival_cancel.cpp` replays on the NYSE:F 15m chart of seven lab tv tapes: badapter-v19dp1-control (no cancel: the margin call closes 928 through X) and v19fix-gapped-stop-control (X's stop at 10.15, gapped by the 05-01 open and parked: the same close), and badapter-v19dp1-cancel, v19fix-cancel-before-pair, v19fix-cancel-at-1000, v19fix-gapped-stop-cancel and v19fix-gapped-stop-cancel-after-pair (a cancel on 05-01 09:30 while X is dormant, on 04-30 15:45 before the reversal pair and on 04-30 10:00 while X is live, and on 05-01 09:30 and after the pair while X is parked; one tape file: margin calls 12 @10.39 and 92 @10.57, 836 open). Untaped, the same row holds the tapes' trades for `strategy.cancel_all` on 05-01 and before the pair and for `strategy.cancel` after the pair, and on the same bars with no quantity grid — where the pair holds X in the cancel's own callback — every spelling books what the cancel on the dormant exit books | ### 1.2 Trailing | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| TR1 | Trail arm → track → trigger, with a state query | **yes** — `Trail{offset, arm_price}` native_order.hpp:290-293; states `TrailWaitArm` native_order.hpp:642-660; arm / track / fire `TrailWaitArm` native_execution_consumer.cpp:6520; `NativeTrailState` native_host.hpp:669-674, `trail_state` native_host.hpp:1092 | K | — | F:B1 O:B1 S:T1 | — | +| TR1 | Trail arm → track → trigger, with a state query | **yes** — `Trail{offset, arm_price}` native_order.hpp:290-293; states `TrailWaitArm` native_order.hpp:642-660; arm / track / fire `TrailWaitArm` native_execution_consumer.cpp:6588; `NativeTrailState` native_host.hpp:669-674, `trail_state` native_host.hpp:1092 | K | — | F:B1 O:B1 S:T1 | — | | TR2 | Trail activation / offset in ticks or points, relative to the entry fill (`trail_points_to_ticks` include/pineforge/compat/pine/trail_ticks.hpp:44 + grid snap exit_lifecycle.cpp:22-27; `trail_activation_level` pine_adapter.hpp:350-352) | **partial** — absolute prices and a raw price distance only **Closed:** `TrailTicks{n}` (`Trail::ticks` native_order.hpp:293) is resolved against `NativeRunSpec::price_tick` at acceptance, so a tick-spelled trail and its price-spelled equal behave identically bar for bar; a relative arm threshold is the `FromOwnerFill` anchor of OL11 (`tests/test_native_order_ergonomics.cpp`, `tests/test_native_anchored_legs.cpp`). | K | L7 | F:B2 O:B2 S:T2 | **Disagree:** S classes all trail operand conversion as TV quirk. R5-1: tick / relative spelling is generic. | -| TR3 | Zero-distance trail, "ride the best, exit on any adverse tick" (adapter substitutes `tick * 0.5` pine_adapter.cpp:15001) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:5255) **Closed:** a zero offset is legal and means *ride the best* — the level is the running best itself and the exit is the first print strictly past it; negative and nonfinite offsets stay rejected (`tests/test_native_order_ergonomics.cpp`, `tests/test_native_anchored_legs.cpp`). | K+A | L7 | O:B3 F:B3 S:T2 | **Disagree:** O kernel / F, S quirk. R5-1: accepting a zero offset is generic; TV zero-offset *pricing* (`zero_trail_sibling_stop` pine_adapter.cpp:14918) stays A. | -| TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:4754) | **partial** — the read exists `trail_state` native_host.hpp:1092; `replace` always rebuilds trigger state **Closed:** `replace(handle, request, ReplaceOptions{/*retain_trigger_state=*/true})` carries the predecessor's live trigger state — a tracking trail's best, an already active stop — into the successor, refusing the replacement when the retained best leaves no representable level (`tests/test_native_order_ergonomics.cpp`, `tests/test_native_anchored_legs.cpp`). | K+A | L7 | O:B4 F:B3 | Status is single-source (O). R5-1: retain-trail replace is generic; TV's "retained best across re-issue" convention (`retained_trail_best` pine_adapter.hpp:353-355) is A. | -| TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:11156, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:14918, retained best `retained_trail_best` pine_adapter.hpp:353-355, operand / restart rules `source_trail_points` pine_adapter.cpp:11153, `trail_one_shot` pine_adapter.cpp:12499 | n/a | A | — | F:B3 S:T2 | R5-1 | +| TR3 | Zero-distance trail, "ride the best, exit on any adverse tick" (adapter substitutes `tick * 0.5` pine_adapter.cpp:15003) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:5323) **Closed:** a zero offset is legal and means *ride the best* — the level is the running best itself and the exit is the first print strictly past it; negative and nonfinite offsets stay rejected (`tests/test_native_order_ergonomics.cpp`, `tests/test_native_anchored_legs.cpp`). | K+A | L7 | O:B3 F:B3 S:T2 | **Disagree:** O kernel / F, S quirk. R5-1: accepting a zero offset is generic; TV zero-offset *pricing* (`zero_trail_sibling_stop` pine_adapter.cpp:14920) stays A. | +| TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:4756) | **partial** — the read exists `trail_state` native_host.hpp:1092; `replace` always rebuilds trigger state **Closed:** `replace(handle, request, ReplaceOptions{/*retain_trigger_state=*/true})` carries the predecessor's live trigger state — a tracking trail's best, an already active stop — into the successor, refusing the replacement when the retained best leaves no representable level (`tests/test_native_order_ergonomics.cpp`, `tests/test_native_anchored_legs.cpp`). | K+A | L7 | O:B4 F:B3 | Status is single-source (O). R5-1: retain-trail replace is generic; TV's "retained best across re-issue" convention (`retained_trail_best` pine_adapter.hpp:353-355) is A. | +| TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:11158, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:14920, retained best `retained_trail_best` pine_adapter.hpp:353-355, operand / restart rules `source_trail_points` pine_adapter.cpp:11155, `trail_one_shot` pine_adapter.cpp:12501 | n/a | A | — | F:B3 S:T2 | R5-1 | ### 1.3 Sizing bases and reservations | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| SZ1 | Explicit units (`Transact{signed}` after a grid floor pine_adapter.cpp:7192, pine_adapter.cpp:7192) | **yes** — `Transact` in `OrderIntent` native_order.hpp:229 | K | — | F:C1 O:C1 S:S1 | — | -| SZ2 | Host override of the resolved price / units (`resolve_terms` pine_adapter.cpp:13726-14954) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:945-949, invoked `resolve_execution_terms` native_execution_consumer.cpp:5740, guarded by `consume_matched_request`'s `try` native_execution_consumer.cpp:5641-5675 | K | — | F:C2 O:H9 S:S2 | **Disagree:** S "partial" (a hook is not a basis). The hook is native; bases are SZ3-SZ5. It stays the custom-policy seam after L3. | -| SZ3 | Percent-of-equity sizing (`default_sizing_units` pine_adapter.cpp:3199) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:5769 **Closed:** `Sized{Side, EquityFraction{f}}` (`OrderIntent` native_order.hpp:229) is the sixth intent — the kernel resolves the units from a fraction of marked equity at the sizing point, so a bare host needs no terms override (`tests/test_native_sizing_bases.cpp`). | K+A | L3 | F:C3 O:C2 S:S3 | TV parts stay A: the percentage's money rounded `source_money_round` pine_adapter.cpp:558 (not the equity: R5 lane W5B-ENG-MARGIN-RESIDUAL, the private fixture set `margin_residual` w5b-sz-*), money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:658 (→ SZ10) | -| SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:3070) | **no** — same **Closed:** `Sized{Side, CashValue{cash}}`, the same resolution over an account-currency amount (`tests/test_native_sizing_bases.cpp`). | K | L3 | F:C2 O:C3 S:S4 | A per-call `qty_type` (cash or percent of equity on one `strategy.entry`) is not Pine — TradingView's compiler refuses it, so only generated or C++ callers reach it — and since R5 lane B-ADAPTER it converts through the kernel's `native_sized_units` exactly as the declared default does (`typed_quantity_units` pine_adapter.cpp:3218, `typed_entry_units` pine_adapter.cpp:3236; `tests/test_adapter_typed_entry_admission.cpp`) | -| SZ5 | Fee-net sizing (commission reserve `/(1+c)`, asked of the kernel as `reserve_percent_fee` pine_adapter.cpp:3191; `percent_commission_live_equity` pine_adapter.cpp:5090) | **no** — only a dead primitive, `reserve_percent_commission` (since deleted), with no caller in `src/` **Closed:** `Sized::reserve_percent_fee` divides the sizing cash by `1 + fee_value / 100` under `NativeFeeKind::Percent` and is an exact no-op for every other fee kind; the dead `reserve_percent_commission` primitive is gone (`tests/test_native_sizing_bases.cpp`, `tests/test_native_sizing_price_rule.cpp`). | K | L3 | F:C3 O:C6 S:S3 | **Disagree:** F lists the reserve under quirk yet offers a knob / O kernel, "partial" / S knob. R5-1: fee-net option is generic. TradingView's cash-commission reserve stays adapter policy over the kernel's `CashValue`: a percent-of-equity default quantity under a cash-per-order or cash-per-contract fee takes its percentage of `strategy.equity` (`PineSizingSnapshot::strategy_equity`, the open entries' cash fees charged) and leaves out the fee its own order pays (`default_sizing_cash`, R5 lane PAR-CASHFEE, §3.10; `tests/test_cash_fee_sizing_tapes.cpp` on the tapes in the private fixture set `cash_fee_sizing`) | -| SZ6 | Placement-time vs fill-time sizing (`sizing_snapshot` pine_adapter.cpp:3025, `PineSizingSnapshot` pine_adapter.hpp:157-169; fill-time exceptions `default_sized` pine_adapter.cpp:8466) | **no** **Closed:** `SizeTime::AtMatch` (default) resolves at the matching candidate and `SizeTime::AtAcceptance` freezes the units against the command point, where the run's own opening admission also runs against the frozen quantity (`RequestRejectReason::PlacementAdmission`). Which price the basis converts at is `SizePrice::{Resolved, Signal, SignalOnTick}` (`tests/test_native_sizing_bases.cpp`, `tests/test_native_sizing_price_rule.cpp`). | K+A | L3 | F:C3 O:C7 S:S3 | **Disagree:** O adapter-only / F, S generic timing knob. R5-11 (L3 scope) + R5-1: the knob is kernel; TV's frozen tuple stays A through `HostSized`. | +| SZ1 | Explicit units (`Transact{signed}` after a grid floor pine_adapter.cpp:7194, pine_adapter.cpp:7194) | **yes** — `Transact` in `OrderIntent` native_order.hpp:229 | K | — | F:C1 O:C1 S:S1 | — | +| SZ2 | Host override of the resolved price / units (`resolve_terms` pine_adapter.cpp:13728-14956) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:945-949, invoked `resolve_execution_terms` native_execution_consumer.cpp:5808, guarded by `consume_matched_request`'s `try` native_execution_consumer.cpp:5709-5743 | K | — | F:C2 O:H9 S:S2 | **Disagree:** S "partial" (a hook is not a basis). The hook is native; bases are SZ3-SZ5. It stays the custom-policy seam after L3. | +| SZ3 | Percent-of-equity sizing (`default_sizing_units` pine_adapter.cpp:3201) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:5837 **Closed:** `Sized{Side, EquityFraction{f}}` (`OrderIntent` native_order.hpp:229) is the sixth intent — the kernel resolves the units from a fraction of marked equity at the sizing point, so a bare host needs no terms override (`tests/test_native_sizing_bases.cpp`). | K+A | L3 | F:C3 O:C2 S:S3 | TV parts stay A: the percentage's money rounded `source_money_round` pine_adapter.cpp:558 (not the equity: R5 lane W5B-ENG-MARGIN-RESIDUAL, the private fixture set `margin_residual` w5b-sz-*), money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:658 (→ SZ10) | +| SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:3072) | **no** — same **Closed:** `Sized{Side, CashValue{cash}}`, the same resolution over an account-currency amount (`tests/test_native_sizing_bases.cpp`). | K | L3 | F:C2 O:C3 S:S4 | A per-call `qty_type` (cash or percent of equity on one `strategy.entry`) is not Pine — TradingView's compiler refuses it, so only generated or C++ callers reach it — and since R5 lane B-ADAPTER it converts through the kernel's `native_sized_units` exactly as the declared default does (`typed_quantity_units` pine_adapter.cpp:3220, `typed_entry_units` pine_adapter.cpp:3238; `tests/test_adapter_typed_entry_admission.cpp`) | +| SZ5 | Fee-net sizing (commission reserve `/(1+c)`, asked of the kernel as `reserve_percent_fee` pine_adapter.cpp:3193; `percent_commission_live_equity` pine_adapter.cpp:5092) | **no** — only a dead primitive, `reserve_percent_commission` (since deleted), with no caller in `src/` **Closed:** `Sized::reserve_percent_fee` divides the sizing cash by `1 + fee_value / 100` under `NativeFeeKind::Percent` and is an exact no-op for every other fee kind; the dead `reserve_percent_commission` primitive is gone (`tests/test_native_sizing_bases.cpp`, `tests/test_native_sizing_price_rule.cpp`). | K | L3 | F:C3 O:C6 S:S3 | **Disagree:** F lists the reserve under quirk yet offers a knob / O kernel, "partial" / S knob. R5-1: fee-net option is generic. TradingView's cash-commission reserve stays adapter policy over the kernel's `CashValue`: a percent-of-equity default quantity under a cash-per-order or cash-per-contract fee takes its percentage of `strategy.equity` (`PineSizingSnapshot::strategy_equity`, the open entries' cash fees charged) and leaves out the fee its own order pays (`default_sizing_cash`, R5 lane PAR-CASHFEE, §3.10; `tests/test_cash_fee_sizing_tapes.cpp` on the tapes in the private fixture set `cash_fee_sizing`) | +| SZ6 | Placement-time vs fill-time sizing (`sizing_snapshot` pine_adapter.cpp:3027, `PineSizingSnapshot` pine_adapter.hpp:157-169; fill-time exceptions `default_sized` pine_adapter.cpp:8468) | **no** **Closed:** `SizeTime::AtMatch` (default) resolves at the matching candidate and `SizeTime::AtAcceptance` freezes the units against the command point, where the run's own opening admission also runs against the frozen quantity (`RequestRejectReason::PlacementAdmission`). Which price the basis converts at is `SizePrice::{Resolved, Signal, SignalOnTick}` (`tests/test_native_sizing_bases.cpp`, `tests/test_native_sizing_price_rule.cpp`). | K+A | L3 | F:C3 O:C7 S:S3 | **Disagree:** O adapter-only / F, S generic timing knob. R5-11 (L3 scope) + R5-1: the knob is kernel; TV's frozen tuple stays A through `HostSized`. | | SZ7 | Quantity step | **yes** — `quantity_grid` admission native_run_spec.hpp:660, native_order.hpp:1421-1438; host-sized terms snap (`ExecutionGridPolicy` native_order.hpp:71-85); exact grid validation `execution_terms_grid_representable` native_execution_consumer.cpp:505; the book's own quantities are on the grid -- a whole-scope `ScopeFraction` and a `Reduce` of a FIFO boundary of its scope (R5 lane K-ULP4) | K | — | F:C4 O:C5 S:S1 | **Disagree:** F, O partial / S yes. R5-3: the quantity grid is native already. L3's `Sized` intent reuses the snap policy. | -| SZ8 | Fractional reduce of the position / a cohort (`quantize_close_units` pine_adapter.cpp:5210, `compute_exit_reservation` pine_adapter.cpp:5250) | **no** — sizes are `ExplicitUnits` / `OwnerOpenedUnits` / `HostSized` only native_order.hpp:60-74 **Closed:** `Reduce{ScopeFraction{fraction, claim, basis}}` takes a fraction in `(0, 1]` of the scope the reduce is bound to — the book, one opening, or a cohort roster (`tests/test_native_sizing_bases.cpp`). | K | L3 | F:A7 O:A12 O:C4 S:S5 | F's "partial" counts the units-reduce of OL2. R5-4 → L3. | -| SZ9 | Exit-quantity reservation against sibling exits (`compute_exit_reservation` pine_adapter.cpp:5250, `reserved_exit_qty` pine_adapter.cpp:11563) | **partial** — `PointBudget` native_order.hpp:379-382 and group `Reduce` native_order.hpp:220 give a generic reservation; no net-of-siblings claim for fraction sizes **Closed:** `ScopeClaim::NetOfSiblings` subtracts the units already claimed by live sibling reduces bound to the same scope, and `ScopeBasis::{AtMatch, AtAcceptance}` chooses when that scope is measured (`tests/test_native_sizing_bases.cpp`). | K+A | L3 | F:A14 S:S5 S:O10 O:A12 | F proposed nothing. R5-4: generic half → L3; the Pine ledger → SZ11, SZ12. | -| SZ10 | *(quirk)* Ten-significant-digit money rule (`source_money_round` pine_adapter.cpp:558, `source_money_floor_lot` pine_adapter.cpp:658; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:5232) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1746 | A | — | O:C8 F:C3 S:M4 S:S5 | R5-1 | -| SZ10a | *(quirk, measured retention)* The money band's affordable price, `sig10(sig10(E) / notional per unit of price)` against the sizing price — at placement (`entry`, `affordable_price` pine_adapter.cpp:8509), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:10033), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:16212), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:16743, pine_adapter.cpp:16796) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:21663) | n/a — not a units conversion. Every money-to-units conversion in the adapter is the kernel's `native_sized_units` under the source's floors since R5 lane B-ADAPTER: the default at placement and at the fill (`default_sizing_units` pine_adapter.cpp:3199), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:3218, `typed_entry_units` pine_adapter.cpp:3236), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:26648) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes and 202 famr tapes (two tests that live in the private fixtures); on a seven-digit tie (E 9998.99999996, 10 000 lots at tick(close) 0.9999) the rule admits while the kernel's conversion buys 9999.99999996 lots and binary64 gives E/Q 0.999899999996 (`tests/test_adapter_fill_qty_probe.cpp`, section 2). | -| SZ11 | *(quirk)* Same-bar `strategy.close` callsite batching, two-call provenance, entry-id ledger (`enqueue_pooc_fifo_close` pine_adapter.cpp:9160; ledger `close_batch_bar_` pine_adapter.hpp:2595; the kernel's comment-only residue was deleted by R5 lanes E6 and E10) | n/a | A | — | S:O8 F:A15 O:A15 | **Disagree:** S "generic → kernel" (its P2 `ReplaceByKey` / `Fifo`). R5-4: adapter. | +| SZ8 | Fractional reduce of the position / a cohort (`quantize_close_units` pine_adapter.cpp:5212, `compute_exit_reservation` pine_adapter.cpp:5252) | **no** — sizes are `ExplicitUnits` / `OwnerOpenedUnits` / `HostSized` only native_order.hpp:60-74 **Closed:** `Reduce{ScopeFraction{fraction, claim, basis}}` takes a fraction in `(0, 1]` of the scope the reduce is bound to — the book, one opening, or a cohort roster (`tests/test_native_sizing_bases.cpp`). | K | L3 | F:A7 O:A12 O:C4 S:S5 | F's "partial" counts the units-reduce of OL2. R5-4 → L3. | +| SZ9 | Exit-quantity reservation against sibling exits (`compute_exit_reservation` pine_adapter.cpp:5252, `reserved_exit_qty` pine_adapter.cpp:11565) | **partial** — `PointBudget` native_order.hpp:379-382 and group `Reduce` native_order.hpp:220 give a generic reservation; no net-of-siblings claim for fraction sizes **Closed:** `ScopeClaim::NetOfSiblings` subtracts the units already claimed by live sibling reduces bound to the same scope, and `ScopeBasis::{AtMatch, AtAcceptance}` chooses when that scope is measured (`tests/test_native_sizing_bases.cpp`). | K+A | L3 | F:A14 S:S5 S:O10 O:A12 | F proposed nothing. R5-4: generic half → L3; the Pine ledger → SZ11, SZ12. | +| SZ10 | *(quirk)* Ten-significant-digit money rule (`source_money_round` pine_adapter.cpp:558, `source_money_floor_lot` pine_adapter.cpp:658; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:5234) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1746 | A | — | O:C8 F:C3 S:M4 S:S5 | R5-1 | +| SZ10a | *(quirk, measured retention)* The money band's affordable price, `sig10(sig10(E) / notional per unit of price)` against the sizing price — at placement (`entry`, `affordable_price` pine_adapter.cpp:8511), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:10035), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:16214), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:16745, pine_adapter.cpp:16798) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:21665) | n/a — not a units conversion. Every money-to-units conversion in the adapter is the kernel's `native_sized_units` under the source's floors since R5 lane B-ADAPTER: the default at placement and at the fill (`default_sizing_units` pine_adapter.cpp:3201), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:3220, `typed_entry_units` pine_adapter.cpp:3238), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:26650) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes and 202 famr tapes (two tests that live in the private fixtures); on a seven-digit tie (E 9998.99999996, 10 000 lots at tick(close) 0.9999) the rule admits while the kernel's conversion buys 9999.99999996 lots and binary64 gives E/Q 0.999899999996 (`tests/test_adapter_fill_qty_probe.cpp`, section 2). | +| SZ11 | *(quirk)* Same-bar `strategy.close` callsite batching, two-call provenance, entry-id ledger (`enqueue_pooc_fifo_close` pine_adapter.cpp:9162; ledger `close_batch_bar_` pine_adapter.hpp:2595; the kernel's comment-only residue was deleted by R5 lanes E6 and E10) | n/a | A | — | S:O8 F:A15 O:A15 | **Disagree:** S "generic → kernel" (its P2 `ReplaceByKey` / `Fifo`). R5-4: adapter. | | SZ12 | *(quirk)* POOC reservation-growth market-entry population predicate (`select_reservation_growth_sources` src/compat/pine/reservation_expansion.cpp:9-34, called from the adapter, `select_reservation_growth_sources` pine_adapter.cpp:1322) | n/a | A | — | S:O10 | (single-source) R5-4 confirms S's own split: concept → SZ9, predicate A. | ### 1.4 Margin, admission, liquidation, risk | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| MG1 | Allowed opening directions (`allowed_open_directions` pine_adapter.cpp:2859) | **yes** — `allowed_open_directions` native_run_spec.hpp:665 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:3621 | K | — | F:D4 O:D7 S:M1 | the one risk rule the adapter delegates (O) | -| MG2 | Max absolute position size | **yes** — `max_abs_units` native_run_spec.hpp:663 tests the resulting book native_execution_consumer.cpp:3628 | K+A | — | F:D5 O:D5 S:M1 S:M5 | **Disagree:** O, S:M5 partial. R5-1: the generic cap is native; the adapter's live-position `>= held` variant (`max_position_size` pine_adapter.cpp:16491) is TV. | -| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:664 → `MaxOpenLots` native_execution_consumer.cpp:3633 | K+A | — | F:A5 O:A13 O:D6 S:M1 | **Disagree:** F, O:A13 partial / O:D6, S yes. R5-1: Pine's per-cycle entry count (`accepted_in_cycle` pine_adapter.cpp:7697, cap left unset `max_open_lots` pine_adapter.cpp:2858-2859) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (the private test `test_pyramiding_count_differential`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 13 (it keeps a slot a close or another entry's exit drained; since lane W5B-ENG-MARGIN-RESIDUAL's SB the later market entries of a flat bar are refused at the fill once the book holds the cap). The fifteenth, a resting entry accepted below the cap and filled at it, the kernel's cap refuses; the adapter books it since lane W8A-SIGSTATE-1's R-B, which also stopped the count from counting a resting LIMIT entry (a pending LIMIT entry does not count at a later call, and one the cap admitted fills over it: the private fixture set `pyramiding_open_order`). A recorded divergence, retained pending a re-lowering ruling. **Re-lowering tried by R5 lane H-THIN and not taken:** on the kernel's cap four corpus probes moved away from TradingView, for want of that first-eligible-point rule, so the count stays. The lane fixed the reservation divergence H-MEASURE measured beside it (P10): a default `strategy.exit(from_entry)` under FIFO reserved the whole book (200 in the adapter, 100 on TradingView's tape); it now reserves the entry's own quantity, the oldest lots first (`fifo_named_entry` pine_adapter.cpp:11587). | -| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:17304 and leaves the kernel gate unset pine_adapter.cpp:17323) | **yes** — `initial_margin_fraction` native_run_spec.hpp:666; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:3651 against marked equity. **Conversion instant, R5 follow-up lane E21:** both sites of the gate convert at their own point's rate — the candidate at its cursor, the placement gate at the acceptance point where a `Sized{SizeTime::AtAcceptance}` freezes its units — for the notional, the equity and a percent ticket, and the freeze's equity basis with them. The rate is threaded, never the clock: `make_command_context` native_execution_consumer.cpp:3497 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (the private test `test_adapter_margin_schedule_differential`). **Since R5 lane TAIL-C** an explicit-quantity market buy from flat is costed at its signal close's tick plus the slippage ticks its fill carries, off `process_orders_on_close` (the private test `test_explicit_market_admission_tapes`), and an infinite `strategy.entry` quantity trades the default quantity, as na does (`tests/test_nonfinite_entry_qty_tape.cpp`). | -| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:17557, `margin_pct` pine_adapter.cpp:17059) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:666 **Closed:** `NativeMarginModel::initial_long` / `initial_short` native_run_spec.hpp:242, opt-in and mutually exclusive with `initial_margin_fraction`; `initial_ == 0.0` is the maintenance-only spelling that leaves opening admission to the host at both of its gates (`tests/test_native_margin_model.cpp`). | K | L4 | F:D1 O:D2 | R5-1 | -| MG6 | Maintenance margin + forced liquidation (`submit_margin_call_slice` pine_adapter.cpp:17944, `submit_margin_call_units` pine_adapter.cpp:17977, `schedule_margin_call_path` pine_adapter.cpp:20222) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:666-667; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:3610 **Closed:** `maintenance_long` / `maintenance_short` with `NativeLiquidationCheck::{PathAdverseExtreme, PathAdverseExtremeMark, CalculationOnly}`; the kernel solves the level, rests its own `Reduce`/`Flatten` bound to the live book, re-prices it with `CancelReason::Superseded` and books a `MarginCallEvent` (`tests/test_native_margin_model.cpp`, `tests/test_native_margin_hooks.cpp`). | K+A | L4 | F:D2 O:D3 S:M3 | TV parts → MG15 | -| MG7 | Liquidation slice sizing (grid floor, ×4 restore, one-contract fallback `source_margin_units` pine_adapter.cpp:17601) | **no** **Closed:** `NativeLiquidationSizing::{RestoreMinimum, ShortfallMultiple, Flatten}` with `shortfall_multiple` and `liquidation_min_units`, capped at the held quantity and floored onto `quantity_grid`; `resolve_margin_call_units` native_host.hpp:995 gives a host the last word, clamped into `(0, held]` (`tests/test_native_margin_model.cpp`, `tests/test_native_margin_hooks.cpp`). | K+A | L4 | O:D4 F:D2 S:M4 | **Disagree:** F generic knob `ShortfallMultiple` / O "×4 is A" / S quirk, yet a 4.0 default inside its generic model. R5-1: policy knob in the kernel, plain default (restore-minimum). **Decided by R5 lane F7 (the D2 question):** the adapter selects NO kernel knob — `project()` leaves `sizing` / `shortfall_multiple` / `liquidation_min_units` unset and its `resolve_margin_call_units` answers every call with TradingView's slice (the restore floored onto the lot grid BEFORE the ×4, floored again, the one-contract band, the dust gate). Measured: on a one-unit grid, 20 long at 100 on 1000 at 50 % reaching 93.9, TradingView books 4 lots and the kernel's own `ShortfallMultiple 4` books 5 (`tests/test_native_margin_hooks_twin.cpp` MG-F3, an executed run, no longer a comment); without a grid the two agree bit for bit (MG-F/H, 4.2105263157894735), but declaring the knob for those runs alone folds `NativeMarginModel::sizing` into every margin run's identity — an epoch decision F7 does not take. Since R5 lane H-THIN the pre-open admission slice (`schedule_preopen_margin_slice`) sizes through the same `source_margin_units`, where it kept its own copy of the ×4 without the band or the dust gates (AUDIT4 §6 G2-12; the private test `test_adapter_margin_schedule_differential` M10: band and fudge now 1 and 1, the kernel's and `ab9714be`'s). A four-times call on a whole-unit grid whose book's oldest lot is one unit is followed by one more unit at the next check point (`whole_unit_follow_up_due` pine_adapter.cpp:19129; R5 lane W5B-ENG-MARGIN-RESIDUAL, the private fixture set `margin_residual` w5b-es-*, w5b-sb-es-*, w5b-f-q1x12). With slippage the one-contract band takes its unit only where that unit restores the book at its own fill: the position's requirement at the liquidation's slipped print, less the equity at the mark, under one unit's margin (`source_margin_units` pine_adapter.cpp:17601; lane INT28-FIX rule OU, the private fixture set `slipped_short`). A one-unit call that leaves the book short at its own fill, by more than one unit's margin and two slippage steps with the book re-marked there, is followed by one more unit at the bar's next path point, where that point's own check calls nothing and its print is inside the call's fill (lane INT28-FIX rule OF). Under a percent commission a call of any size is followed so, on a fractional lot grid too: while the book the call left, re-marked at its fill, is short by more than two slippage steps for each unit called and the exit fees of those units and of one more, the next path point inside that fill takes four times the shortfall's lot floor over the fill, or one unit where that floors to none (`margin_follow_up_units` pine_adapter.cpp:19171, `follow_margin_call` pine_adapter.cpp:19220; R5 lane TAIL-I, the private fixture set `margin_open_print`). | +| MG1 | Allowed opening directions (`allowed_open_directions` pine_adapter.cpp:2859) | **yes** — `allowed_open_directions` native_run_spec.hpp:665 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:3689 | K | — | F:D4 O:D7 S:M1 | the one risk rule the adapter delegates (O) | +| MG2 | Max absolute position size | **yes** — `max_abs_units` native_run_spec.hpp:663 tests the resulting book native_execution_consumer.cpp:3696 | K+A | — | F:D5 O:D5 S:M1 S:M5 | **Disagree:** O, S:M5 partial. R5-1: the generic cap is native; the adapter's live-position `>= held` variant (`max_position_size` pine_adapter.cpp:16493) is TV. | +| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:664 → `MaxOpenLots` native_execution_consumer.cpp:3701 | K+A | — | F:A5 O:A13 O:D6 S:M1 | **Disagree:** F, O:A13 partial / O:D6, S yes. R5-1: Pine's per-cycle entry count (`accepted_in_cycle` pine_adapter.cpp:7699, cap left unset `max_open_lots` pine_adapter.cpp:2858-2859) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (the private test `test_pyramiding_count_differential`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 13 (it keeps a slot a close or another entry's exit drained; since lane W5B-ENG-MARGIN-RESIDUAL's SB the later market entries of a flat bar are refused at the fill once the book holds the cap). The fifteenth, a resting entry accepted below the cap and filled at it, the kernel's cap refuses; the adapter books it since lane W8A-SIGSTATE-1's R-B, which also stopped the count from counting a resting LIMIT entry (a pending LIMIT entry does not count at a later call, and one the cap admitted fills over it: the private fixture set `pyramiding_open_order`). A recorded divergence, retained pending a re-lowering ruling. **Re-lowering tried by R5 lane H-THIN and not taken:** on the kernel's cap four corpus probes moved away from TradingView, for want of that first-eligible-point rule, so the count stays. The lane fixed the reservation divergence H-MEASURE measured beside it (P10): a default `strategy.exit(from_entry)` under FIFO reserved the whole book (200 in the adapter, 100 on TradingView's tape); it now reserves the entry's own quantity, the oldest lots first (`fifo_named_entry` pine_adapter.cpp:11589). | +| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:17306 and leaves the kernel gate unset pine_adapter.cpp:17325) | **yes** — `initial_margin_fraction` native_run_spec.hpp:666; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:3719 against marked equity. **Conversion instant, R5 follow-up lane E21:** both sites of the gate convert at their own point's rate — the candidate at its cursor, the placement gate at the acceptance point where a `Sized{SizeTime::AtAcceptance}` freezes its units — for the notional, the equity and a percent ticket, and the freeze's equity basis with them. The rate is threaded, never the clock: `make_command_context` native_execution_consumer.cpp:3565 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (the private test `test_adapter_margin_schedule_differential`). **Since R5 lane TAIL-C** an explicit-quantity market buy from flat is costed at its signal close's tick plus the slippage ticks its fill carries, off `process_orders_on_close` (the private test `test_explicit_market_admission_tapes`), and an infinite `strategy.entry` quantity trades the default quantity, as na does (`tests/test_nonfinite_entry_qty_tape.cpp`). | +| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:17559, `margin_pct` pine_adapter.cpp:17061) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:666 **Closed:** `NativeMarginModel::initial_long` / `initial_short` native_run_spec.hpp:242, opt-in and mutually exclusive with `initial_margin_fraction`; `initial_ == 0.0` is the maintenance-only spelling that leaves opening admission to the host at both of its gates (`tests/test_native_margin_model.cpp`). | K | L4 | F:D1 O:D2 | R5-1 | +| MG6 | Maintenance margin + forced liquidation (`submit_margin_call_slice` pine_adapter.cpp:17946, `submit_margin_call_units` pine_adapter.cpp:17979, `schedule_margin_call_path` pine_adapter.cpp:20224) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:666-667; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:3678 **Closed:** `maintenance_long` / `maintenance_short` with `NativeLiquidationCheck::{PathAdverseExtreme, PathAdverseExtremeMark, CalculationOnly}`; the kernel solves the level, rests its own `Reduce`/`Flatten` bound to the live book, re-prices it with `CancelReason::Superseded` and books a `MarginCallEvent` (`tests/test_native_margin_model.cpp`, `tests/test_native_margin_hooks.cpp`). | K+A | L4 | F:D2 O:D3 S:M3 | TV parts → MG15 | +| MG7 | Liquidation slice sizing (grid floor, ×4 restore, one-contract fallback `source_margin_units` pine_adapter.cpp:17603) | **no** **Closed:** `NativeLiquidationSizing::{RestoreMinimum, ShortfallMultiple, Flatten}` with `shortfall_multiple` and `liquidation_min_units`, capped at the held quantity and floored onto `quantity_grid`; `resolve_margin_call_units` native_host.hpp:995 gives a host the last word, clamped into `(0, held]` (`tests/test_native_margin_model.cpp`, `tests/test_native_margin_hooks.cpp`). | K+A | L4 | O:D4 F:D2 S:M4 | **Disagree:** F generic knob `ShortfallMultiple` / O "×4 is A" / S quirk, yet a 4.0 default inside its generic model. R5-1: policy knob in the kernel, plain default (restore-minimum). **Decided by R5 lane F7 (the D2 question):** the adapter selects NO kernel knob — `project()` leaves `sizing` / `shortfall_multiple` / `liquidation_min_units` unset and its `resolve_margin_call_units` answers every call with TradingView's slice (the restore floored onto the lot grid BEFORE the ×4, floored again, the one-contract band, the dust gate). Measured: on a one-unit grid, 20 long at 100 on 1000 at 50 % reaching 93.9, TradingView books 4 lots and the kernel's own `ShortfallMultiple 4` books 5 (`tests/test_native_margin_hooks_twin.cpp` MG-F3, an executed run, no longer a comment); without a grid the two agree bit for bit (MG-F/H, 4.2105263157894735), but declaring the knob for those runs alone folds `NativeMarginModel::sizing` into every margin run's identity — an epoch decision F7 does not take. Since R5 lane H-THIN the pre-open admission slice (`schedule_preopen_margin_slice`) sizes through the same `source_margin_units`, where it kept its own copy of the ×4 without the band or the dust gates (AUDIT4 §6 G2-12; the private test `test_adapter_margin_schedule_differential` M10: band and fudge now 1 and 1, the kernel's and `ab9714be`'s). A four-times call on a whole-unit grid whose book's oldest lot is one unit is followed by one more unit at the next check point (`whole_unit_follow_up_due` pine_adapter.cpp:19131; R5 lane W5B-ENG-MARGIN-RESIDUAL, the private fixture set `margin_residual` w5b-es-*, w5b-sb-es-*, w5b-f-q1x12). With slippage the one-contract band takes its unit only where that unit restores the book at its own fill: the position's requirement at the liquidation's slipped print, less the equity at the mark, under one unit's margin (`source_margin_units` pine_adapter.cpp:17603; lane INT28-FIX rule OU, the private fixture set `slipped_short`). A one-unit call that leaves the book short at its own fill, by more than one unit's margin and two slippage steps with the book re-marked there, is followed by one more unit at the bar's next path point, where that point's own check calls nothing and its print is inside the call's fill (lane INT28-FIX rule OF). Under a percent commission a call of any size is followed so, on a fractional lot grid too: while the book the call left, re-marked at its fill, is short by more than two slippage steps for each unit called and the exit fees of those units and of one more, the next path point inside that fill takes four times the shortfall's lot floor over the fill, or one unit where that floors to none (`margin_follow_up_units` pine_adapter.cpp:19173, `follow_margin_call` pine_adapter.cpp:19222; R5 lane TAIL-I, the private fixture set `margin_open_print`). | | MG8 | Liquidation price query (`margin_liquidation_price` pine_strategy_host.cpp:168-184) | **no** **Closed:** `native_liquidation_price()` native_host.hpp:1320 answers the exact solved level, `nullopt` where none exists; `NativeLiquidationLevelBase` chooses whether the solve answers for the open entries' fees (`tests/test_native_margin_model.cpp`, `tests/test_native_open_lots.cpp`). | K+A | L4 | F:D3 S:R5 | **Disagree:** F generic / S adapter-only. R5-1: liquidation price is generic; the Pine spelling stays A. **Lowered by R5 lane H-THIN:** the Pine host's second solve (`compute_liquidation_price`) is gone; `strategy.margin_liquidation_price` reads `native_liquidation_price()`, which the adapter configures with `NativeLiquidationLevelBase::RealizedOnly` (the Pine formula's closed-money solve, bit for bit at every bar of the lane's 1,021 seeded margin-call runs), and keeps only TradingView's tick spelling: floored for a long, ceiled for a short. A run whose margin calls are switched off declares no margin model, so it reads na. | -| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:5677, `apply_fx_opening_margin_slice` pine_adapter.cpp:5689) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1202 but no margin re-check. **Closed by audit lane N6:** `NativeMarginCheckKind::FxRoll` — a step of the declared curve is a check point of its own, measured at the unchanged price before the first point the new rate converts (`tests/test_native_margin_fx_roll.cpp`). The adapter refuses the point by kind, for a measured reason since R5 lane F7: TradingView's rollover is a broker-open checkpoint at the open price, now at any positive margin (`apply_fx_open_margin_slice`, on the one TradingView money / units rule every other checkpoint uses), while the roll point measures at the remaining path's adverse mark and rests the slice there — a carried 1x long on a step bar O 100 L 97 books 0.4116 @ 97 on the adapter's run shape and 0.4036 @ 99, measured a bar early, on a strict calendar, against TradingView's 0.3996 @ 100 (`tests/test_native_margin_hooks_twin.cpp` MG-FX). Both N6 follow-ups are closed (§4.4). **Completed by R5 follow-up lane E3:** the conversion INSTANT is now generic too — every check kind converts at its own cursor's rate (`account_currency_fx_at(cursor.point.effective_time_ms)`) for the requirement, the equity and the level, in batch and stream, so a step landing between a fill's cursor and the clock the engine presents no longer breaches at the fill (`tests/test_native_margin_fx_clock.cpp`; N6's local clock swap inside `fx_roll_margin_check` is removed as redundant). **Extended by R5 follow-up lane E21** to the opening gate's placement site (MG4), and the rate-explicit equity E3 restated beside the engine's accessor is now that accessor's one implementation, `BacktestEngine::marked_equity_at` engine.hpp:729 | K | L4 → N6 → E3 → E21 | O:D8 | (single-source) | -| MG10 | Max drawdown halt (`update_risk_state` pine_adapter.cpp:17481) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1301 **Closed:** `NativeRiskLimits::max_drawdown` native_run_spec.hpp:311, a `NativeLossLimit{value, percent}` measured against the running peak equity at three points of every script bar, blocking openings for the rest of the run (`tests/test_native_risk_limits.cpp`). | K | L9 | F:D6 O:E1 S:M6 | R5-9 | -| MG11 | Max consecutive losing days (`SourceDayLedger` pine_adapter.hpp:1419-1427; gate `max_cons_loss_days` pine_adapter.cpp:16484) | **no** — observation only `loss_days_limit` include/pineforge/source/market_admission.hpp:49 **Closed:** `NativeRiskLimits::max_consecutive_loss_days`, settled from each day's realized result at the next day's open (`tests/test_native_risk_limits.cpp`). | K | L9 | F:D6 O:E2 S:M6 | R5-9 | -| MG12 | Max intraday loss → flatten + block (`intraday_loss_breached` pine_adapter.cpp:17514, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:20856) | **no** — observation only `intraday_loss_limit` include/pineforge/source/market_admission.hpp:51 **Closed:** `NativeRiskLimits::max_intraday_loss` with `NativeRiskAction::FlattenAndBlock`, under which the kernel issues one `Flatten` of its own at the breach point (`RequestOrigin::KernelRisk`) and latches the block before that flatten settles (`tests/test_native_risk_limits.cpp`). | K | L9 | F:D7 O:E3 S:M6 | R5-9 | -| MG13 | Max filled orders per day (`IntradayCap` intraday_cap.hpp:84-263, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:20899) | **no** **Closed:** `NativeRiskLimits::max_fills_per_day` counts every applied execution of the day as it settles, whatever issued it (`tests/test_native_risk_limits.cpp`). | K+A | L9 | F:D8 O:E4 S:M7 | Count cap K; quota transfer, POOC deferral, TV close price A | -| MG14 | Risk-day boundary (`chart_day_key` pine_adapter.cpp:17378) | **no** — the calendar exists `session_day_ordinal` native_calendar.hpp:416-419, there is no risk-day ledger **Closed:** `NativeRiskDay::{SessionDay, CalendarDayInTimezone}` keys the risk day, and `native_risk_state()` native_host.hpp:1329 reads the whole ledger back — block and reason, day, fills counted, loss-day streak, peak equity, day-opening equity (`tests/test_native_risk_limits.cpp`). | K+A | L9 | O:E5 S:M7 | The chart-day shortcut stays A; the kernel keys on session / calendar day | -| MG15 | *(quirk)* TV margin call: money rounding `source_margin_money` pine_adapter.cpp:17546, 4× shortfall `4.0 * minimum` pine_adapter.cpp:17635, POOC chronology exception `resting_order` pine_adapter.cpp:20242 (the commissioned carried short; the competing-entry and rounded-money exceptions retired by R5 lane W5B-ENG-MARGIN-RESIDUAL, the private fixture set `margin_residual` w5b-pc-*), 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:18539, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:18539) | n/a | A | — | F:D2 F:G4 O:D4 S:M4 | R5-1. **Measured (R5 lane H-MEASURE)** against the kernel's own points on the same bars and orders, the kernel's hooks answering TradingView's money and slice (the private test `test_adapter_margin_schedule_differential`): a gap-open breach executes at the open, sized on the open's money, where the kernel's mark check rests at the path's adverse extreme and `CalculationOnly` executes at the calculation (M8); the pre-open slice parts from the kernel's `AfterApplied` point, and from `ab9714be`, on the one-contract band and the restore's `+1e-6` floor (M10; the frozen units it also parted on are TradingView's book since R5 lane PAR-MARGIN froze a default-percent stop above 100 % as eight `lab tv` tapes, the private tapes `pm-m10-*` of `margin_entry_bar`, show TradingView does); the entry bar of a leveraged opening, where four `lab tv` tapes (the private fixture set `margin_entry_bar`) and the kernel's `AfterApplied` point book the margin call at that bar's low, is admitted by `on_applied` since R5 lane PAR-MARGIN on the run shapes `ab9714be` checked it on (the pre-open slice's own opening keeps that slice's verdict), and the adapter books the tapes' rows (M7; H-MEASURE had recorded it as a divergence); R5 lane PAR-MARGIN-2 extended it to limit openings, `process_orders_on_close`, `calc_on_order_fills` and adds to a carried book, and made the kernel's post-fill point reach the low a limit was filled on the way down to, or a short's high on its way up (twenty-eight `lab tv` tapes, the private tapes `pm2-*` of `margin_entry_bar`). Under the magnifier the kernel's `IntrabarSample` point (R5 lane PAR-MARGIN) is admitted on a leveraged long, plain, under `process_orders_on_close` or `calc_on_order_fills`, and on a short outside `process_orders_on_close` (PAR-MARGIN-2), where TradingView books its call at the first lower-timeframe low that crosses -- at its own 2-minute intrabars on a 15-minute chart, which a model reproduces on all 20 magnified tapes (the private fixture set `intrabar_margin`); six tapes part on the one-minute samples the host feeds, a recorded divergence of the magnifier's sampling. A margin call books the nearest tick of its print, as TradingView's market fills do (12.105 books 12.11; the private fixture set `half_tick_rounding`, PAR-MARGIN-2). | -| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:34 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:48); `admission_allocation` pine_adapter.cpp:4544, `record_market_review` pine_adapter.cpp:21155) | n/a | A | — | S:R2 | (single-source) | +| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:5679, `apply_fx_opening_margin_slice` pine_adapter.cpp:5691) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1202 but no margin re-check. **Closed by audit lane N6:** `NativeMarginCheckKind::FxRoll` — a step of the declared curve is a check point of its own, measured at the unchanged price before the first point the new rate converts (`tests/test_native_margin_fx_roll.cpp`). The adapter refuses the point by kind, for a measured reason since R5 lane F7: TradingView's rollover is a broker-open checkpoint at the open price, now at any positive margin (`apply_fx_open_margin_slice`, on the one TradingView money / units rule every other checkpoint uses), while the roll point measures at the remaining path's adverse mark and rests the slice there — a carried 1x long on a step bar O 100 L 97 books 0.4116 @ 97 on the adapter's run shape and 0.4036 @ 99, measured a bar early, on a strict calendar, against TradingView's 0.3996 @ 100 (`tests/test_native_margin_hooks_twin.cpp` MG-FX). Both N6 follow-ups are closed (§4.4). **Completed by R5 follow-up lane E3:** the conversion INSTANT is now generic too — every check kind converts at its own cursor's rate (`account_currency_fx_at(cursor.point.effective_time_ms)`) for the requirement, the equity and the level, in batch and stream, so a step landing between a fill's cursor and the clock the engine presents no longer breaches at the fill (`tests/test_native_margin_fx_clock.cpp`; N6's local clock swap inside `fx_roll_margin_check` is removed as redundant). **Extended by R5 follow-up lane E21** to the opening gate's placement site (MG4), and the rate-explicit equity E3 restated beside the engine's accessor is now that accessor's one implementation, `BacktestEngine::marked_equity_at` engine.hpp:729 | K | L4 → N6 → E3 → E21 | O:D8 | (single-source) | +| MG10 | Max drawdown halt (`update_risk_state` pine_adapter.cpp:17483) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1301 **Closed:** `NativeRiskLimits::max_drawdown` native_run_spec.hpp:311, a `NativeLossLimit{value, percent}` measured against the running peak equity at three points of every script bar, blocking openings for the rest of the run (`tests/test_native_risk_limits.cpp`). | K | L9 | F:D6 O:E1 S:M6 | R5-9 | +| MG11 | Max consecutive losing days (`SourceDayLedger` pine_adapter.hpp:1419-1427; gate `max_cons_loss_days` pine_adapter.cpp:16486) | **no** — observation only `loss_days_limit` include/pineforge/source/market_admission.hpp:49 **Closed:** `NativeRiskLimits::max_consecutive_loss_days`, settled from each day's realized result at the next day's open (`tests/test_native_risk_limits.cpp`). | K | L9 | F:D6 O:E2 S:M6 | R5-9 | +| MG12 | Max intraday loss → flatten + block (`intraday_loss_breached` pine_adapter.cpp:17516, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:20858) | **no** — observation only `intraday_loss_limit` include/pineforge/source/market_admission.hpp:51 **Closed:** `NativeRiskLimits::max_intraday_loss` with `NativeRiskAction::FlattenAndBlock`, under which the kernel issues one `Flatten` of its own at the breach point (`RequestOrigin::KernelRisk`) and latches the block before that flatten settles (`tests/test_native_risk_limits.cpp`). | K | L9 | F:D7 O:E3 S:M6 | R5-9 | +| MG13 | Max filled orders per day (`IntradayCap` intraday_cap.hpp:84-263, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:20901) | **no** **Closed:** `NativeRiskLimits::max_fills_per_day` counts every applied execution of the day as it settles, whatever issued it (`tests/test_native_risk_limits.cpp`). | K+A | L9 | F:D8 O:E4 S:M7 | Count cap K; quota transfer, POOC deferral, TV close price A | +| MG14 | Risk-day boundary (`chart_day_key` pine_adapter.cpp:17380) | **no** — the calendar exists `session_day_ordinal` native_calendar.hpp:416-419, there is no risk-day ledger **Closed:** `NativeRiskDay::{SessionDay, CalendarDayInTimezone}` keys the risk day, and `native_risk_state()` native_host.hpp:1329 reads the whole ledger back — block and reason, day, fills counted, loss-day streak, peak equity, day-opening equity (`tests/test_native_risk_limits.cpp`). | K+A | L9 | O:E5 S:M7 | The chart-day shortcut stays A; the kernel keys on session / calendar day | +| MG15 | *(quirk)* TV margin call: money rounding `source_margin_money` pine_adapter.cpp:17548, 4× shortfall `4.0 * minimum` pine_adapter.cpp:17637, POOC chronology exception `resting_order` pine_adapter.cpp:20244 (the commissioned carried short; the competing-entry and rounded-money exceptions retired by R5 lane W5B-ENG-MARGIN-RESIDUAL, the private fixture set `margin_residual` w5b-pc-*), 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:18541, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:18541) | n/a | A | — | F:D2 F:G4 O:D4 S:M4 | R5-1. **Measured (R5 lane H-MEASURE)** against the kernel's own points on the same bars and orders, the kernel's hooks answering TradingView's money and slice (the private test `test_adapter_margin_schedule_differential`): a gap-open breach executes at the open, sized on the open's money, where the kernel's mark check rests at the path's adverse extreme and `CalculationOnly` executes at the calculation (M8); the pre-open slice parts from the kernel's `AfterApplied` point, and from `ab9714be`, on the one-contract band and the restore's `+1e-6` floor (M10; the frozen units it also parted on are TradingView's book since R5 lane PAR-MARGIN froze a default-percent stop above 100 % as eight `lab tv` tapes, the private tapes `pm-m10-*` of `margin_entry_bar`, show TradingView does); the entry bar of a leveraged opening, where four `lab tv` tapes (the private fixture set `margin_entry_bar`) and the kernel's `AfterApplied` point book the margin call at that bar's low, is admitted by `on_applied` since R5 lane PAR-MARGIN on the run shapes `ab9714be` checked it on (the pre-open slice's own opening keeps that slice's verdict), and the adapter books the tapes' rows (M7; H-MEASURE had recorded it as a divergence); R5 lane PAR-MARGIN-2 extended it to limit openings, `process_orders_on_close`, `calc_on_order_fills` and adds to a carried book, and made the kernel's post-fill point reach the low a limit was filled on the way down to, or a short's high on its way up (twenty-eight `lab tv` tapes, the private tapes `pm2-*` of `margin_entry_bar`). Under the magnifier the kernel's `IntrabarSample` point (R5 lane PAR-MARGIN) is admitted on a leveraged long, plain, under `process_orders_on_close` or `calc_on_order_fills`, and on a short outside `process_orders_on_close` (PAR-MARGIN-2), where TradingView books its call at the first lower-timeframe low that crosses -- at its own 2-minute intrabars on a 15-minute chart, which a model reproduces on all 20 magnified tapes (the private fixture set `intrabar_margin`); six tapes part on the one-minute samples the host feeds, a recorded divergence of the magnifier's sampling. A margin call books the nearest tick of its print, as TradingView's market fills do (12.105 books 12.11; the private fixture set `half_tick_rounding`, PAR-MARGIN-2). | +| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:34 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:48); `admission_allocation` pine_adapter.cpp:4546, `record_market_review` pine_adapter.cpp:21157) | n/a | A | — | S:R2 | (single-source) | ### 1.5 Calculation timing | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| CT1 | Calculate at script-bar close | **yes** — `on_native_bar` native_host.hpp:893, dispatched by `invoke_callback` native_execution_consumer.cpp:8081 (call site `on_native_recalculate` native_execution_consumer.cpp:8109) | K | — | F:E1 O:F1 | — | -| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2816) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:8525 | K | — | F:E2 O:F6 S:C1 | — | -| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:22250) | **yes** — `on_native_bar_open` native_host.hpp:889 runs before the open match native_execution_consumer.cpp:7955; `execute_current` legal native_host.hpp:1104 | K | — | F:E3 O:F2 | hazard → CT4 | -| CT4 | Partial-bar view / open-bar lookahead guard | **no** — the open hook receives the *complete* script bar: `invoke_bar_open_callback(engine, bar, point)` native_execution_consumer.cpp:7898 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7949 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1073 is the lookahead-free bar so far, through the last path point consumed (a fill inside a segment reads it at the segment's origin), and `NativeOpenBarView::OpenOnly` native_run_spec.hpp:109 masks `on_native_bar_open`'s bar to `H = L = C = open` as presentation only — the complete bar is restored before the open match (`tests/test_native_calc_timing.cpp`). | K | L5 | F:E3 | (single-source) The adapter needs the full bar, so the guard is opt-in. | +| CT1 | Calculate at script-bar close | **yes** — `on_native_bar` native_host.hpp:893, dispatched by `invoke_callback` native_execution_consumer.cpp:8149 (call site `on_native_recalculate` native_execution_consumer.cpp:8177) | K | — | F:E1 O:F1 | — | +| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2816) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:8593 | K | — | F:E2 O:F6 S:C1 | — | +| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:22252) | **yes** — `on_native_bar_open` native_host.hpp:889 runs before the open match native_execution_consumer.cpp:8023; `execute_current` legal native_host.hpp:1104 | K | — | F:E3 O:F2 | hazard → CT4 | +| CT4 | Partial-bar view / open-bar lookahead guard | **no** — the open hook receives the *complete* script bar: `invoke_bar_open_callback(engine, bar, point)` native_execution_consumer.cpp:7966 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:8017 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1073 is the lookahead-free bar so far, through the last path point consumed (a fill inside a segment reads it at the segment's origin), and `NativeOpenBarView::OpenOnly` native_run_spec.hpp:109 masks `on_native_bar_open`'s bar to `H = L = C = open` as presentation only — the complete bar is restored before the open match (`tests/test_native_calc_timing.cpp`). | K | L5 | F:E3 | (single-source) The adapter needs the full bar, so the guard is opt-in. | | CT5 | Raw input observation before aggregation | **yes** — `on_native_input` native_host.hpp:875, `NativeInputContext` native_host.hpp:773-778 | K | — | O:F3 | (single-source row) | -| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:727, `coof_recalculation_due` pine_scheduler_native.cpp:693, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:848; `begin_coof_recalc` pine_adapter.cpp:6674, `flush_coof_tail` pine_adapter.cpp:7098, `end_coof_recalc` pine_adapter.cpp:6731) | **partial** — `on_native_applied` native_host.hpp:938 fires mid-path after each fill native_execution_consumer.cpp:7802, native_execution_consumer.cpp:7802; commands are legal there `commands_allowed` native_execution_consumer.cpp:1836; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:6439, `remaining_path_delivery` native_order.cpp:2082-2090. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | -| CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:878 fires per accepted realtime print native_execution_consumer.cpp:8037; no per-tick calculation in batch; the guide pinned "close-only" (`docs/pages/native-engine.md`, "Batch OHLCV vs ticks vs quiet") **Closed:** `NativeCalculationTrigger::EveryModeledPoint` native_run_spec.hpp:97 adds one `Tick` recalculation at every modeled point of the delivered path and at every observed print, in batch and in a stream alike (`tests/test_native_calc_timing.cpp`). | K | L5 | F:E5 O:F4 S:C3 | **Disagree:** F yes / O partial / S no. R5-5: v1. Not an adapter feature either: absent from `PineStrategyConfig` pine_adapter.hpp:62-76, the runner rejects it (`calc_on_every_tick` main.cpp:259), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:291. | -| CT8 | Historical intrabar calculation (per lower-TF sub-bar / magnifier sample) | **no** — with an `IntrabarPath` the callback still fires once per script bar native_execution_consumer.cpp:8613 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:928 fires once after each retained lower-timeframe sub-bar's whole matching path (`tests/test_native_calc_timing.cpp`). | K | L5 | F:E6 O:F8 S:C4 | — | -| CT9 | Intrabar (magnifier) matching path + path-order policy (`volume_weighted_cap` pine_adapter.cpp:2934) | **yes** — `IntrabarPath` native_run_spec.hpp:362-403, delivery `deliver_confirmed_script` native_execution_consumer.cpp:8184-8450; `NativePathOrder` native_run_spec.hpp:335-339 | K+A | — | F:E7 F:G5 O:F7 S:C4 | **Disagree:** S partial (it wants callback cadence → CT8). TV sampler choice is A: TradingView's own intrabars at its table's timeframe, each owned by the chart bar of its last minute, built from the finer feed (`tradingview_magnifier_bars` magnifier_intrabars.hpp:44, R5 lane MAG-INTRABAR). | -| CT10 | Batch → warmup → realtime forward execution | **yes** — `stream_begin` engine.hpp:1771, `stream_push_bar` engine.hpp:1777, `stream_end` engine.hpp:1781, `NativeRunPhase` native_host.hpp:41, C ABI `strategy_stream_begin` c_abi.cpp:698 | K+A | — | F:E8 O:J3 S:C5 | **Disagree:** S partial. R5-1: the lifecycle is native; Pine trade-start suppression and realtime-tail / probe rules (`trading_window_active` pine_strategy_commands.cpp:15-21, `prepare_native_begin` pine_strategy_host.cpp:206-307) are A. S's "neutral tail + trade-window policy" is single-source and unassigned (§4 Q8). | -| CT11 | *(quirk)* COOF TV specifics: waypoint-only refill (`coof_next_waypoint` pine_adapter.cpp:6947), two-fills-at-open rule, script-state rollback (`PineScheduler` pine_scheduler_native.cpp:54-106; hooks `snapshot_script_state` engine.hpp:1332-1333), the first-open guard `kFirstOpenLoopGuard` pine_scheduler_native.cpp:848, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1961-1991 | n/a | A | — | F:E4 O:F5 S:C2 | R5-5: the adapter keeps its path | +| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:727, `coof_recalculation_due` pine_scheduler_native.cpp:693, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:848; `begin_coof_recalc` pine_adapter.cpp:6676, `flush_coof_tail` pine_adapter.cpp:7100, `end_coof_recalc` pine_adapter.cpp:6733) | **partial** — `on_native_applied` native_host.hpp:938 fires mid-path after each fill native_execution_consumer.cpp:7870, native_execution_consumer.cpp:7870; commands are legal there `commands_allowed` native_execution_consumer.cpp:1843; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:6507, `remaining_path_delivery` native_order.cpp:2082-2090. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | +| CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:878 fires per accepted realtime print native_execution_consumer.cpp:8105; no per-tick calculation in batch; the guide pinned "close-only" (`docs/pages/native-engine.md`, "Batch OHLCV vs ticks vs quiet") **Closed:** `NativeCalculationTrigger::EveryModeledPoint` native_run_spec.hpp:97 adds one `Tick` recalculation at every modeled point of the delivered path and at every observed print, in batch and in a stream alike (`tests/test_native_calc_timing.cpp`). | K | L5 | F:E5 O:F4 S:C3 | **Disagree:** F yes / O partial / S no. R5-5: v1. Not an adapter feature either: absent from `PineStrategyConfig` pine_adapter.hpp:62-76, the runner rejects it (`calc_on_every_tick` main.cpp:259), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:295. | +| CT8 | Historical intrabar calculation (per lower-TF sub-bar / magnifier sample) | **no** — with an `IntrabarPath` the callback still fires once per script bar native_execution_consumer.cpp:8681 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:928 fires once after each retained lower-timeframe sub-bar's whole matching path (`tests/test_native_calc_timing.cpp`). | K | L5 | F:E6 O:F8 S:C4 | — | +| CT9 | Intrabar (magnifier) matching path + path-order policy (`volume_weighted_cap` pine_adapter.cpp:2934) | **yes** — `IntrabarPath` native_run_spec.hpp:362-403, delivery `deliver_confirmed_script` native_execution_consumer.cpp:8252-8518; `NativePathOrder` native_run_spec.hpp:335-339 | K+A | — | F:E7 F:G5 O:F7 S:C4 | **Disagree:** S partial (it wants callback cadence → CT8). TV sampler choice is A: TradingView's own intrabars at its table's timeframe, each owned by the chart bar of its last minute, built from the finer feed (`tradingview_magnifier_bars` magnifier_intrabars.hpp:44, R5 lane MAG-INTRABAR). | +| CT10 | Batch → warmup → realtime forward execution | **yes** — `stream_begin` engine.hpp:1771, `stream_push_bar` engine.hpp:1777, `stream_end` engine.hpp:1781, `NativeRunPhase` native_host.hpp:41, C ABI `strategy_stream_begin` c_abi.cpp:698 | K+A | — | F:E8 O:J3 S:C5 | **Disagree:** S partial. R5-1: the lifecycle is native; Pine trade-start suppression and realtime-tail / probe rules (`trading_window_active` pine_strategy_commands.cpp:15-21, `prepare_native_begin` pine_strategy_host.cpp:206-311) are A. S's "neutral tail + trade-window policy" is single-source and unassigned (§4 Q8). | +| CT11 | *(quirk)* COOF TV specifics: waypoint-only refill (`coof_next_waypoint` pine_adapter.cpp:6949), two-fills-at-open rule, script-state rollback (`PineScheduler` pine_scheduler_native.cpp:54-106; hooks `snapshot_script_state` engine.hpp:1332-1333), the first-open guard `kFirstOpenLoopGuard` pine_scheduler_native.cpp:848, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1961-1991 | n/a | A | — | F:E4 O:F5 S:C2 | R5-5: the adapter keeps its path | ### 1.6 Higher timeframes and auxiliary feeds | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| HT1 | HTF series of the same symbol (`request.security`): evaluators registered `scheduler_configure_security_evaluators` pine_strategy_host.cpp:1519, pumped from the scheduler `scheduler_prepare_security_sequence` pine_strategy_host.cpp:1529-1828; state lives in `BacktestEngine` engine.hpp:356-558, engine.hpp:356-402, engine_security.cpp:27 | **no** sanctioned path — `configure_security_evaluators` is an empty virtual engine.hpp:1322 whose only caller is pine_strategy_host.cpp:1520 **Closed:** `NativeRunSpec::subscriptions` (`NativeTimeframeSubscription` native_run_spec.hpp:491), or `declare_timeframe_subscriptions` native_host.hpp:1136 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:885 and `native_series_bar(index)` native_host.hpp:1119 reading them back; the kernel registers the declared series after that callback returns, so a host's own evaluators survive (`tests/test_native_htf_subscriptions.cpp`, `tests/test_native_htf_subscriptions_stream.cpp`). | K | L6 | F:F1 O:G-1 S:H1 | **Disagree:** S partial (it counts kernel-side storage → HT2); F, S v1 / O deferrable. R5-6: v1. | -| HT2 | Authoritative (exchange) HTF bars substituted into a series (`set_native_security_feed` engine.hpp:1731, engine_aux_security.cpp:79-133, C ABI `strategy_set_native_security_feed` c_abi.cpp:937) | **partial (inert)** — pre-run staging is accepted `refuse_source_mutation` native_execution_consumer.cpp:1871 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, its sole call `prepare_native_security_feeds` pine_strategy_host.cpp:1651; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1889 **Closed:** a bare host's subscriptions consume authoritative bars: `NativeTimeframeSubscription::authoritative_bars` native_run_spec.hpp:493 is staged into the feed store by the kernel's own `prepare_native_security_feeds` native_execution_consumer.cpp:9422 call at begin, so a declared series is the venue's own bars (`tests/test_native_htf_subscriptions.cpp`). | K | L6 | F:F2 O:G-2 S:H1 | F says "no" on the same facts. "native" in these names means TradingView's native-timeframe bars, not the native host (the feed store's comment `never from the chart's intraday prints` `sha256:3cc5174fa9a60fc0e2ca7870ef1cccd875abaf5d8e11248e633922612c18d9d8` engine_aux_security.cpp:51-61). | +| HT1 | HTF series of the same symbol (`request.security`): evaluators registered `scheduler_configure_security_evaluators` pine_strategy_host.cpp:1524, pumped from the scheduler `scheduler_prepare_security_sequence` pine_strategy_host.cpp:1534-1833; state lives in `BacktestEngine` engine.hpp:356-558, engine.hpp:356-402, engine_security.cpp:27 | **no** sanctioned path — `configure_security_evaluators` is an empty virtual engine.hpp:1322 whose only caller is pine_strategy_host.cpp:1525 **Closed:** `NativeRunSpec::subscriptions` (`NativeTimeframeSubscription` native_run_spec.hpp:491), or `declare_timeframe_subscriptions` native_host.hpp:1136 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:885 and `native_series_bar(index)` native_host.hpp:1119 reading them back; the kernel registers the declared series after that callback returns, so a host's own evaluators survive (`tests/test_native_htf_subscriptions.cpp`, `tests/test_native_htf_subscriptions_stream.cpp`). | K | L6 | F:F1 O:G-1 S:H1 | **Disagree:** S partial (it counts kernel-side storage → HT2); F, S v1 / O deferrable. R5-6: v1. | +| HT2 | Authoritative (exchange) HTF bars substituted into a series (`set_native_security_feed` engine.hpp:1731, engine_aux_security.cpp:79-133, C ABI `strategy_set_native_security_feed` c_abi.cpp:937) | **partial (inert)** — pre-run staging is accepted `refuse_source_mutation` native_execution_consumer.cpp:1878 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, its sole call `prepare_native_security_feeds` pine_strategy_host.cpp:1656; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1896 **Closed:** a bare host's subscriptions consume authoritative bars: `NativeTimeframeSubscription::authoritative_bars` native_run_spec.hpp:493 is staged into the feed store by the kernel's own `prepare_native_security_feeds` native_execution_consumer.cpp:9490 call at begin, so a declared series is the venue's own bars (`tests/test_native_htf_subscriptions.cpp`). | K | L6 | F:F2 O:G-2 S:H1 | F says "no" on the same facts. "native" in these names means TradingView's native-timeframe bars, not the native host (the feed store's comment `never from the chart's intraday prints` `sha256:3cc5174fa9a60fc0e2ca7870ef1cccd875abaf5d8e11248e633922612c18d9d8` engine_aux_security.cpp:51-61). | | HT3 | Lower-timeframe arrays (`request.security_lower_tf`; `register_security_lower_tf_eval` include/pineforge/source/pine_strategy_host.hpp:832) | **no** — `IntrabarPath::lower_tf` drives matching only **Closed:** `on_native_sub_bar` native_host.hpp:928 delivers every retained `lower_tf` sub-bar after its matching path, so a host that put the lower bars in the path reads them bar by bar (`tests/test_native_calc_timing.cpp`); the series form is the auxiliary feed of HT4 (`tests/test_native_auxiliary_feed.cpp`). | K | L5 (sub-bar hook); series form optional in L6 | F:F4 O:G-3 S:H2 | **Disagree on lane:** F via `on_native_sub_bar` (the host already owns the lower bars it put in the path) / O, S via the series API. v1 = the L5 hook (R5-5), landed; the array form Pine returns stays the adapter's. | | HT4 | Auxiliary finer feed driving `request.security` (`set_aux_security_feed` pine_aux_security.cpp:23-241, `feed_aux_security_for_chart_bar` pine_aux_security.cpp:262-488; the kernel base returns false `set_aux_security_feed` engine_consumer.cpp:158-161) | **no** **Closed:** `NativeRunSpec::auxiliary_feed` (`NativeAuxiliaryFeed` native_run_spec.hpp:524) plus `NativeSeriesSource::AuxiliaryFeed` build a series from the host's finer bars, routed by time alone, with `declare_auxiliary_feed` at begin and `append_auxiliary_bars` on a realtime stream (`tests/test_native_auxiliary_feed.cpp`, `tests/test_native_auxiliary_feed_stream.cpp`). | K+A | L6 | F:F3 S:H2 | **Disagree:** F quirk (TV split-feed harness construct) / S generic. R5-1 + R5-6: aux feed registration is generic; chart-slice mapping and deferred first-bucket publication stay A. **Closed (lane INT28-FIX, rule RC):** a `calc_on_order_fills` recalculation reads the calling bar's own slice, as the bar's close calculation does and as TradingView's does after the open fill or a later fill (a lookahead_on site through the slice's first bucket; `lab tv` tapes (the private fixture set `coof_recalc_security`): the bar's first recalculation before its close callback feeds it (`scheduler_feed_aux_security` pine_scheduler_native.cpp:772). The close callback fed it alone (`scheduler_feed_aux_security` pine_scheduler_native.cpp:634, now only when no recalculation did), after the bar's recalculations had read the previous bar's; a recalculation after that callback feeds nothing. | | HT5 | Publication mode: lookahead, gaps, session / early-close boundaries (`prepare_historical_security_lookahead_projections` pine_scheduler.cpp:42-132, `calling_bar_complete` pine_aux_security.cpp:282-397) | **no** for a bare host **Closed for a bare host:** `NativeTimeframeSubscription::lookahead` and `::gaps` are the two delivery rules, and the calendar boundaries — session close, early close by instrument class, the lazy seal of a clipped script interval — are the kernel's own (`tests/test_native_htf_subscriptions.cpp`). | K+A | L6 | S:H3 F:F1 O:G-1 | **Disagree:** F "no `lookahead_on`, by construction" / O `lookahead = false` default, available / S `lookahead_on` field, status partial. R5-6: user question (§4 U1); the exact TV projection stays A. | -| HT6 | Self-aggregation recipe (interim) | **yes** — `TimeframeAggregator` is public and engine-free timeframe.hpp:288-486, fed from `on_native_input` native_host.hpp:875; the Pine scheduler asks the kernel's `script_bucket_completions` native_execution_consumer.cpp:8918, which feeds the same class `TimeframeAggregator` native_execution_consumer.cpp:8925, how its input span buckets (R5 lane B-ADAPTER) | K | L0 (document) | O:G-4 F:F1 | R5-6 | +| HT6 | Self-aggregation recipe (interim) | **yes** — `TimeframeAggregator` is public and engine-free timeframe.hpp:288-486, fed from `on_native_input` native_host.hpp:875; the Pine scheduler asks the kernel's `script_bucket_completions` native_execution_consumer.cpp:8986, which feeds the same class `TimeframeAggregator` native_execution_consumer.cpp:8993, how its input span buckets (R5 lane B-ADAPTER) | K | L0 (document) | O:G-4 F:F1 | R5-6 | ### 1.7 Fill price, slippage, commission, FX | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| FP1 | Trigger geometry on the modeled path; fill at the level or at the gap price; market-if-touched | **yes** — `native_matching` native_execution_consumer.cpp:6511, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:7101; `Limit::fill_through` native_order.hpp:239-244 enforced where the matched price is limit-protected (`protect_limit` native_execution_consumer.cpp:7097) and re-checked after host terms (`InvalidTerms` native_execution_consumer.cpp:5766) | K | — | F:G3 O:H1 O:H2 O:H3 | — | -| FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:652 applied once native_execution_consumer.cpp:5402, clamped by `protect_limit` native_execution_consumer.cpp:7097 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:15168) is A | +| FP1 | Trigger geometry on the modeled path; fill at the level or at the gap price; market-if-touched | **yes** — `native_matching` native_execution_consumer.cpp:6579, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:7169; `Limit::fill_through` native_order.hpp:239-244 enforced where the matched price is limit-protected (`protect_limit` native_execution_consumer.cpp:7165) and re-checked after host terms (`InvalidTerms` native_execution_consumer.cpp:5834) | K | — | F:G3 O:H1 O:H2 O:H3 | — | +| FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:652 applied once native_execution_consumer.cpp:5470, clamped by `protect_limit` native_execution_consumer.cpp:7165 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:15170) is A | | FP3 | Commission: percent / cash per unit / cash per execution (`fee_kind_for` pine_adapter.cpp:669) | **yes** — `NativeFeeKind` native_run_spec.hpp:19-23; `quote_execution_commissions` engine_execution.cpp:1683 | K | — | F:G2 O:H8 S:F1 | — | -| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:274, `source_bar_fill` pine_adapter.cpp:15168, `directional_tick` pine_adapter.cpp:327) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:7079; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:7252; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_sizing_price_rule.cpp` quantizes the fill basis under `HalfUp`; `tests/test_native_acid_composite.cpp` books the `Directional` fills on the 0.25 ladder; the private test `test_native_price_grid` re-ran the pair under the overlay). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | +| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:274, `source_bar_fill` pine_adapter.cpp:15170, `directional_tick` pine_adapter.cpp:327) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:7147; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:7320; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_sizing_price_rule.cpp` quantizes the fill basis under `HalfUp`; `tests/test_native_acid_composite.cpp` books the `Directional` fills on the 0.25 ladder; the private test `test_native_price_grid` re-ran the pair under the overlay). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | | FP5 | Trigger tested against the tick-quantized bar | **no** — the kernel tests raw prices against raw levels **Closed:** `NativePriceGrid::QuantizeFillsAndTriggers` additionally tests and re-validates every activation against the tick-quantized path, one rule for every trigger kind (`tests/test_native_trail_best_seed.cpp` puts an off-ladder trail seed on the ladder; `tests/test_native_acid_composite.cpp` runs a trail's best and its exit level on it; the private test `test_native_price_grid` re-ran the pair under the overlay). | K+A | L8 | O:H5 F:G4 S:F3 | R5-3: the optional quantized trigger test is K; TV's exact half-tick rule (`source_trigger_threshold` pine_adapter.cpp:384) is A on top. | | PG | The Pine adapter on the kernel price grid: raw levels submitted, `project()` declaring `QuantizeFillsAndTriggers` + `HalfUp`, `source_trigger_threshold` deleted (lane R7; re-tested after L8b by gap lane N13) | **measured-infeasible for the Pine adapter** — B1 (the sub-tick cursor-print abort) is closed by L8b's `ActivationGrid`: 0 aborted runs under the trial (R7: 9 zero-offset-trail tapes + 6 POOC panels; now `test_zero_offset_trail_rides_l4c` 449/450, `test_pooc_short_close_tick_l4d` 183/183). B2 (TradingView quantizes per order kind, the grid is one run-wide rule) moved 30 pinned checks in 4 units when N13 last ran the trial, before lanes E14 and E16, with no adapter-side remedy: `test_coof_market_limit_recross_l4c` 24, `test_stop_tick_rounding_l4d` 3, `test_adapter_grid_relower` 2, `test_zero_offset_trail_rides_l4c` 1; 12 more checks in 3 units have an adapter-side cause (`test_trail_fill_snap_l4c` 2, `test_native_margin_hooks` 2, `test_adapter_brackets_relower` 8 book digests). Corpus: 5/312 probes differ, in the engine-only entry-incarnation column only (every probe runs a 0.01 tick on an on-grid feed, so it cannot arbitrate) | A | waived (R7, N13) | — | **Waiver.** No class of triggers is byte-identical on its own: the grid is a run-wide switch and a per-kind mask would spell TradingView's inconsistency into the kernel (not generic, R5-3). The adapter stays on `NativePriceGrid::None` and keeps `source_trigger_threshold` and its call sites; native-engine.md "The Pine adapter's grid re-lowering is waived". **Native-only by ruling (audit lane P6): §3.6.2**, with `examples/native/native_price_grid_strategy.cpp` and its C twin as the feature's hosts. | -| FP6 | Account-currency FX | **partial** — scalar `account_fx` + immutable `NativeFxCurve` native_host.hpp:1202 in batch; the stream then refused a non-empty curve (today only tick-driven input is refused under one, `refuse_fx_curve_tick_input` native_execution_consumer.cpp:9866). **Closed by audit lane N6:** the declared curve is the run's immutable FX epoch and a confirmed-bar stream runs under it exactly as a batch does (stream twin in `tests/test_native_margin_fx_roll.cpp`); no realtime rate ingestion and no tick-driven input under a curve are stated as permanent limits with their reasons (native-engine.md, "Stream FX"). **Re-examined by R5 follow-up lane E3 and KEPT:** the tick refusal was never only about the margin check — it guards the SCRIPT-VISIBLE conversions (`open_trade_profit()`, `marked_equity()`, the equity curve), which read `active_account_currency_fx()`, i.e. the presented clock the observation hook has not yet moved (re-measured: a step at T+90s left the T+100s and T+110s prints converting at the pre-step rate). E3's cursor rule reaches the margin model only, so the refusal stands and is pinned in `tests/test_native_margin_fx_clock.cpp` | K | unassigned → N6 (E3: refusal kept) | F:D9 S:F4 | **Disagree:** F out of scope ("O7 clock") / S its P10, a stream-safe FX epoch. No R5 lane → §4 U4 (answered by N6: S's "same curve in batch and stream", without a new clock or callback). | -| FP7 | *(quirk)* TV tick conventions: half-tick threshold `source_bar_fill_tick` pine_adapter.cpp:295 (used `source_trigger_threshold` pine_adapter.cpp:384, `exit_limit_trigger` pine_adapter.cpp:14362), `source_bar_fill_tick` pine_adapter.cpp:295, raw-vs-booked spelling `source_same_point` pine_adapter.cpp:313, margin-call fill pricing `source_margin_fill_price` pine_adapter.cpp:17770 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:14844) | A | — | F:G4 O:H5 S:F3 | R5-1, R5-3 | +| FP6 | Account-currency FX | **partial** — scalar `account_fx` + immutable `NativeFxCurve` native_host.hpp:1202 in batch; the stream then refused a non-empty curve (today only tick-driven input is refused under one, `refuse_fx_curve_tick_input` native_execution_consumer.cpp:9934). **Closed by audit lane N6:** the declared curve is the run's immutable FX epoch and a confirmed-bar stream runs under it exactly as a batch does (stream twin in `tests/test_native_margin_fx_roll.cpp`); no realtime rate ingestion and no tick-driven input under a curve are stated as permanent limits with their reasons (native-engine.md, "Stream FX"). **Re-examined by R5 follow-up lane E3 and KEPT:** the tick refusal was never only about the margin check — it guards the SCRIPT-VISIBLE conversions (`open_trade_profit()`, `marked_equity()`, the equity curve), which read `active_account_currency_fx()`, i.e. the presented clock the observation hook has not yet moved (re-measured: a step at T+90s left the T+100s and T+110s prints converting at the pre-step rate). E3's cursor rule reaches the margin model only, so the refusal stands and is pinned in `tests/test_native_margin_fx_clock.cpp` | K | unassigned → N6 (E3: refusal kept) | F:D9 S:F4 | **Disagree:** F out of scope ("O7 clock") / S its P10, a stream-safe FX epoch. No R5 lane → §4 U4 (answered by N6: S's "same curve in batch and stream", without a new clock or callback). | +| FP7 | *(quirk)* TV tick conventions: half-tick threshold `source_bar_fill_tick` pine_adapter.cpp:295 (used `source_trigger_threshold` pine_adapter.cpp:384, `exit_limit_trigger` pine_adapter.cpp:14364), `source_bar_fill_tick` pine_adapter.cpp:295, raw-vs-booked spelling `source_same_point` pine_adapter.cpp:313, margin-call fill pricing `source_margin_fill_price` pine_adapter.cpp:17772 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:14846) | A | — | F:G4 O:H5 S:F3 | R5-1, R5-3 | ### 1.8 Excursion, metrics, report, hash | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| RP1 | Per-lot MFE / MAE, kernel-sampled or host-owned | **yes** — sampler `apply_excursion` native_execution_consumer.cpp:3280, closing-row fold `max_runup` engine_orders.cpp:157-171; `owns_lot_excursions` native_host.hpp:1021-1025 | K+A | — | F:H1 O:I1 O:I2 S:X1 | **Disagree:** S partial + its P8 excursion-mode enum (single-source, not adopted, Appendix A.9). **Lowered by R5 lane H-THIN:** the Pine host keeps the kernel's `owns_lot_excursions()` default, so the sampler is the Pine excursion; the host model, its entry-bar masks and its caches are deleted, and the source layer's fold no longer carries them. **Measured against TradingView first (R5 lane H-MEASURE, `tests/test_e19_excursion_tape.cpp`):** the host-owned model was `ab9714be`'s, not TradingView's — where it and the kernel sampler differ the kernel's number is TradingView's (entry-bar over-mask, exit-bar over-fold after an at-open exit, a `calc_on_order_fills` scratch TradingView reports as 0 / 0); both miss a bar's open print ahead of a same-bar stop fill. On the corpus the two models differ on 341 of 431 390 trades, and of the 326 of those a TradingView export identifies, the kernel's number is TradingView's on 315 and the host's on none. The lowering moved the cells of 335 trades in 19 corpus probes: 324 now equal TradingView's, 11 moved closer, none farther; the all-in residual's own entry-bar call is the tape of the private fixture set `e19_allin_trim`. | +| RP1 | Per-lot MFE / MAE, kernel-sampled or host-owned | **yes** — sampler `apply_excursion` native_execution_consumer.cpp:3348, closing-row fold `max_runup` engine_orders.cpp:157-171; `owns_lot_excursions` native_host.hpp:1021-1025 | K+A | — | F:H1 O:I1 O:I2 S:X1 | **Disagree:** S partial + its P8 excursion-mode enum (single-source, not adopted, Appendix A.9). **Lowered by R5 lane H-THIN:** the Pine host keeps the kernel's `owns_lot_excursions()` default, so the sampler is the Pine excursion; the host model, its entry-bar masks and its caches are deleted, and the source layer's fold no longer carries them. **Measured against TradingView first (R5 lane H-MEASURE, `tests/test_e19_excursion_tape.cpp`):** the host-owned model was `ab9714be`'s, not TradingView's — where it and the kernel sampler differ the kernel's number is TradingView's (entry-bar over-mask, exit-bar over-fold after an at-open exit, a `calc_on_order_fills` scratch TradingView reports as 0 / 0); both miss a bar's open print ahead of a same-bar stop fill. On the corpus the two models differ on 341 of 431 390 trades, and of the 326 of those a TradingView export identifies, the kernel's number is TradingView's on 315 and the host's on none. The lowering moved the cells of 335 trades in 19 corpus probes: 324 now equal TradingView's, 11 moved closer, none farther; the all-in residual's own entry-bar call is the tape of the private fixture set `e19_allin_trim`. | | RP2 | Closed-trade list, trade statistics, report struct | **yes** — `fill_report` engine_report.cpp:39-85, `get_trade` engine.hpp:1792-1829, `compute_trade_stats` engine_metrics.cpp:122 | K | — | F:H2 O:I3 S:X2 | — | | RP3 | Equity curve | **no** — `update_equity_extremes` engine.hpp:1362 / `record_equity_point` engine.hpp:1380 are protected and were called only by the adapter **Closed:** `NativeReportPolicy::KernelRecorded` native_run_spec.hpp:63 has the consumer fold the extremes and append one point per script calculation, labelled with the script interval's open; `KernelRecordedAtHostMarks` records the same series at the host's own marks (`tests/test_native_report_truth.cpp`). | K | L2 | F:H3 O:I4 S:X2 | S's "partial" folds RP2 in | | RP4 | Equity metrics (drawdown, run-up, Sharpe / Sortino, CAGR, time in market) | **no** — `compute_equity_stats` exits early on an empty curve engine_metrics.cpp:203; `fill_metrics_section` engine_report.cpp:138-145 **Closed:** with a recorded curve the drawdown / run-up walk is finite and every equity metric is computed over a real series; `report_open_position_at_end` adds the still-open position as a mark-to-market row (`tests/test_native_report_truth.cpp`). | K | L2 | F:H3 O:I5 | silent degenerate metrics, no error (O) | -| RP5 | Range-end row for a position still open at the end (`scheduler_record_range_end` pine_strategy_host.cpp:1982) | **yes, shared** — one generic producer `NativeExecutionConsumer::append_open_position_report_rows`, called by the kernel's run-end `record_open_position_report_rows` (`KernelRecorded` + `report_open_position_at_end`) and by the adapter at TradingView's mark; `report_trade_count()` engine.hpp:1831 includes the rows | K + A report shape | L2, Q6 | F:H4 O:I6 S:X2 | **Disagree:** S says kernel range-end rows exist (storage only). The ROWS are now the kernel's (§3.7 part 1); TV report shape — the equity re-mark, the extreme re-fold, the same-bar re-sort, three marks — stays A, measured in §3.7 part 2. The short-seed swaps (`project_short_seed_report_rows` pine_strategy_host.cpp:1444) stay A as well: a projection of two rows' `entry_incarnation` onto `ab9714be`'s numbering, which TradingView does not export; measured against the kernel's own lot identities by `tests/test_short_seed_report_swap.cpp` (R5 lane H-MEASURE), and pinned by `test_native_oracle_short_seed_percent_full_l2`, whose two incarnation checks fail when the swap is skipped. The swap runs only when a later close of the seed id closes a remnant; a flat collision keeps the kernel's numbering. | +| RP5 | Range-end row for a position still open at the end (`scheduler_record_range_end` pine_strategy_host.cpp:1987) | **yes, shared** — one generic producer `NativeExecutionConsumer::append_open_position_report_rows`, called by the kernel's run-end `record_open_position_report_rows` (`KernelRecorded` + `report_open_position_at_end`) and by the adapter at TradingView's mark; `report_trade_count()` engine.hpp:1831 includes the rows | K + A report shape | L2, Q6 | F:H4 O:I6 S:X2 | **Disagree:** S says kernel range-end rows exist (storage only). The ROWS are now the kernel's (§3.7 part 1); TV report shape — the equity re-mark, the extreme re-fold, the same-bar re-sort, three marks — stays A, measured in §3.7 part 2. The short-seed swaps (`project_short_seed_report_rows` pine_strategy_host.cpp:1449) stay A as well: a projection of two rows' `entry_incarnation` onto `ab9714be`'s numbering, which TradingView does not export; measured against the kernel's own lot identities by `tests/test_short_seed_report_swap.cpp` (R5 lane H-MEASURE), and pinned by `test_native_oracle_short_seed_percent_full_l2`, whose two incarnation checks fail when the swap is skipped. The swap runs only when a later close of the seed id closes a remnant; a flat collision keeps the kernel's numbering. | | RP6 | Per-trade accessors | **partial** — `get_trade` is public; the `closed_trade_*` family is protected engine.hpp:1391 **Closed:** `closed_trade_count()` / `closed_trade(i)` and `report_trade_count()` / `get_report_trade(i)` are public, and `closed_trade_close_cause(i)` engine.hpp:1849 answers the typed `execution::CloseCause` the closer recorded (`tests/test_native_report_truth.cpp`). | K | L2 | F:H6 | (single-source) | -| RP7 | Live order-action stream (runner ledger / webhook) | **yes** — `stream_observe_actions_` engine_execution.cpp:1383, enabled `stream_observe_actions_` native_execution_consumer.cpp:10589, C ABI `strategy_stream_order_actions_len` c_abi.cpp:737-754 | K | — | F:H5 | (single-source) | -| RP8 | Continuation hash | **yes** — `native_continuation_hash` native_host.hpp:1376, native_execution_consumer.cpp:12152; the spec is folded by `hash_spec` native_execution_consumer.cpp:238-260 | K | — | F:I1 O:J2 S:X3 | — | -| RP9 | Broker-state hash: scalar + per-bar recording | **partial** — scalar `broker_state_hash` engine_state_hash.cpp:22-118, C ABI `strategy_broker_state_hash` c_abi.cpp:593; per-bar rows are appended only by the source scheduler `scheduler_record_broker_hash` pine_strategy_host.cpp:2790 **Closed:** under `KernelRecorded` with `set_broker_state_hash_recording(true)` the consumer appends one `broker_state_hash()` row after each report point, so `broker_state_hash_len == equity_curve_len == script_bars_processed` on a completed run in batch and across a stream's warmup and realtime legs. A Failed or Aborted run can expose a shorter prefix. A row is the run's continuation identity at that bar, per driving mode — the broker half alone is mode-invariant (`tests/test_native_report_truth.cpp`). | K | L2 | F:I2 O:J1 S:X3 | O's "yes" covers the scalar only. §3.9 records the old quadratic adapter fold and closed-row kernel walk as history; V19-E and V19-A replaced them with running digests. | +| RP7 | Live order-action stream (runner ledger / webhook) | **yes** — `stream_observe_actions_` engine_execution.cpp:1383, enabled `stream_observe_actions_` native_execution_consumer.cpp:10666, C ABI `strategy_stream_order_actions_len` c_abi.cpp:737-754 | K | — | F:H5 | (single-source) | +| RP8 | Continuation hash | **yes** — `native_continuation_hash` native_host.hpp:1376, native_execution_consumer.cpp:12229; the spec is folded by `hash_spec` native_execution_consumer.cpp:238-260 | K | — | F:I1 O:J2 S:X3 | — | +| RP9 | Broker-state hash: scalar + per-bar recording | **partial** — scalar `broker_state_hash` engine_state_hash.cpp:22-118, C ABI `strategy_broker_state_hash` c_abi.cpp:593; per-bar rows are appended only by the source scheduler `scheduler_record_broker_hash` pine_strategy_host.cpp:2795 **Closed:** under `KernelRecorded` with `set_broker_state_hash_recording(true)` the consumer appends one `broker_state_hash()` row after each report point, so `broker_state_hash_len == equity_curve_len == script_bars_processed` on a completed run in batch and across a stream's warmup and realtime legs. A Failed or Aborted run can expose a shorter prefix. A row is the run's continuation identity at that bar, per driving mode — the broker half alone is mode-invariant (`tests/test_native_report_truth.cpp`). | K | L2 | F:I2 O:J1 S:X3 | O's "yes" covers the scalar only. §3.9 records the old quadratic adapter fold and closed-row kernel walk as history; V19-E and V19-A replaced them with running digests. | | RP10 | Fold the host's own durable state into the hash | **partial** — the protected virtual `hash_source_extension` engine.hpp:411 is overridable but source-named; the kernel default folds `"source:none"` engine_state_hash.cpp:19-21 **Closed:** `hash_host_extension(BrokerStateHashSink&)` engine.hpp:405 is the generic seam — protected virtual on `BacktestEngine`, called once per hash, last — and `hash_source_extension` engine.hpp:411 is its deprecated spelling, which the generic default forwards to (`tests/test_native_host_hash_extension.cpp`). | K | L2 (the virtual rides the shared bump) | F:I3 O:J1 S:X3 | — | ### 1.9 Other runtime surface @@ -176,8 +176,8 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | OT4 | Cooperative abort | **yes** — `request_abort` engine.hpp:2174, `NativeAbortReporting` native_run_spec.hpp:41-44 | K | — | F:J4 | (single-source) | | OT5 | C-level run configuration, stream, FX curve, contract probe | **yes** — `strategy_configure_native_v1` c_abi.cpp:980, FX curve `strategy_configure_native_fx_curve_v1` c_abi.cpp:1051, probe `strategy_execution_contract` c_abi.cpp:971, stream `strategy_stream_begin` c_abi.cpp:698 **Extended:** `pf_native_run_spec_ext_v1`'s policy tail now carries `intrabar`, `path_order`, `abort_reporting` and the slot-label / feed-tolerance policies (`PF_NATIVE_SPEC_EXT_INTRABAR`, `PF_NATIVE_SPEC_EXT_FEED_POLICY`), so nothing of `NativeRunSpec` but `identity` is unreachable from C (`tests/test_native_c_api.c`). | K | — | O:J4 | The v1 C spec `pf_native_run_spec_v1` pineforge.h:510 omitted `intrabar`, `path_order` and `abort_reporting`; they now travel in the extension's policy tail, `pf_native_abort_reporting_e` native_c_api.h:911 among them (F §2.iii) | | OT6 | C-level order submission + strategy callbacks | **no** — none of the original runtime `PF_API` symbols; then listed as a refusal on native-engine.md **Closed:** `strategy_native_host_create_v1` takes a C callback table and `strategy_native_submit_v1` / `_replace_v1` / `_cancel_v1` / `_cancel_all_v1` / `_cancel_where_v1` / `_execute_current_v1` command the same kernel under the same legality rule; the 41 `strategy_native_*` declarations plus two `strategy_configure_native_ext_*` declarations are a second, disjoint inventory beside the 80 runtime exports (and eight generated per-strategy exports) (`tests/test_native_c_api.c`, `tests/test_native_c_api_frozen_header.cpp`). | K | L13 | F:J6 O:J5 | R5-7: post-v1. S covers it in its §2.3 without a row. | -| OT7 | *(quirk)* Pine language state: series, barstate / session flags, position-view freezing (`PineLanguageState` pine_language_state.hpp:12-68; `publish_series` pine_scheduler_native.cpp:323-358, `chart_index` pine_scheduler_native.cpp:570-703) | n/a | A | — | S:R1 | (single-source) **Closed (E25, lane E20's finding 2; E26, E25's findings 1 and 3):** `session.islastbar` reads one script bar ahead on every driving path: the chart timeframe's next retained bar, an aggregated chart's next bucket (a scheduler lookahead, deleted when the rule moved into the kernel), the calendar for the live tail's bar and for every bar of a stream, warmup included — a stream continues past its replay, so only a batch run's final bar reads "last" from having no successor. Since R4 slice C an aggregated or realtime bar read every in-session bar as last. **What TradingView does:** it ends a session at the session DAY — the flag belongs to the last chart bar whose successor belongs to another session day, in session or not, and `session.isfirstbar` is its dual, the bar after a session's last one. The ordinal is `internal::session_trading_day_index` timeframe.hpp:145, the exchange-timezone day rolling at the symbol's day stamp (09:30 ET on NYSE RTH, midnight on 24x7, 17:00 ET on a 1700-1700 forex session), not local midnight; the unmerged form, since a fused holiday daily bar still holds two intraday sessions. The three `lab tv` tapes (the private fixture set `session_islastbar`) give that rule bar for bar with no disagreement, and the engine now holds 255/255 NYSE:F last bars, 255/255 first bars and 370/370 ETH last bars on the chart-timeframe AND the aggregated path (it held 1, 0 and 1 before). Early closes come with it: the three NYSE:F half days flag 12:45 ET without a holiday calendar, because the next bar is the next session day's. **Generic since R5 lane F5:** the kernel computes that reading for every host — `NativeDecisionContext::in_session` market_driver.hpp:161 and the facts after it (`opens_session_day`, `closes_session_day`, `closes_session_day_open_ended`), in C the session bytes of `pf_native_decision_v1` native_c_api.h:1485 — from the run's own calendar and input (`present_session_day` native_execution_consumer.cpp:8161), so a bare host reproduces the tapes flag for flag (the private test `test_native_session_day_facts`, kernel-only). The adapter computes no regular-day rule of its own: `scheduler_update_session_state` pine_strategy_host.cpp:2223 selects the kernel's facts into the flags, and only widens the plain `session.isfirstbar` / `islastbar` pair to the chart's day on an extended-hours chart (R5 lane K-SESSION-WINDOWS F2: TradingView's `isfirstbar` / `islastbar` span the pre- and post-market bars there while the `_regular` pair keeps the regular day, `tests/test_session_extended_day_tape.cpp`), which are `PineStrategyHost` members now (`session_ismarket_` pine_strategy_host.hpp:1186; `BacktestEngine` holds no Pine session state), and only its live-probe tail reads the open-ended close at its batch's final bar (the private test `test_session_day_facts_adapter`). Selecting the kernel also ended a calc_on_order_fills defect of the adapter's lookahead, which read two bars ahead on a bar a fill recalculation had already published (a fill on a day's second-to-last bar flagged that bar last and the day's real last bar first). **Ruled, not open:** a bar with nothing held after it — a stream's bar, a live-tail bar — steps the calendar one width on, so it cannot see an early close the session string does not declare (ADR-0001, "Session-day facts at a bar with nothing held after it"). **Closed (F1):** a `calc_on_order_fills` recalculation reads the flags of the bar it runs on, not the previous bar's (the kernel presents the current bar's facts on that recalculation), and the close callback after it no longer looks one retained bar too far ahead (§3.8, AG3). | -| OT8 | *(quirk)* Live-tail / probe-suppress harness flags (`realtime_tail_` pine_strategy_host.cpp:296) | n/a — **contained since N14** (the flags were `BacktestEngine` members until then; now `source::PineStrategyHost` state behind two kernel virtual seams, §2.ii q) | A | N14 | F:J7 S:C5 | parity-campaign tooling | +| OT7 | *(quirk)* Pine language state: series, barstate / session flags, position-view freezing (`PineLanguageState` pine_language_state.hpp:12-68; `publish_series` pine_scheduler_native.cpp:323-358, `chart_index` pine_scheduler_native.cpp:570-703) | n/a | A | — | S:R1 | (single-source) **Closed (E25, lane E20's finding 2; E26, E25's findings 1 and 3):** `session.islastbar` reads one script bar ahead on every driving path: the chart timeframe's next retained bar, an aggregated chart's next bucket (a scheduler lookahead, deleted when the rule moved into the kernel), the calendar for the live tail's bar and for every bar of a stream, warmup included — a stream continues past its replay, so only a batch run's final bar reads "last" from having no successor. Since R4 slice C an aggregated or realtime bar read every in-session bar as last. **What TradingView does:** it ends a session at the session DAY — the flag belongs to the last chart bar whose successor belongs to another session day, in session or not, and `session.isfirstbar` is its dual, the bar after a session's last one. The ordinal is `internal::session_trading_day_index` timeframe.hpp:145, the exchange-timezone day rolling at the symbol's day stamp (09:30 ET on NYSE RTH, midnight on 24x7, 17:00 ET on a 1700-1700 forex session), not local midnight; the unmerged form, since a fused holiday daily bar still holds two intraday sessions. The three `lab tv` tapes (the private fixture set `session_islastbar`) give that rule bar for bar with no disagreement, and the engine now holds 255/255 NYSE:F last bars, 255/255 first bars and 370/370 ETH last bars on the chart-timeframe AND the aggregated path (it held 1, 0 and 1 before). Early closes come with it: the three NYSE:F half days flag 12:45 ET without a holiday calendar, because the next bar is the next session day's. **Generic since R5 lane F5:** the kernel computes that reading for every host — `NativeDecisionContext::in_session` market_driver.hpp:161 and the facts after it (`opens_session_day`, `closes_session_day`, `closes_session_day_open_ended`), in C the session bytes of `pf_native_decision_v1` native_c_api.h:1485 — from the run's own calendar and input (`present_session_day` native_execution_consumer.cpp:8229), so a bare host reproduces the tapes flag for flag (the private test `test_native_session_day_facts`, kernel-only). The adapter computes no regular-day rule of its own: `scheduler_update_session_state` pine_strategy_host.cpp:2228 selects the kernel's facts into the flags, and only widens the plain `session.isfirstbar` / `islastbar` pair to the chart's day on an extended-hours chart (R5 lane K-SESSION-WINDOWS F2: TradingView's `isfirstbar` / `islastbar` span the pre- and post-market bars there while the `_regular` pair keeps the regular day, `tests/test_session_extended_day_tape.cpp`), which are `PineStrategyHost` members now (`session_ismarket_` pine_strategy_host.hpp:1186; `BacktestEngine` holds no Pine session state), and only its live-probe tail reads the open-ended close at its batch's final bar (the private test `test_session_day_facts_adapter`). Selecting the kernel also ended a calc_on_order_fills defect of the adapter's lookahead, which read two bars ahead on a bar a fill recalculation had already published (a fill on a day's second-to-last bar flagged that bar last and the day's real last bar first). **Ruled, not open:** a bar with nothing held after it — a stream's bar, a live-tail bar — steps the calendar one width on, so it cannot see an early close the session string does not declare (ADR-0001, "Session-day facts at a bar with nothing held after it"). **Closed (F1):** a `calc_on_order_fills` recalculation reads the flags of the bar it runs on, not the previous bar's (the kernel presents the current bar's facts on that recalculation), and the close callback after it no longer looks one retained bar too far ahead (§3.8, AG3). | +| OT8 | *(quirk)* Live-tail / probe-suppress harness flags (`realtime_tail_` pine_strategy_host.cpp:300) | n/a — **contained since N14** (the flags were `BacktestEngine` members until then; now `source::PineStrategyHost` state behind two kernel virtual seams, §2.ii q) | A | N14 | F:J7 S:C5 | parity-campaign tooling | --- @@ -202,15 +202,15 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | # | Item | Where / what it is used for | Class | Plan | Src | |---|---|---|---|---|---| -| a | `source::StrategyOverrides` opaque pointer | Forward declarations in the kernel headers (since removed: no kernel header names the adapter type now); parameter of `run_rich` (execution_consumer.hpp:46-55, native_execution_consumer.hpp:110-119) and of the rich `BacktestEngine::run` overload (engine.hpp:1796-1801, engine_consumer.cpp:81-93). The kernel only forwards it as `NativeBeginArgs::overrides_opaque` native_host.hpp:758; the source host casts it back `overrides_opaque` pine_strategy_host.cpp:303 | NEUTRAL in behaviour; a consumer-vtable signature change, so it rides the shared `engine_script_run` bump | `const void*` in the kernel signatures (F `host_begin_extras`, O `overrides_opaque`), typed convenience overload in `source/pine_strategy_host.hpp`; then drop the checker's single whitelist entry `test_native_example_batch` check_native_include_independence.py:42-46. S's alternative (`NativeRunOverrides`, or removing the rich overload) not adopted. Unverified: generated code calls the typed overload (codegen repo is not in this tree — F). **Status (L11, `3f4fff5b`): done** — `run_rich` and the rich `BacktestEngine::run` take `const void*` overrides, the rich begin carries them as `overrides_opaque`, and the independence checker whitelists nothing (§2.vi row 6) | F O S | +| a | `source::StrategyOverrides` opaque pointer | Forward declarations in the kernel headers (since removed: no kernel header names the adapter type now); parameter of `run_rich` (execution_consumer.hpp:46-55, native_execution_consumer.hpp:110-119) and of the rich `BacktestEngine::run` overload (engine.hpp:1796-1801, engine_consumer.cpp:81-93). The kernel only forwards it as `NativeBeginArgs::overrides_opaque` native_host.hpp:758; the source host casts it back `overrides_opaque` pine_strategy_host.cpp:307 | NEUTRAL in behaviour; a consumer-vtable signature change, so it rides the shared `engine_script_run` bump | `const void*` in the kernel signatures (F `host_begin_extras`, O `overrides_opaque`), typed convenience overload in `source/pine_strategy_host.hpp`; then drop the checker's single whitelist entry `test_native_example_batch` check_native_include_independence.py:42-46. S's alternative (`NativeRunOverrides`, or removing the rich overload) not adopted. Unverified: generated code calls the typed overload (codegen repo is not in this tree — F). **Status (L11, `3f4fff5b`): done** — `run_rich` and the rich `BacktestEngine::run` take `const void*` overrides, the rich begin carries them as `overrides_opaque`, and the independence checker whitelists nothing (§2.vi row 6) | F O S | | b | `pine_float_compare.hpp` | 1e-10 absolute equality band, used by kernel TA only: `float_band_le` ta_misc.cpp:30 (percentrank), `float_band_gt` ta_oscillators.cpp:462-463 (MFI), `plus_dm` ta_volatility_trend.cpp:337-340 (DMI); no execution TU includes it (O) | NEUTRAL (rename only) | Rename to a neutral header (F `float_band_compare.hpp`, O `ta_compare_band.hpp`), keep `pine_float_*` inline aliases in a source-layer header for generated code. Numerics must not move. S's behavioural `ComparePolicy` split is not neutral → not adopted (§4 U6). **Status (L11 + N14): done** — `ta_compare_band.hpp` is the kernel header; the alias shim is `include/pineforge/source/pine_float_compare.hpp` (N14 moved it off the kernel include root; it has no in-tree includer, generated code emits its own comparator) | F O S | -| c | `NativeSlotLabelPolicy::LegacyTolerant`, `NativeLegacyTolerance` (now `NativeSlotLabelPolicy::FeedTolerant`, `NativeFeedTolerance`) | `NativeSlotLabelPolicy` native_run_spec.hpp:324-353; read by `legacy_tolerant_slot_labels` market_driver.cpp:55-56 and `native_feed_tolerance_enabled` market_driver.cpp:43-49, and by `legacy_tolerant_slot_labels` native_execution_consumer.hpp:1027-1029 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:2318; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:2335, `:9996`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2806. A feed-tolerance policy (raw strictly-increasing labels, zero / NaN-volume legacy bars), not TV semantics (O) | NEUTRAL | Rename with deprecated aliases (F `RawIncreasing` + `NativeInputTolerance::{StructuralBars, WarmupNonNegativeOHLC}`; O `NativeLabelPolicy::ProviderLabels`). Enumerator values stay 0/1 and 1/2: they are hashed `slot_label_policy` native_execution_consumer.cpp:272. **Status:** renamed as `FeedTolerant` / `NativeFeedTolerance` (the enumerator values unchanged), and the C transport carries them now, in the extension's feed-policy block (`PF_NATIVE_SPEC_EXT_FEED_POLICY` native_c_api.h:745) | F O S | +| c | `NativeSlotLabelPolicy::LegacyTolerant`, `NativeLegacyTolerance` (now `NativeSlotLabelPolicy::FeedTolerant`, `NativeFeedTolerance`) | `NativeSlotLabelPolicy` native_run_spec.hpp:324-353; read by `legacy_tolerant_slot_labels` market_driver.cpp:55-56 and `native_feed_tolerance_enabled` market_driver.cpp:43-49, and by `legacy_tolerant_slot_labels` native_execution_consumer.hpp:1027-1029 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:2368; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:2385, `:10068`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2806. A feed-tolerance policy (raw strictly-increasing labels, zero / NaN-volume legacy bars), not TV semantics (O) | NEUTRAL | Rename with deprecated aliases (F `RawIncreasing` + `NativeInputTolerance::{StructuralBars, WarmupNonNegativeOHLC}`; O `NativeLabelPolicy::ProviderLabels`). Enumerator values stay 0/1 and 1/2: they are hashed `slot_label_policy` native_execution_consumer.cpp:272. **Status:** renamed as `FeedTolerant` / `NativeFeedTolerance` (the enumerator values unchanged), and the C transport carries them now, in the extension's feed-policy block (`PF_NATIVE_SPEC_EXT_FEED_POLICY` native_c_api.h:745) | F O S | | d | Orphaned comments | The KI-62 block of `engine_orders.cpp` (*fresh*: comment-only, the bodies are deleted; S read it as live code) and a comment of `engine.hpp`; wider deleted-declaration blocks in `engine.hpp`; the money-rule essay in `engine.hpp` (the code is `source_money_round` pine_adapter.cpp:558) | NEUTRAL | Delete, or move to `docs/` next to the adapter code that implements them. **Status: done** — R5 lanes E6 and E10 deleted the detached comment residue (a census of 0 over the kernel's compile closure) | F O | -| e | Dead helpers in the public kernel header | **Done.** All four are gone from `engine.hpp`: `round_to_mintick_directional` and `apply_slippage` were the pre-R5 spelling of the directional snap and the slippage step and the kernel now fills on `native_matching::grid_round_directional` / `native_matching::apply_slippage` src/native_matching.hpp:259 (the engine.hpp comment that named them went with R5 lane E10's residue sweep); `apply_limit_fill` and `reserve_percent_commission` are deleted outright, and only the adapter still spells the first two names, in its own comments. `round_to_mintick` engine.hpp:753 and `bar_fill_price` engine.hpp:771 stay, live through the call `bar_fill_price` native_execution_consumer.cpp:7253 | NEUTRAL | Reuse as the reference arithmetic of the price-grid and sizing lanes, delete the rest | O (single-source) | +| e | Dead helpers in the public kernel header | **Done.** All four are gone from `engine.hpp`: `round_to_mintick_directional` and `apply_slippage` were the pre-R5 spelling of the directional snap and the slippage step and the kernel now fills on `native_matching::grid_round_directional` / `native_matching::apply_slippage` src/native_matching.hpp:259 (the engine.hpp comment that named them went with R5 lane E10's residue sweep); `apply_limit_fill` and `reserve_percent_commission` are deleted outright, and only the adapter still spells the first two names, in its own comments. `round_to_mintick` engine.hpp:753 and `bar_fill_price` engine.hpp:771 stay, live through the call `bar_fill_price` native_execution_consumer.cpp:7321 | NEUTRAL | Reuse as the reference arithmetic of the price-grid and sizing lanes, delete the rest | O (single-source) | | f | Dead legacy stream path | **Done.** `stream_feed_input_bar` and `dispatch_source_stream_script_bar` are both gone — neither name is in `src/` or `include/` — and the virtual left with the epoch that carried it. `stream_state_hash` engine.hpp:1790 stays, and the declaration/definition pairing is still enforced by `scripts/test_native_source_guard.py` | NEUTRAL (removing the virtual changes the vtable → shared bump) | Delete both | O; F asked (its Q6) | | g | Legacy path resolver | **Done, and then deleted.** `src/engine_path_resolve.cpp` is 118 lines with five functions: `bar_path_uses_high_first`, both `first_touch_position` overloads, `fill_bar_path_points_ordered`, and `BacktestEngine::declare_opened_lot_entry_bar_mask`. Ten test files still mention `resolve_exit_path_fill` in historical comments; the resolver body and test-only oracle are gone. | NEUTRAL (pure move; verify with the link gate) | Keep the generic path helpers in the kernel TU; the old source resolver is not a live counterpart | F (single-source) | | | h | `pine_*`-named generic API | `timeframe_time` session_time.hpp:35-211 (live, e.g. `session_in_market`, which generated code reaches through `pine_session_ismarket` session_time.hpp:263), `str_utils.hpp`, `deterministic_random` math.hpp:9, `tz_util` timezone.hpp:8-30; the guard bans `pine_*` only inside `IDENTIFIER_ROOTS` (test_native_source_guard.py:34, test_native_source_guard.py:34-42) | NEUTRAL, M | Neutral aliases; `pine_*` kept as deprecated inlines for codegen. Large blast radius in generated code. **Status (L11 + N14): done** — session_time / str_utils / math / timezone in L11; N14 applied the same treatment to the two public matrix types: `PineMatrix` → `NumericMatrix`, `PineGenericMatrix` → `GenericMatrix`, old spellings kept as exact aliases (no version script pins either name) | O (single-source) | -| i | `pf_pending_order_v1_t` Pine-named fields | `tv_carry_qty` pending_order_mirror.hpp:73, `pine_birth_reach` pending_order_mirror.hpp:135, `pine_exit_activation_owner_cycle_at_birth` pending_order_mirror.hpp:147-156, `pine_frozen_market_instruction_kind` pending_order_mirror.hpp:164-169; enumerated by `PF_PO_FIELD` pending_order_mirror.cpp:144-178; written only by `pine_birth_reach` pine_adapter.cpp:27061 | no change | Freeze (ABI). The neutral view arrives beside it (L7 / L13) | O (single-source) | +| i | `pf_pending_order_v1_t` Pine-named fields | `tv_carry_qty` pending_order_mirror.hpp:73, `pine_birth_reach` pending_order_mirror.hpp:135, `pine_exit_activation_owner_cycle_at_birth` pending_order_mirror.hpp:147-156, `pine_frozen_market_instruction_kind` pending_order_mirror.hpp:164-169; enumerated by `PF_PO_FIELD` pending_order_mirror.cpp:144-178; written only by `pine_birth_reach` pine_adapter.cpp:27063 | no change | Freeze (ABI). The neutral view arrives beside it (L7 / L13) | O (single-source) | | j | Pine-shaped lot flags | **Mostly done.** `market_pyramid_add` moved to the adapter side table the row asked for — `market_pyramid_adds_` src/source/pine_state_hash.cpp:692, whose own comment records that it was `PyramidEntry::market_pyramid_add` — and `ordinary_market_open`, `pooc_terminal_market_entry`, `ordinary_stop_open`, `bracket_slot_shadowed` and `entry_path_position` are all deleted. What is left on the lot is the intrabar-fill excursion mask, `skip_entry_bar_high` / `skip_entry_bar_low` engine.hpp:153, hashed (`skip_entry_bar_high` engine_state_hash.cpp:74) and, since R5 lane E6, written by no host at all: a host that owns lot excursions declares where its fill sat (`declare_opened_lot_entry_bar_mask` engine_path_resolve.cpp:95; the Pine host did until R5 lane H-THIN left its excursions to the kernel's sampler) and the kernel derives the pair | HASH-VISIBLE | Move the live flag to an adapter side table keyed by `entry_incarnation` (the adapter already keeps `PlacementTable` pine_adapter.hpp:355-675) and fold it through `hash_source_extension`; delete the dead ones; update the hash-coverage waivers. Whether a kernel TU beyond the hash reads them is unverified (O) | F O S | | k | `margin_call_enabled_` | **Done.** The generic kernel model is enabled by `NativeRunSpec::margin` native_run_spec.hpp:672. TradingView-specific toggling remains `source::PineStrategyHost::set_margin_call_enabled` pine_strategy_host.hpp:511; the frozen C symbol remains source-owned. | HASH-VISIBLE (R5-8) | Keep the source setter in the adapter; the kernel toggle is the presence of `spec.margin` | F O | | l | Kernel decodes adapter strings | **Done.** `closed_trade_close_cause` engine_trade_accessors.cpp:158 branches on no string at all: it reads `Trade::open_at_end`, then the typed `execution::CloseCause` the closer recorded on the row, then `exit_from_bracket`. A kernel-originated liquidation or risk flatten states its own cause through the settling fill, so a bare host reads `3` / `4` where the retired string derivation answered `1`. The public C contract it implements is `strategy_closed_trade_close_cause` pineforge.h:1309 | HASH-VISIBLE (adds durable state) | Kernel `enum class CloseCause` recorded on the `Trade` at settlement (the kernel sets `MarginCall` when L4 liquidates; a source virtual covers adapter causes). C numbers unchanged. After L4 | O (single-source) | @@ -267,8 +267,8 @@ PF_API int pf_native_events_v1(pf_strategy_t, uint64_t after_ordinal, pf_native_ | Point | Decision | Src | |---|---|---| | Implementation | One new kernel TU (F `src/native_c_host.cpp`, O `c_native_host.cpp`, S `native_c_api.cpp`): `class CCallbackHost final : public NativeStrategyHost` forwarding each virtual to the table. Streaming reuses the existing `strategy_stream_*` symbols unchanged (they take any `BacktestEngine*`) | F O S | -| Errors | A non-zero callback return maps to `NativeFailureCode::CallbackException`; C callbacks must not unwind. The C++ path already latches exceptions as `CallbackException` native_execution_consumer.cpp:8116 | F O | -| Legality | The existing rule (`commands_allowed` native_execution_consumer.cpp:1836): inside a callback, or between realtime inputs | F | +| Errors | A non-zero callback return maps to `NativeFailureCode::CallbackException`; C callbacks must not unwind. The C++ path already latches exceptions as `CallbackException` native_execution_consumer.cpp:8184 | F O | +| Legality | The existing rule (`commands_allowed` native_execution_consumer.cpp:1843): inside a callback, or between realtime inputs | F | | Hardening | Tagged, size-prefixed PODs; `struct_size` + version; unknown-tag refusal; never cast the C request to a C++ variant; a C fixture compiles against the frozen previous header. S puts the API in a separate header `include/pineforge/native_c_api.h` | S | | SOP | Every symbol goes into `src/c_abi.cpp`, `pineforge.h`, `EXPECTED_RUNTIME` + both counts (check_c_abi_runtime.py:19-79), the README table and the Python harnesses, or the `source-guard-c-abi` stage fails, in preflight and every `ci_verify` profile (`CLAUDE.md`) | F O S | | Not in v1 of the C API | `resolve_execution_terms` / `validate_execution_precommit` are the adapter's seam; a C host that needs sizing uses `Sized` (F). O offers an optional `resolve_terms` callback: deferred. *History: R5 lane F4 routed both (`on_execution_terms`, `on_precommit`); the terms hook's units answer outside a host-sized close is still C++-only (the 1.0 C boundary table)* | F O | @@ -281,16 +281,16 @@ Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the | | F (its §2.iv A) | O (its G10) | S (its P7) | Chosen | |---|---|---|---|---| | API shape | spec `subscriptions` + push callback `on_native_timeframe_bar` | `register_native_series` + pull `native_series_bar(id)` / `native_series_slot_is_new(id)` | spec `security_requests` + `configure_native_security_feed` + `native_security(id)` view | F's subscription in the spec + push callback, plus O's pull accessor | -| Engine reuse | none: generalize the consumer's one script bucket (`ScriptBucket` native_execution_consumer.hpp:755-770, `contribute_input` native_execution_consumer.cpp:9109, `seal_script` native_execution_consumer.cpp:9019) to N; rejects exposing the `request.security` path as Pine-shaped end to end | promote the existing evaluator + feed machinery; edits `engine_security.cpp`, `engine_aux_security.cpp`, `engine_lower_tf.cpp` | same TUs + `native_calendar.cpp` | The existing feed machinery, **called, not edited**; `engine_security.cpp` untouched | -| Pump | consumer seal logic | accepted-input path (`invoke_input_callback` native_execution_consumer.cpp:7987), only for natively registered series | copy at Ready, evaluators created before the first input | O's registration-gated pump + S's copy-at-Ready | +| Engine reuse | none: generalize the consumer's one script bucket (`ScriptBucket` native_execution_consumer.hpp:755-770, `contribute_input` native_execution_consumer.cpp:9177, `seal_script` native_execution_consumer.cpp:9087) to N; rejects exposing the `request.security` path as Pine-shaped end to end | promote the existing evaluator + feed machinery; edits `engine_security.cpp`, `engine_aux_security.cpp`, `engine_lower_tf.cpp` | same TUs + `native_calendar.cpp` | The existing feed machinery, **called, not edited**; `engine_security.cpp` untouched | +| Pump | consumer seal logic | accepted-input path (`invoke_input_callback` native_execution_consumer.cpp:8055), only for natively registered series | copy at Ready, evaluators created before the first input | O's registration-gated pump + S's copy-at-Ready | | Authoritative bars | `authoritative_bars` on the subscription | `Source::ExchangeFeed` through `set_native_security_feed` | `configure_native_security_feed(id, bars, tf)` | F's field, installed through the existing store | | Lookahead | none, by construction | `lookahead = false` default, available | `lookahead_on = false`, `gaps_on = true` | open then → §4 U1; landed as `lookahead` and `gaps`, both default false (HT5) | 1. **Spec:** `std::vector subscriptions` (Appendix A.5), hashed only when non-empty (digest like `native_intrabar_path_digest`). 2. **Wiring**, all inside the native consumer at Ready → begin and only when `subscriptions` is non-empty (zero work for the adapter): register one evaluator state per subscription through the existing `register_security_eval` (engine.hpp:1295-1297, *fresh*); install `authoritative_bars` into the existing store (`set_native_security_feed` engine.hpp:1731, engine_aux_security.cpp:79-133); call `prepare_native_security_feeds` (engine.hpp:1639, defined `BacktestEngine::prepare_native_security_feeds` engine_aux_security.cpp:136, *fresh*) — the "made native-callable" step: reachable from the native consumer, still refused from host code in-run; pump `feed_security_eval_state` (engine.hpp:1312-1314, *fresh*) from the accepted-input path; deliver each completed bucket through `on_native_timeframe_bar`. -3. **Untouched:** `engine_security.cpp`, the Pine scheduler sequence pine_strategy_host.cpp:1593-1677 and its call order. The per-run evaluator init is today a *source-layer* member (`PineStrategyHost::init_security_eval_states_for_run` pine_scheduler.cpp:9-39, declared `init_security_eval_states_for_run` pine_strategy_host.hpp:956, *fresh*), so L6 adds a kernel-side twin instead of moving it. +3. **Untouched:** `engine_security.cpp`, the Pine scheduler sequence pine_strategy_host.cpp:1598-1682 and its call order. The per-run evaluator init is today a *source-layer* member (`PineStrategyHost::init_security_eval_states_for_run` pine_scheduler.cpp:9-39, declared `init_security_eval_states_for_run` pine_strategy_host.hpp:956, *fresh*), so L6 adds a kernel-side twin instead of moving it. 4. **Delivery rule (F):** a subscription bar is delivered on the input bar that completes it — its last contributing input bar, or, for a `LazyComplete` bucket, the next period's first input bar — immediately before that input's script calculation, never earlier — and never before an earlier script interval's lazily sealed calculation has run: the pump is ordered against the script interval, `seal(k) → deliver(i+1) → calc(i+1)` (L6d). -5. **Interim, documented in L0:** `on_native_input` + `TimeframeAggregator` (HT6). Doc notes (O): `set_native_security_feed` is public but inert without evaluators and throws in-run (`guard_native_mutation` engine_aux_security.cpp:83, which latches `UnsupportedSource` native_execution_consumer.cpp:1889); `set_aux_security_feed` returns false in the kernel engine_consumer.cpp:158-160. +5. **Interim, documented in L0:** `on_native_input` + `TimeframeAggregator` (HT6). Doc notes (O): `set_native_security_feed` is public but inert without evaluators and throws in-run (`guard_native_mutation` engine_aux_security.cpp:83, which latches `UnsupportedSource` native_execution_consumer.cpp:1896); `set_aux_security_feed` returns false in the kernel engine_consumer.cpp:158-160. 6. **Entanglement to put in the lane brief** (*fresh* read of `set_native_security_feed` `sha256:eca3dbea0baa461982ba89f13d0a5c43277854069fb6eb2dd2f5c5471313204d` engine_aux_security.cpp:51-83): the feed machinery is TV-calibrated. "W" / "M" buckets are built from the installed native daily bars, and the native stamps define the period partition (a holiday session folds into the next trade date's bar). A bare host that supplies `authoritative_bars` inherits those rules. Document them as the contract or gate them behind a partition policy — decide in the lane, not silently (§4 E8). 7. **HT3 / HT4:** lower-TF arrays come from the L5 sub-bar hook in v1. An auxiliary feed is a subscription whose bars come from the host; the Pine chart-slice mapping is not exposed. Whether v1 accepts a feed *finer* than the input is left to the lane brief (S wants it, F calls it a TV harness construct). **Status (audit lane N7): HT4 done for a bare host; the adapter's auxiliary path retained, measured.** `NativeRunSpec::auxiliary_feed` (`NativeAuxiliaryFeed{tf, bars}`, strictly finer than `input_tf`, opt-in and hashed only when present) plus `NativeTimeframeSubscription::source = NativeSeriesSource::AuxiliaryFeed` build a series from the host's finer bars through the existing evaluator step (`register_security_eval` against the feed's timeframe, `feed_security_eval_state` per feed bar — called, not edited); routing is by time alone (a feed bar rides on the first accepted input whose period it opened before), with `declare_auxiliary_feed` at begin, `append_auxiliary_bars` on a realtime stream and a C spelling (`PF_NATIVE_SPEC_EXT_AUXILIARY_FEED` tail, `strategy_native_append_auxiliary_bars_v1`). No epoch moved: `native_run_spec_v3` and `engine_script_run_v18` were open then (no release tag contained them, no frozen provider fixture of either existed), exactly as L6c's `gaps` and begin-time hook were added. `BacktestEngine::set_aux_security_feed` stays the source host's door and still answers a silent false on a bare host (the pre-begin setter pins, `tests/test_native_example_batch.cpp` among them, hold it to that). **Retained, why:** the adapter does not re-lower its auxiliary sites, on three measurements — 0 of 312 corpus probes install an auxiliary feed, so corpus byte-identity cannot witness the move; TradingView's chart slice leaves pre-range feed coverage inert where the kernel folds it by time (two hourly buckets against three over one feed, `tests/test_native_auxiliary_feed_twin.cpp` row B); and the adapter evaluates a chart bar's slice at its calculation, after that bar's matching pass, where the kernel delivers before it (position 1 against 0 at the same bucket, row C), which no predicate over an opaque generated `evaluate_security` can prove neutral. The congruent shape is the same series on both (row A) and can move once a population exercises it. Full contract: `docs/pages/native-engine.md`, "The auxiliary finer feed". @@ -343,7 +343,7 @@ the campaign's own anchor audit found that the first thing to rot: | the request-epoch sentence | `native_order_v4` | `native_order_v7` native_order.hpp:25 | | the two host-epoch sentences | host epoch v16 | `engine_script_run_v19` native_host.hpp:20 | | "What still requires Pine compatibility to build" | `BacktestEngine` still holds `CapAttachment` / `OrderPriority` / `IntradayCap` | `engine.hpp` has zero references to them; the cap type is `CapAttachment` intraday_cap.hpp:19 and the constructor is `PineStrategyHost`'s pine_strategy_host.hpp:289 | -| the rich-`run` paragraph | the rich `run` overload fails a native host | `run_rich` native_execution_consumer.cpp:10429 shows no such refusal (still unpinned by a test; the page says so) | +| the rich-`run` paragraph | the rich `run` overload fails a native host | `run_rich` native_execution_consumer.cpp:10501 shows no such refusal (still unpinned by a test; the page says so) | | added | — | the self-aggregation recipe beside the subscriptions it is an alternative to; the open-bar lookahead warning with `current_partial_bar` native_host.hpp:1073; and `set_native_security_feed` documented as the pre-run door for a series' authoritative bars, refused in-run | --- @@ -352,7 +352,7 @@ the campaign's own anchor audit found that the first thing to rot: ### 3.1 Parity discipline for every lane (R5-10) — stated once, referenced per lane -- **(a) NEUTRALITY.** Every new behaviour is opt-in by a spec field or a new request kind. The adapter's `project()` (pine_adapter.cpp:2524-2791) never sets the field and never emits the kind, so adapter runs are byte-identical by construction. The four feed-shape fields are folded into `hash_spec` unconditionally today, including defaults (native_execution_consumer.cpp:238-260), so even a defaulted new field would move every continuation hash; the precedent for a conditional digest is `precommit_digest_` (native_execution_consumer.hpp:1919-1923). Each lane adds a test that a default spec hashes to the pre-change constant. Every new durable field is hashed (`check_broker_state_hash_coverage.py`, fail-closed, waivers in `broker_state_hash_waivers.txt`). Proof per PR: `scripts/run_corpus.sh` + `scripts/verify_corpus.py` locally, then ONE campaign sweep on the final tree, run by the maintainers (fixed population, 4190 probes when this plan was written, zero tolerance on the hard band — native-refactor-progress.md:42-48; a fresh exact-HEAD `PASS` verdict is required, `sha256:59adeba8aabaefa740633d52e9216b49065b28b397b87f22c57ad5e19f2948c2` AGENTS.md:144-145). A lane that cannot be shown neutral by construction does not merge. +- **(a) NEUTRALITY.** Every new behaviour is opt-in by a spec field or a new request kind. The adapter's `project()` (pine_adapter.cpp:2524-2791) never sets the field and never emits the kind, so adapter runs are byte-identical by construction. The four feed-shape fields are folded into `hash_spec` unconditionally today, including defaults (native_execution_consumer.cpp:238-260), so even a defaulted new field would move every continuation hash; the precedent for a conditional digest is `precommit_digest_` (native_execution_consumer.hpp:1942-1946). Each lane adds a test that a default spec hashes to the pre-change constant. Every new durable field is hashed (`check_broker_state_hash_coverage.py`, fail-closed, waivers in `broker_state_hash_waivers.txt`). Proof per PR: `scripts/run_corpus.sh` + `scripts/verify_corpus.py` locally, then ONE campaign sweep on the final tree, run by the maintainers (fixed population, 4190 probes when this plan was written, zero tolerance on the hard band — native-refactor-progress.md:42-48; a fresh exact-HEAD `PASS` verdict is required, `sha256:59adeba8aabaefa740633d52e9216b49065b28b397b87f22c57ad5e19f2948c2` AGENTS.md:144-145). A lane that cannot be shown neutral by construction does not merge. - **(b) TWIN.** One chosen probe per lane runs through the adapter AND a hand-written `NativeStrategyHost` on the same bars and spec. Trade lists are diffed **by identity** (entry / exit time, price, qty, pnl, commission), never by trade number; then event, ownership and hash detail (S's D → E order). A difference is allowed only where a named TV-quirk row of §1 is active, and each one is itemized. Because 430 test files drove the adapter and 14 the native host when this plan was written (O; on this tree 543 `tests/*.cpp` name `source::PineStrategyHost` and 110 name `NativeStrategyHost`), every lane also adds native-only tests: a lane is not accepted on the corpus alone, nor on a unit test alone (S). - **(c) EPOCH BUDGET.** Three inline-namespace epochs move — the request epoch, the host epoch and (*fresh*, not in the inputs) the run-spec epoch, which every new spec field moves. They stood at `native_order_v5`, `engine_script_run_v17` and `native_run_spec_v2` when this plan was written; on this tree they are `native_order_v7` (native_order.hpp:25), `engine_script_run_v19` (native_host.hpp:20) and `native_run_spec_v3` (native_run_spec.hpp:15): the budget below was spent exactly once, and the v19 value epoch (R5 lane V19-A) took the host epoch one step past it. One bump each per release batch: `native_order_v6` = L3 + L7 + L4's origin / event; `engine_script_run_v18` = L4's host hooks + L5 + L6 virtuals + L2's `hash_host_extension` + §2.ii a, f; `native_run_spec_v3` = the spec blocks of L2, L4, L5, L6, L8 (L9 if it ships in the batch). A lane that lands before its batch closes stages behind the open epoch, so codegen-built strategies rebuild once per batch. Epoch lanes are not neutral-refactor lanes: the brief lists the header extension explicitly; the frozen C++ ABI fixtures (`tests/fixtures/native_cpp_abi/host-*`) and the ABI checkers (`check_native_cpp_abi.py`, `check_settlement_cpp_abi.py`) move with them; `variant_size` pins change (`OrderIntent` native_order.hpp:229, `CommandEvent` native_order.hpp:1308). Every new test unit compiles first against the frozen previous-epoch header closure (fail-before, `CLAUDE.md`). - **Worker verification** (`CLAUDE.md`): `ci_preflight.py`, then `ci_verify.py release`; plus `check_c_abi_runtime.py` and `check_native_include_independence.py`. No campaign sweep from a worker. @@ -413,8 +413,8 @@ closure markers and `scripts/check_design_inventory.py`). | L2 | `NativeReportPolicy { HostRecorded, KernelRecorded }` (R1 appended a third value, `KernelRecordedAtHostMarks` native_run_spec.hpp:64), `report_open_position_at_end`, `hash_host_extension`, `native_metrics()` → A.1 | at L2 the adapter stayed `HostRecorded`; since R1 it declares `KernelRecordedAtHostMarks` pine_adapter.cpp:2855, so the kernel records the same series at the adapter's own report marks — still one recorder, no double recording, `fill_report` bytes unchanged on the corpus | native example: curve length == script bars, drawdown equals a hand walk (F); 50-bar host asserts a non-empty curve, a finite max drawdown and a range-end row (O); same generic strategy through both hosts → equal rows and curve (S) | | L3 | `Sized{side, basis, time, grid_policy, reserve_percent_fee}` as the 6th `OrderIntent`; `ScopeFraction{fraction, claim}` in `ReductionSize` → A.2 | at L3 the adapter emitted neither. Since R2 it lowers a default cash / percent opening frozen at the signal onto `Sized{CashValue, AtAcceptance, SignalOnTick, ExplicitUnits}`, since N11 its placement-time quantity is the kernel's `native_sized_units`, and since R5 lane F7 a typed quantity (`qty_type` cash / percent) converts through the same query and a staged FX series keeps `Sized` wherever the acceptance coordinate converts at the source's rate (`tests/test_adapter_sizing_relower.cpp` cases 1-4, 8, 11, 12). It still never emits `ScopeFraction` (case 5) | percent-of-equity probe, zero fee, no grid: exact; with grid / fee: differences explained by SZ10 only (F). Differential: `Sized` vs a host-resolved `HostSized` on the same facts → identical `ExecutionAppliedEvent` (O). Gap, reversal, fee-bearing first lot; two 50 % siblings, partial close, pending parent, replacement (S) | | L4 | `NativeMarginModel`, `native_liquidation_price()`, `resolve_margin_call_units`, `on_native_margin_call`, `RequestDefinition::origin`, `MarginCallEvent` → A.3 | at L4 the adapter left `margin` unset. Since R5 `project()` declares a maintenance-only model (per-side `initial_*` 0, `maintenance_*` = `margin_long` / `margin_short`, `PathAdverseExtremeMark`, TradingView's equity basis) and answers its three hooks with TradingView's scheduling, money and slice (MG6, MG7), while every opening is still `AdmitWithHostMargin` (MG4) | first a plain maintenance breach in both hosts (S); then a margin-call probe reproduced with `ShortfallMultiple 4`: same bar, same side; quantity deltas itemized against MG15 (F, O) | -| L5 | `NativeCalculationTrigger`, `on_native_recalculate(…, reason, cause)`, `on_native_sub_bar`, `current_partial_bar()`, `NativeOpenBarView` → A.4 | the adapter keeps `BarClose` + its own cascade; `born_on_remaining_path` (native_execution_consumer.cpp:6439) and `drain_applied_notifications` (native_execution_consumer.cpp:7122) are frozen; accessors are inert | COOF probe re-expressed natively: same fills where the TV waypoint rule is inactive (F); a host that flips on its own fill produces the adapter's cascade in shape (O); callback reason, cursor, newborn eligibility and event ordinals compared (S) | -| L6 | `NativeTimeframeSubscription`, `on_native_timeframe_bar`, `native_series_bar` → A.5, §2.iv | `subscriptions` empty for the adapter; registration-gated pump; `engine_security.cpp` and pine_strategy_host.cpp:1086-1170 untouched; corpus diff before / after (O: highest parity risk of any lane) | HTF-filter probe on a 24x7 symbol: exact (F); `"D"` over a 15m feed equals a `TimeframeAggregator` baseline bar for bar (O); installed-feed and no-feed cases (S) | +| L5 | `NativeCalculationTrigger`, `on_native_recalculate(…, reason, cause)`, `on_native_sub_bar`, `current_partial_bar()`, `NativeOpenBarView` → A.4 | the adapter keeps `BarClose` + its own cascade; `born_on_remaining_path` (native_execution_consumer.cpp:6507) and `drain_applied_notifications` (native_execution_consumer.cpp:7190) are frozen; accessors are inert | COOF probe re-expressed natively: same fills where the TV waypoint rule is inactive (F); a host that flips on its own fill produces the adapter's cascade in shape (O); callback reason, cursor, newborn eligibility and event ordinals compared (S) | +| L6 | `NativeTimeframeSubscription`, `on_native_timeframe_bar`, `native_series_bar` → A.5, §2.iv | `subscriptions` empty for the adapter; registration-gated pump; `engine_security.cpp` and pine_strategy_host.cpp:1091-1175 untouched; corpus diff before / after (O: highest parity risk of any lane) | HTF-filter probe on a 24x7 symbol: exact (F); `"D"` over a 15m feed equals a `TimeframeAggregator` baseline bar for bar (O); installed-feed and no-feed cases (S) | | L7 | `native_working_requests()`, `cancel_all()`, `cancel_where(label)`, `TriggerAnchor`, `TrailTicks`, zero offset, `ReplaceOptions`; toolkit `submit_bracket`, `OrderBook` → A.6 | new kinds unused by the adapter; anchor defaults to `Absolute`; the adapter's `tick * 0.5` sentinel keeps working | bracket + trailing probe through the builder: exact where TR5 / FP7 are inactive (on-grid levels) (F); the zero-offset trail test asserts the run no longer fails (O); parent-limit + stop sibling, parent rejection, replacement, parent-cycle revival (S) | | L8 | `NativePriceGrid { None, QuantizeFills, QuantizeFillsAndTriggers }`, `NativeGridRounding` → A.7 | `None` for the adapter, permanently — **native-only by ruling, audit lane P6: §3.6.2** | a host with `QuantizeFills` books on-grid prices; a second test pins that `QuantizeFillsAndTriggers` still differs from TV's half-tick threshold (FP5) (O) | | L9 | `NativeRiskLimits`, `NativeRiskEvent`, `MatchRejectReason::RiskLimit` → A.8 | `risk` unset for the adapter; its ledger (`SourceDayLedger` pine_adapter.hpp:1423) untouched — **retained after measurement, audit lane N12; native-only by ruling, audit lane P6: §3.6.1** | risk probes: same halt bar (F); each limit + the day-boundary basis (O); breach, forced close, cancellation, next-day reset (S) | @@ -446,7 +446,7 @@ Flagged: | Tier | Lanes | Why | |---|---|---| -| Day-one blockers (O) | L1, L2, L3, L10 | Without L2 `fill_report` silently returns an empty curve and degenerate equity metrics and drops an open final position. Without L3 every non-unit order needs a hand-written `resolve_execution_terms`, and a mistake is a run-terminating `TermsUnresolved` (native_execution_consumer.cpp:5769). Without L1 "kernel" is a claim, not an artifact. L10 is the minimum buildable proof. | +| Day-one blockers (O) | L1, L2, L3, L10 | Without L2 `fill_report` silently returns an empty curve and degenerate equity metrics and drops an open final position. Without L3 every non-unit order needs a hand-written `resolve_execution_terms`, and a mistake is a run-terminating `TermsUnresolved` (native_execution_consumer.cpp:5837). Without L1 "kernel" is a claim, not an artifact. L10 is the minimum buildable proof. | | Required by the promise (R5-11) | L0, L4, L5, L6, L7, L8 | A kernel that cannot liquidate is not a broker model (O); bracket / trailing corner cases are hit immediately (O); calc-timing and HTF are Pine features the user wants natively (R5-5, R5-6); the price grid is generic and missing (R5-3). | | Wanted with v1, not blocking | L11 a, d | removes the last `source::` name from kernel signatures and the orphaned TV commentary (F) | | In scope, 1.x | L9 | R5-9; a toolkit stand-in is acceptable meanwhile (F) | @@ -501,8 +501,8 @@ inside the per-bar body (corpus 442d497, `set_pine_risk_direction(1)` / `set_pine_risk_max_position_size(2)` / `set_pine_risk_max_intraday_filled_orders(3)`), so the adapter first sees a limit on script bar 0, from `PineStrategyHost::set_pine_risk_*` -(pine_strategy_host.cpp:881) — after `prepare_native_begin` has already -projected the spec (pine_strategy_host.cpp:239) and `configure_native` has +(pine_strategy_host.cpp:886) — after `prepare_native_begin` has already +projected the spec (pine_strategy_host.cpp:243) and `configure_native` has folded it into the continuation digest. Measured: an env-gated probe printing the adapter's risk configuration inside `project()` on the corpus reads `max_drawdown=0 cap.active=0` for every risk probe. `NativeRunSpec::risk` is a @@ -518,17 +518,17 @@ adapter's own rule acts. **Per rule (STEP 1), assuming the values were known at begin time.** Adapter line numbers are re-anchored to this tree (the measurement ran on 683a82f); kernel semantics are -`NativeRiskLimits` native_run_spec.hpp:310 and `risk_evaluate` native_execution_consumer.cpp:4381 +`NativeRiskLimits` native_run_spec.hpp:310 and `risk_evaluate` native_execution_consumer.cpp:4449 (evaluated at the script bar's open, its close calculation and after each applied drain; fills counted at the corresponding adapter checkpoints; the block refuses `would_open`). | Pine rule | adapter mechanism | kernel counterpart | class | measured divergence (test scenario) | ruling | |---|---|---|---|---|---| | `strategy.risk.allow_entry_in` | `risk_.direction`; `project()` already declares `spec.allowed_open_directions` (pine_adapter.cpp:2859); the forbidden opposite entry is reshaped to `CloseOpposite` in the adapter's own terms branch | per-opening cap `allowed_open_directions` | (i)+(ii), **already re-lowered** | none to measure: the cap is the kernel's, the close-only reshaping is the retained TV policy | done before N12; nothing to move | -| `strategy.risk.max_position_size` | precommit refuses a flat/same-side ENTRY at its fill when the LIVE book already holds ≥ the limit (`max_position_size` pine_adapter.cpp:16491; the retired legacy rule is `git show ab9714be:src/source/pine_risk.cpp`, lines 115 and 2531) | `max_abs_units`: refuses a fill whose RESULTING book would exceed it | (iv) | `PS`: two-unit entries against 3 — adapter admits the second (live 2 < 3, book 4) and refuses the third; kernel refuses the second (`MaxAbsUnits` at bars 1, 2), book 2 | retained: a pre-fill-book gate is a TV emulation fact, not a generic cap | -| `strategy.risk.max_drawdown` | `update_risk_state(bar.close)` once per script bar at the close mark (`update_risk_state(bar.close)` pine_adapter.cpp:17481, `update_risk_state` pine_adapter.cpp:17481): peak / running max drawdown at close marks, `>=` exact, percent of the current peak; the latch `risk_.halted` gates flat/same-side entries at precommit (`risk_.halted` pine_adapter.cpp:16483), never an opposite entry | `max_drawdown`: current drawdown vs percent-of-peak, at all three points; `BlockOpenings` refuses every opening | (ii) cadence + (iv) scope | `MG10-a`: bar 1 opens 600 under the entry and closes 100 under — kernel breaches at the open mark and refuses bar 2's add (book 100), adapter admits it (book 200). `MG10-b`: latched at bar 1's close, the adapter still reverses twice (`L→S` −600, `S→L`), the kernel refuses both reversals (`RiskLimit` at bars 2, 3) and holds. Corpus: `ta-closedtrades-risk-introspection-01` (20 % of peak) identical 1502/1502 rows with the kernel seeded — the rule never fires there | retained: the latched-reversal exemption cannot be composed on a kernel block; a close-only cadence knob alone would not close it | -| `strategy.risk.max_cons_loss_days` | `SourceDayLedger` (pine_adapter.hpp:1419-1427): +1 per losing TRADE on a new chart day, reset to 0 by any winning trade at its fill (`consecutive_loss_days` pine_adapter.cpp:25721); gated immediately at precommit and latched at the close | `max_consecutive_loss_days`: a day's NET realized result settles the streak when the next day opens | (iv) | `MG11-a` (loss; win then loss; loss): adapter latches at day 3's first loss (4 rows), kernel's netted streak never reaches 2 (6 rows, no event). `MG11-b` (loss then smaller win, twice): adapter resets daily (5 rows), kernel blocks at day 3's open (4 rows, one event). No corpus probe declares the rule | retained: an order-dependent per-trade streak is not a generic day result | -| `strategy.risk.max_intraday_loss` | day-open equity at the day's first bar (`intraday_start_equity` pine_adapter.cpp:22490); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:20856), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:20888, `submit_intraday_loss_close` pine_adapter.cpp:20856), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:24678); epsilon `1e-9·max(1,|limit|)`; ticket `"Close Position (Max intraday Loss)"` → `CloseCause::RiskLimit`; the block refuses ALL placements for the chart day (`entry`/`order`/`close`/`close_all`/`exit`, `intraday_loss_orders_blocked` pine_adapter.cpp:17476, `:17476`, `:17476`, `:17476`, `:17476`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:14577) | `max_intraday_loss` at the three points; `FlattenAndBlock` closes `AsPresented` under `"__kernel_risk__"` / `"Risk limit"`, blocks openings only, leaves working orders | (ii) ticket, day basis + (iv) path check, placement scope, unbooked fill, epsilon | `MG12-a`: day 2's first bar 100→85→95 — adapter closes at the extreme (85, −1500, cause 4) and refuses the day's later entry, kernel measures 0 at the open and 500 at the close, never breaches, ends with 300 units. `MG12-b`: breach at a bar open inside the day — same money on both sides (100 @ 88, −1200) under different tickets; the adapter withdraws a resting limit entry, the kernel leaves it working and it fills on day 3. Both sides mark the day's opening equity at its first bar: a gap there is not an intraday loss for either | retained: the path-extreme check is the rule's substance and the kernel has no path evaluation point; a risk ticket field and a `PathAdverseExtreme` check kind would be generic additions (mirroring `NativeMarginModel`) but would still leave the placement scope, the unbooked fill and the epsilon | -| `strategy.risk.max_intraday_filled_orders` | `compat::pine::IntradayCap` + `IntradayOrderBudget` (243 + 94 lines): charged slots per risk day, quota transfer, POOC deferral, noop-market skip, the close at the fill price or the bar's better extreme (`post_dispatch`), latch → all placements denied (`cap_placement_denied` pine_adapter.cpp:17472); ticket `"Close Position (Max number of filled orders in one day)"` → `CloseCause::FillCap`; day = `session_trading_day_index` for a `HHMM-HHMM` session, else the chart day (`chart_day_key` pine_adapter.cpp:17378, `mday·100+month` in the CHART timezone) | `max_fills_per_day`: applied fills counted as they settle, evaluated at the three points; `FlattenAndBlock` at the drain's cursor; `SessionDay` / `CalendarDayInTimezone` on the SPEC timezone | (i) count + (ii) ticket + (iv) close price, charged-slot budget, placement scope, chart-day key | `MG13`: the second fill reaches the cap on a bar that closes above its open — adapter closes at that bar's HIGH (103) under its ticket, kernel at the close (101) under its own. `MG14`: chart timezone `America/New_York` over a UTC symbol — bar 4 (04:00 UTC) is a new chart day for the adapter and the same UTC day for the kernel. Corpus, kernel seeded with the declared cap: `cap-risk-gates-allow-max-intraday-01` identical 1464/1464 (the third fill of each day flattens the book, so block and latch coincide); `cap-max-intraday-filled-orders-isolate-01` 3840 of 3916 rows differ from row 77 (2025-04-08); `cap-gatekeeper-intraday-risk-01` 312 of 604 differ from row 293 (2025-10-25); `composite-bracket-cap-range-pending-stop-01` 2370 of 2384 differ from row 17 (2025-04-02 02:45), two extra rows | retained: the count alone is expressible, but the kernel's block would refuse fills the transfer admits and the close price is TV's; a report-only action would give the adapter a count it cannot use (charged slots ≠ applied fills) | +| `strategy.risk.max_position_size` | precommit refuses a flat/same-side ENTRY at its fill when the LIVE book already holds ≥ the limit (`max_position_size` pine_adapter.cpp:16493; the retired legacy rule is `git show ab9714be:src/source/pine_risk.cpp`, lines 115 and 2531) | `max_abs_units`: refuses a fill whose RESULTING book would exceed it | (iv) | `PS`: two-unit entries against 3 — adapter admits the second (live 2 < 3, book 4) and refuses the third; kernel refuses the second (`MaxAbsUnits` at bars 1, 2), book 2 | retained: a pre-fill-book gate is a TV emulation fact, not a generic cap | +| `strategy.risk.max_drawdown` | `update_risk_state(bar.close)` once per script bar at the close mark (`update_risk_state(bar.close)` pine_adapter.cpp:17483, `update_risk_state` pine_adapter.cpp:17483): peak / running max drawdown at close marks, `>=` exact, percent of the current peak; the latch `risk_.halted` gates flat/same-side entries at precommit (`risk_.halted` pine_adapter.cpp:16485), never an opposite entry | `max_drawdown`: current drawdown vs percent-of-peak, at all three points; `BlockOpenings` refuses every opening | (ii) cadence + (iv) scope | `MG10-a`: bar 1 opens 600 under the entry and closes 100 under — kernel breaches at the open mark and refuses bar 2's add (book 100), adapter admits it (book 200). `MG10-b`: latched at bar 1's close, the adapter still reverses twice (`L→S` −600, `S→L`), the kernel refuses both reversals (`RiskLimit` at bars 2, 3) and holds. Corpus: `ta-closedtrades-risk-introspection-01` (20 % of peak) identical 1502/1502 rows with the kernel seeded — the rule never fires there | retained: the latched-reversal exemption cannot be composed on a kernel block; a close-only cadence knob alone would not close it | +| `strategy.risk.max_cons_loss_days` | `SourceDayLedger` (pine_adapter.hpp:1419-1427): +1 per losing TRADE on a new chart day, reset to 0 by any winning trade at its fill (`consecutive_loss_days` pine_adapter.cpp:25723); gated immediately at precommit and latched at the close | `max_consecutive_loss_days`: a day's NET realized result settles the streak when the next day opens | (iv) | `MG11-a` (loss; win then loss; loss): adapter latches at day 3's first loss (4 rows), kernel's netted streak never reaches 2 (6 rows, no event). `MG11-b` (loss then smaller win, twice): adapter resets daily (5 rows), kernel blocks at day 3's open (4 rows, one event). No corpus probe declares the rule | retained: an order-dependent per-trade streak is not a generic day result | +| `strategy.risk.max_intraday_loss` | day-open equity at the day's first bar (`intraday_start_equity` pine_adapter.cpp:22492); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:20858), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:20890, `submit_intraday_loss_close` pine_adapter.cpp:20858), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:24680); epsilon `1e-9·max(1,|limit|)`; ticket `"Close Position (Max intraday Loss)"` → `CloseCause::RiskLimit`; the block refuses ALL placements for the chart day (`entry`/`order`/`close`/`close_all`/`exit`, `intraday_loss_orders_blocked` pine_adapter.cpp:17478, `:17478`, `:17478`, `:17478`, `:17478`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:14579) | `max_intraday_loss` at the three points; `FlattenAndBlock` closes `AsPresented` under `"__kernel_risk__"` / `"Risk limit"`, blocks openings only, leaves working orders | (ii) ticket, day basis + (iv) path check, placement scope, unbooked fill, epsilon | `MG12-a`: day 2's first bar 100→85→95 — adapter closes at the extreme (85, −1500, cause 4) and refuses the day's later entry, kernel measures 0 at the open and 500 at the close, never breaches, ends with 300 units. `MG12-b`: breach at a bar open inside the day — same money on both sides (100 @ 88, −1200) under different tickets; the adapter withdraws a resting limit entry, the kernel leaves it working and it fills on day 3. Both sides mark the day's opening equity at its first bar: a gap there is not an intraday loss for either | retained: the path-extreme check is the rule's substance and the kernel has no path evaluation point; a risk ticket field and a `PathAdverseExtreme` check kind would be generic additions (mirroring `NativeMarginModel`) but would still leave the placement scope, the unbooked fill and the epsilon | +| `strategy.risk.max_intraday_filled_orders` | `compat::pine::IntradayCap` + `IntradayOrderBudget` (243 + 94 lines): charged slots per risk day, quota transfer, POOC deferral, noop-market skip, the close at the fill price or the bar's better extreme (`post_dispatch`), latch → all placements denied (`cap_placement_denied` pine_adapter.cpp:17474); ticket `"Close Position (Max number of filled orders in one day)"` → `CloseCause::FillCap`; day = `session_trading_day_index` for a `HHMM-HHMM` session, else the chart day (`chart_day_key` pine_adapter.cpp:17380, `mday·100+month` in the CHART timezone) | `max_fills_per_day`: applied fills counted as they settle, evaluated at the three points; `FlattenAndBlock` at the drain's cursor; `SessionDay` / `CalendarDayInTimezone` on the SPEC timezone | (i) count + (ii) ticket + (iv) close price, charged-slot budget, placement scope, chart-day key | `MG13`: the second fill reaches the cap on a bar that closes above its open — adapter closes at that bar's HIGH (103) under its ticket, kernel at the close (101) under its own. `MG14`: chart timezone `America/New_York` over a UTC symbol — bar 4 (04:00 UTC) is a new chart day for the adapter and the same UTC day for the kernel. Corpus, kernel seeded with the declared cap: `cap-risk-gates-allow-max-intraday-01` identical 1464/1464 (the third fill of each day flattens the book, so block and latch coincide); `cap-max-intraday-filled-orders-isolate-01` 3840 of 3916 rows differ from row 77 (2025-04-08); `cap-gatekeeper-intraday-risk-01` 312 of 604 differ from row 293 (2025-10-25); `composite-bracket-cap-range-pending-stop-01` 2370 of 2384 differ from row 17 (2025-04-02 02:45), two extra rows | retained: the count alone is expressible, but the kernel's block would refuse fills the transfer admits and the close price is TV's; a report-only action would give the adapter a count it cannot use (charged slots ≠ applied fills) | **Proposed generic additions (not implemented — no consumer today).** A risk ticket (`label` / `comment` on `NativeRiskLimits`, folded only when set, @@ -557,8 +557,8 @@ Pine run needs the source change the patch makes, and the in-process pins remain the five probes' baseline rows and the `SW` check above. **The P6 decision: native-only.** Re-verified on main `b8e7976e`: the ordering -stands (`project()` at pine_strategy_host.cpp:345, the kernel's `configure` at :354, -the `set_pine_risk_*` setters (`set_pine_risk_direction` pine_strategy_host.cpp:881), and corpus 442d497 +stands (`project()` at pine_strategy_host.cpp:350, the kernel's `configure` at :359, +the `set_pine_risk_*` setters (`set_pine_risk_direction` pine_strategy_host.cpp:886), and corpus 442d497 `corpus/validation/cap-risk-gates-allow-max-intraday-01`'s generated strategy still emits the three calls inside `on_source_bar`); `rg "spec\.risk|NativeRiskLimits" src/source include/pineforge/source` is still empty; `test_adapter_risk_relower` still @@ -851,7 +851,7 @@ so the gross open profit is 800. | **the equity re-mark** | `last.open_profit = 0; last.equity = initial_capital_ + net_profit_sum_ + range_end_pnl` | none — the kernel leaves the curve exactly as the run marked it | `RE1` (no fee): the adapter reports the curve's last point as `eq=100800 op=0`, the kernel as `eq=100800 op=800` — same equity, the open profit moved into the realized side. `RE2` (0.1 %): `eq=100758.8 op=0` against `eq=100800 op=800`; the adapter's equity is **41.2 lower**, exactly the round-trip commission of both lots (20.5 + 20.7), because the re-mark carries the NET row P&L where the run's own mark carried the GROSS open profit | retained: a report that restates the last equity point off net row P&L is TradingView's report shape. Re-marking the curve from the kernel would move a figure every bare host already reads | | **the extreme re-fold** | `max_equity_`/`min_equity_` reset to `initial_capital_`, `max_drawdown_`/`max_runup_` to 0, then `fold_equity_extreme` over the whole curve | none — the extremes stay as `update_equity_extremes` sampled them | `RE3` (a dip to 90 at bar 1, a peak of 108 at bar 3, 0.1 %): both sides agree on `maxeq=101400 mineq=99000 maxdd=2400`, and the run-up diverges — adapter `maxru=1758.8000000000029`, kernel `maxru=1800`. The re-fold is a no-op **except** where the re-marked last point differs, so the divergence is again the 41.2 round trip, modulo the binary fold order | retained: it is a consequence of the re-mark above, not a mechanism of its own; it cannot be re-lowered while the re-mark is adapter policy | | **the same-bar exit re-sort** | `sort_same_bar_exit_trades(trades_, adapter_)` — same-bar bracket exits ordered by script command sequence | none — the kernel never reorders `trades_` | not exercised by the four scenarios (it reorders CLOSED rows, not range-end rows); it is TradingView command-sequence ordering by construction (`git show ab9714be:src/source/pine_fills.cpp`, lines 664-670) | retained: script command sequence is a source-language fact | -| **the mark cadence** | three call sites, each clearing and rebuilding: the terminal sub-bar of the last source bar (`scheduler_record_range_end` pine_strategy_host.cpp:1982), after every applied execution once terminal (`record_applied_range_end`, :796), and the scheduler's terminal-source-bar hook (`PineScheduler::applied` pine_scheduler_native.cpp:688, before the bar's broker hash) | once, after the last point of the run (`run_batch` ×3, `stream_end`) | structural, not a number: the adapter's rows are re-derived at each mark so the report reflects the book as of the last event on the terminal bar, and its re-marked equity is visible to that bar's broker hash. A one-shot run-end producer cannot occupy those three points | retained: three marks on a terminal bar is a TradingView report cadence, not a generic run-end fact | +| **the mark cadence** | three call sites, each clearing and rebuilding: the terminal sub-bar of the last source bar (`scheduler_record_range_end` pine_strategy_host.cpp:1987), after every applied execution once terminal (`record_applied_range_end`, :801), and the scheduler's terminal-source-bar hook (`PineScheduler::applied` pine_scheduler_native.cpp:688, before the bar's broker hash) | once, after the last point of the run (`run_batch` ×3, `stream_end`) | structural, not a number: the adapter's rows are re-derived at each mark so the report reflects the book as of the last event on the terminal bar, and its re-marked equity is visible to that bar's broker hash. A one-shot run-end producer cannot occupy those three points | retained: three marks on a terminal bar is a TradingView report cadence, not a generic run-end fact | | **the excursion projection gate** | `excursion_range_end_projection_` suppressed the owner's exit-bar path fold for the projection | none — the generic builder folds what it folds | `RE1`: identical run-up/draw-down on both halves (`ru=500 dd=0`, `ru=300 dd=0`) | retired by R5 lane H-THIN: the Pine host owns no lot excursion since, so no owner fold is left to suppress; `RE1` still reads the same magnitudes, now the kernel's | | **the tail / warm-up suppression** | `if (stream_warmup_mode_ \|\| realtime_tail_ \|\| equity_curve_.empty()) return;` | none — the kernel emits at `stream_end` whatever the run left open | structural: the kernel has no warm-up or realtime-tail notion, and `run_corpus`'s probe-tail suppression (`suppress_probe_tail`) is a harness fact | retained: a tail is a source-layer replay concept | @@ -1133,10 +1133,10 @@ moving a single value, but not from inside lane F9's files: 1,339 rows each one callback old, an OCA re-price and a trail attach rows two callbacks old, and the row of an entry that filled at bar 1 is rewritten at bar 60, when its first exit is attached - (`has_full_entry_bracket` pine_adapter.cpp:11423). Full-table scans + (`has_full_entry_bracket` pine_adapter.cpp:11425). Full-table scans rewrite rows as well (`suspend_brackets_for_reversal` pine_adapter.cpp:1965, `revive_brackets_after_margin` pine_adapter.cpp:2200, - `preserved_by_close_all` pine_adapter.cpp:14645), and + `preserved_by_close_all` pine_adapter.cpp:14647), and `admission::Journal::retain` market_admission.hpp:205 drops events from the middle of the journal. So "a row stops changing once its request is done" is false, and a cache resting on it would move a value. @@ -1224,13 +1224,13 @@ retires every leg of each exit row it names and drops such a parked stop (`retire_cancelled_exits` pine_adapter.cpp:2123), the revival skips a withdrawn exit, and K1 holds a withdrawn leg only while its origin is live or opened -- the one reader left is `origin_leg_consumed`, and it asks about such origins alone (`erase_retired_rows` -pine_adapter.cpp:3637). Both shapes now cost ×1.89 to ×2.12 per doubling up +pine_adapter.cpp:3639). Both shapes now cost ×1.89 to ×2.12 per doubling up to 64,000 bars and retain 3 rows. Row D was the kernel's half: the adapter enrolled every opening in its id's kernel roster (`cohort_add`) and never took one off, and `hash_cohorts` folds every member at every read. An origin whose request chain no longer works and that holds no open lot can never be bound again, so at every bar open that follows a command event the adapter takes -it off (`release_closed_cohort_origins` pine_adapter.cpp:4085); a lot opened +it off (`release_closed_cohort_origins` pine_adapter.cpp:4087); a lot opened by a replace successor keeps every older member of its roster while it is open, because its root is not a public fact. The flip then costs ×1.99, ×2.01, ×2.02 per doubling, and its rosters end with one member where they @@ -1288,7 +1288,7 @@ engine.hpp:1667) is not. The other is a contract that the six folded fields of a booked row are never rewritten. In the tree they are not; the only writer after booking, the Pine host, rewrites `close_cause`, `exit_from_bracket`, `exit_comment` and `entry_incarnation` only; its -writes are `close_cause` pine_strategy_host.cpp:634, `exit_comment` :763, `close_cause` :777-782, `exit_from_bracket` :1283 and `entry_incarnation` :1464-1472. +writes are `close_cause` pine_strategy_host.cpp:639, `exit_comment` :768, `close_cause` :782-787, `exit_from_bracket` :1288 and `entry_incarnation` :1469-1477. But `trades_` engine.hpp:546 is a protected member any host can write, so a lane that takes the consumer-side digest has to make that contract explicit. Folding `(count, digest)` in place of the rows saves the 256-entry table but @@ -1320,8 +1320,8 @@ the bars cost 3.8 times the CPU with a round trip every ten bars script point, not after the run's teardown, so that it is one value with recording on or off (`broker_state_hash_projection` pine_strategy_host.cpp:166). `capture_script_continuation_hash` -pine_strategy_host.cpp:527 takes that snapshot at the last batch bar -(`last_batch` pine_strategy_host.cpp:608), and taking it is one full +pine_strategy_host.cpp:532 takes that snapshot at the last batch bar +(`last_batch` pine_strategy_host.cpp:613), and taking it is one full continuation fold per run. Measured on the gated replay — `ReissueReplay` on 43,008 bars, `run()` process CPU, best of 5, three rounds — it is 0.2116 / 0.2137 / 0.2141 s with the capture and 0.1632 / 0.1614 / 0.1652 s with it @@ -1599,14 +1599,14 @@ the rule gives 9.9 + 9.89). | # | Risk | Why it bites | Containment | Src | |---|---|---|---|---| -| E1 | **Spec hashing.** `hash_spec` folded every field unconditionally (native_execution_consumer.cpp:234-256); every block added since folds only when set | Any new field, even defaulted, changes every continuation / broker-state hash, adapter runs included | Fold new blocks only when set, as a tagged extension (precedent `precommit_digest_` native_execution_consumer.hpp:1919); test that a default spec hashes to the pre-change constant | F | -| E2 | **Margin.** The TV margin call is built host-side from kernel primitives (a Stop-triggered `Reduce` bound to openings, or `execute_current`, which survives in `submit_margin_call_slice` pine_adapter.cpp:17944); its chronology lives at `schedule_margin_call_path` pine_adapter.cpp:20222 and its excursion sample differed by chronology (the Pine host's own excursion model, retired by R5 lane H-THIN: the kernel's sampler books it) | A kernel model under an adapter run would liquidate twice or reorder settlement; a checkpoint that reduces mid-path changes lot state exactly where the adapter's chronology lives | Superseded by R5: the adapter declares `spec.margin` — maintenance-only, so no opening is decided twice (`AdmitWithHostMargin`, MG4) — and the kernel owns the mechanism (check points, resting liquidation, receipt) while the adapter's hooks answer TradingView's scheduling, money and slice (ADR-0001 "Margin"). What stays host-side is named there: the chronology exceptions, the pre-open slice, the 1x-long money call and the broker-open FX rollover, whose kernel `FxRoll` point measures elsewhere (MG9). Corrected by R5 lane F7 | F O S | -| E3 | **Sizing.** TV sizes from the tick-rounded signal close, 10-digit-rounded equity and fee-reserved cash, frozen at signal, with fill-time exceptions in COOF paths (`default_sizing_shape` pine_adapter.cpp:3182, the COOF `at_fill` exception pine_adapter.cpp:8393) | One "generic" percent sizing serving both would break parity or import TV rounding into the kernel; the last ulp differs on lot-stepped instruments | Two paths on purpose: `Sized` (kernel, plain arithmetic, timing as a first-class field) vs `HostSized` + the adapter resolver. Never "simplify" the adapter onto the kernel basis. What the two paths share is the conversion: since R5 lanes N11 and F7 the division by price × point value × FX and the percentage fee reserve exist once, in the kernel (`native_sized_units`), for a default AND a typed (`qty_type` cash / percent) quantity. The adapter keeps what is measured TradingView: the money basis (signal-time equity, TradingView's margin equity at a typed fill, the hypothetical Flatten's balance for a typed reversal), the freeze, the fill-time exceptions and both lot floors (`tests/test_adapter_sizing_relower.cpp` cases 8, 9, 11: kernel 0.0392 against the percent floor's 0.0391 on a 0.0001 lot), and a staged FX series keeps `Sized` wherever the acceptance coordinate converts at the source's rate (case 12) | F O S | -| E4 | **Calc on fill.** Kernel suffix eligibility is continuous along the segment; TV refills only at O/H/L/C waypoints -- on a magnified path at each intrabar's open, twice, and its two extremes, never its close (R5 lane MAG-INTRABAR) (`PineScheduler::recalculate` pine_scheduler_native.cpp:727, `coof_next_waypoint` pine_adapter.cpp:6947) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:6439 and `drain_applied_notifications` (native_execution_consumer.cpp:7122); new behaviour only behind new opt-ins | F | -| E5 | **Open-bar lookahead.** The adapter reads the full bar in `on_bar_open` (margin path scheduling uses H/L, pine_adapter.cpp:22250) | The complete bar cannot be masked globally | Opt-in `OpenOnly` view (A.4); loud doc warning in L0 | F | -| E6 | **Double equity recording.** The source host already records per bar (history: only its cadence remains, `scheduler_mark_report_point` pine_strategy_host.cpp:2786) | Kernel recording would duplicate points and shift drawdown | Report policy / ownership (A.1); the adapter declares `KernelRecordedAtHostMarks` (R1): one recorder, at the adapter's own marks | F O | +| E1 | **Spec hashing.** `hash_spec` folded every field unconditionally (native_execution_consumer.cpp:234-256); every block added since folds only when set | Any new field, even defaulted, changes every continuation / broker-state hash, adapter runs included | Fold new blocks only when set, as a tagged extension (precedent `precommit_digest_` native_execution_consumer.hpp:1942); test that a default spec hashes to the pre-change constant | F | +| E2 | **Margin.** The TV margin call is built host-side from kernel primitives (a Stop-triggered `Reduce` bound to openings, or `execute_current`, which survives in `submit_margin_call_slice` pine_adapter.cpp:17946); its chronology lives at `schedule_margin_call_path` pine_adapter.cpp:20224 and its excursion sample differed by chronology (the Pine host's own excursion model, retired by R5 lane H-THIN: the kernel's sampler books it) | A kernel model under an adapter run would liquidate twice or reorder settlement; a checkpoint that reduces mid-path changes lot state exactly where the adapter's chronology lives | Superseded by R5: the adapter declares `spec.margin` — maintenance-only, so no opening is decided twice (`AdmitWithHostMargin`, MG4) — and the kernel owns the mechanism (check points, resting liquidation, receipt) while the adapter's hooks answer TradingView's scheduling, money and slice (ADR-0001 "Margin"). What stays host-side is named there: the chronology exceptions, the pre-open slice, the 1x-long money call and the broker-open FX rollover, whose kernel `FxRoll` point measures elsewhere (MG9). Corrected by R5 lane F7 | F O S | +| E3 | **Sizing.** TV sizes from the tick-rounded signal close, 10-digit-rounded equity and fee-reserved cash, frozen at signal, with fill-time exceptions in COOF paths (`default_sizing_shape` pine_adapter.cpp:3184, the COOF `at_fill` exception pine_adapter.cpp:8395) | One "generic" percent sizing serving both would break parity or import TV rounding into the kernel; the last ulp differs on lot-stepped instruments | Two paths on purpose: `Sized` (kernel, plain arithmetic, timing as a first-class field) vs `HostSized` + the adapter resolver. Never "simplify" the adapter onto the kernel basis. What the two paths share is the conversion: since R5 lanes N11 and F7 the division by price × point value × FX and the percentage fee reserve exist once, in the kernel (`native_sized_units`), for a default AND a typed (`qty_type` cash / percent) quantity. The adapter keeps what is measured TradingView: the money basis (signal-time equity, TradingView's margin equity at a typed fill, the hypothetical Flatten's balance for a typed reversal), the freeze, the fill-time exceptions and both lot floors (`tests/test_adapter_sizing_relower.cpp` cases 8, 9, 11: kernel 0.0392 against the percent floor's 0.0391 on a 0.0001 lot), and a staged FX series keeps `Sized` wherever the acceptance coordinate converts at the source's rate (case 12) | F O S | +| E4 | **Calc on fill.** Kernel suffix eligibility is continuous along the segment; TV refills only at O/H/L/C waypoints -- on a magnified path at each intrabar's open, twice, and its two extremes, never its close (R5 lane MAG-INTRABAR) (`PineScheduler::recalculate` pine_scheduler_native.cpp:727, `coof_next_waypoint` pine_adapter.cpp:6949) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:6507 and `drain_applied_notifications` (native_execution_consumer.cpp:7190); new behaviour only behind new opt-ins | F | +| E5 | **Open-bar lookahead.** The adapter reads the full bar in `on_bar_open` (margin path scheduling uses H/L, pine_adapter.cpp:22252) | The complete bar cannot be masked globally | Opt-in `OpenOnly` view (A.4); loud doc warning in L0 | F | +| E6 | **Double equity recording.** The source host already records per bar (history: only its cadence remains, `scheduler_mark_report_point` pine_strategy_host.cpp:2791) | Kernel recording would duplicate points and shift drawdown | Report policy / ownership (A.1); the adapter declares `KernelRecordedAtHostMarks` (R1): one recorder, at the adapter's own marks | F O | | E7 | **Price grid.** TV does not merely quantize: it tests triggers against a quantized bar while booking the fill on the *unrounded* level, so the adapter shifts the threshold (`source_trigger_threshold` pine_adapter.cpp:384) and books elsewhere | A kernel grid that quantizes both sides is correct and not TV; moving rounding into the kernel would change both native results and the adapter | `NativePriceGrid::None` for the adapter permanently; never "unify". Before L8: a byte-level experiment that the default price spelling on decimal ticks (one-ULP representations) does not move (S) | O S | -| E8 | **HTF.** The evaluator machinery carries TV publication semantics (lookahead, boundary deferral, early close, `syminfo.type` branching engine_security.cpp:67-73). The feed store is TV-calibrated too: "W" / "M" from native daily bars, native stamps as the period partition (`set_native_security_feed` `sha256:eca3dbea0baa461982ba89f13d0a5c43277854069fb6eb2dd2f5c5471313204d` engine_aux_security.cpp:51-83, *fresh*). Two aggregators exist: script buckets use `native_calendar`, `request.security` uses `TimeframeAggregator`, whose completion rules are TV-pinned (timeframe.hpp:288-330) | Reuse risks changing the *order* of the Pine scheduler's pumps; a bare host inherits TV partition rules; native HTF may differ from Pine HTF on early-close sessions | Registration-gated pump; no change to `scheduler_feed_security_input` pine_strategy_host.cpp:1811; full corpus diff; supplied-bar provenance preserved, never silently replaced by chart aggregation (S); differences on early-close sessions are recorded, not "fixed" (F). The Pine scheduler keeps one more `TimeframeAggregator` pass, before the run (`PineScheduler::run_begin`, audit M26): the script-bar count it yields (`terminal_source_bar`, the probe-tail suppression) and the per-input completion flags it hands `on_native_input` are needed before the first input arrives, while the kernel seals script intervals only as inputs arrive and offers no pre-run interval query (`NativeExecutionConsumer::script_interval_at` is consumer-private). Retained by R5 lane F7: replacing it needs that kernel capability | F O S | +| E8 | **HTF.** The evaluator machinery carries TV publication semantics (lookahead, boundary deferral, early close, `syminfo.type` branching engine_security.cpp:67-73). The feed store is TV-calibrated too: "W" / "M" from native daily bars, native stamps as the period partition (`set_native_security_feed` `sha256:eca3dbea0baa461982ba89f13d0a5c43277854069fb6eb2dd2f5c5471313204d` engine_aux_security.cpp:51-83, *fresh*). Two aggregators exist: script buckets use `native_calendar`, `request.security` uses `TimeframeAggregator`, whose completion rules are TV-pinned (timeframe.hpp:288-330) | Reuse risks changing the *order* of the Pine scheduler's pumps; a bare host inherits TV partition rules; native HTF may differ from Pine HTF on early-close sessions | Registration-gated pump; no change to `scheduler_feed_security_input` pine_strategy_host.cpp:1816; full corpus diff; supplied-bar provenance preserved, never silently replaced by chart aggregation (S); differences on early-close sessions are recorded, not "fixed" (F). The Pine scheduler keeps one more `TimeframeAggregator` pass, before the run (`PineScheduler::run_begin`, audit M26): the script-bar count it yields (`terminal_source_bar`, the probe-tail suppression) and the per-input completion flags it hands `on_native_input` are needed before the first input arrives, while the kernel seals script intervals only as inputs arrive and offers no pre-run interval query (`NativeExecutionConsumer::script_interval_at` is consumer-private). Retained by R5 lane F7: replacing it needs that kernel capability | F O S | | E9 | **TA numerics are TV-calibrated** (1e-10 band in DMI / MFI / percentrank; conditional-call extremum ring `ExtremeRing` ta.hpp:134) | Non-Pine users may expect IEEE semantics; changing them breaks parity | Rename only (§2.ii b). For an every-bar caller the ring equals a positional window (the ring comment `sha256:fd1f92206558863ad8df33fd3ef3b8cc8b9bd6678a25fc98ca866168c1a81aca` ta.hpp:87-88). Document. → U6 | F O S | | E10 | **Dead-but-hashed lot flags** (§2.ii j) | Deleting them changes `broker_state_hash` | Separate, explicitly non-neutral lane (L12) | F O | | E11 | **OCA and ownership.** Pine's default-sized OCA and `from_entry` reservation rules are source policies | Source IDs, callsite tokens or Pine labels leaking into `WorkingRequestCore` | Opaque keys and receipts only (R5-4) | S | @@ -1623,7 +1623,7 @@ the rule gives 9.9 + 9.89). | P5 | **Test asymmetry:** 430 adapter test files vs 14 native when this plan was written (on this tree 543 `tests/*.cpp` name `source::PineStrategyHost`, 110 `NativeStrategyHost`) | native-only tests in every lane (§3.1 b) | O | | P6 | **Stream vs batch.** The stream refuses monthly input, mixed modes, in-session gaps, security feeds and source setters, and tick input under an FX curve (any non-empty curve until audit lane N6) (`native-engine.md`, "Stream limitations (current)") | L5 and L6 must state which refusals are generic and which are deliberate source limits | S | | P7 | **C ABI variant growth** | §2.iii hardening rules | S | -| P8 | **Unverified in the inputs:** whether `execution::LifecycleEffects` (execution.hpp:136-139) is ever non-empty from the Pine path, else `PlacementSnapshot::legs` (pine_adapter.hpp:364) is the only live lifecycle (O); the `strategy.risk.allow_entry_in` setter chain beyond `set_pine_risk_direction` pine_strategy_host.cpp:881 → `set_risk_direction` pine_adapter.cpp:26463 (O); what the ABI receipts pin (F); the probe corpus itself lives outside this tree (F) | verify inside the lane that touches each | F O | +| P8 | **Unverified in the inputs:** whether `execution::LifecycleEffects` (execution.hpp:136-139) is ever non-empty from the Pine path, else `PlacementSnapshot::legs` (pine_adapter.hpp:364) is the only live lifecycle (O); the `strategy.risk.allow_entry_in` setter chain beyond `set_pine_risk_direction` pine_strategy_host.cpp:886 → `set_risk_direction` pine_adapter.cpp:26465 (O); what the ABI receipts pin (F); the probe corpus itself lives outside this tree (F) | verify inside the lane that touches each | F O | ### 4.3 Open design questions (supervisor level) @@ -1632,7 +1632,7 @@ the rule gives 9.9 + 9.89). | Q1 | Does `engine_script_run_v18` open at L4 and close after L5 ∥ L6, or does L4's sizing hook ship with that batch (§3.4 row 4)? | — (raised by this merge) | | Q2 | Is a hash-domain bump (needed by §2.ii j) acceptable before the R5 final audit (phase table, native-refactor-progress.md:24, *fresh*), or must L12 wait? | F asks; none | | Q3 | Does the fixed-population gate (4190 probes then) compare broker-state hashes across releases, or only trades? If hashes, L4 and L12 need a hash-version plan | O asks; none | -| Q4 | `SizeTime::AtAcceptance` needs a current execution point (`current_execution_point` native_execution_consumer.cpp:11995); between realtime inputs there is none — reject or fall back to `AtMatch`? | F: reject with `InvalidQuantityBasis` | +| Q4 | `SizeTime::AtAcceptance` needs a current execution point (`current_execution_point` native_execution_consumer.cpp:12072); between realtime inputs there is none — reject or fall back to `AtMatch`? | F: reject with `InvalidQuantityBasis` | | Q5 | Liquidation check granularity: `PathAdverseExtreme` default; is a cheaper `CalculationOnly` worth shipping in v1? Pure spec value or injectable policy object (S)? | F: path-accurate default; merged A.3 = spec value + host sizing hook | | Q6 | Does a native series return the current value, a retained series object, or both? It decides C ABI ownership and hash size | S asks; A.5 offers push + current-bar pull | | Q7 | Is a close-reservation key stable across host restarts or only within one `RunIdentity`? | S: identity rules suggest run scope | @@ -1650,7 +1650,7 @@ All six are settled by what landed: U1 (native subscriptions take `lookahead` an | **U3** | **Do risk limits ship in v1?** | R5-9: in scope, not a blocker. S wanted them as a prerequisite; F accepts a toolkit stand-in; O defers. | | **U4** | **Stream-safe FX (FP6): in the roadmap or not?** It has no R5 lane. | F: out of scope here ("O7 clock"). S: its P10, an FX epoch consumed while flat, same curve in batch and stream. | | — | **U4, answered by audit lane N6.** In: the curve declared through `configure_native_fx_curve` is the FX epoch of the run, batch or stream; the account converts at the rate of each driver point's effective time, so the epoch is consumed by the walk itself, flat or not, and a confirmed-bar stream is the batch of the same bars. No `NativeFxEpoch` type and no `on_native_fx_epoch` callback were needed. Out, permanently: realtime rate ingestion (a new rate is a new run) and tick-driven input under a curve (its hooks and partial slots read a stale conversion clock — measured). | — | -| — | **N6 follow-ups (measured on the adapter; both closed by R5 lane F7).** (1) `apply_fx_open_margin_slice` (pine_adapter.cpp:5677) throws on a rate step over a carried position whose margin is not 100 %: a 2× long carried through a 1.0 → 1.5 step ends the whole run with status 1 and an empty report, where the same book without a series, or with a series that never steps, completes. The kernel's `FxRoll` point is the generic mechanism that case can re-lower onto. (2) `default_sizing_intent` (pine_adapter.cpp:3255) returns nullopt whenever any FX series is staged — even one point that restates the scalar rate — so an FX run silently loses the kernel `Sized` path R2b landed (measured: 1 kernel `Sized` request without a series, 0 with a one-point 1.0 series, same 50-unit book). **Closed by R5 lane F7.** (1) The throw is gone: the carried roll takes TradingView's broker-open checkpoint at any positive margin — its money at the open under the new rate and the side's own maintenance ratio, its lot-floored 4x restore, executed at the open before the step bar's script — and a survivor that is not a 1x long is re-checked over the rest of the bar by the kernel's liquidation. A 2x long or short of 15 at 100 on 1000 across 1 → 1.5 takes 6.666666666666667 at the open and keeps 8.33; at a 25 % side ratio it takes nothing; a constant curve never rolls (`tests/test_affordability_fx.cpp` F7 rows; G6's leveraged pin and its L0 oracle twin moved with an `expectation corrected:` note). The kernel's `FxRoll` point stays refused, measured (MG9). (2) `Sized` is withdrawn only where the kernel would convert at another rate than the source sized at — a curve step between the sub-bar open and the acceptance coordinate — so a one-point series lowers exactly as no series does, and on a stepped curve only the step's own bar keeps its host-resolved intent (`tests/test_adapter_sizing_relower.cpp` case 12). | — | +| — | **N6 follow-ups (measured on the adapter; both closed by R5 lane F7).** (1) `apply_fx_open_margin_slice` (pine_adapter.cpp:5679) throws on a rate step over a carried position whose margin is not 100 %: a 2× long carried through a 1.0 → 1.5 step ends the whole run with status 1 and an empty report, where the same book without a series, or with a series that never steps, completes. The kernel's `FxRoll` point is the generic mechanism that case can re-lower onto. (2) `default_sizing_intent` (pine_adapter.cpp:3257) returns nullopt whenever any FX series is staged — even one point that restates the scalar rate — so an FX run silently loses the kernel `Sized` path R2b landed (measured: 1 kernel `Sized` request without a series, 0 with a one-point 1.0 series, same 50-unit book). **Closed by R5 lane F7.** (1) The throw is gone: the carried roll takes TradingView's broker-open checkpoint at any positive margin — its money at the open under the new rate and the side's own maintenance ratio, its lot-floored 4x restore, executed at the open before the step bar's script — and a survivor that is not a 1x long is re-checked over the rest of the bar by the kernel's liquidation. A 2x long or short of 15 at 100 on 1000 across 1 → 1.5 takes 6.666666666666667 at the open and keeps 8.33; at a 25 % side ratio it takes nothing; a constant curve never rolls (`tests/test_affordability_fx.cpp` F7 rows; G6's leveraged pin and its L0 oracle twin moved with an `expectation corrected:` note). The kernel's `FxRoll` point stays refused, measured (MG9). (2) `Sized` is withdrawn only where the kernel would convert at another rate than the source sized at — a curve step between the sub-bar open and the acceptance coordinate — so a one-point series lowers exactly as no series does, and on a stepped curve only the step's own bar keeps its host-resolved intent (`tests/test_adapter_sizing_relower.cpp` case 12). | — | | **U5** | **Open-bar view default.** Should `OpenOnly` be the default for hosts that are not `LegacyTolerant`? | F: safer for users, but a behaviour change for the ~20 existing native tests; default `Complete` proposed. | | **U6** | **TA comparison band.** Is TradingView's 1e-10 tie-breaking a feature of the TA library, or a default to make configurable? | F, O: rename only; a switch changes indicator outputs and needs its own lane. S: separate policies, hash the policy identity. | @@ -1672,11 +1672,11 @@ virtual void hash_host_extension(BrokerStateHashSink&) const {} // F (S: hash_ NativeMetricsView native_metrics() const; // S: net_profit, equity, closed_trades, open_lots; non-virtual ``` -- `KernelRecorded`: the consumer calls the existing `update_equity_extremes()` (engine.hpp:1362) + `record_equity_point(script_open_ms)` (engine.hpp:1380) once per script calculation (after `invoke_callback` native_execution_consumer.cpp:8081, :8081, :8081, :8081) and after the `AfterCalculation` close; appends the per-bar broker hash when recording is on; synthesizes the open-position row at run end / `stream_end` with the existing `build_close_trade_with_costs` (engine_orders.cpp:70). +- `KernelRecorded`: the consumer calls the existing `update_equity_extremes()` (engine.hpp:1362) + `record_equity_point(script_open_ms)` (engine.hpp:1380) once per script calculation (after `invoke_callback` native_execution_consumer.cpp:8149, :8149, :8149, :8149) and after the `AfterCalculation` close; appends the per-bar broker hash when recording is on; synthesizes the open-position row at run end / `stream_end` with the existing `build_close_trade_with_costs` (engine_orders.cpp:70). - `KernelRecordedAtHostMarks` (R1, KERNEL-EDGE): the same recording, at the points a C++ host names with `NativeStrategyHost::mark_native_report_point` instead of once per calculation; the consumer never records on its own initiative under it, so the continuation identity stays where `HostRecorded` leaves it. The Pine adapter's existing `mark_script_report_point` route has the same kernel producer. It is what the adapter declares (`KernelRecordedAtHostMarks` pine_adapter.cpp:2855), with `report_open_position_at_end == false`. - Publish read-only `closed_trade_*` accessors (RP6). - Alternative: F's virtual `owns_equity_recording()` (the A48 ownership pattern of `owns_lot_excursions` native_host.hpp:1023). Same semantics, but it costs a host-epoch bump, so the spec form was chosen; the virtual was never added, and `NativeReportPolicy` is what landed (§3.4 row 3). -- TUs: `native_execution_consumer.cpp` (seal path, `seal_script` native_execution_consumer.cpp:9019), `engine_run.cpp`, `engine_report.cpp`, `engine_state_hash.cpp`, `native_run_spec.{hpp,cpp}`. +- TUs: `native_execution_consumer.cpp` (seal path, `seal_script` native_execution_consumer.cpp:9087), `engine_run.cpp`, `engine_report.cpp`, `engine_state_hash.cpp`, `native_run_spec.{hpp,cpp}`. ### A.2 L3 — sizing, fractional reduce, sibling reservation (F P1, P7 + O G2 + S P2, P3) @@ -1699,7 +1699,7 @@ struct ScopeFraction { double fraction = 1.0; ScopeClaim claim = ScopeClaim::Gro using ReductionSize = std::variant; // F ``` -- Resolution: `units = cash / (price * point_value * fx)`, `cash` = the value or `fraction * marked_equity(price)`, `price` = the candidate's `default_resolved_price`. It plugs in where `HostSized` units resolve today (native_execution_consumer.cpp:732; terms resolution `resolve_execution_terms` native_execution_consumer.cpp:5740). A host override of `resolve_execution_terms` keeps the last word. Non-representable → `MatchRejectReason::TermsUnresolved` (native_order.hpp:877); validation reuses `RequestRejectReason::InvalidQuantityBasis` (native_order.hpp:827). +- Resolution: `units = cash / (price * point_value * fx)`, `cash` = the value or `fraction * marked_equity(price)`, `price` = the candidate's `default_resolved_price`. It plugs in where `HostSized` units resolve today (native_execution_consumer.cpp:732; terms resolution `resolve_execution_terms` native_execution_consumer.cpp:5808). A host override of `resolve_execution_terms` keeps the last word. Non-representable → `MatchRejectReason::TermsUnresolved` (native_order.hpp:877); validation reuses `RequestRejectReason::InvalidQuantityBasis` (native_order.hpp:827). - `NetOfSiblings`: a fraction-sized reduce claims the bound scope minus the units already claimed by live sibling reduces on it; keys are opaque, never source IDs (S). - Not adopted: S's spec-level `default_sizing` + virtual `resolve_native_sizing` (duplicates the per-request intent, costs an epoch); S's `PercentFreeMargin` (needs L4's free-margin definition) and `EveryCalculation` timing; O's spec-level `size_net_of_fees` (per-request flag, 2 of 3); O's `PercentOfScope` basis (expressed as `ScopeFraction`); S's `ReplaceByKey` / `Fifo` modes (callsite semantics → adapter, R5-4). - TUs: `native_order.{hpp,cpp}`, `native_execution_consumer.cpp`, `native_run_spec.cpp`, the definition hash. @@ -1726,9 +1726,9 @@ virtual std::optional resolve_margin_call_units(const NativeMarginCallVi virtual void on_native_margin_call(const native_order::MarginCallEvent&) {} // O, S ``` -- Mechanism (F): a **kernel-originated request**. At each script-bar open and after each applied fill the kernel computes the liquidation level and rests a `Reduce` / `Flatten` with `Stop{level}` bound to the live openings — the shape the adapter builds host-side today (pine_adapter.cpp:20209). New hashed `RequestDefinition::origin { Host, KernelLiquidation, KernelRisk }`, new `CancelReason::Superseded` on re-price, `MarginCallEvent` joins `CommandEvent` (17 → 18, native_order.hpp:1308). O's alternative, a checkpoint on the excursion walk (`apply_excursion` native_execution_consumer.cpp:3280), is not chosen: a reduction mid-path is the entanglement O itself flags (E2). +- Mechanism (F): a **kernel-originated request**. At each script-bar open and after each applied fill the kernel computes the liquidation level and rests a `Reduce` / `Flatten` with `Stop{level}` bound to the live openings — the shape the adapter builds host-side today (pine_adapter.cpp:20211). New hashed `RequestDefinition::origin { Host, KernelLiquidation, KernelRisk }`, new `CancelReason::Superseded` on re-price, `MarginCallEvent` joins `CommandEvent` (17 → 18, native_order.hpp:1308). O's alternative, a checkpoint on the excursion walk (`apply_excursion` native_execution_consumer.cpp:3348), is not chosen: a reduction mid-path is the entanglement O itself flags (E2). - FX revaluation (MG9) falls out once `native_fx_curve` drives the mark (`native_fx_curve.cpp`). -- TUs: `native_run_spec.{hpp,cpp}`, `native_execution_consumer.cpp` (`admit_opening_inspect` native_execution_consumer.cpp:3604 per side; scheduling next to `invoke_bar_open_callback` / `drain_after_applied`), `native_order.*`, `engine_state_hash.cpp` + the coverage checker, `c_abi.cpp` + `pineforge.h` (`pf_native_run_spec_v2` or an extension struct; `CLAUDE.md` SOP). +- TUs: `native_run_spec.{hpp,cpp}`, `native_execution_consumer.cpp` (`admit_opening_inspect` native_execution_consumer.cpp:3672 per side; scheduling next to `invoke_bar_open_callback` / `drain_after_applied`), `native_order.*`, `engine_state_hash.cpp` + the coverage checker, `c_abi.cpp` + `pineforge.h` (`pf_native_run_spec_v2` or an extension struct; `CLAUDE.md` SOP). ### A.4 L5 — calculation timing (F P4 + O G8, G9 + S P6) @@ -1748,10 +1748,10 @@ std::optional current_partial_bar() const; Callback chronology contract (the normative part of the lane; F P4.1 + O G8): -1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1729-1730) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:7866-7878) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). -2. Requests born in any of these callbacks follow the existing birth rule: eligible on the unconsumed rest of the bar (`born_on_remaining_path` native_execution_consumer.cpp:6439). `born_on_remaining_path` (native_execution_consumer.cpp:6439) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1961-1991) is calibrated against it. -3. `EveryModeledPoint`: a calculation at each magnifier sample / observed print, in batch as well as stream (every delivery loop asks the per-point guard native_execution_consumer.hpp:1283-1290, whose body is `recalculate_at_modeled_point` native_execution_consumer.cpp:7712). `on_native_tick` stays the observation hook (S). -4. `current_partial_bar()` derives from the driver points already recorded (`record_driver` native_execution_consumer.cpp:3224); valid in bar-open, applied and tick callbacks. +1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1729-1730) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:7934-7946) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). +2. Requests born in any of these callbacks follow the existing birth rule: eligible on the unconsumed rest of the bar (`born_on_remaining_path` native_execution_consumer.cpp:6507). `born_on_remaining_path` (native_execution_consumer.cpp:6507) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1961-1991) is calibrated against it. +3. `EveryModeledPoint`: a calculation at each magnifier sample / observed print, in batch as well as stream (every delivery loop asks the per-point guard native_execution_consumer.hpp:1283-1290, whose body is `recalculate_at_modeled_point` native_execution_consumer.cpp:7780). `on_native_tick` stays the observation hook (S). +4. `current_partial_bar()` derives from the driver points already recorded (`record_driver` native_execution_consumer.cpp:3292); valid in bar-open, applied and tick callbacks. 5. The kernel never attempts language-state rollback; that stays Pine's (`snapshot / restore / commit_coof_script_state`, pine_scheduler_native.cpp:54-106) (O). ### A.5 L6 — native HTF subscriptions (F §2.iv + O G10 + S P7; design in §2.iv) @@ -1817,7 +1817,7 @@ NativeGridRounding grid_rounding = NativeGridRounding::HalfUp; ``` - `QuantizeFills`: the kernel rounds `resolved_price` onto `price_tick` **before** slippage (`HalfUp`) or in the direction that favours the resting order (`Directional`). `QuantizeFillsAndTriggers`: the matcher additionally compares the quantized path against the raw level. -- TUs: `native_run_spec.cpp` (validation), `native_execution_consumer.cpp` (resolve path native_execution_consumer.cpp:4099-4121, `execute_current` path native_execution_consumer.cpp:7440), `native_matching.hpp` (threshold form). The dead helpers were the reference arithmetic and are deleted (§2.ii e); the kernel's own step is `native_matching::apply_slippage` native_matching.hpp:259. +- TUs: `native_run_spec.cpp` (validation), `native_execution_consumer.cpp` (resolve path native_execution_consumer.cpp:4167-4189, `execute_current` path native_execution_consumer.cpp:7508), `native_matching.hpp` (threshold form). The dead helpers were the reference arithmetic and are deleted (§2.ii e); the kernel's own step is `native_matching::apply_slippage` native_matching.hpp:259. ### A.8 L9 — risk limits (F P3 + O G7 + S P5) @@ -1836,8 +1836,8 @@ std::optional risk; // NativeRunSpec - Breach: `MatchRejectReason::RiskLimit` for openings (O; F: `RiskHalted`), an optional kernel-issued flatten through A.3's kernel-originated request (`origin = KernelRisk`, F), and a `NativeRiskEvent` in `native_events` (native_host.hpp:1335) (O) or a callback `on_native_risk_event` (S — a virtual, so only inside an epoch batch). The day key uses the existing calendar (`session_day_ordinal` native_calendar.hpp:416), not a chart clock. - S's `directions` / `max_units` / `max_position_units` are existing spec fields (MG1-MG3), not duplicated. -- Interim (F): a C++ host can build all four today from `native_marked_equity` + `on_native_applied`, which is what the adapter does (pine_adapter.cpp:17310-17318); a toolkit helper is an acceptable stand-in until L9. -- TUs: `native_run_spec.{hpp,cpp}`, `native_execution_consumer.cpp` (`admit_opening_inspect` native_execution_consumer.cpp:3604 and the per-point walk), `native_order.hpp`, the state hash. +- Interim (F): a C++ host can build all four today from `native_marked_equity` + `on_native_applied`, which is what the adapter does (pine_adapter.cpp:17312-17320); a toolkit helper is an acceptable stand-in until L9. +- TUs: `native_run_spec.{hpp,cpp}`, `native_execution_consumer.cpp` (`admit_opening_inspect` native_execution_consumer.cpp:3672 and the per-point walk), `native_order.hpp`, the state hash. ### A.9 Single-source proposals not adopted diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index b1f7523e7..3c158c39a 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -470,7 +470,7 @@ spec fields. The rich `run(bars, n, input_tf, script_tf, inputs, syminfo, overrides, …)` overload (`engine.hpp:1800-1812`) is **not** refused as a source mutation: it reaches `NativeExecutionConsumer::run_rich` -(`native_execution_consumer.cpp:10429-10471`), which admits the begin, checks the +(`native_execution_consumer.cpp:10501-10543`), which admits the begin, checks the timeframe arguments against the spec, preflights and pumps the batch exactly like the plain overload. `inputs` / `syminfo` / `overrides` are carried only as `NativeBeginArgs` fields to `prepare_native_begin` — the overrides as the @@ -638,13 +638,13 @@ a host reacts to its own execution and may submit again. A request born there, mid-bar on a continuous segment, is eligible on the **remaining path suffix** of that segment — the birth is admitted at the current cursor and the geometric search then sees only the unconsumed suffix (`born_on_remaining_path`, -`native_execution_consumer.cpp:6439-6443`). Requests accepted before the +`native_execution_consumer.cpp:6507-6511`). Requests accepted before the segment, and discrete points, keep the ordinary birth gate above. `on_native_bar_open` fires at the modeled opening, before that point's matching -pass (`native_execution_consumer.cpp:7955-7957`). **Lookahead warning:** the +pass (`native_execution_consumer.cpp:8023-8025`). **Lookahead warning:** the `Bar` it receives is the *complete* script bar — the consumer has already set -`engine.current_bar_ = open_view` (`native_execution_consumer.cpp:7428`), the +`engine.current_bar_ = open_view` (`native_execution_consumer.cpp:7496`), the complete bar unless the spec asks for `NativeOpenBarView::OpenOnly` — so its high, low and close are the finished bar's, not what is known at the open. A host that must decide on open-only information reads @@ -1496,7 +1496,7 @@ adapter answers `margin_check_allowed` with TradingView's scheduling — which includes the post-exit re-size: when a priced bracket leg of the script bar fills, the slice resting at that bar's adverse extreme was sized on the pre-exit book, and the legacy broker cancelled and re-scheduled it there -(`margin_check_allowed` `pine_adapter.cpp:17830-17863`), so the adapter admits +(`margin_check_allowed` `pine_adapter.cpp:17832-17865`), so the adapter admits the kernel's own point for that driver point while (and only while) a slice rests — `resolve_margin_requirement` with its ten-significant-digit money and @@ -2112,7 +2112,7 @@ default, set while no run is active — because each row is a full the live state, not the run's length: the closed rows enter through a running digest). With the switch on, one row follows each point, after the extremes that point just folded -(`record_script_report_point`, `native_execution_consumer.cpp:8841`), so +(`record_script_report_point`, `native_execution_consumer.cpp:8909`), so ```text broker_state_hash_len == equity_curve_len == script_bars_processed @@ -3086,7 +3086,7 @@ Only completed buckets are published, so this recipe has no lookahead by construction. It is the same class the kernel's own subscription evaluator aggregates with, and the one the kernel's `script_bucket_completions` query feeds when the Pine scheduler asks how its input span buckets -(`TimeframeAggregator` `native_execution_consumer.cpp:8925`). What it does +(`TimeframeAggregator` `native_execution_consumer.cpp:8993`). What it does **not** give you is what a declared subscription does: an `authoritative_bars` feed, the `gaps` and `lookahead` delivery rules, the lazy-seal chronology, a C spelling, and the @@ -3150,7 +3150,7 @@ These are existing refusals, not implied future features: - In-session gaps on a realtime stream, and in a `Canonical` stream warmup - Source `calc_on_every_tick` / `calc_on_order_fills` enabled (the runner rejects an explicit true override, and the Pine host refuses a stream begin - with `calc_on_order_fills`, `pine_strategy_host.cpp:291-294`). This is a + with `calc_on_order_fills`, `pine_strategy_host.cpp:295-298`). This is a **source-route** refusal, not a limit on the native hooks: `on_native_tick` and `on_native_applied` are delivered on a stream, and a native host's own `NativeRunSpec::calculation` is accepted there, where `EveryModeledPoint` diff --git a/docs/pages/pine-to-native.md b/docs/pages/pine-to-native.md index 021f93441..0d72af7ad 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -126,9 +126,9 @@ rather than defaulted. | `currency` | `currency` native_run_spec.hpp:639 | `currency` pineforge.h:499 | `hello_kernel.cpp` | With `ticker` native_run_spec.hpp:636, `tickerid` native_run_spec.hpp:637, `type` native_run_spec.hpp:638, `basecurrency` native_run_spec.hpp:640, `description` native_run_spec.hpp:641 and `volumetype` native_run_spec.hpp:642 this is Pine's whole `syminfo` block, named rather than inherited from a chart. | | `commission_type` | `NativeFeeKind` native_run_spec.hpp:19 | `fee_kind` pineforge.h:502 | `native_open_lots_strategy.cpp` | `Percent`, `CashPerUnit`, `CashPerExecution` — the same three TradingView offers. | | `commission_value` | `fee_value` native_run_spec.hpp:658 | `fee_value` pineforge.h:502 | `native_open_lots_strategy.cpp` | Quoted per execution by `quote_execution_commissions` engine_execution.cpp:1683. A cash-per-execution ticket is split by units over every slice of that execution. | -| `slippage` | `slippage_ticks` native_run_spec.hpp:652 | `slippage_ticks` pineforge.h:502 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:7008-7012. TradingView's round-then-slip order is adapter-only. | -| `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:503 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:8525-8526, so a request submitted in bar *k*'s calculation fills at bar *k*'s own close. | -| `pyramiding` | `max_open_lots` native_run_spec.hpp:664 | `max_open_lots` pineforge.h:505 | `native_open_lots_strategy.cpp` | Caps surviving+new physical lots (`resulting_lot_count` native_execution_consumer.cpp:3632). The Pine adapter keeps its own per-cycle *entry count*, ruled **adapter-policy** in ADR-0001's table row `max_open_lots`; measured against TradingView it is not TradingView's rule, and this cap is the closer one (14 of 15 tape scenarios against 13, the private test `test_pyramiding_count_differential`). Lowered onto the cap, four corpus probes move away from TradingView, for want of its first-eligible-point rule, so the adapter keeps its count. | +| `slippage` | `slippage_ticks` native_run_spec.hpp:652 | `slippage_ticks` pineforge.h:502 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:7076-7080. TradingView's round-then-slip order is adapter-only. | +| `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:503 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:8593-8594, so a request submitted in bar *k*'s calculation fills at bar *k*'s own close. | +| `pyramiding` | `max_open_lots` native_run_spec.hpp:664 | `max_open_lots` pineforge.h:505 | `native_open_lots_strategy.cpp` | Caps surviving+new physical lots (`resulting_lot_count` native_execution_consumer.cpp:3700). The Pine adapter keeps its own per-cycle *entry count*, ruled **adapter-policy** in ADR-0001's table row `max_open_lots`; measured against TradingView it is not TradingView's rule, and this cap is the closer one (14 of 15 tape scenarios against 13, the private test `test_pyramiding_count_differential`). Lowered onto the cap, four corpus probes move away from TradingView, for want of its first-eligible-point rule, so the adapter keeps its count. | | `default_qty_type` | `Sized` native_order.hpp:164 with `CashValue` native_order.hpp:91 or `EquityFraction` native_order.hpp:97 | `PF_NATIVE_INTENT_SIZED` native_c_api.h:348 with `size_basis` native_c_api.h:2147 | `native_sized_report_strategy.cpp` | The size is a property of the *request*, not of the run: a native host may size one order by cash and the next by equity fraction. `Transact` native_order.hpp:41 is the fixed-units form. Pine itself has no per-call `qty_type` — TradingView's compiler refuses one — so a script's `strategy.entry(qty = ...)` is always units; the source layer's typed per-call entry, which only generated or C++ callers reach, converts its money exactly as the declared default does. | | `default_qty_value` | the basis's own scalar: `CashValue::cash` native_order.hpp:92, `EquityFraction::fraction` native_order.hpp:98 | `intent_value` native_c_api.h:2151 | `native_sized_report_strategy.cpp` | `SizeTime` native_order.hpp:106 chooses *when* the basis resolves and `SizePrice` native_order.hpp:146 *which* price it converts at; both compose. | | `calc_on_order_fills` | `NativeCalculationTrigger` native_run_spec.hpp:94 | `calculation` native_c_api.h:2321 | `native_calc_on_fills_strategy.cpp` | `BarCloseAndFills` recalculates once at the cursor of each applied execution, bounded by `max_recalculations_per_point` native_run_spec.hpp:695. The generic cadence only: TradingView's waypoint-only refill, its two-fills-at-open rule and its script-state rollback stay in the adapter. | @@ -296,7 +296,7 @@ is `strategy_native_submit_v1` native_c_api.h:2742 over a | `strategy.oca.reduce` | `GroupEffect::Reduce` native_order.hpp:466 | `PF_NATIVE_GROUP_REDUCE` native_c_api.h:479 | `tests/test_native_resting_group_contract.cpp` | The sibling's quantity is reduced instead of cancelled. | | `strategy.oca.none` | `NoGroup` native_order.hpp:467 | `PF_NATIVE_GROUP_NONE` native_c_api.h:472 | `hello_kernel.cpp` | The default: no group, no sibling effect. | | `strategy.direction.all` | `NativeOpenDirections::Both` native_run_spec.hpp:76 | `allowed_open_directions` pineforge.h:503 | `native_risk_limits_strategy.cpp` | The default. | -| `strategy.direction.long` | `NativeOpenDirections::Long` native_run_spec.hpp:74 | `allowed_open_directions` pineforge.h:503 | `tests/test_native_run_spec.cpp` | An opening the direction forbids is refused with `OpeningDirection` native_execution_consumer.cpp:3621. | +| `strategy.direction.long` | `NativeOpenDirections::Long` native_run_spec.hpp:74 | `allowed_open_directions` pineforge.h:503 | `tests/test_native_run_spec.cpp` | An opening the direction forbids is refused with `OpeningDirection` native_execution_consumer.cpp:3689. | | `strategy.direction.short` | `NativeOpenDirections::Short` native_run_spec.hpp:75 | `allowed_open_directions` pineforge.h:503 | `tests/test_native_run_spec.cpp` | Same, the other way. | | `strategy.commission.percent` | `NativeFeeKind::Percent` native_run_spec.hpp:20 | `fee_kind` pineforge.h:502 | `native_sized_report_strategy.cpp` | `fee_value` is percent ÷ 100 of the absolute account notional. | | `strategy.commission.cash_per_contract` | `NativeFeeKind::CashPerUnit` native_run_spec.hpp:21 | `fee_kind` pineforge.h:502 | `tests/test_native_selected_requests.cpp` | Account currency per unit. | @@ -405,7 +405,7 @@ block is the `PF_NATIVE_SPEC_EXT_RISK` native_c_api.h:739 tail. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `strategy.risk.allow_entry_in()` | `allowed_open_directions` native_run_spec.hpp:665 | `allowed_open_directions` pineforge.h:503 | `native_risk_limits_strategy.cpp` | The one `strategy.risk.*` rule that is already the kernel's, per ADR-0001's `risk` ruling row. Rejects with `OpeningDirection` native_execution_consumer.cpp:3621. | +| `strategy.risk.allow_entry_in()` | `allowed_open_directions` native_run_spec.hpp:665 | `allowed_open_directions` pineforge.h:503 | `native_risk_limits_strategy.cpp` | The one `strategy.risk.*` rule that is already the kernel's, per ADR-0001's `risk` ruling row. Rejects with `OpeningDirection` native_execution_consumer.cpp:3689. | | `strategy.risk.max_position_size()` | `max_abs_units` native_run_spec.hpp:663 | `max_abs_units` pineforge.h:504 | `tests/test_native_run_spec.cpp` | Tests the *resulting* book. TradingView gates the *live* book before the fill, so an entry is refused only once the book already holds the limit — ruled **adapter-policy** in ADR-0001's `max_abs_units` row, with lane N12's measurement (the adapter ends at 4 where the kernel cap ends at 2). | | `strategy.risk.max_drawdown()` | `NativeRiskLimits::max_drawdown` native_run_spec.hpp:311 | `risk_max_drawdown` native_c_api.h:2357 | `native_risk_limits_strategy.cpp` | `NativeLossLimit` native_run_spec.hpp:259 is account currency, or a percent of the running peak when `percent` native_run_spec.hpp:261 is set. Blocks openings to the end of the run. | | `strategy.risk.max_intraday_loss()` | `NativeRiskLimits::max_intraday_loss` native_run_spec.hpp:312 | `risk_max_intraday_loss` native_c_api.h:2360 | `native_risk_limits_strategy.cpp` | Percent is of the day's opening equity. Blocks to the end of its own day. | @@ -463,7 +463,7 @@ part both ways (the private test `test_adapter_margin_schedule_differential`, M1 | `barstate.isrealtime` | `NativeStateView::phase` native_host.hpp:285 with `NativeRunPhase` native_host.hpp:41 | `pf_native_state_v1` native_c_api.h:1791 | `native_market_strategy.cpp` | `Batch`, `Warmup`, `Realtime`. | | bar-open pre-pass | `on_native_bar_open` native_host.hpp:889 | `on_bar_open` native_c_api.h:2506 | `native_calc_on_fills_strategy.cpp` | Receives the whole script bar by default; `NativeOpenBarView::OpenOnly` native_run_spec.hpp:109 masks that lookahead (H = L = C = open, volume 0) for a host that must decide at the open with open-only information. | | the bar so far, mid-bar | `current_partial_bar` native_host.hpp:1073 | `strategy_native_partial_bar_v1` native_c_api.h:3039 | `native_calc_on_fills_strategy.cpp` | Never runs ahead of the cursor: at a fill inside a leg it holds the points already reached and never the later high. | -| a fill, as it happens | `on_native_applied` native_host.hpp:938 | `on_applied` native_c_api.h:2509 | `native_calc_on_fills_strategy.cpp` | Fires mid-path after each fill; a request born there is eligible on the unconsumed rest of the bar (`born_on_remaining_path` native_execution_consumer.cpp:6439-6443). | +| a fill, as it happens | `on_native_applied` native_host.hpp:938 | `on_applied` native_c_api.h:2509 | `native_calc_on_fills_strategy.cpp` | Fires mid-path after each fill; a request born there is eligible on the unconsumed rest of the bar (`born_on_remaining_path` native_execution_consumer.cpp:6507-6511). | | raw input before aggregation | `on_native_input` native_host.hpp:875 | `on_input` native_c_api.h:2504 | `native_auxiliary_feed_strategy.cpp` | Every accepted confirmed input bar, before aggregation or matching. | | a lower-timeframe sub-bar | `on_native_sub_bar` native_host.hpp:928 | `on_sub_bar` native_c_api.h:2545 | `tests/test_native_calc_timing.cpp` | `NativeCalculationReason::SubBar` is reserved and never delivered to the recalculate hook — a sub-bar has its own callback. | | `time`, `bar_index`, the bar's calendar interval, session facts | `NativeDecisionContext` market_driver.hpp:120 with `NativeCoordinate` market_driver.hpp:72 | `pf_native_decision_v1` native_c_api.h:1485 | `hello_kernel.cpp` | `NativeCoordinate::interval_index` is the sequential SCRIPT-bar index on every run, including aggregation; `input_interval_index` names the input slot for a host that needs input cadence. Both are copied onto the callback stack, and mutating them changes nothing. In C++ `script_interval` market_driver.hpp:124 and `input_interval` market_driver.hpp:123 add the calendar facts (eligible open, last traded close, next period and next input opens); the C decision carries the script interval's five instants and the session days of its open and of the next input in its tail, presented to a callback table at `PF_NATIVE_CALLBACKS_V1_POLICY_SIZE` or later, and no input interval. The C `on_input` callback receives the input index separately. The session-day facts are the three rows below. | From ddc9c94c9d578b83ddca68930bb7e8af3f3beb0c Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sun, 18 Oct 2026 21:24:22 +0800 Subject: [PATCH 05/35] test(cr150): keep FX index pins and expect the L15a tail Round 4, lab rj-20261018t124444-007ef1: update only NativeFxCurveVersions.test_new_paths_append_without_reindexing_existing_pins. Retain FX at FILES[12:14], the identity pin at 11 and both uniqueness checks; expect the seven L15a paths in FILES[14:]. Comparison with core/cr150-p00-contracts (ee3083d4a) confirms its full 14-entry FILES prefix is already unchanged, so do not reorder or rewrite the guard list. Follow the FX append precedent from e7cdf052f without preserving its stale newest-tail assertion. No build, compilation, test or guard executed here. --- scripts/test_native_cpp_versions.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/scripts/test_native_cpp_versions.py b/scripts/test_native_cpp_versions.py index 6d2df3048..0b3dcd737 100644 --- a/scripts/test_native_cpp_versions.py +++ b/scripts/test_native_cpp_versions.py @@ -906,7 +906,16 @@ def reject(self, path, before, after): check_texts(changed) def test_new_paths_append_without_reindexing_existing_pins(self): - self.assertEqual(FILES[-2:], (NATIVE_FX_CURVE_HEADER, NATIVE_FX_CURVE_SOURCE)) + self.assertEqual(FILES[12:14], (NATIVE_FX_CURVE_HEADER, NATIVE_FX_CURVE_SOURCE)) + self.assertEqual(FILES[14:], ( + 'include/pineforge/native_bar_views.hpp', + 'src/native_bar_views.cpp', + 'src/source/pine_security_eval.cpp', + 'src/source/pine_strategy_host.cpp', + 'src/source/pine_adapter.cpp', + 'src/source/pine_scheduler_native.cpp', + 'src/source/magnifier_intrabars.cpp', + )) self.assertEqual(FILES[11], 'include/pineforge/native_order_identity.hpp') self.assertEqual(FILES.count(NATIVE_FX_CURVE_HEADER), 1) self.assertEqual(FILES.count(NATIVE_FX_CURVE_SOURCE), 1) From 545c55a29d1cd06ca0ed5dc0f528503f76084510 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sun, 18 Oct 2026 23:26:16 +0800 Subject: [PATCH 06/35] test: specify CR-150 heap census and unique-id controls --- tests/CMakeLists.txt | 8 + .../fixtures/flat_memory/unique_entry_id.pine | 17 ++ tests/test_heap_census.py | 88 ++++++++ tests/test_heap_census_unique_ids.cpp | 198 ++++++++++++++++++ 4 files changed, 311 insertions(+) create mode 100644 tests/fixtures/flat_memory/unique_entry_id.pine create mode 100644 tests/test_heap_census.py create mode 100644 tests/test_heap_census_unique_ids.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d11ecee74..971f05651 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -7087,3 +7087,11 @@ pineforge_add_unit_test(test_cr150_contract_headers) target_sources(test_cr150_contract_headers PRIVATE test_cr150_contract_headers_c.c) set_target_properties(test_cr150_contract_headers PROPERTIES C_STANDARD 11 C_STANDARD_REQUIRED ON C_EXTENSIONS OFF) + +# CR-150 L0B: append only; private_fixture_reads.evidence pins earlier lines. +add_test(NAME test_heap_census + COMMAND ${Python3_EXECUTABLE} -B ${CMAKE_CURRENT_SOURCE_DIR}/test_heap_census.py) +if(PINEFORGE_BUILD_SOURCE_LAYER) + list(APPEND TEST_SOURCES test_heap_census_unique_ids) + pineforge_add_unit_test(test_heap_census_unique_ids) +endif() diff --git a/tests/fixtures/flat_memory/unique_entry_id.pine b/tests/fixtures/flat_memory/unique_entry_id.pine new file mode 100644 index 000000000..2dff3f9c4 --- /dev/null +++ b/tests/fixtures/flat_memory/unique_entry_id.pine @@ -0,0 +1,17 @@ +//@version=6 +strategy("CR-150 unique entry ids", initial_capital=1000000, pyramiding=1, process_orders_on_close=true) +// Q9: keep bars, trade count and id byte length equal in both populations. +// No strategy.closedtrades.* or strategy.opentrades.* read is used. +uniqueIds = input.bool(true, "unique_ids") +longIds = input.bool(false, "long_ids") +every = input.int(4, "every", minval=2) +limit = input.int(10000, "trades", minval=1) +var int opened = 0 +var string active = "" +if bar_index % every == 0 and opened < limit + ordinal = uniqueIds ? opened : 0 + active := (longIds ? "census-entry-" : "E") + str.tostring(ordinal, "00000000000000") + strategy.entry(active, strategy.long, qty=1) + opened += 1 +if bar_index % every == 1 + strategy.close(active) diff --git a/tests/test_heap_census.py b/tests/test_heap_census.py new file mode 100644 index 000000000..68356f063 --- /dev/null +++ b/tests/test_heap_census.py @@ -0,0 +1,88 @@ +"""L0B: the census must count live bytes once and never hide an unknown slope. + +Written before heap_census.py. The lab also runs the real battery on the base: +its red result must be retained engine allocations, not a missing sink flag. +These controls test the evidence reader, not the engine's memory contract. +""" +import unittest + +import heap_census as census + + +def massif(rows, total): + return ("desc: --tool=massif\ncmd: fixture\ntime_unit: B\n" + "#-----------\nsnapshot=0\n#-----------\ntime=42\n" + f"mem_heap_B={total}\nmem_heap_extra_B=16\nmem_stacks_B=0\n" + "heap_tree=detailed\n" + rows) + + +class CensusEvidence(unittest.TestCase): + def test_only_leaves_are_counted_and_addresses_do_not_name_sites(self): + tree = ("n2: 96 (heap allocation functions) malloc/new/new[]\n" + " n1: 64 0x123: operator new(unsigned long) (new_op.cc:95)\n" + " n0: 64 0x234: pineforge::source::OriginRoster::push_back() (a.hpp:950)\n" + " n0: 32 0x345: unrecognized_cache() (cache.cpp:7)\n") + snap = census.parse_massif(massif(tree, 96)) + self.assertEqual(sum(snap.sites.values()), 96) + self.assertEqual(len(snap.sites), 2) + changed = tree.replace("0x123", "0xFFF").replace("a.hpp:950", "a.hpp:999") + self.assertEqual(snap.sites, census.parse_massif(massif(changed, 96)).sites) + self.assertEqual(snap.heap_bytes, 96) + + def test_a_growing_unknown_is_L19j_and_is_never_admitted(self): + low = census.Snapshot(10, {"new_cache()": 10}) + high = census.Snapshot(50, {"new_cache()": 50}) + rows = census.growth(low, high, 10, 30) + self.assertEqual(len(rows), 1) + self.assertEqual(rows[0]["lane"], "L19j") + self.assertFalse(rows[0]["admitted"]) + self.assertEqual(rows[0]["bytes_per_unit"], 2.0) + self.assertFalse(census.flat(rows)) + + def test_only_the_callers_explicit_ohlcv_allocation_is_admitted(self): + low = census.Snapshot(48, {"census_chart_ohlcv(int)": 48}) + high = census.Snapshot(192, {"census_chart_ohlcv(int)": 192}) + self.assertTrue(census.flat(census.growth(low, high, 1, 4))) + copied = "pineforge::source::PineScheduler::capture_begin()" + self.assertEqual(census.owner(copied), ("L3", False)) + self.assertEqual(census.owner("input_next_ms_"), ("L19i (or L3/L4)", False)) + self.assertEqual(census.owner("something_else (pine_adapter.cpp)"), + ("L19j", False)) + + def test_no_stack_or_unexpanded_tree_cannot_pass(self): + for text in ("", massif("", 40), + massif("n0: 39 (heap allocation functions)\n", 40), + massif("n1: 40 (heap allocation functions)\n", 40), + massif("n0: 40 (heap allocation functions)\n", 40)): + with self.subTest(text=text), self.assertRaises(ValueError): + census.parse_massif(text) + hidden = census.parse_massif(massif( + "n1: 40 (heap allocation functions)\n" + " n0: 40 in 1 place, below massif's threshold\n", 40)) + self.assertFalse(census.flat(census.growth(census.Snapshot(0, {}), hidden, 1, 4))) + + def test_broken_tree_and_duplicate_snapshot_are_refused(self): + broken = "n1: 40 (heap allocation functions)\n n0: 50 cache()\n" + with self.assertRaises(ValueError): + census.parse_massif(massif(broken, 40)) + text = massif("n1: 40 (heap allocation functions)\n n0: 40 cache()\n", 40) + with self.assertRaises(ValueError): + census.parse_massif(text + text) + + def test_q9_subtracts_the_same_trade_work_at_both_rungs(self): + # Both cases pay 100 B/trade. Unique ids pay 20 B/additional id. + reused_low = census.Snapshot(1000, {"trade": 1000}) + unique_low = census.Snapshot(1180, {"trade": 1000, "ids": 180}) + reused_high = census.Snapshot(1000000, {"trade": 1000000}) + unique_high = census.Snapshot(1199980, {"trade": 1000000, "ids": 199980}) + result = census.id_cost(reused_low, unique_low, reused_high, unique_high, 10, 10000) + self.assertEqual(result["bytes_per_additional_id"], 20.0) + self.assertEqual(result["denominator_ids"], 9990) + self.assertEqual(result["sites"][0]["site"], "ids") + self.assertEqual(result["sites"][0]["lane"], "L19j") + with self.assertRaises(ValueError): + census.id_cost(reused_low, unique_low, reused_high, unique_high, 10, 10) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_heap_census_unique_ids.cpp b/tests/test_heap_census_unique_ids.cpp new file mode 100644 index 000000000..888b79c8f --- /dev/null +++ b/tests/test_heap_census_unique_ids.cpp @@ -0,0 +1,198 @@ +// CR-150 L0B. A live Pine host, not a model of its id tables. The profile +// checkpoint is BEFORE host destruction and BEFORE allocating a report copy. +// No closedtrades/opentrades reads occur in on_source_bar. Normal ctest checks +// the fixture's fills and ids; heap_census.py checks its retained-byte slope. +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { +using namespace pineforge; +constexpr double kNa = std::numeric_limits::quiet_NaN(); +constexpr std::int64_t kStart = 1736121600000LL; + +void require(bool ok, const char* why) { + if (!ok) throw std::runtime_error(why); +} + +std::string entry_id(int ordinal, bool long_id) { + char digits[32]; + // Equal byte lengths in the unique and reused controls, including when + // ordinal crosses a power of ten. Short ids fit libstdc++'s SSO buffer; + // the second population deliberately crosses it. + std::snprintf(digits, sizeof digits, "%014d", ordinal); + return std::string(long_id ? "census-entry-" : "E") + digits; +} + +// Kept out of line so only this caller-owned OHLCV allocation is admissible +// in the Massif reader. An engine copy must have another allocation stack. +[[gnu::noinline]] std::vector census_chart_ohlcv(int n, bool daily) { + std::vector bars(static_cast(n)); + const std::int64_t step = daily ? 86400000LL : 60000LL; + for (int i = 0; i < n; ++i) + bars[static_cast(i)] = + {100.0, 101.0, 99.0, 100.0, 1000.0, kStart + i * step}; + return bars; +} + +class CensusHost final : public source::PineStrategyHost { +public: + CensusHost(std::string mode, int trades, int stride, int period, bool long_id) + : mode_(std::move(mode)), limit_(trades), stride_(stride), period_(period), + long_id_(long_id) { + source::PineStrategyConfig config; + config.initial_capital = 1000000.0; + config.default_qty_type = static_cast(QtyType::FIXED); + config.default_qty_value = 1.0; + config.pyramiding = mode_ == "pyramiding" ? 4 : 1; + config.process_orders_on_close = true; + config.commission_value = 0.0; + configure_pine_strategy(config); + set_syminfo_timezone("UTC"); + set_syminfo_session("24x7"); + set_syminfo_mintick(0.25); + // Keep recording OFF: eager admission folding must also work on the + // production path, which never asks for a broker hash array. + if (mode_ == "routed_security") register_security_eval(0, "60", "1", false, false); + if (mode_ == "nday_security") register_security_eval(0, "3D", "1D", false, false); + } + + void evaluate_security(int, const Bar&, bool) override { ++evaluations_; } + + void on_source_bar(const Bar& bar) override { + ++callbacks_; + const int i = pine_bar_index(); + if (mode_ == "moved_exits") { + if (i == 0) strategy_entry("L", true, kNa, kNa, 1.0); + if (live_position_size() > 0.0) { + const double distance = 10.0 + static_cast(i % 7); + strategy_exit("guard", "L", bar.close + distance, bar.close - distance); + ++exit_calls_; + } + return; // Deliberately hold the origin for the whole K1 run. + } + if (mode_ == "oca_brackets") { + if (i % 4 == 0) strategy_entry("L", true, kNa, kNa, 2.0); + if (live_position_size() > 0.0) { + strategy_exit("pair", "L", 100.5, 99.5); + ++exit_calls_; + } + return; + } + if (mode_ == "pyramiding") { + if (i % 8 < 4) strategy_entry("L", true, kNa, kNa, 1.0); + if (i % 8 == 6) strategy_close_all(); + return; + } + if (mode_ == "partial_exits") { + if (i % 6 == 0) strategy_entry("L", true, kNa, kNa, 4.0); + if (i % 6 == 1) { + strategy_exit("half", "L", 100.5, 99.5, kNa, kNa, kNa, 50.0); + ++exit_calls_; + } + if (i % 6 == 4) strategy_close_all(); + return; + } + // This path backs both Q9 populations. Only period_ changes: + // 1 reuses one id; 0 uses a new id; 31 tests an old id recurring. + if (i % stride_ == 0 && opened_ < limit_) { + active_ = entry_id(period_ == 0 ? opened_ : opened_ % period_, long_id_); + strategy_entry(active_, true, kNa, kNa, 1.0); + ++opened_; + } + if (i % stride_ == 1 && live_position_size() > 0.0) strategy_close(active_); + } + + void hash_host_extension(BrokerStateHashSink& sink) const override { + source::PineStrategyHost::hash_host_extension(sink); + sink.s("cr150-l0b/v1"); + sink.s(mode_); sink.i(limit_); sink.i(stride_); sink.i(period_); + sink.i(long_id_ ? 1 : 0); sink.s(active_); sink.i(opened_); + sink.i(callbacks_); sink.i(evaluations_); sink.i(exit_calls_); + } + + int opened() const { return opened_; } + int evaluations() const { return evaluations_; } + int exit_calls() const { return exit_calls_; } + int callbacks() const { return callbacks_; } + +private: + std::string mode_, active_; + int limit_, stride_, period_; + bool long_id_; + int opened_ = 0, callbacks_ = 0, evaluations_ = 0, exit_calls_ = 0; +}; + +void run_case(const std::string& mode, int bars_count, int trades, int period, + bool long_id, bool wait) { + require(bars_count >= 16 && trades > 0, "invalid census population"); + const bool split = mode == "split_magnifier"; + const bool daily = mode == "nday_security"; + const int script_bars = split ? bars_count / 15 : bars_count; + require(script_bars >= trades * 2, "population needs two script bars per trade"); + const int stride = std::max(2, script_bars / trades); + const auto bars = census_chart_ohlcv(bars_count, daily); + CensusHost host(mode, trades, stride, period, long_id); + host.run(bars.data(), bars_count, daily ? "1D" : "1", split ? "15" : (daily ? "1D" : "1"), + split, 4, MagnifierDistribution::ENDPOINTS); + require(host.last_error().empty(), host.last_error().c_str()); + require(host.callbacks() > 0, "fixture never calculated"); + if (mode == "routed_security" || mode == "nday_security") + require(host.evaluations() > 0, "security fixture never evaluated"); + if (mode == "moved_exits" || mode == "oca_brackets" || mode == "partial_exits") + require(host.exit_calls() > 1, "exit fixture did not reissue"); + // Validation reads are outside script execution. They make no growing + // fixture-owned collection and allocate no report/trade copy. + const bool exact = mode == "unique_ids" || mode == "reused_id"; + if (exact) { + require(host.opened() == trades, "entry population was not reached"); + require(host.trade_count() == trades, "closed-trade population was not reached"); + for (int i = 0; i < trades; ++i) { + const Trade& row = host.get_trade(i); + require(row.entry_id == entry_id(period == 0 ? i : i % period, long_id), + "entry identity changed"); + require(row.qty == 1.0 && !row.open_at_end, "fixture did not close a whole lot"); + } + } else if (mode != "moved_exits") { + require(host.trade_count() > 0, "battery fixture produced no closed rows"); + } + const int ids = host.opened() == 0 ? 1 : + (period == 0 ? host.opened() : std::min(host.opened(), period)); + std::printf("PF_HEAP_CENSUS {\"bars\":%d,\"trades\":%d,\"ids\":%d," + "\"id_bytes\":%zu,\"callbacks\":%d,\"security_evaluations\":%d," + "\"exit_calls\":%d,\"phase\":\"native_return\"}\n", + bars_count, host.trade_count(), ids, entry_id(0, long_id).size(), + host.callbacks(), host.evaluations(), host.exit_calls()); + std::fflush(stdout); + if (wait) require(std::getchar() == '\n', "profiler did not release checkpoint"); +} +} // namespace + +int main(int argc, char** argv) { + try { + if (argc == 1) { + for (const bool long_id : {false, true}) + for (const int period : {0, 1, 31}) + run_case(period == 1 ? "reused_id" : "unique_ids", 256, 40, + period, long_id, false); + } else { + require(argc == 8, "usage: test_heap_census_unique_ids MODE BARS TRADES PERIOD LONG_ID --wait yes"); + require(std::string(argv[6]) == "--wait" && std::string(argv[7]) == "yes", + "profiling requires an acknowledged live checkpoint"); + run_case(argv[1], std::stoi(argv[2]), std::stoi(argv[3]), std::stoi(argv[4]), + std::stoi(argv[5]) != 0, true); + } + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "heap census fixture: %s\n", error.what()); + return 1; + } +} From d84c1d294687b3474d0b249c65521f226a73f713 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sun, 18 Oct 2026 23:37:40 +0800 Subject: [PATCH 07/35] test(cr150): add L15b borrowed feed witness --- tests/CMakeLists.txt | 4 + tests/test_native_bar_views_l15b.cpp | 226 +++++++++++++++++++++++++++ 2 files changed, 230 insertions(+) create mode 100644 tests/test_native_bar_views_l15b.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index be69219ab..81c185081 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -7095,3 +7095,7 @@ pineforge_add_unit_test(test_native_bar_views_l15a) add_test(NAME test_native_bar_views_guard_l15a COMMAND ${Python3_EXECUTABLE} ${PROJECT_SOURCE_DIR}/scripts/check_native_cpp_versions.py --self-test-bar-views) + +# CR-150 L15b: append after every existing line-pinned registration. +list(APPEND TEST_SOURCES test_native_bar_views_l15b) +pineforge_add_unit_test(test_native_bar_views_l15b) diff --git a/tests/test_native_bar_views_l15b.cpp b/tests/test_native_bar_views_l15b.cpp new file mode 100644 index 000000000..7042e48c0 --- /dev/null +++ b/tests/test_native_bar_views_l15b.cpp @@ -0,0 +1,226 @@ +// CR-150 L15b: borrowed symbol-feed export and host storage. +// The conditional declaration keeps the first witness linkable against the +// L15a tree: before the implementation, the missing runtime symbol is the +// intended red edge. +#include +#include + +#include +#include +#include +#include +#include + +#if !defined(PINEFORGE_HAS_SYMBOL_FEED_BORROWED_V1) +extern "C" int strategy_set_symbol_feed_borrowed_v1( + pf_strategy_t, const char*, const char*, const pf_bar_t*, + const std::int64_t*, int); +#endif + +namespace { + +int checks = 0; +int failures = 0; +const char* scenario = "initialization"; + +#define CHECK(expression) \ + do { \ + ++checks; \ + if (!(expression)) { \ + ++failures; \ + std::printf("FAIL [%s] line %d: %s\n", scenario, __LINE__, \ + #expression); \ + } \ + } while (false) + +constexpr std::int64_t kMinute = 60000; +constexpr std::int64_t kStart = 1744032600000LL; +const char* kSymbol = "SYN:BORROWED"; +const char* kTimeframe = "1"; + +struct Row { + double close = std::nan(""); + double time = std::nan(""); + double column = std::nan(""); +}; + +struct Feed { + std::vector bars; + std::vector close_ms; + std::vector column; +}; + +class Reader final : public pineforge::source::PineStrategyHost { +public: + std::vector rows; + Row value; + + void configure_security_evaluators() override { + security_eval_states_.clear(); + value = Row{}; + register_security_eval(0, kSymbol, kTimeframe, input_tf_, false, false, false); + } + + void evaluate_security(int, const pineforge::Bar& bar, bool) override { + value.close = bar.close; + value.time = static_cast(bar.timestamp); + value.column = security_column_value(0, "carry"); + } + + void clear_security(int) override { value = Row{}; } + + void on_source_bar(const pineforge::Bar&) override { rows.push_back(value); } +}; + +pf_strategy_t handle(pineforge::BacktestEngine& engine) { + return static_cast(&engine); +} + +void prime(Reader& reader) { + reader.set_syminfo_timezone("Etc/UTC"); + reader.set_syminfo_session("24x7"); + reader.set_syminfo_type("crypto"); + reader.set_syminfo_string("tickerid", "SYN:CHART"); + reader.set_syminfo_string("ticker", "CHART"); +} + +std::vector chart() { + std::vector bars; + for (int i = 0; i < 6; ++i) { + const double open = 100.0 + static_cast(i); + bars.push_back({open, open + 0.5, open - 0.5, open + 0.25, + 1000.0 + static_cast(i), kStart + i * kMinute}); + } + return bars; +} + +Feed feed(int shift = 0) { + Feed result; + for (int i = 0; i < 6; ++i) { + const std::int64_t open = kStart + (i - 1) * kMinute; + const double close = 40.0 + static_cast(i + shift); + result.bars.push_back({close - 0.25, close + 0.5, close - 0.5, close, + std::nan(""), open}); + result.close_ms.push_back(open + kMinute); + result.column.push_back(10.0 + static_cast(i + shift)); + } + return result; +} + +bool install_copied(Reader& reader, const Feed& tape) { + const pf_strategy_t strategy = handle(reader); + return strategy_set_symbol_feed(strategy, kSymbol, kTimeframe, + tape.bars.data(), tape.close_ms.data(), + static_cast(tape.bars.size())) == 0 + && strategy_set_symbol_feed_column(strategy, kSymbol, kTimeframe, "carry", + tape.column.data(), + static_cast(tape.column.size())) == 0; +} + +bool install_borrowed(Reader& reader, const Feed& tape) { + const pf_strategy_t strategy = handle(reader); + return strategy_set_symbol_feed_borrowed_v1( + strategy, kSymbol, kTimeframe, tape.bars.data(), tape.close_ms.data(), + static_cast(tape.bars.size())) == 0 + && strategy_set_symbol_feed_column(strategy, kSymbol, kTimeframe, "carry", + tape.column.data(), + static_cast(tape.column.size())) == 0; +} + +bool run(Reader& reader, const std::vector& bars) { + reader.rows.clear(); + reader.run(bars.data(), static_cast(bars.size()), kTimeframe, kTimeframe, + false, 4, pineforge::MagnifierDistribution::ENDPOINTS); + return reader.last_error().empty(); +} + +bool same(double left, double right) { + if (std::isnan(left) || std::isnan(right)) return std::isnan(left) && std::isnan(right); + return left == right; +} + +void copied_and_borrowed_are_equal() { + scenario = "copied versus borrowed"; + const Feed tape = feed(); + const std::vector bars = chart(); + Reader copied; + Reader borrowed; + prime(copied); + prime(borrowed); + CHECK(install_copied(copied, tape)); + CHECK(install_borrowed(borrowed, tape)); + CHECK(run(copied, bars)); + CHECK(run(borrowed, bars)); + CHECK(copied.rows.size() == borrowed.rows.size()); + for (std::size_t i = 0; i < copied.rows.size() && i < borrowed.rows.size(); ++i) { + CHECK(same(copied.rows[i].close, borrowed.rows[i].close)); + CHECK(same(copied.rows[i].time, borrowed.rows[i].time)); + CHECK(same(copied.rows[i].column, borrowed.rows[i].column)); + } + CHECK(copied.native_continuation_hash() == borrowed.native_continuation_hash()); + const auto copied_state = copied.native_state(); + const auto borrowed_state = borrowed.native_state(); + CHECK(copied_state.spec != nullptr && borrowed_state.spec != nullptr); + if (copied_state.spec != nullptr && borrowed_state.spec != nullptr) { + CHECK(copied_state.spec->instrument_feeds.size() == 1); + CHECK(borrowed_state.spec->instrument_feeds.size() == 1); + CHECK(copied_state.spec->instrument_feeds[0].bars.size() == tape.bars.size()); + CHECK(borrowed_state.spec->instrument_feeds[0].bars.empty()); + CHECK(borrowed_state.spec->instrument_feeds[0].close_ms.empty()); + } +} + +void replacement_and_empty_feed_keep_lifetime_rules() { + scenario = "replacement and empty borrowed feed"; + const Feed first = feed(); + const Feed replacement = feed(100); + const std::vector bars = chart(); + Reader reader; + prime(reader); + CHECK(install_borrowed(reader, first)); + CHECK(run(reader, bars)); + CHECK(!reader.rows.empty() && same(reader.rows.front().close, first.bars[1].close)); + CHECK(install_borrowed(reader, replacement)); + CHECK(run(reader, bars)); + CHECK(!reader.rows.empty() && same(reader.rows.front().close, replacement.bars[1].close)); + CHECK(strategy_set_symbol_feed_borrowed_v1(handle(reader), kSymbol, kTimeframe, + nullptr, nullptr, 0) == 0); + CHECK(run(reader, bars)); + for (const Row& row : reader.rows) { + CHECK(std::isnan(row.close)); + CHECK(std::isnan(row.column)); + } +} + +void malformed_arguments_are_refused_before_install() { + scenario = "borrowed argument refusals"; + Reader reader; + prime(reader); + const Feed tape = feed(); + const pf_strategy_t strategy = handle(reader); + CHECK(strategy_set_symbol_feed_borrowed_v1(nullptr, kSymbol, kTimeframe, + tape.bars.data(), tape.close_ms.data(), + static_cast(tape.bars.size())) == -1); + CHECK(strategy_set_symbol_feed_borrowed_v1(strategy, kSymbol, kTimeframe, + nullptr, tape.close_ms.data(), 1) == -1); + CHECK(strategy_set_symbol_feed_borrowed_v1(strategy, kSymbol, kTimeframe, + tape.bars.data(), nullptr, 1) == -1); + CHECK(strategy_set_symbol_feed_borrowed_v1(strategy, kSymbol, kTimeframe, + tape.bars.data(), tape.close_ms.data(), -1) == -1); + pf_bar_t invalid = tape.bars.front(); + invalid.high = std::nan(""); + CHECK(strategy_set_symbol_feed_borrowed_v1(strategy, kSymbol, kTimeframe, + &invalid, tape.close_ms.data(), 1) == -1); + CHECK(std::string(reader.last_error()).find("strategy_set_symbol_feed_borrowed_v1") + != std::string::npos); +} + +} // namespace + +int main() { + copied_and_borrowed_are_equal(); + replacement_and_empty_feed_keep_lifetime_rules(); + malformed_arguments_are_refused_before_install(); + std::printf("test_native_bar_views_l15b: %d checks, %d failures\n", checks, failures); + return failures == 0 ? 0 : 1; +} From d61e68b9efff7db33e00f03a8957869e4c0b74c6 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sun, 18 Oct 2026 23:43:56 +0800 Subject: [PATCH 08/35] test: add live CR-150 heap census and lane inventory --- tests/CMakeLists.txt | 2 + .../flat_memory/heap_census_battery.json | 15 + tests/heap_census.py | 561 ++++++++++++++++++ tests/heap_census_report.md | 195 ++++++ tests/test_heap_census.py | 36 ++ tests/test_heap_census_unique_ids.cpp | 86 ++- 6 files changed, 888 insertions(+), 7 deletions(-) create mode 100644 tests/fixtures/flat_memory/heap_census_battery.json create mode 100644 tests/heap_census.py create mode 100644 tests/heap_census_report.md diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 971f05651..473b2c9fe 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -7094,4 +7094,6 @@ add_test(NAME test_heap_census if(PINEFORGE_BUILD_SOURCE_LAYER) list(APPEND TEST_SOURCES test_heap_census_unique_ids) pineforge_add_unit_test(test_heap_census_unique_ids) + target_link_libraries(test_heap_census_unique_ids PRIVATE ${CMAKE_DL_LIBS}) + set_target_properties(test_heap_census_unique_ids PROPERTIES ENABLE_EXPORTS ON) endif() diff --git a/tests/fixtures/flat_memory/heap_census_battery.json b/tests/fixtures/flat_memory/heap_census_battery.json new file mode 100644 index 000000000..249f8e8f9 --- /dev/null +++ b/tests/fixtures/flat_memory/heap_census_battery.json @@ -0,0 +1,15 @@ +{ + "schema": "pineforge-heap-census-battery/v1", + "native_modes": [ + "reused_id", + "moved_exits", + "oca_brackets", + "pyramiding", + "partial_exits", + "split_magnifier", + "split_no_magnifier", + "routed_security", + "nday_security", + "foreign_security" + ] +} diff --git a/tests/heap_census.py b/tests/heap_census.py new file mode 100644 index 000000000..7539abe27 --- /dev/null +++ b/tests/heap_census.py @@ -0,0 +1,561 @@ +#!/usr/bin/env python3 +"""CR-150 L0B: live allocation-site census, for the Linux lab only. + +No builds, code generation, feed derivation or corpus writes. Prerequisites: +a fresh complete build, Valgrind/Massif + vgdb, and prebuilt corpus modules. +Use --battery tests/fixtures/flat_memory --corpus corpus. Raw snapshots, +commands, module hashes, measured populations and every growing stack are kept +under --output (a NEW directory). Missing proof exits 2; non-flat proof exits 1. +Caller OHLCV and explicitly declared closed-trade history are the only admissions. +""" +from __future__ import annotations + +import argparse +import ctypes +import hashlib +import json +import mmap +import os +from pathlib import Path +import re +import selectors +import shutil +import subprocess +import sys +import time +from dataclasses import dataclass + +ROOT = Path(__file__).resolve().parents[1] +MARKER = "PF_HEAP_CENSUS " +SCHEMA = "pineforge-heap-census/v1" + + +@dataclass +class Snapshot: + heap_bytes: int + sites: dict[str, int] + + +def _frame(text: str) -> str: + text = re.sub(r"\b0x[0-9a-fA-F]+:\s*", "", text) + text = re.sub(r"(\.(?:cpp|hpp|cc|c|h)):\d+(?::\d+)?", r"\1", text) + return text.replace(str(ROOT) + "/", "").strip() + + +def parse_massif(text: str) -> Snapshot: + """One explicit detailed_snapshot; automatic/peak/last snapshots are not evidence. + + Walk the complete tree, counting each allocation only at its leaf. Reject + truncated trees and unaccounted bytes instead of silently losing a site. + Threshold/depth leaves remain visible and fail the gate as L19j. + """ + if len(re.findall(r"^snapshot=\d+$", text, re.M)) != 1: + raise ValueError("expected exactly one checkpoint snapshot") + totals = re.findall(r"^mem_heap_B=(\d+)$", text, re.M) + if len(totals) != 1 or "heap_tree=detailed\n" not in text: + raise ValueError("checkpoint has no detailed heap tree") + nodes = [] + for line in text.splitlines(): + match = re.fullmatch(r"( *)n(\d+): (\d+) (.+)", line) + if match: + indent, children, size, label = match.groups() + nodes.append((len(indent), int(children), int(size), _frame(label))) + if not nodes or nodes[0][1] == 0: + raise ValueError("unexpanded allocation root") + sites: dict[str, int] = {} + + def walk(index: int, path: tuple[str, ...], parent_indent: int) -> tuple[int, int]: + if index >= len(nodes): + raise ValueError("truncated allocation tree") + indent, children, size, label = nodes[index] + if indent <= parent_indent: + raise ValueError("invalid allocation-tree nesting") + index += 1 + stack = path + (label,) + if children == 0: + site = " <- ".join(stack[1:]) + sites[site] = sites.get(site, 0) + size + else: + child_bytes = 0 + for _ in range(children): + index, count = walk(index, stack, indent) + child_bytes += count + if child_bytes != size: + raise ValueError("allocation-tree children do not sum to parent") + return index, size + + end, total = walk(0, (), -1) + if end != len(nodes) or total != int(totals[0]) or sum(sites.values()) != total: + raise ValueError("snapshot live-byte total disagrees with allocation tree") + return Snapshot(total, sites) + + +# Attribution is deliberately symbol-specific. A whole-file match would hide +# a newly discovered adapter allocation under an already assigned lane. +# Unknown, ambiguous, stripped and threshold leaves all go to L19j. +OWNERS = ( + ("L19d", ("OriginRoster::push_back", "OriginRoster::positions_")), + ("L19h", ("BracketRoster::", "PlacementTable::unfolded_")), + ("L19e", ("CommandObservation", "admission::Journal::append", "admission::Journal::reserve")), + ("L19c", ("vector tuple[str, bool]: + if "below massif's threshold" in site or "???" in site: + return "L19j", False + if "census_chart_ohlcv(" in site or "census_feed_close_ms(" in site: + return "caller OHLCV", True + for lane, symbols in OWNERS: + if any(symbol in site for symbol in symbols): + return lane, False + return "L19j", False + + +def growth(low: Snapshot, high: Snapshot, low_units: int, high_units: int) -> list[dict]: + if high_units <= low_units: + raise ValueError("comparison did not increase its measured population") + rows = [] + for site in sorted(low.sites.keys() | high.sites.keys()): + before, after = low.sites.get(site, 0), high.sites.get(site, 0) + if after <= before: + continue + lane, admitted = owner(site) + rows.append(dict(site=site, lane=lane, admitted=admitted, + low_bytes=before, high_bytes=after, delta_bytes=after - before, + bytes_per_unit=(after - before) / (high_units - low_units))) + return sorted(rows, key=lambda row: (-row["delta_bytes"], row["site"])) + + +def flat(rows: list[dict]) -> bool: + return all(row["admitted"] for row in rows) + + +def id_cost(reused_low: Snapshot, unique_low: Snapshot, + reused_high: Snapshot, unique_high: Snapshot, + low_ids: int, high_ids: int) -> dict: + """Signed difference of differences. Ordinary trade work cancels, not just id strings. + + Both reused populations have ONE distinct id. Denominator is therefore + (high_ids - 1) - (low_ids - 1). Keep negative sites too: per-id layout and + capacity effects can offset a positive site and must not disappear. + """ + if high_ids <= low_ids: + raise ValueError("Q9 needs two increasing distinct-id populations") + sites = [] + all_sites = (reused_low.sites.keys() | unique_low.sites.keys() | + reused_high.sites.keys() | unique_high.sites.keys()) + for site in sorted(all_sites): + low = unique_low.sites.get(site, 0) - reused_low.sites.get(site, 0) + high = unique_high.sites.get(site, 0) - reused_high.sites.get(site, 0) + if high != low: + sites.append(dict(site=site, lane=owner(site)[0], low_extra_bytes=low, + high_extra_bytes=high, + bytes_per_additional_id=(high - low) / (high_ids - low_ids))) + delta = ((unique_high.heap_bytes - reused_high.heap_bytes) - + (unique_low.heap_bytes - reused_low.heap_bytes)) + return dict(denominator_ids=high_ids - low_ids, + bytes_per_additional_id=delta / (high_ids - low_ids), sites=sites) + + +def validate_sink_ids(path: Path, trades: int, period: int, long_id: bool) -> None: + """Check Q9's identities after streaming, when old row access is unavailable. + + This small fixture check is not the version-1 format acceptance suite. + It validates the trailer digest/count and each T's id, qty and closed bit. + The production reference reader remains L6a/L0's responsibility. + """ + digest, count, trailer = hashlib.sha256(), 0, None + with path.open("rb") as source: + first = source.readline() + header = json.loads(first) + if header.get("schema") != "pf-report-sink" or header.get("version") != 1: + raise ValueError("Q9 sink has no version-1 header") + digest.update(first) + fields = next(record["fields"] for record in header["records"] if record["type"] == "T") + for raw in source: + record = json.loads(raw) + if isinstance(record, dict): + if trailer is not None or record.get("trailer") != "pf-report-sink": + raise ValueError("invalid Q9 sink trailer") + trailer = record + continue + if trailer is not None: + raise ValueError("Q9 sink has bytes after its trailer") + digest.update(raw) + if record[0] == "T": + trade = dict(zip(fields, record[1:])) + ordinal = count if period == 0 else count % period + expected = ("census-entry-" if long_id else "E") + f"{ordinal:014d}" + count += 1 + if (trade["n"] != count or trade["entry_id"] != expected or + trade["qty"] != 1 or trade["open_at_end"] is not False): + raise ValueError("Q9 sink changed entry identity or trade shape") + if (trailer is None or trailer.get("sha256") != digest.hexdigest() or + trailer.get("counts", {}).get("T") != trades or count != trades): + raise ValueError("Q9 sink digest or trade population disagrees") + + +def _sha(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as source: + for chunk in iter(lambda: source.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _fresh(build: Path, executable: Path) -> None: + library = build / "lib/libpineforge.a" + if not library.is_file() or not executable.is_file(): + raise ValueError(f"missing complete lab build: {library} / {executable}") + sources = [ROOT / "CMakeLists.txt"] + sources += [path for directory in ("src", "include") + for path in (ROOT / directory).rglob("*") if path.is_file()] + if any(path.stat().st_mtime_ns > library.stat().st_mtime_ns for path in sources): + raise ValueError("stale libpineforge.a; lab must complete an all-target build") + if executable.stat().st_mtime_ns < library.stat().st_mtime_ns: + raise ValueError(f"stale statically linked executable/module: {executable}") + if executable.name == "test_heap_census_unique_ids" and any( + path.stat().st_mtime_ns > executable.stat().st_mtime_ns + for path in (ROOT / "tests/test_heap_census_unique_ids.cpp", ROOT / "tests/CMakeLists.txt")): + raise ValueError("stale census fixture executable") + + +def profile(command: list[str], directory: Path, timeout: int) -> tuple[Snapshot, dict]: + directory.mkdir() + snapshot = directory / "checkpoint.massif" + prefix = directory / "vgdb" + argv = ["valgrind", "--tool=massif", "--vgdb=yes", f"--vgdb-prefix={prefix}", + "--pages-as-heap=no", "--stacks=no", "--time-unit=B", + "--depth=100", "--threshold=0.0", "--peak-inaccuracy=0.0", + f"--massif-out-file={directory / 'automatic.massif'}", + f"--log-file={directory / 'valgrind.log'}", *command] + (directory / "command.json").write_text(json.dumps(argv, indent=2) + "\n") + env = dict(os.environ, PYTHONMALLOC="malloc", PYTHONDONTWRITEBYTECODE="1", + PF_HEAP_CENSUS_SINK_PATH=str(directory / "report-sink.jsonl")) + started = time.monotonic() + output = bytearray() + metadata = None + with (directory / "stderr.log").open("wb") as stderr: + child = subprocess.Popen(argv, cwd=ROOT, env=env, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=stderr) + try: + with selectors.DefaultSelector() as ready: + ready.register(child.stdout, selectors.EVENT_READ) + while metadata is None: + if time.monotonic() - started > timeout: + raise ValueError(f"checkpoint timed out: {directory}") + if not ready.select(1): + continue + chunk = os.read(child.stdout.fileno(), 65536) + if not chunk: + raise ValueError(f"child exited before live checkpoint: {directory}") + output.extend(chunk) + for line in output.decode("utf-8", "replace").splitlines(): + if line.startswith(MARKER) and output.endswith(b"\n"): + metadata = json.loads(line[len(MARKER):]) + break + if metadata.get("phase") != "native_return": + raise ValueError("wrong census phase") + monitor = ["vgdb", f"--vgdb-prefix={prefix}", f"--pid={child.pid}", + "detailed_snapshot", str(snapshot)] + reply = subprocess.run(monitor, capture_output=True, timeout=60, check=False) + (directory / "vgdb.stdout").write_bytes(reply.stdout) + (directory / "vgdb.stderr").write_bytes(reply.stderr) + if reply.returncode != 0 or not snapshot.is_file(): + raise ValueError(f"Massif checkpoint failed: {directory}") + measured = parse_massif(snapshot.read_text()) + child.stdin.write(b"\n") + child.stdin.flush() + tail, _ = child.communicate(timeout=max(1, timeout - (time.monotonic() - started))) + output.extend(tail) + if child.returncode != 0: + raise ValueError(f"profiled run failed ({child.returncode}): {directory}") + if output.decode("utf-8", "replace").count(MARKER) != 1: + raise ValueError("profiled run did not have exactly one checkpoint") + metadata.update(command=argv, snapshot_sha256=_sha(snapshot), + live_heap_bytes=measured.heap_bytes) + (directory / "measurement.json").write_text(json.dumps(metadata, indent=2) + "\n") + return measured, metadata + finally: + if child.poll() is None: + child.kill() + child.wait() + (directory / "stdout.log").write_bytes(output) + + +def corpus_worker(probe: Path, bars_limit: int, output: Path) -> int: + """Drive the real corpus CLI, preserving all of its per-probe settings. + + Wrap its documented on_report callback: after a successful engine call, + before report_free/strategy_free. Prefix only the primary chart; other + _load_bars feed loads use its final timestamp as their upper bound. This + is a retained-heap census, not a peak/load-transient measurement. + """ + sys.path.insert(0, str(ROOT / "scripts")) + import run_strategy as runner + + original_load, original_run, original_init = runner._load_bars, runner.Strategy.run, runner.Strategy.__init__ + chart = {} + sink_state = {"sink": False, "streamable_policy": False, "history_declared": None} + mappings = [] + + class Descriptor(ctypes.Structure): + _fields_ = [("struct_size", ctypes.c_uint32), ("version", ctypes.c_uint32), + ("path", ctypes.c_char_p), ("run_id", ctypes.c_char_p), ("scratch_dir", ctypes.c_char_p)] + + def initialize(self, *args, **kwargs): + original_init(self, *args, **kwargs) + lib = self.lib + if hasattr(lib, "strategy_reads_trade_history"): + lib.strategy_reads_trade_history.restype = ctypes.c_int + sink_state["history_declared"] = lib.strategy_reads_trade_history() != 0 + sink_state["streamable_policy"] = not sink_state["history_declared"] + if hasattr(lib, "strategy_set_report_sink_v1"): + setter = lib.strategy_set_report_sink_v1 + setter.argtypes = [ctypes.c_void_p, ctypes.POINTER(Descriptor)] + setter.restype = ctypes.c_int + original_full = lib.run_backtest_full + + def full(*call_args): + descriptor = Descriptor(ctypes.sizeof(Descriptor), 1, + os.fsencode(output / "report-sink.jsonl"), b"L0B-corpus", None) + if setter(call_args[0], ctypes.byref(descriptor)) != 0: + raise ValueError("corpus sink installation refused") + sink_state["sink"] = True + return original_full(*call_args) + + lib.run_backtest_full = full + + def load(path, **kwargs): + if chart: + previous = kwargs.get("ohlcv_end_ms") + kwargs["ohlcv_end_ms"] = min(previous, chart["end"]) if previous is not None else chart["end"] + bars, count, identity = original_load(path, **kwargs) + if not chart: + if count < bars_limit: + raise ValueError(f"corpus prefix too short: {probe} has {count}, needs {bars_limit}") + count = bars_limit + chart.update(bars=count, end=int(bars[count - 1].timestamp)) + if count == 0: + return bars, count, identity + # Only caller OHLCV may grow. Put these exact bytes in a mapping so + # Massif's malloc census does not mistake a ctypes input tape for an + # engine leak. Engine-owned copies remain malloc allocations and fail. + # Input-loading/transient RSS stays the L0 acceptance harness's job. + path = output / f"caller-ohlcv-{len(mappings)}.bin" + with path.open("w+b") as tape: + tape.write(memoryview(bars).cast("B")[:ctypes.sizeof(runner.BarC) * count]) + tape.flush() + mapping = mmap.mmap(tape.fileno(), 0) + mappings.append(mapping) + return (runner.BarC * count).from_buffer(mapping), count, identity + + def run(self, *args, **kwargs): + previous = kwargs.get("on_report") + + def checkpoint(report): + if not chart or report.input_bars_processed != bars_limit: + raise ValueError("corpus run did not process the requested chart prefix") + print(MARKER + json.dumps(dict(bars=int(report.input_bars_processed), + trades=int(report.total_trades), ids=None, phase="native_return", + report_copy_included=not sink_state["sink"], **sink_state)), flush=True) + if os.read(sys.stdin.fileno(), 1) != b"\n": + raise ValueError("profiler did not release corpus checkpoint") + if previous is not None: + previous(report) + + kwargs["on_report"] = checkpoint + return original_run(self, *args, **kwargs) + + # Derived feeds must be made fresh ONCE by CORE-TOP before starting the + # census. Refuse a derivation attempt rather than racing its fixed .new. + def no_derive(): + import derive_corpus_feeds as feeds + if (not feeds.SOURCE_1M.is_file() or not feeds._derived_cache_is_fresh( + feeds.SOURCE_1M, (feeds.DERIVED_15M, feeds.DERIVED_15M_WINDOW), feeds.DERIVATION_STAMP)): + raise ValueError("CORE-TOP must prepare derived corpus feeds before census") + + runner._load_bars, runner.Strategy.run, runner.ensure_derived = load, run, no_derive + runner.Strategy.__init__ = initialize + sys.argv = [str(ROOT / "scripts/run_strategy.py"), str(probe), + "--output", str(output / "engine_trades.csv"), "--no-trim-output"] + return runner.main() + + +def _markdown(report: dict) -> str: + lines = ["# CR-150 L0B measured heap census", "", f"Status: **{report['status']}**.", "", + f"Engine HEAD: `{report['head']}`. Schema: `{SCHEMA}`.", "", + "Live Massif requested bytes at native_return; allocator overhead and RSS are separate.", + "Every positive allocation-site delta is listed in census.json. Unknown sites belong to L19j.", "", + "| Comparison | Unit | Growing sites | Non-admitted sites |", "|---|---|---:|---:|"] + for case in report["comparisons"]: + rows = case["sites"] + lines.append(f"| {case['name']} | {case['unit']} | {len(rows)} | " + f"{sum(not row['admitted'] for row in rows)} |") + lines += ["", "## Q9 bytes per additional distinct entry id", "", + "Matched bars/trades/id lengths; signed difference of unique minus reused slopes.", "", + "| ID length (bytes) | B/additional id | Denominator (ids) |", "|---:|---:|---:|"] + for row in report["q9"]: + lines.append(f"| {row['id_bytes']} | {row['bytes_per_additional_id']:.6f} | {row['denominator_ids']} |") + for case in report["comparisons"]: + lines += ["", f"## Sites: {case['name']}", ""] + for row in case["sites"]: + lines += [f"- {row['lane']}: +{row['delta_bytes']} B, " + f"{row['bytes_per_unit']:.6f} B/{case['unit']}; admitted={row['admitted']}.", + "", "```text", row["site"], "```", ""] + if report["errors"]: + lines += ["", "## Open proof", "", *[f"- {error}" for error in report["errors"]]] + lines += ["", "No PASS is inferred from missing tools, missing modules, an empty corpus or a short prefix.", ""] + return "\n".join(lines) + + +def main(argv=None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--build-dir", type=Path, default=ROOT / "build") + parser.add_argument("--battery", type=Path, required=True) + parser.add_argument("--corpus", type=Path) + parser.add_argument("--bars", type=int, default=4000, help="bar population N; compared with 4N") + parser.add_argument("--trade-bars", type=int, default=40000, help="fixed bars at both trade rungs") + parser.add_argument("--output", type=Path) + parser.add_argument("--timeout", type=int, default=3600) + args = parser.parse_args(argv) + build = args.build_dir.resolve() + output = (args.output or build / f"heap-census-{time.time_ns()}").resolve() + output.mkdir(parents=True, exist_ok=False) + report = dict(schema=SCHEMA, status="OPEN", head=subprocess.check_output( + ["git", "rev-parse", "HEAD"], cwd=ROOT, text=True).strip(), + comparisons=[], q9=[], runs={}, errors=[], corpus_requested=args.corpus is not None) + try: + if sys.platform != "linux" or not all(shutil.which(tool) for tool in ("valgrind", "vgdb")): + raise ValueError("heap census requires the Linux lab with valgrind and vgdb") + if args.bars < 300 or args.trade_bars < 20000 or args.timeout <= 0: + raise ValueError("need N >= 300, fixed trade bars >= 20000 and a positive timeout") + battery = args.battery.resolve() + if not (battery / "unique_entry_id.pine").is_file(): + raise ValueError("battery has no unique-entry-id fixture") + manifest = json.loads((battery / "heap_census_battery.json").read_text()) + required = {"reused_id", "moved_exits", "oca_brackets", "pyramiding", "partial_exits", + "split_magnifier", "split_no_magnifier", "routed_security", "nday_security", "foreign_security"} + if manifest.get("schema") != "pineforge-heap-census-battery/v1" or set(manifest.get("native_modes", [])) != required: + raise ValueError("incomplete census battery") + executable = build / "bin/test_heap_census_unique_ids" + _fresh(build, executable) + report.update(library_sha256=_sha(build / "lib/libpineforge.a"), + fixture_sha256=_sha(executable), battery_sha256=_sha(battery / "heap_census_battery.json"), + unique_pine_sha256=_sha(battery / "unique_entry_id.pine")) + samples: dict[str, Snapshot] = {} + + def native(name, mode, bars, trades, period, long_id=False): + print(f"heap census: {name}", flush=True) + measured, metadata = profile([str(executable), mode, str(bars), str(trades), + str(period), str(int(long_id)), "--wait", "yes"], output / name, args.timeout) + if metadata["bars"] != bars: + raise ValueError(f"wrong bar population: {name}") + if metadata.get("sink") and mode in ("unique_ids", "reused_id"): + validate_sink_ids(output / name / "report-sink.jsonl", trades, period, long_id) + samples[name], report["runs"][name] = measured, metadata + return name + + def compare(name, low, high, unit): + before, after = report["runs"][low][unit], report["runs"][high][unit] + sites = growth(samples[low], samples[high], before, after) + for row in sites: + if row["lane"] == "L19j" and any(token in row["site"] for token in ("PlacementSnapshot", "PlacementTable::place")): + if name == "moved_exits": + row["lane"] = "L19g" + elif name in ("split_magnifier", "split_no_magnifier"): + row["lane"] = "L19f" + history = all(report["runs"][key].get("history_declared") is True for key in (low, high)) + if history: + for row in sites: + if row["lane"] == "L8a/L8c/L18": + row.update(admitted=True, admission="explicit closed-trade history module") + report["comparisons"].append(dict(name=name, low=low, high=high, unit=unit, + low_units=before, high_units=after, history_exception=history, sites=sites)) + + for mode in manifest["native_modes"]: + low = native(mode + "-N", mode, args.bars, 10, 1) + high = native(mode + "-4N", mode, 4 * args.bars, 10, 1) + compare(mode, low, high, "bars") + for long_id in (False, True): + for mode, period in (("reused_id", 1), ("unique_ids", 0)): + names = [] + for trades in (10, 10000): + name = f"{mode}-{'long' if long_id else 'short'}-{trades}" + native(name, mode, args.trade_bars, trades, period, long_id) + expected_ids = trades if period == 0 else 1 + if report["runs"][name]["trades"] != trades or report["runs"][name]["ids"] != expected_ids: + raise ValueError(f"Q9 populations disagree: {name}") + names.append(name) + compare(f"{mode}-{'long' if long_id else 'short'}", *names, "trades") + kind = "long" if long_id else "short" + keys = [f"{mode}-{kind}-{trades}" for trades in (10, 10000) + for mode in ("reused_id", "unique_ids")] + lengths = {report["runs"][key]["id_bytes"] for key in keys} + if len(lengths) != 1: + raise ValueError("Q9 id lengths are not equal") + cost = id_cost(*(samples[key] for key in keys), 10, 10000) + report["q9"].append(dict(id_bytes=lengths.pop(), runs=keys, **cost)) + if args.corpus is not None: + corpus = args.corpus.resolve() + probes = sorted(path for path in (corpus / "validation").glob("*") + if (path / "strategy.pine").is_file() or (path / "generated.cpp").is_file()) + if not probes: + raise ValueError("corpus has no source-marked validation probes") + report["corpus_population"] = [str(probe.relative_to(corpus)) for probe in probes] + for index, probe in enumerate(probes): + try: + module = probe / "strategy.so" + _fresh(build, module) + names = [] + for bars in (args.bars, 4 * args.bars): + name = f"corpus-{index:04d}-{bars}" + measured, metadata = profile([sys.executable, "-B", str(Path(__file__).resolve()), + "--worker", str(probe), str(bars), str(output / name)], output / name, args.timeout) + metadata.update(module_sha256=_sha(module), probe=str(probe)) + samples[name], report["runs"][name] = measured, metadata + names.append(name) + compare(str(probe.relative_to(corpus)), *names, "bars") + except (ValueError, OSError, subprocess.SubprocessError) as error: + report["errors"].append(f"{probe}: {error}") + if not report["errors"]: + is_flat = all(flat(case["sites"]) for case in report["comparisons"]) + streaming = all(run.get("sink") and (run.get("streamable_policy") or run.get("history_declared") is True) + for run in report["runs"].values()) + report["memory_verdict"] = "FLAT" if is_flat else "RED" + if is_flat and not streaming: + report["errors"].append("flat samples lack a sink or an explicit trade-history policy") + else: + report["status"] = ("PASS" if is_flat else "RED") if args.corpus is not None else "BATTERY_ONLY" + except (ValueError, OSError, KeyError, StopIteration, TypeError, subprocess.SubprocessError) as error: + report["errors"].append(str(error)) + finally: + (output / "census.json").write_text(json.dumps(report, indent=2, sort_keys=True) + "\n") + (output / "CENSUS.md").write_text(_markdown(report)) + print(f"heap census: {report['status']} — {output / 'CENSUS.md'}", flush=True) + return 0 if report["status"] == "PASS" else (1 if report["status"] == "RED" else 2) + + +if __name__ == "__main__": + if len(sys.argv) == 5 and sys.argv[1] == "--worker": + raise SystemExit(corpus_worker(Path(sys.argv[2]), int(sys.argv[3]), Path(sys.argv[4]))) + raise SystemExit(main()) diff --git a/tests/heap_census_report.md b/tests/heap_census_report.md new file mode 100644 index 000000000..10417ab34 --- /dev/null +++ b/tests/heap_census_report.md @@ -0,0 +1,195 @@ +# CR-150 L0B heap census handoff + +**OPEN LAB — source inventory and measurement harness checked in; no measured +census or bytes-per-id verdict is claimed.** CORE-TOP forbids building, compiling +or executing tests/profilers on this Mac. The numeric Q9 cells below remain OPEN +until CORE-TOP attaches the Linux lab artifacts. This document is not the +completeness proof required by acceptance (k). + +Runtime/source basis: `ee3083d4a23fdae23d2cfa6ac254a8066de1b780`, branch +`core/cr150-l0b`. DESIGN revision 6 describes `3fc010a8b`; its quoted measurements +come from F39 box 6 at 1.6.0-rc.1, not from a run of this lane. The dispatch +authority is the L0b row of PARALLEL-PLAN section 3, DESIGN sections 2.1, 3.9, +3.10 and 7(k), Q9, and REVISION's amendments through 10-18 19:50. L19l remains +unconditional. The 18:21 scope ruling assigns the L0 acceptance harnesses to +integration; this lane does not modify them. + +## Allocation-site ownership inventory + +Every row is a source-identified term or an explicitly attributed prior +measurement, not a newly measured allocation site. The lab driver reports +every positive leaf-stack delta, its before/after bytes, B/bar or B/trade, and +an owner. Unknown, ambiguous and unsymbolized sites go to **L19j**, remain +non-admitted and must be resolved before acceptance. A known owner is an +assignment, never a waiver of growth. + +| Site/container | Size/unit or existing evidence | Lane | Fixture/path | +|---|---|---|---| +| Caller chart OHLCV | 48 B/input bar; the only admitted native chart allocation | Caller; L2 maps it in run_json | All native cases; corpus caller tapes are mapped | +| Caller foreign close times | 8 B/feed bar, part of the admitted feed tape | Caller; L2 | `foreign_security`; shared chart/feed OHLCV bytes | +| Scheduler `RetainedBegin::bars`, native module input copy, stream warmup temporary | 48 B/input bar per copy, DESIGN 2.1 | L3 | Native battery; generated corpus modules | +| Scheduler completion and source-boundary arrays | 1 + 1 B/input bar | L4 | Bar ladder, both input/script splits | +| `equity_curve_`, final C report curve | 24 B/script point per copy; F39's combined 63.5 B/bar is separately flagged in DESIGN | L6b-1/L6b-2 | All bar ladders; C report copy is present in corpus baseline only | +| Month-end/monthly-return/per-bar-return metric scratch | About 16 B/month plus 8 B/script bar; report-time transient | L7b/L7c | Corpus metric phase; transient peak remains OPEN/L0 | +| `trades_` | 240 B/row nominal, 314.6 B/trade at F39's 1.31072 capacity ratio | L8a/L8c | Fixed 40,000 bars, 10 vs 10,000 trades | +| `closed_row_prefix_`, `trade_exit_phase_` | 8 + 1 B/row nominal; 10.5 + 1.3 at that capacity ratio | L8a/L8c | Same trade ladder | +| C report `TradeC` and presentation copies | 104 B/row plus selected/observed curve and trade copies | L9a/L9s/L17a | Corpus callback before report destruction; selected acceptance is L0 at integration | +| Rows closed inside a still-open position | Trade rows and percentage-exit reader tail | L18 | `partial_exits`; long scale-out acceptance belongs to L0 | +| `successor_roots_` | 16 B/replace successor before capacity slack | L19a | Exit repricing every bar | +| Group-effect `receipts_` | One `ReceiptKey` per group effect; platform size must be measured | L19b | OCA bracket pairs | +| `cohort_receipts_` | 40 B/receipt nominal; 2 x 40 x 1.31072 = 104.9 B/round trip in F39's inventory | L19c | Trade ladder, pyramiding | +| `OriginRoster::items_`, `positions_` and their inner vectors/buckets | 24 B/handle plus maps/vectors; about 119.5 B/trade in the prior structural inventory | L19d | Reused id vs unique id, recurring-id unit control | +| Admission `Journal::events_`, shared `CommandObservation`, review and sizing payloads (F0a) | Prior structural estimate ~480 B observation + 288 B event slot x capacity = ~857 B/admitted entry | L19e | All entry/order cases, recording OFF; review/margin variants also require L19e's own tests | +| Placement rows pinned by K5 (F0b) | About 1.3 KB/placed entry | L19f | `split_magnifier`, `split_no_magnifier` | +| Placement rows pinned by K1 (F0c) | About 1.3 KB/reissued leg while its origin stays open | L19g | `moved_exits`; margin-call parity fixture remains L19g's proof | +| `PlacementTable::unfolded_` (F0d) | 8 B/request nominal; 21.0 B/round trip at the prior capacity ratio | L19h | Market trade ladder, exit repricing | +| `BracketRoster::settled_` (F0d) | 1–2 B/settled leg/family plus capacity | L19h | OCA pairs and partial exits | +| `input_next_ms_` (F1) | 8 B/input bar with a subscription | L19i, unless L3/L4 takes it | `routed_security`, `foreign_security` | +| N-day stamps and native-group opens (F2) | 16 B/N trading days/site, not per trading day; prepare also has a per-day map transient | L19k if the measured slope exists | `nday_security`, 3D over daily bars; prepare transient remains OPEN | +| Adapter `cohorts_by_id_`, `cohort_order_` (F3) | One map node plus one string-vector slot per distinct id; bytes/id OPEN | L19l | Short/long unique-entry populations and reused controls | +| Entry-id rank table `exit_rank_keys_` (F3) | Source read below: at most 27 recorded keys at this base | L19l contract; bound reported for CORE-TOP | 10 vs 10,000 distinct ids, recurring 31-id unit control | +| Kernel `cohorts_`, each roster's retained vector capacity (F3) | One 32 B roster slot nominal per distinct id plus member capacity; bytes/id OPEN | L19l | Unique-id control; empty roster lifetime is not the same as empty members | +| `PlacementTable::sequence_index_`, `erased_minima_`, nested `by_from_entry` (F3) | Per (id, from_entry), maps/string storage; bytes/id OPEN | L19l | Unique entry ids and derived `strategy.close` labels | +| `consumed_partial_exit_cycles_` and per-exit-id auxiliaries (F3) | Per (exit id, from_entry); population/bytes require the profiler | L19l | Partial exits and corpus; per-trade exit-id coverage remains OPEN if absent from corpus | +| Foreign symbol feed bars and close-time copies | 56 B/feed bar nominal; F39 engine copy ~57 B/feed bar | L15a/L15b | `foreign_security`; borrowed setter discovered when implemented | +| Finer-input magnifier path and preparation temporaries | Up to 48 B/path bar; preparation has additional vectors | L16 | `split_magnifier`; transient peak is L0's acceptance | +| Recorded outputs, trace and hashes when enabled | Per output value/record; hash array 8 B/script bar | L6b-3 | Corpus declarations; dedicated enable/trace/hash acceptance is L0/integration | +| run_json CSV tuples, arrays and report dictionaries | F39: 298.6 B/chart CSV row, 395.5 B/feed row, 288.8 B/equity dict, 922.2 B/trade dict | L2/L9a/L9s | L0 RSS harness at integration; this census samples retained native/report state | +| Any additional growing allocation stack | No size assumed | L19j | Full battery and every source-marked corpus validation probe | + +Bounded and non-production terms are not silently admitted. `range_end_trades_` +and pyramid lots are bounded by current open lots; issued ranges, repriced rows +and the Window command journal have existing bounds. Native/auxiliary security +copies, opt-in historical projections and Full driver logs are recorded as +non-production in DESIGN 2.4/3.9. If any appears as a growing stack in the +production battery/corpus, the driver reports it; it does not exempt a whole +file or class name. + +Relevant declarations at this base: `trades_` in include/pineforge/engine.hpp:546; +`equity_curve_` in include/pineforge/engine.hpp:586; `closed_row_prefix_` in +src/native_execution_consumer.hpp:1914; `events_` in +include/pineforge/source/market_admission.hpp:213; `OriginRoster` in +include/pineforge/source/pine_adapter.hpp:935; the per-id cohort members in +include/pineforge/source/pine_adapter.hpp:2518 and :2519; `trade_exit_phase_` in +include/pineforge/source/pine_adapter.hpp:2734; `exit_rank_keys_` in +include/pineforge/source/pine_adapter.hpp:2740; `cohorts_` in +include/pineforge/native_order.hpp:2318; N-day stamp arrays in +include/pineforge/timeframe.hpp:555 and :556. + +## Q9 bytes per additional distinct entry id + +The measured result is **OPEN**, not 0 and not the prior 1,323.4 B/trade. That +F39 number is an engine trade slope for a different population. It cannot be +relabelled as bytes/id. L19l is required regardless of the result. + +The fixture uses the same 40,000 one-minute bars and the same trade schedule in +each pair. Each trade opens one unit and closes it on the next script bar. +`unique_ids` uses a new entry id on every opening; `reused_id` uses one id. +Both use 14 zero-padded digits, so decimal transitions do not change id length. +The short population has 15-byte ids; the long population has 27-byte ids. +No trade-history accessor runs in the strategy body. The ordinary unit also +revisits ids after 31 openings, testing recurrence rather than assuming that an +old id can be forgotten. The Pine fixture expresses the same unique/reused rule; +the CMake-wired native fixture is what the profiler executes. + +| ID length | Low pair | High pair | Incremental distinct ids | Measured B/additional id | +|---:|---|---|---:|---| +| 15 B | 10 trades, 1 vs 10 ids | 10,000 trades, 1 vs 10,000 ids | 9,990 | OPEN LAB | +| 27 B | 10 trades, 1 vs 10 ids | 10,000 trades, 1 vs 10,000 ids | 9,990 | OPEN LAB | + +For every leaf stack s, use the signed difference of differences: + +```text +q9(s) = ((unique_10000(s) - reused_10000(s)) + - (unique_10(s) - reused_10(s))) / 9990 +``` + +The aggregate uses total live heap bytes with the same formula. Negative +site costs remain in the output because distributing openings among ids can +change vector capacity and bucket allocation. Ordinary trade-history work is +controlled by equal trades, bars and id lengths, rather than charged to Q9. +The driver reports both aggregate and per-site B/id in `census.json`, with the +four raw snapshots and population receipts. Massif requested bytes do not +include allocator chunk overhead; they are not RSS measurements. + +Source-only lower-level sizes are useful for orientation but not a measured +total: a libstdc++ `std::string` slot is 32 B, a kernel `CohortRoster` is an +8-byte handle plus a 24-byte vector, and a `RequestHandle` is 24 B. Map nodes, +bucket arrays, nested vectors, capacity slack and long-string allocations +still have to be counted on the lab. The table inventory deliberately does +not add a fabricated B/id total. + +One source-level qualification: `note_exit_rank_key` in +src/source/pine_adapter.cpp:6331 returns once the table exceeds 26 keys. The +check is at :6334 and the append at :6339, so this particular table saturates +at 27 at the brief's base. DESIGN's F3 inventory groups it with per-distinct-id +tables. This lane changes neither the bound nor the contract; CORE-TOP/L19l +should retain the observation when attributing the early-rung cost. + +## Lab procedure and artifacts + +Do not overlap the census with `scripts/run_corpus.sh` or another producer of +the derived feeds/build library. CORE-TOP first prepares derived feeds once +to freshness and completes an all-target, symbolized Release/RelWithDebInfo +build, including corpus modules. The driver performs no build or derivation. +It refuses a library older than any working-tree source/header or root CMake +file, and refuses test/module binaries older than that static library. + +Run the unit/guard/full verification commands prescribed by AGENTS.md on the +lab, including the previous-epoch compile diagnostic before trusting the new +C++ row. Then profile this lane's base runtime with the harness added and +the integrated runtime separately: + +```bash +python3 tests/heap_census.py --build-dir build \ + --battery tests/fixtures/flat_memory --corpus corpus \ + --output build/heap-census-l0b-baseline +``` + +The output directory must be new. It preserves `command.json`, live checkpoint +`checkpoint.massif`, automatic Massif output, profiler/monitor/stdout/stderr +logs, sink and caller-tape files, `measurement.json`, `census.json`, and the +generated `CENSUS.md`. Its source HEAD, module/library hashes, measured bars, +trades, ids, id byte lengths, sink and trade-policy state bind the evidence. +Exit 1 means observed non-admitted growth with complete requested coverage; +exit 2 means OPEN/incomplete proof (including a battery-only run). Exit 0 +requires the battery and nonempty corpus, increasing measured populations, +no non-admitted growing site and implemented sink/history-policy evidence. + +The native checkpoint precedes both host destruction and report allocation. +The corpus checkpoint wraps `Strategy.run`'s live `on_report` seam before +report/state destruction and before Python report conversion. Its baseline +includes the C report copy, which is recorded. Caller OHLCV arrays are mapped +from byte-identical packed records in the output directory so the malloc +census does not misclassify the one permitted input tape. Engine-owned copies +still show up as allocations. The real corpus CLI supplies settings, requests +and magnifier routing, with N/4N chart prefixes and feed end bounds. Short +prefixes and missing modules fail explicitly rather than skipping probes. + +P00 declares the sink but does not implement it. The native fixture discovers +implemented exports at runtime and uses the old retaining path when absent; +there is no red result caused solely by requesting a missing sink flag. +The expected base red reason is the live growth in the inventory, especially +the admission journal, origin/cohort state, trades and chart/curve copies. +After integration, the fixture detects the streamable policy method without +a preprocessor feature branch and validates streamed ids from T records. + +## Remaining proof + +- Execute the new C++ row, Python controls, previous-epoch compile, preflight, + full release verification and full corpus sweep on the lab. No local test or + guard summary/log exists; only a source/whitespace review was performed here. +- Execute the profiler on the base and integrated exact trees, attach the raw + allocation stacks, fill both Q9 numeric cells, and check in the measured + census/JSON evidence. No fresh growing allocation site has been measured by + this lane; completeness is still OPEN. +- Confirm the lab's Valgrind monitor handshake and the optional sink/policy + discovery against the actual implementing lanes. A failure is OPEN, not a + memory PASS. Symbols need debug information for useful attribution. +- L0/integration must still cover selected windows, long scale-out, enabled + outputs/trace/hashes, binary/CSV loading and transient prepare/report peaks. + This retained-heap driver does not replace those acceptance harnesses. +- The base has a per-ID rank-table bound as noted above; CORE-TOP owns its + reconciliation with the F3 wording. L19l is not gated on this observation. +- OPEN FROZEN: none encountered. No frozen file, P00 declaration, measured + runtime source, source/header ABI list or existing fixture/helper was edited. diff --git a/tests/test_heap_census.py b/tests/test_heap_census.py index 68356f063..288874ac0 100644 --- a/tests/test_heap_census.py +++ b/tests/test_heap_census.py @@ -4,6 +4,10 @@ its red result must be retained engine allocations, not a missing sink flag. These controls test the evidence reader, not the engine's memory contract. """ +import hashlib +import json +from pathlib import Path +import tempfile import unittest import heap_census as census @@ -83,6 +87,38 @@ def test_q9_subtracts_the_same_trade_work_at_both_rungs(self): with self.assertRaises(ValueError): census.id_cost(reused_low, unique_low, reused_high, unique_high, 10, 10) + def test_q9_keeps_negative_sites_and_does_not_sum_only_the_positive_costs(self): + empty = census.Snapshot(0, {}) + old = census.Snapshot(100, {"reuse_capacity": 100}) + new = census.Snapshot(160, {"ids": 160}) + result = census.id_cost(empty, empty, old, new, 10, 20) + self.assertEqual(result["bytes_per_additional_id"], 6.0) + self.assertEqual(sorted(row["bytes_per_additional_id"] for row in result["sites"]), [-10.0, 16.0]) + + def test_streamed_q9_checks_identity_even_when_the_digest_is_valid(self): + def sink(second): + documents = [ + {"schema": "pf-report-sink", "version": 1, "records": [ + {"type": "T", "fields": ["n", "entry_id", "qty", "open_at_end"]}]}, + ["T", 1, "E00000000000000", 1.0, False], + ["T", 2, second, 1.0, False], + ] + raw = b"".join((json.dumps(item, separators=(",", ":")) + "\n").encode() for item in documents) + trailer = dict(trailer="pf-report-sink", counts={"T": 2}, sha256=hashlib.sha256(raw).hexdigest()) + return raw + (json.dumps(trailer) + "\n").encode() + + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "sink.jsonl" + path.write_bytes(sink("E00000000000001")) + census.validate_sink_ids(path, 2, 0, False) + path.write_bytes(sink("E00000000000000")) + with self.assertRaisesRegex(ValueError, "identity"): + census.validate_sink_ids(path, 2, 0, False) + + def test_nonincreasing_populations_are_not_a_flat_verdict(self): + with self.assertRaises(ValueError): + census.growth(census.Snapshot(100, {}), census.Snapshot(100, {}), 10, 10) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_heap_census_unique_ids.cpp b/tests/test_heap_census_unique_ids.cpp index 888b79c8f..4b09134b8 100644 --- a/tests/test_heap_census_unique_ids.cpp +++ b/tests/test_heap_census_unique_ids.cpp @@ -3,15 +3,19 @@ // No closedtrades/opentrades reads occur in on_source_bar. Normal ctest checks // the fixture's fills and ids; heap_census.py checks its retained-byte slope. #include +#include #include #include #include #include #include +#include #include #include #include +#include +#include #include namespace { @@ -43,6 +47,12 @@ std::string entry_id(int ordinal, bool long_id) { return bars; } +[[gnu::noinline]] std::vector census_feed_close_ms(const std::vector& bars) { + std::vector closes(bars.size()); + for (std::size_t i = 0; i < bars.size(); ++i) closes[i] = bars[i].timestamp + 60000; + return closes; +} + class CensusHost final : public source::PineStrategyHost { public: CensusHost(std::string mode, int trades, int stride, int period, bool long_id) @@ -63,6 +73,10 @@ class CensusHost final : public source::PineStrategyHost { // production path, which never asks for a broker hash array. if (mode_ == "routed_security") register_security_eval(0, "60", "1", false, false); if (mode_ == "nday_security") register_security_eval(0, "3D", "1D", false, false); + if (mode_ == "foreign_security") { + set_syminfo_string("tickerid", "TEST:CHART"); + register_security_eval(0, "TEST:OTHER", "1", std::string("1"), false, false, false); + } } void evaluate_security(int, const Bar&, bool) override { ++evaluations_; } @@ -124,7 +138,19 @@ class CensusHost final : public source::PineStrategyHost { int exit_calls() const { return exit_calls_; } int callbacks() const { return callbacks_; } + // The P00 base has declarations only. Detect the later policy method + // through substitution, so the same fixture runs the old retaining path + // and the implemented streaming path without a build-only test macro. + bool make_streamable() { return streamable(*this, 0); } + private: + template + static auto streamable(Host& host, int) + -> decltype(host.set_trade_history_streamable(true), bool()) { + host.set_trade_history_streamable(true); + return true; + } + template static bool streamable(Host&, long) { return false; } std::string mode_, active_; int limit_, stride_, period_; bool long_id_; @@ -134,18 +160,50 @@ class CensusHost final : public source::PineStrategyHost { void run_case(const std::string& mode, int bars_count, int trades, int period, bool long_id, bool wait) { require(bars_count >= 16 && trades > 0, "invalid census population"); - const bool split = mode == "split_magnifier"; + const bool split = mode == "split_magnifier" || mode == "split_no_magnifier"; const bool daily = mode == "nday_security"; const int script_bars = split ? bars_count / 15 : bars_count; require(script_bars >= trades * 2, "population needs two script bars per trade"); const int stride = std::max(2, script_bars / trades); const auto bars = census_chart_ohlcv(bars_count, daily); + const auto closes = mode == "foreign_security" ? census_feed_close_ms(bars) : std::vector{}; CensusHost host(mode, trades, stride, period, long_id); + bool sink_on = false; + bool policy = false; + const char* sink_path = std::getenv("PF_HEAP_CENSUS_SINK_PATH"); + // The strong reference links c_abi.cpp from the static archive. CMake's + // ENABLE_EXPORTS makes its implemented symbols visible to dlsym. P00's + // declarations alone do not make a feature present. + require(pf_abi_version() == PF_ABI_VERSION, "fixture/runtime ABI mismatch"); + auto set_sink = reinterpret_cast( + dlsym(RTLD_DEFAULT, "strategy_set_report_sink_v1")); + auto get_receipt = reinterpret_cast( + dlsym(RTLD_DEFAULT, "strategy_report_sink_receipt_v1")); + if (mode == "foreign_security") { + using BorrowedFeed = int (*)(pf_strategy_t, const char*, const char*, const pf_bar_t*, const int64_t*, int); + auto borrowed = reinterpret_cast(dlsym(RTLD_DEFAULT, "strategy_set_symbol_feed_borrowed_v1")); + if (borrowed) { + require(borrowed(static_cast(&host), "TEST:OTHER", "1", + reinterpret_cast(bars.data()), closes.data(), bars_count) == 0, + "borrowed foreign feed refused"); + } else { + require(host.set_symbol_feed("TEST:OTHER", "1", bars.data(), closes.data(), bars_count), + "foreign feed refused"); + } + } + if (wait && sink_path && set_sink) { + require(get_receipt != nullptr, "sink implementation lacks receipt query"); + policy = host.make_streamable(); + const pf_report_sink_v1 descriptor{sizeof(pf_report_sink_v1), 1, sink_path, "L0B", nullptr}; + require(set_sink(static_cast(&host), &descriptor) == 0, + "sink installation refused"); + sink_on = true; + } host.run(bars.data(), bars_count, daily ? "1D" : "1", split ? "15" : (daily ? "1D" : "1"), - split, 4, MagnifierDistribution::ENDPOINTS); + mode == "split_magnifier", 4, MagnifierDistribution::ENDPOINTS); require(host.last_error().empty(), host.last_error().c_str()); require(host.callbacks() > 0, "fixture never calculated"); - if (mode == "routed_security" || mode == "nday_security") + if (mode == "routed_security" || mode == "nday_security" || mode == "foreign_security") require(host.evaluations() > 0, "security fixture never evaluated"); if (mode == "moved_exits" || mode == "oca_brackets" || mode == "partial_exits") require(host.exit_calls() > 1, "exit fixture did not reissue"); @@ -155,7 +213,7 @@ void run_case(const std::string& mode, int bars_count, int trades, int period, if (exact) { require(host.opened() == trades, "entry population was not reached"); require(host.trade_count() == trades, "closed-trade population was not reached"); - for (int i = 0; i < trades; ++i) { + for (int i = 0; !sink_on && i < trades; ++i) { const Trade& row = host.get_trade(i); require(row.entry_id == entry_id(period == 0 ? i : i % period, long_id), "entry identity changed"); @@ -164,15 +222,29 @@ void run_case(const std::string& mode, int bars_count, int trades, int period, } else if (mode != "moved_exits") { require(host.trade_count() > 0, "battery fixture produced no closed rows"); } + if (sink_on) { + pf_report_sink_receipt_v1 receipt{}; + receipt.struct_size = sizeof receipt; + receipt.version = 1; + require(get_receipt(static_cast(&host), &receipt) == 0 && receipt.complete == 1, + "sink run did not complete"); + require(!policy || receipt.trades_retained == 0, "streamable fixture retained trade history"); + } const int ids = host.opened() == 0 ? 1 : (period == 0 ? host.opened() : std::min(host.opened(), period)); std::printf("PF_HEAP_CENSUS {\"bars\":%d,\"trades\":%d,\"ids\":%d," "\"id_bytes\":%zu,\"callbacks\":%d,\"security_evaluations\":%d," - "\"exit_calls\":%d,\"phase\":\"native_return\"}\n", + "\"exit_calls\":%d,\"sink\":%s,\"streamable_policy\":%s," + "\"phase\":\"native_return\"}\n", bars_count, host.trade_count(), ids, entry_id(0, long_id).size(), - host.callbacks(), host.evaluations(), host.exit_calls()); + host.callbacks(), host.evaluations(), host.exit_calls(), + sink_on ? "true" : "false", policy ? "true" : "false"); std::fflush(stdout); - if (wait) require(std::getchar() == '\n', "profiler did not release checkpoint"); + if (wait) { + char acknowledgement = 0; + require(::read(STDIN_FILENO, &acknowledgement, 1) == 1 && acknowledgement == '\n', + "profiler did not release checkpoint"); + } } } // namespace From bfa1bffee25d1ab60bfb4d702d64407bfe3c8766 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sun, 18 Oct 2026 23:53:36 +0800 Subject: [PATCH 09/35] test(cr150): require eager admission journal retirement with recording off --- tests/CMakeLists.txt | 6 + tests/test_admission_journal_window.cpp | 178 ++++++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 tests/test_admission_journal_window.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 473b2c9fe..3a05fdddb 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -7097,3 +7097,9 @@ if(PINEFORGE_BUILD_SOURCE_LAYER) target_link_libraries(test_heap_census_unique_ids PRIVATE ${CMAKE_DL_LIBS}) set_target_properties(test_heap_census_unique_ids PROPERTIES ENABLE_EXPORTS ON) endif() + +# CR-150 L19E: append after every existing line-pinned registration. +if(PINEFORGE_BUILD_SOURCE_LAYER) + list(APPEND TEST_SOURCES test_admission_journal_window) + pineforge_add_unit_test(test_admission_journal_window) +endif() diff --git a/tests/test_admission_journal_window.cpp b/tests/test_admission_journal_window.cpp new file mode 100644 index 000000000..7194d5fdc --- /dev/null +++ b/tests/test_admission_journal_window.cpp @@ -0,0 +1,178 @@ +// CR-150 L19E: exercise retirement on the real Pine host with recording OFF. +// The first checkpoint uses only base-tree APIs, before any hash/report query: +// an append-only journal fails because completed events still own their payloads. +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace { +using namespace pineforge; +constexpr double kNa = std::numeric_limits::quiet_NaN(); +constexpr std::int64_t kStart = 1736121600000LL; +int checks = 0; +int failures = 0; + +#define CHECK(condition) do { \ + ++checks; \ + if (!(condition)) { \ + ++failures; \ + std::fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #condition); \ + } \ +} while (false) + +std::vector chart(int count) { + std::vector bars; + bars.reserve(static_cast(count)); + for (int i = 0; i < count; ++i) + bars.push_back({100.0, 101.0, 99.0, 100.0, 1.0, kStart + i * 60000LL}); + return bars; +} + +class TradingHost final : public source::PineStrategyHost { +public: + TradingHost() { + source::PineStrategyConfig config; + config.initial_capital = 1000000.0; + config.default_qty_type = static_cast(QtyType::FIXED); + config.default_qty_value = 1.0; + config.pyramiding = 1; + config.process_orders_on_close = true; + config.commission_value = 0.0; + configure_pine_strategy(config); + set_syminfo_timezone("UTC"); + set_syminfo_session("24x7"); + set_syminfo_mintick(0.25); + } + void on_source_bar(const Bar&) override { + if (bar_index_ % 2 == 0) strategy_entry("E", true, kNa, kNa, 1.0); + else strategy_close_all(); + } +}; + +class RejectionHost final : public source::PineStrategyHost { +public: + RejectionHost() { + source::PineStrategyConfig config; + config.initial_capital = 1000.0; + config.default_qty_type = static_cast(QtyType::FIXED); + config.default_qty_value = 1.0; + config.pyramiding = 1; + config.margin_long = 100.0; + config.margin_short = 100.0; + config.commission_value = 0.0; + configure_pine_strategy(config); + set_syminfo_timezone("UTC"); + set_syminfo_session("24x7"); + set_syminfo_mintick(0.25); + } + void on_source_bar(const Bar&) override { + if (bar_index_ == 0 || bar_index_ == 2) + strategy_entry("unaffordable", true, kNa, kNa, 100000.0); + } +}; + +void recording_off_retires_without_a_query() { + for (const int count : {64, 256}) { + const auto bars = chart(count); + TradingHost host; + host.run(bars.data(), count, "1", "1"); + CHECK(host.last_error().empty()); + // Keep this before fill_report, broker_state_hash and reflection. A + // lazy hash fold would hide the production recording-off regression. + CHECK(host.market_admission_journal().events().empty()); + CHECK(host.market_admission_journal().events().capacity() <= 64); + CHECK(host.market_admission_journal().sequence_frontier() > 1); + CHECK(host.trade_count() == count / 2); + ReportC report{}; + host.fill_report(&report); + CHECK(report.broker_state_hash_len == 0); + BacktestEngine::free_report(&report); + } +} + +void recording_and_prefix_identity() { + const auto bars = chart(64); + TradingHost off; + TradingHost on; + TradingHost prefix; + on.set_broker_state_hash_recording(true); + prefix.set_broker_state_hash_recording(true); + off.run(bars.data(), 64, "1", "1"); + on.run(bars.data(), 64, "1", "1"); + prefix.run(bars.data(), 32, "1", "1"); + CHECK(off.last_error().empty() && on.last_error().empty() && prefix.last_error().empty()); + CHECK(off.market_admission_journal().events().empty()); + CHECK(on.market_admission_journal().events().empty()); + CHECK(prefix.market_admission_journal().events().empty()); + CHECK(off.broker_state_hash() == on.broker_state_hash()); + CHECK(off.trade_count() == on.trade_count()); + ReportC whole{}; + ReportC part{}; + on.fill_report(&whole); + prefix.fill_report(&part); + CHECK(whole.broker_state_hash_len == 64); + CHECK(part.broker_state_hash_len == 32); + if (whole.broker_state_hash_len == 64 && part.broker_state_hash_len == 32) { + for (int i = 0; i < 32; ++i) + CHECK(part.broker_state_hash[i] == whole.broker_state_hash[i]); + CHECK(whole.broker_state_hash[63] == on.broker_state_hash()); + } + const auto digest = on.broker_state_hash(); + CHECK(on.broker_state_hash() == digest); + BacktestEngine::free_report(&whole); + BacktestEngine::free_report(&part); +} + +void split_script_boundaries() { + const auto bars = chart(64 * 5); + for (const bool magnifier : {false, true}) { + TradingHost host; + host.run(bars.data(), static_cast(bars.size()), "1", "5", magnifier, + 4, MagnifierDistribution::ENDPOINTS); + CHECK(host.last_error().empty()); + CHECK(host.market_admission_journal().events().empty()); + CHECK(host.market_admission_journal().events().capacity() <= 64); + CHECK(host.trade_count() > 0); + } +} + +void rejection_reader_survives_retirement_and_reset() { + const auto bars = chart(64); + RejectionHost host; + host.run(bars.data(), static_cast(bars.size()), "1", "1"); + CHECK(host.last_error().empty()); + CHECK(host.market_admission_journal().events().empty()); + CHECK(compat::pine::last_rejected_command_bar(host.market_admission_journal()) == 2); + CHECK(host.trade_count() == 0); + // A fresh run must not inherit the prior run's remembered rejection. + host.run(bars.data(), 1, "1", "1"); + CHECK(host.last_error().empty()); + CHECK(host.market_admission_journal().events().empty()); + CHECK(compat::pine::last_rejected_command_bar(host.market_admission_journal()) == 0); +} +} // namespace + +int main() { + const struct Case { const char* name; void (*run)(); } cases[] = { + {"recording off without a query", recording_off_retires_without_a_query}, + {"recording and prefix identity", recording_and_prefix_identity}, + {"split script boundaries", split_script_boundaries}, + {"rejection reader and reset", rejection_reader_survives_retirement_and_reset}, + }; + for (const auto& test : cases) { + std::printf("CASE %s\n", test.name); + try { test.run(); } + catch (const std::exception& error) { + ++failures; + std::fprintf(stderr, "FAIL %s: %s\n", test.name, error.what()); + } + } + std::printf("admission journal window: %d checks, %d failures\n", checks, failures); + return failures ? 1 : 0; +} From 0d8b2031573db65da3d2db18ee05a3469707f813 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sun, 18 Oct 2026 23:55:45 +0800 Subject: [PATCH 10/35] feat(cr150): add borrowed symbol feed export --- README.md | 10 +- docs/design/cr150-contracts.md | 6 +- docs/pages/native-engine.md | 7 +- include/pineforge/engine.hpp | 2 +- include/pineforge/pineforge.h | 10 ++ .../pineforge/source/pine_strategy_host.hpp | 6 +- scripts/broker_state_hash_waivers.txt | 1 + scripts/check_c_abi_runtime.py | 8 +- scripts/test_ci_verify.py | 7 +- src/c_abi.cpp | 16 +++- src/source/pine_security_eval.cpp | 95 +++++++++++++++++-- src/source/pine_strategy_host.cpp | 30 ++++-- 12 files changed, 160 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 974b947c5..65abe2210 100644 --- a/README.md +++ b/README.md @@ -519,11 +519,11 @@ is in [CHANGELOG.md](CHANGELOG.md). The counts below include recorded outputs and describe the released 1.5.0 API. -A newly generated strategy `.so` exposes 96 compiled-strategy `PF_API` declarations -(80 runtime implementations plus sixteen generated exports) plus 43 native-host -declarations: 139 `PF_API` exports in total; `use_bar_magnifier = true` adds +A newly generated strategy `.so` exposes 97 compiled-strategy `PF_API` declarations +(81 runtime implementations plus sixteen generated exports) plus 43 native-host +declarations: 140 `PF_API` exports in total; `use_bar_magnifier = true` adds `strategy_declares_bar_magnifier` and a module that records [outputs](docs/outputs.md) its -three outputs exports, bringing the inventories to at most 100 and 143. Older modules lack the six opt-in +three outputs exports, bringing the inventories to at most 101 and 144. Older modules lack the six opt-in [checked-settings exports](docs/checked-settings.md). Older modules can also lack the two opt-in [execution-capability exports](docs/strategy-capabilities.md). In an optimized build `nm -gU` also shows libc++'s @@ -548,7 +548,7 @@ inventories are pinned by `scripts/check_c_abi_runtime.py`: | `strategy_set_chart_timezone` / `strategy_set_syminfo_timezone` / `strategy_set_syminfo_session` | Chart and exchange time | | `strategy_set_syminfo_mintick` / `_pointvalue` / `_metadata` / `_type` / `_string` | Symbol tick size, point value, numeric metadata, instrument class, string members | | `strategy_set_native_security_feed` / `strategy_set_aux_security_feed` | Feed `request.security()` from a native higher-timeframe series / an auxiliary bar-aligned feed | -| `strategy_set_symbol_feed` / `_feed_column` / `strategy_set_symbol_facts` / `strategy_set_recorded_series` | Another symbol's data for `request.security()` of that symbol: its own bars (each with its close) and named columns, its `syminfo.*` facts, and recorded request values per chart bar (each behind its own `PINEFORGE_HAS_…_V1` probe; historical runs only) | +| `strategy_set_symbol_feed` / `strategy_set_symbol_feed_borrowed_v1` / `_feed_column` / `strategy_set_symbol_facts` / `strategy_set_recorded_series` | Another symbol's data for `request.security()` of that symbol: its own copied or borrowed bars (each with its close) and named columns, its `syminfo.*` facts, and recorded request values per chart bar (each behind its own `PINEFORGE_HAS_…_V1` probe; historical runs only) | | `strategy_set_account_currency_fx_series` | Effective-time quote-to-account FX | | `strategy_get_last_error` | The latest runtime error | | `strategy_get_last_error_code` / `strategy_get_last_error_args` | The latest failure's stable code from the closed [run-failure vocabulary](docs/pages/run-failure-codes.md) (`""` when the run succeeded) and its typed arguments as canonical JSON; behind `PINEFORGE_HAS_RUN_FAILURE_CODES_V1` | diff --git a/docs/design/cr150-contracts.md b/docs/design/cr150-contracts.md index f103c497e..d175e1458 100644 --- a/docs/design/cr150-contracts.md +++ b/docs/design/cr150-contracts.md @@ -211,9 +211,9 @@ references; L15a must maintain these anchors and mechanically pin every current | Feed validation: bar values/order, close length/bounds, column length; configure and consumer entry points | `feed.bars` `src/native_run_spec.cpp:259-290`; `instrument_feed_shapes` `src/native_run_spec.cpp:743`; `validate_native_instrument_feeds` `src/native_run_spec.cpp:1019-1022`; `validate_native_timeframe_subscriptions` `src/native_execution_consumer.cpp:9536-9539`; `validate_native_timeframe_subscriptions` `src/native_execution_consumer.cpp:9561-9563` | 361-363 | | Adapter projection validation: use the context normalizer; host stages after lending and before projection, using a context built from its store | `normalize_projected_spec` `src/source/pine_adapter.cpp:2997-3003` | 361-363; V1/V2 change 5 | | Instrument-feed digest and cached/uncached continuation folds | `digest_instrument_feed_views` `src/native_run_spec.cpp:1162-1205`; `native_instrument_feeds_digest` `src/native_execution_consumer.cpp:321-323`; `native_instrument_feeds_digest` `src/native_execution_consumer.cpp:2774-2775`; `native_run_spec_digest` `src/native_execution_consumer.cpp:12807-12812` | 364-365 | -| Staged feed counts in host state and its hash | `installed_symbol_feed` `src/source/pine_strategy_host.cpp:230-238`; `staged_.instrument_feeds` `src/source/pine_state_hash.cpp:292-299` | 366-367 | -| Refresh of symbol-data digest and host hash fold | `refresh_symbol_data_digest` `src/source/pine_security_eval.cpp:1545-1553`; `symbol_data_digest_` `src/source/pine_state_hash.cpp:1123-1129` | 367-368 | -| Restore/store upkeep and column-setter length check | `held.bars` `src/source/pine_security_eval.cpp:1091-1101`; `data.bars` `src/source/pine_security_eval.cpp:1428-1431` | 369-370 | +| Staged feed counts in host state and its hash | `installed_symbol_feed` `src/source/pine_strategy_host.cpp:232-236`; `staged_.instrument_feeds` `src/source/pine_state_hash.cpp:292-299` | 366-367 | +| Refresh of symbol-data digest and host hash fold | `refresh_symbol_data_digest` `src/source/pine_security_eval.cpp:1552-1564`; `symbol_data_digest_` `src/source/pine_state_hash.cpp:1123-1129` | 367-368 | +| Restore/store upkeep and column-setter length check | `held.bars` `src/source/pine_security_eval.cpp:1091-1101`; `feed_count` `src/source/pine_security_eval.cpp:1433-1438` | 369-370 | | Lower-path preflight/validation | `NativeExecutionConsumer::preflight_intrabar_path` `src/native_execution_consumer.cpp:3216-3249` | 371,1221-1222 | | Lower-path digest (count first) and continuation hash | `native_intrabar_path_digest` `src/native_run_spec.cpp:891-919`; `native_intrabar_path_digest` `src/native_execution_consumer.cpp:288` | 371,1223-1224 | | Per-script-bar lower-path binary search/walk | `candidate` `src/native_execution_consumer.cpp:8630-8644` | 371,1225 | diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 3c158c39a..21d19c1fb 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -3966,10 +3966,9 @@ hands the runtime a callback table and gets the same kernel back. `` (included by `pineforge.h`) is that surface — 43 additive `PF_API` symbols implemented in `src/native_c_host.cpp` by `CCallbackHost`, a `final NativeStrategyHost` that forwards each existing -virtual to the table. No new virtual, no epoch bump, and nothing about the -established C ABI moves: the compiled-strategy runtime symbols and their -counts are untouched, and `scripts/check_c_abi_runtime.py` pins the new set as -a second, disjoint inventory. +virtual to the table. No epoch bump or reordering occurs: the borrowed +symbol-feed setter is an additive compiled-strategy runtime symbol, and +`scripts/check_c_abi_runtime.py` pins it alongside the established inventory. ```c #include diff --git a/include/pineforge/engine.hpp b/include/pineforge/engine.hpp index 47f7f4ffa..fc7546aeb 100644 --- a/include/pineforge/engine.hpp +++ b/include/pineforge/engine.hpp @@ -2087,7 +2087,7 @@ class BacktestEngine { virtual int observe_pending_effective_levels(int index, double* stop, double* limit, double* trail_activation) const; - virtual double observe_trail_best_price_v1() const; + virtual double observe_trail_best_price_v1() const; virtual bool set_symbol_feed_borrowed(const std::string&, const std::string&, const Bar*, const int64_t*, int) { guard_native_mutation("set_symbol_feed_borrowed"); return false; } double live_current_equity() const { return current_equity(); } // ABI v4 live-runtime surface (task 9): total SCRIPT bars dispatched by // the most recent run() (mirrors pf_report_t::script_bars_processed, diff --git a/include/pineforge/pineforge.h b/include/pineforge/pineforge.h index 6131e3fdc..42c5fa4d1 100644 --- a/include/pineforge/pineforge.h +++ b/include/pineforge/pineforge.h @@ -1861,6 +1861,16 @@ PF_API int strategy_signal_safety_receipt(pf_strategy_t s, char* json, size_t ca /** @} */ /* end of pf_outputs */ +/** Feature probe for the borrowed symbol-feed setter. */ +#define PINEFORGE_HAS_SYMBOL_FEED_BORROWED_V1 1 +/** Install another symbol's bars without copying them. The caller keeps both + * arrays alive until the feed is replaced or cleared, or #strategy_free. + * Validation and return values match #strategy_set_symbol_feed. Historical + * runs only: stream_begin() fails closed while a symbol feed is installed. */ +PF_API int strategy_set_symbol_feed_borrowed_v1( + pf_strategy_t s, const char* key, const char* timeframe, + const pf_bar_t* bars, const int64_t* close_ms, int n); + #ifdef __cplusplus } /* extern "C" */ #endif diff --git a/include/pineforge/source/pine_strategy_host.hpp b/include/pineforge/source/pine_strategy_host.hpp index 3472d8693..1b24f8c01 100644 --- a/include/pineforge/source/pine_strategy_host.hpp +++ b/include/pineforge/source/pine_strategy_host.hpp @@ -444,7 +444,7 @@ class PineStrategyHost : public NativeStrategyHost, public BrokerStateHashProvid // The request-data doors (pineforge.h strategy_set_symbol_feed and its // siblings); see BacktestEngine for the contract (lane XSYM-D). bool set_symbol_feed(const std::string& key, const std::string& timeframe, - const Bar* bars, const int64_t* close_ms, int n) override; + const Bar* bars, const int64_t* close_ms, int n) override; bool set_symbol_feed_borrowed(const std::string& key, const std::string& timeframe, const Bar* bars, const int64_t* close_ms, int n) override; bool set_symbol_feed_column(const std::string& key, const std::string& timeframe, const std::string& name, const double* values, int n) override; @@ -947,7 +947,7 @@ class PineStrategyHost : public NativeStrategyHost, public BrokerStateHashProvid // this host's store, or the kernel's run spec while they are lent to it. const NativeInstrumentFeed& installed_symbol_feed(std::size_t i) const; // Copies lent bytes back from the kernel's spec, which keeps its own. - void restore_symbol_feeds(); + void restore_symbol_feeds(); void stage_symbol_feed_views(); // The same for a request-data door: the reason it failed, or empty. std::string restore_symbol_feeds_reason(); // True while the running spec names this host's sites: the kernel steps @@ -1152,7 +1152,7 @@ class PineStrategyHost : public NativeStrategyHost, public BrokerStateHashProvid // The foreign site whose payload is being evaluated: its requested // context answers pine_bar_index(), time_close() and syminfo_ for the // length of the dispatch, and nothing outside one. Never durable. - const ForeignSecuritySite* foreign_context_ = nullptr; + struct BorrowedFeedSlot { const Bar* bars = nullptr; const std::int64_t* close_ms = nullptr; std::size_t size = 0; bool present = false; }; std::vector symbol_feed_views_; const ForeignSecuritySite* foreign_context_ = nullptr; // Provider configuration, not book state: it is staged before a run and // projected into the adapter at begin, so it is waived from the durable diff --git a/scripts/broker_state_hash_waivers.txt b/scripts/broker_state_hash_waivers.txt index 25f82e901..95646d19a 100644 --- a/scripts/broker_state_hash_waivers.txt +++ b/scripts/broker_state_hash_waivers.txt @@ -82,3 +82,4 @@ symbol_calendar_opens_ # C-ABI transport of that calendar (syminfo metadata "sym waypoint_chain_bar_ # Transient within-bar value: the script bar whose path took a waypoint margin call (rest_waypoint_margin_call), set on that bar's path and cleared by the same bar's post-script close; compared only for equality with the current script bar, so it is dead at every bar boundary. script_average_held_ # Transient within-pass flag: hold_script_average_price sets it after a current-close exit flattened the book, and release_script_average_price clears it right after on_source_bar returns, before the pass's orders settle; it is false at every fold (the same class as security_history_publication_replay_). symbol_feeds_lent_ # Ownership flag: whether the installed symbol feeds' bars are held in symbol_feeds_ or lent to the kernel's run spec; the folds read the same keys, counts and digest either way and no decision reads it (AR-R4). +symbol_feed_views_ # Caller-owned feed descriptors are representation-only; logical feed bytes and counts fold through symbol_data_digest_ and the native run-spec component digest (CR-150 L15b). diff --git a/scripts/check_c_abi_runtime.py b/scripts/check_c_abi_runtime.py index 143071835..64fce0901 100644 --- a/scripts/check_c_abi_runtime.py +++ b/scripts/check_c_abi_runtime.py @@ -87,6 +87,7 @@ "strategy_configure_native_fx_curve_v1", "strategy_configure_native_fx_curve_ext_v1", "strategy_set_symbol_feed", + "strategy_set_symbol_feed_borrowed_v1", "strategy_set_symbol_feed_column", "strategy_set_symbol_facts", "strategy_set_recorded_series", @@ -125,18 +126,19 @@ "selected_window.h", ) -EXPECTED_PUBLIC_DECLARATIONS = 100 -EXPECTED_RUNTIME_IMPLEMENTATIONS = 80 +EXPECTED_PUBLIC_DECLARATIONS = 101 +EXPECTED_RUNTIME_IMPLEMENTATIONS = 81 # Runtime setters whose data a stream cannot take: the source host refuses # stream_begin() while any of them holds data (the native daily feed since its -# lane, and lane XSYM-D's four symbol-data setters). Each declaration's own +# lane, and lane XSYM-D's five symbol-data setters). Each declaration's own # doc comment in pineforge.h must say so, so the contract cannot drop out of # the header while the refusal stays in the host (or the other way round # unnoticed): the phrase is pinned below. HISTORICAL_ONLY_RUNTIME = frozenset({ "strategy_set_native_security_feed", "strategy_set_symbol_feed", + "strategy_set_symbol_feed_borrowed_v1", "strategy_set_symbol_feed_column", "strategy_set_symbol_facts", "strategy_set_recorded_series", diff --git a/scripts/test_ci_verify.py b/scripts/test_ci_verify.py index 9ddd7f0b2..289eb9caf 100644 --- a/scripts/test_ci_verify.py +++ b/scripts/test_ci_verify.py @@ -1203,8 +1203,8 @@ def test_public_and_runtime_export_counts_are_exact(self): header = [name for relative in RUNTIME_HEADERS for name in _pf_api_names(ROOT / 'include/pineforge' / relative)] runtime = _pf_api_names(ROOT / 'src/c_abi.cpp') - self.assertEqual(EXPECTED_PUBLIC_DECLARATIONS, 100) - self.assertEqual(EXPECTED_RUNTIME_IMPLEMENTATIONS, 80) + self.assertEqual(EXPECTED_PUBLIC_DECLARATIONS, 101) + self.assertEqual(EXPECTED_RUNTIME_IMPLEMENTATIONS, 81) self.assertEqual(len(EXPECTED_RUNTIME), EXPECTED_RUNTIME_IMPLEMENTATIONS) self.assertEqual(len(header), EXPECTED_PUBLIC_DECLARATIONS) self.assertEqual(len(header), len(set(header))) @@ -1214,7 +1214,8 @@ def test_public_and_runtime_export_counts_are_exact(self): self.assertIn('strategy_configure_native_fx_curve_v1', runtime) self.assertIn('strategy_configure_native_fx_curve_ext_v1', header) self.assertIn('strategy_configure_native_fx_curve_ext_v1', runtime) - for name in ('strategy_set_symbol_feed', 'strategy_set_symbol_feed_column', + for name in ('strategy_set_symbol_feed', 'strategy_set_symbol_feed_borrowed_v1', + 'strategy_set_symbol_feed_column', 'strategy_set_symbol_facts', 'strategy_set_recorded_series'): self.assertIn(name, header) self.assertIn(name, runtime) diff --git a/src/c_abi.cpp b/src/c_abi.cpp index 5ca7ea09c..e24049bf8 100644 --- a/src/c_abi.cpp +++ b/src/c_abi.cpp @@ -10,7 +10,8 @@ * - The runtime-library-side `extern "C"` symbols (the closed-trade * incarnation accessor, setters, strategy_get_last_error, * strategy_get_last_error_code, strategy_get_last_error_args, the - * security-feed and symbol-data setters, the strategy_stream_* + * security-feed and symbol-data setters (including the borrowed feed + * export), the strategy_stream_* * lifecycle, the strategy_outputs_* readers, native stream bar * input/action polling/fingerprint/API-version exports, the live-runtime * surface (strategy_request_abort, strategy_last_run_status, @@ -1301,4 +1302,17 @@ PF_API int strategy_selected_window_counts_v1( static_cast(s), out); } +PF_API int strategy_set_symbol_feed_borrowed_v1( + pf_strategy_t s, const char* key, const char* timeframe, + const pf_bar_t* bars, const int64_t* close_ms, int n) { + return pf_cabi_int(s, [&] { + if (!s || !key || !timeframe || n < 0 || (n > 0 && (!bars || !close_ms))) return -1; + const auto* native = reinterpret_cast(bars); + return static_cast(s) + ->set_symbol_feed_borrowed(std::string(key), std::string(timeframe), + native, close_ms, n) + ? 0 : -1; + }); +} + } /* extern "C" */ diff --git a/src/source/pine_security_eval.cpp b/src/source/pine_security_eval.cpp index e19acc887..1d88c1a6c 100644 --- a/src/source/pine_security_eval.cpp +++ b/src/source/pine_security_eval.cpp @@ -1091,12 +1091,12 @@ void source::PineStrategyHost::restore_symbol_feeds() { for (std::size_t i = 0; i < symbol_feeds_.size(); ++i) { const NativeInstrumentFeed& held = spec->instrument_feeds[i]; NativeInstrumentFeed& feed = symbol_feeds_[i]; - const auto views = NativeExecutionConsumer::bound(*this).bar_views(); - const auto* slot = views.instrument_feed_count == 0 - ? nullptr : views.instrument_feeds[i]; - if (slot == nullptr) { + if (i >= symbol_feed_views_.size() || !symbol_feed_views_[i].present) { feed.bars = held.bars; feed.close_ms = held.close_ms; + } else if (!held.bars.empty() || !held.close_ms.empty()) { + throw coded(RunFailureCode::engine_invariant, {}, + "request data: borrowed feed acquired owned bytes"); } for (std::size_t c = 0; c < feed.columns.size(); ++c) feed.columns[c].values = held.columns[c].values; @@ -1391,9 +1391,14 @@ bool source::PineStrategyHost::set_symbol_feed(const std::string& key, if (const std::string why = restore_symbol_feeds_reason(); !why.empty()) return refuse("restore_failed", why); if (existing >= 0) { - symbol_feeds_[static_cast(existing)] = std::move(feed); + const std::size_t at = static_cast(existing); + symbol_feeds_[at] = std::move(feed); + symbol_feed_views_[at] = {}; } else { + symbol_feeds_.reserve(symbol_feeds_.size() + 1); + symbol_feed_views_.reserve(symbol_feed_views_.size() + 1); symbol_feeds_.push_back(std::move(feed)); + symbol_feed_views_.push_back({}); } refresh_symbol_data_digest(); clear_run_failure(*this); @@ -1425,10 +1430,12 @@ bool source::PineStrategyHost::set_symbol_feed_column(const std::string& key, if (const std::string why = restore_symbol_feeds_reason(); !why.empty()) return refuse("restore_failed", why); NativeInstrumentFeed& feed = symbol_feeds_[static_cast(found)]; - const auto data = native_instrument_feed_view(feed, nullptr); - if (n < 0 || static_cast(n) != data.bars.size || (n > 0 && values == nullptr)) + const std::size_t feed_count = static_cast(found) < symbol_feed_views_.size() + && symbol_feed_views_[static_cast(found)].present + ? symbol_feed_views_[static_cast(found)].size : feed.bars.size(); + if (n < 0 || static_cast(n) != feed_count || (n > 0 && values == nullptr)) return refuse("column_length", "column '" + name + "' has " + std::to_string(n) - + " values for " + std::to_string(data.bars.size) + " bars"); + + " values for " + std::to_string(feed_count) + " bars"); // The kernel's judgement of the name, here rather than at the next begin: // a feed of the same key without bars carries it (the count is checked // above, and no column value is refused). @@ -1548,7 +1555,11 @@ void source::PineStrategyHost::refresh_symbol_data_digest() noexcept { for (const auto& feed : symbol_feeds_) { f.s(feed.instrument); f.s(feed.tf); - f.u(native_instrument_feed_view(feed, nullptr).bars.size); + const std::size_t count = (&feed - symbol_feeds_.data()) < symbol_feed_views_.size() + && symbol_feed_views_[static_cast(&feed - symbol_feeds_.data())].present + ? symbol_feed_views_[static_cast(&feed - symbol_feeds_.data())].size + : feed.bars.size(); + f.u(count); f.u(feed.columns.size()); for (const auto& column : feed.columns) f.s(column.name); } @@ -1577,3 +1588,69 @@ void source::PineStrategyHost::refresh_symbol_data_digest() noexcept { } } // namespace pineforge + +namespace pineforge { + +bool source::PineStrategyHost::set_symbol_feed_borrowed( + const std::string& key, const std::string& timeframe, + const Bar* bars, const int64_t* close_ms, int n) { + guard_native_mutation("set_symbol_feed_borrowed"); + const auto refuse = [this](const char* reason, const std::string& why, + const char* field = nullptr) { + RunFailureArgs args{{"reason", reason}}; + if (field) args.emplace_back("field", field); + note_run_failure(*this, "strategy_set_symbol_feed_borrowed_v1: " + why, + RunFailureCode::symbol_feeds_refused, args); + return false; + }; + if (run_in_progress(*this)) return refuse("run_in_progress", kRunInProgress); + if (key.empty()) return refuse("empty_key", "empty symbol key"); + const std::string tf = canonical_symbol_timeframe(timeframe); + if (tf.empty() || !native_calendar::parse_timeframe(tf)) + return refuse("timeframe_unparsable", "timeframe '" + timeframe + "' does not parse"); + if (n < 0 || (n > 0 && (bars == nullptr || close_ms == nullptr))) + return refuse("invalid_bars", "invalid bar array"); + + const std::size_t count = static_cast(n); + NativeInstrumentFeedView view{{bars, count}, close_ms}; + const NativeInstrumentFeedView* slots[] = {&view}; + NativeBarViews views{slots, 1}; + std::vector judged_feeds(1); + judged_feeds.front().instrument = key; + judged_feeds.front().tf = tf; + const auto judged = validate_native_instrument_feeds(judged_feeds, views); + if (!judged) { + return refuse("kernel_refused", "feed refused (NativeRunSpecError " + + std::to_string(static_cast(judged.error)) + ", field " + + std::to_string(static_cast(judged.field)) + ")", + instrument_feed_field_name(judged.field)); + } + if (const std::string why = restore_symbol_feeds_reason(); !why.empty()) + return refuse("restore_failed", why); + + const std::int64_t existing = find_symbol_feed(key, tf); + const BorrowedFeedSlot replacement{bars, close_ms, count, true}; + if (existing >= 0) { + const std::size_t at = static_cast(existing); + NativeInstrumentFeed& feed = symbol_feeds_[at]; + feed.instrument = key; + feed.tf = tf; + feed.bars.clear(); + feed.close_ms.clear(); + feed.columns.clear(); + symbol_feed_views_[at] = replacement; + } else { + symbol_feeds_.reserve(symbol_feeds_.size() + 1); + symbol_feed_views_.reserve(symbol_feed_views_.size() + 1); + NativeInstrumentFeed feed; + feed.instrument = key; + feed.tf = tf; + symbol_feeds_.push_back(std::move(feed)); + symbol_feed_views_.push_back(replacement); + } + refresh_symbol_data_digest(); + clear_run_failure(*this); + return true; +} + +} // namespace pineforge diff --git a/src/source/pine_strategy_host.cpp b/src/source/pine_strategy_host.cpp index bd14070c0..ce4cbd01c 100644 --- a/src/source/pine_strategy_host.cpp +++ b/src/source/pine_strategy_host.cpp @@ -230,12 +230,10 @@ StagedConfiguration source::PineStrategyHost::staged_configuration() const { staged.instrument_feeds.reserve(symbol_feeds_.size()); for (std::size_t i = 0; i < symbol_feeds_.size(); ++i) { const NativeInstrumentFeed& feed = installed_symbol_feed(i); - const auto views = NativeExecutionConsumer::bound(*this).bar_views(); - const auto* slot = views.instrument_feed_count == 0 - ? nullptr : views.instrument_feeds[i]; - const auto data = native_instrument_feed_view(feed, slot); + const std::size_t count = i < symbol_feed_views_.size() && symbol_feed_views_[i].present + ? symbol_feed_views_[i].size : feed.bars.size(); staged.instrument_feeds.push_back( - {feed.instrument, feed.tf, data.bars.size, feed.columns.size()}); + {feed.instrument, feed.tf, count, feed.columns.size()}); } return staged; } @@ -346,7 +344,7 @@ void source::PineStrategyHost::prepare_native_begin(const NativeBeginArgs& args) std::vector lent = lend_feed_bytes(symbol_feeds_); NativeRunSpec spec = [&] { try { - NativeExecutionConsumer::bound(*this).stage_instrument_feed_views(nullptr, 0); + stage_symbol_feed_views(); return adapter_.project(effective, staged, args, path_order, &lent); } catch (...) { return_feed_bytes(symbol_feeds_, lent); @@ -3071,3 +3069,23 @@ int64_t source::PineStrategyHost::pine_time_offset(int64_t bar_open_ms, int bars } } // namespace pineforge + +namespace pineforge { + +void source::PineStrategyHost::stage_symbol_feed_views() { + if (symbol_feed_views_.size() != symbol_feeds_.size()) + throw coded(RunFailureCode::engine_invariant, {}, + "request data: symbol feed view table is out of sync"); + std::vector descriptors(symbol_feed_views_.size()); + std::vector slots(symbol_feed_views_.size(), nullptr); + for (std::size_t i = 0; i < symbol_feed_views_.size(); ++i) { + const BorrowedFeedSlot& slot = symbol_feed_views_[i]; + if (!slot.present) continue; + descriptors[i] = {{slot.bars, slot.size}, slot.close_ms}; + slots[i] = &descriptors[i]; + } + NativeExecutionConsumer::bound(*this).stage_instrument_feed_views( + slots.empty() ? nullptr : slots.data(), slots.size()); +} + +} // namespace pineforge From 731a7217b3ef609d6fbb5c0ef4f0da928fa847f8 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sun, 18 Oct 2026 23:58:39 +0800 Subject: [PATCH 11/35] test(cr150-l19h): specify settled-prefix parking --- tests/test_bracket_roster_parking.cpp | 55 ++++++++++++++------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/tests/test_bracket_roster_parking.cpp b/tests/test_bracket_roster_parking.cpp index 48afde671..f62465997 100644 --- a/tests/test_bracket_roster_parking.cpp +++ b/tests/test_bracket_roster_parking.cpp @@ -1,36 +1,36 @@ -// R5 lane V19-D witness: a bracket family's roster parks the erased members -// behind a retained one, and stays the same roster. +// CR-150 L19h witness: a bracket family's roster parks erased members behind +// a retained one, and the settled prefix keeps only its fold state. // // source::BracketRoster keeps every leg a strategy.exit family placed, in -// order (the bracket cancel cancels each of them, and a leg that no longer -// works leaves a NotWorking receipt whose ordinal is observable). V19-E kept -// the members whose placement rows were erased as a settled prefix of -// incarnations and every member from the first retained one onward as a -// handle, which the v4 fold reads in full: a family whose first leg a pin -// keeps (a leg of the current position cycle) held every later member as a -// handle, and a recording run paid a fold over all of them per bar (INT21's -// straddle: 0.15 / 1.67 / 23.5 s at 1k / 4k / 16k bars). V19-D parks every -// erased member behind a retained one in a run between the two handles it -// sits between. +// order. V19-E kept the members whose placement rows were erased as a +// settled prefix of incarnations and every member from the first retained +// one onward as a handle, which the v4 fold reads in full: a family whose +// first leg a pin keeps (a leg of the current position cycle) held every +// later member as a handle, and a recording run paid a fold over all of them +// per bar (INT21's straddle: 0.15 / 1.67 / 23.5 s at 1k / 4k / 16k bars). +// V19-D parks every erased member behind a retained one in a run between the +// two handles it sits between. L19h drops the settled prefix's varint bytes; +// its count, last incarnation and digest remain, while walks begin after +// that prefix. Interior parked runs remain visible until they are absorbed. // // Randomized operation streams -- legs appended, rows erased (a member's row // only ever goes), settles, and live legs removed (the bracket cancel of a // part of the family) -- are played on a roster and on a plain model: the // members in order, each marked erased or not. After every operation the -// roster must list the model's members in order (members(), iteration, -// size()); after every settle its working tail must be exactly the members -// whose rows are retained, count() and holds() must answer for them, and -// its fold words (settled_count(), settled_digest()) must be the model's: +// roster must list the model's visible members in order (members(), +// iteration; size() still includes the settled prefix); after every settle +// its working tail must be exactly the members whose rows are retained, +// count() and holds() must answer for them, and its fold words +// (settled_count(), settled_digest()) must be the model's: // the maximal erased prefix's count and running digest, then each run of // erased members between retained ones as (handles before it, count, digest). // A second roster that settles only at the end reaches the same words. // -// Fail-before, this TU against the lane's base 6c081f5d (spark aarch64, -// GCC 13, Release): it builds, and fails 282,041 of 2,237,468 checks, first -// roster.working_tail() == words.tail, roster.settled_digest() == -// words.digest and roster.holds(member.handle) == held -- the working tail -// kept every member from the first retained one onward, erased ones -// included. +// Fail-before on d61e68b9 (the L0b base): the earlier V19-D implementation +// still materializes the settled prefix, so the first settle after a leading +// erased run returns those handles from members()/iteration instead of +// starting after the settled part. This is the feature-missing red reason; +// the existing V19-D checks remain in the same witness. #include #include @@ -119,15 +119,16 @@ Words expected(const std::vector& model) { bool same_members(const source::BracketRoster& roster, const std::vector& model) { const auto members = roster.members(); - if (members.size() != model.size() || roster.size() != model.size()) return false; - std::size_t index = 0; + const std::size_t settled = std::min(roster.settled_count(), model.size()); + if (members.size() + settled != model.size() || roster.size() != model.size()) return false; + std::size_t index = settled; for (const auto& handle : roster) { if (index >= model.size() || !(handle == model[index].handle)) return false; ++index; } - for (std::size_t at = 0; at < model.size(); ++at) - if (!(members[at] == model[at].handle)) return false; - return index == model.size(); + for (std::size_t at = 0; at < members.size(); ++at) + if (!(members[at] == model[settled + at].handle)) return false; + return index == model.size() && members.size() + settled == roster.size(); } int streams = 0; From b8b617ed6accdf36756cbc1e0abb487225fa52b2 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Mon, 19 Oct 2026 00:36:18 +0800 Subject: [PATCH 12/35] fix(cr150): repair L15b lab preflight failures --- README.md | 2 +- docs/adr/0001-kernel-adapter-boundary.md | 36 +++++++++--------- docs/coverage.md | 4 +- docs/design/cr150-contracts.md | 2 +- docs/design/native-feature-parity.md | 48 ++++++++++++------------ docs/pages/abi-stability.md | 4 +- docs/pages/index.md | 4 +- docs/pages/native-engine.md | 9 +++-- scripts/check_native_cpp_versions.py | 8 ++-- src/source/pine_security_eval.cpp | 6 ++- 10 files changed, 65 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 65abe2210..758e62023 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ The separate PineForge compiler, [`pineforge-codegen`](https://github.com/pinefo - **Open runtime.** The engine and native live runner are Apache-2.0. The separately distributed [PineForge compiler](https://github.com/pineforge-4pass/pineforge-codegen-oss/blob/main/LICENSE) is source-available under the PineForge Source License 1.2 from its release 1.3.0 (release 1.2.0 keeps the PineForge Source License 1.1, and releases up to 1.1.0 keep the PolyForm Noncommercial terms they shipped with); personal trading is free, and commercial use requires a separate license; the capital in an account a proprietary-trading firm or funded-trader program provides or allocates, including a challenge, evaluation or simulated account, is investment capital. Public reference strategies, benchmarks and validation tooling are available in their respective repositories; the community-script test set is not redistributed. - **Fast.** In-process, no interpreter: median **36× faster than PyneCore** on the 200 strategies both engines time (100 corpus probes and 100 held-out private strategies), measured at engine `35db01c8` on 2026-09-29 on an AWS c7a.8xlarge with PyneCore timed as a subprocess, its interpreter start-up included (PineForge runs a median 807k bars/s per strategy over its 201 slots with the bar magnifier on; [method](benchmarks/results/speed.md)). Parameter sweeps re-run a loaded `.so` with new inputs — no recompile, no fork. - **Deterministic to the bit.** Two runs with the same inputs produce identical trade lists. Same on Linux and macOS. -- **Yours to embed.** 145 `PF_API` declarations across five installed headers — `pineforge.h`'s 94 (74 of the 80 runtime exports, the other six being declared in `execution_observer.h` and `selected_window.h`, three each, and up to 20 per-strategy functions a generated module defines), `native_c_api.h`'s 43 native-host declarations and `selected_window_plan.h`'s two planner declarations — an ABI that is append-only within a major version from 1.0 on. Call it from C, Python, Rust, Go, Node, Julia — or let an AI agent drive it over MCP. +- **Yours to embed.** 145 `PF_API` declarations across five installed headers — `pineforge.h`'s 95 (75 of the 81 runtime exports, the other six being declared in `execution_observer.h` and `selected_window.h`, three each, and up to 20 per-strategy functions a generated module defines), `native_c_api.h`'s 43 native-host declarations and `selected_window_plan.h`'s two planner declarations — an ABI that is append-only within a major version from 1.0 on. Call it from C, Python, Rust, Go, Node, Julia — or let an AI agent drive it over MCP. Recorded outputs and their additional API declarations described here are part of **1.5.0**. diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index bca57350b..52821b662 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -211,11 +211,11 @@ into kernel driving — never by special-casing the kernel *for a specific scrip kernel and own these quirks, each at its site: - **Seams the adapter overrides.** The kernel is driven, not patched. `PineStrategyHost` overrides - `resolve_execution_terms` (`pine_strategy_host.cpp:828`) for TV sizing and fill spelling; - `validate_execution_precommit` (`pine_strategy_host.cpp:853`), whose + `resolve_execution_terms` (`pine_strategy_host.cpp:826`) for TV sizing and fill spelling; + `validate_execution_precommit` (`pine_strategy_host.cpp:851`), whose `validate_precommit` (`pine_adapter.cpp:16373`) returns `AdmitWithHostMargin` (`pine_adapter.cpp:16577`) to own the opening margin decision; `on_native_tick` - (`pine_strategy_host.cpp:555`) / `on_native_applied` (`pine_strategy_host.cpp:627`) for + (`pine_strategy_host.cpp:553`) / `on_native_applied` (`pine_strategy_host.cpp:625`) for `calc_on_order_fills` re-entry. It does not own lot excursions: since R5 lane H-THIN it keeps the kernel's `owns_lot_excursions()` default, so the kernel's sampler books every Pine lot's MFE/MAE, and `ab9714be`'s host model (RULING A48) is gone. R5 lane H-MEASURE had measured the @@ -244,7 +244,7 @@ kernel and own these quirks, each at its site: `pine_adapter.cpp:558-564`, twin `tv_money_round` `pine_policy_support.hpp:9-15`). - **Quantity dust.** After every applied execution the Pine host erases any lot of at most `kQtyEpsilon` (`1e-10`) from the book it shares with the kernel, with no closing row - (`on_native_applied` `pine_strategy_host.cpp:627`), inside a live book as well as at a flat + (`on_native_applied` `pine_strategy_host.cpp:625`), inside a live book as well as at a flat reset — broader than `ab9714be`, which reset only a whole book at or under that size. Measured by R5 lane H-MEASURE (`tests/test_pine_dust_sweep_paired.cpp`): TradingView's decimal quantities leave no remnant row on a `lab tv` tape, which the Pine host books row for row @@ -340,7 +340,7 @@ kernel and own these quirks, each at its site: position-view freezing (`PineLanguageState` `pine_language_state.hpp:12`); the session flags, `PineStrategyHost` members since R5 lane F5 (`session_ismarket_` `pine_strategy_host.hpp:1186`), which `scheduler_update_session_state` - (`pine_strategy_host.cpp:2228`) selects from the kernel's session-day facts before each source + (`pine_strategy_host.cpp:2226`) selects from the kernel's session-day facts before each source callback — `session_ismarket_` and the `_regular` pair are those facts as they are, and the plain `session.isfirstbar` / `islastbar` pair widens the regular day by the pre- and post-market bars an extended-hours chart holds (`update_extended_session_day`, Pine policy on TradingView's @@ -423,7 +423,7 @@ citations, 264 are in `src/source/` and its headers; 6 sit in kernel files (`float_band_eq` `ta_compare_band.hpp:38`), the TV-named fields of the frozen `pf_pending_order_v1_t` mirror, and the lot-flag pair above. - **The C API trades.** Two disjoint runtime inventories, both pinned. - `src/c_abi.cpp` implements the **80** codegen-facing runtime `PF_API` symbols that + `src/c_abi.cpp` implements the **81** 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:2682`), runs a batch @@ -600,17 +600,17 @@ name is usually a plain word. The fourth audit found these gaps (AUDIT4-opus X10 | the margin model's check points on an intrabar path -- `NativeMarginCheckKind::IntrabarSample` (`include/pineforge/native_host.hpp`; C `PF_NATIVE_MARGIN_CHECK_INTRABAR_SAMPLE`), offered at every delivered sample after the script bar's first of a `lower_tf` path matched as `ContinuousSegments`, at that sample's price, immediately before it is matched (`NativeExecutionConsumer::deliver_intrabar_script`, `src/native_execution_consumer.cpp`) | every host with a margin model and such a path, through `margin_check_allowed`; the Pine adapter admits it on a magnified run's leveraged long and, outside `process_orders_on_close`, its short at any margin (R5 lane PAR-MARGIN-2) (`PineExecutionAdapter::intrabar_sample_checked`, `src/source/pine_adapter.cpp`) | **a check point of the kernel's own** (R5 lane PAR-MARGIN, item 3; H-MEASURE's acid finding A). Rule 2 (a), mechanism: a run with no whole-bar waypoint model measures its maintenance requirement where its path is -- at each delivered sample -- the rule the docs and `native_run_spec.hpp` stated and the kernel had not kept (it checked the first sample and after fills only); nothing in it knows a venue, and a host that checks somewhere else suppresses the points it does not share, as for every other kind. Rule 2 (b), knob: none -- the path is the run's own declaration, and a one-price distribution path (`synthesized`, `DistributionSamples`) is not offered the point, since a request armed at a discrete point is matched only at a later one. What moved: a margin run on a continuous intrabar path whose book crosses its line inside a bar now books that call at the crossing sample (H-MEASURE's intrabar probe: 55 points offered, a slice at 105 where none was booked). TradingView's magnified broker does the same at its own intrabars, 2 minutes on a 15-minute chart (the private fixture set `intrabar_margin`: the first call at the first lower-timeframe low that crosses on 4 of 4 tapes; a model of the check at TradingView's 2-minute intrabars books every row of all 20 magnified tapes, R5 lane PAR-MARGIN-2). No pinned value moved. Pinned by `tests/test_native_margin_intrabar_samples.cpp` (kernel-only) and the "intrabar margin on tapes" section of the private test `test_adapter_margin_schedule_differential`. | | the path a post-fill margin check measures -- `NativeExecutionConsumer::margin_segment_origin` (`src/native_execution_consumer.cpp`), the phase the `AfterApplied` re-arm hands `margin_sizing_price`: the waypoint the segment into the fill's driver point starts from, with the fill price as the point's own mark | every host with a margin model on a path walked by its modeled waypoints (no intrabar path), through the `AfterApplied` point `margin_check_allowed` admits | **a mechanism correction** (R5 lane PAR-MARGIN-2). Rule 2 (a), mechanism: a request matched at a driver point was reached on the segment into it, so the book its fill leaves still faces that point's waypoint -- a limit filled on its way down to the bar's low faces the low, a short filled on its way up faces the high -- and the scan that started strictly after the point's phase measured a path the book had not finished; the waypoints the path already passed stay out, as `fx_roll_margin_check_at`'s continuous roll measures from its segment's origin. Nothing in it knows a venue. Rule 2 (b), knob: none. What moved: a post-fill point after a fill matched on a segment now measures that segment's destination waypoint -- on a high-first bar O100 H101 L80 C81 a buy limit at 95 measures 80 where it measured the close 81, and a low-first bar or a short filled on its way up is called where nothing was (`tests/test_native_margin_post_fill_path.cpp`, kernel-only); a fill at a waypoint's own price, the open's included, measures what it did. TradingView books that call on the fill's bar (lab tv tapes `pm2-m7-lim-*` of the private fixture set `margin_entry_bar` at the low, `pm2-m7-slim-*` and `pm2-m7-s1lim-*` at the high, 12 of 12; with the adapter's admissions `pm2-m7-poocl-*` and `pm2-m7b-lim-*` too), as `ab9714be`'s entry-bar suffix did; the differential's M7-B pin moved to that answer, 10 @80. One v19 witness value moved with it: `tests/test_publication_witness.cpp` Config03, a short filled on its way up a bar, now called at that bar's high (461a919ebdcd41e9 -> 4593ef08ab33e045); the corpus is 312/312, moved 0. | | the samples of a retained lower bar -- `NativeExecutionConsumer::deliver_intrabar_script` (`src/native_execution_consumer.cpp`): a bar the direct four-sample walk declines (a leg of zero length, an open at its own low) is walked through its own four turning points, the repeated one included, where the general `ENDPOINTS` sampler filled the missing point uniformly; a bar that traded nothing at one price (volume 0, open = high = low = close) is one discrete point, walked once | every host with a `lower_tf` path; the Pine adapter's magnified path marks a chart bar's own open and close this way (R5 lane MAG-INTRABAR, `source::tradingview_magnifier_bars`, `src/source/magnifier_intrabars.cpp`) | **a mechanism correction** (R5 lane MAG-INTRABAR). Rule 2 (a), mechanism: a retained bar supplies its own four ticks, and every sample is a check point of the margin model since R5 lane PAR-MARGIN, so a sample between them measured -- and a request matched at -- a price the bar did not print at that point of its path; a bar that traded nothing is one quote with no path, and its four coincident samples were three path points, three phases and three margin checks that are not there. Nothing in it knows a venue: the rule reads the bar's own prices and volume. Rule 2 (b), knob: none. TradingView's magnified broker walks an intrabar as its four ticks whatever their values (tape `pm2-i3-pooc-mag-0402-2000` of the private fixture set `intrabar_margin`: its first call is booked at an intrabar that opens at its low). What moved: an open-at-its-low bar's second mark is its low, 60, where the sampler put 77.5, and the call armed at that open is booked there; a zero-volume one-price bar gives one mark where it gave four (`tests/test_native_intrabar_turning_points.cpp`, kernel-only), and the sampler test's lower-path expectation moved with it (`tests/test_magnifier_ordered_sampler.cpp`: a sub-bar with a zero-length leg walks its corners). No witness value moved; the corpus runs no lower path (312/312, moved 0). | -| the presented bar -- `current_bar_`, `bar_index_`, `prev_bar_timestamp_` (`include/pineforge/engine.hpp`) | the kernel at every callback point: the consumer presents the bar at the point's instant (`src/native_execution_consumer.cpp`). For the body it runs, the Pine host then presents its own script bar. `scheduler_publish_source_bar` and `scheduler_publish_suppressed_tail` write the whole `current_bar_`, stamped at the bar's label (`current_bar_ = bar` `pine_strategy_host.cpp:2365` and `:2495`). They also write the slot's `bar_index_`, restored after a temporary publication, and each published slot's `prev_bar_timestamp_`. `PineScheduler::input` holds `bar_index_` at the input's own index for one input (`InputBarIndexScope` `pine_scheduler_native.cpp:427`) | **retained, the host's view; the fourth audit's E21 check e**. Execution reads its cursor, not this bar: `execute_current` converts at its cursor's rate and neither writes nor restores the host's clock (Section B's row). The broker-state hash folds none of the three, and `scripts/broker_state_hash_waivers.txt` waives `current_bar_` (a convenience copy) and `bar_index_` (a reconstructed cursor). While the Pine body runs, the host's bar is what these read: generated code's `time` and OHLC reads, the presented-clock readers `open_trade_profit` and `active_account_currency_fx`, and `strategy_stream_state_hash`, which folds `current_bar_` and `prev_bar_timestamp_` (`src/engine_stream.cpp`) | -| the run's words on the codegen-ABI class -- `input_tf_`, `script_tf_`, `script_tf_seconds_`, `last_bar_index_`, `last_bar_time_`, `qty_step_` (`include/pineforge/engine.hpp`) | the Pine host alone. `scheduler_prepare_script_run` sets the timeframe words and the feed's last bar from its scheduler's run spec (`script_tf_seconds_` `pine_strategy_host.cpp:1489`). `apply_realtime_tail_horizon` narrows the last bar to a live probe's horizon, and `prepare_native_begin` copies the symbol's `qty_step` | **retained as the Pine host's run facts**. Generated code reads the timeframe words. The kernel reads them for the report (`src/engine_report.cpp`), the implicit chart feed and the chart-day partition (`src/engine_aux_security.cpp`), all of which a bare host reaches with them empty. A bare host's timeframes are its `NativeRunSpec`'s, which the consumer parses into its own. `last_bar_index_`, `last_bar_time_` and `qty_step_` sit in the hashed region and are waived as feed and symbol metadata; the timeframe words sit outside it. `script_tf_seconds_`'s comment used to place `script_tf_`'s writers in `engine_run.cpp`; it now names the one writer (R5 lane H-DOCGATES) | -| the security-evaluator registry the Pine host fills -- `security_eval_states_`, `security_input_tf_`, `security_next_input_ms_` (`include/pineforge/engine.hpp`) | the Pine host. `declare_security_sites_to_kernel` hands a routable run's sites to the kernel and empties the registry (`security_eval_states_` `pine_strategy_host.cpp:1749`). `scheduler_prepare_security_sequence` sets the input timeframe and resets the next-input cursor, and the auxiliary drive and the scheduler advance that cursor (`src/source/pine_aux_security.cpp`, `src/source/pine_scheduler.cpp`) | **retained by design §2.ii row m**: the kernel keeps the registry, the aggregator and one generic step, and the Pine semantics run in `src/source/pine_security_eval.cpp`. `security_next_input_ms_` is waived: the source scheduler's deferred-boundary state is hashed on the source side. `strategy_stream_state_hash` folds the registry | +| the presented bar -- `current_bar_`, `bar_index_`, `prev_bar_timestamp_` (`include/pineforge/engine.hpp`) | the kernel at every callback point: the consumer presents the bar at the point's instant (`src/native_execution_consumer.cpp`). For the body it runs, the Pine host then presents its own script bar. `scheduler_publish_source_bar` and `scheduler_publish_suppressed_tail` write the whole `current_bar_`, stamped at the bar's label (`current_bar_ = bar` `pine_strategy_host.cpp:2363` and `:2493`). They also write the slot's `bar_index_`, restored after a temporary publication, and each published slot's `prev_bar_timestamp_`. `PineScheduler::input` holds `bar_index_` at the input's own index for one input (`InputBarIndexScope` `pine_scheduler_native.cpp:427`) | **retained, the host's view; the fourth audit's E21 check e**. Execution reads its cursor, not this bar: `execute_current` converts at its cursor's rate and neither writes nor restores the host's clock (Section B's row). The broker-state hash folds none of the three, and `scripts/broker_state_hash_waivers.txt` waives `current_bar_` (a convenience copy) and `bar_index_` (a reconstructed cursor). While the Pine body runs, the host's bar is what these read: generated code's `time` and OHLC reads, the presented-clock readers `open_trade_profit` and `active_account_currency_fx`, and `strategy_stream_state_hash`, which folds `current_bar_` and `prev_bar_timestamp_` (`src/engine_stream.cpp`) | +| the run's words on the codegen-ABI class -- `input_tf_`, `script_tf_`, `script_tf_seconds_`, `last_bar_index_`, `last_bar_time_`, `qty_step_` (`include/pineforge/engine.hpp`) | the Pine host alone. `scheduler_prepare_script_run` sets the timeframe words and the feed's last bar from its scheduler's run spec (`script_tf_seconds_` `pine_strategy_host.cpp:1487`). `apply_realtime_tail_horizon` narrows the last bar to a live probe's horizon, and `prepare_native_begin` copies the symbol's `qty_step` | **retained as the Pine host's run facts**. Generated code reads the timeframe words. The kernel reads them for the report (`src/engine_report.cpp`), the implicit chart feed and the chart-day partition (`src/engine_aux_security.cpp`), all of which a bare host reaches with them empty. A bare host's timeframes are its `NativeRunSpec`'s, which the consumer parses into its own. `last_bar_index_`, `last_bar_time_` and `qty_step_` sit in the hashed region and are waived as feed and symbol metadata; the timeframe words sit outside it. `script_tf_seconds_`'s comment used to place `script_tf_`'s writers in `engine_run.cpp`; it now names the one writer (R5 lane H-DOCGATES) | +| the security-evaluator registry the Pine host fills -- `security_eval_states_`, `security_input_tf_`, `security_next_input_ms_` (`include/pineforge/engine.hpp`) | the Pine host. `declare_security_sites_to_kernel` hands a routable run's sites to the kernel and empties the registry (`security_eval_states_` `pine_strategy_host.cpp:1747`). `scheduler_prepare_security_sequence` sets the input timeframe and resets the next-input cursor, and the auxiliary drive and the scheduler advance that cursor (`src/source/pine_aux_security.cpp`, `src/source/pine_scheduler.cpp`) | **retained by design §2.ii row m**: the kernel keeps the registry, the aggregator and one generic step, and the Pine semantics run in `src/source/pine_security_eval.cpp`. `security_next_input_ms_` is waived: the source scheduler's deferred-boundary state is hashed on the source side. `strategy_stream_state_hash` folds the registry | | the auxiliary-feed seams -- `source_aux_security_feed_enabled`, `source_aux_security_input_view` (`include/pineforge/engine.hpp`, behind `PINEFORGE_HAS_AUX_SECURITY_FEED_V1`) -- and `last_error_` | the kernel declares both seams with an inert default (disabled, no bars: `src/engine_consumer.cpp`). It asks them once, in `prepare_native_security_feeds` (`src/engine_aux_security.cpp`), for the auxiliary slice it feeds the `request.security` evaluators. The Pine host overrides both from `strategy_set_aux_security_feed`'s bars (`src/source/pine_aux_security.cpp`). It writes `last_error_` when that setter refuses its input and when its run's preparation throws | **retained with the `auxiliary_feed` adapter-policy ruling above**. The Pine host drives its own auxiliary slice (lane N7's three measurements), and these seams are how that slice reaches the kernel's feed preparation. A bare host declares `NativeRunSpec::auxiliary_feed` and never meets them. `last_error_` is the handle's one error slot, which any layer that refuses a call writes | | `source_stream_entry_comment` (`include/pineforge/engine.hpp`) | nobody. The kernel's default (`src/engine_consumer.cpp`) and the Pine host's override (`src/source/pine_strategy_host.cpp`) both do nothing, so `stream_observe_entry` keeps the lot's own comment (`src/engine_stream.cpp`) | **dead, kept for the script ABI**. It is a virtual of `BacktestEngine`, which every generated strategy derives from, so deleting it moves every generated script's vtable: an `engine_script_run` epoch. It goes at the next one (`docs/native-refactor-progress.md`, "Scheduled for the next script-ABI epoch") | | the magnifier report facts -- `bar_magnifier_enabled_`, `diag_magnifier_sub_bars_processed_`, `diag_magnifier_sample_ticks_processed_` (`include/pineforge/engine.hpp`) | the kernel, from the run spec's intrabar path and its driver statistics (`src/native_execution_consumer.cpp`). The Pine host writes them again at every bar callback, from its scheduler's magnifier switch and the context's driver statistics (`on_native_bar_open`, `on_native_bar`) | **retained, report-only**: the report's `bar_magnifier_enabled` and magnifier totals (`src/engine_report.cpp`), outside the hashed region. The kernel publishes them for a bare host itself (the C-surface row "kernel-owned report facts" above), so the Pine host's second write is an adapter-thinness question, not a boundary one | | the timeframe report facts -- `diag_script_tf_ratio_`, `diag_needs_aggregation_` (`include/pineforge/engine.hpp`) | the Pine host alone. Its scheduler's run begin stores the ratio of the run spec's script and input timeframes and whether the run aggregates (`diag_script_tf_ratio_` `pine_scheduler_native.cpp:185`, in `PineScheduler::run_begin`), as the retired scheduler's run did before #254. A bare host's report reads 0 for both: the kernel does not write them | **retained, report-only** (R5 lane FIX-E1E2): the report's `script_tf_ratio` and `needs_aggregation` (`src/engine_report.cpp`), outside the hashed region. Execution never reads them | -| `broker_fill_event_seq_` (`include/pineforge/engine.hpp`, in the hashed region) | the Pine host alone, one step per applied broker instruction (`broker_fill_event_seq_` `pine_strategy_host.cpp:726`, in `on_native_applied`); the kernel only resets and folds it | **retained, hashed: the adapter's counter on the kernel's class**. Its placement facts (`signal_close_mc_fill_seq`) compare against it in fill-time gates that cross a bar; the execution authority is the kernel's own ordinal. It is folded into the broker-state hash (`src/engine_state_hash.cpp`), so moving it to the source layer's hash extension moves every run's hash: an epoch decision, not a docs change | -| `broker_state_hashes_` (`include/pineforge/engine.hpp`) | the kernel under `KernelRecorded` (`src/native_execution_consumer.cpp`), and the Pine host at each script point it marks while recording is on (`scheduler_record_broker_hash` `pine_strategy_host.cpp:2795`) | **retained, per driving mode** (design Q4): the report's per-bar hash series, waived as append-only report output that execution never consults | -| the position book's mirror -- `position_qty_`, `position_entry_price_`, `position_entry_count_`, `pyramid_entries_` (`include/pineforge/engine.hpp`, all hashed) | the kernel books and settles them. The Pine host writes them in two places. After every applied event it erases each lot at or below `internal::kQtyEpsilon` and recomputes the position's size, average price and count (`pyramid_entries_` `pine_strategy_host.cpp:665-691`, in `on_native_applied`, restating `ab9714be`'s partial-exit settlement). Before every script body it overwrites `position_entry_count_` with its own entry-slot count (`source_entry_slot_count` `pine_strategy_host.cpp:2432`) | **OPEN, two rulings nobody has made**. The dust sweep erases a lot inside a live book, which the fourth audit found with neither a row nor a tape (its item 32): lane H-THIN's A4-DUST. The count overwrite gives one hashed member two writers with two meanings: the kernel's lots in the current direction, Pine's opened entry slots. It is recorded here first and has no lane yet | -| fields of the kernel's rows the Pine host amends -- `PyramidEntry::time`, `Trade::entry_time`, `Trade::exit_time`, `Trade::close_cause`, `Trade::exit_id`, `Trade::exit_comment`, `Trade::entry_incarnation`, `Trade::exit_from_bracket`, and on a global exit's re-cut rows `Trade::qty`, `Trade::pnl`, `Trade::pnl_pct`, `Trade::commission`, `Trade::max_runup`, `Trade::max_drawdown` (`include/pineforge/engine.hpp`) | the Pine host, inside the applied notification that booked them or at a bar callback. On an aggregated or magnified chart it dates their fills at the chart bar's open (`on_native_applied`; design row AG2), the entry time of a row a later execution of the same pass booked from the lot included (R5 lane FIX-E1E2); their bar indices are the kernel's own script-bar indices, which it books for every host (design row AG1, R5 lane K-IDX), so the host writes no index. It writes no excursion of its own: since R5 lane H-THIN the kernel's sampler books every Pine lot's run-up and drawdown, as it does for any host that keeps the `owns_lot_excursions()` default. The one exception is the re-cut of a global `strategy.exit` fill. When one fill of an exit that names no entry closes the lots of two or more entry ids and leaves the book flat, the host re-cuts that fill's rows into TradingView's children (`order_global_exit_children`, the private fixture set `global_exit_children`). Under `close_entries_rule = "ANY"` it permutes the rows. Otherwise each child is a transaction of its lot's quantity settled oldest lot first, and each slice is a row carrying its share of quantity, P&L, excursions and ten-significant-digit fees; the kernel's book and fills are untouched. It states the close cause from its own labels, clearing the kernel's `Bracket` on its rows (R5 lanes L12 and F3). It relabels an intraday-loss close's `exit_id` and `exit_comment`, swaps `entry_incarnation` on the short-seed report rows (`project_short_seed_report_rows`), and sets `exit_from_bracket` by order family (`exit_from_bracket` `pine_strategy_host.cpp:1288`, in `adapter_label_bracket_trades`) | **retained: the source layer's labels and report dates on the kernel's rows**. A closed row is final for the hash only once its notification returns (`closed_rows_digest`), so the amendments made inside it are folded as the host leaves them. The hash folds a lot's time, entry bar index and two excursions (`src/engine_state_hash.cpp`), and a closed row's entry and exit times, prices, quantity and P&L (`fold_closed_row` `src/native_execution_consumer.cpp`). The labels are not folded. The kernel reads `exit_from_bracket` in one place, `closed_trade_close_cause`, after the row's recorded cause; no kernel path sets it | +| `broker_fill_event_seq_` (`include/pineforge/engine.hpp`, in the hashed region) | the Pine host alone, one step per applied broker instruction (`broker_fill_event_seq_` `pine_strategy_host.cpp:724`, in `on_native_applied`); the kernel only resets and folds it | **retained, hashed: the adapter's counter on the kernel's class**. Its placement facts (`signal_close_mc_fill_seq`) compare against it in fill-time gates that cross a bar; the execution authority is the kernel's own ordinal. It is folded into the broker-state hash (`src/engine_state_hash.cpp`), so moving it to the source layer's hash extension moves every run's hash: an epoch decision, not a docs change | +| `broker_state_hashes_` (`include/pineforge/engine.hpp`) | the kernel under `KernelRecorded` (`src/native_execution_consumer.cpp`), and the Pine host at each script point it marks while recording is on (`scheduler_record_broker_hash` `pine_strategy_host.cpp:2793`) | **retained, per driving mode** (design Q4): the report's per-bar hash series, waived as append-only report output that execution never consults | +| the position book's mirror -- `position_qty_`, `position_entry_price_`, `position_entry_count_`, `pyramid_entries_` (`include/pineforge/engine.hpp`, all hashed) | the kernel books and settles them. The Pine host writes them in two places. After every applied event it erases each lot at or below `internal::kQtyEpsilon` and recomputes the position's size, average price and count (`pyramid_entries_` `pine_strategy_host.cpp:665-691`, in `on_native_applied`, restating `ab9714be`'s partial-exit settlement). Before every script body it overwrites `position_entry_count_` with its own entry-slot count (`source_entry_slot_count` `pine_strategy_host.cpp:2429`) | **OPEN, two rulings nobody has made**. The dust sweep erases a lot inside a live book, which the fourth audit found with neither a row nor a tape (its item 32): lane H-THIN's A4-DUST. The count overwrite gives one hashed member two writers with two meanings: the kernel's lots in the current direction, Pine's opened entry slots. It is recorded here first and has no lane yet | +| fields of the kernel's rows the Pine host amends -- `PyramidEntry::time`, `Trade::entry_time`, `Trade::exit_time`, `Trade::close_cause`, `Trade::exit_id`, `Trade::exit_comment`, `Trade::entry_incarnation`, `Trade::exit_from_bracket`, and on a global exit's re-cut rows `Trade::qty`, `Trade::pnl`, `Trade::pnl_pct`, `Trade::commission`, `Trade::max_runup`, `Trade::max_drawdown` (`include/pineforge/engine.hpp`) | the Pine host, inside the applied notification that booked them or at a bar callback. On an aggregated or magnified chart it dates their fills at the chart bar's open (`on_native_applied`; design row AG2), the entry time of a row a later execution of the same pass booked from the lot included (R5 lane FIX-E1E2); their bar indices are the kernel's own script-bar indices, which it books for every host (design row AG1, R5 lane K-IDX), so the host writes no index. It writes no excursion of its own: since R5 lane H-THIN the kernel's sampler books every Pine lot's run-up and drawdown, as it does for any host that keeps the `owns_lot_excursions()` default. The one exception is the re-cut of a global `strategy.exit` fill. When one fill of an exit that names no entry closes the lots of two or more entry ids and leaves the book flat, the host re-cuts that fill's rows into TradingView's children (`order_global_exit_children`, the private fixture set `global_exit_children`). Under `close_entries_rule = "ANY"` it permutes the rows. Otherwise each child is a transaction of its lot's quantity settled oldest lot first, and each slice is a row carrying its share of quantity, P&L, excursions and ten-significant-digit fees; the kernel's book and fills are untouched. It states the close cause from its own labels, clearing the kernel's `Bracket` on its rows (R5 lanes L12 and F3). It relabels an intraday-loss close's `exit_id` and `exit_comment`, swaps `entry_incarnation` on the short-seed report rows (`project_short_seed_report_rows`), and sets `exit_from_bracket` by order family (`exit_from_bracket` `pine_strategy_host.cpp:1286`, in `adapter_label_bracket_trades`) | **retained: the source layer's labels and report dates on the kernel's rows**. A closed row is final for the hash only once its notification returns (`closed_rows_digest`), so the amendments made inside it are folded as the host leaves them. The hash folds a lot's time, entry bar index and two excursions (`src/engine_state_hash.cpp`), and a closed row's entry and exit times, prices, quantity and P&L (`fold_closed_row` `src/native_execution_consumer.cpp`). The labels are not folded. The kernel reads `exit_from_bracket` in one place, `closed_trade_close_cause`, after the row's recorded cause; no kernel path sets it | | the closed-row counters -- `win_trades_count_`, `loss_trades_count_`, `eventrades_count_`, `gross_profit_sum_`, `gross_loss_sum_` (`include/pineforge/engine.hpp`) | the kernel, once per row it books (`engine_orders.cpp`); the Pine host re-counts them over the children's rows when it re-cuts a global exit's fill into more rows than the kernel booked (`order_global_exit_children`), so `strategy.wintrades`, `strategy.losstrades` and `strategy.eventrades` count the rows the report shows | **retained: the counts follow the report's rows**. Hashed (`src/engine_state_hash.cpp`) and folded as the host leaves them; the re-cut runs inside the applied notification, before the next hash read. `net_profit_sum_` is not re-counted: the equity stays the kernel's | | the range-end report -- `range_end_trades_`, the extremes `max_equity_`, `min_equity_`, `max_drawdown_`, `max_runup_`, and `stream_warmup_mode_` (`include/pineforge/engine.hpp`) | the Pine host: `scheduler_record_range_end` produces report-only terminal rows and preserves ordinary stored marks in REALTIME; `PineStrategyHost::present_report` shapes only the caller-owned report copy through the engine's generic default-no-op presentation seam, before metrics are derived. Historical batch keeps its existing terminal re-mark and extreme fold. Complete-prefix stream reports equal batch from the first realtime script bar; an open position at zero realtime bars retains the warmup mark, and mid-bar reads are host presentation, not a pinned batch boundary. The stream hash contract changes: hashed extremes no longer fold hypothetical terminal marks in REALTIME, so stream and batch terminal hashes need not match; restart/replay binds the exact library digest | **adapter-policy, the `report_open_position_at_end` row above** (design RP5 and §3.7 part 2): report shape, not a mark-to-market row, and the kernel's run-end producer is gated out of every Pine run. The extremes are hashed; the report rows are waived as report-only | | `host_mutation_guard_inert_` (`include/pineforge/engine.hpp`) | the Pine host, once, in its constructor (`host_mutation_guard_inert_ = true` `pine_strategy_host.cpp:159`) | **retained, the host's declared choice**: an in-run call of a frozen C setter is a no-op on a Pine handle, as `ab9714be`'s legacy consumer refused it, and throws on a native host. Outside the hashed region | @@ -710,13 +710,13 @@ Every line number in this section names its symbol on this tree, which | `NativeRunSpec` field | ruling | what the adapter runs instead | measurement and ruling of record | executed native consumers | |---|---|---|---|---| | `price_grid`, `grid_rounding` | **native-only** | TradingView's per-order-kind tick rules on top of `None`: `source_trigger_threshold` (`pine_adapter.cpp:384`), `source_level_on_price_grid` (`:367`), `nearest_tick` (`:274`) / `source_bar_fill_tick` (`:295`) / `directional_tick` (`:327`), behind the terms seam | R5-3 and design risk E7 ruled `None` for the adapter before the lane ran; lanes R7 and N13 measured the alternative anyway (raw levels submitted, `QuantizeFillsAndTriggers` with `HalfUp` declared). L8b closed the first blocker (no run aborts). The second has no remedy on either side: TradingView quantizes per order kind (stop and limit legs and a trail's activation on the quantized bar; the trail stop, the running best, stop-limit entries and the `calc_on_order_fills` cursors raw), the grid is one rule for the run, and the old N13 trial moved 30 pinned checks in 4 units; this is historical evidence, not a current-head result; a per-kind mask would spell that inconsistency into the kernel. The corpus cannot arbitrate (every probe runs a 0.01 tick on an on-grid feed; 5 of 312 differ in an engine-only column). Permanent witness: `tests/test_adapter_grid_relower.cpp`, whose section 5 pins the trail stop the grid fires a bar early. Design row PG and §3.6 | `examples/native/native_price_grid_strategy.cpp`, `examples/native/native_price_grid_c.c`, the private test `test_native_price_grid` | -| `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:17483`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:20858`), `chart_day_key` (`pine_adapter.cpp:17380`), `compat::pine::IntradayCap` with `IntradayOrderBudget` (337 lines) | Structural first: Pine's risk calls are per-bar statements, so a limit reaches the adapter on script bar 0, after `project()` (`pine_strategy_host.cpp:350`) and the kernel's `configure` (`pine_strategy_host.cpp:359`) have fixed and digested the spec. In substance (lane N12, `tests/test_adapter_risk_relower.cpp`, 62 checks over nine paired scenarios): the drawdown latch samples at the close only and still admits a reversal; the loss-day streak counts trades, not days; the intraday loss closes at the path's adverse extreme, refuses every placement and withdraws the book; the fill cap charges slots, transfers quota and closes at the bar's better extreme on the chart timezone's day. With the kernel seeded on the corpus, 3 of 4 cap probes diverge (3840 of 3916, 312 of 604, 2370 of 2384 rows; re-derived by `scripts/check_seeded_risk_experiment.sh` since R5 lane H-MEASURE). `strategy.risk.allow_entry_in` is the one rule that is the kernel's already (`allowed_open_directions`). Design §3.6 | `examples/native/native_risk_limits_strategy.cpp`, `examples/native/native_trail_risk_strategy.cpp`, `tests/test_native_risk_limits.cpp`, `tests/test_native_c_api.c` | +| `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:17483`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:20858`), `chart_day_key` (`pine_adapter.cpp:17380`), `compat::pine::IntradayCap` with `IntradayOrderBudget` (337 lines) | Structural first: Pine's risk calls are per-bar statements, so a limit reaches the adapter on script bar 0, after `project()` (`pine_strategy_host.cpp:348`) and the kernel's `configure` (`pine_strategy_host.cpp:357`) have fixed and digested the spec. In substance (lane N12, `tests/test_adapter_risk_relower.cpp`, 62 checks over nine paired scenarios): the drawdown latch samples at the close only and still admits a reversal; the loss-day streak counts trades, not days; the intraday loss closes at the path's adverse extreme, refuses every placement and withdraws the book; the fill cap charges slots, transfers quota and closes at the bar's better extreme on the chart timezone's day. With the kernel seeded on the corpus, 3 of 4 cap probes diverge (3840 of 3916, 312 of 604, 2370 of 2384 rows; re-derived by `scripts/check_seeded_risk_experiment.sh` since R5 lane H-MEASURE). `strategy.risk.allow_entry_in` is the one rule that is the kernel's already (`allowed_open_directions`). Design §3.6 | `examples/native/native_risk_limits_strategy.cpp`, `examples/native/native_trail_risk_strategy.cpp`, `tests/test_native_risk_limits.cpp`, `tests/test_native_c_api.c` | | `max_abs_units` | **adapter-policy** | `strategy.risk.max_position_size` as a gate on the LIVE book before the fill (`max_position_size` `pine_adapter.cpp:16492-16493`): an entry is refused once the book already holds the limit | design row MG2 (R5-1): the resulting-book cap is the generic one. Measured by N12's scenario `PS` in `tests/test_adapter_risk_relower.cpp`: two-unit entries against a limit of 3 leave the adapter at 4 and the kernel cap at 2 | `tests/test_native_resting_matching_contract.cpp`, `tests/test_native_run_spec.cpp` | | `max_open_lots` | **adapter-policy** | Pine pyramiding is a per-cycle entry count in the adapter's command policy; a resting source entry must not consume a physical-lot cap before it fills, so `project()` leaves the cap unset (`max_open_lots` `pine_adapter.cpp:2646-2648`) | design row MG3 (R5-1); the contract comment in `project()`. Measured against TradingView by R5 lane H-MEASURE (the private test `test_pyramiding_count_differential`, 15 scenarios on three `lab tv` tapes): TradingView checks an entry once, at its first eligible point, against the trades then open, and does not check a resting entry again at its fill; the kernel's cap books 14 of the 15 as TradingView does, the adapter's per-cycle count 13 (it keeps a slot a close or another entry's exit drained; since lane W8A-SIGSTATE-1's R-B a resting LIMIT entry no longer counts, and one the count admitted fills over the cap, and since lane W5B-ENG-MARGIN-RESIDUAL's SB the later market entries of a flat bar are refused at the fill once the book holds the cap, as on TradingView's tapes). A recorded divergence: the retention stands pending a re-lowering ruling. R5 lane H-THIN tried the lowering and kept the count: on the kernel's cap four corpus probes moved away from TradingView, for want of the first-eligible-point rule (H-MEASURE's P4b); it fixed the reservation divergence measured beside it (P10), so a default `strategy.exit(from_entry)` under FIFO now reserves its entry's own quantity, the oldest lots first, as TradingView does | `tests/test_native_resting_matching_contract.cpp`, `tests/test_native_margin_model.cpp` | | `initial_margin_fraction` | **adapter-policy** | TradingView's ten-significant-digit money admission against the signal-time tuple, answered as `AdmitWithHostMargin`; the `margin` model the adapter does declare is maintenance-only (`NativeMarginModel` `pine_adapter.cpp:2885-2893`, `margin.maintenance_long` `pine_adapter.cpp:2900`) | design row MG4 and the wave-4 ruling recorded in `project()`: a positive initial requirement would decline openings TradingView takes. Measured both ways by R5 lane H-MEASURE (the private test `test_adapter_margin_schedule_differential`, M11): the kernel's gate declines a 1x long whose entry fee the adapter admits (`InitialMargin`), and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times | `tests/test_native_precommit_view.cpp`, `tests/test_native_margin_model.cpp` | -| `report_open_position_at_end` | **adapter-policy** | The Pine host owns TradingView's range-end shape: batch re-marks the stored final point and re-folds extremes, while REALTIME retains ordinary marks and `PineStrategyHost::present_report` re-marks only the caller-owned copy before generic metric derivation; the stream hash excludes that terminal extreme fold (`scheduler_record_range_end` pine_strategy_host.cpp:1987): report shape, not a mark-to-market row (`KernelRecordedAtHostMarks` pine_adapter.cpp:2855) | design row RP5; the kernel reads the field under `KernelRecorded` only, which `scripts/check_adapter_spec_shadowing.py` gates | `examples/native/native_sized_report_strategy.cpp`, `tests/test_native_report_truth.cpp` | +| `report_open_position_at_end` | **adapter-policy** | The Pine host owns TradingView's range-end shape: batch re-marks the stored final point and re-folds extremes, while REALTIME retains ordinary marks and `PineStrategyHost::present_report` re-marks only the caller-owned copy before generic metric derivation; the stream hash excludes that terminal extreme fold (`scheduler_record_range_end` pine_strategy_host.cpp:1985): report shape, not a mark-to-market row (`KernelRecordedAtHostMarks` pine_adapter.cpp:2855) | design row RP5; the kernel reads the field under `KernelRecorded` only, which `scripts/check_adapter_spec_shadowing.py` gates | `examples/native/native_sized_report_strategy.cpp`, `tests/test_native_report_truth.cpp` | | `open_bar_view` | **adapter-policy** | `Complete`: TradingView's bar-open scheduling and its `calc_on_order_fills` callback read the whole script bar (`pine_adapter.cpp:2825-2827`) | design rows CT4 and E5: the open-only view is opt-in because the adapter needs the full bar | `examples/native/native_calc_on_fills_strategy.cpp`, `tests/test_native_calc_timing.cpp` | -| `subscriptions` | **adapter-hook** | `declare_timeframe_subscriptions` from the begin-time hook (`pine_strategy_host.cpp:1717`): a plain `request.security` site is a kernel subscription | lane R3b over the L6c hook: 21 of the corpus's 23 `request.security` probes run their sites on the kernel, byte-identical. The predicate (`declare_security_sites_to_kernel` `pine_strategy_host.cpp:1708`) declares every site of a run or none. A run keeps all of its sites on the source evaluator when it is a stream's warmup, runs the bar magnifier, cuts its range start to na (KI-55), projects historical lookahead, installs an auxiliary feed, aggregates its chart or has no detected timeframe. It keeps them there too when any one site uses `lookahead_on`, `ticker.heikinashi`, a lower timeframe (requested, emulated, read from the input, or as an array), a publication gate, or a calling-bar close or open latch; when a forex or CFD intraday chart without a native feed asks for `D`; when a site asks for N days, whose bars TradingView counts in the symbol's trading dates from each year's first (`tests/test_security_multi_day_buckets_tapes.cpp`), not on the kernel's calendar-day grid; when the registry's `sec_id`s are not dense from 0; and when the kernel refuses the declaration. R5 lane H-MEASURE pins each reachable condition with what it changes (`tests/test_adapter_security_route_conditions.cpp`, a site through the evaluator against the same site through a bare kernel host): on the feeds measured the magnifier and an aggregated chart change no delivered value, while the range-start cut, the projection's incomplete tail, `lookahead_on` and the OTC pins do; the four lower-timeframe flags, the publish gate and the calling-bar latches are subsumed by other clauses, and the empty or undetected timeframe checks are unreachable after begin-time validation | `examples/native/native_htf_strategy.cpp`, `tests/test_native_htf_subscriptions.cpp` | +| `subscriptions` | **adapter-hook** | `declare_timeframe_subscriptions` from the begin-time hook (`pine_strategy_host.cpp:1715`): a plain `request.security` site is a kernel subscription | lane R3b over the L6c hook: 21 of the corpus's 23 `request.security` probes run their sites on the kernel, byte-identical. The predicate (`declare_security_sites_to_kernel` `pine_strategy_host.cpp:1706`) declares every site of a run or none. A run keeps all of its sites on the source evaluator when it is a stream's warmup, runs the bar magnifier, cuts its range start to na (KI-55), projects historical lookahead, installs an auxiliary feed, aggregates its chart or has no detected timeframe. It keeps them there too when any one site uses `lookahead_on`, `ticker.heikinashi`, a lower timeframe (requested, emulated, read from the input, or as an array), a publication gate, or a calling-bar close or open latch; when a forex or CFD intraday chart without a native feed asks for `D`; when a site asks for N days, whose bars TradingView counts in the symbol's trading dates from each year's first (`tests/test_security_multi_day_buckets_tapes.cpp`), not on the kernel's calendar-day grid; when the registry's `sec_id`s are not dense from 0; and when the kernel refuses the declaration. R5 lane H-MEASURE pins each reachable condition with what it changes (`tests/test_adapter_security_route_conditions.cpp`, a site through the evaluator against the same site through a bare kernel host): on the feeds measured the magnifier and an aggregated chart change no delivered value, while the range-start cut, the projection's incomplete tail, `lookahead_on` and the OTC pins do; the four lower-timeframe flags, the publish gate and the calling-bar latches are subsumed by other clauses, and the empty or undetected timeframe checks are unreachable after begin-time validation | `examples/native/native_htf_strategy.cpp`, `tests/test_native_htf_subscriptions.cpp` | | `auxiliary_feed` | **adapter-policy** | the adapter's own auxiliary drive: the chart-slice mapping and the deferred first bucket (`src/source/pine_aux_security.cpp`) | lane N7, retained on three measurements: 0 of 312 corpus probes install an auxiliary feed; TradingView's chart slice leaves pre-range coverage inert where the kernel folds it by time, and evaluates after the bar's matching pass where the kernel delivers before it (`tests/test_native_auxiliary_feed_twin.cpp`, rows B and C). Design §2.iv | `tests/test_native_auxiliary_feed.cpp`, `tests/test_native_auxiliary_feed_stream.cpp` | | `quantity_tolerance` | **adapter-policy** | TradingView's `1e-10` quantity rules on top of the exact default: a close within `1e-10` of the held position is a `Flatten`, a batched process-orders-on-close (POOC) close ending within `internal::kQtyEpsilon` (`1e-10`) of an interior lot boundary is one selected `Flatten` of whole lots (`source_fifo_prefix_openings`); an ambiguous ordinary deferred FIFO `strategy.close` instead uses `HostSized` to authenticate the exact physical FIFO prefix sum for its `Reduce` at fill time, with that same tolerance (`source_fifo_close_units`, `src/source/pine_adapter.cpp`). Detection checks only the current prefix and prefixes reachable after already-pending same-bar closes, so non-ambiguous request shapes, hashes and readback remain unchanged. Same-bar market transactions, `immediately` closes and the ShortSeed placeholder remain out of scope, and after every fill `PineStrategyHost::on_native_applied` erases lots of at most `kQtyEpsilon` without a closing row (`src/source/pine_strategy_host.cpp`) | lane K-ULP4 (the owner-level ruling of audit X1, part b): opt-in and absent by default, so every adapter run is byte-identical by construction. The tolerance charges a snapped `Reduce` its request and books every dust lot it closes as a row, where the adapter's prefix `Flatten` is charged the lots it holds and its sweep books no row; declaring it would move the adapter's fills, ledgers and hashes, so whether the adapter can move onto it is a later lane's measurement. R5 lane H-THIN measured each of those comparisons (design §3.9b) and kept them: made exact, the sweep fails its own tape row (`test_pine_dust_sweep_paired`) | `tests/test_native_quantity_tolerance.cpp` | @@ -761,7 +761,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:661`) leaves its value unnamed, and the enum guard holds that exclusion. A C++ host now reaches the same kernel producer through `NativeStrategyHost::mark_native_report_point`; a mark outside a running HostMarks run returns false. | | `strategy_set_path_order` (`pineforge.h:948`) clamps an out-of-range mode to AUTO | **retained** | A `void` ABI v4 setter cannot answer a refusal. Its values are now documented as `pf_native_path_order_e`'s (`native_c_api.h:904`); a C host that wants an unknown word refused declares `path_order` in `pf_native_run_spec_ext_v1`, which answers `PF_NATIVE_E_TAG`. | | the enumerated `int32` fields of `pf_pending_order_v1_t` (`strategy_pending_order_get` `pineforge.h:1096`) | **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:980`) versus `strategy_configure_native_ext_result_v1` (`native_c_host.cpp:4066`) | **retained legacy base plus typed additive route** | A base specification the kernel rejects still returns `-1` and latches `Failed`, preserving its frozen caller contract; a Ready misuse retains that Contract latch. The C boundary refuses arguments, a Running phase and a Completed hash hook without entering kernel configure, so a read cannot reconfigure its host. The extended result call validates before its first configure, writes `pf_native_spec_error_t` / `pf_native_spec_field_t`, and names reused-host session-key and run-number refusals; no old signature or layout moved. | +| `strategy_configure_native_v1` (`c_abi.cpp:981`) versus `strategy_configure_native_ext_result_v1` (`native_c_host.cpp:4066`) | **retained legacy base plus typed additive route** | A base specification the kernel rejects still returns `-1` and latches `Failed`, preserving its frozen caller contract; a Ready misuse retains that Contract latch. The C boundary refuses arguments, a Running phase and a Completed hash hook without entering kernel configure, so a read cannot reconfigure its host. The extended result call validates before its first configure, writes `pf_native_spec_error_t` / `pf_native_spec_field_t`, and names reused-host session-key and run-number refusals; no old signature or layout moved. | | `strategy_configure_native_fx_curve_v1` (`pineforge.h:546`) | **typed additive route** | `strategy_configure_native_fx_curve_ext_v1` writes the C twin of `NativeFxCurveValidation` and its offending index. The old `-1` route remains unchanged, while a C caller can now distinguish a decreasing timestamp, non-positive rate, allocation failure or wrong phase. | | `on_native_timeframe_bar` (`native_host.hpp:885`) and `NativeTimeframeBarContext::interval` (`native_host.hpp:809`) | **query routed** | The callback signature stays frozen. `strategy_native_timeframe_bar_interval_v1` is valid only during that callback and copies the kernel calendar interval without deriving it from the delivered bar or delivery timestamp. | | `magnifier_sub_bars_total` / `magnifier_sample_ticks_total` (`engine_report.cpp:75-76`) | **kernel-owned report facts** | The generic intrabar driver already increments `NativeDriverStatistics::sub_bars_processed` / `sample_ticks_processed` for every retained path (`native_execution_consumer.cpp:8749-8761`); the kernel now publishes those same counters for bare hosts. They are report fields, not new durable state: the existing `native_continuation_hash` fold of `NativeDriverStatistics` (`native_execution_consumer.cpp:1125-1130`) sees the same values, so no hash contract moves. | @@ -842,7 +842,7 @@ The table records, at the tree the doc wave read, what each block described and | `PhysicalExecutionContext::account_fx` `include/pineforge/execution.hpp:92`, read by `build_close_trade_with_costs` `src/engine_orders.cpp:70-87` and handed by `append_open_position_report_rows` `src/native_execution_consumer.cpp:8961-8982` | a closing or mark-to-market row converts at the account-currency rate its execution names -- a mark at a report point's instant, a fill at its own cursor -- and at the presented clock's rate when it names none, as before (R5 lane B-ADAPTER, item 3) | the rate is data a host of any venue supplies with the row it asks for; nothing branches on a platform, and the row's currency is the basis its owner's magnitudes are recorded in | none: no spec field; a caller names the rate per call, and a call naming none converts exactly as before | `tests/test_adapter_range_end_fx.cpp` (a stepped FX curve at seven instants around the range end, plain and aggregated charts) | | `NativeExecutionConsumer::execute_current` `src/native_execution_consumer.cpp:7508` | a current execution converts at its cursor's rate, threaded through its terms facts, inspection, preview and settlement contexts, and neither writes nor restores the host's presented clock, so its own hooks and the host after it see the frame's clock; its preview `inspect_current_execution` still converts at the presented clock (R5 lane B-ADAPTER, item 5) | an execution converting at its own instant's rate is the same for every host and venue, and the host's presented clock stays its own | none: the rate is the one the run already chose (the cursor's instant), carried as data | `tests/test_native_current_execution_rate.cpp` (kernel-only) | | `NativeExecutionConsumer::validate_current_execution` `src/native_execution_consumer.cpp:7231` | a current execution refuses a request bound to a host roster (`BindCohort`) as `UnsupportedRequest`, before its point is taken and whatever the roster holds: it admits exactly the owners the evaluation's current shape executes (`current_shape` `src/native_order.cpp:383`: `Independent`, `BindOpening`, `BindOpenings`). Since R4 slice C it had admitted `BindCohort` as well, which the evaluation then left unevaluated, so a roster holding a live lot failed the run with "native current evaluated allowance mismatch" and an empty one answered `UnreadyOwner`; its read-only preview (`inspect_current_execution`) settled what the execution could not (R5 lane PAR-ORDERS-2) | one legality rule, the evaluation's own, decides a command before any point is taken, the same for every host and for the C surface (`strategy_native_execute_current_v1` answers through this validation); a roster read at the match is not a membership fixed at the command, which is what the documented contract names (`docs/pages/native-engine.md` "Selected exposure and current execution") | none: no spec field or hook. A refused request stays queued and fills at the next match like any market close; a host that wants a cohort's lots closed at once fixes its roster first (`BindOpenings`), as the Pine adapter's immediate closes do | `tests/test_native_current_cohort_refusal.cpp` (kernel-only: a live and an empty roster refused, then filled as queued closes; a fixed roster executing at once) , `tests/test_native_cohort_sliced_close.cpp` (its preview rows: the roster close's preview is refused, the same selection's settles) and `tests/test_adapter_live_state_equivalence.cpp` (reversals seed 3246599, which the failure used to end) | -| the chart symbol's daily partition -- `prepare_chart_day_partition` `src/engine_aux_security.cpp:358`, kept in `chart_day_partition_` `include/pineforge/engine.hpp:1218` and presented to the language runtime through `RuntimeAmbient::day_partition` `src/runtime_ambient.hpp:38` (the TradingView comment above `NativeDayPartition` in `include/pineforge/timeframe.hpp`) | on an intraday chart whose run installed its own symbol's `D` feed, the chart-level D period follows that feed's stamps: the exchange's trade-date bars, a holiday-merged day included. It governs `time("D")`, `timeframe.change("1D")`, `ta.change(time("D"))`, the default-anchored `ta.vwap` and `crosses_boundary(DAY)`. Without the feed, on a 1D chart or on another symbol's clock every rule is the nominal session day | the stamps are the venue's own data, as for the authoritative-feed partition above: another venue is driven by installing its own daily bars, and nothing branches on a platform | the policy knob is installing the feed or not, so a "which daily partition" field would restate it. Only the Pine host builds it (`prepare_chart_day_partition` `pine_strategy_host.cpp:1651`, `:1692`) and presents it for each bar it publishes (`AmbientDayPartitionScope` `pine_strategy_host.cpp:2394`). A bare host never calls the builder, so its calendar stays nominal | the private test `test_o_close_pct_day_anchor` (the partition is built from the D feed, and none without it) and `tests/test_chart_day_memo.cpp`; the tape is `lab tv` `o-cme-dayanchor-full`, recorded at the site | +| the chart symbol's daily partition -- `prepare_chart_day_partition` `src/engine_aux_security.cpp:358`, kept in `chart_day_partition_` `include/pineforge/engine.hpp:1218` and presented to the language runtime through `RuntimeAmbient::day_partition` `src/runtime_ambient.hpp:38` (the TradingView comment above `NativeDayPartition` in `include/pineforge/timeframe.hpp`) | on an intraday chart whose run installed its own symbol's `D` feed, the chart-level D period follows that feed's stamps: the exchange's trade-date bars, a holiday-merged day included. It governs `time("D")`, `timeframe.change("1D")`, `ta.change(time("D"))`, the default-anchored `ta.vwap` and `crosses_boundary(DAY)`. Without the feed, on a 1D chart or on another symbol's clock every rule is the nominal session day | the stamps are the venue's own data, as for the authoritative-feed partition above: another venue is driven by installing its own daily bars, and nothing branches on a platform | the policy knob is installing the feed or not, so a "which daily partition" field would restate it. Only the Pine host builds it (`prepare_chart_day_partition` `pine_strategy_host.cpp:1649`, `:1692`) and presents it for each bar it publishes (`AmbientDayPartitionScope` `pine_strategy_host.cpp:2392`). A bare host never calls the builder, so its calendar stays nominal | the private test `test_o_close_pct_day_anchor` (the partition is built from the D feed, and none without it) and `tests/test_chart_day_memo.cpp`; the tape is `lab tv` `o-cme-dayanchor-full`, recorded at the site | | the bar-time accessors -- `_decompose_bar_time` and `_bar_hour` through `_bar_weekofyear` `include/pineforge/engine.hpp:1066-1093` | the presented bar's stamp broken into calendar fields in UTC, memoized per stamp | a function of the stamp alone, and UTC is the engine's storage timezone | none: they read no spec field and fold into no digest. They are protected `BacktestEngine` members, part of the codegen contract. Their comment claimed that generated code reads them for Pine's bare `hour` / `minute` / `dayofweek`. It does not: the codegen transpiler emits `pine_hour(current_bar_.timestamp, syminfo_.timezone)` and its siblings on the exchange timezone (`pineforge_codegen/codegen/tables.py` at the corpus's pinned commit `f3285d79`), which are the language calendar functions above. No generated strategy in `corpus/` names a `_bar_*` accessor. The comment now says so (R5 lane H-DOCGATES) | `tests/test_chart_timezone.cpp` (`_bar_hour()` is the UTC hour) and `tests/test_intraday_rollover_chart_tz_l4a.cpp` | | the conventional offset spellings of `timezone_accepted` `include/pineforge/native_calendar.hpp:208` (its comment's "TV sign" line) | `UTC+05:30` and `GMT-4` mean what they say: five and a half hours ahead of UTC, four behind (ISO 8601's sign), where a POSIX `TZ` string of the same shape means the opposite; a POSIX rule string keeps POSIX's own sign | a zone spelling is data, and the sign is the one a venue printing `UTC+8` means; nothing branches on a platform | a sign-convention switch over a spelling whose meaning is fixed | `tests/test_native_calendar.cpp` (`resolve_civil("UTC+05:30", …)` and `resolve_civil("GMT-4", …)` against their fixed instants) | | the grid and lot helpers -- `tick_grid_price` `include/pineforge/engine.hpp:799`, `price_grid_decimals` `include/pineforge/engine.hpp:833`, `level_on_price_grid` `include/pineforge/engine.hpp:843`, `apply_qty_step` `include/pineforge/engine.hpp:932`, `apply_exit_qty_step` `include/pineforge/engine.hpp:952` (their TradingView calibration comments sit above each) | nearest-tick and price-grid quantization and the lot-increment floors, each calibrated on TradingView tapes | **not a kernel mechanism**: no kernel or source-layer code calls any of them (`tests/test_f6_dead_kernel_members.cpp`, witness 2). The adapter runs its own copies of TradingView's per-order-kind tick rules (ruled native-only in the `price_grid` row above) and sizes its own lots; a native host declares a price grid and a quantity grid in its run spec | nothing to spell. They are rule 5's legacy, kept only because the twin-parity freeze (`scripts/check_twin_parity.py`, base `ab9714be`) holds the checks of the suites that reach them, and they leave with those checks | `tests/test_stop_tick_rounding_l4d.cpp`, `tests/test_level_grid_snap_l4d.cpp`, `tests/test_qty_step_epsilon_floor_l4b.cpp` | diff --git a/docs/coverage.md b/docs/coverage.md index 0a54d1817..473b4141b 100644 --- a/docs/coverage.md +++ b/docs/coverage.md @@ -62,7 +62,7 @@ ## Public C ABI `` is the **single canonical consumer header**. It has -exactly 94 public `PF_API` declarations: 74 of the 80 runtime implementations (the other six are declared in `execution_observer.h` and `selected_window.h`) and twenty +exactly 95 public `PF_API` declarations: 75 of the 81 runtime implementations (the other six are declared in `execution_observer.h` and `selected_window.h`) and twenty per-strategy generated exports. Every compiled PineForge strategy `.so` exports that public set, except that `strategy_declares_bar_magnifier` is exported only by a script that declares `use_bar_magnifier = true` and the three [outputs](outputs.md) exports only by a module that records outputs; older modules lack the six @@ -132,7 +132,7 @@ single `.hpp`): | Module | Header | Source | Pine-facing role | | ------------------ | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Public C ABI | `pineforge.h` | `c_abi.cpp` (+ layout `static_assert`s) | 94 public `PF_API` declarations: 74 of the 80 runtime implementations plus twenty per-strategy generated exports. `strategy_configure_native_fx_curve_v1` stages the additive native FX curve. | +| Public C ABI | `pineforge.h` | `c_abi.cpp` (+ layout `static_assert`s) | 95 public `PF_API` declarations: 75 of the 81 runtime implementations plus twenty per-strategy generated exports. `strategy_configure_native_fx_curve_v1` stages the additive native FX curve. | | Engine | `engine.hpp` | `engine_run.cpp`, `engine_stream.cpp`, `engine_execution.cpp`, `engine_orders.cpp`, `engine_path_resolve.cpp`, `engine_trade_accessors.cpp`, `engine_security.cpp`, `engine_lower_tf.cpp`, `engine_report.cpp`, `native_execution_consumer.cpp` | One-shot and continuous lifecycle, native request matching/settlement, orders, reports, inputs / syminfo, magnifier, TF aggregation, and `request.security` plumbing. | | Engine internals | `engine_internal.hpp` | (private cross-TU header) | `pineforge::internal::*` types and helpers shared between engine `.cpp` partitions; not part of the public ABI. | | Technical analysis | `ta.hpp` | `ta_moving_averages.cpp`, `ta_oscillators.cpp`, `ta_volatility_trend.cpp`, `ta_extremes_volume.cpp`, `ta_misc.cpp` | Official `ta.*` functions and series variables backed by stateful runtime classes with `compute` / `recompute`, plus `pivot_point_levels(...)` free function. | diff --git a/docs/design/cr150-contracts.md b/docs/design/cr150-contracts.md index d175e1458..aa0e522cb 100644 --- a/docs/design/cr150-contracts.md +++ b/docs/design/cr150-contracts.md @@ -213,7 +213,7 @@ references; L15a must maintain these anchors and mechanically pin every current | Instrument-feed digest and cached/uncached continuation folds | `digest_instrument_feed_views` `src/native_run_spec.cpp:1162-1205`; `native_instrument_feeds_digest` `src/native_execution_consumer.cpp:321-323`; `native_instrument_feeds_digest` `src/native_execution_consumer.cpp:2774-2775`; `native_run_spec_digest` `src/native_execution_consumer.cpp:12807-12812` | 364-365 | | Staged feed counts in host state and its hash | `installed_symbol_feed` `src/source/pine_strategy_host.cpp:232-236`; `staged_.instrument_feeds` `src/source/pine_state_hash.cpp:292-299` | 366-367 | | Refresh of symbol-data digest and host hash fold | `refresh_symbol_data_digest` `src/source/pine_security_eval.cpp:1552-1564`; `symbol_data_digest_` `src/source/pine_state_hash.cpp:1123-1129` | 367-368 | -| Restore/store upkeep and column-setter length check | `held.bars` `src/source/pine_security_eval.cpp:1091-1101`; `feed_count` `src/source/pine_security_eval.cpp:1433-1438` | 369-370 | +| Restore/store upkeep and column-setter length check | `held.bars` `src/source/pine_security_eval.cpp:1091-1101`; `feed_count` `src/source/pine_security_eval.cpp:1433-1439` | 369-370 | | Lower-path preflight/validation | `NativeExecutionConsumer::preflight_intrabar_path` `src/native_execution_consumer.cpp:3216-3249` | 371,1221-1222 | | Lower-path digest (count first) and continuation hash | `native_intrabar_path_digest` `src/native_run_spec.cpp:891-919`; `native_intrabar_path_digest` `src/native_execution_consumer.cpp:288` | 371,1223-1224 | | Per-script-bar lower-path binary search/walk | `candidate` `src/native_execution_consumer.cpp:8630-8644` | 371,1225 | diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index d01bc41e7..63e339162 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -121,18 +121,18 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | CT4 | Partial-bar view / open-bar lookahead guard | **no** — the open hook receives the *complete* script bar: `invoke_bar_open_callback(engine, bar, point)` native_execution_consumer.cpp:7966 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:8017 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1073 is the lookahead-free bar so far, through the last path point consumed (a fill inside a segment reads it at the segment's origin), and `NativeOpenBarView::OpenOnly` native_run_spec.hpp:109 masks `on_native_bar_open`'s bar to `H = L = C = open` as presentation only — the complete bar is restored before the open match (`tests/test_native_calc_timing.cpp`). | K | L5 | F:E3 | (single-source) The adapter needs the full bar, so the guard is opt-in. | | CT5 | Raw input observation before aggregation | **yes** — `on_native_input` native_host.hpp:875, `NativeInputContext` native_host.hpp:773-778 | K | — | O:F3 | (single-source row) | | CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:727, `coof_recalculation_due` pine_scheduler_native.cpp:693, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:848; `begin_coof_recalc` pine_adapter.cpp:6676, `flush_coof_tail` pine_adapter.cpp:7100, `end_coof_recalc` pine_adapter.cpp:6733) | **partial** — `on_native_applied` native_host.hpp:938 fires mid-path after each fill native_execution_consumer.cpp:7870, native_execution_consumer.cpp:7870; commands are legal there `commands_allowed` native_execution_consumer.cpp:1843; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:6507, `remaining_path_delivery` native_order.cpp:2082-2090. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | -| CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:878 fires per accepted realtime print native_execution_consumer.cpp:8105; no per-tick calculation in batch; the guide pinned "close-only" (`docs/pages/native-engine.md`, "Batch OHLCV vs ticks vs quiet") **Closed:** `NativeCalculationTrigger::EveryModeledPoint` native_run_spec.hpp:97 adds one `Tick` recalculation at every modeled point of the delivered path and at every observed print, in batch and in a stream alike (`tests/test_native_calc_timing.cpp`). | K | L5 | F:E5 O:F4 S:C3 | **Disagree:** F yes / O partial / S no. R5-5: v1. Not an adapter feature either: absent from `PineStrategyConfig` pine_adapter.hpp:62-76, the runner rejects it (`calc_on_every_tick` main.cpp:259), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:295. | +| CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:878 fires per accepted realtime print native_execution_consumer.cpp:8105; no per-tick calculation in batch; the guide pinned "close-only" (`docs/pages/native-engine.md`, "Batch OHLCV vs ticks vs quiet") **Closed:** `NativeCalculationTrigger::EveryModeledPoint` native_run_spec.hpp:97 adds one `Tick` recalculation at every modeled point of the delivered path and at every observed print, in batch and in a stream alike (`tests/test_native_calc_timing.cpp`). | K | L5 | F:E5 O:F4 S:C3 | **Disagree:** F yes / O partial / S no. R5-5: v1. Not an adapter feature either: absent from `PineStrategyConfig` pine_adapter.hpp:62-76, the runner rejects it (`calc_on_every_tick` main.cpp:259), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:293. | | CT8 | Historical intrabar calculation (per lower-TF sub-bar / magnifier sample) | **no** — with an `IntrabarPath` the callback still fires once per script bar native_execution_consumer.cpp:8681 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:928 fires once after each retained lower-timeframe sub-bar's whole matching path (`tests/test_native_calc_timing.cpp`). | K | L5 | F:E6 O:F8 S:C4 | — | | CT9 | Intrabar (magnifier) matching path + path-order policy (`volume_weighted_cap` pine_adapter.cpp:2934) | **yes** — `IntrabarPath` native_run_spec.hpp:362-403, delivery `deliver_confirmed_script` native_execution_consumer.cpp:8252-8518; `NativePathOrder` native_run_spec.hpp:335-339 | K+A | — | F:E7 F:G5 O:F7 S:C4 | **Disagree:** S partial (it wants callback cadence → CT8). TV sampler choice is A: TradingView's own intrabars at its table's timeframe, each owned by the chart bar of its last minute, built from the finer feed (`tradingview_magnifier_bars` magnifier_intrabars.hpp:44, R5 lane MAG-INTRABAR). | -| CT10 | Batch → warmup → realtime forward execution | **yes** — `stream_begin` engine.hpp:1771, `stream_push_bar` engine.hpp:1777, `stream_end` engine.hpp:1781, `NativeRunPhase` native_host.hpp:41, C ABI `strategy_stream_begin` c_abi.cpp:698 | K+A | — | F:E8 O:J3 S:C5 | **Disagree:** S partial. R5-1: the lifecycle is native; Pine trade-start suppression and realtime-tail / probe rules (`trading_window_active` pine_strategy_commands.cpp:15-21, `prepare_native_begin` pine_strategy_host.cpp:206-311) are A. S's "neutral tail + trade-window policy" is single-source and unassigned (§4 Q8). | +| CT10 | Batch → warmup → realtime forward execution | **yes** — `stream_begin` engine.hpp:1771, `stream_push_bar` engine.hpp:1777, `stream_end` engine.hpp:1781, `NativeRunPhase` native_host.hpp:41, C ABI `strategy_stream_begin` c_abi.cpp:699 | K+A | — | F:E8 O:J3 S:C5 | **Disagree:** S partial. R5-1: the lifecycle is native; Pine trade-start suppression and realtime-tail / probe rules (`trading_window_active` pine_strategy_commands.cpp:15-21, `prepare_native_begin` pine_strategy_host.cpp:206-311) are A. S's "neutral tail + trade-window policy" is single-source and unassigned (§4 Q8). | | CT11 | *(quirk)* COOF TV specifics: waypoint-only refill (`coof_next_waypoint` pine_adapter.cpp:6949), two-fills-at-open rule, script-state rollback (`PineScheduler` pine_scheduler_native.cpp:54-106; hooks `snapshot_script_state` engine.hpp:1332-1333), the first-open guard `kFirstOpenLoopGuard` pine_scheduler_native.cpp:848, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1961-1991 | n/a | A | — | F:E4 O:F5 S:C2 | R5-5: the adapter keeps its path | ### 1.6 Higher timeframes and auxiliary feeds | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| HT1 | HTF series of the same symbol (`request.security`): evaluators registered `scheduler_configure_security_evaluators` pine_strategy_host.cpp:1524, pumped from the scheduler `scheduler_prepare_security_sequence` pine_strategy_host.cpp:1534-1833; state lives in `BacktestEngine` engine.hpp:356-558, engine.hpp:356-402, engine_security.cpp:27 | **no** sanctioned path — `configure_security_evaluators` is an empty virtual engine.hpp:1322 whose only caller is pine_strategy_host.cpp:1525 **Closed:** `NativeRunSpec::subscriptions` (`NativeTimeframeSubscription` native_run_spec.hpp:491), or `declare_timeframe_subscriptions` native_host.hpp:1136 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:885 and `native_series_bar(index)` native_host.hpp:1119 reading them back; the kernel registers the declared series after that callback returns, so a host's own evaluators survive (`tests/test_native_htf_subscriptions.cpp`, `tests/test_native_htf_subscriptions_stream.cpp`). | K | L6 | F:F1 O:G-1 S:H1 | **Disagree:** S partial (it counts kernel-side storage → HT2); F, S v1 / O deferrable. R5-6: v1. | -| HT2 | Authoritative (exchange) HTF bars substituted into a series (`set_native_security_feed` engine.hpp:1731, engine_aux_security.cpp:79-133, C ABI `strategy_set_native_security_feed` c_abi.cpp:937) | **partial (inert)** — pre-run staging is accepted `refuse_source_mutation` native_execution_consumer.cpp:1878 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, its sole call `prepare_native_security_feeds` pine_strategy_host.cpp:1656; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1896 **Closed:** a bare host's subscriptions consume authoritative bars: `NativeTimeframeSubscription::authoritative_bars` native_run_spec.hpp:493 is staged into the feed store by the kernel's own `prepare_native_security_feeds` native_execution_consumer.cpp:9490 call at begin, so a declared series is the venue's own bars (`tests/test_native_htf_subscriptions.cpp`). | K | L6 | F:F2 O:G-2 S:H1 | F says "no" on the same facts. "native" in these names means TradingView's native-timeframe bars, not the native host (the feed store's comment `never from the chart's intraday prints` `sha256:3cc5174fa9a60fc0e2ca7870ef1cccd875abaf5d8e11248e633922612c18d9d8` engine_aux_security.cpp:51-61). | +| HT1 | HTF series of the same symbol (`request.security`): evaluators registered `scheduler_configure_security_evaluators` pine_strategy_host.cpp:1522, pumped from the scheduler `scheduler_prepare_security_sequence` pine_strategy_host.cpp:1534-1833; state lives in `BacktestEngine` engine.hpp:356-558, engine.hpp:356-402, engine_security.cpp:27 | **no** sanctioned path — `configure_security_evaluators` is an empty virtual engine.hpp:1322 whose only caller is pine_strategy_host.cpp:1523 **Closed:** `NativeRunSpec::subscriptions` (`NativeTimeframeSubscription` native_run_spec.hpp:491), or `declare_timeframe_subscriptions` native_host.hpp:1136 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:885 and `native_series_bar(index)` native_host.hpp:1119 reading them back; the kernel registers the declared series after that callback returns, so a host's own evaluators survive (`tests/test_native_htf_subscriptions.cpp`, `tests/test_native_htf_subscriptions_stream.cpp`). | K | L6 | F:F1 O:G-1 S:H1 | **Disagree:** S partial (it counts kernel-side storage → HT2); F, S v1 / O deferrable. R5-6: v1. | +| HT2 | Authoritative (exchange) HTF bars substituted into a series (`set_native_security_feed` engine.hpp:1731, engine_aux_security.cpp:79-133, C ABI `strategy_set_native_security_feed` c_abi.cpp:938) | **partial (inert)** — pre-run staging is accepted `refuse_source_mutation` native_execution_consumer.cpp:1878 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, its sole call `prepare_native_security_feeds` pine_strategy_host.cpp:1654; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1896 **Closed:** a bare host's subscriptions consume authoritative bars: `NativeTimeframeSubscription::authoritative_bars` native_run_spec.hpp:493 is staged into the feed store by the kernel's own `prepare_native_security_feeds` native_execution_consumer.cpp:9490 call at begin, so a declared series is the venue's own bars (`tests/test_native_htf_subscriptions.cpp`). | K | L6 | F:F2 O:G-2 S:H1 | F says "no" on the same facts. "native" in these names means TradingView's native-timeframe bars, not the native host (the feed store's comment `never from the chart's intraday prints` `sha256:3cc5174fa9a60fc0e2ca7870ef1cccd875abaf5d8e11248e633922612c18d9d8` engine_aux_security.cpp:51-61). | | HT3 | Lower-timeframe arrays (`request.security_lower_tf`; `register_security_lower_tf_eval` include/pineforge/source/pine_strategy_host.hpp:832) | **no** — `IntrabarPath::lower_tf` drives matching only **Closed:** `on_native_sub_bar` native_host.hpp:928 delivers every retained `lower_tf` sub-bar after its matching path, so a host that put the lower bars in the path reads them bar by bar (`tests/test_native_calc_timing.cpp`); the series form is the auxiliary feed of HT4 (`tests/test_native_auxiliary_feed.cpp`). | K | L5 (sub-bar hook); series form optional in L6 | F:F4 O:G-3 S:H2 | **Disagree on lane:** F via `on_native_sub_bar` (the host already owns the lower bars it put in the path) / O, S via the series API. v1 = the L5 hook (R5-5), landed; the array form Pine returns stays the adapter's. | | HT4 | Auxiliary finer feed driving `request.security` (`set_aux_security_feed` pine_aux_security.cpp:23-241, `feed_aux_security_for_chart_bar` pine_aux_security.cpp:262-488; the kernel base returns false `set_aux_security_feed` engine_consumer.cpp:158-161) | **no** **Closed:** `NativeRunSpec::auxiliary_feed` (`NativeAuxiliaryFeed` native_run_spec.hpp:524) plus `NativeSeriesSource::AuxiliaryFeed` build a series from the host's finer bars, routed by time alone, with `declare_auxiliary_feed` at begin and `append_auxiliary_bars` on a realtime stream (`tests/test_native_auxiliary_feed.cpp`, `tests/test_native_auxiliary_feed_stream.cpp`). | K+A | L6 | F:F3 S:H2 | **Disagree:** F quirk (TV split-feed harness construct) / S generic. R5-1 + R5-6: aux feed registration is generic; chart-slice mapping and deferred first-bucket publication stay A. **Closed (lane INT28-FIX, rule RC):** a `calc_on_order_fills` recalculation reads the calling bar's own slice, as the bar's close calculation does and as TradingView's does after the open fill or a later fill (a lookahead_on site through the slice's first bucket; `lab tv` tapes (the private fixture set `coof_recalc_security`): the bar's first recalculation before its close callback feeds it (`scheduler_feed_aux_security` pine_scheduler_native.cpp:772). The close callback fed it alone (`scheduler_feed_aux_security` pine_scheduler_native.cpp:634, now only when no recalculation did), after the bar's recalculations had read the previous bar's; a recalculation after that callback feeds nothing. | | HT5 | Publication mode: lookahead, gaps, session / early-close boundaries (`prepare_historical_security_lookahead_projections` pine_scheduler.cpp:42-132, `calling_bar_complete` pine_aux_security.cpp:282-397) | **no** for a bare host **Closed for a bare host:** `NativeTimeframeSubscription::lookahead` and `::gaps` are the two delivery rules, and the calendar boundaries — session close, early close by instrument class, the lazy seal of a clipped script interval — are the kernel's own (`tests/test_native_htf_subscriptions.cpp`). | K+A | L6 | S:H3 F:F1 O:G-1 | **Disagree:** F "no `lookahead_on`, by construction" / O `lookahead = false` default, available / S `lookahead_on` field, status partial. R5-6: user question (§4 U1); the exact TV projection stays A. | @@ -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-85, `get_trade` engine.hpp:1792-1829, `compute_trade_stats` engine_metrics.cpp:122 | K | — | F:H2 O:I3 S:X2 | — | | RP3 | Equity curve | **no** — `update_equity_extremes` engine.hpp:1362 / `record_equity_point` engine.hpp:1380 are protected and were called only by the adapter **Closed:** `NativeReportPolicy::KernelRecorded` native_run_spec.hpp:63 has the consumer fold the extremes and append one point per script calculation, labelled with the script interval's open; `KernelRecordedAtHostMarks` records the same series at the host's own marks (`tests/test_native_report_truth.cpp`). | K | L2 | F:H3 O:I4 S:X2 | S's "partial" folds RP2 in | | RP4 | Equity metrics (drawdown, run-up, Sharpe / Sortino, CAGR, time in market) | **no** — `compute_equity_stats` exits early on an empty curve engine_metrics.cpp:203; `fill_metrics_section` engine_report.cpp:138-145 **Closed:** with a recorded curve the drawdown / run-up walk is finite and every equity metric is computed over a real series; `report_open_position_at_end` adds the still-open position as a mark-to-market row (`tests/test_native_report_truth.cpp`). | K | L2 | F:H3 O:I5 | silent degenerate metrics, no error (O) | -| RP5 | Range-end row for a position still open at the end (`scheduler_record_range_end` pine_strategy_host.cpp:1987) | **yes, shared** — one generic producer `NativeExecutionConsumer::append_open_position_report_rows`, called by the kernel's run-end `record_open_position_report_rows` (`KernelRecorded` + `report_open_position_at_end`) and by the adapter at TradingView's mark; `report_trade_count()` engine.hpp:1831 includes the rows | K + A report shape | L2, Q6 | F:H4 O:I6 S:X2 | **Disagree:** S says kernel range-end rows exist (storage only). The ROWS are now the kernel's (§3.7 part 1); TV report shape — the equity re-mark, the extreme re-fold, the same-bar re-sort, three marks — stays A, measured in §3.7 part 2. The short-seed swaps (`project_short_seed_report_rows` pine_strategy_host.cpp:1449) stay A as well: a projection of two rows' `entry_incarnation` onto `ab9714be`'s numbering, which TradingView does not export; measured against the kernel's own lot identities by `tests/test_short_seed_report_swap.cpp` (R5 lane H-MEASURE), and pinned by `test_native_oracle_short_seed_percent_full_l2`, whose two incarnation checks fail when the swap is skipped. The swap runs only when a later close of the seed id closes a remnant; a flat collision keeps the kernel's numbering. | +| RP5 | Range-end row for a position still open at the end (`scheduler_record_range_end` pine_strategy_host.cpp:1985) | **yes, shared** — one generic producer `NativeExecutionConsumer::append_open_position_report_rows`, called by the kernel's run-end `record_open_position_report_rows` (`KernelRecorded` + `report_open_position_at_end`) and by the adapter at TradingView's mark; `report_trade_count()` engine.hpp:1831 includes the rows | K + A report shape | L2, Q6 | F:H4 O:I6 S:X2 | **Disagree:** S says kernel range-end rows exist (storage only). The ROWS are now the kernel's (§3.7 part 1); TV report shape — the equity re-mark, the extreme re-fold, the same-bar re-sort, three marks — stays A, measured in §3.7 part 2. The short-seed swaps (`project_short_seed_report_rows` pine_strategy_host.cpp:1447) stay A as well: a projection of two rows' `entry_incarnation` onto `ab9714be`'s numbering, which TradingView does not export; measured against the kernel's own lot identities by `tests/test_short_seed_report_swap.cpp` (R5 lane H-MEASURE), and pinned by `test_native_oracle_short_seed_percent_full_l2`, whose two incarnation checks fail when the swap is skipped. The swap runs only when a later close of the seed id closes a remnant; a flat collision keeps the kernel's numbering. | | RP6 | Per-trade accessors | **partial** — `get_trade` is public; the `closed_trade_*` family is protected engine.hpp:1391 **Closed:** `closed_trade_count()` / `closed_trade(i)` and `report_trade_count()` / `get_report_trade(i)` are public, and `closed_trade_close_cause(i)` engine.hpp:1849 answers the typed `execution::CloseCause` the closer recorded (`tests/test_native_report_truth.cpp`). | K | L2 | F:H6 | (single-source) | | RP7 | Live order-action stream (runner ledger / webhook) | **yes** — `stream_observe_actions_` engine_execution.cpp:1383, enabled `stream_observe_actions_` native_execution_consumer.cpp:10666, C ABI `strategy_stream_order_actions_len` c_abi.cpp:737-754 | K | — | F:H5 | (single-source) | | RP8 | Continuation hash | **yes** — `native_continuation_hash` native_host.hpp:1376, native_execution_consumer.cpp:12229; the spec is folded by `hash_spec` native_execution_consumer.cpp:238-260 | K | — | F:I1 O:J2 S:X3 | — | -| RP9 | Broker-state hash: scalar + per-bar recording | **partial** — scalar `broker_state_hash` engine_state_hash.cpp:22-118, C ABI `strategy_broker_state_hash` c_abi.cpp:593; per-bar rows are appended only by the source scheduler `scheduler_record_broker_hash` pine_strategy_host.cpp:2795 **Closed:** under `KernelRecorded` with `set_broker_state_hash_recording(true)` the consumer appends one `broker_state_hash()` row after each report point, so `broker_state_hash_len == equity_curve_len == script_bars_processed` on a completed run in batch and across a stream's warmup and realtime legs. A Failed or Aborted run can expose a shorter prefix. A row is the run's continuation identity at that bar, per driving mode — the broker half alone is mode-invariant (`tests/test_native_report_truth.cpp`). | K | L2 | F:I2 O:J1 S:X3 | O's "yes" covers the scalar only. §3.9 records the old quadratic adapter fold and closed-row kernel walk as history; V19-E and V19-A replaced them with running digests. | +| 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:594; per-bar rows are appended only by the source scheduler `scheduler_record_broker_hash` pine_strategy_host.cpp:2793 **Closed:** under `KernelRecorded` with `set_broker_state_hash_recording(true)` the consumer appends one `broker_state_hash()` row after each report point, so `broker_state_hash_len == equity_curve_len == script_bars_processed` on a completed run in batch and across a stream's warmup and realtime legs. A Failed or Aborted run can expose a shorter prefix. A row is the run's continuation identity at that bar, per driving mode — the broker half alone is mode-invariant (`tests/test_native_report_truth.cpp`). | K | L2 | F:I2 O:J1 S:X3 | O's "yes" covers the scalar only. §3.9 records the old quadratic adapter fold and closed-row kernel walk as history; V19-E and V19-A replaced them with running digests. | | RP10 | Fold the host's own durable state into the hash | **partial** — the protected virtual `hash_source_extension` engine.hpp:411 is overridable but source-named; the kernel default folds `"source:none"` engine_state_hash.cpp:19-21 **Closed:** `hash_host_extension(BrokerStateHashSink&)` engine.hpp:405 is the generic seam — protected virtual on `BacktestEngine`, called once per hash, last — and `hash_source_extension` engine.hpp:411 is its deprecated spelling, which the generic default forwards to (`tests/test_native_host_hash_extension.cpp`). | K | L2 (the virtual rides the shared bump) | F:I3 O:J1 S:X3 | — | ### 1.9 Other runtime surface @@ -174,10 +174,10 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | OT2 | Symbol info, sessions, calendars, timezones (`timezone` pine_adapter.cpp:2737, `chart_timezone` pine_adapter.cpp:2780) | **yes** — spec fields `ticker` native_run_spec.hpp:636-646; `parse_session` native_calendar.hpp:180-467; intervals on every decision context `input_interval` market_driver.hpp:123-124 | K | — | F:J2 O:G-5 | — | | OT3 | Indicators, series, math, matrix, map, string utils | **yes** — `ta.hpp` includes only `na/series/window_sum` `window_sum` ta.hpp:2-8 | K | L11 (naming only) | F:J3 | (single-source row) TV-calibrated numerics → §2.ii b | | OT4 | Cooperative abort | **yes** — `request_abort` engine.hpp:2174, `NativeAbortReporting` native_run_spec.hpp:41-44 | K | — | F:J4 | (single-source) | -| OT5 | C-level run configuration, stream, FX curve, contract probe | **yes** — `strategy_configure_native_v1` c_abi.cpp:980, FX curve `strategy_configure_native_fx_curve_v1` c_abi.cpp:1051, probe `strategy_execution_contract` c_abi.cpp:971, stream `strategy_stream_begin` c_abi.cpp:698 **Extended:** `pf_native_run_spec_ext_v1`'s policy tail now carries `intrabar`, `path_order`, `abort_reporting` and the slot-label / feed-tolerance policies (`PF_NATIVE_SPEC_EXT_INTRABAR`, `PF_NATIVE_SPEC_EXT_FEED_POLICY`), so nothing of `NativeRunSpec` but `identity` is unreachable from C (`tests/test_native_c_api.c`). | K | — | O:J4 | The v1 C spec `pf_native_run_spec_v1` pineforge.h:510 omitted `intrabar`, `path_order` and `abort_reporting`; they now travel in the extension's policy tail, `pf_native_abort_reporting_e` native_c_api.h:911 among them (F §2.iii) | -| OT6 | C-level order submission + strategy callbacks | **no** — none of the original runtime `PF_API` symbols; then listed as a refusal on native-engine.md **Closed:** `strategy_native_host_create_v1` takes a C callback table and `strategy_native_submit_v1` / `_replace_v1` / `_cancel_v1` / `_cancel_all_v1` / `_cancel_where_v1` / `_execute_current_v1` command the same kernel under the same legality rule; the 41 `strategy_native_*` declarations plus two `strategy_configure_native_ext_*` declarations are a second, disjoint inventory beside the 80 runtime exports (and eight generated per-strategy exports) (`tests/test_native_c_api.c`, `tests/test_native_c_api_frozen_header.cpp`). | K | L13 | F:J6 O:J5 | R5-7: post-v1. S covers it in its §2.3 without a row. | -| OT7 | *(quirk)* Pine language state: series, barstate / session flags, position-view freezing (`PineLanguageState` pine_language_state.hpp:12-68; `publish_series` pine_scheduler_native.cpp:323-358, `chart_index` pine_scheduler_native.cpp:570-703) | n/a | A | — | S:R1 | (single-source) **Closed (E25, lane E20's finding 2; E26, E25's findings 1 and 3):** `session.islastbar` reads one script bar ahead on every driving path: the chart timeframe's next retained bar, an aggregated chart's next bucket (a scheduler lookahead, deleted when the rule moved into the kernel), the calendar for the live tail's bar and for every bar of a stream, warmup included — a stream continues past its replay, so only a batch run's final bar reads "last" from having no successor. Since R4 slice C an aggregated or realtime bar read every in-session bar as last. **What TradingView does:** it ends a session at the session DAY — the flag belongs to the last chart bar whose successor belongs to another session day, in session or not, and `session.isfirstbar` is its dual, the bar after a session's last one. The ordinal is `internal::session_trading_day_index` timeframe.hpp:145, the exchange-timezone day rolling at the symbol's day stamp (09:30 ET on NYSE RTH, midnight on 24x7, 17:00 ET on a 1700-1700 forex session), not local midnight; the unmerged form, since a fused holiday daily bar still holds two intraday sessions. The three `lab tv` tapes (the private fixture set `session_islastbar`) give that rule bar for bar with no disagreement, and the engine now holds 255/255 NYSE:F last bars, 255/255 first bars and 370/370 ETH last bars on the chart-timeframe AND the aggregated path (it held 1, 0 and 1 before). Early closes come with it: the three NYSE:F half days flag 12:45 ET without a holiday calendar, because the next bar is the next session day's. **Generic since R5 lane F5:** the kernel computes that reading for every host — `NativeDecisionContext::in_session` market_driver.hpp:161 and the facts after it (`opens_session_day`, `closes_session_day`, `closes_session_day_open_ended`), in C the session bytes of `pf_native_decision_v1` native_c_api.h:1485 — from the run's own calendar and input (`present_session_day` native_execution_consumer.cpp:8229), so a bare host reproduces the tapes flag for flag (the private test `test_native_session_day_facts`, kernel-only). The adapter computes no regular-day rule of its own: `scheduler_update_session_state` pine_strategy_host.cpp:2228 selects the kernel's facts into the flags, and only widens the plain `session.isfirstbar` / `islastbar` pair to the chart's day on an extended-hours chart (R5 lane K-SESSION-WINDOWS F2: TradingView's `isfirstbar` / `islastbar` span the pre- and post-market bars there while the `_regular` pair keeps the regular day, `tests/test_session_extended_day_tape.cpp`), which are `PineStrategyHost` members now (`session_ismarket_` pine_strategy_host.hpp:1186; `BacktestEngine` holds no Pine session state), and only its live-probe tail reads the open-ended close at its batch's final bar (the private test `test_session_day_facts_adapter`). Selecting the kernel also ended a calc_on_order_fills defect of the adapter's lookahead, which read two bars ahead on a bar a fill recalculation had already published (a fill on a day's second-to-last bar flagged that bar last and the day's real last bar first). **Ruled, not open:** a bar with nothing held after it — a stream's bar, a live-tail bar — steps the calendar one width on, so it cannot see an early close the session string does not declare (ADR-0001, "Session-day facts at a bar with nothing held after it"). **Closed (F1):** a `calc_on_order_fills` recalculation reads the flags of the bar it runs on, not the previous bar's (the kernel presents the current bar's facts on that recalculation), and the close callback after it no longer looks one retained bar too far ahead (§3.8, AG3). | -| OT8 | *(quirk)* Live-tail / probe-suppress harness flags (`realtime_tail_` pine_strategy_host.cpp:300) | n/a — **contained since N14** (the flags were `BacktestEngine` members until then; now `source::PineStrategyHost` state behind two kernel virtual seams, §2.ii q) | A | N14 | F:J7 S:C5 | parity-campaign tooling | +| OT5 | C-level run configuration, stream, FX curve, contract probe | **yes** — `strategy_configure_native_v1` c_abi.cpp:981, FX curve `strategy_configure_native_fx_curve_v1` c_abi.cpp:1052, probe `strategy_execution_contract` c_abi.cpp:972, stream `strategy_stream_begin` c_abi.cpp:699 **Extended:** `pf_native_run_spec_ext_v1`'s policy tail now carries `intrabar`, `path_order`, `abort_reporting` and the slot-label / feed-tolerance policies (`PF_NATIVE_SPEC_EXT_INTRABAR`, `PF_NATIVE_SPEC_EXT_FEED_POLICY`), so nothing of `NativeRunSpec` but `identity` is unreachable from C (`tests/test_native_c_api.c`). | K | — | O:J4 | The v1 C spec `pf_native_run_spec_v1` pineforge.h:510 omitted `intrabar`, `path_order` and `abort_reporting`; they now travel in the extension's policy tail, `pf_native_abort_reporting_e` native_c_api.h:911 among them (F §2.iii) | +| OT6 | C-level order submission + strategy callbacks | **no** — none of the original runtime `PF_API` symbols; then listed as a refusal on native-engine.md **Closed:** `strategy_native_host_create_v1` takes a C callback table and `strategy_native_submit_v1` / `_replace_v1` / `_cancel_v1` / `_cancel_all_v1` / `_cancel_where_v1` / `_execute_current_v1` command the same kernel under the same legality rule; the 41 `strategy_native_*` declarations plus two `strategy_configure_native_ext_*` declarations are a second, disjoint inventory beside the 81 runtime exports (and eight generated per-strategy exports) (`tests/test_native_c_api.c`, `tests/test_native_c_api_frozen_header.cpp`). | K | L13 | F:J6 O:J5 | R5-7: post-v1. S covers it in its §2.3 without a row. | +| OT7 | *(quirk)* Pine language state: series, barstate / session flags, position-view freezing (`PineLanguageState` pine_language_state.hpp:12-68; `publish_series` pine_scheduler_native.cpp:323-358, `chart_index` pine_scheduler_native.cpp:570-703) | n/a | A | — | S:R1 | (single-source) **Closed (E25, lane E20's finding 2; E26, E25's findings 1 and 3):** `session.islastbar` reads one script bar ahead on every driving path: the chart timeframe's next retained bar, an aggregated chart's next bucket (a scheduler lookahead, deleted when the rule moved into the kernel), the calendar for the live tail's bar and for every bar of a stream, warmup included — a stream continues past its replay, so only a batch run's final bar reads "last" from having no successor. Since R4 slice C an aggregated or realtime bar read every in-session bar as last. **What TradingView does:** it ends a session at the session DAY — the flag belongs to the last chart bar whose successor belongs to another session day, in session or not, and `session.isfirstbar` is its dual, the bar after a session's last one. The ordinal is `internal::session_trading_day_index` timeframe.hpp:145, the exchange-timezone day rolling at the symbol's day stamp (09:30 ET on NYSE RTH, midnight on 24x7, 17:00 ET on a 1700-1700 forex session), not local midnight; the unmerged form, since a fused holiday daily bar still holds two intraday sessions. The three `lab tv` tapes (the private fixture set `session_islastbar`) give that rule bar for bar with no disagreement, and the engine now holds 255/255 NYSE:F last bars, 255/255 first bars and 370/370 ETH last bars on the chart-timeframe AND the aggregated path (it held 1, 0 and 1 before). Early closes come with it: the three NYSE:F half days flag 12:45 ET without a holiday calendar, because the next bar is the next session day's. **Generic since R5 lane F5:** the kernel computes that reading for every host — `NativeDecisionContext::in_session` market_driver.hpp:161 and the facts after it (`opens_session_day`, `closes_session_day`, `closes_session_day_open_ended`), in C the session bytes of `pf_native_decision_v1` native_c_api.h:1485 — from the run's own calendar and input (`present_session_day` native_execution_consumer.cpp:8229), so a bare host reproduces the tapes flag for flag (the private test `test_native_session_day_facts`, kernel-only). The adapter computes no regular-day rule of its own: `scheduler_update_session_state` pine_strategy_host.cpp:2226 selects the kernel's facts into the flags, and only widens the plain `session.isfirstbar` / `islastbar` pair to the chart's day on an extended-hours chart (R5 lane K-SESSION-WINDOWS F2: TradingView's `isfirstbar` / `islastbar` span the pre- and post-market bars there while the `_regular` pair keeps the regular day, `tests/test_session_extended_day_tape.cpp`), which are `PineStrategyHost` members now (`session_ismarket_` pine_strategy_host.hpp:1186; `BacktestEngine` holds no Pine session state), and only its live-probe tail reads the open-ended close at its batch's final bar (the private test `test_session_day_facts_adapter`). Selecting the kernel also ended a calc_on_order_fills defect of the adapter's lookahead, which read two bars ahead on a bar a fill recalculation had already published (a fill on a day's second-to-last bar flagged that bar last and the day's real last bar first). **Ruled, not open:** a bar with nothing held after it — a stream's bar, a live-tail bar — steps the calendar one width on, so it cannot see an early close the session string does not declare (ADR-0001, "Session-day facts at a bar with nothing held after it"). **Closed (F1):** a `calc_on_order_fills` recalculation reads the flags of the bar it runs on, not the previous bar's (the kernel presents the current bar's facts on that recalculation), and the close callback after it no longer looks one retained bar too far ahead (§3.8, AG3). | +| OT8 | *(quirk)* Live-tail / probe-suppress harness flags (`realtime_tail_` pine_strategy_host.cpp:298) | 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,7 +202,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | # | Item | Where / what it is used for | Class | Plan | Src | |---|---|---|---|---|---| -| a | `source::StrategyOverrides` opaque pointer | Forward declarations in the kernel headers (since removed: no kernel header names the adapter type now); parameter of `run_rich` (execution_consumer.hpp:46-55, native_execution_consumer.hpp:110-119) and of the rich `BacktestEngine::run` overload (engine.hpp:1796-1801, engine_consumer.cpp:81-93). The kernel only forwards it as `NativeBeginArgs::overrides_opaque` native_host.hpp:758; the source host casts it back `overrides_opaque` pine_strategy_host.cpp:307 | NEUTRAL in behaviour; a consumer-vtable signature change, so it rides the shared `engine_script_run` bump | `const void*` in the kernel signatures (F `host_begin_extras`, O `overrides_opaque`), typed convenience overload in `source/pine_strategy_host.hpp`; then drop the checker's single whitelist entry `test_native_example_batch` check_native_include_independence.py:42-46. S's alternative (`NativeRunOverrides`, or removing the rich overload) not adopted. Unverified: generated code calls the typed overload (codegen repo is not in this tree — F). **Status (L11, `3f4fff5b`): done** — `run_rich` and the rich `BacktestEngine::run` take `const void*` overrides, the rich begin carries them as `overrides_opaque`, and the independence checker whitelists nothing (§2.vi row 6) | F O S | +| a | `source::StrategyOverrides` opaque pointer | Forward declarations in the kernel headers (since removed: no kernel header names the adapter type now); parameter of `run_rich` (execution_consumer.hpp:46-55, native_execution_consumer.hpp:110-119) and of the rich `BacktestEngine::run` overload (engine.hpp:1796-1801, engine_consumer.cpp:81-93). The kernel only forwards it as `NativeBeginArgs::overrides_opaque` native_host.hpp:758; the source host casts it back `overrides_opaque` pine_strategy_host.cpp:305 | NEUTRAL in behaviour; a consumer-vtable signature change, so it rides the shared `engine_script_run` bump | `const void*` in the kernel signatures (F `host_begin_extras`, O `overrides_opaque`), typed convenience overload in `source/pine_strategy_host.hpp`; then drop the checker's single whitelist entry `test_native_example_batch` check_native_include_independence.py:42-46. S's alternative (`NativeRunOverrides`, or removing the rich overload) not adopted. Unverified: generated code calls the typed overload (codegen repo is not in this tree — F). **Status (L11, `3f4fff5b`): done** — `run_rich` and the rich `BacktestEngine::run` take `const void*` overrides, the rich begin carries them as `overrides_opaque`, and the independence checker whitelists nothing (§2.vi row 6) | F O S | | b | `pine_float_compare.hpp` | 1e-10 absolute equality band, used by kernel TA only: `float_band_le` ta_misc.cpp:30 (percentrank), `float_band_gt` ta_oscillators.cpp:462-463 (MFI), `plus_dm` ta_volatility_trend.cpp:337-340 (DMI); no execution TU includes it (O) | NEUTRAL (rename only) | Rename to a neutral header (F `float_band_compare.hpp`, O `ta_compare_band.hpp`), keep `pine_float_*` inline aliases in a source-layer header for generated code. Numerics must not move. S's behavioural `ComparePolicy` split is not neutral → not adopted (§4 U6). **Status (L11 + N14): done** — `ta_compare_band.hpp` is the kernel header; the alias shim is `include/pineforge/source/pine_float_compare.hpp` (N14 moved it off the kernel include root; it has no in-tree includer, generated code emits its own comparator) | F O S | | c | `NativeSlotLabelPolicy::LegacyTolerant`, `NativeLegacyTolerance` (now `NativeSlotLabelPolicy::FeedTolerant`, `NativeFeedTolerance`) | `NativeSlotLabelPolicy` native_run_spec.hpp:324-353; read by `legacy_tolerant_slot_labels` market_driver.cpp:55-56 and `native_feed_tolerance_enabled` market_driver.cpp:43-49, and by `legacy_tolerant_slot_labels` native_execution_consumer.hpp:1027-1029 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:2368; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:2385, `:10068`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2806. A feed-tolerance policy (raw strictly-increasing labels, zero / NaN-volume legacy bars), not TV semantics (O) | NEUTRAL | Rename with deprecated aliases (F `RawIncreasing` + `NativeInputTolerance::{StructuralBars, WarmupNonNegativeOHLC}`; O `NativeLabelPolicy::ProviderLabels`). Enumerator values stay 0/1 and 1/2: they are hashed `slot_label_policy` native_execution_consumer.cpp:272. **Status:** renamed as `FeedTolerant` / `NativeFeedTolerance` (the enumerator values unchanged), and the C transport carries them now, in the extension's feed-policy block (`PF_NATIVE_SPEC_EXT_FEED_POLICY` native_c_api.h:745) | F O S | | d | Orphaned comments | The KI-62 block of `engine_orders.cpp` (*fresh*: comment-only, the bodies are deleted; S read it as live code) and a comment of `engine.hpp`; wider deleted-declaration blocks in `engine.hpp`; the money-rule essay in `engine.hpp` (the code is `source_money_round` pine_adapter.cpp:558) | NEUTRAL | Delete, or move to `docs/` next to the adapter code that implements them. **Status: done** — R5 lanes E6 and E10 deleted the detached comment residue (a census of 0 over the kernel's compile closure) | F O | @@ -230,7 +230,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa 1.0 C surface still does not expose is the 1.0 C boundary table of `docs/pages/native-engine.md`. "Today" below is the tree L13 read.* -Today a C host can configure (`strategy_configure_native_v1` c_abi.cpp:980-1045), stream (`strategy_stream_*` c_abi.cpp:623-732) 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:981-1045), stream (`strategy_stream_*` c_abi.cpp:623-732) and read results, but strategy logic is C++-only and the batch entry points (`strategy_create`, `run_backtest*`) are per-strategy symbols a native author hand-writes. Design retained from F (most complete), with O's event polling and S's hardening rules: ```c typedef struct pf_native_decision_v1 { uint64_t ordinal; int32_t interval_index; /* SCRIPT-bar index */ int64_t effective_time_ms, @@ -327,7 +327,7 @@ Union of F §2.vi (9), O §2.vi (5 + 1) and S §2.6 (7), deduplicated. S had no | 5 | "`engine_aux_security.cpp` includes `source/pine_strategy_host.hpp`" (ADR L129-132) | False at `73817c1`: its includes are `engine_internal.hpp`, `ta.hpp` and std headers (*fresh* re-read; the run-failure codes added `run_failure.hpp`, a kernel header). No kernel TU includes a source header | `sha256:560a7f68b243d921382a69ed0390c398f28370ac066b84f30b5f96cbb385fb60` engine_aux_security.cpp:6-14 | F | | 6 | "Kernel TUs reference adapter types (`source::StrategyOverrides` …)" (ADR L129-131) | True only as a forward-declared opaque pointer the kernel never dereferences; the independence checker whitelists exactly that symbol | at `73817c1`; overtaken by `3f4fff5b` (§2.ii a), after which the rich begin carries `overrides_opaque` native_host.hpp:758 and the independence checker whitelists nothing | F O | | 7 | `engine.hpp` hosts "the ten-significant-digit money rule, KI-62, the `strategy.close` batching + entry-id ledger, a TradingView margin-call toggle" (ADR L131-134) | Two of four survive as code. Money rule: a comment block; the arithmetic is in the adapter. KI-62: orphaned comments + one live, hashed lot flag. Close batching + id ledger: gone from the kernel (the ledger is in the adapter; the old names survive only in comments). Live: `margin_call_enabled_`, the KI-62 flag and — unmentioned by the ADR — string-sentinel decoding of adapter comments | the money-rule block was lines 927-961 of `engine.hpp` and R5 lane E6 deleted it (lines 73-180 were and are live code), the arithmetic `source_money_round` pine_adapter.cpp:558 and its twin `tv_money_round` pine_policy_support.hpp:9-15; `skip_entry_bar_high` engine.hpp:152, `skip_entry_bar_low` engine_state_hash.cpp:74, `declare_opened_lot_entry_bar_mask` engine_path_resolve.cpp:95; the ledger `close_logical_units_` pine_adapter.hpp:2580; `closed_trade_close_cause` engine_trade_accessors.cpp:158. **Overtaken:** the margin-call toggle and the string decoding are both gone — see ADR-0001, "Current state vs. target" | F O | -| 8 | "No kernel-level C API. `c_abi.cpp` exports only version/descriptor symbols" (ADR L136-138) | 80 runtime `PF_API` symbols: native configure, the 12-symbol stream family, pending-order views, broker-state hash, security feeds, FX curve, reports. The 80 compiled-strategy runtime symbols in `c_abi.cpp` are distinct from the native C host API in `native_c_host.cpp`: the latter exposes submit, replace, cancel and a callback table to a C host. | `sha256:733786ed81c9137223b98e1887e28d01afdb3c64ba5e8ca52ce5161f2caf3f75` c_abi.cpp:325-1013, `sha256:6327f1829019fdd172cd76a96e3d8c9c0af1610ad0dc9878640dda0ca21a686b` check_c_abi_runtime.py:20-81, c_abi.cpp:946, `sha256:5ff4c65a56410fe29b21cd68bc21ac87bdb34b77344e4f75f90efa8887fe48e8` c_abi.cpp:664-788, `sha256:051fe087eb9d30e9960dc4f0ab3f7cc837531ebeaa71b5871a3f1cdc22fb37c2` c_abi.cpp:566-641, `sha256:f18d46ac323234f7e3082367d30840b704b8a7548f7ae95ee8cff8714f29fd50` c_abi.cpp:545-562, `sha256:6a9ef4e688f33dc10ea8297d9eaa8144ea7923ef6ee6f8fe9ff20f6af4e9acb3` c_abi.cpp:881-917, c_abi.cpp:1013 | F O S | +| 8 | "No kernel-level C API. `c_abi.cpp` exports only version/descriptor symbols" (ADR L136-138) | 81 runtime `PF_API` symbols: native configure, the 12-symbol stream family, pending-order views, broker-state hash, security feeds, FX curve, reports. The 81 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:feac4f5f90ac5b9937d135a91ffad7f8b8995376eb18debd023654f66248dc91` c_abi.cpp:325-1013, `sha256:6327f1829019fdd172cd76a96e3d8c9c0af1610ad0dc9878640dda0ca21a686b` check_c_abi_runtime.py:20-81, c_abi.cpp:946, `sha256:2dcb54d244bdfd0786cf40d32db6102cc2a4e5facd0de3e8fcb30acb73b7dd1e` c_abi.cpp:664-788, `sha256:0f22c43bf10dd4c46335a227184542f7d7a14ea54a0ed2664a584fbf9194e219` c_abi.cpp:566-641, `sha256:61e72f0ca6ab5b6d99ef953b60bb7d6059031a2ee7d634e248e3303b1360e47` c_abi.cpp:545-562, `sha256:435f085c3434863cf4e7edb0f403723114fcbccacdf9c51f7239c75c920d68b0` c_abi.cpp:881-917, c_abi.cpp:1013 | F O S | | 9 | "No examples. `PINEFORGE_BUILD_EXAMPLES` is 'none yet'" (ADR L139) | The option is inert, but two Pine-free `NativeStrategyHost` examples exist, are compiled by the independence checker and tested through the live runner, only under `PINEFORGE_BUILD_LIVE_RUNNER`. Wording: "no top-level examples target" | at `73817c1` (overtaken; ADR-0001, "Current state vs. target") | F O S | | 10 | Follow-ups (1)-(4) (ADR L168-170) | Add native reporting, sizing, margin, HTF — and, per R5, calc-timing, price grid and order ergonomics. Without them "usable without Pine" is a build-system claim, not a feature claim | §3.5 | F | | 11 | Build layout | One `pineforge` library always appends the source list; a kernel-only target does not exist yet; `compat/pine/market_admission.cpp` sits in the main list, a boundary bug. **Overtaken:** `PINEFORGE_KERNEL_SOURCES` CMakeLists.txt:115 and `add_library` CMakeLists.txt:159 build `PineForge::kernel`, and the compat unit is in `PINEFORGE_SOURCE_LAYER_SOURCES` CMakeLists.txt:91 | `add_library` CMakeLists.txt:159 | S (F, O agree in §2.i) | @@ -501,8 +501,8 @@ inside the per-bar body (corpus 442d497, `set_pine_risk_direction(1)` / `set_pine_risk_max_position_size(2)` / `set_pine_risk_max_intraday_filled_orders(3)`), so the adapter first sees a limit on script bar 0, from `PineStrategyHost::set_pine_risk_*` -(pine_strategy_host.cpp:886) — after `prepare_native_begin` has already -projected the spec (pine_strategy_host.cpp:243) and `configure_native` has +(pine_strategy_host.cpp:884) — after `prepare_native_begin` has already +projected the spec (pine_strategy_host.cpp:241) 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 @@ -557,8 +557,8 @@ Pine run needs the source change the patch makes, and the in-process pins remain the five probes' baseline rows and the `SW` check above. **The P6 decision: native-only.** Re-verified on main `b8e7976e`: the ordering -stands (`project()` at pine_strategy_host.cpp:350, the kernel's `configure` at :359, -the `set_pine_risk_*` setters (`set_pine_risk_direction` pine_strategy_host.cpp:886), and corpus 442d497 +stands (`project()` at pine_strategy_host.cpp:348, the kernel's `configure` at :359, +the `set_pine_risk_*` setters (`set_pine_risk_direction` pine_strategy_host.cpp:884), and corpus 442d497 `corpus/validation/cap-risk-gates-allow-max-intraday-01`'s generated strategy still emits the three calls inside `on_source_bar`); `rg "spec\.risk|NativeRiskLimits" src/source include/pineforge/source` is still empty; `test_adapter_risk_relower` still @@ -851,7 +851,7 @@ so the gross open profit is 800. | **the equity re-mark** | `last.open_profit = 0; last.equity = initial_capital_ + net_profit_sum_ + range_end_pnl` | none — the kernel leaves the curve exactly as the run marked it | `RE1` (no fee): the adapter reports the curve's last point as `eq=100800 op=0`, the kernel as `eq=100800 op=800` — same equity, the open profit moved into the realized side. `RE2` (0.1 %): `eq=100758.8 op=0` against `eq=100800 op=800`; the adapter's equity is **41.2 lower**, exactly the round-trip commission of both lots (20.5 + 20.7), because the re-mark carries the NET row P&L where the run's own mark carried the GROSS open profit | retained: a report that restates the last equity point off net row P&L is TradingView's report shape. Re-marking the curve from the kernel would move a figure every bare host already reads | | **the extreme re-fold** | `max_equity_`/`min_equity_` reset to `initial_capital_`, `max_drawdown_`/`max_runup_` to 0, then `fold_equity_extreme` over the whole curve | none — the extremes stay as `update_equity_extremes` sampled them | `RE3` (a dip to 90 at bar 1, a peak of 108 at bar 3, 0.1 %): both sides agree on `maxeq=101400 mineq=99000 maxdd=2400`, and the run-up diverges — adapter `maxru=1758.8000000000029`, kernel `maxru=1800`. The re-fold is a no-op **except** where the re-marked last point differs, so the divergence is again the 41.2 round trip, modulo the binary fold order | retained: it is a consequence of the re-mark above, not a mechanism of its own; it cannot be re-lowered while the re-mark is adapter policy | | **the same-bar exit re-sort** | `sort_same_bar_exit_trades(trades_, adapter_)` — same-bar bracket exits ordered by script command sequence | none — the kernel never reorders `trades_` | not exercised by the four scenarios (it reorders CLOSED rows, not range-end rows); it is TradingView command-sequence ordering by construction (`git show ab9714be:src/source/pine_fills.cpp`, lines 664-670) | retained: script command sequence is a source-language fact | -| **the mark cadence** | three call sites, each clearing and rebuilding: the terminal sub-bar of the last source bar (`scheduler_record_range_end` pine_strategy_host.cpp:1987), after every applied execution once terminal (`record_applied_range_end`, :801), and the scheduler's terminal-source-bar hook (`PineScheduler::applied` pine_scheduler_native.cpp:688, before the bar's broker hash) | once, after the last point of the run (`run_batch` ×3, `stream_end`) | structural, not a number: the adapter's rows are re-derived at each mark so the report reflects the book as of the last event on the terminal bar, and its re-marked equity is visible to that bar's broker hash. A one-shot run-end producer cannot occupy those three points | retained: three marks on a terminal bar is a TradingView report cadence, not a generic run-end fact | +| **the 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:1985), after every applied execution once terminal (`record_applied_range_end`, :801), and the scheduler's terminal-source-bar hook (`PineScheduler::applied` pine_scheduler_native.cpp:688, before the bar's broker hash) | once, after the last point of the run (`run_batch` ×3, `stream_end`) | structural, not a number: the adapter's rows are re-derived at each mark so the report reflects the book as of the last event on the terminal bar, and its re-marked equity is visible to that bar's broker hash. A one-shot run-end producer cannot occupy those three points | retained: three marks on a terminal bar is a TradingView report cadence, not a generic run-end fact | | **the excursion projection gate** | `excursion_range_end_projection_` suppressed the owner's exit-bar path fold for the projection | none — the generic builder folds what it folds | `RE1`: identical run-up/draw-down on both halves (`ru=500 dd=0`, `ru=300 dd=0`) | retired by R5 lane H-THIN: the Pine host owns no lot excursion since, so no owner fold is left to suppress; `RE1` still reads the same magnitudes, now the kernel's | | **the tail / warm-up suppression** | `if (stream_warmup_mode_ \|\| realtime_tail_ \|\| equity_curve_.empty()) return;` | none — the kernel emits at `stream_end` whatever the run left open | structural: the kernel has no warm-up or realtime-tail notion, and `run_corpus`'s probe-tail suppression (`suppress_probe_tail`) is a harness fact | retained: a tail is a source-layer replay concept | @@ -1288,7 +1288,7 @@ engine.hpp:1667) is not. The other is a contract that the six folded fields of a booked row are never rewritten. In the tree they are not; the only writer after booking, the Pine host, rewrites `close_cause`, `exit_from_bracket`, `exit_comment` and `entry_incarnation` only; its -writes are `close_cause` pine_strategy_host.cpp:639, `exit_comment` :768, `close_cause` :782-787, `exit_from_bracket` :1288 and `entry_incarnation` :1469-1477. +writes are `close_cause` pine_strategy_host.cpp:637, `exit_comment` :768, `close_cause` :782-787, `exit_from_bracket` :1288 and `entry_incarnation` :1469-1477. But `trades_` engine.hpp:546 is a protected member any host can write, so a lane that takes the consumer-side digest has to make that contract explicit. Folding `(count, digest)` in place of the rows saves the 256-entry table but @@ -1320,8 +1320,8 @@ the bars cost 3.8 times the CPU with a round trip every ten bars script point, not after the run's teardown, so that it is one value with recording on or off (`broker_state_hash_projection` pine_strategy_host.cpp:166). `capture_script_continuation_hash` -pine_strategy_host.cpp:532 takes that snapshot at the last batch bar -(`last_batch` pine_strategy_host.cpp:613), and taking it is one full +pine_strategy_host.cpp:530 takes that snapshot at the last batch bar +(`last_batch` pine_strategy_host.cpp:611), 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 @@ -1604,9 +1604,9 @@ the rule gives 9.9 + 9.89). | E3 | **Sizing.** TV sizes from the tick-rounded signal close, 10-digit-rounded equity and fee-reserved cash, frozen at signal, with fill-time exceptions in COOF paths (`default_sizing_shape` pine_adapter.cpp:3184, the COOF `at_fill` exception pine_adapter.cpp:8395) | One "generic" percent sizing serving both would break parity or import TV rounding into the kernel; the last ulp differs on lot-stepped instruments | Two paths on purpose: `Sized` (kernel, plain arithmetic, timing as a first-class field) vs `HostSized` + the adapter resolver. Never "simplify" the adapter onto the kernel basis. What the two paths share is the conversion: since R5 lanes N11 and F7 the division by price × point value × FX and the percentage fee reserve exist once, in the kernel (`native_sized_units`), for a default AND a typed (`qty_type` cash / percent) quantity. The adapter keeps what is measured TradingView: the money basis (signal-time equity, TradingView's margin equity at a typed fill, the hypothetical Flatten's balance for a typed reversal), the freeze, the fill-time exceptions and both lot floors (`tests/test_adapter_sizing_relower.cpp` cases 8, 9, 11: kernel 0.0392 against the percent floor's 0.0391 on a 0.0001 lot), and a staged FX series keeps `Sized` wherever the acceptance coordinate converts at the source's rate (case 12) | F O S | | E4 | **Calc on fill.** Kernel suffix eligibility is continuous along the segment; TV refills only at O/H/L/C waypoints -- on a magnified path at each intrabar's open, twice, and its two extremes, never its close (R5 lane MAG-INTRABAR) (`PineScheduler::recalculate` pine_scheduler_native.cpp:727, `coof_next_waypoint` pine_adapter.cpp:6949) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:6507 and `drain_applied_notifications` (native_execution_consumer.cpp:7190); new behaviour only behind new opt-ins | F | | E5 | **Open-bar lookahead.** The adapter reads the full bar in `on_bar_open` (margin path scheduling uses H/L, pine_adapter.cpp:22252) | The complete bar cannot be masked globally | Opt-in `OpenOnly` view (A.4); loud doc warning in L0 | F | -| E6 | **Double equity recording.** The source host already records per bar (history: only its cadence remains, `scheduler_mark_report_point` pine_strategy_host.cpp:2791) | Kernel recording would duplicate points and shift drawdown | Report policy / ownership (A.1); the adapter declares `KernelRecordedAtHostMarks` (R1): one recorder, at the adapter's own marks | F O | +| E6 | **Double equity recording.** The source host already records per bar (history: only its cadence remains, `scheduler_mark_report_point` pine_strategy_host.cpp:2789) | Kernel recording would duplicate points and shift drawdown | Report policy / ownership (A.1); the adapter declares `KernelRecordedAtHostMarks` (R1): one recorder, at the adapter's own marks | F O | | E7 | **Price grid.** TV does not merely quantize: it tests triggers against a quantized bar while booking the fill on the *unrounded* level, so the adapter shifts the threshold (`source_trigger_threshold` pine_adapter.cpp:384) and books elsewhere | A kernel grid that quantizes both sides is correct and not TV; moving rounding into the kernel would change both native results and the adapter | `NativePriceGrid::None` for the adapter permanently; never "unify". Before L8: a byte-level experiment that the default price spelling on decimal ticks (one-ULP representations) does not move (S) | O S | -| E8 | **HTF.** The evaluator machinery carries TV publication semantics (lookahead, boundary deferral, early close, `syminfo.type` branching engine_security.cpp:67-73). The feed store is TV-calibrated too: "W" / "M" from native daily bars, native stamps as the period partition (`set_native_security_feed` `sha256:eca3dbea0baa461982ba89f13d0a5c43277854069fb6eb2dd2f5c5471313204d` engine_aux_security.cpp:51-83, *fresh*). Two aggregators exist: script buckets use `native_calendar`, `request.security` uses `TimeframeAggregator`, whose completion rules are TV-pinned (timeframe.hpp:288-330) | Reuse risks changing the *order* of the Pine scheduler's pumps; a bare host inherits TV partition rules; native HTF may differ from Pine HTF on early-close sessions | Registration-gated pump; no change to `scheduler_feed_security_input` pine_strategy_host.cpp:1816; full corpus diff; supplied-bar provenance preserved, never silently replaced by chart aggregation (S); differences on early-close sessions are recorded, not "fixed" (F). The Pine scheduler keeps one more `TimeframeAggregator` pass, before the run (`PineScheduler::run_begin`, audit M26): the script-bar count it yields (`terminal_source_bar`, the probe-tail suppression) and the per-input completion flags it hands `on_native_input` are needed before the first input arrives, while the kernel seals script intervals only as inputs arrive and offers no pre-run interval query (`NativeExecutionConsumer::script_interval_at` is consumer-private). Retained by R5 lane F7: replacing it needs that kernel capability | F O S | +| E8 | **HTF.** The evaluator machinery carries TV publication semantics (lookahead, boundary deferral, early close, `syminfo.type` branching engine_security.cpp:67-73). The feed store is TV-calibrated too: "W" / "M" from native daily bars, native stamps as the period partition (`set_native_security_feed` `sha256:eca3dbea0baa461982ba89f13d0a5c43277854069fb6eb2dd2f5c5471313204d` engine_aux_security.cpp:51-83, *fresh*). Two aggregators exist: script buckets use `native_calendar`, `request.security` uses `TimeframeAggregator`, whose completion rules are TV-pinned (timeframe.hpp:288-330) | Reuse risks changing the *order* of the Pine scheduler's pumps; a bare host inherits TV partition rules; native HTF may differ from Pine HTF on early-close sessions | Registration-gated pump; no change to `scheduler_feed_security_input` pine_strategy_host.cpp:1814; full corpus diff; supplied-bar provenance preserved, never silently replaced by chart aggregation (S); differences on early-close sessions are recorded, not "fixed" (F). The Pine scheduler keeps one more `TimeframeAggregator` pass, before the run (`PineScheduler::run_begin`, audit M26): the script-bar count it yields (`terminal_source_bar`, the probe-tail suppression) and the per-input completion flags it hands `on_native_input` are needed before the first input arrives, while the kernel seals script intervals only as inputs arrive and offers no pre-run interval query (`NativeExecutionConsumer::script_interval_at` is consumer-private). Retained by R5 lane F7: replacing it needs that kernel capability | F O S | | E9 | **TA numerics are TV-calibrated** (1e-10 band in DMI / MFI / percentrank; conditional-call extremum ring `ExtremeRing` ta.hpp:134) | Non-Pine users may expect IEEE semantics; changing them breaks parity | Rename only (§2.ii b). For an every-bar caller the ring equals a positional window (the ring comment `sha256:fd1f92206558863ad8df33fd3ef3b8cc8b9bd6678a25fc98ca866168c1a81aca` ta.hpp:87-88). Document. → U6 | F O S | | E10 | **Dead-but-hashed lot flags** (§2.ii j) | Deleting them changes `broker_state_hash` | Separate, explicitly non-neutral lane (L12) | F O | | E11 | **OCA and ownership.** Pine's default-sized OCA and `from_entry` reservation rules are source policies | Source IDs, callsite tokens or Pine labels leaking into `WorkingRequestCore` | Opaque keys and receipts only (R5-4) | S | @@ -1623,7 +1623,7 @@ the rule gives 9.9 + 9.89). | P5 | **Test asymmetry:** 430 adapter test files vs 14 native when this plan was written (on this tree 543 `tests/*.cpp` name `source::PineStrategyHost`, 110 `NativeStrategyHost`) | native-only tests in every lane (§3.1 b) | O | | P6 | **Stream vs batch.** The stream refuses monthly input, mixed modes, in-session gaps, security feeds and source setters, and tick input under an FX curve (any non-empty curve until audit lane N6) (`native-engine.md`, "Stream limitations (current)") | L5 and L6 must state which refusals are generic and which are deliberate source limits | S | | P7 | **C ABI variant growth** | §2.iii hardening rules | S | -| P8 | **Unverified in the inputs:** whether `execution::LifecycleEffects` (execution.hpp:136-139) is ever non-empty from the Pine path, else `PlacementSnapshot::legs` (pine_adapter.hpp:364) is the only live lifecycle (O); the `strategy.risk.allow_entry_in` setter chain beyond `set_pine_risk_direction` pine_strategy_host.cpp:886 → `set_risk_direction` pine_adapter.cpp:26465 (O); what the ABI receipts pin (F); the probe corpus itself lives outside this tree (F) | verify inside the lane that touches each | F O | +| P8 | **Unverified in the inputs:** whether `execution::LifecycleEffects` (execution.hpp:136-139) is ever non-empty from the Pine path, else `PlacementSnapshot::legs` (pine_adapter.hpp:364) is the only live lifecycle (O); the `strategy.risk.allow_entry_in` setter chain beyond `set_pine_risk_direction` pine_strategy_host.cpp:884 → `set_risk_direction` pine_adapter.cpp:26465 (O); what the ABI receipts pin (F); the probe corpus itself lives outside this tree (F) | verify inside the lane that touches each | F O | ### 4.3 Open design questions (supervisor level) diff --git a/docs/pages/abi-stability.md b/docs/pages/abi-stability.md index 4ddc5ef5d..fb8321420 100644 --- a/docs/pages/abi-stability.md +++ b/docs/pages/abi-stability.md @@ -70,7 +70,7 @@ Three layers: The counts below include recorded outputs and describe the released 1.5.0 API; recorded outputs are not part of 1.4.0. A built strategy `.so` exposes 96 compiled-strategy `PF_API` declarations -(80 runtime implementations plus sixteen generated exports) plus 43 native-host +(81 runtime implementations plus sixteen generated exports) plus 43 native-host declarations: 139 `PF_API` exports in total; a script that declares `use_bar_magnifier = true` also exports `strategy_declares_bar_magnifier`, and a module that records outputs its three outputs exports. `nm -gU` also reports libc++'s @@ -124,7 +124,7 @@ not the complete inventory: | `strategy_signal_safety_receipt` | @ref pf_outputs (a recording module's own) | Sixteen per-strategy exports include the five create/run/free lifecycle -functions. The remaining 80 runtime implementations, including the +functions. The remaining 81 runtime implementations, including the closed-trade incarnation accessor, are force-linked into each strategy library, so consumers resolve the same complete ABI from the strategy `.so`. All additions remain covered by the minor-version append-only guarantee. diff --git a/docs/pages/index.md b/docs/pages/index.md index 5df22eeef..95744c948 100644 --- a/docs/pages/index.md +++ b/docs/pages/index.md @@ -139,7 +139,7 @@ The counts below include recorded outputs and describe the released 1.5.0 API. The public C surface is **145 `PF_API` declarations** across the five installed headers that declare any: -- `` — **94**: 74 of the 80 runtime implementations (the other six are declared in `execution_observer.h` and `selected_window.h`) plus twenty +- `` — **95**: 75 of the 81 runtime implementations (the other six are declared in `execution_observer.h` and `selected_window.h`) plus twenty per-strategy generated exports (`strategy_declares_bar_magnifier` only in a script that declares `use_bar_magnifier = true`, the three recorded-outputs exports only in a module that records outputs). This is what a compiled strategy `.so` exports and what a harness calls. Older modules lack the six @@ -150,7 +150,7 @@ The public C surface is **145 `PF_API` declarations** across the five installed execute, read the book, read the lots. Additive: no symbol, struct or behaviour of the first set changes. - `` and `` — - three each: the six of the 80 runtime implementations that `pineforge.h` + three each: the six of the 81 runtime implementations that `pineforge.h` does not declare (each header's version reader, the execution observer's setter and observation reader, the selected window's setter and counts reader). diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 21d19c1fb..4561dae2a 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -3966,9 +3966,12 @@ hands the runtime a callback table and gets the same kernel back. `` (included by `pineforge.h`) is that surface — 43 additive `PF_API` symbols implemented in `src/native_c_host.cpp` by `CCallbackHost`, a `final NativeStrategyHost` that forwards each existing -virtual to the table. No epoch bump or reordering occurs: the borrowed -symbol-feed setter is an additive compiled-strategy runtime symbol, and -`scripts/check_c_abi_runtime.py` pins it alongside the established inventory. +virtual to the table. The established C-host surface carries the statement: +“No new virtual, no epoch bump, and nothing about the established C ABI moves: +the compiled-strategy runtime symbols and their counts are untouched.” The +borrowed symbol-feed setter is an additive compiled-strategy runtime symbol, +and `scripts/check_c_abi_runtime.py` pins it alongside the established +inventory. ```c #include diff --git a/scripts/check_native_cpp_versions.py b/scripts/check_native_cpp_versions.py index e166ec219..0fe929b49 100644 --- a/scripts/check_native_cpp_versions.py +++ b/scripts/check_native_cpp_versions.py @@ -171,6 +171,7 @@ 'src/source/pine_security_eval.cpp': ( 'feed.bars = held.bars;', 'feed.close_ms = held.close_ms;', + '} else if (!held.bars.empty() || !held.close_ms.empty()) {', 'site.close_ms = 0;', 'site.close_ms = close_ms;', 'while (static_cast(site.delivered) < feed.bars.size) {', @@ -179,9 +180,10 @@ 'run_foreign_site_bar(site, feed.bars.data[at], feed.close_ms[at]);', 'feed.bars.assign(bars, bars + n);', 'feed.close_ms.assign(close_ms, close_ms + n);', - 'if (n < 0 || static_cast(n) != data.bars.size || (n > 0 && values == nullptr))', - '+ " values for " + std::to_string(data.bars.size) + " bars");', - 'f.u(native_instrument_feed_view(feed, nullptr).bars.size);', + 'feed.bars.clear();', + 'feed.close_ms.clear();', + '? symbol_feed_views_[static_cast(found)].size : data.bars.size;', + ': data.bars.size;', ), 'src/source/pine_strategy_host.cpp': ( 'if (args.n > 0 && args.bars == nullptr) {', diff --git a/src/source/pine_security_eval.cpp b/src/source/pine_security_eval.cpp index 1d88c1a6c..e3d667458 100644 --- a/src/source/pine_security_eval.cpp +++ b/src/source/pine_security_eval.cpp @@ -1430,9 +1430,10 @@ bool source::PineStrategyHost::set_symbol_feed_column(const std::string& key, if (const std::string why = restore_symbol_feeds_reason(); !why.empty()) return refuse("restore_failed", why); NativeInstrumentFeed& feed = symbol_feeds_[static_cast(found)]; + const auto data = native_instrument_feed_view(feed, nullptr); const std::size_t feed_count = static_cast(found) < symbol_feed_views_.size() && symbol_feed_views_[static_cast(found)].present - ? symbol_feed_views_[static_cast(found)].size : feed.bars.size(); + ? symbol_feed_views_[static_cast(found)].size : data.bars.size; if (n < 0 || static_cast(n) != feed_count || (n > 0 && values == nullptr)) return refuse("column_length", "column '" + name + "' has " + std::to_string(n) + " values for " + std::to_string(feed_count) + " bars"); @@ -1555,10 +1556,11 @@ void source::PineStrategyHost::refresh_symbol_data_digest() noexcept { for (const auto& feed : symbol_feeds_) { f.s(feed.instrument); f.s(feed.tf); + const auto data = native_instrument_feed_view(feed, nullptr); const std::size_t count = (&feed - symbol_feeds_.data()) < symbol_feed_views_.size() && symbol_feed_views_[static_cast(&feed - symbol_feeds_.data())].present ? symbol_feed_views_[static_cast(&feed - symbol_feeds_.data())].size - : feed.bars.size(); + : data.bars.size; f.u(count); f.u(feed.columns.size()); for (const auto& column : feed.columns) f.s(column.name); From e194c46b23accf69d9edac0d3c9ad5a3b8e5b507 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Mon, 19 Oct 2026 00:51:16 +0800 Subject: [PATCH 13/35] feat(cr150-l19h): retire settled adapter history --- include/pineforge/source/pine_adapter.hpp | 49 ++++++++++++----------- src/source/pine_adapter.cpp | 4 +- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/include/pineforge/source/pine_adapter.hpp b/include/pineforge/source/pine_adapter.hpp index f51067868..2a43ff9f6 100644 --- a/include/pineforge/source/pine_adapter.hpp +++ b/include/pineforge/source/pine_adapter.hpp @@ -661,7 +661,7 @@ class PlacementTable { erased_rows_ = 0; erased_digest_ = kFoldBasis; sequence_index_.clear(); - unfolded_.clear(); + std::vector().swap(unfolded_); erased_minima_.clear(); erased_foreign_ids_.clear(); #if PINEFORGE_PLACEMENT_AUDIT @@ -774,6 +774,7 @@ class PlacementTable { place(incarnation, std::move(node)); } catch (...) { unfolded_.pop_back(); + if (unfolded_.empty()) std::vector().swap(unfolded_); throw; } ++size_; @@ -786,7 +787,7 @@ class PlacementTable { void replace(std::uint64_t incarnation, PlacementSnapshot&& snapshot) { at(incarnation) = std::move(snapshot); sequence_index_.clear(); - unfolded_.clear(); + std::vector().swap(unfolded_); for (const auto& row : *this) unfolded_.push_back(row.first); } @@ -796,6 +797,14 @@ class PlacementTable { bool erase(std::uint64_t incarnation) { std::unique_ptr node = take(incarnation); if (!node) return false; + // A row that leaves before the next command-sequence lookup no longer + // belongs in the pending index fold. Release the allocation when the + // pending set becomes empty; retained rows stay the only live ids. + const auto pending_end = std::remove(unfolded_.begin(), unfolded_.end(), incarnation); + if (pending_end != unfolded_.end()) { + unfolded_.erase(pending_end, unfolded_.end()); + if (unfolded_.empty()) std::vector().swap(unfolded_); + } remember_erased(*node); --size_; ++erased_rows_; @@ -1052,8 +1061,9 @@ class BracketRoster { }; public: - // Every member in roster order: the settled prefix, then the working tail - // with each parked run in its place. + // Every materialized member in roster order after the settled prefix, + // with each parked run in its place. The settled prefix is represented by + // its count and digest rather than by historical handle bytes. class const_iterator { public: using iterator_category = std::forward_iterator_tag; @@ -1075,8 +1085,7 @@ class BracketRoster { return before; } bool operator==(const const_iterator& other) const noexcept { - return owner_ == other.owner_ && settled_ == other.settled_ - && offset_ == other.offset_ && hot_ == other.hot_ && run_ == other.run_ + return owner_ == other.owner_ && hot_ == other.hot_ && run_ == other.run_ && in_run_ == other.in_run_ && run_offset_ == other.run_offset_; } bool operator!=(const const_iterator& other) const noexcept { return !(*this == other); } @@ -1085,8 +1094,6 @@ class BracketRoster { friend class BracketRoster; const_iterator(const BracketRoster* owner, bool at_end) : owner_(owner) { if (at_end) { - settled_ = owner->settled_count_; - offset_ = owner->settled_.size(); hot_ = owner->hot_.size(); run_ = owner->parked_.size(); return; @@ -1097,10 +1104,7 @@ class BracketRoster { return run_ < owner_->parked_.size() && owner_->parked_[run_].at == hot_; } void load() { - if (settled_ < owner_->settled_count_) { - previous_ = get(owner_->settled_, offset_, previous_); - current_ = native_order::RequestHandle{owner_->run_, previous_}; - } else if (in_parked()) { + if (in_parked()) { run_previous_ = get(owner_->parked_[run_].bytes, run_offset_, run_previous_); current_ = native_order::RequestHandle{owner_->run_, run_previous_}; } else if (hot_ < owner_->hot_.size()) { @@ -1108,9 +1112,7 @@ class BracketRoster { } } void advance() { - if (settled_ < owner_->settled_count_) { - ++settled_; - } else if (in_parked()) { + if (in_parked()) { if (++in_run_ == owner_->parked_[run_].count) { ++run_; in_run_ = 0; @@ -1124,13 +1126,10 @@ class BracketRoster { } const BracketRoster* owner_ = nullptr; - std::size_t settled_ = 0; - std::size_t offset_ = 0; std::size_t hot_ = 0; std::size_t run_ = 0; std::size_t in_run_ = 0; std::size_t run_offset_ = 0; - std::uint64_t previous_ = 0; std::uint64_t run_previous_ = 0; native_order::RequestHandle current_{}; }; @@ -1140,10 +1139,11 @@ class BracketRoster { hot_.push_back(handle); ++count; } - // Every member, materialized (the bracket cancel's roster). + // Every materialized member after the settled prefix (the bracket + // cancel's roster). `size()` still includes the settled prefix count. std::vector members() const { std::vector out; - out.reserve(size()); + out.reserve(size() - settled_count_); for (const auto& handle : *this) out.push_back(handle); return out; } @@ -1181,7 +1181,7 @@ class BracketRoster { if (moved == hot_.size()) break; const auto& handle = hot_[moved]; if (!erased(handle.incarnation) || !claims(handle)) break; - put(settled_, settled_last_, handle.incarnation); + settled_last_ = handle.incarnation; ++settled_count_; settled_digest_ = fold(settled_digest_, handle.incarnation); uncount(handle.incarnation); @@ -1351,16 +1351,17 @@ class BracketRoster { std::uint64_t previous = 0; for (std::size_t index = 0; index < run.count; ++index) { previous = get(run.bytes, offset, previous); - put(settled_, settled_last_, previous); + settled_last_ = previous; settled_digest_ = fold(settled_digest_, previous); } settled_count_ += run.count; parked_count_ -= run.count; } - // The settled prefix: incarnations of run_, as zigzag varint deltas. + // The settled prefix is represented by its last incarnation, count and + // running digest. Its historical varint bytes are intentionally dropped; + // walks begin at the parked runs and working tail below it. native_order::RunIdentity run_{}; - std::vector settled_; std::uint64_t settled_last_ = 0; std::size_t settled_count_ = 0; std::uint64_t settled_digest_ = kDigestBasis; diff --git a/src/source/pine_adapter.cpp b/src/source/pine_adapter.cpp index af99883b2..8d2c224b8 100644 --- a/src/source/pine_adapter.cpp +++ b/src/source/pine_adapter.cpp @@ -902,7 +902,9 @@ std::uint64_t PlacementTable::command_sequence_for(const std::string& source_id, for (const auto incarnation : unfolded_) { if (const PlacementSnapshot* row = locate(incarnation)) fold_sequence(sequence_index_, *row); } - unfolded_.clear(); + // The pending ids are folded now; release their storage so an erased + // history cannot leave a capacity-sized allocation behind. + std::vector().swap(unfolded_); constexpr auto absent = std::numeric_limits::max(); bool known = false; std::uint64_t any = absent; From ad7e7baaefcd3c563ab91f4f698c2048d4f58afd Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Mon, 19 Oct 2026 01:22:22 +0800 Subject: [PATCH 14/35] test(cr150): correct admission rejection reader inputs for fix round 1 --- tests/test_admission_journal_window.cpp | 26 ++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/test_admission_journal_window.cpp b/tests/test_admission_journal_window.cpp index 7194d5fdc..f63fa8d57 100644 --- a/tests/test_admission_journal_window.cpp +++ b/tests/test_admission_journal_window.cpp @@ -5,10 +5,13 @@ #include #include +#include #include #include #include #include +#include +#include #include namespace { @@ -72,8 +75,23 @@ class RejectionHost final : public source::PineStrategyHost { set_syminfo_mintick(0.25); } void on_source_bar(const Bar&) override { - if (bar_index_ == 0 || bar_index_ == 2) + if (bar_index_ == 0 || bar_index_ == 2) { strategy_entry("unaffordable", true, kNa, kNa, 100000.0); + // The native route refuses this quantity before submit() appends + // an Admitted observation. Exercise the rejection reader with an + // explicit Journal API input, independent of that producer path. + auto& journal = market_admission_journal(); + auto observation = std::make_shared(); + observation->command = journal.next_sequence(); + observation->bar = bar_index_; + observation->id = "reader-rejection"; + observation->requested_quantity = 100000.0; + admission::CommandEvent event; + event.observation = std::move(observation); + event.outcome = admission::Outcome::RejectedAffordability; + journal.append(std::move(event)); + CHECK(compat::pine::last_rejected_command_bar(journal) == bar_index_); + } } }; @@ -112,6 +130,7 @@ void recording_and_prefix_identity() { CHECK(prefix.market_admission_journal().events().empty()); CHECK(off.broker_state_hash() == on.broker_state_hash()); CHECK(off.trade_count() == on.trade_count()); + const auto before_report = on.broker_state_hash(); ReportC whole{}; ReportC part{}; on.fill_report(&whole); @@ -121,8 +140,13 @@ void recording_and_prefix_identity() { if (whole.broker_state_hash_len == 64 && part.broker_state_hash_len == 32) { for (int i = 0; i < 32; ++i) CHECK(part.broker_state_hash[i] == whole.broker_state_hash[i]); + if (whole.broker_state_hash[63] != on.broker_state_hash()) + std::fprintf(stderr, "hash boundary: recorded=%016" PRIx64 + " scalar_after_run=%016" PRIx64 " scalar_after_report=%016" PRIx64 "\n", + whole.broker_state_hash[63], before_report, on.broker_state_hash()); CHECK(whole.broker_state_hash[63] == on.broker_state_hash()); } + CHECK(on.broker_state_hash() == before_report); const auto digest = on.broker_state_hash(); CHECK(on.broker_state_hash() == digest); BacktestEngine::free_report(&whole); From 06b7729cba0c6ceb6a137c7d8125f660e05cc5cf Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Mon, 19 Oct 2026 02:06:43 +0800 Subject: [PATCH 15/35] fix(cr150): refresh merged doc anchor --- 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 f474ae7b0..205db720d 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -502,7 +502,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:895) — after `prepare_native_begin` has already -projected the spec (pine_strategy_host.cpp:243) 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 From 51630c19c751ecd0c5c012bcc992fab1ea28058d Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Mon, 19 Oct 2026 02:30:13 +0800 Subject: [PATCH 16/35] fix(cr150-l19h): preserve settled cancel outcomes --- include/pineforge/source/pine_adapter.hpp | 3 +++ src/source/pine_adapter.cpp | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/include/pineforge/source/pine_adapter.hpp b/include/pineforge/source/pine_adapter.hpp index 2a43ff9f6..4ad821665 100644 --- a/include/pineforge/source/pine_adapter.hpp +++ b/include/pineforge/source/pine_adapter.hpp @@ -1255,6 +1255,9 @@ class BracketRoster { // its running digest -- with every parked run's place, count and digest // folded after it while any is parked (R5 lane V19-D). std::size_t settled_count() const noexcept { return settled_count_; } + // The run identity is enough to reproduce a settled member's non-working + // cancel outcome after its incarnation bytes have been dropped. + native_order::RunIdentity run_identity() const noexcept { return run_; } std::uint64_t settled_digest() const noexcept { std::uint64_t digest = settled_digest_; for (const auto& run : parked_) { diff --git a/src/source/pine_adapter.cpp b/src/source/pine_adapter.cpp index 777b5930f..5b65fa137 100644 --- a/src/source/pine_adapter.cpp +++ b/src/source/pine_adapter.cpp @@ -14492,13 +14492,23 @@ void PineExecutionAdapter::exit_cancel_bracket(const SourceId& exit_id, // Cancellation can synchronously change source state. Copy the family // roster and release the map iterator before issuing any host operation. std::vector handles; + std::size_t settled = 0; + native_order::RunIdentity settled_run{}; bool found_family = false; if (const auto found = bracket_families_.find(key); found != bracket_families_.end()) { handles = found->second.members(); + settled = found->second.settled_count(); + settled_run = found->second.run_identity(); bracket_families_.erase(found); found_family = true; } if (!found_family) return; + // Settled members no longer retain incarnation bytes. They have already + // left the working book, so a same-run sentinel is classified as + // NotWorking and emits the same compact cancel event while advancing the + // command ordinal once per settled member. + for (std::size_t index = 0; index < settled; ++index) + (void)require_host().cancel(native_order::RequestHandle{settled_run, 0}); for (const auto& handle : handles) { const auto result = require_host().cancel(handle); if (result.status == native_order::CancelStatus::Cancelled) retire(handle); From 3b930bf3f0e9a31764dfed1bd0996dee99726c45 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Mon, 19 Oct 2026 02:39:00 +0800 Subject: [PATCH 17/35] docs(cr150-l19h): refresh shifted source anchors --- docs/adr/0001-kernel-adapter-boundary.md | 18 +++--- docs/design/native-feature-parity.md | 82 ++++++++++++------------ docs/native-refactor-progress.md | 4 +- docs/pages/native-engine.md | 2 +- 4 files changed, 53 insertions(+), 53 deletions(-) diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index c7e2bc765..94d4450c5 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -229,7 +229,7 @@ kernel and own these quirks, each at its site: `NativeCloseExecution::AfterCalculation` in the projected spec (`pine_adapter.cpp:2817-2818`); calc cadence and language publication are `PineScheduler`'s (`pine_scheduler.hpp:21`). - **Batching and open-order priority.** Same-bar command batching and its deferred queues - (`PendingSameBarCommand` pine_adapter.hpp:1881, `pending_bracket_legs_` pine_adapter.hpp:2515-2533); the retained parent-before-child ordering of live handles — an + (`PendingSameBarCommand` pine_adapter.hpp:1884, `pending_bracket_legs_` pine_adapter.hpp:2518-2536); the retained parent-before-child ordering of live handles — an exactly-shaped entry and its `from_entry` exit, re-created after a named cancel, on a flat book under `process_orders_on_close` (`src/compat/pine/order_priority.cpp:13-59`, applied by `update_l4c_priority` `pine_adapter.cpp:1364-1434`). Kernel priority is acceptance/incarnation order. @@ -239,7 +239,7 @@ kernel and own these quirks, each at its site: - **Trail and tick conventions.** The half-tick arm threshold measured against tick-quantized extremes while the raw running best is retained (`has_trail_request` `pine_adapter.cpp:12502-12507`); the half-tick trigger threshold (`source_trigger_threshold` pine_adapter.cpp:384) and raw-vs-booked fill spelling, behind the - adapter's terms seam (`resolve_terms` `pine_adapter.cpp:14846`). + adapter's terms seam (`resolve_terms` `pine_adapter.cpp:14858`). - **Money arithmetic.** Ten-significant-digit half-up money (`source_money_round` `pine_adapter.cpp:558-564`, twin `tv_money_round` `pine_policy_support.hpp:9-15`). - **Quantity dust.** After every applied execution the Pine host erases any lot of at most @@ -256,12 +256,12 @@ kernel and own these quirks, each at its site: restore's second floor) fail tape-pinned rows (`test_pine_dust_sweep_paired`, `test_tv_money_band_l4b`); `quantity_tolerance` is not their rule (its row below). - **Close reservations.** `strategy.close` callsite batching, two-call provenance and the entry-id - ledger (`close_logical_units_` `pine_adapter.hpp:2580-2610`); the POOC reservation-growth population predicate + ledger (`close_logical_units_` `pine_adapter.hpp:2583-2613`); the POOC reservation-growth population predicate (`select_reservation_growth_sources` src/compat/pine/reservation_expansion.cpp:9). - **Margin.** The kernel owns the margin *mechanism* — the level solve, the check points, the kernel request, its re-pricing, the receipt — and the adapter answers its three policy hooks - with TradingView's: `margin_check_allowed` (`pine_adapter.cpp:17832`) for the scheduling, - `resolve_margin_requirement` (`pine_adapter.cpp:17901`) for the ten-significant-digit money, + with TradingView's: `margin_check_allowed` (`pine_adapter.cpp:17844`) for the scheduling, + `resolve_margin_requirement` (`pine_adapter.cpp:17913`) for the ten-significant-digit money, and `resolve_margin_call_units` for the lot-floored 4x restore. That hook answers every call on purpose (R5 lane F7): TradingView floors the restore onto the lot grid before the multiple, so on a gridded tape the kernel's own `ShortfallMultiple 4` books 5 lots where TradingView @@ -272,7 +272,7 @@ kernel and own these quirks, each at its site: margin since F7), while the roll point measures at the remaining path's adverse mark (MG-FX: 0.4116 @ 97 there against TradingView's 0.3996 @ 100). What has no kernel check point at all stays adapter-side: the `process_orders_on_close` chronology exception - (`non_pooc_commissioned_short` `pine_adapter.cpp:24167`) and + (`non_pooc_commissioned_short` `pine_adapter.cpp:24179`) and the 1x-long money call — plus the TV admission scopes (`explicit_pair_scope` `src/compat/pine/market_admission.cpp:34-47`) and the review fold they feed (`awaits_pair_review` `:68-73`, `fold_admission_history` `:83-140`). @@ -394,7 +394,7 @@ citations, 264 are in `src/source/` and its headers; 6 sit in kernel files the lines 73-180 earlier drafts cited are and were live code (`ClosedLotExcursionFacts`, `PyramidEntry`, `Trade`, each with its own doc). The arithmetic is adapter-side (`source_money_round` `pine_adapter.cpp:558-576`). `strategy.close` batching and the entry-id ledger - are **gone** from the kernel — the ledger is adapter state (`close_logical_units_` `pine_adapter.hpp:2580-2610`) and + are **gone** from the kernel — the ledger is adapter state (`close_logical_units_` `pine_adapter.hpp:2583-2613`) and only names survive in comments. The TradingView margin-call toggle remains source-owned as `source::PineStrategyHost::set_margin_call_enabled` (`pine_strategy_host.hpp:511`); what enables the generic model is the presence of `NativeRunSpec::margin`. So is the string-sentinel decoding of adapter-written comments: @@ -710,8 +710,8 @@ Every line number in this section names its symbol on this tree, which | `NativeRunSpec` field | ruling | what the adapter runs instead | measurement and ruling of record | executed native consumers | |---|---|---|---|---| | `price_grid`, `grid_rounding` | **native-only** | TradingView's per-order-kind tick rules on top of `None`: `source_trigger_threshold` (`pine_adapter.cpp:384`), `source_level_on_price_grid` (`:367`), `nearest_tick` (`:274`) / `source_bar_fill_tick` (`:295`) / `directional_tick` (`:327`), behind the terms seam | R5-3 and design risk E7 ruled `None` for the adapter before the lane ran; lanes R7 and N13 measured the alternative anyway (raw levels submitted, `QuantizeFillsAndTriggers` with `HalfUp` declared). L8b closed the first blocker (no run aborts). The second has no remedy on either side: TradingView quantizes per order kind (stop and limit legs and a trail's activation on the quantized bar; the trail stop, the running best, stop-limit entries and the `calc_on_order_fills` cursors raw), the grid is one rule for the run, and the old N13 trial moved 30 pinned checks in 4 units; this is historical evidence, not a current-head result; a per-kind mask would spell that inconsistency into the kernel. The corpus cannot arbitrate (every probe runs a 0.01 tick on an on-grid feed; 5 of 312 differ in an engine-only column). Permanent witness: `tests/test_adapter_grid_relower.cpp`, whose section 5 pins the trail stop the grid fires a bar early. Design row PG and §3.6 | `examples/native/native_price_grid_strategy.cpp`, `examples/native/native_price_grid_c.c`, the private test `test_native_price_grid` | -| `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:17483`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:20858`), `chart_day_key` (`pine_adapter.cpp:17380`), `compat::pine::IntradayCap` with `IntradayOrderBudget` (337 lines) | Structural first: Pine's risk calls are per-bar statements, so a limit reaches the adapter on script bar 0, after `project()` (`pine_strategy_host.cpp:350`) and the kernel's `configure` (`pine_strategy_host.cpp:359`) have fixed and digested the spec. In substance (lane N12, `tests/test_adapter_risk_relower.cpp`, 62 checks over nine paired scenarios): the drawdown latch samples at the close only and still admits a reversal; the loss-day streak counts trades, not days; the intraday loss closes at the path's adverse extreme, refuses every placement and withdraws the book; the fill cap charges slots, transfers quota and closes at the bar's better extreme on the chart timezone's day. With the kernel seeded on the corpus, 3 of 4 cap probes diverge (3840 of 3916, 312 of 604, 2370 of 2384 rows; re-derived by `scripts/check_seeded_risk_experiment.sh` since R5 lane H-MEASURE). `strategy.risk.allow_entry_in` is the one rule that is the kernel's already (`allowed_open_directions`). Design §3.6 | `examples/native/native_risk_limits_strategy.cpp`, `examples/native/native_trail_risk_strategy.cpp`, `tests/test_native_risk_limits.cpp`, `tests/test_native_c_api.c` | -| `max_abs_units` | **adapter-policy** | `strategy.risk.max_position_size` as a gate on the LIVE book before the fill (`max_position_size` `pine_adapter.cpp:16492-16493`): an entry is refused once the book already holds the limit | design row MG2 (R5-1): the resulting-book cap is the generic one. Measured by N12's scenario `PS` in `tests/test_adapter_risk_relower.cpp`: two-unit entries against a limit of 3 leave the adapter at 4 and the kernel cap at 2 | `tests/test_native_resting_matching_contract.cpp`, `tests/test_native_run_spec.cpp` | +| `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:17495`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:20870`), `chart_day_key` (`pine_adapter.cpp:17392`), `compat::pine::IntradayCap` with `IntradayOrderBudget` (337 lines) | Structural first: Pine's risk calls are per-bar statements, so a limit reaches the adapter on script bar 0, after `project()` (`pine_strategy_host.cpp:350`) and the kernel's `configure` (`pine_strategy_host.cpp:359`) have fixed and digested the spec. In substance (lane N12, `tests/test_adapter_risk_relower.cpp`, 62 checks over nine paired scenarios): the drawdown latch samples at the close only and still admits a reversal; the loss-day streak counts trades, not days; the intraday loss closes at the path's adverse extreme, refuses every placement and withdraws the book; the fill cap charges slots, transfers quota and closes at the bar's better extreme on the chart timezone's day. With the kernel seeded on the corpus, 3 of 4 cap probes diverge (3840 of 3916, 312 of 604, 2370 of 2384 rows; re-derived by `scripts/check_seeded_risk_experiment.sh` since R5 lane H-MEASURE). `strategy.risk.allow_entry_in` is the one rule that is the kernel's already (`allowed_open_directions`). Design §3.6 | `examples/native/native_risk_limits_strategy.cpp`, `examples/native/native_trail_risk_strategy.cpp`, `tests/test_native_risk_limits.cpp`, `tests/test_native_c_api.c` | +| `max_abs_units` | **adapter-policy** | `strategy.risk.max_position_size` as a gate on the LIVE book before the fill (`max_position_size` `pine_adapter.cpp:16504-16505`): an entry is refused once the book already holds the limit | design row MG2 (R5-1): the resulting-book cap is the generic one. Measured by N12's scenario `PS` in `tests/test_adapter_risk_relower.cpp`: two-unit entries against a limit of 3 leave the adapter at 4 and the kernel cap at 2 | `tests/test_native_resting_matching_contract.cpp`, `tests/test_native_run_spec.cpp` | | `max_open_lots` | **adapter-policy** | Pine pyramiding is a per-cycle entry count in the adapter's command policy; a resting source entry must not consume a physical-lot cap before it fills, so `project()` leaves the cap unset (`max_open_lots` `pine_adapter.cpp:2646-2648`) | design row MG3 (R5-1); the contract comment in `project()`. Measured against TradingView by R5 lane H-MEASURE (the private test `test_pyramiding_count_differential`, 15 scenarios on three `lab tv` tapes): TradingView checks an entry once, at its first eligible point, against the trades then open, and does not check a resting entry again at its fill; the kernel's cap books 14 of the 15 as TradingView does, the adapter's per-cycle count 13 (it keeps a slot a close or another entry's exit drained; since lane W8A-SIGSTATE-1's R-B a resting LIMIT entry no longer counts, and one the count admitted fills over the cap, and since lane W5B-ENG-MARGIN-RESIDUAL's SB the later market entries of a flat bar are refused at the fill once the book holds the cap, as on TradingView's tapes). A recorded divergence: the retention stands pending a re-lowering ruling. R5 lane H-THIN tried the lowering and kept the count: on the kernel's cap four corpus probes moved away from TradingView, for want of the first-eligible-point rule (H-MEASURE's P4b); it fixed the reservation divergence measured beside it (P10), so a default `strategy.exit(from_entry)` under FIFO now reserves its entry's own quantity, the oldest lots first, as TradingView does | `tests/test_native_resting_matching_contract.cpp`, `tests/test_native_margin_model.cpp` | | `initial_margin_fraction` | **adapter-policy** | TradingView's ten-significant-digit money admission against the signal-time tuple, answered as `AdmitWithHostMargin`; the `margin` model the adapter does declare is maintenance-only (`NativeMarginModel` `pine_adapter.cpp:2885-2893`, `margin.maintenance_long` `pine_adapter.cpp:2900`) | design row MG4 and the wave-4 ruling recorded in `project()`: a positive initial requirement would decline openings TradingView takes. Measured both ways by R5 lane H-MEASURE (the private test `test_adapter_margin_schedule_differential`, M11): the kernel's gate declines a 1x long whose entry fee the adapter admits (`InitialMargin`), and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times | `tests/test_native_precommit_view.cpp`, `tests/test_native_margin_model.cpp` | | `report_open_position_at_end` | **adapter-policy** | The Pine host owns TradingView's range-end shape: batch re-marks the stored final point and re-folds extremes, while REALTIME retains ordinary marks and `PineStrategyHost::present_report` re-marks only the caller-owned copy before generic metric derivation; the stream hash excludes that terminal extreme fold (`scheduler_record_range_end` pine_strategy_host.cpp:1987): report shape, not a mark-to-market row (`KernelRecordedAtHostMarks` pine_adapter.cpp:2855) | design row RP5; the kernel reads the field under `KernelRecorded` only, which `scripts/check_adapter_spec_shadowing.py` gates | `examples/native/native_sized_report_strategy.cpp`, `tests/test_native_report_truth.cpp` | diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 906805140..0d7b1be8a 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -47,19 +47,19 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:3436; `entry()` pine_adapter.cpp:7159, `order()` pine_adapter.cpp:14612) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:6579; `submit` native_host.hpp:1213 | K | — | F:A1 O:A1 S:O2 | — | +| OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:3436; `entry()` pine_adapter.cpp:7159, `order()` pine_adapter.cpp:14624) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:6579; `submit` native_host.hpp:1213 | K | — | F:A1 O:A1 S:O2 | — | | OL2 | Intents: flatten, reduce by units, reduce owner-opened units, transact, reverse-to (`close_all()` pine_adapter.cpp:10543 → `Flatten{}` pine_adapter.cpp:10577; `Reduce{ExplicitUnits}` pine_adapter.cpp:9416; `ReverseTo` pine_adapter.cpp:7835) | **yes** — `OrderIntent` native_order.hpp:229, reduce sizes `ReductionSize` native_order.hpp:215, `ReverseTo` native_order.hpp:60-62, `OpeningShape` native_order.hpp:910-914, `Flatten` native_order.hpp:36 | K | — | F:A4 F:A8 O:A2 O:A3 S:O1 | — | | OL3 | Amend / cancel one working order (`submit_or_replace` pine_adapter.cpp:4363 → `host.replace` pine_adapter.cpp:4788) | **yes** — `replace` native_host.hpp:1218-1220, `cancel` native_host.hpp:1249; `ReplacedEvent` native_order.hpp:1017-1031; result and event types `SubmitStatus` native_order.hpp:835-861, `ReplacedEvent` native_order.hpp:1001-1067 | K | — | F:A2 O:A4 S:O3 | id → handle map is host bookkeeping (F) → OL4 | -| OL4 | Cancel all / cancel by label; label → handle book (`cancel(id)` pine_adapter.cpp:14506, `cancel_all` pine_adapter.cpp:14579; `live_by_source_key_` pine_adapter.hpp:2521; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4801) | **partial** — single-handle `cancel` native_host.hpp:1249; no enumeration (`WorkingRequestCore::live()` is consumer-internal native_order.hpp:1703); labels exist but are unindexed (`Request::label` native_order.hpp:485) **Closed:** `cancel_all` native_host.hpp:1267, `cancel_where(comment)` native_host.hpp:1272 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1272 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1263 is the enumeration; the id → handle book is `native_toolkit::OrderBook`, host-side and Pine-free (`tests/test_native_order_ergonomics.cpp`, `tests/test_native_anchored_legs.cpp`). | K+A | L7 | F:A3 F:A2 O:A5 O:A14 S:O3 | **Disagree:** F partial / O no (A5) + partial (A14) / S yes (folded into O3). R5-1: cancel-all / by-label is generic → kernel. TV state inheritance across a same-id re-issue stays A. The adapter's `strategy.cancel` / `strategy.cancel_all` keep their per-handle loops, measured by R5 lane F7 (audit M27): TradingView cancels in the SOURCE book, which holds commands of the same script call that have not reached the kernel yet (and a live `strategy.close` is labelled `__close__` there), so at the same point `cancel_where(id, NativeRequestField::Label)` and `cancel_all()` withdraw 0 — the close flattens at the next open, the reversal lands — where the source cancels withdraw them (`tests/test_adapter_brackets_relower.cpp`, M27 rows); each withdrawn source handle is also retired one by one. | +| OL4 | Cancel all / cancel by label; label → handle book (`cancel(id)` pine_adapter.cpp:14518, `cancel_all` pine_adapter.cpp:14591; `live_by_source_key_` pine_adapter.hpp:2524; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4801) | **partial** — single-handle `cancel` native_host.hpp:1249; no enumeration (`WorkingRequestCore::live()` is consumer-internal native_order.hpp:1703); labels exist but are unindexed (`Request::label` native_order.hpp:485) **Closed:** `cancel_all` native_host.hpp:1267, `cancel_where(comment)` native_host.hpp:1272 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1272 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1263 is the enumeration; the id → handle book is `native_toolkit::OrderBook`, host-side and Pine-free (`tests/test_native_order_ergonomics.cpp`, `tests/test_native_anchored_legs.cpp`). | K+A | L7 | F:A3 F:A2 O:A5 O:A14 S:O3 | **Disagree:** F partial / O no (A5) + partial (A14) / S yes (folded into O3). R5-1: cancel-all / by-label is generic → kernel. TV state inheritance across a same-id re-issue stays A. The adapter's `strategy.cancel` / `strategy.cancel_all` keep their per-handle loops, measured by R5 lane F7 (audit M27): TradingView cancels in the SOURCE book, which holds commands of the same script call that have not reached the kernel yet (and a live `strategy.close` is labelled `__close__` there), so at the same point `cancel_where(id, NativeRequestField::Label)` and `cancel_all()` withdraw 0 — the close flattens at the next open, the reversal lands — where the source cancels withdraw them (`tests/test_adapter_brackets_relower.cpp`, M27 rows); each withdrawn source handle is also retired one by one. | | OL5 | Per-point fill capacity, partial fills, residual working quantity (Pine uses only `ImmediateRemaining` pine_adapter.cpp:5007; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:9162) | **yes** — `Capacity` / `PointBudget` native_order.hpp:379-383, `Remaining` native_order.hpp:557-579, allowances `Allowance` native_order.hpp:662-675 | K | — | O:A16 S:O4 | — | | OL6 | Ownership: legs bound to a parent / opening(s) / cohort; close the lots of a given entry, FIFO or ANY (`owner_for_close` pine_adapter.cpp:6579, `cohort_for` pine_adapter.cpp:3012; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:9257) | **yes** — `Owner` native_order.hpp:464-479, `Authority` native_order.hpp:620-621, `CohortClose` native_order.hpp:617-619, `SelectedExposure` native_order.hpp:625-629, `cohort_open` native_host.hpp:1282, `CancelReason::OwnerGone` native_order.hpp:949 | K | — | F:A6 O:A8 O:A9 S:O5 | FIFO *ordering* is host-computed (O) | | OL7 | OCA cancel / reduce groups (`group_for` pine_adapter.cpp:3445) | **yes** — `Group` / `GroupEffect` native_order.hpp:466-473, `CancelReason::Group` native_order.hpp:948-953, `ReservationReducedEvent` native_order.hpp:1191-1201 (deferred form `DeferredGroupAdjustmentEvent` native_order.hpp:1211-1221) | K | — | F:A12 O:A6 O:A7 S:O7 | — | | OL8 | Immediate execution at the current point (`immediately=true`) | **yes** — `inspect_current_execution` / `execute_current` native_host.hpp:1104-1105 | K | — | F:A9 | (single-source row; O:F2 cites the same call) | | OL9 | Birth eligibility: an order accepted at bar N cannot fill on N's already-delivered points | **yes** — `point_eligible` native_order.hpp:786-790, the generic birth record `OrderBirth` include/pineforge/order_birth.hpp:59, contract `docs/pages/native-engine.md` ("Acceptance is not a fill") | K+A | — | F:A13 O:A10 S:R4 | **Disagree:** F yes / O partial / S "no, TV quirk". R5-1: the generic core (birth cursor) is native; TV's bar-granular first-bar rule is the A half → OL14. | | OL10 | TP/SL bracket per entry, armed before the entry fills, OCA between legs (`exit()` pine_adapter.cpp:11036; lowering `submit_one` pine_adapter.cpp:11914, `BindCohort` pine_adapter.cpp:12377, `submit_leg` pine_adapter.cpp:11670; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1940); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | -| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:11296; `materialize_relative_exits` pine_adapter.cpp:13985, pine_adapter.hpp:2259-2271) | **partial** — composable: the host computes levels in `on_native_applied` and submits; suffix eligibility makes that same-bar correct `born_on_remaining_path` native_execution_consumer.cpp:6507. `Trigger` itself is absolute-only native_order.hpp:296 **Closed:** `Request::anchor = FromOwnerFill{offset, ticks, rounding}` (`TriggerAnchor` native_order.hpp:336) defers the level to the owner's fill, the kernel materializes it once at the arm under `NativeAnchorRounding`, and `resolve_anchored_level` native_host.hpp:1012 is where a source language's own level arithmetic lives (`tests/test_native_anchored_legs.cpp`, `tests/test_native_arm_options.cpp`). | K | L7 | F:A11 O:A11 S:O6 | **Disagree:** F partial (toolkit is enough) / O no (kernel `TriggerAnchor`). R5-2: L7 delivers relative anchors; kernel anchor primary, toolkit materialization is the zero-kernel fallback. | -| OL12 | Working-order view for the host and outside observers (`strategy_pending_order_*`; `PendingIntentView` pine_adapter.hpp:1449-1484; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:27063) | **no** — base returns 0 / -1 `observe_last_bar_dual_entry_path_v1` engine_consumer.cpp:124-133; the POD is TV-shaped (`tv_carry_qty` pending_order_mirror.hpp:73, `pine_birth_reach` pending_order_mirror.hpp:135) **Closed for a bare host:** `native_working_requests()` native_host.hpp:1263 returns one owning `NativeWorkingRequest` native_host.hpp:681 per live request — definition, remaining, trigger state — with the C spelling `strategy_native_working_len_v1` / `_get_v1`. The TV-shaped POD stays frozen and stays the source layer's projection (`tests/test_native_limit_fill_through.cpp`, `tests/test_native_arm_options.cpp`, `tests/test_native_c_api.c`). | K+A | L7 (C++), L13 (C) | F:J5 O:J6 S:X4 | **Disagree:** O "partial + TV-shaped" / F, S no. A bare host reads zeros → no. R5-1: neutral view → kernel; POD shape and Pine journal retention stay A (POD frozen, §2.ii j). | -| OL13 | *(quirk)* Same-bar command batching and the seven deferred queues (`pending_bracket_legs_` pine_adapter.hpp:2515-2534); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1364); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:4194, `materialize_snapshot` pine_adapter.cpp:13959), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1357-1384, `DeferredOpenMarketableSell` pine_adapter.hpp:1845-1868, `CloseCallsiteState` pine_adapter.hpp:1983-1997; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:21617, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:21851) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:676-678 | A | — | F:A15 O:A15 S:O9 S:R3 | R5-1 names KI-62. S: the kernel exposes deterministic insertion order only. | +| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:11296; `materialize_relative_exits` pine_adapter.cpp:13985, pine_adapter.hpp:2262-2274) | **partial** — composable: the host computes levels in `on_native_applied` and submits; suffix eligibility makes that same-bar correct `born_on_remaining_path` native_execution_consumer.cpp:6507. `Trigger` itself is absolute-only native_order.hpp:296 **Closed:** `Request::anchor = FromOwnerFill{offset, ticks, rounding}` (`TriggerAnchor` native_order.hpp:336) defers the level to the owner's fill, the kernel materializes it once at the arm under `NativeAnchorRounding`, and `resolve_anchored_level` native_host.hpp:1012 is where a source language's own level arithmetic lives (`tests/test_native_anchored_legs.cpp`, `tests/test_native_arm_options.cpp`). | K | L7 | F:A11 O:A11 S:O6 | **Disagree:** F partial (toolkit is enough) / O no (kernel `TriggerAnchor`). R5-2: L7 delivers relative anchors; kernel anchor primary, toolkit materialization is the zero-kernel fallback. | +| OL12 | Working-order view for the host and outside observers (`strategy_pending_order_*`; `PendingIntentView` pine_adapter.hpp:1452-1487; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:27075) | **no** — base returns 0 / -1 `observe_last_bar_dual_entry_path_v1` engine_consumer.cpp:124-133; the POD is TV-shaped (`tv_carry_qty` pending_order_mirror.hpp:73, `pine_birth_reach` pending_order_mirror.hpp:135) **Closed for a bare host:** `native_working_requests()` native_host.hpp:1263 returns one owning `NativeWorkingRequest` native_host.hpp:681 per live request — definition, remaining, trigger state — with the C spelling `strategy_native_working_len_v1` / `_get_v1`. The TV-shaped POD stays frozen and stays the source layer's projection (`tests/test_native_limit_fill_through.cpp`, `tests/test_native_arm_options.cpp`, `tests/test_native_c_api.c`). | K+A | L7 (C++), L13 (C) | F:J5 O:J6 S:X4 | **Disagree:** O "partial + TV-shaped" / F, S no. A bare host reads zeros → no. R5-1: neutral view → kernel; POD shape and Pine journal retention stay A (POD frozen, §2.ii j). | +| OL13 | *(quirk)* Same-bar command batching and the seven deferred queues (`pending_bracket_legs_` pine_adapter.hpp:2518-2537); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1364); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:4194, `materialize_snapshot` pine_adapter.cpp:13959), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1360-1387, `DeferredOpenMarketableSell` pine_adapter.hpp:1848-1871, `CloseCallsiteState` pine_adapter.hpp:1986-2000; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:21629, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:21863) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:676-678 | A | — | F:A15 O:A15 S:O9 S:R3 | R5-1 names KI-62. S: the kernel exposes deterministic insertion order only. | | OL14 | *(quirk)* Exit activation / suspension / revival barriers, historical birth reach (`select_exit_activation` exit_activation.cpp:44-90, enforced `leg_activation` pine_adapter.cpp:1261; `select_exit_activation` exit_activation.cpp:20-90, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:1078) | n/a | A | — | S:R4 O:A10 | TV half of OL9. **V19D-P1 (R5 lanes B-ADAPTER, V19-FIX):** a margin-call revival never restores an exit the script cancelled: `strategy.cancel` / `cancel_all` retire every leg of each exit row they name — the live ones they take off the book (caused at the Cancelled event, after anything the same callback did to the leg), the dormant ones, and the ones the adapter took off the book itself — and drop a gapped stop a declined reversal parked for the next margin-call slice (`retire_cancelled_exits` pine_adapter.cpp:2123); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:2200). What `tests/test_adapter_margin_revival_cancel.cpp` replays on the NYSE:F 15m chart of seven lab tv tapes: badapter-v19dp1-control (no cancel: the margin call closes 928 through X) and v19fix-gapped-stop-control (X's stop at 10.15, gapped by the 05-01 open and parked: the same close), and badapter-v19dp1-cancel, v19fix-cancel-before-pair, v19fix-cancel-at-1000, v19fix-gapped-stop-cancel and v19fix-gapped-stop-cancel-after-pair (a cancel on 05-01 09:30 while X is dormant, on 04-30 15:45 before the reversal pair and on 04-30 10:00 while X is live, and on 05-01 09:30 and after the pair while X is parked; one tape file: margin calls 12 @10.39 and 92 @10.57, 836 open). Untaped, the same row holds the tapes' trades for `strategy.cancel_all` on 05-01 and before the pair and for `strategy.cancel` after the pair, and on the same bars with no quantity grid — where the pair holds X in the cancel's own callback — every spelling books what the cancel on the dormant exit books | ### 1.2 Trailing @@ -68,9 +68,9 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa |---|---|---|---|---|---|---| | TR1 | Trail arm → track → trigger, with a state query | **yes** — `Trail{offset, arm_price}` native_order.hpp:290-293; states `TrailWaitArm` native_order.hpp:642-660; arm / track / fire `TrailWaitArm` native_execution_consumer.cpp:6588; `NativeTrailState` native_host.hpp:669-674, `trail_state` native_host.hpp:1092 | K | — | F:B1 O:B1 S:T1 | — | | TR2 | Trail activation / offset in ticks or points, relative to the entry fill (`trail_points_to_ticks` include/pineforge/compat/pine/trail_ticks.hpp:44 + grid snap exit_lifecycle.cpp:22-27; `trail_activation_level` pine_adapter.hpp:350-352) | **partial** — absolute prices and a raw price distance only **Closed:** `TrailTicks{n}` (`Trail::ticks` native_order.hpp:293) is resolved against `NativeRunSpec::price_tick` at acceptance, so a tick-spelled trail and its price-spelled equal behave identically bar for bar; a relative arm threshold is the `FromOwnerFill` anchor of OL11 (`tests/test_native_order_ergonomics.cpp`, `tests/test_native_anchored_legs.cpp`). | K | L7 | F:B2 O:B2 S:T2 | **Disagree:** S classes all trail operand conversion as TV quirk. R5-1: tick / relative spelling is generic. | -| TR3 | Zero-distance trail, "ride the best, exit on any adverse tick" (adapter substitutes `tick * 0.5` pine_adapter.cpp:15003) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:5323) **Closed:** a zero offset is legal and means *ride the best* — the level is the running best itself and the exit is the first print strictly past it; negative and nonfinite offsets stay rejected (`tests/test_native_order_ergonomics.cpp`, `tests/test_native_anchored_legs.cpp`). | K+A | L7 | O:B3 F:B3 S:T2 | **Disagree:** O kernel / F, S quirk. R5-1: accepting a zero offset is generic; TV zero-offset *pricing* (`zero_trail_sibling_stop` pine_adapter.cpp:14920) stays A. | +| TR3 | Zero-distance trail, "ride the best, exit on any adverse tick" (adapter substitutes `tick * 0.5` pine_adapter.cpp:15015) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:5323) **Closed:** a zero offset is legal and means *ride the best* — the level is the running best itself and the exit is the first print strictly past it; negative and nonfinite offsets stay rejected (`tests/test_native_order_ergonomics.cpp`, `tests/test_native_anchored_legs.cpp`). | K+A | L7 | O:B3 F:B3 S:T2 | **Disagree:** O kernel / F, S quirk. R5-1: accepting a zero offset is generic; TV zero-offset *pricing* (`zero_trail_sibling_stop` pine_adapter.cpp:14932) stays A. | | TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:4756) | **partial** — the read exists `trail_state` native_host.hpp:1092; `replace` always rebuilds trigger state **Closed:** `replace(handle, request, ReplaceOptions{/*retain_trigger_state=*/true})` carries the predecessor's live trigger state — a tracking trail's best, an already active stop — into the successor, refusing the replacement when the retained best leaves no representable level (`tests/test_native_order_ergonomics.cpp`, `tests/test_native_anchored_legs.cpp`). | K+A | L7 | O:B4 F:B3 | Status is single-source (O). R5-1: retain-trail replace is generic; TV's "retained best across re-issue" convention (`retained_trail_best` pine_adapter.hpp:353-355) is A. | -| TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:11158, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:14920, retained best `retained_trail_best` pine_adapter.hpp:353-355, operand / restart rules `source_trail_points` pine_adapter.cpp:11155, `trail_one_shot` pine_adapter.cpp:12501 | n/a | A | — | F:B3 S:T2 | R5-1 | +| TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:11158, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:14932, retained best `retained_trail_best` pine_adapter.hpp:353-355, operand / restart rules `source_trail_points` pine_adapter.cpp:11155, `trail_one_shot` pine_adapter.cpp:12501 | n/a | A | — | F:B3 S:T2 | R5-1 | ### 1.3 Sizing bases and reservations @@ -86,8 +86,8 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | SZ8 | Fractional reduce of the position / a cohort (`quantize_close_units` pine_adapter.cpp:5212, `compute_exit_reservation` pine_adapter.cpp:5252) | **no** — sizes are `ExplicitUnits` / `OwnerOpenedUnits` / `HostSized` only native_order.hpp:60-74 **Closed:** `Reduce{ScopeFraction{fraction, claim, basis}}` takes a fraction in `(0, 1]` of the scope the reduce is bound to — the book, one opening, or a cohort roster (`tests/test_native_sizing_bases.cpp`). | K | L3 | F:A7 O:A12 O:C4 S:S5 | F's "partial" counts the units-reduce of OL2. R5-4 → L3. | | SZ9 | Exit-quantity reservation against sibling exits (`compute_exit_reservation` pine_adapter.cpp:5252, `reserved_exit_qty` pine_adapter.cpp:11565) | **partial** — `PointBudget` native_order.hpp:379-382 and group `Reduce` native_order.hpp:220 give a generic reservation; no net-of-siblings claim for fraction sizes **Closed:** `ScopeClaim::NetOfSiblings` subtracts the units already claimed by live sibling reduces bound to the same scope, and `ScopeBasis::{AtMatch, AtAcceptance}` chooses when that scope is measured (`tests/test_native_sizing_bases.cpp`). | K+A | L3 | F:A14 S:S5 S:O10 O:A12 | F proposed nothing. R5-4: generic half → L3; the Pine ledger → SZ11, SZ12. | | SZ10 | *(quirk)* Ten-significant-digit money rule (`source_money_round` pine_adapter.cpp:558, `source_money_floor_lot` pine_adapter.cpp:658; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:5234) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1753 | A | — | O:C8 F:C3 S:M4 S:S5 | R5-1 | -| SZ10a | *(quirk, measured retention)* The money band's affordable price, `sig10(sig10(E) / notional per unit of price)` against the sizing price — at placement (`entry`, `affordable_price` pine_adapter.cpp:8511), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:10035), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:16214), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:16745, pine_adapter.cpp:16798) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:21665) | n/a — not a units conversion. Every money-to-units conversion in the adapter is the kernel's `native_sized_units` under the source's floors since R5 lane B-ADAPTER: the default at placement and at the fill (`default_sizing_units` pine_adapter.cpp:3201), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:3220, `typed_entry_units` pine_adapter.cpp:3238), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:26650) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes and 202 famr tapes (two tests that live in the private fixtures); on a seven-digit tie (E 9998.99999996, 10 000 lots at tick(close) 0.9999) the rule admits while the kernel's conversion buys 9999.99999996 lots and binary64 gives E/Q 0.999899999996 (`tests/test_adapter_fill_qty_probe.cpp`, section 2). | -| SZ11 | *(quirk)* Same-bar `strategy.close` callsite batching, two-call provenance, entry-id ledger (`enqueue_pooc_fifo_close` pine_adapter.cpp:9162; ledger `close_batch_bar_` pine_adapter.hpp:2595; the kernel's comment-only residue was deleted by R5 lanes E6 and E10) | n/a | A | — | S:O8 F:A15 O:A15 | **Disagree:** S "generic → kernel" (its P2 `ReplaceByKey` / `Fifo`). R5-4: adapter. | +| SZ10a | *(quirk, measured retention)* The money band's affordable price, `sig10(sig10(E) / notional per unit of price)` against the sizing price — at placement (`entry`, `affordable_price` pine_adapter.cpp:8511), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:10035), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:16226), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:16757, pine_adapter.cpp:16810) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:21677) | n/a — not a units conversion. Every money-to-units conversion in the adapter is the kernel's `native_sized_units` under the source's floors since R5 lane B-ADAPTER: the default at placement and at the fill (`default_sizing_units` pine_adapter.cpp:3201), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:3220, `typed_entry_units` pine_adapter.cpp:3238), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:26650) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes and 202 famr tapes (two tests that live in the private fixtures); on a seven-digit tie (E 9998.99999996, 10 000 lots at tick(close) 0.9999) the rule admits while the kernel's conversion buys 9999.99999996 lots and binary64 gives E/Q 0.999899999996 (`tests/test_adapter_fill_qty_probe.cpp`, section 2). | +| SZ11 | *(quirk)* Same-bar `strategy.close` callsite batching, two-call provenance, entry-id ledger (`enqueue_pooc_fifo_close` pine_adapter.cpp:9162; ledger `close_batch_bar_` pine_adapter.hpp:2598; the kernel's comment-only residue was deleted by R5 lanes E6 and E10) | n/a | A | — | S:O8 F:A15 O:A15 | **Disagree:** S "generic → kernel" (its P2 `ReplaceByKey` / `Fifo`). R5-4: adapter. | | SZ12 | *(quirk)* POOC reservation-growth market-entry population predicate (`select_reservation_growth_sources` src/compat/pine/reservation_expansion.cpp:9-34, called from the adapter, `select_reservation_growth_sources` pine_adapter.cpp:1322) | n/a | A | — | S:O10 | (single-source) R5-4 confirms S's own split: concept → SZ9, predicate A. | ### 1.4 Margin, admission, liquidation, risk @@ -95,21 +95,21 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | MG1 | Allowed opening directions (`allowed_open_directions` pine_adapter.cpp:2859) | **yes** — `allowed_open_directions` native_run_spec.hpp:665 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:3689 | K | — | F:D4 O:D7 S:M1 | the one risk rule the adapter delegates (O) | -| MG2 | Max absolute position size | **yes** — `max_abs_units` native_run_spec.hpp:663 tests the resulting book native_execution_consumer.cpp:3696 | K+A | — | F:D5 O:D5 S:M1 S:M5 | **Disagree:** O, S:M5 partial. R5-1: the generic cap is native; the adapter's live-position `>= held` variant (`max_position_size` pine_adapter.cpp:16493) is TV. | +| MG2 | Max absolute position size | **yes** — `max_abs_units` native_run_spec.hpp:663 tests the resulting book native_execution_consumer.cpp:3696 | K+A | — | F:D5 O:D5 S:M1 S:M5 | **Disagree:** O, S:M5 partial. R5-1: the generic cap is native; the adapter's live-position `>= held` variant (`max_position_size` pine_adapter.cpp:16505) is TV. | | MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:664 → `MaxOpenLots` native_execution_consumer.cpp:3701 | K+A | — | F:A5 O:A13 O:D6 S:M1 | **Disagree:** F, O:A13 partial / O:D6, S yes. R5-1: Pine's per-cycle entry count (`accepted_in_cycle` pine_adapter.cpp:7699, cap left unset `max_open_lots` pine_adapter.cpp:2858-2859) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (the private test `test_pyramiding_count_differential`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 13 (it keeps a slot a close or another entry's exit drained; since lane W5B-ENG-MARGIN-RESIDUAL's SB the later market entries of a flat bar are refused at the fill once the book holds the cap). The fifteenth, a resting entry accepted below the cap and filled at it, the kernel's cap refuses; the adapter books it since lane W8A-SIGSTATE-1's R-B, which also stopped the count from counting a resting LIMIT entry (a pending LIMIT entry does not count at a later call, and one the cap admitted fills over it: the private fixture set `pyramiding_open_order`). A recorded divergence, retained pending a re-lowering ruling. **Re-lowering tried by R5 lane H-THIN and not taken:** on the kernel's cap four corpus probes moved away from TradingView, for want of that first-eligible-point rule, so the count stays. The lane fixed the reservation divergence H-MEASURE measured beside it (P10): a default `strategy.exit(from_entry)` under FIFO reserved the whole book (200 in the adapter, 100 on TradingView's tape); it now reserves the entry's own quantity, the oldest lots first (`fifo_named_entry` pine_adapter.cpp:11589). | -| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:17306 and leaves the kernel gate unset pine_adapter.cpp:17325) | **yes** — `initial_margin_fraction` native_run_spec.hpp:666; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:3719 against marked equity. **Conversion instant, R5 follow-up lane E21:** both sites of the gate convert at their own point's rate — the candidate at its cursor, the placement gate at the acceptance point where a `Sized{SizeTime::AtAcceptance}` freezes its units — for the notional, the equity and a percent ticket, and the freeze's equity basis with them. The rate is threaded, never the clock: `make_command_context` native_execution_consumer.cpp:3565 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:650, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (the private test `test_adapter_margin_schedule_differential`). **Since R5 lane TAIL-C** an explicit-quantity market buy from flat is costed at its signal close's tick plus the slippage ticks its fill carries, off `process_orders_on_close` (the private test `test_explicit_market_admission_tapes`), and an infinite `strategy.entry` quantity trades the default quantity, as na does (`tests/test_nonfinite_entry_qty_tape.cpp`). | -| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:17559, `margin_pct` pine_adapter.cpp:17061) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:666 **Closed:** `NativeMarginModel::initial_long` / `initial_short` native_run_spec.hpp:242, opt-in and mutually exclusive with `initial_margin_fraction`; `initial_ == 0.0` is the maintenance-only spelling that leaves opening admission to the host at both of its gates (`tests/test_native_margin_model.cpp`). | K | L4 | F:D1 O:D2 | R5-1 | -| MG6 | Maintenance margin + forced liquidation (`submit_margin_call_slice` pine_adapter.cpp:17946, `submit_margin_call_units` pine_adapter.cpp:17979, `schedule_margin_call_path` pine_adapter.cpp:20224) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:666-667; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:3678 **Closed:** `maintenance_long` / `maintenance_short` with `NativeLiquidationCheck::{PathAdverseExtreme, PathAdverseExtremeMark, CalculationOnly}`; the kernel solves the level, rests its own `Reduce`/`Flatten` bound to the live book, re-prices it with `CancelReason::Superseded` and books a `MarginCallEvent` (`tests/test_native_margin_model.cpp`, `tests/test_native_margin_hooks.cpp`). | K+A | L4 | F:D2 O:D3 S:M3 | TV parts → MG15 | -| MG7 | Liquidation slice sizing (grid floor, ×4 restore, one-contract fallback `source_margin_units` pine_adapter.cpp:17603) | **no** **Closed:** `NativeLiquidationSizing::{RestoreMinimum, ShortfallMultiple, Flatten}` with `shortfall_multiple` and `liquidation_min_units`, capped at the held quantity and floored onto `quantity_grid`; `resolve_margin_call_units` native_host.hpp:995 gives a host the last word, clamped into `(0, held]` (`tests/test_native_margin_model.cpp`, `tests/test_native_margin_hooks.cpp`). | K+A | L4 | O:D4 F:D2 S:M4 | **Disagree:** F generic knob `ShortfallMultiple` / O "×4 is A" / S quirk, yet a 4.0 default inside its generic model. R5-1: policy knob in the kernel, plain default (restore-minimum). **Decided by R5 lane F7 (the D2 question):** the adapter selects NO kernel knob — `project()` leaves `sizing` / `shortfall_multiple` / `liquidation_min_units` unset and its `resolve_margin_call_units` answers every call with TradingView's slice (the restore floored onto the lot grid BEFORE the ×4, floored again, the one-contract band, the dust gate). Measured: on a one-unit grid, 20 long at 100 on 1000 at 50 % reaching 93.9, TradingView books 4 lots and the kernel's own `ShortfallMultiple 4` books 5 (`tests/test_native_margin_hooks_twin.cpp` MG-F3, an executed run, no longer a comment); without a grid the two agree bit for bit (MG-F/H, 4.2105263157894735), but declaring the knob for those runs alone folds `NativeMarginModel::sizing` into every margin run's identity — an epoch decision F7 does not take. Since R5 lane H-THIN the pre-open admission slice (`schedule_preopen_margin_slice`) sizes through the same `source_margin_units`, where it kept its own copy of the ×4 without the band or the dust gates (AUDIT4 §6 G2-12; the private test `test_adapter_margin_schedule_differential` M10: band and fudge now 1 and 1, the kernel's and `ab9714be`'s). A four-times call on a whole-unit grid whose book's oldest lot is one unit is followed by one more unit at the next check point (`whole_unit_follow_up_due` pine_adapter.cpp:19131; R5 lane W5B-ENG-MARGIN-RESIDUAL, the private fixture set `margin_residual` w5b-es-*, w5b-sb-es-*, w5b-f-q1x12). With slippage the one-contract band takes its unit only where that unit restores the book at its own fill: the position's requirement at the liquidation's slipped print, less the equity at the mark, under one unit's margin (`source_margin_units` pine_adapter.cpp:17603; lane INT28-FIX rule OU, the private fixture set `slipped_short`). A one-unit call that leaves the book short at its own fill, by more than one unit's margin and two slippage steps with the book re-marked there, is followed by one more unit at the bar's next path point, where that point's own check calls nothing and its print is inside the call's fill (lane INT28-FIX rule OF). Under a percent commission a call of any size is followed so, on a fractional lot grid too: while the book the call left, re-marked at its fill, is short by more than two slippage steps for each unit called and the exit fees of those units and of one more, the next path point inside that fill takes four times the shortfall's lot floor over the fill, or one unit where that floors to none (`margin_follow_up_units` pine_adapter.cpp:19173, `follow_margin_call` pine_adapter.cpp:19222; R5 lane TAIL-I, the private fixture set `margin_open_print`). | +| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:17318 and leaves the kernel gate unset pine_adapter.cpp:17337) | **yes** — `initial_margin_fraction` native_run_spec.hpp:666; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:3719 against marked equity. **Conversion instant, R5 follow-up lane E21:** both sites of the gate convert at their own point's rate — the candidate at its cursor, the placement gate at the acceptance point where a `Sized{SizeTime::AtAcceptance}` freezes its units — for the notional, the equity and a percent ticket, and the freeze's equity basis with them. The rate is threaded, never the clock: `make_command_context` native_execution_consumer.cpp:3565 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:650, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (the private test `test_adapter_margin_schedule_differential`). **Since R5 lane TAIL-C** an explicit-quantity market buy from flat is costed at its signal close's tick plus the slippage ticks its fill carries, off `process_orders_on_close` (the private test `test_explicit_market_admission_tapes`), and an infinite `strategy.entry` quantity trades the default quantity, as na does (`tests/test_nonfinite_entry_qty_tape.cpp`). | +| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:17571, `margin_pct` pine_adapter.cpp:17073) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:666 **Closed:** `NativeMarginModel::initial_long` / `initial_short` native_run_spec.hpp:242, opt-in and mutually exclusive with `initial_margin_fraction`; `initial_ == 0.0` is the maintenance-only spelling that leaves opening admission to the host at both of its gates (`tests/test_native_margin_model.cpp`). | K | L4 | F:D1 O:D2 | R5-1 | +| MG6 | Maintenance margin + forced liquidation (`submit_margin_call_slice` pine_adapter.cpp:17958, `submit_margin_call_units` pine_adapter.cpp:17991, `schedule_margin_call_path` pine_adapter.cpp:20236) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:666-667; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:3678 **Closed:** `maintenance_long` / `maintenance_short` with `NativeLiquidationCheck::{PathAdverseExtreme, PathAdverseExtremeMark, CalculationOnly}`; the kernel solves the level, rests its own `Reduce`/`Flatten` bound to the live book, re-prices it with `CancelReason::Superseded` and books a `MarginCallEvent` (`tests/test_native_margin_model.cpp`, `tests/test_native_margin_hooks.cpp`). | K+A | L4 | F:D2 O:D3 S:M3 | TV parts → MG15 | +| MG7 | Liquidation slice sizing (grid floor, ×4 restore, one-contract fallback `source_margin_units` pine_adapter.cpp:17615) | **no** **Closed:** `NativeLiquidationSizing::{RestoreMinimum, ShortfallMultiple, Flatten}` with `shortfall_multiple` and `liquidation_min_units`, capped at the held quantity and floored onto `quantity_grid`; `resolve_margin_call_units` native_host.hpp:995 gives a host the last word, clamped into `(0, held]` (`tests/test_native_margin_model.cpp`, `tests/test_native_margin_hooks.cpp`). | K+A | L4 | O:D4 F:D2 S:M4 | **Disagree:** F generic knob `ShortfallMultiple` / O "×4 is A" / S quirk, yet a 4.0 default inside its generic model. R5-1: policy knob in the kernel, plain default (restore-minimum). **Decided by R5 lane F7 (the D2 question):** the adapter selects NO kernel knob — `project()` leaves `sizing` / `shortfall_multiple` / `liquidation_min_units` unset and its `resolve_margin_call_units` answers every call with TradingView's slice (the restore floored onto the lot grid BEFORE the ×4, floored again, the one-contract band, the dust gate). Measured: on a one-unit grid, 20 long at 100 on 1000 at 50 % reaching 93.9, TradingView books 4 lots and the kernel's own `ShortfallMultiple 4` books 5 (`tests/test_native_margin_hooks_twin.cpp` MG-F3, an executed run, no longer a comment); without a grid the two agree bit for bit (MG-F/H, 4.2105263157894735), but declaring the knob for those runs alone folds `NativeMarginModel::sizing` into every margin run's identity — an epoch decision F7 does not take. Since R5 lane H-THIN the pre-open admission slice (`schedule_preopen_margin_slice`) sizes through the same `source_margin_units`, where it kept its own copy of the ×4 without the band or the dust gates (AUDIT4 §6 G2-12; the private test `test_adapter_margin_schedule_differential` M10: band and fudge now 1 and 1, the kernel's and `ab9714be`'s). A four-times call on a whole-unit grid whose book's oldest lot is one unit is followed by one more unit at the next check point (`whole_unit_follow_up_due` pine_adapter.cpp:19143; R5 lane W5B-ENG-MARGIN-RESIDUAL, the private fixture set `margin_residual` w5b-es-*, w5b-sb-es-*, w5b-f-q1x12). With slippage the one-contract band takes its unit only where that unit restores the book at its own fill: the position's requirement at the liquidation's slipped print, less the equity at the mark, under one unit's margin (`source_margin_units` pine_adapter.cpp:17615; lane INT28-FIX rule OU, the private fixture set `slipped_short`). A one-unit call that leaves the book short at its own fill, by more than one unit's margin and two slippage steps with the book re-marked there, is followed by one more unit at the bar's next path point, where that point's own check calls nothing and its print is inside the call's fill (lane INT28-FIX rule OF). Under a percent commission a call of any size is followed so, on a fractional lot grid too: while the book the call left, re-marked at its fill, is short by more than two slippage steps for each unit called and the exit fees of those units and of one more, the next path point inside that fill takes four times the shortfall's lot floor over the fill, or one unit where that floors to none (`margin_follow_up_units` pine_adapter.cpp:19185, `follow_margin_call` pine_adapter.cpp:19234; R5 lane TAIL-I, the private fixture set `margin_open_print`). | | MG8 | Liquidation price query (`margin_liquidation_price` pine_strategy_host.cpp:168-184) | **no** **Closed:** `native_liquidation_price()` native_host.hpp:1320 answers the exact solved level, `nullopt` where none exists; `NativeLiquidationLevelBase` chooses whether the solve answers for the open entries' fees (`tests/test_native_margin_model.cpp`, `tests/test_native_open_lots.cpp`). | K+A | L4 | F:D3 S:R5 | **Disagree:** F generic / S adapter-only. R5-1: liquidation price is generic; the Pine spelling stays A. **Lowered by R5 lane H-THIN:** the Pine host's second solve (`compute_liquidation_price`) is gone; `strategy.margin_liquidation_price` reads `native_liquidation_price()`, which the adapter configures with `NativeLiquidationLevelBase::RealizedOnly` (the Pine formula's closed-money solve, bit for bit at every bar of the lane's 1,021 seeded margin-call runs), and keeps only TradingView's tick spelling: floored for a long, ceiled for a short. A run whose margin calls are switched off declares no margin model, so it reads na. | | MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:5679, `apply_fx_opening_margin_slice` pine_adapter.cpp:5691) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1202 but no margin re-check. **Closed by audit lane N6:** `NativeMarginCheckKind::FxRoll` — a step of the declared curve is a check point of its own, measured at the unchanged price before the first point the new rate converts (`tests/test_native_margin_fx_roll.cpp`). The adapter refuses the point by kind, for a measured reason since R5 lane F7: TradingView's rollover is a broker-open checkpoint at the open price, now at any positive margin (`apply_fx_open_margin_slice`, on the one TradingView money / units rule every other checkpoint uses), while the roll point measures at the remaining path's adverse mark and rests the slice there — a carried 1x long on a step bar O 100 L 97 books 0.4116 @ 97 on the adapter's run shape and 0.4036 @ 99, measured a bar early, on a strict calendar, against TradingView's 0.3996 @ 100 (`tests/test_native_margin_hooks_twin.cpp` MG-FX). Both N6 follow-ups are closed (§4.4). **Completed by R5 follow-up lane E3:** the conversion INSTANT is now generic too — every check kind converts at its own cursor's rate (`account_currency_fx_at(cursor.point.effective_time_ms)`) for the requirement, the equity and the level, in batch and stream, so a step landing between a fill's cursor and the clock the engine presents no longer breaches at the fill (`tests/test_native_margin_fx_clock.cpp`; N6's local clock swap inside `fx_roll_margin_check` is removed as redundant). **Extended by R5 follow-up lane E21** to the opening gate's placement site (MG4), and the rate-explicit equity E3 restated beside the engine's accessor is now that accessor's one implementation, `BacktestEngine::marked_equity_at` engine.hpp:730 | K | L4 → N6 → E3 → E21 | O:D8 | (single-source) | -| MG10 | Max drawdown halt (`update_risk_state` pine_adapter.cpp:17483) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1301 **Closed:** `NativeRiskLimits::max_drawdown` native_run_spec.hpp:311, a `NativeLossLimit{value, percent}` measured against the running peak equity at three points of every script bar, blocking openings for the rest of the run (`tests/test_native_risk_limits.cpp`). | K | L9 | F:D6 O:E1 S:M6 | R5-9 | -| MG11 | Max consecutive losing days (`SourceDayLedger` pine_adapter.hpp:1419-1427; gate `max_cons_loss_days` pine_adapter.cpp:16486) | **no** — observation only `loss_days_limit` include/pineforge/source/market_admission.hpp:49 **Closed:** `NativeRiskLimits::max_consecutive_loss_days`, settled from each day's realized result at the next day's open (`tests/test_native_risk_limits.cpp`). | K | L9 | F:D6 O:E2 S:M6 | R5-9 | -| MG12 | Max intraday loss → flatten + block (`intraday_loss_breached` pine_adapter.cpp:17516, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:20858) | **no** — observation only `intraday_loss_limit` include/pineforge/source/market_admission.hpp:51 **Closed:** `NativeRiskLimits::max_intraday_loss` with `NativeRiskAction::FlattenAndBlock`, under which the kernel issues one `Flatten` of its own at the breach point (`RequestOrigin::KernelRisk`) and latches the block before that flatten settles (`tests/test_native_risk_limits.cpp`). | K | L9 | F:D7 O:E3 S:M6 | R5-9 | -| MG13 | Max filled orders per day (`IntradayCap` intraday_cap.hpp:84-263, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:20901) | **no** **Closed:** `NativeRiskLimits::max_fills_per_day` counts every applied execution of the day as it settles, whatever issued it (`tests/test_native_risk_limits.cpp`). | K+A | L9 | F:D8 O:E4 S:M7 | Count cap K; quota transfer, POOC deferral, TV close price A | -| MG14 | Risk-day boundary (`chart_day_key` pine_adapter.cpp:17380) | **no** — the calendar exists `session_day_ordinal` native_calendar.hpp:416-419, there is no risk-day ledger **Closed:** `NativeRiskDay::{SessionDay, CalendarDayInTimezone}` keys the risk day, and `native_risk_state()` native_host.hpp:1329 reads the whole ledger back — block and reason, day, fills counted, loss-day streak, peak equity, day-opening equity (`tests/test_native_risk_limits.cpp`). | K+A | L9 | O:E5 S:M7 | The chart-day shortcut stays A; the kernel keys on session / calendar day | -| MG15 | *(quirk)* TV margin call: money rounding `source_margin_money` pine_adapter.cpp:17548, 4× shortfall `4.0 * minimum` pine_adapter.cpp:17637, POOC chronology exception `resting_order` pine_adapter.cpp:20244 (the commissioned carried short; the competing-entry and rounded-money exceptions retired by R5 lane W5B-ENG-MARGIN-RESIDUAL, the private fixture set `margin_residual` w5b-pc-*), 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:18541, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:18541) | n/a | A | — | F:D2 F:G4 O:D4 S:M4 | R5-1. **Measured (R5 lane H-MEASURE)** against the kernel's own points on the same bars and orders, the kernel's hooks answering TradingView's money and slice (the private test `test_adapter_margin_schedule_differential`): a gap-open breach executes at the open, sized on the open's money, where the kernel's mark check rests at the path's adverse extreme and `CalculationOnly` executes at the calculation (M8); the pre-open slice parts from the kernel's `AfterApplied` point, and from `ab9714be`, on the one-contract band and the restore's `+1e-6` floor (M10; the frozen units it also parted on are TradingView's book since R5 lane PAR-MARGIN froze a default-percent stop above 100 % as eight `lab tv` tapes, the private tapes `pm-m10-*` of `margin_entry_bar`, show TradingView does); the entry bar of a leveraged opening, where four `lab tv` tapes (the private fixture set `margin_entry_bar`) and the kernel's `AfterApplied` point book the margin call at that bar's low, is admitted by `on_applied` since R5 lane PAR-MARGIN on the run shapes `ab9714be` checked it on (the pre-open slice's own opening keeps that slice's verdict), and the adapter books the tapes' rows (M7; H-MEASURE had recorded it as a divergence); R5 lane PAR-MARGIN-2 extended it to limit openings, `process_orders_on_close`, `calc_on_order_fills` and adds to a carried book, and made the kernel's post-fill point reach the low a limit was filled on the way down to, or a short's high on its way up (twenty-eight `lab tv` tapes, the private tapes `pm2-*` of `margin_entry_bar`). Under the magnifier the kernel's `IntrabarSample` point (R5 lane PAR-MARGIN) is admitted on a leveraged long, plain, under `process_orders_on_close` or `calc_on_order_fills`, and on a short outside `process_orders_on_close` (PAR-MARGIN-2), where TradingView books its call at the first lower-timeframe low that crosses -- at its own 2-minute intrabars on a 15-minute chart, which a model reproduces on all 20 magnified tapes (the private fixture set `intrabar_margin`); six tapes part on the one-minute samples the host feeds, a recorded divergence of the magnifier's sampling. A margin call books the nearest tick of its print, as TradingView's market fills do (12.105 books 12.11; the private fixture set `half_tick_rounding`, PAR-MARGIN-2). | -| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:34 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:48); `admission_allocation` pine_adapter.cpp:4546, `record_market_review` pine_adapter.cpp:21157) | n/a | A | — | S:R2 | (single-source) | +| MG10 | Max drawdown halt (`update_risk_state` pine_adapter.cpp:17495) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1301 **Closed:** `NativeRiskLimits::max_drawdown` native_run_spec.hpp:311, a `NativeLossLimit{value, percent}` measured against the running peak equity at three points of every script bar, blocking openings for the rest of the run (`tests/test_native_risk_limits.cpp`). | K | L9 | F:D6 O:E1 S:M6 | R5-9 | +| MG11 | Max consecutive losing days (`SourceDayLedger` pine_adapter.hpp:1422-1430; gate `max_cons_loss_days` pine_adapter.cpp:16498) | **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:17528, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:20870) | **no** — observation only `intraday_loss_limit` include/pineforge/source/market_admission.hpp:51 **Closed:** `NativeRiskLimits::max_intraday_loss` with `NativeRiskAction::FlattenAndBlock`, under which the kernel issues one `Flatten` of its own at the breach point (`RequestOrigin::KernelRisk`) and latches the block before that flatten settles (`tests/test_native_risk_limits.cpp`). | K | L9 | F:D7 O:E3 S:M6 | R5-9 | +| MG13 | Max filled orders per day (`IntradayCap` intraday_cap.hpp:84-263, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:20913) | **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:17392) | **no** — the calendar exists `session_day_ordinal` native_calendar.hpp:416-419, there is no risk-day ledger **Closed:** `NativeRiskDay::{SessionDay, CalendarDayInTimezone}` keys the risk day, and `native_risk_state()` native_host.hpp:1329 reads the whole ledger back — block and reason, day, fills counted, loss-day streak, peak equity, day-opening equity (`tests/test_native_risk_limits.cpp`). | K+A | L9 | O:E5 S:M7 | The chart-day shortcut stays A; the kernel keys on session / calendar day | +| MG15 | *(quirk)* TV margin call: money rounding `source_margin_money` pine_adapter.cpp:17560, 4× shortfall `4.0 * minimum` pine_adapter.cpp:17649, POOC chronology exception `resting_order` pine_adapter.cpp:20256 (the commissioned carried short; the competing-entry and rounded-money exceptions retired by R5 lane W5B-ENG-MARGIN-RESIDUAL, the private fixture set `margin_residual` w5b-pc-*), 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:18553, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:18553) | n/a | A | — | F:D2 F:G4 O:D4 S:M4 | R5-1. **Measured (R5 lane H-MEASURE)** against the kernel's own points on the same bars and orders, the kernel's hooks answering TradingView's money and slice (the private test `test_adapter_margin_schedule_differential`): a gap-open breach executes at the open, sized on the open's money, where the kernel's mark check rests at the path's adverse extreme and `CalculationOnly` executes at the calculation (M8); the pre-open slice parts from the kernel's `AfterApplied` point, and from `ab9714be`, on the one-contract band and the restore's `+1e-6` floor (M10; the frozen units it also parted on are TradingView's book since R5 lane PAR-MARGIN froze a default-percent stop above 100 % as eight `lab tv` tapes, the private tapes `pm-m10-*` of `margin_entry_bar`, show TradingView does); the entry bar of a leveraged opening, where four `lab tv` tapes (the private fixture set `margin_entry_bar`) and the kernel's `AfterApplied` point book the margin call at that bar's low, is admitted by `on_applied` since R5 lane PAR-MARGIN on the run shapes `ab9714be` checked it on (the pre-open slice's own opening keeps that slice's verdict), and the adapter books the tapes' rows (M7; H-MEASURE had recorded it as a divergence); R5 lane PAR-MARGIN-2 extended it to limit openings, `process_orders_on_close`, `calc_on_order_fills` and adds to a carried book, and made the kernel's post-fill point reach the low a limit was filled on the way down to, or a short's high on its way up (twenty-eight `lab tv` tapes, the private tapes `pm2-*` of `margin_entry_bar`). Under the magnifier the kernel's `IntrabarSample` point (R5 lane PAR-MARGIN) is admitted on a leveraged long, plain, under `process_orders_on_close` or `calc_on_order_fills`, and on a short outside `process_orders_on_close` (PAR-MARGIN-2), where TradingView books its call at the first lower-timeframe low that crosses -- at its own 2-minute intrabars on a 15-minute chart, which a model reproduces on all 20 magnified tapes (the private fixture set `intrabar_margin`); six tapes part on the one-minute samples the host feeds, a recorded divergence of the magnifier's sampling. A margin call books the nearest tick of its print, as TradingView's market fills do (12.105 books 12.11; the private fixture set `half_tick_rounding`, PAR-MARGIN-2). | +| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:34 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:48); `admission_allocation` pine_adapter.cpp:4546, `record_market_review` pine_adapter.cpp:21169) | n/a | A | — | S:R2 | (single-source) | ### 1.5 Calculation timing @@ -117,7 +117,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa |---|---|---|---|---|---|---| | CT1 | Calculate at script-bar close | **yes** — `on_native_bar` native_host.hpp:893, dispatched by `invoke_callback` native_execution_consumer.cpp:8149 (call site `on_native_recalculate` native_execution_consumer.cpp:8177) | K | — | F:E1 O:F1 | — | | CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2816) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:8593 | K | — | F:E2 O:F6 S:C1 | — | -| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:22252) | **yes** — `on_native_bar_open` native_host.hpp:889 runs before the open match native_execution_consumer.cpp:8023; `execute_current` legal native_host.hpp:1104 | K | — | F:E3 O:F2 | hazard → CT4 | +| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:22264) | **yes** — `on_native_bar_open` native_host.hpp:889 runs before the open match native_execution_consumer.cpp:8023; `execute_current` legal native_host.hpp:1104 | K | — | F:E3 O:F2 | hazard → CT4 | | CT4 | Partial-bar view / open-bar lookahead guard | **no** — the open hook receives the *complete* script bar: `invoke_bar_open_callback(engine, bar, point)` native_execution_consumer.cpp:7966 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:8017 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1073 is the lookahead-free bar so far, through the last path point consumed (a fill inside a segment reads it at the segment's origin), and `NativeOpenBarView::OpenOnly` native_run_spec.hpp:109 masks `on_native_bar_open`'s bar to `H = L = C = open` as presentation only — the complete bar is restored before the open match (`tests/test_native_calc_timing.cpp`). | K | L5 | F:E3 | (single-source) The adapter needs the full bar, so the guard is opt-in. | | CT5 | Raw input observation before aggregation | **yes** — `on_native_input` native_host.hpp:875, `NativeInputContext` native_host.hpp:773-778 | K | — | O:F3 | (single-source row) | | CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:727, `coof_recalculation_due` pine_scheduler_native.cpp:693, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:848; `begin_coof_recalc` pine_adapter.cpp:6676, `flush_coof_tail` pine_adapter.cpp:7100, `end_coof_recalc` pine_adapter.cpp:6733) | **partial** — `on_native_applied` native_host.hpp:938 fires mid-path after each fill native_execution_consumer.cpp:7870, native_execution_consumer.cpp:7870; commands are legal there `commands_allowed` native_execution_consumer.cpp:1843; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:6507, `remaining_path_delivery` native_order.cpp:2082-2090. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | @@ -125,7 +125,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | CT8 | Historical intrabar calculation (per lower-TF sub-bar / magnifier sample) | **no** — with an `IntrabarPath` the callback still fires once per script bar native_execution_consumer.cpp:8681 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:928 fires once after each retained lower-timeframe sub-bar's whole matching path (`tests/test_native_calc_timing.cpp`). | K | L5 | F:E6 O:F8 S:C4 | — | | CT9 | Intrabar (magnifier) matching path + path-order policy (`volume_weighted_cap` pine_adapter.cpp:2934) | **yes** — `IntrabarPath` native_run_spec.hpp:362-403, delivery `deliver_confirmed_script` native_execution_consumer.cpp:8252-8518; `NativePathOrder` native_run_spec.hpp:335-339 | K+A | — | F:E7 F:G5 O:F7 S:C4 | **Disagree:** S partial (it wants callback cadence → CT8). TV sampler choice is A: TradingView's own intrabars at its table's timeframe, each owned by the chart bar of its last minute, built from the finer feed (`tradingview_magnifier_bars` magnifier_intrabars.hpp:44, R5 lane MAG-INTRABAR). | | CT10 | Batch → warmup → realtime forward execution | **yes** — `stream_begin` engine.hpp:1772, `stream_push_bar` engine.hpp:1778, `stream_end` engine.hpp:1782, `NativeRunPhase` native_host.hpp:41, C ABI `strategy_stream_begin` c_abi.cpp:698 | K+A | — | F:E8 O:J3 S:C5 | **Disagree:** S partial. R5-1: the lifecycle is native; Pine trade-start suppression and realtime-tail / probe rules (`trading_window_active` pine_strategy_commands.cpp:15-21, `prepare_native_begin` pine_strategy_host.cpp:206-311) are A. S's "neutral tail + trade-window policy" is single-source and unassigned (§4 Q8). | -| CT11 | *(quirk)* COOF TV specifics: waypoint-only refill (`coof_next_waypoint` pine_adapter.cpp:6949), two-fills-at-open rule, script-state rollback (`PineScheduler` pine_scheduler_native.cpp:54-106; hooks `snapshot_script_state` engine.hpp:1333-1334), the first-open guard `kFirstOpenLoopGuard` pine_scheduler_native.cpp:848, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1961-1991 | n/a | A | — | F:E4 O:F5 S:C2 | R5-5: the adapter keeps its path | +| CT11 | *(quirk)* COOF TV specifics: waypoint-only refill (`coof_next_waypoint` pine_adapter.cpp:6949), two-fills-at-open rule, script-state rollback (`PineScheduler` pine_scheduler_native.cpp:54-106; hooks `snapshot_script_state` engine.hpp:1333-1334), the first-open guard `kFirstOpenLoopGuard` pine_scheduler_native.cpp:848, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1964-1994 | n/a | A | — | F:E4 O:F5 S:C2 | R5-5: the adapter keeps its path | ### 1.6 Higher timeframes and auxiliary feeds @@ -143,13 +143,13 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | FP1 | Trigger geometry on the modeled path; fill at the level or at the gap price; market-if-touched | **yes** — `native_matching` native_execution_consumer.cpp:6579, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:7169; `Limit::fill_through` native_order.hpp:239-244 enforced where the matched price is limit-protected (`protect_limit` native_execution_consumer.cpp:7165) and re-checked after host terms (`InvalidTerms` native_execution_consumer.cpp:5834) | K | — | F:G3 O:H1 O:H2 O:H3 | — | -| FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:652 applied once native_execution_consumer.cpp:5470, clamped by `protect_limit` native_execution_consumer.cpp:7165 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:15170) is A | +| FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:652 applied once native_execution_consumer.cpp:5470, clamped by `protect_limit` native_execution_consumer.cpp:7165 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:15182) is A | | FP3 | Commission: percent / cash per unit / cash per execution (`fee_kind_for` pine_adapter.cpp:669) | **yes** — `NativeFeeKind` native_run_spec.hpp:19-23; `quote_execution_commissions` engine_execution.cpp:1690 | K | — | F:G2 O:H8 S:F1 | — | -| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:274, `source_bar_fill` pine_adapter.cpp:15170, `directional_tick` pine_adapter.cpp:327) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:7147; `bar_fill_price` engine.hpp:772 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:7320; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_sizing_price_rule.cpp` quantizes the fill basis under `HalfUp`; `tests/test_native_acid_composite.cpp` books the `Directional` fills on the 0.25 ladder; the private test `test_native_price_grid` re-ran the pair under the overlay). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | +| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:274, `source_bar_fill` pine_adapter.cpp:15182, `directional_tick` pine_adapter.cpp:327) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:7147; `bar_fill_price` engine.hpp:772 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:7320; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_sizing_price_rule.cpp` quantizes the fill basis under `HalfUp`; `tests/test_native_acid_composite.cpp` books the `Directional` fills on the 0.25 ladder; the private test `test_native_price_grid` re-ran the pair under the overlay). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | | FP5 | Trigger tested against the tick-quantized bar | **no** — the kernel tests raw prices against raw levels **Closed:** `NativePriceGrid::QuantizeFillsAndTriggers` additionally tests and re-validates every activation against the tick-quantized path, one rule for every trigger kind (`tests/test_native_trail_best_seed.cpp` puts an off-ladder trail seed on the ladder; `tests/test_native_acid_composite.cpp` runs a trail's best and its exit level on it; the private test `test_native_price_grid` re-ran the pair under the overlay). | K+A | L8 | O:H5 F:G4 S:F3 | R5-3: the optional quantized trigger test is K; TV's exact half-tick rule (`source_trigger_threshold` pine_adapter.cpp:384) is A on top. | | PG | The Pine adapter on the kernel price grid: raw levels submitted, `project()` declaring `QuantizeFillsAndTriggers` + `HalfUp`, `source_trigger_threshold` deleted (lane R7; re-tested after L8b by gap lane N13) | **measured-infeasible for the Pine adapter** — B1 (the sub-tick cursor-print abort) is closed by L8b's `ActivationGrid`: 0 aborted runs under the trial (R7: 9 zero-offset-trail tapes + 6 POOC panels; now `test_zero_offset_trail_rides_l4c` 449/450, `test_pooc_short_close_tick_l4d` 183/183). B2 (TradingView quantizes per order kind, the grid is one run-wide rule) moved 30 pinned checks in 4 units when N13 last ran the trial, before lanes E14 and E16, with no adapter-side remedy: `test_coof_market_limit_recross_l4c` 24, `test_stop_tick_rounding_l4d` 3, `test_adapter_grid_relower` 2, `test_zero_offset_trail_rides_l4c` 1; 12 more checks in 3 units have an adapter-side cause (`test_trail_fill_snap_l4c` 2, `test_native_margin_hooks` 2, `test_adapter_brackets_relower` 8 book digests). Corpus: 5/312 probes differ, in the engine-only entry-incarnation column only (every probe runs a 0.01 tick on an on-grid feed, so it cannot arbitrate) | A | waived (R7, N13) | — | **Waiver.** No class of triggers is byte-identical on its own: the grid is a run-wide switch and a per-kind mask would spell TradingView's inconsistency into the kernel (not generic, R5-3). The adapter stays on `NativePriceGrid::None` and keeps `source_trigger_threshold` and its call sites; native-engine.md "The Pine adapter's grid re-lowering is waived". **Native-only by ruling (audit lane P6): §3.6.2**, with `examples/native/native_price_grid_strategy.cpp` and its C twin as the feature's hosts. | | FP6 | Account-currency FX | **partial** — scalar `account_fx` + immutable `NativeFxCurve` native_host.hpp:1202 in batch; the stream then refused a non-empty curve (today only tick-driven input is refused under one, `refuse_fx_curve_tick_input` native_execution_consumer.cpp:9934). **Closed by audit lane N6:** the declared curve is the run's immutable FX epoch and a confirmed-bar stream runs under it exactly as a batch does (stream twin in `tests/test_native_margin_fx_roll.cpp`); no realtime rate ingestion and no tick-driven input under a curve are stated as permanent limits with their reasons (native-engine.md, "Stream FX"). **Re-examined by R5 follow-up lane E3 and KEPT:** the tick refusal was never only about the margin check — it guards the SCRIPT-VISIBLE conversions (`open_trade_profit()`, `marked_equity()`, the equity curve), which read `active_account_currency_fx()`, i.e. the presented clock the observation hook has not yet moved (re-measured: a step at T+90s left the T+100s and T+110s prints converting at the pre-step rate). E3's cursor rule reaches the margin model only, so the refusal stands and is pinned in `tests/test_native_margin_fx_clock.cpp` | K | unassigned → N6 (E3: refusal kept) | F:D9 S:F4 | **Disagree:** F out of scope ("O7 clock") / S its P10, a stream-safe FX epoch. No R5 lane → §4 U4 (answered by N6: S's "same curve in batch and stream", without a new clock or callback). | -| FP7 | *(quirk)* TV tick conventions: half-tick threshold `source_bar_fill_tick` pine_adapter.cpp:295 (used `source_trigger_threshold` pine_adapter.cpp:384, `exit_limit_trigger` pine_adapter.cpp:14364), `source_bar_fill_tick` pine_adapter.cpp:295, raw-vs-booked spelling `source_same_point` pine_adapter.cpp:313, margin-call fill pricing `source_margin_fill_price` pine_adapter.cpp:17772 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:14846) | A | — | F:G4 O:H5 S:F3 | R5-1, R5-3 | +| FP7 | *(quirk)* TV tick conventions: half-tick threshold `source_bar_fill_tick` pine_adapter.cpp:295 (used `source_trigger_threshold` pine_adapter.cpp:384, `exit_limit_trigger` pine_adapter.cpp:14364), `source_bar_fill_tick` pine_adapter.cpp:295, raw-vs-booked spelling `source_same_point` pine_adapter.cpp:313, margin-call fill pricing `source_margin_fill_price` pine_adapter.cpp:17784 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:14858) | A | — | F:G4 O:H5 S:F3 | R5-1, R5-3 | ### 1.8 Excursion, metrics, report, hash @@ -210,7 +210,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | f | Dead legacy stream path | **Done.** `stream_feed_input_bar` and `dispatch_source_stream_script_bar` are both gone — neither name is in `src/` or `include/` — and the virtual left with the epoch that carried it. `stream_state_hash` engine.hpp:1791 stays, and the declaration/definition pairing is still enforced by `scripts/test_native_source_guard.py` | NEUTRAL (removing the virtual changes the vtable → shared bump) | Delete both | O; F asked (its Q6) | | g | Legacy path resolver | **Done, and then deleted.** `src/engine_path_resolve.cpp` is 118 lines with five functions: `bar_path_uses_high_first`, both `first_touch_position` overloads, `fill_bar_path_points_ordered`, and `BacktestEngine::declare_opened_lot_entry_bar_mask`. Ten test files still mention `resolve_exit_path_fill` in historical comments; the resolver body and test-only oracle are gone. | NEUTRAL (pure move; verify with the link gate) | Keep the generic path helpers in the kernel TU; the old source resolver is not a live counterpart | F (single-source) | | | h | `pine_*`-named generic API | `timeframe_time` session_time.hpp:35-211 (live, e.g. `session_in_market`, which generated code reaches through `pine_session_ismarket` session_time.hpp:263), `str_utils.hpp`, `deterministic_random` math.hpp:9, `tz_util` timezone.hpp:8-30; the guard bans `pine_*` only inside `IDENTIFIER_ROOTS` (test_native_source_guard.py:34, test_native_source_guard.py:34-42) | NEUTRAL, M | Neutral aliases; `pine_*` kept as deprecated inlines for codegen. Large blast radius in generated code. **Status (L11 + N14): done** — session_time / str_utils / math / timezone in L11; N14 applied the same treatment to the two public matrix types: `PineMatrix` → `NumericMatrix`, `PineGenericMatrix` → `GenericMatrix`, old spellings kept as exact aliases (no version script pins either name) | O (single-source) | -| i | `pf_pending_order_v1_t` Pine-named fields | `tv_carry_qty` pending_order_mirror.hpp:73, `pine_birth_reach` pending_order_mirror.hpp:135, `pine_exit_activation_owner_cycle_at_birth` pending_order_mirror.hpp:147-156, `pine_frozen_market_instruction_kind` pending_order_mirror.hpp:164-169; enumerated by `PF_PO_FIELD` pending_order_mirror.cpp:144-178; written only by `pine_birth_reach` pine_adapter.cpp:27063 | 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:27075 | no change | Freeze (ABI). The neutral view arrives beside it (L7 / L13) | O (single-source) | | j | Pine-shaped lot flags | **Mostly done.** `market_pyramid_add` moved to the adapter side table the row asked for — `market_pyramid_adds_` src/source/pine_state_hash.cpp:692, whose own comment records that it was `PyramidEntry::market_pyramid_add` — and `ordinary_market_open`, `pooc_terminal_market_entry`, `ordinary_stop_open`, `bracket_slot_shadowed` and `entry_path_position` are all deleted. What is left on the lot is the intrabar-fill excursion mask, `skip_entry_bar_high` / `skip_entry_bar_low` engine.hpp:154, hashed (`skip_entry_bar_high` engine_state_hash.cpp:74) and, since R5 lane E6, written by no host at all: a host that owns lot excursions declares where its fill sat (`declare_opened_lot_entry_bar_mask` engine_path_resolve.cpp:95; the Pine host did until R5 lane H-THIN left its excursions to the kernel's sampler) and the kernel derives the pair | HASH-VISIBLE | Move the live flag to an adapter side table keyed by `entry_incarnation` (the adapter already keeps `PlacementTable` pine_adapter.hpp:355-675) and fold it through `hash_source_extension`; delete the dead ones; update the hash-coverage waivers. Whether a kernel TU beyond the hash reads them is unverified (O) | F O S | | k | `margin_call_enabled_` | **Done.** The generic kernel model is enabled by `NativeRunSpec::margin` native_run_spec.hpp:672. TradingView-specific toggling remains `source::PineStrategyHost::set_margin_call_enabled` pine_strategy_host.hpp:511; the frozen C symbol remains source-owned. | HASH-VISIBLE (R5-8) | Keep the source setter in the adapter; the kernel toggle is the presence of `spec.margin` | F O | | l | Kernel decodes adapter strings | **Done.** `closed_trade_close_cause` engine_trade_accessors.cpp:158 branches on no string at all: it reads `Trade::open_at_end`, then the typed `execution::CloseCause` the closer recorded on the row, then `exit_from_bracket`. A kernel-originated liquidation or risk flatten states its own cause through the settling fill, so a bare host reads `3` / `4` where the retired string derivation answered `1`. The public C contract it implements is `strategy_closed_trade_close_cause` pineforge.h:1309 | HASH-VISIBLE (adds durable state) | Kernel `enum class CloseCause` recorded on the `Trade` at settlement (the kernel sets `MarginCall` when L4 liquidates; a source virtual covers adapter causes). C numbers unchanged. After L4 | O (single-source) | @@ -326,7 +326,7 @@ Union of F §2.vi (9), O §2.vi (5 + 1) and S §2.6 (7), deduplicated. S had no | 4 | HTF "comes from the kernel feed `set_native_security_feed` / `prepare_native_security_feeds`" (ADR L84-89) | Not usable by a bare host: a partial boundary, not an absent kernel. `prepare_native_security_feeds` is protected with one caller; `configure_security_evaluators` has one caller; the public setter is inert pre-run and refused in-run | `prepare_native_security_feeds` engine.hpp:1640, `configure_security_evaluators` engine.hpp:1323, `set_native_security_feed` engine.hpp:1732, `guard_native_mutation` engine_aux_security.cpp:83 | F O S | | 5 | "`engine_aux_security.cpp` includes `source/pine_strategy_host.hpp`" (ADR L129-132) | False at `73817c1`: its includes are `engine_internal.hpp`, `ta.hpp` and std headers (*fresh* re-read; the run-failure codes added `run_failure.hpp`, a kernel header). No kernel TU includes a source header | `sha256:560a7f68b243d921382a69ed0390c398f28370ac066b84f30b5f96cbb385fb60` engine_aux_security.cpp:6-14 | F | | 6 | "Kernel TUs reference adapter types (`source::StrategyOverrides` …)" (ADR L129-131) | True only as a forward-declared opaque pointer the kernel never dereferences; the independence checker whitelists exactly that symbol | at `73817c1`; overtaken by `3f4fff5b` (§2.ii a), after which the rich begin carries `overrides_opaque` native_host.hpp:758 and the independence checker whitelists nothing | F O | -| 7 | `engine.hpp` hosts "the ten-significant-digit money rule, KI-62, the `strategy.close` batching + entry-id ledger, a TradingView margin-call toggle" (ADR L131-134) | Two of four survive as code. Money rule: a comment block; the arithmetic is in the adapter. KI-62: orphaned comments + one live, hashed lot flag. Close batching + id ledger: gone from the kernel (the ledger is in the adapter; the old names survive only in comments). Live: `margin_call_enabled_`, the KI-62 flag and — unmentioned by the ADR — string-sentinel decoding of adapter comments | the money-rule block was lines 927-961 of `engine.hpp` and R5 lane E6 deleted it (lines 73-180 were and are live code), the arithmetic `source_money_round` pine_adapter.cpp:558 and its twin `tv_money_round` pine_policy_support.hpp:9-15; `skip_entry_bar_high` engine.hpp:153, `skip_entry_bar_low` engine_state_hash.cpp:74, `declare_opened_lot_entry_bar_mask` engine_path_resolve.cpp:95; the ledger `close_logical_units_` pine_adapter.hpp:2580; `closed_trade_close_cause` engine_trade_accessors.cpp:158. **Overtaken:** the margin-call toggle and the string decoding are both gone — see ADR-0001, "Current state vs. target" | F O | +| 7 | `engine.hpp` hosts "the ten-significant-digit money rule, KI-62, the `strategy.close` batching + entry-id ledger, a TradingView margin-call toggle" (ADR L131-134) | Two of four survive as code. Money rule: a comment block; the arithmetic is in the adapter. KI-62: orphaned comments + one live, hashed lot flag. Close batching + id ledger: gone from the kernel (the ledger is in the adapter; the old names survive only in comments). Live: `margin_call_enabled_`, the KI-62 flag and — unmentioned by the ADR — string-sentinel decoding of adapter comments | the money-rule block was lines 927-961 of `engine.hpp` and R5 lane E6 deleted it (lines 73-180 were and are live code), the arithmetic `source_money_round` pine_adapter.cpp:558 and its twin `tv_money_round` pine_policy_support.hpp:9-15; `skip_entry_bar_high` engine.hpp:153, `skip_entry_bar_low` engine_state_hash.cpp:74, `declare_opened_lot_entry_bar_mask` engine_path_resolve.cpp:95; the ledger `close_logical_units_` pine_adapter.hpp:2583; `closed_trade_close_cause` engine_trade_accessors.cpp:158. **Overtaken:** the margin-call toggle and the string decoding are both gone — see ADR-0001, "Current state vs. target" | F O | | 8 | "No kernel-level C API. `c_abi.cpp` exports only version/descriptor symbols" (ADR L136-138) | 80 runtime `PF_API` symbols: native configure, the 12-symbol stream family, pending-order views, broker-state hash, security feeds, FX curve, reports. The 80 compiled-strategy runtime symbols in `c_abi.cpp` are distinct from the native C host API in `native_c_host.cpp`: the latter exposes submit, replace, cancel and a callback table to a C host. | `sha256:733786ed81c9137223b98e1887e28d01afdb3c64ba5e8ca52ce5161f2caf3f75` c_abi.cpp:325-1013, `sha256:6327f1829019fdd172cd76a96e3d8c9c0af1610ad0dc9878640dda0ca21a686b` check_c_abi_runtime.py:20-81, c_abi.cpp:946, `sha256:5ff4c65a56410fe29b21cd68bc21ac87bdb34b77344e4f75f90efa8887fe48e8` c_abi.cpp:664-788, `sha256:051fe087eb9d30e9960dc4f0ab3f7cc837531ebeaa71b5871a3f1cdc22fb37c2` c_abi.cpp:566-641, `sha256:f18d46ac323234f7e3082367d30840b704b8a7548f7ae95ee8cff8714f29fd50` c_abi.cpp:545-562, `sha256:6a9ef4e688f33dc10ea8297d9eaa8144ea7923ef6ee6f8fe9ff20f6af4e9acb3` c_abi.cpp:881-917, c_abi.cpp:1013 | F O S | | 9 | "No examples. `PINEFORGE_BUILD_EXAMPLES` is 'none yet'" (ADR L139) | The option is inert, but two Pine-free `NativeStrategyHost` examples exist, are compiled by the independence checker and tested through the live runner, only under `PINEFORGE_BUILD_LIVE_RUNNER`. Wording: "no top-level examples target" | at `73817c1` (overtaken; ADR-0001, "Current state vs. target") | F O S | | 10 | Follow-ups (1)-(4) (ADR L168-170) | Add native reporting, sizing, margin, HTF — and, per R5, calc-timing, price grid and order ergonomics. Without them "usable without Pine" is a build-system claim, not a feature claim | §3.5 | F | @@ -352,7 +352,7 @@ the campaign's own anchor audit found that the first thing to rot: ### 3.1 Parity discipline for every lane (R5-10) — stated once, referenced per lane -- **(a) NEUTRALITY.** Every new behaviour is opt-in by a spec field or a new request kind. The adapter's `project()` (pine_adapter.cpp:2524-2791) never sets the field and never emits the kind, so adapter runs are byte-identical by construction. The four feed-shape fields are folded into `hash_spec` unconditionally today, including defaults (native_execution_consumer.cpp:238-260), so even a defaulted new field would move every continuation hash; the precedent for a conditional digest is `precommit_digest_` (native_execution_consumer.hpp:1942-1946). Each lane adds a test that a default spec hashes to the pre-change constant. Every new durable field is hashed (`check_broker_state_hash_coverage.py`, fail-closed, waivers in `broker_state_hash_waivers.txt`). Proof per PR: `scripts/run_corpus.sh` + `scripts/verify_corpus.py` locally, then ONE campaign sweep on the final tree, run by the maintainers (fixed population, 4190 probes when this plan was written, zero tolerance on the hard band — native-refactor-progress.md:42-48; a fresh exact-HEAD `PASS` verdict is required, `sha256:59adeba8aabaefa740633d52e9216b49065b28b397b87f22c57ad5e19f2948c2` AGENTS.md:144-145). A lane that cannot be shown neutral by construction does not merge. +- **(a) NEUTRALITY.** Every new behaviour is opt-in by a spec field or a new request kind. The adapter's `project()` (pine_adapter.cpp:2536-2803) never sets the field and never emits the kind, so adapter runs are byte-identical by construction. The four feed-shape fields are folded into `hash_spec` unconditionally today, including defaults (native_execution_consumer.cpp:238-260), so even a defaulted new field would move every continuation hash; the precedent for a conditional digest is `precommit_digest_` (native_execution_consumer.hpp:1942-1946). Each lane adds a test that a default spec hashes to the pre-change constant. Every new durable field is hashed (`check_broker_state_hash_coverage.py`, fail-closed, waivers in `broker_state_hash_waivers.txt`). Proof per PR: `scripts/run_corpus.sh` + `scripts/verify_corpus.py` locally, then ONE campaign sweep on the final tree, run by the maintainers (fixed population, 4190 probes when this plan was written, zero tolerance on the hard band — native-refactor-progress.md:42-48; a fresh exact-HEAD `PASS` verdict is required, `sha256:59adeba8aabaefa740633d52e9216b49065b28b397b87f22c57ad5e19f2948c2` AGENTS.md:144-145). A lane that cannot be shown neutral by construction does not merge. - **(b) TWIN.** One chosen probe per lane runs through the adapter AND a hand-written `NativeStrategyHost` on the same bars and spec. Trade lists are diffed **by identity** (entry / exit time, price, qty, pnl, commission), never by trade number; then event, ownership and hash detail (S's D → E order). A difference is allowed only where a named TV-quirk row of §1 is active, and each one is itemized. Because 430 test files drove the adapter and 14 the native host when this plan was written (O; on this tree 543 `tests/*.cpp` name `source::PineStrategyHost` and 110 name `NativeStrategyHost`), every lane also adds native-only tests: a lane is not accepted on the corpus alone, nor on a unit test alone (S). - **(c) EPOCH BUDGET.** Three inline-namespace epochs move — the request epoch, the host epoch and (*fresh*, not in the inputs) the run-spec epoch, which every new spec field moves. They stood at `native_order_v5`, `engine_script_run_v17` and `native_run_spec_v2` when this plan was written; on this tree they are `native_order_v7` (native_order.hpp:25), `engine_script_run_v19` (native_host.hpp:20) and `native_run_spec_v3` (native_run_spec.hpp:15): the budget below was spent exactly once, and the v19 value epoch (R5 lane V19-A) took the host epoch one step past it. One bump each per release batch: `native_order_v6` = L3 + L7 + L4's origin / event; `engine_script_run_v18` = L4's host hooks + L5 + L6 virtuals + L2's `hash_host_extension` + §2.ii a, f; `native_run_spec_v3` = the spec blocks of L2, L4, L5, L6, L8 (L9 if it ships in the batch). A lane that lands before its batch closes stages behind the open epoch, so codegen-built strategies rebuild once per batch. Epoch lanes are not neutral-refactor lanes: the brief lists the header extension explicitly; the frozen C++ ABI fixtures (`tests/fixtures/native_cpp_abi/host-*`) and the ABI checkers (`check_native_cpp_abi.py`, `check_settlement_cpp_abi.py`) move with them; `variant_size` pins change (`OrderIntent` native_order.hpp:229, `CommandEvent` native_order.hpp:1308). Every new test unit compiles first against the frozen previous-epoch header closure (fail-before, `CLAUDE.md`). - **Worker verification** (`CLAUDE.md`): `ci_preflight.py`, then `ci_verify.py release`; plus `check_c_abi_runtime.py` and `check_native_include_independence.py`. No campaign sweep from a worker. @@ -417,7 +417,7 @@ closure markers and `scripts/check_design_inventory.py`). | L6 | `NativeTimeframeSubscription`, `on_native_timeframe_bar`, `native_series_bar` → A.5, §2.iv | `subscriptions` empty for the adapter; registration-gated pump; `engine_security.cpp` and pine_strategy_host.cpp:1091-1175 untouched; corpus diff before / after (O: highest parity risk of any lane) | HTF-filter probe on a 24x7 symbol: exact (F); `"D"` over a 15m feed equals a `TimeframeAggregator` baseline bar for bar (O); installed-feed and no-feed cases (S) | | L7 | `native_working_requests()`, `cancel_all()`, `cancel_where(label)`, `TriggerAnchor`, `TrailTicks`, zero offset, `ReplaceOptions`; toolkit `submit_bracket`, `OrderBook` → A.6 | new kinds unused by the adapter; anchor defaults to `Absolute`; the adapter's `tick * 0.5` sentinel keeps working | bracket + trailing probe through the builder: exact where TR5 / FP7 are inactive (on-grid levels) (F); the zero-offset trail test asserts the run no longer fails (O); parent-limit + stop sibling, parent rejection, replacement, parent-cycle revival (S) | | L8 | `NativePriceGrid { None, QuantizeFills, QuantizeFillsAndTriggers }`, `NativeGridRounding` → A.7 | `None` for the adapter, permanently — **native-only by ruling, audit lane P6: §3.6.2** | a host with `QuantizeFills` books on-grid prices; a second test pins that `QuantizeFillsAndTriggers` still differs from TV's half-tick threshold (FP5) (O) | -| L9 | `NativeRiskLimits`, `NativeRiskEvent`, `MatchRejectReason::RiskLimit` → A.8 | `risk` unset for the adapter; its ledger (`SourceDayLedger` pine_adapter.hpp:1423) untouched — **retained after measurement, audit lane N12; native-only by ruling, audit lane P6: §3.6.1** | risk probes: same halt bar (F); each limit + the day-boundary basis (O); breach, forced close, cancellation, next-day reset (S) | +| L9 | `NativeRiskLimits`, `NativeRiskEvent`, `MatchRejectReason::RiskLimit` → A.8 | `risk` unset for the adapter; its ledger (`SourceDayLedger` pine_adapter.hpp:1426) untouched — **retained after measurement, audit lane N12; native-only by ruling, audit lane P6: §3.6.1** | risk probes: same halt bar (F); each limit + the day-boundary basis (O); breach, forced close, cancellation, next-day reset (S) | | L10 | `PINEFORGE_EXPORT_NATIVE_STRATEGY(Class)` (§2.v) | build-only | the examples run in ctest; the independence checker compiles the relocated examples | | L11 | — (renames / moves, §2.ii a-i) | rename / move only; hashed enumerator values pinned; sweep unchanged | — | | L12 | — (§2.ii j-m) | **not neutral**: coordinated sweep, waiver updates, hash-domain plan (the `"pineforge-broker-state/v17"` literal of that wave, since moved to `"pineforge-broker-state/v19"` engine_state_hash.cpp:41 and pinned to one occurrence by check_broker_state_hash_coverage.py:223) | trades identical, hashes re-baselined once | @@ -524,11 +524,11 @@ line numbers are re-anchored to this tree (the measurement ran on 683a82f); kern | Pine rule | adapter mechanism | kernel counterpart | class | measured divergence (test scenario) | ruling | |---|---|---|---|---|---| | `strategy.risk.allow_entry_in` | `risk_.direction`; `project()` already declares `spec.allowed_open_directions` (pine_adapter.cpp:2859); the forbidden opposite entry is reshaped to `CloseOpposite` in the adapter's own terms branch | per-opening cap `allowed_open_directions` | (i)+(ii), **already re-lowered** | none to measure: the cap is the kernel's, the close-only reshaping is the retained TV policy | done before N12; nothing to move | -| `strategy.risk.max_position_size` | precommit refuses a flat/same-side ENTRY at its fill when the LIVE book already holds ≥ the limit (`max_position_size` pine_adapter.cpp:16493; the retired legacy rule is `git show ab9714be:src/source/pine_risk.cpp`, lines 115 and 2531) | `max_abs_units`: refuses a fill whose RESULTING book would exceed it | (iv) | `PS`: two-unit entries against 3 — adapter admits the second (live 2 < 3, book 4) and refuses the third; kernel refuses the second (`MaxAbsUnits` at bars 1, 2), book 2 | retained: a pre-fill-book gate is a TV emulation fact, not a generic cap | -| `strategy.risk.max_drawdown` | `update_risk_state(bar.close)` once per script bar at the close mark (`update_risk_state(bar.close)` pine_adapter.cpp:17483, `update_risk_state` pine_adapter.cpp:17483): peak / running max drawdown at close marks, `>=` exact, percent of the current peak; the latch `risk_.halted` gates flat/same-side entries at precommit (`risk_.halted` pine_adapter.cpp:16485), never an opposite entry | `max_drawdown`: current drawdown vs percent-of-peak, at all three points; `BlockOpenings` refuses every opening | (ii) cadence + (iv) scope | `MG10-a`: bar 1 opens 600 under the entry and closes 100 under — kernel breaches at the open mark and refuses bar 2's add (book 100), adapter admits it (book 200). `MG10-b`: latched at bar 1's close, the adapter still reverses twice (`L→S` −600, `S→L`), the kernel refuses both reversals (`RiskLimit` at bars 2, 3) and holds. Corpus: `ta-closedtrades-risk-introspection-01` (20 % of peak) identical 1502/1502 rows with the kernel seeded — the rule never fires there | retained: the latched-reversal exemption cannot be composed on a kernel block; a close-only cadence knob alone would not close it | -| `strategy.risk.max_cons_loss_days` | `SourceDayLedger` (pine_adapter.hpp:1419-1427): +1 per losing TRADE on a new chart day, reset to 0 by any winning trade at its fill (`consecutive_loss_days` pine_adapter.cpp:25723); gated immediately at precommit and latched at the close | `max_consecutive_loss_days`: a day's NET realized result settles the streak when the next day opens | (iv) | `MG11-a` (loss; win then loss; loss): adapter latches at day 3's first loss (4 rows), kernel's netted streak never reaches 2 (6 rows, no event). `MG11-b` (loss then smaller win, twice): adapter resets daily (5 rows), kernel blocks at day 3's open (4 rows, one event). No corpus probe declares the rule | retained: an order-dependent per-trade streak is not a generic day result | -| `strategy.risk.max_intraday_loss` | day-open equity at the day's first bar (`intraday_start_equity` pine_adapter.cpp:22492); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:20858), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:20890, `submit_intraday_loss_close` pine_adapter.cpp:20858), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:24680); epsilon `1e-9·max(1,|limit|)`; ticket `"Close Position (Max intraday Loss)"` → `CloseCause::RiskLimit`; the block refuses ALL placements for the chart day (`entry`/`order`/`close`/`close_all`/`exit`, `intraday_loss_orders_blocked` pine_adapter.cpp:17478, `:17478`, `:17478`, `:17478`, `:17478`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:14579) | `max_intraday_loss` at the three points; `FlattenAndBlock` closes `AsPresented` under `"__kernel_risk__"` / `"Risk limit"`, blocks openings only, leaves working orders | (ii) ticket, day basis + (iv) path check, placement scope, unbooked fill, epsilon | `MG12-a`: day 2's first bar 100→85→95 — adapter closes at the extreme (85, −1500, cause 4) and refuses the day's later entry, kernel measures 0 at the open and 500 at the close, never breaches, ends with 300 units. `MG12-b`: breach at a bar open inside the day — same money on both sides (100 @ 88, −1200) under different tickets; the adapter withdraws a resting limit entry, the kernel leaves it working and it fills on day 3. Both sides mark the day's opening equity at its first bar: a gap there is not an intraday loss for either | retained: the path-extreme check is the rule's substance and the kernel has no path evaluation point; a risk ticket field and a `PathAdverseExtreme` check kind would be generic additions (mirroring `NativeMarginModel`) but would still leave the placement scope, the unbooked fill and the epsilon | -| `strategy.risk.max_intraday_filled_orders` | `compat::pine::IntradayCap` + `IntradayOrderBudget` (243 + 94 lines): charged slots per risk day, quota transfer, POOC deferral, noop-market skip, the close at the fill price or the bar's better extreme (`post_dispatch`), latch → all placements denied (`cap_placement_denied` pine_adapter.cpp:17474); ticket `"Close Position (Max number of filled orders in one day)"` → `CloseCause::FillCap`; day = `session_trading_day_index` for a `HHMM-HHMM` session, else the chart day (`chart_day_key` pine_adapter.cpp:17380, `mday·100+month` in the CHART timezone) | `max_fills_per_day`: applied fills counted as they settle, evaluated at the three points; `FlattenAndBlock` at the drain's cursor; `SessionDay` / `CalendarDayInTimezone` on the SPEC timezone | (i) count + (ii) ticket + (iv) close price, charged-slot budget, placement scope, chart-day key | `MG13`: the second fill reaches the cap on a bar that closes above its open — adapter closes at that bar's HIGH (103) under its ticket, kernel at the close (101) under its own. `MG14`: chart timezone `America/New_York` over a UTC symbol — bar 4 (04:00 UTC) is a new chart day for the adapter and the same UTC day for the kernel. Corpus, kernel seeded with the declared cap: `cap-risk-gates-allow-max-intraday-01` identical 1464/1464 (the third fill of each day flattens the book, so block and latch coincide); `cap-max-intraday-filled-orders-isolate-01` 3840 of 3916 rows differ from row 77 (2025-04-08); `cap-gatekeeper-intraday-risk-01` 312 of 604 differ from row 293 (2025-10-25); `composite-bracket-cap-range-pending-stop-01` 2370 of 2384 differ from row 17 (2025-04-02 02:45), two extra rows | retained: the count alone is expressible, but the kernel's block would refuse fills the transfer admits and the close price is TV's; a report-only action would give the adapter a count it cannot use (charged slots ≠ applied fills) | +| `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:16505; 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:17495, `update_risk_state` pine_adapter.cpp:17495): 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:16497), 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:1422-1430): +1 per losing TRADE on a new chart day, reset to 0 by any winning trade at its fill (`consecutive_loss_days` pine_adapter.cpp:25735); 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:22504); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:20870), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:20902, `submit_intraday_loss_close` pine_adapter.cpp:20870), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:24692); 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:17490, `:17490`, `:17490`, `:17490`, `:17490`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:14591) | `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:17486); 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:17392, `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, @@ -1136,7 +1136,7 @@ moving a single value, but not from inside lane F9's files: (`has_full_entry_bracket` pine_adapter.cpp:11425). Full-table scans rewrite rows as well (`suspend_brackets_for_reversal` pine_adapter.cpp:1965, `revive_brackets_after_margin` pine_adapter.cpp:2200, - `preserved_by_close_all` pine_adapter.cpp:14647), and + `preserved_by_close_all` pine_adapter.cpp:14658), 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. @@ -1600,7 +1600,7 @@ the rule gives 9.9 + 9.89). | # | Risk | Why it bites | Containment | Src | |---|---|---|---|---| | E1 | **Spec hashing.** `hash_spec` folded every field unconditionally (native_execution_consumer.cpp:234-256); every block added since folds only when set | Any new field, even defaulted, changes every continuation / broker-state hash, adapter runs included | Fold new blocks only when set, as a tagged extension (precedent `precommit_digest_` native_execution_consumer.hpp:1942); test that a default spec hashes to the pre-change constant | F | -| E2 | **Margin.** The TV margin call is built host-side from kernel primitives (a Stop-triggered `Reduce` bound to openings, or `execute_current`, which survives in `submit_margin_call_slice` pine_adapter.cpp:17946); its chronology lives at `schedule_margin_call_path` pine_adapter.cpp:20224 and its excursion sample differed by chronology (the Pine host's own excursion model, retired by R5 lane H-THIN: the kernel's sampler books it) | A kernel model under an adapter run would liquidate twice or reorder settlement; a checkpoint that reduces mid-path changes lot state exactly where the adapter's chronology lives | Superseded by R5: the adapter declares `spec.margin` — maintenance-only, so no opening is decided twice (`AdmitWithHostMargin`, MG4) — and the kernel owns the mechanism (check points, resting liquidation, receipt) while the adapter's hooks answer TradingView's scheduling, money and slice (ADR-0001 "Margin"). What stays host-side is named there: the chronology exceptions, the pre-open slice, the 1x-long money call and the broker-open FX rollover, whose kernel `FxRoll` point measures elsewhere (MG9). Corrected by R5 lane F7 | F O S | +| E2 | **Margin.** The TV margin call is built host-side from kernel primitives (a Stop-triggered `Reduce` bound to openings, or `execute_current`, which survives in `submit_margin_call_slice` pine_adapter.cpp:17958); its chronology lives at `schedule_margin_call_path` pine_adapter.cpp:20236 and its excursion sample differed by chronology (the Pine host's own excursion model, retired by R5 lane H-THIN: the kernel's sampler books it) | A kernel model under an adapter run would liquidate twice or reorder settlement; a checkpoint that reduces mid-path changes lot state exactly where the adapter's chronology lives | Superseded by R5: the adapter declares `spec.margin` — maintenance-only, so no opening is decided twice (`AdmitWithHostMargin`, MG4) — and the kernel owns the mechanism (check points, resting liquidation, receipt) while the adapter's hooks answer TradingView's scheduling, money and slice (ADR-0001 "Margin"). What stays host-side is named there: the chronology exceptions, the pre-open slice, the 1x-long money call and the broker-open FX rollover, whose kernel `FxRoll` point measures elsewhere (MG9). Corrected by R5 lane F7 | F O S | | E3 | **Sizing.** TV sizes from the tick-rounded signal close, 10-digit-rounded equity and fee-reserved cash, frozen at signal, with fill-time exceptions in COOF paths (`default_sizing_shape` pine_adapter.cpp:3184, the COOF `at_fill` exception pine_adapter.cpp:8395) | One "generic" percent sizing serving both would break parity or import TV rounding into the kernel; the last ulp differs on lot-stepped instruments | Two paths on purpose: `Sized` (kernel, plain arithmetic, timing as a first-class field) vs `HostSized` + the adapter resolver. Never "simplify" the adapter onto the kernel basis. What the two paths share is the conversion: since R5 lanes N11 and F7 the division by price × point value × FX and the percentage fee reserve exist once, in the kernel (`native_sized_units`), for a default AND a typed (`qty_type` cash / percent) quantity. The adapter keeps what is measured TradingView: the money basis (signal-time equity, TradingView's margin equity at a typed fill, the hypothetical Flatten's balance for a typed reversal), the freeze, the fill-time exceptions and both lot floors (`tests/test_adapter_sizing_relower.cpp` cases 8, 9, 11: kernel 0.0392 against the percent floor's 0.0391 on a 0.0001 lot), and a staged FX series keeps `Sized` wherever the acceptance coordinate converts at the source's rate (case 12) | F O S | | E4 | **Calc on fill.** Kernel suffix eligibility is continuous along the segment; TV refills only at O/H/L/C waypoints -- on a magnified path at each intrabar's open, twice, and its two extremes, never its close (R5 lane MAG-INTRABAR) (`PineScheduler::recalculate` pine_scheduler_native.cpp:727, `coof_next_waypoint` pine_adapter.cpp:6949) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:6507 and `drain_applied_notifications` (native_execution_consumer.cpp:7190); new behaviour only behind new opt-ins | F | | E5 | **Open-bar lookahead.** The adapter reads the full bar in `on_bar_open` (margin path scheduling uses H/L, pine_adapter.cpp:22252) | The complete bar cannot be masked globally | Opt-in `OpenOnly` view (A.4); loud doc warning in L0 | F | @@ -1623,7 +1623,7 @@ the rule gives 9.9 + 9.89). | P5 | **Test asymmetry:** 430 adapter test files vs 14 native when this plan was written (on this tree 543 `tests/*.cpp` name `source::PineStrategyHost`, 110 `NativeStrategyHost`) | native-only tests in every lane (§3.1 b) | O | | P6 | **Stream vs batch.** The stream refuses monthly input, mixed modes, in-session gaps, security feeds and source setters, and tick input under an FX curve (any non-empty curve until audit lane N6) (`native-engine.md`, "Stream limitations (current)") | L5 and L6 must state which refusals are generic and which are deliberate source limits | S | | P7 | **C ABI variant growth** | §2.iii hardening rules | S | -| P8 | **Unverified in the inputs:** whether `execution::LifecycleEffects` (execution.hpp:136-139) is ever non-empty from the Pine path, else `PlacementSnapshot::legs` (pine_adapter.hpp:364) is the only live lifecycle (O); the `strategy.risk.allow_entry_in` setter chain beyond `set_pine_risk_direction` pine_strategy_host.cpp:886 → `set_risk_direction` pine_adapter.cpp:26465 (O); what the ABI receipts pin (F); the probe corpus itself lives outside this tree (F) | verify inside the lane that touches each | F O | +| P8 | **Unverified in the inputs:** whether `execution::LifecycleEffects` (execution.hpp:136-139) is ever non-empty from the Pine path, else `PlacementSnapshot::legs` (pine_adapter.hpp:364) is the only live lifecycle (O); the `strategy.risk.allow_entry_in` setter chain beyond `set_pine_risk_direction` pine_strategy_host.cpp:886 → `set_risk_direction` pine_adapter.cpp:26477 (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) @@ -1726,7 +1726,7 @@ virtual std::optional resolve_margin_call_units(const NativeMarginCallVi virtual void on_native_margin_call(const native_order::MarginCallEvent&) {} // O, S ``` -- Mechanism (F): a **kernel-originated request**. At each script-bar open and after each applied fill the kernel computes the liquidation level and rests a `Reduce` / `Flatten` with `Stop{level}` bound to the live openings — the shape the adapter builds host-side today (pine_adapter.cpp:20211). New hashed `RequestDefinition::origin { Host, KernelLiquidation, KernelRisk }`, new `CancelReason::Superseded` on re-price, `MarginCallEvent` joins `CommandEvent` (17 → 18, native_order.hpp:1308). O's alternative, a checkpoint on the excursion walk (`apply_excursion` native_execution_consumer.cpp:3348), is not chosen: a reduction mid-path is the entanglement O itself flags (E2). +- 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:20223). New hashed `RequestDefinition::origin { Host, KernelLiquidation, KernelRisk }`, new `CancelReason::Superseded` on re-price, `MarginCallEvent` joins `CommandEvent` (17 → 18, native_order.hpp:1308). O's alternative, a checkpoint on the excursion walk (`apply_excursion` native_execution_consumer.cpp:3348), is not chosen: a reduction mid-path is the entanglement O itself flags (E2). - FX revaluation (MG9) falls out once `native_fx_curve` drives the mark (`native_fx_curve.cpp`). - TUs: `native_run_spec.{hpp,cpp}`, `native_execution_consumer.cpp` (`admit_opening_inspect` native_execution_consumer.cpp:3672 per side; scheduling next to `invoke_bar_open_callback` / `drain_after_applied`), `native_order.*`, `engine_state_hash.cpp` + the coverage checker, `c_abi.cpp` + `pineforge.h` (`pf_native_run_spec_v2` or an extension struct; `CLAUDE.md` SOP). @@ -1749,7 +1749,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:1729-1730) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:7934-7946) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). -2. Requests born in any of these callbacks follow the existing birth rule: eligible on the unconsumed rest of the bar (`born_on_remaining_path` native_execution_consumer.cpp:6507). `born_on_remaining_path` (native_execution_consumer.cpp:6507) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1961-1991) is calibrated against it. +2. Requests born in any of these callbacks follow the existing birth rule: eligible on the unconsumed rest of the bar (`born_on_remaining_path` native_execution_consumer.cpp:6507). `born_on_remaining_path` (native_execution_consumer.cpp:6507) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1964-1994) is calibrated against it. 3. `EveryModeledPoint`: a calculation at each magnifier sample / observed print, in batch as well as stream (every delivery loop asks the per-point guard native_execution_consumer.hpp:1283-1290, whose body is `recalculate_at_modeled_point` native_execution_consumer.cpp:7780). `on_native_tick` stays the observation hook (S). 4. `current_partial_bar()` derives from the driver points already recorded (`record_driver` native_execution_consumer.cpp:3292); valid in bar-open, applied and tick callbacks. 5. The kernel never attempts language-state rollback; that stays Pine's (`snapshot / restore / commit_coof_script_state`, pine_scheduler_native.cpp:54-106) (O). @@ -1836,7 +1836,7 @@ std::optional risk; // NativeRunSpec - Breach: `MatchRejectReason::RiskLimit` for openings (O; F: `RiskHalted`), an optional kernel-issued flatten through A.3's kernel-originated request (`origin = KernelRisk`, F), and a `NativeRiskEvent` in `native_events` (native_host.hpp:1335) (O) or a callback `on_native_risk_event` (S — a virtual, so only inside an epoch batch). The day key uses the existing calendar (`session_day_ordinal` native_calendar.hpp:416), not a chart clock. - S's `directions` / `max_units` / `max_position_units` are existing spec fields (MG1-MG3), not duplicated. -- Interim (F): a C++ host can build all four today from `native_marked_equity` + `on_native_applied`, which is what the adapter does (pine_adapter.cpp:17312-17320); 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:17324-17332); a toolkit helper is an acceptable stand-in until L9. - TUs: `native_run_spec.{hpp,cpp}`, `native_execution_consumer.cpp` (`admit_opening_inspect` native_execution_consumer.cpp:3672 and the per-point walk), `native_order.hpp`, the state hash. ### A.9 Single-source proposals not adopted diff --git a/docs/native-refactor-progress.md b/docs/native-refactor-progress.md index 87baa3f23..819f23f8d 100644 --- a/docs/native-refactor-progress.md +++ b/docs/native-refactor-progress.md @@ -183,7 +183,7 @@ them for the record only. "Linux aarch64" is the maintainers' Cortex-A725 host. | Compact journal records and a history ring buffer (L3) | Smaller command records and a ring buffer for the request core's history. `WorkingRequestCore::history` (`include/pineforge/native_order.hpp:1709`) hands out the whole vector and the core holds `history_` (`include/pineforge/native_order.hpp:2280`) inside `native_order_v7`, so both are a layout and API change | L3, Linux aarch64 (Cortex-A725), `perf` call graphs, inclusive shares: at most 3.68 % of bracket, 11.99 % of re-issue, 4.26 % of market and 3.42 % of the 14 probes (retiring the history alone: 1.64, 4.51, 2.53 and 1.00 %); only part is realisable, since records must still be written | "R5 L3 DONE + ACCEPTED 2026-09-24 22:10 Taipei", ruling 1: "DEFERRED to the PERF-D3 structural plan"; TRIAGE1 §D: "compact journal records" | Lane L3's code, merged by INT22 (#285); its profiles are unpublished | A `native_order` layout epoch | | A checked-values handoff for executions (L3) | `execution_values` runs twice, in `check_execution` (`src/native_order.cpp:4887`) and again in `apply_execution` (`src/native_order.cpp:4910`); handing the checked values across needs a public type, an API addition with no layout change | L3, as above: 2.21 % of bracket for the two together, about 1.1 % recoverable | L3 ruling 2: "a public checked-values handoff for executions (~1.1% bracket) -> DEFERRED" | As above | An owner decision on the API addition | | A chunked `trades_` store (L3) | Closed trades in chunks rather than one vector; `trades_` (`include/pineforge/engine.hpp:547`) is protected `BacktestEngine` state, so script ABI | L3, as above: on bracket, vector growth 0.64 %, `Trade` moves 0.74 % and `record_close_trade` 0.43 % (at most about 1.4 %); at most 0.35 % on the 14 probes | L3 ruling 4: "chunked trades_ store (ABI) -> DEFERRED" | As above | An `engine_script_run` epoch that changes the engine's protected layout | -| A zoned chart-day memo (D2-D finding 4) | On a chart with a timezone, `libc_chart_day_key` (`src/source/pine_adapter.cpp:17353`) takes the process timezone lock through `ScopedTimezone` and calls `localtime_r` on every bar, because `on_bar_open` (`src/source/pine_adapter.cpp:22252`) reads `chart_day_key` (`src/source/pine_adapter.cpp:22489`) unconditionally. The non-UTC monthly Sharpe/Sortino walk, `month_key_local` (`src/engine_metrics.cpp:33`), pays one `localtime_r` per equity point. UTC charts use D2-D's memo, `civil_chart_day_key` (`src/source/pine_adapter.cpp:17337`) | Not measured on a zoned chart. The UTC path cost 134 instructions a bar before D2-D's memo and 68 after (D2-D, Linux aarch64, callgrind on the floor). PERF-K1: `ScopedTimezone` copies the zone name, which allocates for names of 16 or more characters under libstdc++ | TRIAGE1 §D: "D2-D finding 4 (the timezone `localtime_r` memo)", under the owner's cap. D2-D's report: an exact zoned memo "would need the zone's transition table, which belongs in `timezone.cpp`" | None; the UTC memo's witness is `tests/test_chart_day_memo.cpp` | A lane that owns `src/timezone.cpp` and builds an exact zone-transition memo (PERF-K1's Sitka case shows why a shortcut is not exact), byte-identical, with the zoned keys of `test_chart_day_memo` still equal to the computed ones | +| A zoned chart-day memo (D2-D finding 4) | On a chart with a timezone, `libc_chart_day_key` (`src/source/pine_adapter.cpp:17365`) takes the process timezone lock through `ScopedTimezone` and calls `localtime_r` on every bar, because `on_bar_open` (`src/source/pine_adapter.cpp:22264`) reads `chart_day_key` (`src/source/pine_adapter.cpp:22501`) unconditionally. The non-UTC monthly Sharpe/Sortino walk, `month_key_local` (`src/engine_metrics.cpp:33`), pays one `localtime_r` per equity point. UTC charts use D2-D's memo, `civil_chart_day_key` (`src/source/pine_adapter.cpp:17349`) | Not measured on a zoned chart. The UTC path cost 134 instructions a bar before D2-D's memo and 68 after (D2-D, Linux aarch64, callgrind on the floor). PERF-K1: `ScopedTimezone` copies the zone name, which allocates for names of 16 or more characters under libstdc++ | TRIAGE1 §D: "D2-D finding 4 (the timezone `localtime_r` memo)", under the owner's cap. D2-D's report: an exact zoned memo "would need the zone's transition table, which belongs in `timezone.cpp`" | None; the UTC memo's witness is `tests/test_chart_day_memo.cpp` | A lane that owns `src/timezone.cpp` and builds an exact zone-transition memo (PERF-K1's Sitka case shows why a shortcut is not exact), byte-identical, with the zoned keys of `test_chart_day_memo` still equal to the computed ones | | Thread-local reads that remain per call | D2-C moved the three scopes' writes onto the pump's block, `pump_ambient` (`src/native_execution_consumer.hpp:496`). A reader still reaches the block through `tl_runtime_ambient` (`src/ta_extremes_volume.cpp:29`): `bar_context` (`src/ta_extremes_volume.cpp:82`), read by every `ExtremeRing::update` (`src/ta_extremes_volume.cpp:117`), which backs `ta.highest`, `ta.lowest`, `ta.highestbars` and `ta.lowestbars` and, through an embedded `Highest` and `Lowest`, `ta.stoch`, `ta.wpr` and `ta.range`; and `matching_day_partition` (`src/timeframe.cpp:628`), read by the default-anchored `ta.vwap` through `session_day_index`, by `crosses_boundary` on a DAY period (`timeframe.change("D")`) and by the session-period helpers. `decompose_ms_local` (`src/session_time.cpp:250`) reads two thread-locals per call on a zoned clock, and the prepared order storage's `free_blocks` (`src/native_order.cpp:620`) one per prepared mutation. `ema_na_warmup_flag` is read once per EMA, at its first compute | One thread-local access per reader call. AUDIT4, macOS arm64, a dlopen'd tutorial-MACD probe, counted by `_tlv_get_addr` breakpoints and by descriptor patching: 1.000 a bar for one `ta::Highest` and for one default `ta::VWAP`, 1.999 for one `pine_hour` on a New York clock, 0.000 on the no-order floor and with orders. On Linux a TLS-descriptor call costs about 20 instructions: D2-C's 13 → 4 calls a bar was 260 → 80 instructions (Linux aarch64, GCC 13, callgrind). INT23: 2.00 a bar on the zoned probe 064, 0.08 on probe 008 | None for the TA and day-partition readers: "RULING: INT23 ACCEPTED" (ledger, 2026-09-24 23:30 UTC) reads "dlopen TLS 13->0/bar", and INT23's report names only the timezone and order-core residuals. This row is their record | Unpublished (AUDIT4's scratch) | A byte-identical lane that hands these readers the pump's block without a thread-local read. `ta::bar_context()` is script ABI, so its public declaration stays as it is | | "audit-off mode", "LTO", "data-oriented layout" | Named in "OWNER DECISION 2026-09-25 00:20 Taipei" among the post-release structural items: "S1 bulk quiet bars, S2 per-strategy specialization, audit-off mode, LTO+PGO, data-oriented layout". No engine or campaign document defines, designs or measures the three | None | The owner decision | None | Struck from this inventory until a design note defines one of them | @@ -241,7 +241,7 @@ Mac's 489 and 412 and 1003 of 1249, and no Mac output moved. - **PERF-L4's retired reader slots.** `event_high_water_reader_` and `terminal_receipt_high_water_reader_` - (`include/pineforge/source/pine_adapter.hpp:2388-2389`), with their type + (`include/pineforge/source/pine_adapter.hpp:2391-2392`), with their type `ReceiptHighWaterReader`, are storage nothing reads or writes since PERF-L4: they keep `PineExecutionAdapter`'s layout, which is part of `PineStrategyHost`'s and so of the generated script's ABI. They go at the next diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 817eac8e3..c5d8aa2af 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -1496,7 +1496,7 @@ adapter answers `margin_check_allowed` with TradingView's scheduling — which includes the post-exit re-size: when a priced bracket leg of the script bar fills, the slice resting at that bar's adverse extreme was sized on the pre-exit book, and the legacy broker cancelled and re-scheduled it there -(`margin_check_allowed` `pine_adapter.cpp:17832-17865`), so the adapter admits +(`margin_check_allowed` `pine_adapter.cpp:17844-17877`), 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 From ade37d9c3515a851baafd2e046b0f28d92611b72 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Mon, 19 Oct 2026 03:26:05 +0800 Subject: [PATCH 18/35] docs(cr150): restore named C ABI history 0d8b203 70ddaa3 4c78f39 --- docs/pages/native-engine.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 05cd02e83..de706b3fb 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -3969,7 +3969,9 @@ hands the runtime a callback table and gets the same kernel back. virtual to the table. No new virtual, no epoch bump, and nothing about the established C ABI moves: the compiled-strategy runtime symbols and their counts are untouched, and `scripts/check_c_abi_runtime.py` pins the new set as -a second, disjoint inventory. +a second, disjoint inventory. No epoch bump or reordering occurs: the borrowed +symbol-feed setter is an additive compiled-strategy runtime symbol, and +`scripts/check_c_abi_runtime.py` pins it alongside the established inventory. ```c #include From edd92f392eadbe091f932525c99fdb6c031ebf21 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Mon, 19 Oct 2026 03:33:16 +0800 Subject: [PATCH 19/35] docs(cr150): refresh L16 ADR anchors --- docs/adr/0001-kernel-adapter-boundary.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index ec0ec0f37..4f5ae2a30 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -600,7 +600,7 @@ name is usually a plain word. The fourth audit found these gaps (AUDIT4-opus X10 | the margin model's check points on an intrabar path -- `NativeMarginCheckKind::IntrabarSample` (`include/pineforge/native_host.hpp`; C `PF_NATIVE_MARGIN_CHECK_INTRABAR_SAMPLE`), offered at every delivered sample after the script bar's first of a `lower_tf` path matched as `ContinuousSegments`, at that sample's price, immediately before it is matched (`NativeExecutionConsumer::deliver_intrabar_script`, `src/native_execution_consumer.cpp`) | every host with a margin model and such a path, through `margin_check_allowed`; the Pine adapter admits it on a magnified run's leveraged long and, outside `process_orders_on_close`, its short at any margin (R5 lane PAR-MARGIN-2) (`PineExecutionAdapter::intrabar_sample_checked`, `src/source/pine_adapter.cpp`) | **a check point of the kernel's own** (R5 lane PAR-MARGIN, item 3; H-MEASURE's acid finding A). Rule 2 (a), mechanism: a run with no whole-bar waypoint model measures its maintenance requirement where its path is -- at each delivered sample -- the rule the docs and `native_run_spec.hpp` stated and the kernel had not kept (it checked the first sample and after fills only); nothing in it knows a venue, and a host that checks somewhere else suppresses the points it does not share, as for every other kind. Rule 2 (b), knob: none -- the path is the run's own declaration, and a one-price distribution path (`synthesized`, `DistributionSamples`) is not offered the point, since a request armed at a discrete point is matched only at a later one. What moved: a margin run on a continuous intrabar path whose book crosses its line inside a bar now books that call at the crossing sample (H-MEASURE's intrabar probe: 55 points offered, a slice at 105 where none was booked). TradingView's magnified broker does the same at its own intrabars, 2 minutes on a 15-minute chart (the private fixture set `intrabar_margin`: the first call at the first lower-timeframe low that crosses on 4 of 4 tapes; a model of the check at TradingView's 2-minute intrabars books every row of all 20 magnified tapes, R5 lane PAR-MARGIN-2). No pinned value moved. Pinned by `tests/test_native_margin_intrabar_samples.cpp` (kernel-only) and the "intrabar margin on tapes" section of the private test `test_adapter_margin_schedule_differential`. | | the path a post-fill margin check measures -- `NativeExecutionConsumer::margin_segment_origin` (`src/native_execution_consumer.cpp`), the phase the `AfterApplied` re-arm hands `margin_sizing_price`: the waypoint the segment into the fill's driver point starts from, with the fill price as the point's own mark | every host with a margin model on a path walked by its modeled waypoints (no intrabar path), through the `AfterApplied` point `margin_check_allowed` admits | **a mechanism correction** (R5 lane PAR-MARGIN-2). Rule 2 (a), mechanism: a request matched at a driver point was reached on the segment into it, so the book its fill leaves still faces that point's waypoint -- a limit filled on its way down to the bar's low faces the low, a short filled on its way up faces the high -- and the scan that started strictly after the point's phase measured a path the book had not finished; the waypoints the path already passed stay out, as `fx_roll_margin_check_at`'s continuous roll measures from its segment's origin. Nothing in it knows a venue. Rule 2 (b), knob: none. What moved: a post-fill point after a fill matched on a segment now measures that segment's destination waypoint -- on a high-first bar O100 H101 L80 C81 a buy limit at 95 measures 80 where it measured the close 81, and a low-first bar or a short filled on its way up is called where nothing was (`tests/test_native_margin_post_fill_path.cpp`, kernel-only); a fill at a waypoint's own price, the open's included, measures what it did. TradingView books that call on the fill's bar (lab tv tapes `pm2-m7-lim-*` of the private fixture set `margin_entry_bar` at the low, `pm2-m7-slim-*` and `pm2-m7-s1lim-*` at the high, 12 of 12; with the adapter's admissions `pm2-m7-poocl-*` and `pm2-m7b-lim-*` too), as `ab9714be`'s entry-bar suffix did; the differential's M7-B pin moved to that answer, 10 @80. One v19 witness value moved with it: `tests/test_publication_witness.cpp` Config03, a short filled on its way up a bar, now called at that bar's high (461a919ebdcd41e9 -> 4593ef08ab33e045); the corpus is 312/312, moved 0. | | the samples of a retained lower bar -- `NativeExecutionConsumer::deliver_intrabar_script` (`src/native_execution_consumer.cpp`): a bar the direct four-sample walk declines (a leg of zero length, an open at its own low) is walked through its own four turning points, the repeated one included, where the general `ENDPOINTS` sampler filled the missing point uniformly; a bar that traded nothing at one price (volume 0, open = high = low = close) is one discrete point, walked once | every host with a `lower_tf` path; the Pine adapter's magnified path marks a chart bar's own open and close this way (R5 lane MAG-INTRABAR, `source::tradingview_magnifier_bars`, `src/source/magnifier_intrabars.cpp`) | **a mechanism correction** (R5 lane MAG-INTRABAR). Rule 2 (a), mechanism: a retained bar supplies its own four ticks, and every sample is a check point of the margin model since R5 lane PAR-MARGIN, so a sample between them measured -- and a request matched at -- a price the bar did not print at that point of its path; a bar that traded nothing is one quote with no path, and its four coincident samples were three path points, three phases and three margin checks that are not there. Nothing in it knows a venue: the rule reads the bar's own prices and volume. Rule 2 (b), knob: none. TradingView's magnified broker walks an intrabar as its four ticks whatever their values (tape `pm2-i3-pooc-mag-0402-2000` of the private fixture set `intrabar_margin`: its first call is booked at an intrabar that opens at its low). What moved: an open-at-its-low bar's second mark is its low, 60, where the sampler put 77.5, and the call armed at that open is booked there; a zero-volume one-price bar gives one mark where it gave four (`tests/test_native_intrabar_turning_points.cpp`, kernel-only), and the sampler test's lower-path expectation moved with it (`tests/test_magnifier_ordered_sampler.cpp`: a sub-bar with a zero-length leg walks its corners). No witness value moved; the corpus runs no lower path (312/312, moved 0). | -| the presented bar -- `current_bar_`, `bar_index_`, `prev_bar_timestamp_` (`include/pineforge/engine.hpp`) | the kernel at every callback point: the consumer presents the bar at the point's instant (`src/native_execution_consumer.cpp`). For the body it runs, the Pine host then presents its own script bar. `scheduler_publish_source_bar` and `scheduler_publish_suppressed_tail` write the whole `current_bar_`, stamped at the bar's label (`current_bar_ = bar` `pine_strategy_host.cpp:2374` and `:2495`). They also write the slot's `bar_index_`, restored after a temporary publication, and each published slot's `prev_bar_timestamp_`. `PineScheduler::input` holds `bar_index_` at the input's own index for one input (`InputBarIndexScope` `pine_scheduler_native.cpp:445`) | **retained, the host's view; the fourth audit's E21 check e**. Execution reads its cursor, not this bar: `execute_current` converts at its cursor's rate and neither writes nor restores the host's clock (Section B's row). The broker-state hash folds none of the three, and `scripts/broker_state_hash_waivers.txt` waives `current_bar_` (a convenience copy) and `bar_index_` (a reconstructed cursor). While the Pine body runs, the host's bar is what these read: generated code's `time` and OHLC reads, the presented-clock readers `open_trade_profit` and `active_account_currency_fx`, and `strategy_stream_state_hash`, which folds `current_bar_` and `prev_bar_timestamp_` (`src/engine_stream.cpp`) | +| the presented bar -- `current_bar_`, `bar_index_`, `prev_bar_timestamp_` (`include/pineforge/engine.hpp`) | the kernel at every callback point: the consumer presents the bar at the point's instant (`src/native_execution_consumer.cpp`). For the body it runs, the Pine host then presents its own script bar. `scheduler_publish_source_bar` and `scheduler_publish_suppressed_tail` write the whole `current_bar_`, stamped at the bar's label (`current_bar_ = bar` `pine_strategy_host.cpp:2374` and `:2504`). They also write the slot's `bar_index_`, restored after a temporary publication, and each published slot's `prev_bar_timestamp_`. `PineScheduler::input` holds `bar_index_` at the input's own index for one input (`InputBarIndexScope` `pine_scheduler_native.cpp:445`) | **retained, the host's view; the fourth audit's E21 check e**. Execution reads its cursor, not this bar: `execute_current` converts at its cursor's rate and neither writes nor restores the host's clock (Section B's row). The broker-state hash folds none of the three, and `scripts/broker_state_hash_waivers.txt` waives `current_bar_` (a convenience copy) and `bar_index_` (a reconstructed cursor). While the Pine body runs, the host's bar is what these read: generated code's `time` and OHLC reads, the presented-clock readers `open_trade_profit` and `active_account_currency_fx`, and `strategy_stream_state_hash`, which folds `current_bar_` and `prev_bar_timestamp_` (`src/engine_stream.cpp`) | | the run's words on the codegen-ABI class -- `input_tf_`, `script_tf_`, `script_tf_seconds_`, `last_bar_index_`, `last_bar_time_`, `qty_step_` (`include/pineforge/engine.hpp`) | the Pine host alone. `scheduler_prepare_script_run` sets the timeframe words and the feed's last bar from its scheduler's run spec (`script_tf_seconds_` `pine_strategy_host.cpp:1498`). `apply_realtime_tail_horizon` narrows the last bar to a live probe's horizon, and `prepare_native_begin` copies the symbol's `qty_step` | **retained as the Pine host's run facts**. Generated code reads the timeframe words. The kernel reads them for the report (`src/engine_report.cpp`), the implicit chart feed and the chart-day partition (`src/engine_aux_security.cpp`), all of which a bare host reaches with them empty. A bare host's timeframes are its `NativeRunSpec`'s, which the consumer parses into its own. `last_bar_index_`, `last_bar_time_` and `qty_step_` sit in the hashed region and are waived as feed and symbol metadata; the timeframe words sit outside it. `script_tf_seconds_`'s comment used to place `script_tf_`'s writers in `engine_run.cpp`; it now names the one writer (R5 lane H-DOCGATES) | | the security-evaluator registry the Pine host fills -- `security_eval_states_`, `security_input_tf_`, `security_next_input_ms_` (`include/pineforge/engine.hpp`) | the Pine host. `declare_security_sites_to_kernel` hands a routable run's sites to the kernel and empties the registry (`security_eval_states_` `pine_strategy_host.cpp:1758`). `scheduler_prepare_security_sequence` sets the input timeframe and resets the next-input cursor, and the auxiliary drive and the scheduler advance that cursor (`src/source/pine_aux_security.cpp`, `src/source/pine_scheduler.cpp`) | **retained by design §2.ii row m**: the kernel keeps the registry, the aggregator and one generic step, and the Pine semantics run in `src/source/pine_security_eval.cpp`. `security_next_input_ms_` is waived: the source scheduler's deferred-boundary state is hashed on the source side. `strategy_stream_state_hash` folds the registry | | the auxiliary-feed seams -- `source_aux_security_feed_enabled`, `source_aux_security_input_view` (`include/pineforge/engine.hpp`, behind `PINEFORGE_HAS_AUX_SECURITY_FEED_V1`) -- and `last_error_` | the kernel declares both seams with an inert default (disabled, no bars: `src/engine_consumer.cpp`). It asks them once, in `prepare_native_security_feeds` (`src/engine_aux_security.cpp`), for the auxiliary slice it feeds the `request.security` evaluators. The Pine host overrides both from `strategy_set_aux_security_feed`'s bars (`src/source/pine_aux_security.cpp`). It writes `last_error_` when that setter refuses its input and when its run's preparation throws | **retained with the `auxiliary_feed` adapter-policy ruling above**. The Pine host drives its own auxiliary slice (lane N7's three measurements), and these seams are how that slice reaches the kernel's feed preparation. A bare host declares `NativeRunSpec::auxiliary_feed` and never meets them. `last_error_` is the handle's one error slot, which any layer that refuses a call writes | @@ -710,13 +710,13 @@ Every line number in this section names its symbol on this tree, which | `NativeRunSpec` field | ruling | what the adapter runs instead | measurement and ruling of record | executed native consumers | |---|---|---|---|---| | `price_grid`, `grid_rounding` | **native-only** | TradingView's per-order-kind tick rules on top of `None`: `source_trigger_threshold` (`pine_adapter.cpp:384`), `source_level_on_price_grid` (`:367`), `nearest_tick` (`:274`) / `source_bar_fill_tick` (`:295`) / `directional_tick` (`:327`), behind the terms seam | R5-3 and design risk E7 ruled `None` for the adapter before the lane ran; lanes R7 and N13 measured the alternative anyway (raw levels submitted, `QuantizeFillsAndTriggers` with `HalfUp` declared). L8b closed the first blocker (no run aborts). The second has no remedy on either side: TradingView quantizes per order kind (stop and limit legs and a trail's activation on the quantized bar; the trail stop, the running best, stop-limit entries and the `calc_on_order_fills` cursors raw), the grid is one rule for the run, and the old N13 trial moved 30 pinned checks in 4 units; this is historical evidence, not a current-head result; a per-kind mask would spell that inconsistency into the kernel. The corpus cannot arbitrate (every probe runs a 0.01 tick on an on-grid feed; 5 of 312 differ in an engine-only column). Permanent witness: `tests/test_adapter_grid_relower.cpp`, whose section 5 pins the trail stop the grid fires a bar early. Design row PG and §3.6 | `examples/native/native_price_grid_strategy.cpp`, `examples/native/native_price_grid_c.c`, the private test `test_native_price_grid` | -| `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:17492`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:20867`), `chart_day_key` (`pine_adapter.cpp:17389`), `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:368`) and the kernel's `configure` (`pine_strategy_host.cpp:368`) have fixed and digested the spec. In substance (lane N12, `tests/test_adapter_risk_relower.cpp`, 62 checks over nine paired scenarios): the drawdown latch samples at the close only and still admits a reversal; the loss-day streak counts trades, not days; the intraday loss closes at the path's adverse extreme, refuses every placement and withdraws the book; the fill cap charges slots, transfers quota and closes at the bar's better extreme on the chart timezone's day. With the kernel seeded on the corpus, 3 of 4 cap probes diverge (3840 of 3916, 312 of 604, 2370 of 2384 rows; re-derived by `scripts/check_seeded_risk_experiment.sh` since R5 lane H-MEASURE). `strategy.risk.allow_entry_in` is the one rule that is the kernel's already (`allowed_open_directions`). Design §3.6 | `examples/native/native_risk_limits_strategy.cpp`, `examples/native/native_trail_risk_strategy.cpp`, `tests/test_native_risk_limits.cpp`, `tests/test_native_c_api.c` | +| `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:17492`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:20867`), `chart_day_key` (`pine_adapter.cpp:17389`), `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:359`) and the kernel's `configure` (`pine_strategy_host.cpp:368`) have fixed and digested the spec. In substance (lane N12, `tests/test_adapter_risk_relower.cpp`, 62 checks over nine paired scenarios): the drawdown latch samples at the close only and still admits a reversal; the loss-day streak counts trades, not days; the intraday loss closes at the path's adverse extreme, refuses every placement and withdraws the book; the fill cap charges slots, transfers quota and closes at the bar's better extreme on the chart timezone's day. With the kernel seeded on the corpus, 3 of 4 cap probes diverge (3840 of 3916, 312 of 604, 2370 of 2384 rows; re-derived by `scripts/check_seeded_risk_experiment.sh` since R5 lane H-MEASURE). `strategy.risk.allow_entry_in` is the one rule that is the kernel's already (`allowed_open_directions`). Design §3.6 | `examples/native/native_risk_limits_strategy.cpp`, `examples/native/native_trail_risk_strategy.cpp`, `tests/test_native_risk_limits.cpp`, `tests/test_native_c_api.c` | | `max_abs_units` | **adapter-policy** | `strategy.risk.max_position_size` as a gate on the LIVE book before the fill (`max_position_size` `pine_adapter.cpp:16501-16502`): an entry is refused once the book already holds the limit | design row MG2 (R5-1): the resulting-book cap is the generic one. Measured by N12's scenario `PS` in `tests/test_adapter_risk_relower.cpp`: two-unit entries against a limit of 3 leave the adapter at 4 and the kernel cap at 2 | `tests/test_native_resting_matching_contract.cpp`, `tests/test_native_run_spec.cpp` | | `max_open_lots` | **adapter-policy** | Pine pyramiding is a per-cycle entry count in the adapter's command policy; a resting source entry must not consume a physical-lot cap before it fills, so `project()` leaves the cap unset (`max_open_lots` `pine_adapter.cpp:2646-2648`) | design row MG3 (R5-1); the contract comment in `project()`. Measured against TradingView by R5 lane H-MEASURE (the private test `test_pyramiding_count_differential`, 15 scenarios on three `lab tv` tapes): TradingView checks an entry once, at its first eligible point, against the trades then open, and does not check a resting entry again at its fill; the kernel's cap books 14 of the 15 as TradingView does, the adapter's per-cycle count 13 (it keeps a slot a close or another entry's exit drained; since lane W8A-SIGSTATE-1's R-B a resting LIMIT entry no longer counts, and one the count admitted fills over the cap, and since lane W5B-ENG-MARGIN-RESIDUAL's SB the later market entries of a flat bar are refused at the fill once the book holds the cap, as on TradingView's tapes). A recorded divergence: the retention stands pending a re-lowering ruling. R5 lane H-THIN tried the lowering and kept the count: on the kernel's cap four corpus probes moved away from TradingView, for want of the first-eligible-point rule (H-MEASURE's P4b); it fixed the reservation divergence measured beside it (P10), so a default `strategy.exit(from_entry)` under FIFO now reserves its entry's own quantity, the oldest lots first, as TradingView does | `tests/test_native_resting_matching_contract.cpp`, `tests/test_native_margin_model.cpp` | | `initial_margin_fraction` | **adapter-policy** | TradingView's ten-significant-digit money admission against the signal-time tuple, answered as `AdmitWithHostMargin`; the `margin` model the adapter does declare is maintenance-only (`NativeMarginModel` `pine_adapter.cpp:2885-2893`, `margin.maintenance_long` `pine_adapter.cpp:2900`) | design row MG4 and the wave-4 ruling recorded in `project()`: a positive initial requirement would decline openings TradingView takes. Measured both ways by R5 lane H-MEASURE (the private test `test_adapter_margin_schedule_differential`, M11): the kernel's gate declines a 1x long whose entry fee the adapter admits (`InitialMargin`), and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times | `tests/test_native_precommit_view.cpp`, `tests/test_native_margin_model.cpp` | | `report_open_position_at_end` | **adapter-policy** | The Pine host owns TradingView's range-end shape: batch re-marks the stored final point and re-folds extremes, while REALTIME retains ordinary marks and `PineStrategyHost::present_report` re-marks only the caller-owned copy before generic metric derivation; the stream hash excludes that terminal extreme fold (`scheduler_record_range_end` pine_strategy_host.cpp:1996): report shape, not a mark-to-market row (`KernelRecordedAtHostMarks` pine_adapter.cpp:2855) | design row RP5; the kernel reads the field under `KernelRecorded` only, which `scripts/check_adapter_spec_shadowing.py` gates | `examples/native/native_sized_report_strategy.cpp`, `tests/test_native_report_truth.cpp` | | `open_bar_view` | **adapter-policy** | `Complete`: TradingView's bar-open scheduling and its `calc_on_order_fills` callback read the whole script bar (`pine_adapter.cpp:2825-2827`) | design rows CT4 and E5: the open-only view is opt-in because the adapter needs the full bar | `examples/native/native_calc_on_fills_strategy.cpp`, `tests/test_native_calc_timing.cpp` | -| `subscriptions` | **adapter-hook** | `declare_timeframe_subscriptions` from the begin-time hook (`pine_strategy_host.cpp:1726`): a plain `request.security` site is a kernel subscription | lane R3b over the L6c hook: 21 of the corpus's 23 `request.security` probes run their sites on the kernel, byte-identical. The predicate (`declare_security_sites_to_kernel` `pine_strategy_host.cpp:1726`) declares every site of a run or none. A run keeps all of its sites on the source evaluator when it is a stream's warmup, runs the bar magnifier, cuts its range start to na (KI-55), projects historical lookahead, installs an auxiliary feed, aggregates its chart or has no detected timeframe. It keeps them there too when any one site uses `lookahead_on`, `ticker.heikinashi`, a lower timeframe (requested, emulated, read from the input, or as an array), a publication gate, or a calling-bar close or open latch; when a forex or CFD intraday chart without a native feed asks for `D`; when a site asks for N days, whose bars TradingView counts in the symbol's trading dates from each year's first (`tests/test_security_multi_day_buckets_tapes.cpp`), not on the kernel's calendar-day grid; when the registry's `sec_id`s are not dense from 0; and when the kernel refuses the declaration. R5 lane H-MEASURE pins each reachable condition with what it changes (`tests/test_adapter_security_route_conditions.cpp`, a site through the evaluator against the same site through a bare kernel host): on the feeds measured the magnifier and an aggregated chart change no delivered value, while the range-start cut, the projection's incomplete tail, `lookahead_on` and the OTC pins do; the four lower-timeframe flags, the publish gate and the calling-bar latches are subsumed by other clauses, and the empty or undetected timeframe checks are unreachable after begin-time validation | `examples/native/native_htf_strategy.cpp`, `tests/test_native_htf_subscriptions.cpp` | +| `subscriptions` | **adapter-hook** | `declare_timeframe_subscriptions` from the begin-time hook (`pine_strategy_host.cpp:1726`): a plain `request.security` site is a kernel subscription | lane R3b over the L6c hook: 21 of the corpus's 23 `request.security` probes run their sites on the kernel, byte-identical. The predicate (`declare_security_sites_to_kernel` `pine_strategy_host.cpp:1717`) declares every site of a run or none. A run keeps all of its sites on the source evaluator when it is a stream's warmup, runs the bar magnifier, cuts its range start to na (KI-55), projects historical lookahead, installs an auxiliary feed, aggregates its chart or has no detected timeframe. It keeps them there too when any one site uses `lookahead_on`, `ticker.heikinashi`, a lower timeframe (requested, emulated, read from the input, or as an array), a publication gate, or a calling-bar close or open latch; when a forex or CFD intraday chart without a native feed asks for `D`; when a site asks for N days, whose bars TradingView counts in the symbol's trading dates from each year's first (`tests/test_security_multi_day_buckets_tapes.cpp`), not on the kernel's calendar-day grid; when the registry's `sec_id`s are not dense from 0; and when the kernel refuses the declaration. R5 lane H-MEASURE pins each reachable condition with what it changes (`tests/test_adapter_security_route_conditions.cpp`, a site through the evaluator against the same site through a bare kernel host): on the feeds measured the magnifier and an aggregated chart change no delivered value, while the range-start cut, the projection's incomplete tail, `lookahead_on` and the OTC pins do; the four lower-timeframe flags, the publish gate and the calling-bar latches are subsumed by other clauses, and the empty or undetected timeframe checks are unreachable after begin-time validation | `examples/native/native_htf_strategy.cpp`, `tests/test_native_htf_subscriptions.cpp` | | `auxiliary_feed` | **adapter-policy** | the adapter's own auxiliary drive: the chart-slice mapping and the deferred first bucket (`src/source/pine_aux_security.cpp`) | lane N7, retained on three measurements: 0 of 312 corpus probes install an auxiliary feed; TradingView's chart slice leaves pre-range coverage inert where the kernel folds it by time, and evaluates after the bar's matching pass where the kernel delivers before it (`tests/test_native_auxiliary_feed_twin.cpp`, rows B and C). Design §2.iv | `tests/test_native_auxiliary_feed.cpp`, `tests/test_native_auxiliary_feed_stream.cpp` | | `quantity_tolerance` | **adapter-policy** | TradingView's `1e-10` quantity rules on top of the exact default: a close within `1e-10` of the held position is a `Flatten`, a batched process-orders-on-close (POOC) close ending within `internal::kQtyEpsilon` (`1e-10`) of an interior lot boundary is one selected `Flatten` of whole lots (`source_fifo_prefix_openings`); an ambiguous ordinary deferred FIFO `strategy.close` instead uses `HostSized` to authenticate the exact physical FIFO prefix sum for its `Reduce` at fill time, with that same tolerance (`source_fifo_close_units`, `src/source/pine_adapter.cpp`). Detection checks only the current prefix and prefixes reachable after already-pending same-bar closes, so non-ambiguous request shapes, hashes and readback remain unchanged. Same-bar market transactions, `immediately` closes and the ShortSeed placeholder remain out of scope, and after every fill `PineStrategyHost::on_native_applied` erases lots of at most `kQtyEpsilon` without a closing row (`src/source/pine_strategy_host.cpp`) | lane K-ULP4 (the owner-level ruling of audit X1, part b): opt-in and absent by default, so every adapter run is byte-identical by construction. The tolerance charges a snapped `Reduce` its request and books every dust lot it closes as a row, where the adapter's prefix `Flatten` is charged the lots it holds and its sweep books no row; declaring it would move the adapter's fills, ledgers and hashes, so whether the adapter can move onto it is a later lane's measurement. R5 lane H-THIN measured each of those comparisons (design §3.9b) and kept them: made exact, the sweep fails its own tape row (`test_pine_dust_sweep_paired`) | `tests/test_native_quantity_tolerance.cpp` | From f1c862ae31ba23bd74d95f546c0451fa9259ae30 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Mon, 19 Oct 2026 03:35:35 +0800 Subject: [PATCH 20/35] fix(cr150): retire admission events at completed script boundaries --- docs/adr/0001-kernel-adapter-boundary.md | 2 +- docs/design/cr150-contracts.md | 2 +- docs/design/native-feature-parity.md | 30 +-- include/pineforge/source/market_admission.hpp | 26 ++ include/pineforge/source/pine_adapter.hpp | 13 +- .../pineforge/source/pine_strategy_host.hpp | 2 + scripts/market_admission_schema.json | 4 +- src/compat/pine/market_admission.cpp | 2 +- src/source/market_admission.cpp | 59 +++- src/source/pine_adapter.cpp | 16 ++ src/source/pine_state_hash.cpp | 42 ++- src/source/pine_strategy_host.cpp | 21 +- tests/test_adapter_live_state_equivalence.cpp | 3 + tests/test_admission_journal_window.cpp | 252 ++++++++++++++++++ ...native_market_admission_projection_l4d.cpp | 2 +- 15 files changed, 436 insertions(+), 40 deletions(-) diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index 3dea0457a..26b85ccec 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -272,7 +272,7 @@ kernel and own these quirks, each at its site: margin since F7), while the roll point measures at the remaining path's adverse mark (MG-FX: 0.4116 @ 97 there against TradingView's 0.3996 @ 100). What has no kernel check point at all stays adapter-side: the `process_orders_on_close` chronology exception - (`non_pooc_commissioned_short` `pine_adapter.cpp:24176`) and + (`non_pooc_commissioned_short` `pine_adapter.cpp:24192`) and the 1x-long money call — plus the TV admission scopes (`explicit_pair_scope` `src/compat/pine/market_admission.cpp:34-47`) and the review fold they feed (`awaits_pair_review` `:68-73`, `fold_admission_history` `:83-140`). diff --git a/docs/design/cr150-contracts.md b/docs/design/cr150-contracts.md index 65bae430a..2e9ed38a4 100644 --- a/docs/design/cr150-contracts.md +++ b/docs/design/cr150-contracts.md @@ -212,7 +212,7 @@ references; L15a must maintain these anchors and mechanically pin every current | Adapter projection validation: use the context normalizer; host stages after lending and before projection, using a context built from its store | `normalize_projected_spec` `src/source/pine_adapter.cpp:3004-3010` | 361-363; V1/V2 change 5 | | Instrument-feed digest and cached/uncached continuation folds | `digest_instrument_feed_views` `src/native_run_spec.cpp:1162-1205`; `native_instrument_feeds_digest` `src/native_execution_consumer.cpp:322-324`; `native_instrument_feeds_digest` `src/native_execution_consumer.cpp:2775-2776`; `native_run_spec_digest` `src/native_execution_consumer.cpp:12870-12875` | 364-365 | | Staged feed counts in host state and its hash | `installed_symbol_feed` `src/source/pine_strategy_host.cpp:230-238`; `staged_.instrument_feeds` `src/source/pine_state_hash.cpp:292-299` | 366-367 | -| Refresh of symbol-data digest and host hash fold | `refresh_symbol_data_digest` `src/source/pine_security_eval.cpp:1545-1553`; `symbol_data_digest_` `src/source/pine_state_hash.cpp:1123-1129` | 367-368 | +| Refresh of symbol-data digest and host hash fold | `refresh_symbol_data_digest` `src/source/pine_security_eval.cpp:1545-1553`; `symbol_data_digest_` `src/source/pine_state_hash.cpp:1141-1147` | 367-368 | | Restore/store upkeep and column-setter length check | `held.bars` `src/source/pine_security_eval.cpp:1091-1101`; `data.bars` `src/source/pine_security_eval.cpp:1428-1431` | 369-370 | | Lower-path preflight/validation | `NativeExecutionConsumer::preflight_intrabar_path` `src/native_execution_consumer.cpp:3217-3293` | 371,1221-1222 | | Lower-path digest (count first) and continuation hash | `native_intrabar_path_digest` `src/native_run_spec.cpp:891-919`; `native_intrabar_path_digest` `src/native_execution_consumer.cpp:289` | 371,1223-1224 | diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 6f0c4231a..b4e80f92b 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -58,7 +58,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | 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:11045; lowering `submit_one` pine_adapter.cpp:11923, `BindCohort` pine_adapter.cpp:12386, `submit_leg` pine_adapter.cpp:11679; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1940); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | | OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:11305; `materialize_relative_exits` pine_adapter.cpp:13994, pine_adapter.hpp:2259-2271) | **partial** — composable: the host computes levels in `on_native_applied` and submits; suffix eligibility makes that same-bar correct `born_on_remaining_path` native_execution_consumer.cpp:6551. `Trigger` itself is absolute-only native_order.hpp:296 **Closed:** `Request::anchor = FromOwnerFill{offset, ticks, rounding}` (`TriggerAnchor` native_order.hpp:336) defers the level to the owner's fill, the kernel materializes it once at the arm under `NativeAnchorRounding`, and `resolve_anchored_level` native_host.hpp:1012 is where a source language's own level arithmetic lives (`tests/test_native_anchored_legs.cpp`, `tests/test_native_arm_options.cpp`). | K | L7 | F:A11 O:A11 S:O6 | **Disagree:** F partial (toolkit is enough) / O no (kernel `TriggerAnchor`). R5-2: L7 delivers relative anchors; kernel anchor primary, toolkit materialization is the zero-kernel fallback. | -| OL12 | Working-order view for the host and outside observers (`strategy_pending_order_*`; `PendingIntentView` pine_adapter.hpp:1449-1484; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:27072) | **no** — base returns 0 / -1 `observe_last_bar_dual_entry_path_v1` engine_consumer.cpp:124-133; the POD is TV-shaped (`tv_carry_qty` pending_order_mirror.hpp:73, `pine_birth_reach` pending_order_mirror.hpp:135) **Closed for a bare host:** `native_working_requests()` native_host.hpp:1263 returns one owning `NativeWorkingRequest` native_host.hpp:681 per live request — definition, remaining, trigger state — with the C spelling `strategy_native_working_len_v1` / `_get_v1`. The TV-shaped POD stays frozen and stays the source layer's projection (`tests/test_native_limit_fill_through.cpp`, `tests/test_native_arm_options.cpp`, `tests/test_native_c_api.c`). | K+A | L7 (C++), L13 (C) | F:J5 O:J6 S:X4 | **Disagree:** O "partial + TV-shaped" / F, S no. A bare host reads zeros → no. R5-1: neutral view → kernel; POD shape and Pine journal retention stay A (POD frozen, §2.ii j). | +| OL12 | Working-order view for the host and outside observers (`strategy_pending_order_*`; `PendingIntentView` pine_adapter.hpp:1449-1484; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:27088) | **no** — base returns 0 / -1 `observe_last_bar_dual_entry_path_v1` engine_consumer.cpp:124-133; the POD is TV-shaped (`tv_carry_qty` pending_order_mirror.hpp:73, `pine_birth_reach` pending_order_mirror.hpp:135) **Closed for a bare host:** `native_working_requests()` native_host.hpp:1263 returns one owning `NativeWorkingRequest` native_host.hpp:681 per live request — definition, remaining, trigger state — with the C spelling `strategy_native_working_len_v1` / `_get_v1`. The TV-shaped POD stays frozen and stays the source layer's projection (`tests/test_native_limit_fill_through.cpp`, `tests/test_native_arm_options.cpp`, `tests/test_native_c_api.c`). | K+A | L7 (C++), L13 (C) | F:J5 O:J6 S:X4 | **Disagree:** O "partial + TV-shaped" / F, S no. A bare host reads zeros → no. R5-1: neutral view → kernel; POD shape and Pine journal retention stay A (POD frozen, §2.ii j). | | OL13 | *(quirk)* Same-bar command batching and the seven deferred queues (`pending_bracket_legs_` pine_adapter.hpp:2515-2534); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1364); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:4203, `materialize_snapshot` pine_adapter.cpp:13968), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1357-1384, `DeferredOpenMarketableSell` pine_adapter.hpp:1845-1868, `CloseCallsiteState` pine_adapter.hpp:1983-1997; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:21626, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:21860) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:676-678 | A | — | F:A15 O:A15 S:O9 S:R3 | R5-1 names KI-62. S: the kernel exposes deterministic insertion order only. | | OL14 | *(quirk)* Exit activation / suspension / revival barriers, historical birth reach (`select_exit_activation` exit_activation.cpp:44-90, enforced `leg_activation` pine_adapter.cpp:1261; `select_exit_activation` exit_activation.cpp:20-90, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:1078) | n/a | A | — | S:R4 O:A10 | TV half of OL9. **V19D-P1 (R5 lanes B-ADAPTER, V19-FIX):** a margin-call revival never restores an exit the script cancelled: `strategy.cancel` / `cancel_all` retire every leg of each exit row they name — the live ones they take off the book (caused at the Cancelled event, after anything the same callback did to the leg), the dormant ones, and the ones the adapter took off the book itself — and drop a gapped stop a declined reversal parked for the next margin-call slice (`retire_cancelled_exits` pine_adapter.cpp:2123); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:2200). What `tests/test_adapter_margin_revival_cancel.cpp` replays on the NYSE:F 15m chart of seven lab tv tapes: badapter-v19dp1-control (no cancel: the margin call closes 928 through X) and v19fix-gapped-stop-control (X's stop at 10.15, gapped by the 05-01 open and parked: the same close), and badapter-v19dp1-cancel, v19fix-cancel-before-pair, v19fix-cancel-at-1000, v19fix-gapped-stop-cancel and v19fix-gapped-stop-cancel-after-pair (a cancel on 05-01 09:30 while X is dormant, on 04-30 15:45 before the reversal pair and on 04-30 10:00 while X is live, and on 05-01 09:30 and after the pair while X is parked; one tape file: margin calls 12 @10.39 and 92 @10.57, 836 open). Untaped, the same row holds the tapes' trades for `strategy.cancel_all` on 05-01 and before the pair and for `strategy.cancel` after the pair, and on the same bars with no quantity grid — where the pair holds X in the cancel's own callback — every spelling books what the cancel on the dormant exit books | @@ -86,7 +86,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | SZ8 | Fractional reduce of the position / a cohort (`quantize_close_units` pine_adapter.cpp:5221, `compute_exit_reservation` pine_adapter.cpp:5261) | **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:5261, `reserved_exit_qty` pine_adapter.cpp:11574) | **partial** — `PointBudget` native_order.hpp:379-382 and group `Reduce` native_order.hpp:220 give a generic reservation; no net-of-siblings claim for fraction sizes **Closed:** `ScopeClaim::NetOfSiblings` subtracts the units already claimed by live sibling reduces bound to the same scope, and `ScopeBasis::{AtMatch, AtAcceptance}` chooses when that scope is measured (`tests/test_native_sizing_bases.cpp`). | K+A | L3 | F:A14 S:S5 S:O10 O:A12 | F proposed nothing. R5-4: generic half → L3; the Pine ledger → SZ11, SZ12. | | SZ10 | *(quirk)* Ten-significant-digit money rule (`source_money_round` pine_adapter.cpp:558, `source_money_floor_lot` pine_adapter.cpp:658; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:5243) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1753 | 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:8520), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:10044), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:16223), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:16754, pine_adapter.cpp:16807) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:21674) | 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:3208), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:3227, `typed_entry_units` pine_adapter.cpp:3245), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:26659) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes and 202 famr tapes (two tests that live in the private fixtures); on a seven-digit tie (E 9998.99999996, 10 000 lots at tick(close) 0.9999) the rule admits while the kernel's conversion buys 9999.99999996 lots and binary64 gives E/Q 0.999899999996 (`tests/test_adapter_fill_qty_probe.cpp`, section 2). | +| 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:8520), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:10044), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:16223), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:16754, pine_adapter.cpp:16807) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:21674) | 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:3208), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:3227, `typed_entry_units` pine_adapter.cpp:3245), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:26675) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes and 202 famr tapes (two tests that live in the private fixtures); on a seven-digit tie (E 9998.99999996, 10 000 lots at tick(close) 0.9999) the rule admits while the kernel's conversion buys 9999.99999996 lots and binary64 gives E/Q 0.999899999996 (`tests/test_adapter_fill_qty_probe.cpp`, section 2). | | SZ11 | *(quirk)* Same-bar `strategy.close` callsite batching, two-call provenance, entry-id ledger (`enqueue_pooc_fifo_close` pine_adapter.cpp:9171; ledger `close_batch_bar_` pine_adapter.hpp:2595; the kernel's comment-only residue was deleted by R5 lanes E6 and E10) | n/a | A | — | S:O8 F:A15 O:A15 | **Disagree:** S "generic → kernel" (its P2 `ReplaceByKey` / `Fifo`). R5-4: adapter. | | SZ12 | *(quirk)* POOC reservation-growth market-entry population predicate (`select_reservation_growth_sources` src/compat/pine/reservation_expansion.cpp:9-34, called from the adapter, `select_reservation_growth_sources` pine_adapter.cpp:1322) | n/a | A | — | S:O10 | (single-source) R5-4 confirms S's own split: concept → SZ9, predicate A. | @@ -103,9 +103,9 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | MG7 | Liquidation slice sizing (grid floor, ×4 restore, one-contract fallback `source_margin_units` pine_adapter.cpp:17612) | **no** **Closed:** `NativeLiquidationSizing::{RestoreMinimum, ShortfallMultiple, Flatten}` with `shortfall_multiple` and `liquidation_min_units`, capped at the held quantity and floored onto `quantity_grid`; `resolve_margin_call_units` native_host.hpp:995 gives a host the last word, clamped into `(0, held]` (`tests/test_native_margin_model.cpp`, `tests/test_native_margin_hooks.cpp`). | K+A | L4 | O:D4 F:D2 S:M4 | **Disagree:** F generic knob `ShortfallMultiple` / O "×4 is A" / S quirk, yet a 4.0 default inside its generic model. R5-1: policy knob in the kernel, plain default (restore-minimum). **Decided by R5 lane F7 (the D2 question):** the adapter selects NO kernel knob — `project()` leaves `sizing` / `shortfall_multiple` / `liquidation_min_units` unset and its `resolve_margin_call_units` answers every call with TradingView's slice (the restore floored onto the lot grid BEFORE the ×4, floored again, the one-contract band, the dust gate). Measured: on a one-unit grid, 20 long at 100 on 1000 at 50 % reaching 93.9, TradingView books 4 lots and the kernel's own `ShortfallMultiple 4` books 5 (`tests/test_native_margin_hooks_twin.cpp` MG-F3, an executed run, no longer a comment); without a grid the two agree bit for bit (MG-F/H, 4.2105263157894735), but declaring the knob for those runs alone folds `NativeMarginModel::sizing` into every margin run's identity — an epoch decision F7 does not take. Since R5 lane H-THIN the pre-open admission slice (`schedule_preopen_margin_slice`) sizes through the same `source_margin_units`, where it kept its own copy of the ×4 without the band or the dust gates (AUDIT4 §6 G2-12; the private test `test_adapter_margin_schedule_differential` M10: band and fudge now 1 and 1, the kernel's and `ab9714be`'s). A four-times call on a whole-unit grid whose book's oldest lot is one unit is followed by one more unit at the next check point (`whole_unit_follow_up_due` pine_adapter.cpp:19140; R5 lane W5B-ENG-MARGIN-RESIDUAL, the private fixture set `margin_residual` w5b-es-*, w5b-sb-es-*, w5b-f-q1x12). With slippage the one-contract band takes its unit only where that unit restores the book at its own fill: the position's requirement at the liquidation's slipped print, less the equity at the mark, under one unit's margin (`source_margin_units` pine_adapter.cpp:17612; lane INT28-FIX rule OU, the private fixture set `slipped_short`). A one-unit call that leaves the book short at its own fill, by more than one unit's margin and two slippage steps with the book re-marked there, is followed by one more unit at the bar's next path point, where that point's own check calls nothing and its print is inside the call's fill (lane INT28-FIX rule OF). Under a percent commission a call of any size is followed so, on a fractional lot grid too: while the book the call left, re-marked at its fill, is short by more than two slippage steps for each unit called and the exit fees of those units and of one more, the next path point inside that fill takes four times the shortfall's lot floor over the fill, or one unit where that floors to none (`margin_follow_up_units` pine_adapter.cpp:19182, `follow_margin_call` pine_adapter.cpp:19231; R5 lane TAIL-I, the private fixture set `margin_open_print`). | | MG8 | Liquidation price query (`margin_liquidation_price` pine_strategy_host.cpp:168-184) | **no** **Closed:** `native_liquidation_price()` native_host.hpp:1320 answers the exact solved level, `nullopt` where none exists; `NativeLiquidationLevelBase` chooses whether the solve answers for the open entries' fees (`tests/test_native_margin_model.cpp`, `tests/test_native_open_lots.cpp`). | K+A | L4 | F:D3 S:R5 | **Disagree:** F generic / S adapter-only. R5-1: liquidation price is generic; the Pine spelling stays A. **Lowered by R5 lane H-THIN:** the Pine host's second solve (`compute_liquidation_price`) is gone; `strategy.margin_liquidation_price` reads `native_liquidation_price()`, which the adapter configures with `NativeLiquidationLevelBase::RealizedOnly` (the Pine formula's closed-money solve, bit for bit at every bar of the lane's 1,021 seeded margin-call runs), and keeps only TradingView's tick spelling: floored for a long, ceiled for a short. A run whose margin calls are switched off declares no margin model, so it reads na. | | MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:5688, `apply_fx_opening_margin_slice` pine_adapter.cpp:5700) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1202 but no margin re-check. **Closed by audit lane N6:** `NativeMarginCheckKind::FxRoll` — a step of the declared curve is a check point of its own, measured at the unchanged price before the first point the new rate converts (`tests/test_native_margin_fx_roll.cpp`). The adapter refuses the point by kind, for a measured reason since R5 lane F7: TradingView's rollover is a broker-open checkpoint at the open price, now at any positive margin (`apply_fx_open_margin_slice`, on the one TradingView money / units rule every other checkpoint uses), while the roll point measures at the remaining path's adverse mark and rests the slice there — a carried 1x long on a step bar O 100 L 97 books 0.4116 @ 97 on the adapter's run shape and 0.4036 @ 99, measured a bar early, on a strict calendar, against TradingView's 0.3996 @ 100 (`tests/test_native_margin_hooks_twin.cpp` MG-FX). Both N6 follow-ups are closed (§4.4). **Completed by R5 follow-up lane E3:** the conversion INSTANT is now generic too — every check kind converts at its own cursor's rate (`account_currency_fx_at(cursor.point.effective_time_ms)`) for the requirement, the equity and the level, in batch and stream, so a step landing between a fill's cursor and the clock the engine presents no longer breaches at the fill (`tests/test_native_margin_fx_clock.cpp`; N6's local clock swap inside `fx_roll_margin_check` is removed as redundant). **Extended by R5 follow-up lane E21** to the opening gate's placement site (MG4), and the rate-explicit equity E3 restated beside the engine's accessor is now that accessor's one implementation, `BacktestEngine::marked_equity_at` engine.hpp:730 | K | L4 → N6 → E3 → E21 | O:D8 | (single-source) | -| MG10 | Max drawdown halt (`update_risk_state` pine_adapter.cpp:17492) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1301 **Closed:** `NativeRiskLimits::max_drawdown` native_run_spec.hpp:311, a `NativeLossLimit{value, percent}` measured against the running peak equity at three points of every script bar, blocking openings for the rest of the run (`tests/test_native_risk_limits.cpp`). | K | L9 | F:D6 O:E1 S:M6 | R5-9 | -| MG11 | Max consecutive losing days (`SourceDayLedger` pine_adapter.hpp:1419-1427; gate `max_cons_loss_days` pine_adapter.cpp:16495) | **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:17525, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:20867) | **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 | +| MG10 | Max drawdown halt (`update_risk_state` pine_adapter.cpp:17492) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:51; composable today from `native_marked_equity` native_host.hpp:1301 **Closed:** `NativeRiskLimits::max_drawdown` native_run_spec.hpp:311, a `NativeLossLimit{value, percent}` measured against the running peak equity at three points of every script bar, blocking openings for the rest of the run (`tests/test_native_risk_limits.cpp`). | K | L9 | F:D6 O:E1 S:M6 | R5-9 | +| MG11 | Max consecutive losing days (`SourceDayLedger` pine_adapter.hpp:1419-1427; gate `max_cons_loss_days` pine_adapter.cpp:16495) | **no** — observation only `loss_days_limit` include/pineforge/source/market_admission.hpp:50 **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:17525, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:20867) | **no** — observation only `intraday_loss_limit` include/pineforge/source/market_admission.hpp:52 **Closed:** `NativeRiskLimits::max_intraday_loss` with `NativeRiskAction::FlattenAndBlock`, under which the kernel issues one `Flatten` of its own at the breach point (`RequestOrigin::KernelRisk`) and latches the block before that flatten settles (`tests/test_native_risk_limits.cpp`). | K | L9 | F:D7 O:E3 S:M6 | R5-9 | | MG13 | Max filled orders per day (`IntradayCap` intraday_cap.hpp:84-263, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:20910) | **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:17389) | **no** — the calendar exists `session_day_ordinal` native_calendar.hpp:416-419, there is no risk-day ledger **Closed:** `NativeRiskDay::{SessionDay, CalendarDayInTimezone}` keys the risk day, and `native_risk_state()` native_host.hpp:1329 reads the whole ledger back — block and reason, day, fills counted, loss-day streak, peak equity, day-opening equity (`tests/test_native_risk_limits.cpp`). | K+A | L9 | O:E5 S:M7 | The chart-day shortcut stays A; the kernel keys on session / calendar day | | MG15 | *(quirk)* TV margin call: money rounding `source_margin_money` pine_adapter.cpp:17557, 4× shortfall `4.0 * minimum` pine_adapter.cpp:17646, POOC chronology exception `resting_order` pine_adapter.cpp:20253 (the commissioned carried short; the competing-entry and rounded-money exceptions retired by R5 lane W5B-ENG-MARGIN-RESIDUAL, the private fixture set `margin_residual` w5b-pc-*), 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:18550, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:18550) | n/a | A | — | F:D2 F:G4 O:D4 S:M4 | R5-1. **Measured (R5 lane H-MEASURE)** against the kernel's own points on the same bars and orders, the kernel's hooks answering TradingView's money and slice (the private test `test_adapter_margin_schedule_differential`): a gap-open breach executes at the open, sized on the open's money, where the kernel's mark check rests at the path's adverse extreme and `CalculationOnly` executes at the calculation (M8); the pre-open slice parts from the kernel's `AfterApplied` point, and from `ab9714be`, on the one-contract band and the restore's `+1e-6` floor (M10; the frozen units it also parted on are TradingView's book since R5 lane PAR-MARGIN froze a default-percent stop above 100 % as eight `lab tv` tapes, the private tapes `pm-m10-*` of `margin_entry_bar`, show TradingView does); the entry bar of a leveraged opening, where four `lab tv` tapes (the private fixture set `margin_entry_bar`) and the kernel's `AfterApplied` point book the margin call at that bar's low, is admitted by `on_applied` since R5 lane PAR-MARGIN on the run shapes `ab9714be` checked it on (the pre-open slice's own opening keeps that slice's verdict), and the adapter books the tapes' rows (M7; H-MEASURE had recorded it as a divergence); R5 lane PAR-MARGIN-2 extended it to limit openings, `process_orders_on_close`, `calc_on_order_fills` and adds to a carried book, and made the kernel's post-fill point reach the low a limit was filled on the way down to, or a short's high on its way up (twenty-eight `lab tv` tapes, the private tapes `pm2-*` of `margin_entry_bar`). Under the magnifier the kernel's `IntrabarSample` point (R5 lane PAR-MARGIN) is admitted on a leveraged long, plain, under `process_orders_on_close` or `calc_on_order_fills`, and on a short outside `process_orders_on_close` (PAR-MARGIN-2), where TradingView books its call at the first lower-timeframe low that crosses -- at its own 2-minute intrabars on a 15-minute chart, which a model reproduces on all 20 magnified tapes (the private fixture set `intrabar_margin`); six tapes part on the one-minute samples the host feeds, a recorded divergence of the magnifier's sampling. A margin call books the nearest tick of its print, as TradingView's market fills do (12.105 books 12.11; the private fixture set `half_tick_rounding`, PAR-MARGIN-2). | @@ -210,7 +210,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | f | Dead legacy stream path | **Done.** `stream_feed_input_bar` and `dispatch_source_stream_script_bar` are both gone — neither name is in `src/` or `include/` — and the virtual left with the epoch that carried it. `stream_state_hash` engine.hpp:1791 stays, and the declaration/definition pairing is still enforced by `scripts/test_native_source_guard.py` | NEUTRAL (removing the virtual changes the vtable → shared bump) | Delete both | O; F asked (its Q6) | | g | Legacy path resolver | **Done, and then deleted.** `src/engine_path_resolve.cpp` is 118 lines with five functions: `bar_path_uses_high_first`, both `first_touch_position` overloads, `fill_bar_path_points_ordered`, and `BacktestEngine::declare_opened_lot_entry_bar_mask`. Ten test files still mention `resolve_exit_path_fill` in historical comments; the resolver body and test-only oracle are gone. | NEUTRAL (pure move; verify with the link gate) | Keep the generic path helpers in the kernel TU; the old source resolver is not a live counterpart | F (single-source) | | | h | `pine_*`-named generic API | `timeframe_time` session_time.hpp:35-211 (live, e.g. `session_in_market`, which generated code reaches through `pine_session_ismarket` session_time.hpp:263), `str_utils.hpp`, `deterministic_random` math.hpp:9, `tz_util` timezone.hpp:8-30; the guard bans `pine_*` only inside `IDENTIFIER_ROOTS` (test_native_source_guard.py:34, test_native_source_guard.py:34-42) | NEUTRAL, M | Neutral aliases; `pine_*` kept as deprecated inlines for codegen. Large blast radius in generated code. **Status (L11 + N14): done** — session_time / str_utils / math / timezone in L11; N14 applied the same treatment to the two public matrix types: `PineMatrix` → `NumericMatrix`, `PineGenericMatrix` → `GenericMatrix`, old spellings kept as exact aliases (no version script pins either name) | O (single-source) | -| i | `pf_pending_order_v1_t` Pine-named fields | `tv_carry_qty` pending_order_mirror.hpp:73, `pine_birth_reach` pending_order_mirror.hpp:135, `pine_exit_activation_owner_cycle_at_birth` pending_order_mirror.hpp:147-156, `pine_frozen_market_instruction_kind` pending_order_mirror.hpp:164-169; enumerated by `PF_PO_FIELD` pending_order_mirror.cpp:144-178; written only by `pine_birth_reach` pine_adapter.cpp:27072 | 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:27088 | no change | Freeze (ABI). The neutral view arrives beside it (L7 / L13) | O (single-source) | | j | Pine-shaped lot flags | **Mostly done.** `market_pyramid_add` moved to the adapter side table the row asked for — `market_pyramid_adds_` src/source/pine_state_hash.cpp:692, whose own comment records that it was `PyramidEntry::market_pyramid_add` — and `ordinary_market_open`, `pooc_terminal_market_entry`, `ordinary_stop_open`, `bracket_slot_shadowed` and `entry_path_position` are all deleted. What is left on the lot is the intrabar-fill excursion mask, `skip_entry_bar_high` / `skip_entry_bar_low` engine.hpp:154, hashed (`skip_entry_bar_high` engine_state_hash.cpp:74) and, since R5 lane E6, written by no host at all: a host that owns lot excursions declares where its fill sat (`declare_opened_lot_entry_bar_mask` engine_path_resolve.cpp:95; the Pine host did until R5 lane H-THIN left its excursions to the kernel's sampler) and the kernel derives the pair | HASH-VISIBLE | Move the live flag to an adapter side table keyed by `entry_incarnation` (the adapter already keeps `PlacementTable` pine_adapter.hpp:355-675) and fold it through `hash_source_extension`; delete the dead ones; update the hash-coverage waivers. Whether a kernel TU beyond the hash reads them is unverified (O) | F O S | | k | `margin_call_enabled_` | **Done.** The generic kernel model is enabled by `NativeRunSpec::margin` native_run_spec.hpp:672. TradingView-specific toggling remains `source::PineStrategyHost::set_margin_call_enabled` pine_strategy_host.hpp:511; the frozen C symbol remains source-owned. | HASH-VISIBLE (R5-8) | Keep the source setter in the adapter; the kernel toggle is the presence of `spec.margin` | F O | | l | Kernel decodes adapter strings | **Done.** `closed_trade_close_cause` engine_trade_accessors.cpp:158 branches on no string at all: it reads `Trade::open_at_end`, then the typed `execution::CloseCause` the closer recorded on the row, then `exit_from_bracket`. A kernel-originated liquidation or risk flatten states its own cause through the settling fill, so a bare host reads `3` / `4` where the retired string derivation answered `1`. The public C contract it implements is `strategy_closed_trade_close_cause` pineforge.h:1309 | HASH-VISIBLE (adds durable state) | Kernel `enum class CloseCause` recorded on the `Trade` at settlement (the kernel sets `MarginCall` when L4 liquidates; a source virtual covers adapter causes). C numbers unchanged. After L4 | O (single-source) | @@ -526,8 +526,8 @@ line numbers are re-anchored to this tree (the measurement ran on 683a82f); kern | `strategy.risk.allow_entry_in` | `risk_.direction`; `project()` already declares `spec.allowed_open_directions` (pine_adapter.cpp:2859); the forbidden opposite entry is reshaped to `CloseOpposite` in the adapter's own terms branch | per-opening cap `allowed_open_directions` | (i)+(ii), **already re-lowered** | none to measure: the cap is the kernel's, the close-only reshaping is the retained TV policy | done before N12; nothing to move | | `strategy.risk.max_position_size` | precommit refuses a flat/same-side ENTRY at its fill when the LIVE book already holds ≥ the limit (`max_position_size` pine_adapter.cpp:16502; 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:17492, `update_risk_state` pine_adapter.cpp:17492): 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:16494), never an opposite entry | `max_drawdown`: current drawdown vs percent-of-peak, at all three points; `BlockOpenings` refuses every opening | (ii) cadence + (iv) scope | `MG10-a`: bar 1 opens 600 under the entry and closes 100 under — kernel breaches at the open mark and refuses bar 2's add (book 100), adapter admits it (book 200). `MG10-b`: latched at bar 1's close, the adapter still reverses twice (`L→S` −600, `S→L`), the kernel refuses both reversals (`RiskLimit` at bars 2, 3) and holds. Corpus: `ta-closedtrades-risk-introspection-01` (20 % of peak) identical 1502/1502 rows with the kernel seeded — the rule never fires there | retained: the latched-reversal exemption cannot be composed on a kernel block; a close-only cadence knob alone would not close it | -| `strategy.risk.max_cons_loss_days` | `SourceDayLedger` (pine_adapter.hpp:1419-1427): +1 per losing TRADE on a new chart day, reset to 0 by any winning trade at its fill (`consecutive_loss_days` pine_adapter.cpp:25732); 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:22501); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:20867), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:20899, `submit_intraday_loss_close` pine_adapter.cpp:20867), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:24689); 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:17487, `:17487`, `:17487`, `:17487`, `:17487`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:14588) | `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_cons_loss_days` | `SourceDayLedger` (pine_adapter.hpp:1419-1427): +1 per losing TRADE on a new chart day, reset to 0 by any winning trade at its fill (`consecutive_loss_days` pine_adapter.cpp:25748); 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:22501); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:20867), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:20899, `submit_intraday_loss_close` pine_adapter.cpp:20867), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:24705); 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:17487, `:17487`, `:17487`, `:17487`, `:17487`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:14588) | `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:17483); 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:17389, `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 @@ -1088,9 +1088,9 @@ key, since replaced by the walk of the retained rows, `placement_` pine_state_hash.cpp:330) — one per accepted request, cleared only by `reset_for_run` pine_adapter.cpp:2396 — and reflected the whole admission journal (since replaced by a running digest of its events, `admission_journal` -pine_state_hash.cpp:759). `PineScheduler::hash_state` pine_state_hash.cpp:797 +pine_state_hash.cpp:764). `PineScheduler::hash_state` pine_state_hash.cpp:815 re-folded the consumed source prefix too (`consumed` -pine_state_hash.cpp:783-805). Measured with recording on and off, in two +pine_state_hash.cpp:801-823). Measured with recording on and off, in two shapes: - the audit's order-and-cancel probe (one `strategy_order` accepted and @@ -1106,7 +1106,7 @@ A `sample` call tree of each run splits the adapter fold: the placement snapshots are 55 % of it on the order-and-cancel shape and 97 % on the re-issue shape, the journal reflection the other 45 % and 1 %, the scheduler prefix under 2 %. Most of a snapshot's bytes are field paths the reflection -builds as strings, one per field (`Reflect::field` market_admission.cpp:212-215). +builds as strings, one per field (`Reflect::field` market_admission.cpp:261-264). Why it is not fixed in this lane. The fold can be made incremental without moving a single value, but not from inside lane F9's files: @@ -1121,7 +1121,7 @@ moving a single value, but not from inside lane F9's files: 2. **The transforms need a home.** A row's transform, or a composed segment's, has to outlive the call. The fold is const; the sink is created per call (engine_state_hash.cpp:32); and none of `PlacementTable` - pine_adapter.hpp:571, `admission::Journal` market_admission.hpp:188 or + pine_adapter.hpp:571, `admission::Journal` market_admission.hpp:189 or `PineScheduler` has a member it could keep one in. Adding one is a header change in `include/pineforge/source/`, outside this lane. 3. **A cache must know which rows changed, and nothing records it.** @@ -1137,7 +1137,7 @@ moving a single value, but not from inside lane F9's files: rewrite rows as well (`suspend_brackets_for_reversal` pine_adapter.cpp:1965, `revive_brackets_after_margin` pine_adapter.cpp:2200, `preserved_by_close_all` pine_adapter.cpp:14656), and - `admission::Journal::retain` market_admission.hpp:205 drops events from + `admission::Journal::retain` market_admission.hpp:214 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. @@ -1619,11 +1619,11 @@ the rule gives 9.9 + 9.89). | P1 | **Epoch bumps** move the frozen C++ ABI fixtures and the settlement ABI checker `CLAUDE.md` names; a neutral-refactor worker may not edit frozen headers | Bump lanes are not neutral-refactor lanes; the brief lists the header extension (§3.1 c) | F O S | | P2 | **Stale fence.** `CLAUDE.md` names "measured sources" `engine_strategy_commands.cpp`, `engine_fills.cpp`, `engine_risk.cpp`, `engine_market_admission.cpp`; none exists in `src/` at `73817c1` (only `engine_orders.cpp` and `engine_run.cpp` do) | Done: `CLAUDE.md` now names the post-R4-C measured sources (`src/native_execution_consumer.cpp`, `src/native_order.cpp`, `src/engine_execution.cpp`, `src/engine_orders.cpp`, `src/engine_run.cpp`, `src/engine_path_resolve.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). Resolved since: the corpus was re-pinned (the submodule is at `b40aa8e`) and the sweep runs on `main` | S (single-source) | -| P4 | **Hash compatibility.** The source hash covers hundreds of adapter and scheduler fields (`PineExecutionAdapter::hash_state` pine_state_hash.cpp:270, `PineScheduler::hash_state` pine_state_hash.cpp:797); L4 / L9 add hashed state, L12 removes some | A hash mismatch is a diagnostic until the trade diff is known, never a reason to relax parity (S). Open: does the campaign gate compare broker-state hashes across releases, or only trades (O)? → Q3 | O S | +| P4 | **Hash compatibility.** The source hash covers hundreds of adapter and scheduler fields (`PineExecutionAdapter::hash_state` pine_state_hash.cpp:270, `PineScheduler::hash_state` pine_state_hash.cpp:815); 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 when this plan was written (on this tree 543 `tests/*.cpp` name `source::PineStrategyHost`, 110 `NativeStrategyHost`) | native-only tests in every lane (§3.1 b) | O | | P6 | **Stream vs batch.** The stream refuses monthly input, mixed modes, in-session gaps, security feeds and source setters, and tick input under an FX curve (any non-empty curve until audit lane N6) (`native-engine.md`, "Stream limitations (current)") | L5 and L6 must state which refusals are generic and which are deliberate source limits | S | | P7 | **C ABI variant growth** | §2.iii hardening rules | S | -| P8 | **Unverified in the inputs:** whether `execution::LifecycleEffects` (execution.hpp:136-139) is ever non-empty from the Pine path, else `PlacementSnapshot::legs` (pine_adapter.hpp:364) is the only live lifecycle (O); the `strategy.risk.allow_entry_in` setter chain beyond `set_pine_risk_direction` pine_strategy_host.cpp:886 → `set_risk_direction` pine_adapter.cpp:26474 (O); what the ABI receipts pin (F); the probe corpus itself lives outside this tree (F) | verify inside the lane that touches each | F O | +| P8 | **Unverified in the inputs:** whether `execution::LifecycleEffects` (execution.hpp:136-139) is ever non-empty from the Pine path, else `PlacementSnapshot::legs` (pine_adapter.hpp:364) is the only live lifecycle (O); the `strategy.risk.allow_entry_in` setter chain beyond `set_pine_risk_direction` pine_strategy_host.cpp:886 → `set_risk_direction` pine_adapter.cpp:26490 (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) diff --git a/include/pineforge/source/market_admission.hpp b/include/pineforge/source/market_admission.hpp index 36c0adbac..b927bba08 100644 --- a/include/pineforge/source/market_admission.hpp +++ b/include/pineforge/source/market_admission.hpp @@ -6,6 +6,7 @@ // translation unit consumed it). It is hashed through the source host's // extension fold (src/source/pine_state_hash.cpp), never by the kernel. #include +#include #include #include #include @@ -187,6 +188,14 @@ void reflect(const Event& value, const std::string& path, const FieldVisitor& vi class Allocation; class Journal { public: + // The retired window's v4 fold, not a second event history. The rejection + // bar is an index into the same folded observations, for the command gate. + struct Prefix { + std::size_t count = 0; + uint64_t digest = 1469598103934665603ULL; + uint64_t last_sequence = 0; + int last_rejected_command_bar = -1; + }; Journal() = default; Journal(const Journal& other); Journal& operator=(const Journal& other); @@ -204,6 +213,19 @@ class Journal { // Keep actual causes still referenced by history, instructions or settlement. void retain(const std::vector& sequences); void reflect(const std::string& path, const FieldVisitor& visit) const; + const Prefix& retired_prefix() const { return retired_prefix_; } + std::size_t event_count() const { return retired_prefix_.count + events_.size(); } + bool has_unfinished_events() const { + return active_allocations_ != 0 || !outstanding_sequences_.empty(); + } + // Explicit diagnostic projection, like the native Full event observer. + // Install before a run; keeps original reflection paths for tools which + // deliberately inspect every event. Execution and v4 hash bytes are equal + // under both storage policies. Ordinary hosts always use the window. + void retain_full_observation_history(); + // Called only at a completed script-bar boundary, after every retained + // event has entered the v4 fold and no producer can insert before it. + void retire_prefix(std::size_t count, uint64_t digest, uint64_t last_sequence); private: friend class Allocation; void release_allocation(uint64_t sequence) noexcept; @@ -211,6 +233,10 @@ class Journal { uint64_t active_allocations_ = 0; std::vector outstanding_sequences_; std::vector events_; + Prefix retired_prefix_; + // Observation-only configuration, persistent across reset like recording; + // not independent broker state and deliberately absent from the v4 fold. + bool retain_full_observation_history_ = false; }; // Ephemeral ownership of an unfinished event. Completed/reclaimed IDs cannot // be re-created by keeping this handle: append consumes the journal allocation. diff --git a/include/pineforge/source/pine_adapter.hpp b/include/pineforge/source/pine_adapter.hpp index f51067868..f21c170eb 100644 --- a/include/pineforge/source/pine_adapter.hpp +++ b/include/pineforge/source/pine_adapter.hpp @@ -2748,11 +2748,11 @@ class PineExecutionAdapter { // folded once into exit_phase_digest_; a write below the mark refolds. std::size_t exit_phase_final_ = 0; std::uint64_t exit_phase_digest_ = 1469598103934665603ULL; - // R5 lane V19-E: the admission journal's events as the v4 fold reads them: - // a running digest over its first admission_events_folded_ events (the - // last of them carrying sequence admission_events_last_), extended over - // the events appended since, refolded when one arrived before them or one - // was removed. + // The v4 admission fold uses global event counts. Its retired prefix is + // the restart point; the window may still change within a command/bar. + // Folding at each script-bar end keeps recording-off runs bounded too. + // admission_events_last_ validates the boundary without indexing a row + // below the journal's retired prefix. mutable std::size_t admission_events_folded_ = 0; mutable std::uint64_t admission_events_last_ = 0; mutable std::uint64_t admission_events_digest_ = 1469598103934665603ULL; @@ -2894,6 +2894,9 @@ class PineExecutionAdapter { double price) const; // Item 3: a flat market-and-priced pair at a process_orders_on_close close. bool fill_pooc_close_pair(double raw_close, const NativeDecisionContext& context); + void finish_admission_bar(); + void on_bar_close_policy(const Bar&, const NativeDecisionContext&); + void fold_admission_events() const; }; namespace detail { diff --git a/include/pineforge/source/pine_strategy_host.hpp b/include/pineforge/source/pine_strategy_host.hpp index 3472d8693..937590ff7 100644 --- a/include/pineforge/source/pine_strategy_host.hpp +++ b/include/pineforge/source/pine_strategy_host.hpp @@ -1298,6 +1298,8 @@ class PineStrategyHost : public NativeStrategyHost, public BrokerStateHashProvid // hold_script_average_price() wrote position_entry_price_ on the flat // book this pass; release_script_average_price() clears it. bool script_average_held_ = false; + void finish_applied_admission(const native_order::ExecutionAppliedEvent&, + const NativeDecisionContext&); }; using PineNativeHost = PineStrategyHost; diff --git a/scripts/market_admission_schema.json b/scripts/market_admission_schema.json index 32501c518..363b9111e 100644 --- a/scripts/market_admission_schema.json +++ b/scripts/market_admission_schema.json @@ -131,7 +131,9 @@ "next_sequence_": "uint64_t", "active_allocations_": "uint64_t", "outstanding_sequences_": "std::vector", - "events_": "std::vector" + "events_": "std::vector", + "retired_prefix_": "Prefix", + "retain_full_observation_history_": "bool" }, "CurrentPrices": { "limit": "double", diff --git a/src/compat/pine/market_admission.cpp b/src/compat/pine/market_admission.cpp index 08d5200af..6f2a5236d 100644 --- a/src/compat/pine/market_admission.cpp +++ b/src/compat/pine/market_admission.cpp @@ -148,7 +148,7 @@ int last_rejected_command_bar(const admission::Journal& journal) { default:break; } } - return -1; + return journal.retired_prefix().last_rejected_command_bar; } History admission_history(const admission::Journal& journal){return fold_admission_history(journal.events());} } // namespace pineforge::compat::pine diff --git a/src/source/market_admission.cpp b/src/source/market_admission.cpp index d911183b8..4223007f2 100644 --- a/src/source/market_admission.cpp +++ b/src/source/market_admission.cpp @@ -80,6 +80,8 @@ Journal::Journal(const Journal& other) { next_sequence_ = other.next_sequence_; outstanding_sequences_ = other.outstanding_sequences_; events_ = other.events_; + retired_prefix_ = other.retired_prefix_; + retain_full_observation_history_ = other.retain_full_observation_history_; } Journal& Journal::operator=(const Journal& other) { if (this == &other) return *this; @@ -90,6 +92,8 @@ Journal& Journal::operator=(const Journal& other) { std::swap(next_sequence_, copy.next_sequence_); outstanding_sequences_.swap(copy.outstanding_sequences_); events_.swap(copy.events_); + std::swap(retired_prefix_, copy.retired_prefix_); + std::swap(retain_full_observation_history_, copy.retain_full_observation_history_); return *this; } Journal::Journal(Journal&& other) { @@ -99,9 +103,13 @@ Journal::Journal(Journal&& other) { next_sequence_ = other.next_sequence_; outstanding_sequences_ = std::move(other.outstanding_sequences_); events_ = std::move(other.events_); + retired_prefix_ = other.retired_prefix_; + retain_full_observation_history_ = other.retain_full_observation_history_; other.outstanding_sequences_.clear(); other.events_.clear(); other.next_sequence_ = 1; + other.retired_prefix_ = {}; + other.retain_full_observation_history_ = false; } Journal& Journal::operator=(Journal&& other) { if (this == &other) return *this; @@ -112,6 +120,8 @@ Journal& Journal::operator=(Journal&& other) { std::swap(next_sequence_, moved.next_sequence_); outstanding_sequences_.swap(moved.outstanding_sequences_); events_.swap(moved.events_); + std::swap(retired_prefix_, moved.retired_prefix_); + std::swap(retain_full_observation_history_, moved.retain_full_observation_history_); return *this; } uint64_t Journal::next_sequence() { @@ -162,7 +172,46 @@ void Journal::reset() { if (!outstanding_sequences_.empty() || active_allocations_ != 0) throw coded(RunFailureCode::engine_invariant, {}, "cannot reset admission journal with unfinished events"); - events_.clear();next_sequence_=1; + events_.clear();next_sequence_=1;retired_prefix_={}; +} +void Journal::retain_full_observation_history() { + if (next_sequence_ != 1 || has_unfinished_events() || !events_.empty() + || retired_prefix_.count != 0) + throw coded(RunFailureCode::engine_invariant, {}, + "full admission observation requires a fresh journal"); + retain_full_observation_history_ = true; +} +void Journal::retire_prefix(std::size_t count, uint64_t digest, uint64_t last_sequence) { + if (!outstanding_sequences_.empty() || active_allocations_ != 0) + throw coded(RunFailureCode::engine_invariant, {}, + "cannot retire admission journal with unfinished events"); + if (events_.size() > std::numeric_limits::max() - retired_prefix_.count + || count != retired_prefix_.count + events_.size() + || last_sequence != (events_.empty() ? retired_prefix_.last_sequence + : sequence(events_.back())) + || (events_.empty() && digest != retired_prefix_.digest)) + throw coded(RunFailureCode::engine_invariant, {}, + "admission retirement requires the exact folded window"); + if (retain_full_observation_history_) return; + auto prefix = retired_prefix_; + for (const auto& event : events_) + if (const auto* command = std::get_if(&event)) { + switch (command->outcome) { + case Outcome::RejectedIntradayCap: case Outcome::RejectedFrozenMarketCap: + case Outcome::RejectedAffordability: case Outcome::RejectedPricedCap: + case Outcome::OpeningRejectedReductionAdmitted: + prefix.last_rejected_command_bar = command->observation->bar; + break; + default: break; + } + } + prefix.count = count; + prefix.digest = digest; + prefix.last_sequence = last_sequence; + retired_prefix_ = prefix; + // Clear destroys all variant payloads, including before/book/review vectors + // and shared observations. Capacity is bounded by the largest live bar. + events_.clear(); } CommandCapture::CommandCapture(Allocation allocation,CommandObservation input,std::vector before, std::function complete):allocation_(std::move(allocation)),input_(std::move(input)),before_(std::move(before)),complete_(std::move(complete)) { @@ -301,6 +350,14 @@ void Journal::reflect(const std::string& path,const FieldVisitor& visit)const { r.field(path,"active_allocations",active_allocations_); r.array(outstanding_sequences_,path+".outstanding_sequences",[&](auto sequence,const auto& p){r.field(p,"sequence",sequence);}); r.array(events_,path+".events",[&](const auto& event,const auto& p){r.event(event,p);}); + // These are the original event fold and its derived lookup index, not new + // broker hash bytes. The v4 count/digest already covers their observations. + if (retired_prefix_.count != 0) { + r.field(path+".base","count",uint64_t(retired_prefix_.count)); + r.field(path+".base","digest",retired_prefix_.digest); + r.field(path+".base","last_sequence",retired_prefix_.last_sequence); + r.field(path+".base","last_rejected_command_bar",retired_prefix_.last_rejected_command_bar); + } } std::vector fields(const Draft& draft) { std::vector result;reflect(draft,"draft",[&](const auto& field){result.push_back(field);});return result; diff --git a/src/source/pine_adapter.cpp b/src/source/pine_adapter.cpp index e61431228..1b5c05766 100644 --- a/src/source/pine_adapter.cpp +++ b/src/source/pine_adapter.cpp @@ -24046,6 +24046,22 @@ void PineExecutionAdapter::on_bar_close_before_script( void PineExecutionAdapter::on_bar_close( const Bar& bar, const NativeDecisionContext& context) { + on_bar_close_policy(bar, context); + if (context.is_terminal_sub_bar) { + // Normal policy returns (quiet bars and margin checkpoints included) + // all reach this boundary. Exceptions leave unfinished events intact. + finish_admission_bar(); + } +} + +void PineExecutionAdapter::finish_admission_bar() { + fold_admission_events(); + admission_journal.retire_prefix(admission_events_folded_, + admission_events_digest_, admission_events_last_); +} + +void PineExecutionAdapter::on_bar_close_policy( + const Bar& bar, const NativeDecisionContext& context) { // Quiet-bar gates (pine_quiet_bar.hpp), as at the opening. None fires on // an unbound adapter: its hooks still refuse through require_host(). using detail::QuietHook; diff --git a/src/source/pine_state_hash.cpp b/src/source/pine_state_hash.cpp index f21a63c8a..5b5381fdf 100644 --- a/src/source/pine_state_hash.cpp +++ b/src/source/pine_state_hash.cpp @@ -753,18 +753,38 @@ void source::PineExecutionAdapter::hash_state(BrokerStateHashSink& f) const { } f.u(cap.next_action()); f.b(priority.attached()); f.b(priority.retained_parent_first()); - // v4: the journal's events as a running digest, each folded once, and its - // next sequence. Its outstanding allocations and active captures live - // inside one command and are empty at every hash point. - const auto& events = admission_journal.events(); - if (admission_events_folded_ > events.size() - || (admission_events_folded_ != 0 - && admission::sequence(events[admission_events_folded_ - 1]) + // The eager boundary fold and this query share the original v4 byte loop. + // A retired prefix substitutes for its events, without adding hash bytes. + fold_admission_events(); + f.u(admission_journal.sequence_frontier()); + f.u(admission_events_folded_); f.u(admission_events_digest_); +} + +void source::PineExecutionAdapter::fold_admission_events() const { + const auto& base = admission_journal.retired_prefix(); + const auto& window = admission_journal.events(); + // The v4 cursor is global, whereas the vector now begins at base.count. + // Retain the original per-event expression and byte loop on this view. + struct GlobalEvents { + const std::vector& window; + std::size_t base; + const admission::Event& operator[](std::size_t index) const { + return window[index - base]; + } + }; + const GlobalEvents events{window, base.count}; + const auto total = base.count + window.size(); + if (admission_events_folded_ < base.count || admission_events_folded_ > total + || (admission_events_folded_ == base.count + ? (admission_events_last_ != base.last_sequence + || admission_events_digest_ != base.digest) + : admission::sequence(events[admission_events_folded_ - 1]) != admission_events_last_)) { - admission_events_folded_ = 0; - admission_events_digest_ = 1469598103934665603ULL; + admission_events_folded_ = base.count; + admission_events_digest_ = base.digest; + admission_events_last_ = base.last_sequence; } - for (; admission_events_folded_ < events.size(); ++admission_events_folded_) { + for (; admission_events_folded_ < total; ++admission_events_folded_) { BrokerStateHashSink event_fold; event_fold.h = admission_events_digest_; admission::reflect(events[admission_events_folded_], "event", @@ -772,8 +792,6 @@ void source::PineExecutionAdapter::hash_state(BrokerStateHashSink& f) const { admission_events_digest_ = event_fold.h; admission_events_last_ = admission::sequence(events[admission_events_folded_]); } - f.u(admission_journal.sequence_frontier()); - f.u(admission_events_folded_); f.u(admission_events_digest_); } std::uint64_t source::PineExecutionAdapter::fold_dropped_close( diff --git a/src/source/pine_strategy_host.cpp b/src/source/pine_strategy_host.cpp index 92add35ae..23a7a7825 100644 --- a/src/source/pine_strategy_host.cpp +++ b/src/source/pine_strategy_host.cpp @@ -794,8 +794,8 @@ void source::PineStrategyHost::on_native_applied( // recalculation next, in the same drain iteration and at the same cursor // (NativeCalculationTrigger::BarCloseAndFills). The range-end row stays // after it, exactly where the adapter's own cascade used to put it. - if (!scheduler_.coof_recalculation_due(event, context, *this)) - record_applied_range_end(); + // Finish the same range-end mark, then any post-calculation journal tail. + finish_applied_admission(event, context); } void source::PineStrategyHost::record_applied_range_end() { @@ -3070,4 +3070,21 @@ int64_t source::PineStrategyHost::pine_time_offset(int64_t bar_open_ms, int bars return close ? tf_close_of(open) : open; } +void source::PineStrategyHost::finish_applied_admission( + const native_order::ExecutionAppliedEvent& event, + const NativeDecisionContext& context) { + const bool recalculation_due = scheduler_.coof_recalculation_due(event, context, *this); + if (!recalculation_due) record_applied_range_end(); + // A process-on-close fill can settle after on_native_bar captured the + // script's row. Its adapter receipts and final book belong to that row, + // not to the following bar. A synchronous fill inside an unfinished + // command is folded by the enclosing on_bar_close instead. + if (!recalculation_due && context.is_terminal_sub_bar + && context.coordinate.provenance == NativePriceProvenance::AfterCalculationClose + && !adapter_.admission_journal.has_unfinished_events()) { + adapter_.finish_admission_bar(); + capture_script_continuation_hash(); + } +} + } // namespace pineforge diff --git a/tests/test_adapter_live_state_equivalence.cpp b/tests/test_adapter_live_state_equivalence.cpp index dfa071f28..cc29a9bb1 100644 --- a/tests/test_adapter_live_state_equivalence.cpp +++ b/tests/test_adapter_live_state_equivalence.cpp @@ -354,6 +354,9 @@ struct Fold { class ScriptHost final : public source::PineStrategyHost { public: explicit ScriptHost(const Config& config) : config_(config), rng_(config.seed) { + // This placement-retirement witness compares every historical admission + // reflection leaf to its original literals, so opt into that projection. + market_admission_journal().retain_full_observation_history(); set_syminfo_timezone("UTC"); set_syminfo_session("24x7"); if (config.family == Family::Revival) { diff --git a/tests/test_admission_journal_window.cpp b/tests/test_admission_journal_window.cpp index f63fa8d57..112a0a84a 100644 --- a/tests/test_admission_journal_window.cpp +++ b/tests/test_admission_journal_window.cpp @@ -11,6 +11,9 @@ #include #include #include +#include +#include +#include #include #include @@ -180,6 +183,251 @@ void rejection_reader_survives_retirement_and_reset() { CHECK(host.market_admission_journal().events().empty()); CHECK(compat::pine::last_rejected_command_bar(host.market_admission_journal()) == 0); } + +// Test-only access to the v4 cache, using the same explicit-instantiation +// access pattern as the existing admission mutation controls. +template struct Access { + friend auto access(Tag) { return Member; } +}; +#define MEMBER(Tag, field) \ + struct Tag { friend auto access(Tag); }; \ + template struct Access +MEMBER(Fold, fold_admission_events); +MEMBER(Count, admission_events_folded_); +MEMBER(Digest, admission_events_digest_); +MEMBER(Last, admission_events_last_); +#undef MEMBER + +std::uint64_t reference_fold(std::uint64_t digest, const admission::Event& event) { + BrokerStateHashSink sink; + sink.h = digest; + admission::reflect(event, "event", [&](const admission::Field& field) { + sink.s(field.path); + sink.u(field.value.index()); + std::visit([&](const auto& value) { + using T = std::decay_t; + if constexpr (std::is_same_v) sink.u(value); + else if constexpr (std::is_same_v) sink.i(value); + else if constexpr (std::is_same_v) sink.d(value); + else sink.s(value); + }, field.value); + }); + return sink.h; +} + +void fold(source::PineExecutionAdapter& adapter) { + (adapter.*access(Fold{}))(); +} +void retire(source::PineExecutionAdapter& adapter) { + adapter.admission_journal.retire_prefix(adapter.*access(Count{}), + adapter.*access(Digest{}), adapter.*access(Last{})); +} +template void invariant(Fn&& call) { + bool rejected = false; + try { call(); } + catch (const std::logic_error&) { rejected = true; } + CHECK(rejected); +} + +admission::CommandEvent command(admission::Journal& journal, int bar, + admission::Outcome outcome = admission::Outcome::Admitted) { + auto observation = std::make_shared(); + observation->command = journal.next_sequence(); + observation->bar = bar; + observation->id = std::string(128, 'a') + std::to_string(bar); + observation->requested_quantity = 2.5; + admission::CommandEvent event; + event.observation = std::move(observation); + event.outcome = outcome; + event.removed = {13, 17}; + return event; +} + +void variants_fold_once_and_release_payloads() { + source::PineExecutionAdapter adapter; + auto& journal = adapter.admission_journal; + std::uint64_t expected = 1469598103934665603ULL; + for (int bar = 0; bar < 128; ++bar) { + std::weak_ptr lifetime; + { + const auto gap = journal.next_sequence(); + journal.abandon(gap); // Sequence frontier is not the event count. + auto opening = command(journal, bar, admission::Outcome::RejectedAffordability); + lifetime = opening.observation; + expected = reference_fold(expected, opening); + const auto origin = opening.observation; + journal.append(std::move(opening)); + admission::ReviewEvent review; + review.receipt = {journal.next_sequence(), admission::Checkpoint::DefaultGross, bar, 0}; + admission::BookObservation book; + book.draft.bind(origin); + book.bar = bar; + book.id = origin->id; + review.book.push_back(book); + review.reviewed.push_back(book); + review.causes = {origin->command}; + expected = reference_fold(expected, review); + journal.append(std::move(review)); + admission::SizingEvent sizing; + sizing.receipt = {journal.next_sequence(), 21, bar, origin->command}; + sizing.before.quantity = 2.5; + sizing.after.quantity = 1.25; + expected = reference_fold(expected, sizing); + journal.append(std::move(sizing)); + } + CHECK(!lifetime.expired()); + fold(adapter); + fold(adapter); + CHECK(adapter.*access(Digest{}) == expected); + CHECK(adapter.*access(Count{}) == static_cast((bar + 1) * 3)); + retire(adapter); + CHECK(journal.events().empty()); + CHECK(lifetime.expired()); + CHECK(journal.retired_prefix().digest == expected); + CHECK(journal.retired_prefix().count == static_cast((bar + 1) * 3)); + CHECK(compat::pine::last_rejected_command_bar(journal) == bar); + fold(adapter); + CHECK(adapter.*access(Digest{}) == expected); + } + const auto& prefix = journal.retired_prefix(); + CHECK(prefix.last_sequence == journal.sequence_frontier() - 1); + bool count = false, digest = false, bar = false; + journal.reflect("journal", [&](const admission::Field& field) { + if (field.path == "journal.base.count") + count = std::get(field.value) == prefix.count; + if (field.path == "journal.base.digest") + digest = std::get(field.value) == prefix.digest; + if (field.path == "journal.base.last_rejected_command_bar") + bar = std::get(field.value) == 127; + }); + CHECK(count && digest && bar); +} + +void unfinished_captures_and_window_restart() { + source::PineExecutionAdapter adapter; + auto& journal = adapter.admission_journal; + auto first = command(journal, 0); + std::uint64_t expected = reference_fold(1469598103934665603ULL, first); + journal.append(std::move(first)); + fold(adapter); + retire(adapter); + const auto base = journal.retired_prefix(); + { + auto allocation = journal.reserve(); + auto observation = std::make_shared(); + observation->command = allocation.sequence(); + admission::CommandEvent event; + event.observation = std::move(observation); + expected = reference_fold(expected, event); + journal.append(std::move(event)); + fold(adapter); + invariant([&] { retire(adapter); }); + CHECK(journal.retired_prefix().count == base.count); + } + retire(adapter); + CHECK(journal.retired_prefix().digest == expected); + auto outer = command(journal, 1); + auto inner = command(journal, 1); + journal.append(inner); + fold(adapter); + invariant([&] { retire(adapter); }); // The earlier allocation is unfinished. + journal.append(outer); + expected = reference_fold(reference_fold(expected, outer), inner); + fold(adapter); + CHECK(adapter.*access(Digest{}) == expected); + retire(adapter); + + const auto stable = journal.retired_prefix(); + auto removed = command(journal, 2); + auto kept = command(journal, 2); + journal.append(removed); + journal.append(kept); + fold(adapter); + journal.retain({kept.observation->command}); + fold(adapter); // Restart from the nonzero retired base, not the FNV seed. + CHECK(adapter.*access(Count{}) == stable.count + 1); + CHECK(adapter.*access(Digest{}) == reference_fold(stable.digest, kept)); + retire(adapter); + invariant([&] { journal.retire_prefix(0, 0, 0); }); + CHECK(journal.retired_prefix().count == stable.count + 1); + const admission::Journal replacement = journal; + journal.reset(); + fold(adapter); + CHECK(adapter.*access(Count{}) == 0); + CHECK(adapter.*access(Digest{}) == 1469598103934665603ULL); + journal = replacement; + fold(adapter); + CHECK(adapter.*access(Digest{}) == replacement.retired_prefix().digest); +} + +void retired_prefix_value_semantics() { + source::PineExecutionAdapter adapter; + auto& journal = adapter.admission_journal; + journal.append(command(journal, 4, admission::Outcome::RejectedPricedCap)); + fold(adapter); + retire(adapter); + const auto check_prefix = [&](const admission::Journal& value) { + CHECK(value.events().empty()); + CHECK(value.retired_prefix().count == 1); + CHECK(value.retired_prefix().digest == journal.retired_prefix().digest); + CHECK(value.retired_prefix().last_sequence == journal.retired_prefix().last_sequence); + CHECK(compat::pine::last_rejected_command_bar(value) == 4); + }; + admission::Journal copied(journal); + check_prefix(copied); + admission::Journal assigned; + assigned = journal; + check_prefix(assigned); + admission::Journal moved(std::move(copied)); + check_prefix(moved); + CHECK(copied.retired_prefix().count == 0 && copied.sequence_frontier() == 1); + admission::Journal move_assigned; + move_assigned = std::move(assigned); + check_prefix(move_assigned); + CHECK(assigned.retired_prefix().count == 0 && assigned.sequence_frontier() == 1); + moved.reset(); + CHECK(moved.retired_prefix().count == 0); + CHECK(moved.retired_prefix().digest == 1469598103934665603ULL); + CHECK(moved.retired_prefix().last_sequence == 0); + CHECK(compat::pine::last_rejected_command_bar(moved) == -1); + source::PineExecutionAdapter other; + other.admission_journal.append(command(other.admission_journal, 9)); + fold(other); + retire(other); + CHECK(other.admission_journal.retired_prefix().count == journal.retired_prefix().count); + CHECK(other.admission_journal.retired_prefix().last_sequence + == journal.retired_prefix().last_sequence); + journal = other.admission_journal; + fold(adapter); + CHECK(adapter.*access(Digest{}) == other.admission_journal.retired_prefix().digest); +} + +void full_observation_preserves_execution() { + const auto bars = chart(64); + TradingHost window; + TradingHost full; + full.market_admission_journal().retain_full_observation_history(); + window.set_broker_state_hash_recording(true); + full.set_broker_state_hash_recording(true); + window.run(bars.data(), 64, "1", "1"); + full.run(bars.data(), 64, "1", "1"); + CHECK(window.last_error().empty() && full.last_error().empty()); + CHECK(window.market_admission_journal().events().empty()); + CHECK(!full.market_admission_journal().events().empty()); + CHECK(window.market_admission_journal().event_count() + == full.market_admission_journal().event_count()); + CHECK(window.broker_state_hash() == full.broker_state_hash()); + ReportC a{}, b{}; + window.fill_report(&a); + full.fill_report(&b); + CHECK(a.broker_state_hash_len == 64 && b.broker_state_hash_len == 64); + if (a.broker_state_hash_len == 64 && b.broker_state_hash_len == 64) + for (int i = 0; i < 64; ++i) CHECK(a.broker_state_hash[i] == b.broker_state_hash[i]); + CHECK(a.total_trades == b.total_trades && a.total_trades == 32); + BacktestEngine::free_report(&a); + BacktestEngine::free_report(&b); + invariant([&] { window.market_admission_journal().retain_full_observation_history(); }); +} } // namespace int main() { @@ -188,6 +436,10 @@ int main() { {"recording and prefix identity", recording_and_prefix_identity}, {"split script boundaries", split_script_boundaries}, {"rejection reader and reset", rejection_reader_survives_retirement_and_reset}, + {"v4 variants and payload release", variants_fold_once_and_release_payloads}, + {"unfinished captures and restart", unfinished_captures_and_window_restart}, + {"prefix value semantics", retired_prefix_value_semantics}, + {"full observation execution identity", full_observation_preserves_execution}, }; for (const auto& test : cases) { std::printf("CASE %s\n", test.name); diff --git a/tests/test_native_market_admission_projection_l4d.cpp b/tests/test_native_market_admission_projection_l4d.cpp index 1dcd12a77..1b305a2f1 100644 --- a/tests/test_native_market_admission_projection_l4d.cpp +++ b/tests/test_native_market_admission_projection_l4d.cpp @@ -25,7 +25,7 @@ int main() { AdmissionProbe probe; probe.run(bars.data(), static_cast(bars.size())); CHECK(probe.last_error().empty()); - CHECK(!probe.market_admission_journal().events().empty()); + CHECK(probe.market_admission_journal().event_count() > 0); CHECK(!probe.market_admission_fields().empty()); std::printf("native market-admission projection: %d failures\n", failures); return failures == 0 ? 0 : 1; From 7ffb83208c858dce3c1ddb5112b66fd596e8f98b Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Mon, 19 Oct 2026 03:37:30 +0800 Subject: [PATCH 21/35] fix(cr150): preserve recording-off continuation capture cadence --- src/source/pine_strategy_host.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/source/pine_strategy_host.cpp b/src/source/pine_strategy_host.cpp index 23a7a7825..e1a0b2730 100644 --- a/src/source/pine_strategy_host.cpp +++ b/src/source/pine_strategy_host.cpp @@ -3083,7 +3083,11 @@ void source::PineStrategyHost::finish_applied_admission( && context.coordinate.provenance == NativePriceProvenance::AfterCalculationClose && !adapter_.admission_journal.has_unfinished_events()) { adapter_.finish_admission_bar(); - capture_script_continuation_hash(); + // Retire on every completed bar, but keep the ordinary recording-off + // snapshot cadence: final batch point or the realtime stream point. + if (broker_state_hash_recording_ || scheduler_.terminal_source_bar() + || stream_phase_ == StreamPhase::REALTIME) + capture_script_continuation_hash(); } } From 8803600d89d352c50bb48626b5356e9ae44c81b7 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Mon, 19 Oct 2026 04:23:00 +0800 Subject: [PATCH 22/35] docs(cr150): preserve named C ABI history Restores "No new virtual, no epoch bump" and "No epoch bump or reordering occurs" from 0d8b203 and 70ddaa3; count context is inherited from 4c78f39. --- docs/pages/native-engine.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 7c12a05f3..86da227c0 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -3969,9 +3969,9 @@ hands the runtime a callback table and gets the same kernel back. virtual to the table. No new virtual, no epoch bump, and nothing about the established C ABI moves: the compiled-strategy runtime symbols and their counts are untouched, and `scripts/check_c_abi_runtime.py` pins the new set as -a second, disjoint inventory (70ddaa3). No epoch bump or reordering occurs: the borrowed +a second, disjoint inventory (history: 70ddaa3). No epoch bump or reordering occurs: the borrowed symbol-feed setter is an additive compiled-strategy runtime symbol, and -`scripts/check_c_abi_runtime.py` pins it alongside the established inventory (0d8b203). +`scripts/check_c_abi_runtime.py` pins it alongside the established inventory (history: 0d8b203). ```c #include From 004a05b56c99024082d2e9da3679a66b10bf7f9d Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Mon, 19 Oct 2026 05:43:11 +0800 Subject: [PATCH 23/35] fix(cr150): preserve hash capture boundaries during admission retirement --- src/source/pine_strategy_host.cpp | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/source/pine_strategy_host.cpp b/src/source/pine_strategy_host.cpp index e1a0b2730..fd833e362 100644 --- a/src/source/pine_strategy_host.cpp +++ b/src/source/pine_strategy_host.cpp @@ -3075,19 +3075,15 @@ void source::PineStrategyHost::finish_applied_admission( const NativeDecisionContext& context) { const bool recalculation_due = scheduler_.coof_recalculation_due(event, context, *this); if (!recalculation_due) record_applied_range_end(); - // A process-on-close fill can settle after on_native_bar captured the - // script's row. Its adapter receipts and final book belong to that row, - // not to the following bar. A synchronous fill inside an unfinished - // command is folded by the enclosing on_bar_close instead. + // A process-on-close fill can append admission events after on_native_bar. + // Retire that completed tail without recapturing the broker-state row: + // F0a preserves the existing recording and continuation boundaries. + // A synchronous fill inside an unfinished command is folded by the + // enclosing on_bar_close instead. if (!recalculation_due && context.is_terminal_sub_bar && context.coordinate.provenance == NativePriceProvenance::AfterCalculationClose && !adapter_.admission_journal.has_unfinished_events()) { adapter_.finish_admission_bar(); - // Retire on every completed bar, but keep the ordinary recording-off - // snapshot cadence: final batch point or the realtime stream point. - if (broker_state_hash_recording_ || scheduler_.terminal_source_bar() - || stream_phase_ == StreamPhase::REALTIME) - capture_script_continuation_hash(); } } From 4db535d62283df64b34381474f6034157c89cb98 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Mon, 19 Oct 2026 06:28:57 +0800 Subject: [PATCH 24/35] docs: refresh adapter boundary anchors --- docs/adr/0001-kernel-adapter-boundary.md | 26 ++++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index b8f589018..7e27fc0be 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -213,8 +213,8 @@ kernel and own these quirks, each at its site: - **Seams the adapter overrides.** The kernel is driven, not patched. `PineStrategyHost` overrides `resolve_execution_terms` (`pine_strategy_host.cpp:828`) for TV sizing and fill spelling; `validate_execution_precommit` (`pine_strategy_host.cpp:853`), whose - `validate_precommit` (`pine_adapter.cpp:16382`) returns `AdmitWithHostMargin` - (`pine_adapter.cpp:16586`) to own the opening margin decision; `on_native_tick` + `validate_precommit` (`pine_adapter.cpp:16395`) returns `AdmitWithHostMargin` + (`pine_adapter.cpp:16590`) to own the opening margin decision; `on_native_tick` (`pine_strategy_host.cpp:555`) / `on_native_applied` (`pine_strategy_host.cpp:627`) for `calc_on_order_fills` re-entry. It does not own lot excursions: since R5 lane H-THIN it keeps the kernel's `owns_lot_excursions()` default, so the kernel's sampler books every Pine lot's @@ -226,10 +226,10 @@ kernel and own these quirks, each at its site: lowering moved the excursion cells of 335 trades in 19 corpus probes: 324 now equal TradingView's, 11 moved closer, none farther (the private fixture set `e19_allin_trim` holds the tape of the all-in residual's own entry-bar call). `process_orders_on_close` becomes - `NativeCloseExecution::AfterCalculation` in the projected spec (`pine_adapter.cpp:2817-2818`); + `NativeCloseExecution::AfterCalculation` in the projected spec (`pine_adapter.cpp:2819-2820`); calc cadence and language publication are `PineScheduler`'s (`pine_scheduler.hpp:21`). - **Batching and open-order priority.** Same-bar command batching and its deferred queues - (`PendingSameBarCommand` pine_adapter.hpp:1884, `pending_bracket_legs_` pine_adapter.hpp:2518-2536); the retained parent-before-child ordering of live handles — an + (`PendingSameBarCommand` pine_adapter.hpp:1885, `pending_bracket_legs_` pine_adapter.hpp:2518-2536); the retained parent-before-child ordering of live handles — an exactly-shaped entry and its `from_entry` exit, re-created after a named cancel, on a flat book under `process_orders_on_close` (`src/compat/pine/order_priority.cpp:13-59`, applied by `update_l4c_priority` `pine_adapter.cpp:1364-1434`). Kernel priority is acceptance/incarnation order. @@ -239,7 +239,7 @@ kernel and own these quirks, each at its site: - **Trail and tick conventions.** The half-tick arm threshold measured against tick-quantized extremes while the raw running best is retained (`has_trail_request` `pine_adapter.cpp:12511-12516`); the half-tick trigger threshold (`source_trigger_threshold` pine_adapter.cpp:384) and raw-vs-booked fill spelling, behind the - adapter's terms seam (`resolve_terms` `pine_adapter.cpp:14867`). + adapter's terms seam (`resolve_terms` `pine_adapter.cpp:14868`). - **Money arithmetic.** Ten-significant-digit half-up money (`source_money_round` `pine_adapter.cpp:558-564`, twin `tv_money_round` `pine_policy_support.hpp:9-15`). - **Quantity dust.** After every applied execution the Pine host erases any lot of at most @@ -260,8 +260,8 @@ kernel and own these quirks, each at its site: (`select_reservation_growth_sources` src/compat/pine/reservation_expansion.cpp:9). - **Margin.** The kernel owns the margin *mechanism* — the level solve, the check points, the kernel request, its re-pricing, the receipt — and the adapter answers its three policy hooks - with TradingView's: `margin_check_allowed` (`pine_adapter.cpp:17853`) for the scheduling, - `resolve_margin_requirement` (`pine_adapter.cpp:17922`) for the ten-significant-digit money, + with TradingView's: `margin_check_allowed` (`pine_adapter.cpp:17854`) for the scheduling, + `resolve_margin_requirement` (`pine_adapter.cpp:17923`) for the ten-significant-digit money, and `resolve_margin_call_units` for the lot-floored 4x restore. That hook answers every call on purpose (R5 lane F7): TradingView floors the restore onto the lot grid before the multiple, so on a gridded tape the kernel's own `ShortfallMultiple 4` books 5 lots where TradingView @@ -272,7 +272,7 @@ kernel and own these quirks, each at its site: margin since F7), while the roll point measures at the remaining path's adverse mark (MG-FX: 0.4116 @ 97 there against TradingView's 0.3996 @ 100). What has no kernel check point at all stays adapter-side: the `process_orders_on_close` chronology exception - (`non_pooc_commissioned_short` `pine_adapter.cpp:24188`) and + (`non_pooc_commissioned_short` `pine_adapter.cpp:24189`) and the 1x-long money call — plus the TV admission scopes (`explicit_pair_scope` `src/compat/pine/market_admission.cpp:34-47`) and the review fold they feed (`awaits_pair_review` `:68-73`, `fold_admission_history` `:83-140`). @@ -333,9 +333,9 @@ kernel and own these quirks, each at its site: projects `NativeCalculationTrigger::BarCloseAndFills` with TradingView's guard literal as `max_recalculations_per_point`. What stays are the specifics COOF adds on top — the language-state snapshot/restore around a recalculation, the waypoint-only refill deferral - (`next_source_path_waypoint` `pine_adapter.cpp:10475`), the first-open execution chain and its + (`next_source_path_waypoint` `pine_adapter.cpp:10478`), the first-open execution chain and its own loop guard (`kFirstOpenLoopGuard` `pine_scheduler_native.cpp:866`), and the two fills Pine refuses to - recalculate on (`suppress_grouped_stop_recalc` `pine_adapter.cpp:6755`). + recalculate on (`suppress_grouped_stop_recalc` `pine_adapter.cpp:6758`). - **Pine language state and harness flags.** Series, the tick-level barstate flags and position-view freezing (`PineLanguageState` `pine_language_state.hpp:12`); the session flags, `PineStrategyHost` members since R5 lane F5 (`session_ismarket_` @@ -710,11 +710,11 @@ Every line number in this section names its symbol on this tree, which | `NativeRunSpec` field | ruling | what the adapter runs instead | measurement and ruling of record | executed native consumers | |---|---|---|---|---| | `price_grid`, `grid_rounding` | **native-only** | TradingView's per-order-kind tick rules on top of `None`: `source_trigger_threshold` (`pine_adapter.cpp:384`), `source_level_on_price_grid` (`:367`), `nearest_tick` (`:274`) / `source_bar_fill_tick` (`:295`) / `directional_tick` (`:327`), behind the terms seam | R5-3 and design risk E7 ruled `None` for the adapter before the lane ran; lanes R7 and N13 measured the alternative anyway (raw levels submitted, `QuantizeFillsAndTriggers` with `HalfUp` declared). L8b closed the first blocker (no run aborts). The second has no remedy on either side: TradingView quantizes per order kind (stop and limit legs and a trail's activation on the quantized bar; the trail stop, the running best, stop-limit entries and the `calc_on_order_fills` cursors raw), the grid is one rule for the run, and the old N13 trial moved 30 pinned checks in 4 units; this is historical evidence, not a current-head result; a per-kind mask would spell that inconsistency into the kernel. The corpus cannot arbitrate (every probe runs a 0.01 tick on an on-grid feed; 5 of 312 differ in an engine-only column). Permanent witness: `tests/test_adapter_grid_relower.cpp`, whose section 5 pins the trail stop the grid fires a bar early. Design row PG and §3.6 | `examples/native/native_price_grid_strategy.cpp`, `examples/native/native_price_grid_c.c`, the private test `test_native_price_grid` | -| `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:17504`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:20879`), `chart_day_key` (`pine_adapter.cpp:17401`), `compat::pine::IntradayCap` with `IntradayOrderBudget` (337 lines) | Structural first: Pine's risk calls are per-bar statements, so a limit reaches the adapter on script bar 0, after `project()` (`pine_strategy_host.cpp:350`) and the kernel's `configure` (`pine_strategy_host.cpp:359`) have fixed and digested the spec. In substance (lane N12, `tests/test_adapter_risk_relower.cpp`, 62 checks over nine paired scenarios): the drawdown latch samples at the close only and still admits a reversal; the loss-day streak counts trades, not days; the intraday loss closes at the path's adverse extreme, refuses every placement and withdraws the book; the fill cap charges slots, transfers quota and closes at the bar's better extreme on the chart timezone's day. With the kernel seeded on the corpus, 3 of 4 cap probes diverge (3840 of 3916, 312 of 604, 2370 of 2384 rows; re-derived by `scripts/check_seeded_risk_experiment.sh` since R5 lane H-MEASURE). `strategy.risk.allow_entry_in` is the one rule that is the kernel's already (`allowed_open_directions`). Design §3.6 | `examples/native/native_risk_limits_strategy.cpp`, `examples/native/native_trail_risk_strategy.cpp`, `tests/test_native_risk_limits.cpp`, `tests/test_native_c_api.c` | +| `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:17505`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:20880`), `chart_day_key` (`pine_adapter.cpp:17402`), `compat::pine::IntradayCap` with `IntradayOrderBudget` (337 lines) | Structural first: Pine's risk calls are per-bar statements, so a limit reaches the adapter on script bar 0, after `project()` (`pine_strategy_host.cpp:350`) and the kernel's `configure` (`pine_strategy_host.cpp:359`) have fixed and digested the spec. In substance (lane N12, `tests/test_adapter_risk_relower.cpp`, 62 checks over nine paired scenarios): the drawdown latch samples at the close only and still admits a reversal; the loss-day streak counts trades, not days; the intraday loss closes at the path's adverse extreme, refuses every placement and withdraws the book; the fill cap charges slots, transfers quota and closes at the bar's better extreme on the chart timezone's day. With the kernel seeded on the corpus, 3 of 4 cap probes diverge (3840 of 3916, 312 of 604, 2370 of 2384 rows; re-derived by `scripts/check_seeded_risk_experiment.sh` since R5 lane H-MEASURE). `strategy.risk.allow_entry_in` is the one rule that is the kernel's already (`allowed_open_directions`). Design §3.6 | `examples/native/native_risk_limits_strategy.cpp`, `examples/native/native_trail_risk_strategy.cpp`, `tests/test_native_risk_limits.cpp`, `tests/test_native_c_api.c` | | `max_abs_units` | **adapter-policy** | `strategy.risk.max_position_size` as a gate on the LIVE book before the fill (`max_position_size` `pine_adapter.cpp:16513-16514`): an entry is refused once the book already holds the limit | design row MG2 (R5-1): the resulting-book cap is the generic one. Measured by N12's scenario `PS` in `tests/test_adapter_risk_relower.cpp`: two-unit entries against a limit of 3 leave the adapter at 4 and the kernel cap at 2 | `tests/test_native_resting_matching_contract.cpp`, `tests/test_native_run_spec.cpp` | | `max_open_lots` | **adapter-policy** | Pine pyramiding is a per-cycle entry count in the adapter's command policy; a resting source entry must not consume a physical-lot cap before it fills, so `project()` leaves the cap unset (`max_open_lots` `pine_adapter.cpp:2646-2648`) | design row MG3 (R5-1); the contract comment in `project()`. Measured against TradingView by R5 lane H-MEASURE (the private test `test_pyramiding_count_differential`, 15 scenarios on three `lab tv` tapes): TradingView checks an entry once, at its first eligible point, against the trades then open, and does not check a resting entry again at its fill; the kernel's cap books 14 of the 15 as TradingView does, the adapter's per-cycle count 13 (it keeps a slot a close or another entry's exit drained; since lane W8A-SIGSTATE-1's R-B a resting LIMIT entry no longer counts, and one the count admitted fills over the cap, and since lane W5B-ENG-MARGIN-RESIDUAL's SB the later market entries of a flat bar are refused at the fill once the book holds the cap, as on TradingView's tapes). A recorded divergence: the retention stands pending a re-lowering ruling. R5 lane H-THIN tried the lowering and kept the count: on the kernel's cap four corpus probes moved away from TradingView, for want of the first-eligible-point rule (H-MEASURE's P4b); it fixed the reservation divergence measured beside it (P10), so a default `strategy.exit(from_entry)` under FIFO now reserves its entry's own quantity, the oldest lots first, as TradingView does | `tests/test_native_resting_matching_contract.cpp`, `tests/test_native_margin_model.cpp` | -| `initial_margin_fraction` | **adapter-policy** | TradingView's ten-significant-digit money admission against the signal-time tuple, answered as `AdmitWithHostMargin`; the `margin` model the adapter does declare is maintenance-only (`NativeMarginModel` `pine_adapter.cpp:2885-2893`, `margin.maintenance_long` `pine_adapter.cpp:2900`) | design row MG4 and the wave-4 ruling recorded in `project()`: a positive initial requirement would decline openings TradingView takes. Measured both ways by R5 lane H-MEASURE (the private test `test_adapter_margin_schedule_differential`, M11): the kernel's gate declines a 1x long whose entry fee the adapter admits (`InitialMargin`), and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times | `tests/test_native_precommit_view.cpp`, `tests/test_native_margin_model.cpp` | -| `report_open_position_at_end` | **adapter-policy** | The Pine host owns TradingView's range-end shape: batch re-marks the stored final point and re-folds extremes, while REALTIME retains ordinary marks and `PineStrategyHost::present_report` re-marks only the caller-owned copy before generic metric derivation; the stream hash excludes that terminal extreme fold (`scheduler_record_range_end` pine_strategy_host.cpp:1987): report shape, not a mark-to-market row (`KernelRecordedAtHostMarks` pine_adapter.cpp:2855) | design row RP5; the kernel reads the field under `KernelRecorded` only, which `scripts/check_adapter_spec_shadowing.py` gates | `examples/native/native_sized_report_strategy.cpp`, `tests/test_native_report_truth.cpp` | +| `initial_margin_fraction` | **adapter-policy** | TradingView's ten-significant-digit money admission against the signal-time tuple, answered as `AdmitWithHostMargin`; the `margin` model the adapter does declare is maintenance-only (`NativeMarginModel` `pine_adapter.cpp:2885-2893`, `margin.maintenance_long` `pine_adapter.cpp:2902`) | design row MG4 and the wave-4 ruling recorded in `project()`: a positive initial requirement would decline openings TradingView takes. Measured both ways by R5 lane H-MEASURE (the private test `test_adapter_margin_schedule_differential`, M11): the kernel's gate declines a 1x long whose entry fee the adapter admits (`InitialMargin`), and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times | `tests/test_native_precommit_view.cpp`, `tests/test_native_margin_model.cpp` | +| `report_open_position_at_end` | **adapter-policy** | The Pine host owns TradingView's range-end shape: batch re-marks the stored final point and re-folds extremes, while REALTIME retains ordinary marks and `PineStrategyHost::present_report` re-marks only the caller-owned copy before generic metric derivation; the stream hash excludes that terminal extreme fold (`scheduler_record_range_end` pine_strategy_host.cpp:1987): report shape, not a mark-to-market row (`KernelRecordedAtHostMarks` pine_adapter.cpp:2857) | design row RP5; the kernel reads the field under `KernelRecorded` only, which `scripts/check_adapter_spec_shadowing.py` gates | `examples/native/native_sized_report_strategy.cpp`, `tests/test_native_report_truth.cpp` | | `open_bar_view` | **adapter-policy** | `Complete`: TradingView's bar-open scheduling and its `calc_on_order_fills` callback read the whole script bar (`pine_adapter.cpp:2825-2827`) | design rows CT4 and E5: the open-only view is opt-in because the adapter needs the full bar | `examples/native/native_calc_on_fills_strategy.cpp`, `tests/test_native_calc_timing.cpp` | | `subscriptions` | **adapter-hook** | `declare_timeframe_subscriptions` from the begin-time hook (`pine_strategy_host.cpp:1717`): a plain `request.security` site is a kernel subscription | lane R3b over the L6c hook: 21 of the corpus's 23 `request.security` probes run their sites on the kernel, byte-identical. The predicate (`declare_security_sites_to_kernel` `pine_strategy_host.cpp:1708`) declares every site of a run or none. A run keeps all of its sites on the source evaluator when it is a stream's warmup, runs the bar magnifier, cuts its range start to na (KI-55), projects historical lookahead, installs an auxiliary feed, aggregates its chart or has no detected timeframe. It keeps them there too when any one site uses `lookahead_on`, `ticker.heikinashi`, a lower timeframe (requested, emulated, read from the input, or as an array), a publication gate, or a calling-bar close or open latch; when a forex or CFD intraday chart without a native feed asks for `D`; when a site asks for N days, whose bars TradingView counts in the symbol's trading dates from each year's first (`tests/test_security_multi_day_buckets_tapes.cpp`), not on the kernel's calendar-day grid; when the registry's `sec_id`s are not dense from 0; and when the kernel refuses the declaration. R5 lane H-MEASURE pins each reachable condition with what it changes (`tests/test_adapter_security_route_conditions.cpp`, a site through the evaluator against the same site through a bare kernel host): on the feeds measured the magnifier and an aggregated chart change no delivered value, while the range-start cut, the projection's incomplete tail, `lookahead_on` and the OTC pins do; the four lower-timeframe flags, the publish gate and the calling-bar latches are subsumed by other clauses, and the empty or undetected timeframe checks are unreachable after begin-time validation | `examples/native/native_htf_strategy.cpp`, `tests/test_native_htf_subscriptions.cpp` | | `auxiliary_feed` | **adapter-policy** | the adapter's own auxiliary drive: the chart-slice mapping and the deferred first bucket (`src/source/pine_aux_security.cpp`) | lane N7, retained on three measurements: 0 of 312 corpus probes install an auxiliary feed; TradingView's chart slice leaves pre-range coverage inert where the kernel folds it by time, and evaluates after the bar's matching pass where the kernel delivers before it (`tests/test_native_auxiliary_feed_twin.cpp`, rows B and C). Design §2.iv | `tests/test_native_auxiliary_feed.cpp`, `tests/test_native_auxiliary_feed_stream.cpp` | From cc2d00f4c3b0abc4b13b1666885bee189ffc5a2a Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Mon, 19 Oct 2026 06:34:59 +0800 Subject: [PATCH 25/35] docs: refresh native parity anchors --- docs/design/native-feature-parity.md | 148 +++++++++++++-------------- 1 file changed, 74 insertions(+), 74 deletions(-) diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 5b8622262..a61f11ff8 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:3445; `entry()` pine_adapter.cpp:7168, `order()` pine_adapter.cpp:14633) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:6623; `submit` native_host.hpp:1213 | K | — | F:A1 O:A1 S:O2 | — | -| OL2 | Intents: flatten, reduce by units, reduce owner-opened units, transact, reverse-to (`close_all()` pine_adapter.cpp:10552 → `Flatten{}` pine_adapter.cpp:10586; `Reduce{ExplicitUnits}` pine_adapter.cpp:9425; `ReverseTo` pine_adapter.cpp:7844) | **yes** — `OrderIntent` native_order.hpp:229, reduce sizes `ReductionSize` native_order.hpp:215, `ReverseTo` native_order.hpp:60-62, `OpeningShape` native_order.hpp:910-914, `Flatten` native_order.hpp:36 | K | — | F:A4 F:A8 O:A2 O:A3 S:O1 | — | -| OL3 | Amend / cancel one working order (`submit_or_replace` pine_adapter.cpp:4372 → `host.replace` pine_adapter.cpp:4797) | **yes** — `replace` native_host.hpp:1218-1220, `cancel` native_host.hpp:1249; `ReplacedEvent` native_order.hpp:1017-1031; result and event types `SubmitStatus` native_order.hpp:835-861, `ReplacedEvent` native_order.hpp:1001-1067 | K | — | F:A2 O:A4 S:O3 | id → handle map is host bookkeeping (F) → OL4 | -| OL4 | Cancel all / cancel by label; label → handle book (`cancel(id)` pine_adapter.cpp:14527, `cancel_all` pine_adapter.cpp:14600; `live_by_source_key_` pine_adapter.hpp:2524; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4810) | **partial** — single-handle `cancel` native_host.hpp:1249; no enumeration (`WorkingRequestCore::live()` is consumer-internal native_order.hpp:1703); labels exist but are unindexed (`Request::label` native_order.hpp:485) **Closed:** `cancel_all` native_host.hpp:1267, `cancel_where(comment)` native_host.hpp:1272 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1272 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1263 is the enumeration; the id → handle book is `native_toolkit::OrderBook`, host-side and Pine-free (`tests/test_native_order_ergonomics.cpp`, `tests/test_native_anchored_legs.cpp`). | K+A | L7 | F:A3 F:A2 O:A5 O:A14 S:O3 | **Disagree:** F partial / O no (A5) + partial (A14) / S yes (folded into O3). R5-1: cancel-all / by-label is generic → kernel. TV state inheritance across a same-id re-issue stays A. The adapter's `strategy.cancel` / `strategy.cancel_all` keep their per-handle loops, measured by R5 lane F7 (audit M27): TradingView cancels in the SOURCE book, which holds commands of the same script call that have not reached the kernel yet (and a live `strategy.close` is labelled `__close__` there), so at the same point `cancel_where(id, NativeRequestField::Label)` and `cancel_all()` withdraw 0 — the close flattens at the next open, the reversal lands — where the source cancels withdraw them (`tests/test_adapter_brackets_relower.cpp`, M27 rows); each withdrawn source handle is also retired one by one. | -| OL5 | Per-point fill capacity, partial fills, residual working quantity (Pine uses only `ImmediateRemaining` pine_adapter.cpp:5016; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:9171) | **yes** — `Capacity` / `PointBudget` native_order.hpp:379-383, `Remaining` native_order.hpp:557-579, allowances `Allowance` native_order.hpp:662-675 | K | — | O:A16 S:O4 | — | -| OL6 | Ownership: legs bound to a parent / opening(s) / cohort; close the lots of a given entry, FIFO or ANY (`owner_for_close` pine_adapter.cpp:6588, `cohort_for` pine_adapter.cpp:3019; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:9266) | **yes** — `Owner` native_order.hpp:464-479, `Authority` native_order.hpp:620-621, `CohortClose` native_order.hpp:617-619, `SelectedExposure` native_order.hpp:625-629, `cohort_open` native_host.hpp:1282, `CancelReason::OwnerGone` native_order.hpp:949 | K | — | F:A6 O:A8 O:A9 S:O5 | FIFO *ordering* is host-computed (O) | -| OL7 | OCA cancel / reduce groups (`group_for` pine_adapter.cpp:3454) | **yes** — `Group` / `GroupEffect` native_order.hpp:466-473, `CancelReason::Group` native_order.hpp:948-953, `ReservationReducedEvent` native_order.hpp:1191-1201 (deferred form `DeferredGroupAdjustmentEvent` native_order.hpp:1211-1221) | K | — | F:A12 O:A6 O:A7 S:O7 | — | +| OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:3447; `entry()` pine_adapter.cpp:7171, `order()` pine_adapter.cpp:14634) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:6623; `submit` native_host.hpp:1213 | K | — | F:A1 O:A1 S:O2 | — | +| OL2 | Intents: flatten, reduce by units, reduce owner-opened units, transact, reverse-to (`close_all()` pine_adapter.cpp:10555 → `Flatten{}` pine_adapter.cpp:10589; `Reduce{ExplicitUnits}` pine_adapter.cpp:9428; `ReverseTo` pine_adapter.cpp:7847) | **yes** — `OrderIntent` native_order.hpp:229, reduce sizes `ReductionSize` native_order.hpp:215, `ReverseTo` native_order.hpp:60-62, `OpeningShape` native_order.hpp:910-914, `Flatten` native_order.hpp:36 | K | — | F:A4 F:A8 O:A2 O:A3 S:O1 | — | +| OL3 | Amend / cancel one working order (`submit_or_replace` pine_adapter.cpp:4375 → `host.replace` pine_adapter.cpp:4800) | **yes** — `replace` native_host.hpp:1218-1220, `cancel` native_host.hpp:1249; `ReplacedEvent` native_order.hpp:1017-1031; result and event types `SubmitStatus` native_order.hpp:835-861, `ReplacedEvent` native_order.hpp:1001-1067 | K | — | F:A2 O:A4 S:O3 | id → handle map is host bookkeeping (F) → OL4 | +| OL4 | Cancel all / cancel by label; label → handle book (`cancel(id)` pine_adapter.cpp:14528, `cancel_all` pine_adapter.cpp:14601; `live_by_source_key_` pine_adapter.hpp:2525; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4813) | **partial** — single-handle `cancel` native_host.hpp:1249; no enumeration (`WorkingRequestCore::live()` is consumer-internal native_order.hpp:1703); labels exist but are unindexed (`Request::label` native_order.hpp:485) **Closed:** `cancel_all` native_host.hpp:1267, `cancel_where(comment)` native_host.hpp:1272 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1272 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1263 is the enumeration; the id → handle book is `native_toolkit::OrderBook`, host-side and Pine-free (`tests/test_native_order_ergonomics.cpp`, `tests/test_native_anchored_legs.cpp`). | K+A | L7 | F:A3 F:A2 O:A5 O:A14 S:O3 | **Disagree:** F partial / O no (A5) + partial (A14) / S yes (folded into O3). R5-1: cancel-all / by-label is generic → kernel. TV state inheritance across a same-id re-issue stays A. The adapter's `strategy.cancel` / `strategy.cancel_all` keep their per-handle loops, measured by R5 lane F7 (audit M27): TradingView cancels in the SOURCE book, which holds commands of the same script call that have not reached the kernel yet (and a live `strategy.close` is labelled `__close__` there), so at the same point `cancel_where(id, NativeRequestField::Label)` and `cancel_all()` withdraw 0 — the close flattens at the next open, the reversal lands — where the source cancels withdraw them (`tests/test_adapter_brackets_relower.cpp`, M27 rows); each withdrawn source handle is also retired one by one. | +| OL5 | Per-point fill capacity, partial fills, residual working quantity (Pine uses only `ImmediateRemaining` pine_adapter.cpp:5019; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:9174) | **yes** — `Capacity` / `PointBudget` native_order.hpp:379-383, `Remaining` native_order.hpp:557-579, allowances `Allowance` native_order.hpp:662-675 | K | — | O:A16 S:O4 | — | +| OL6 | Ownership: legs bound to a parent / opening(s) / cohort; close the lots of a given entry, FIFO or ANY (`owner_for_close` pine_adapter.cpp:6591, `cohort_for` pine_adapter.cpp:3021; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:9269) | **yes** — `Owner` native_order.hpp:464-479, `Authority` native_order.hpp:620-621, `CohortClose` native_order.hpp:617-619, `SelectedExposure` native_order.hpp:625-629, `cohort_open` native_host.hpp:1282, `CancelReason::OwnerGone` native_order.hpp:949 | K | — | F:A6 O:A8 O:A9 S:O5 | FIFO *ordering* is host-computed (O) | +| OL7 | OCA cancel / reduce groups (`group_for` pine_adapter.cpp:3456) | **yes** — `Group` / `GroupEffect` native_order.hpp:466-473, `CancelReason::Group` native_order.hpp:948-953, `ReservationReducedEvent` native_order.hpp:1191-1201 (deferred form `DeferredGroupAdjustmentEvent` native_order.hpp:1211-1221) | K | — | F:A12 O:A6 O:A7 S:O7 | — | | OL8 | Immediate execution at the current point (`immediately=true`) | **yes** — `inspect_current_execution` / `execute_current` native_host.hpp:1104-1105 | K | — | F:A9 | (single-source row; O:F2 cites the same call) | | OL9 | Birth eligibility: an order accepted at bar N cannot fill on N's already-delivered points | **yes** — `point_eligible` native_order.hpp:786-790, the generic birth record `OrderBirth` include/pineforge/order_birth.hpp:59, contract `docs/pages/native-engine.md` ("Acceptance is not a fill") | K+A | — | F:A13 O:A10 S:R4 | **Disagree:** F yes / O partial / S "no, TV quirk". R5-1: the generic core (birth cursor) is native; TV's bar-granular first-bar rule is the A half → OL14. | -| OL10 | TP/SL bracket per entry, armed before the entry fills, OCA between legs (`exit()` pine_adapter.cpp:11045; lowering `submit_one` pine_adapter.cpp:11923, `BindCohort` pine_adapter.cpp:12386, `submit_leg` pine_adapter.cpp:11679; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1940); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | -| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:11305; `materialize_relative_exits` pine_adapter.cpp:13994, pine_adapter.hpp:2262-2274) | **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:6551. `Trigger` itself is absolute-only native_order.hpp:296 **Closed:** `Request::anchor = FromOwnerFill{offset, ticks, rounding}` (`TriggerAnchor` native_order.hpp:336) defers the level to the owner's fill, the kernel materializes it once at the arm under `NativeAnchorRounding`, and `resolve_anchored_level` native_host.hpp:1012 is where a source language's own level arithmetic lives (`tests/test_native_anchored_legs.cpp`, `tests/test_native_arm_options.cpp`). | K | L7 | F:A11 O:A11 S:O6 | **Disagree:** F partial (toolkit is enough) / O no (kernel `TriggerAnchor`). R5-2: L7 delivers relative anchors; kernel anchor primary, toolkit materialization is the zero-kernel fallback. | -| OL12 | Working-order view for the host and outside observers (`strategy_pending_order_*`; `PendingIntentView` pine_adapter.hpp:1452-1487; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:27084) | **no** — base returns 0 / -1 `observe_last_bar_dual_entry_path_v1` engine_consumer.cpp:124-133; the POD is TV-shaped (`tv_carry_qty` pending_order_mirror.hpp:73, `pine_birth_reach` pending_order_mirror.hpp:135) **Closed for a bare host:** `native_working_requests()` native_host.hpp:1263 returns one owning `NativeWorkingRequest` native_host.hpp:681 per live request — definition, remaining, trigger state — with the C spelling `strategy_native_working_len_v1` / `_get_v1`. The TV-shaped POD stays frozen and stays the source layer's projection (`tests/test_native_limit_fill_through.cpp`, `tests/test_native_arm_options.cpp`, `tests/test_native_c_api.c`). | K+A | L7 (C++), L13 (C) | F:J5 O:J6 S:X4 | **Disagree:** O "partial + TV-shaped" / F, S no. A bare host reads zeros → no. R5-1: neutral view → kernel; POD shape and Pine journal retention stay A (POD frozen, §2.ii j). | -| OL13 | *(quirk)* Same-bar command batching and the seven deferred queues (`pending_bracket_legs_` pine_adapter.hpp:2518-2537); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1364); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:4203, `materialize_snapshot` pine_adapter.cpp:13968), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1360-1387, `DeferredOpenMarketableSell` pine_adapter.hpp:1848-1871, `CloseCallsiteState` pine_adapter.hpp:1986-2000; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:21638, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:21872) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:676-678 | A | — | F:A15 O:A15 S:O9 S:R3 | R5-1 names KI-62. S: the kernel exposes deterministic insertion order only. | -| OL14 | *(quirk)* Exit activation / suspension / revival barriers, historical birth reach (`select_exit_activation` exit_activation.cpp:44-90, enforced `leg_activation` pine_adapter.cpp:1261; `select_exit_activation` exit_activation.cpp:20-90, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:1078) | n/a | A | — | S:R4 O:A10 | TV half of OL9. **V19D-P1 (R5 lanes B-ADAPTER, V19-FIX):** a margin-call revival never restores an exit the script cancelled: `strategy.cancel` / `cancel_all` retire every leg of each exit row they name — the live ones they take off the book (caused at the Cancelled event, after anything the same callback did to the leg), the dormant ones, and the ones the adapter took off the book itself — and drop a gapped stop a declined reversal parked for the next margin-call slice (`retire_cancelled_exits` pine_adapter.cpp:2123); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:2200). What `tests/test_adapter_margin_revival_cancel.cpp` replays on the NYSE:F 15m chart of seven lab tv tapes: badapter-v19dp1-control (no cancel: the margin call closes 928 through X) and v19fix-gapped-stop-control (X's stop at 10.15, gapped by the 05-01 open and parked: the same close), and badapter-v19dp1-cancel, v19fix-cancel-before-pair, v19fix-cancel-at-1000, v19fix-gapped-stop-cancel and v19fix-gapped-stop-cancel-after-pair (a cancel on 05-01 09:30 while X is dormant, on 04-30 15:45 before the reversal pair and on 04-30 10:0 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:11048; lowering `submit_one` pine_adapter.cpp:11926, `BindCohort` pine_adapter.cpp:12389, `submit_leg` pine_adapter.cpp:11682; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1940); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | +| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:11307; `materialize_relative_exits` pine_adapter.cpp:13997, pine_adapter.hpp:2262-2274) | **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:6551. `Trigger` itself is absolute-only native_order.hpp:296 **Closed:** `Request::anchor = FromOwnerFill{offset, ticks, rounding}` (`TriggerAnchor` native_order.hpp:336) defers the level to the owner's fill, the kernel materializes it once at the arm under `NativeAnchorRounding`, and `resolve_anchored_level` native_host.hpp:1012 is where a source language's own level arithmetic lives (`tests/test_native_anchored_legs.cpp`, `tests/test_native_arm_options.cpp`). | K | L7 | F:A11 O:A11 S:O6 | **Disagree:** F partial (toolkit is enough) / O no (kernel `TriggerAnchor`). R5-2: L7 delivers relative anchors; kernel anchor primary, toolkit materialization is the zero-kernel fallback. | +| OL12 | Working-order view for the host and outside observers (`strategy_pending_order_*`; `PendingIntentView` pine_adapter.hpp:1452-1487; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:27085) | **no** — base returns 0 / -1 `observe_last_bar_dual_entry_path_v1` engine_consumer.cpp:124-133; the POD is TV-shaped (`tv_carry_qty` pending_order_mirror.hpp:73, `pine_birth_reach` pending_order_mirror.hpp:135) **Closed for a bare host:** `native_working_requests()` native_host.hpp:1263 returns one owning `NativeWorkingRequest` native_host.hpp:681 per live request — definition, remaining, trigger state — with the C spelling `strategy_native_working_len_v1` / `_get_v1`. The TV-shaped POD stays frozen and stays the source layer's projection (`tests/test_native_limit_fill_through.cpp`, `tests/test_native_arm_options.cpp`, `tests/test_native_c_api.c`). | K+A | L7 (C++), L13 (C) | F:J5 O:J6 S:X4 | **Disagree:** O "partial + TV-shaped" / F, S no. A bare host reads zeros → no. R5-1: neutral view → kernel; POD shape and Pine journal retention stay A (POD frozen, §2.ii j). | +| OL13 | *(quirk)* Same-bar command batching and the seven deferred queues (`pending_bracket_legs_` pine_adapter.hpp:2518-2537); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1366); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:4206, `materialize_snapshot` pine_adapter.cpp:13971), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1360-1387, `DeferredOpenMarketableSell` pine_adapter.hpp:1848-1871, `CloseCallsiteState` pine_adapter.hpp:1986-2000; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:21639, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:21873) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:676-678 | A | — | F:A15 O:A15 S:O9 S:R3 | R5-1 names KI-62. S: the kernel exposes deterministic insertion order only. | +| OL14 | *(quirk)* Exit activation / suspension / revival barriers, historical birth reach (`select_exit_activation` exit_activation.cpp:44-90, enforced `leg_activation` pine_adapter.cpp:1263; `select_exit_activation` exit_activation.cpp:20-90, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:1080) | 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:2125); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:2202). What `tests/test_adapter_margin_revival_cancel.cpp` replays on the NYSE:F 15m chart of seven lab tv tapes: badapter-v19dp1-control (no cancel: the margin call closes 928 through X) and v19fix-gapped-stop-control (X's stop at 10.15, gapped by the 05-01 open and parked: the same close), and badapter-v19dp1-cancel, v19fix-cancel-before-pair, v19fix-cancel-at-1000, v19fix-gapped-stop-cancel and v19fix-gapped-stop-cancel-after-pair (a cancel on 05-01 09:30 while X is dormant, on 04-30 15:45 before the reversal pair and on 04-30 10:0 while X is live, and on 05-01 09:30 and after the pair while X is parked; one tape file: margin calls 12 @10.39 and 92 @10.57, 836 open). Untaped, the same row holds the tapes' trades for `strategy.cancel_all` on 05-01 and before the pair and for `strategy.cancel` after the pair, and on the same bars with no quantity grid — where the pair holds X in the cancel's own callback — every spelling books what the cancel on the dormant exit books | ### 1.2 Trailing @@ -68,64 +68,64 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa |---|---|---|---|---|---|---| | TR1 | Trail arm → track → trigger, with a state query | **yes** — `Trail{offset, arm_price}` native_order.hpp:290-293; states `TrailWaitArm` native_order.hpp:642-660; arm / track / fire `TrailWaitArm` native_execution_consumer.cpp:6632; `NativeTrailState` native_host.hpp:669-674, `trail_state` native_host.hpp:1092 | K | — | F:B1 O:B1 S:T1 | — | | TR2 | Trail activation / offset in ticks or points, relative to the entry fill (`trail_points_to_ticks` include/pineforge/compat/pine/trail_ticks.hpp:44 + grid snap exit_lifecycle.cpp:22-27; `trail_activation_level` pine_adapter.hpp:350-352) | **partial** — absolute prices and a raw price distance only **Closed:** `TrailTicks{n}` (`Trail::ticks` native_order.hpp:293) is resolved against `NativeRunSpec::price_tick` at acceptance, so a tick-spelled trail and its price-spelled equal behave identically bar for bar; a relative arm threshold is the `FromOwnerFill` anchor of OL11 (`tests/test_native_order_ergonomics.cpp`, `tests/test_native_anchored_legs.cpp`). | K | L7 | F:B2 O:B2 S:T2 | **Disagree:** S classes all trail operand conversion as TV quirk. R5-1: tick / relative spelling is generic. | -| TR3 | Zero-distance trail, "ride the best, exit on any adverse tick" (adapter substitutes `tick * 0.5` pine_adapter.cpp:15024) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:5367) **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:14941) stays A. | -| TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:4765) | **partial** — the read exists `trail_state` native_host.hpp:1092; `replace` always rebuilds trigger state **Closed:** `replace(handle, request, ReplaceOptions{/*retain_trigger_state=*/true})` carries the predecessor's live trigger state — a tracking trail's best, an already active stop — into the successor, refusing the replacement when the retained best leaves no representable level (`tests/test_native_order_ergonomics.cpp`, `tests/test_native_anchored_legs.cpp`). | K+A | L7 | O:B4 F:B3 | Status is single-source (O). R5-1: retain-trail replace is generic; TV's "retained best across re-issue" convention (`retained_trail_best` pine_adapter.hpp:353-355) is A. | -| TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:11167, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:14941, retained best `retained_trail_best` pine_adapter.hpp:353-355, operand / restart rules `source_trail_points` pine_adapter.cpp:11164, `trail_one_shot` pine_adapter.cpp:12510 | 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:15025) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:5367) **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:14942) stays A. | +| TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:4768) | **partial** — the read exists `trail_state` native_host.hpp:1092; `replace` always rebuilds trigger state **Closed:** `replace(handle, request, ReplaceOptions{/*retain_trigger_state=*/true})` carries the predecessor's live trigger state — a tracking trail's best, an already active stop — into the successor, refusing the replacement when the retained best leaves no representable level (`tests/test_native_order_ergonomics.cpp`, `tests/test_native_anchored_legs.cpp`). | K+A | L7 | O:B4 F:B3 | Status is single-source (O). R5-1: retain-trail replace is generic; TV's "retained best across re-issue" convention (`retained_trail_best` pine_adapter.hpp:353-355) is A. | +| TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:11170, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:14942, retained best `retained_trail_best` pine_adapter.hpp:353-355, operand / restart rules `source_trail_points` pine_adapter.cpp:11167, `trail_one_shot` pine_adapter.cpp:12513 | 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:7203, pine_adapter.cpp:7203) | **yes** — `Transact` in `OrderIntent` native_order.hpp:229 | K | — | F:C1 O:C1 S:S1 | — | +| SZ1 | Explicit units (`Transact{signed}` after a grid floor pine_adapter.cpp:7206, pine_adapter.cpp:7206) | **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:13737-14965) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:945-949, invoked `resolve_execution_terms` native_execution_consumer.cpp:5852, guarded by `consume_matched_request`'s `try` native_execution_consumer.cpp:5753-5787 | 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:3208) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:5881 **Closed:** `Sized{Side, EquityFraction{f}}` (`OrderIntent` native_order.hpp:229) is the sixth intent — the kernel resolves the units from a fraction of marked equity at the sizing point, so a bare host needs no terms override (`tests/test_native_sizing_bases.cpp`). | K+A | L3 | F:C3 O:C2 S:S3 | TV parts stay A: the percentage's money rounded `source_money_round` pine_adapter.cpp:558 (not the equity: R5 lane W5B-ENG-MARGIN-RESIDUAL, the private fixture set `margin_residual` w5b-sz-*), money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:658 (→ SZ10) | -| SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:3079) | **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:3227, `typed_entry_units` pine_adapter.cpp:3245; `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:3200; `percent_commission_live_equity` pine_adapter.cpp:5101) | **no** — only a dead primitive, `reserve_percent_commission` (since deleted), with no caller in `src/` **Closed:** `Sized::reserve_percent_fee` divides the sizing cash by `1 + fee_value / 100` under `NativeFeeKind::Percent` and is an exact no-op for every other fee kind; the dead `reserve_percent_commission` primitive is gone (`tests/test_native_sizing_bases.cpp`, `tests/test_native_sizing_price_rule.cpp`). | K | L3 | F:C3 O:C6 S:S3 | **Disagree:** F lists the reserve under quirk yet offers a knob / O kernel, "partial" / S knob. R5-1: fee-net option is generic. TradingView's cash-commission reserve stays adapter policy over the kernel's `CashValue`: a percent-of-equity default quantity under a cash-per-order or cash-per-contract fee takes its percentage of `strategy.equity` (`PineSizingSnapshot::strategy_equity`, the open entries' cash fees charged) and leaves out the fee its own order pays (`default_sizing_cash`, R5 lane PAR-CASHFEE, §3.10; `tests/test_cash_fee_sizing_tapes.cpp` on the tapes in the private fixture set `cash_fee_sizing`) | -| SZ6 | Placement-time vs fill-time sizing (`sizing_snapshot` pine_adapter.cpp:3034, `PineSizingSnapshot` pine_adapter.hpp:157-169; fill-time exceptions `default_sized` pine_adapter.cpp:8477) | **no** **Closed:** `SizeTime::AtMatch` (default) resolves at the matching candidate and `SizeTime::AtAcceptance` freezes the units against the command point, where the run's own opening admission also runs against the frozen quantity (`RequestRejectReason::PlacementAdmission`). Which price the basis converts at is `SizePrice::{Resolved, Signal, SignalOnTick}` (`tests/test_native_sizing_bases.cpp`, `tests/test_native_sizing_price_rule.cpp`). | K+A | L3 | F:C3 O:C7 S:S3 | **Disagree:** O adapter-only / F, S generic timing knob. R5-11 (L3 scope) + R5-1: the knob is kernel; TV's frozen tuple stays A through `HostSized`. | +| SZ3 | Percent-of-equity sizing (`default_sizing_units` pine_adapter.cpp:3210) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:5881 **Closed:** `Sized{Side, EquityFraction{f}}` (`OrderIntent` native_order.hpp:229) is the sixth intent — the kernel resolves the units from a fraction of marked equity at the sizing point, so a bare host needs no terms override (`tests/test_native_sizing_bases.cpp`). | K+A | L3 | F:C3 O:C2 S:S3 | TV parts stay A: the percentage's money rounded `source_money_round` pine_adapter.cpp:558 (not the equity: R5 lane W5B-ENG-MARGIN-RESIDUAL, the private fixture set `margin_residual` w5b-sz-*), money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:658 (→ SZ10) | +| SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:3081) | **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:3229, `typed_entry_units` pine_adapter.cpp:3247; `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:3202; `percent_commission_live_equity` pine_adapter.cpp:5101) | **no** — only a dead primitive, `reserve_percent_commission` (since deleted), with no caller in `src/` **Closed:** `Sized::reserve_percent_fee` divides the sizing cash by `1 + fee_value / 100` under `NativeFeeKind::Percent` and is an exact no-op for every other fee kind; the dead `reserve_percent_commission` primitive is gone (`tests/test_native_sizing_bases.cpp`, `tests/test_native_sizing_price_rule.cpp`). | K | L3 | F:C3 O:C6 S:S3 | **Disagree:** F lists the reserve under quirk yet offers a knob / O kernel, "partial" / S knob. R5-1: fee-net option is generic. TradingView's cash-commission reserve stays adapter policy over the kernel's `CashValue`: a percent-of-equity default quantity under a cash-per-order or cash-per-contract fee takes its percentage of `strategy.equity` (`PineSizingSnapshot::strategy_equity`, the open entries' cash fees charged) and leaves out the fee its own order pays (`default_sizing_cash`, R5 lane PAR-CASHFEE, §3.10; `tests/test_cash_fee_sizing_tapes.cpp` on the tapes in the private fixture set `cash_fee_sizing`) | +| SZ6 | Placement-time vs fill-time sizing (`sizing_snapshot` pine_adapter.cpp:3036, `PineSizingSnapshot` pine_adapter.hpp:157-169; fill-time exceptions `default_sized` pine_adapter.cpp:8480) | **no** **Closed:** `SizeTime::AtMatch` (default) resolves at the matching candidate and `SizeTime::AtAcceptance` freezes the units against the command point, where the run's own opening admission also runs against the frozen quantity (`RequestRejectReason::PlacementAdmission`). Which price the basis converts at is `SizePrice::{Resolved, Signal, SignalOnTick}` (`tests/test_native_sizing_bases.cpp`, `tests/test_native_sizing_price_rule.cpp`). | K+A | L3 | F:C3 O:C7 S:S3 | **Disagree:** O adapter-only / F, S generic timing knob. R5-11 (L3 scope) + R5-1: the knob is kernel; TV's frozen tuple stays A through `HostSized`. | | SZ7 | Quantity step | **yes** — `quantity_grid` admission native_run_spec.hpp:660, native_order.hpp:1421-1438; host-sized terms snap (`ExecutionGridPolicy` native_order.hpp:71-85); exact grid validation `execution_terms_grid_representable` native_execution_consumer.cpp:506; 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:5221, `compute_exit_reservation` pine_adapter.cpp:5261) | **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:5261, `reserved_exit_qty` pine_adapter.cpp:11574) | **partial** — `PointBudget` native_order.hpp:379-382 and group `Reduce` native_order.hpp:220 give a generic reservation; no net-of-siblings claim for fraction sizes **Closed:** `ScopeClaim::NetOfSiblings` subtracts the units already claimed by live sibling reduces bound to the same scope, and `ScopeBasis::{AtMatch, AtAcceptance}` chooses when that scope is measured (`tests/test_native_sizing_bases.cpp`). | K+A | L3 | F:A14 S:S5 S:O10 O:A12 | F proposed nothing. R5-4: generic half → L3; the Pine ledger → SZ11, SZ12. | +| SZ8 | Fractional reduce of the position / a cohort (`quantize_close_units` pine_adapter.cpp:5224, `compute_exit_reservation` pine_adapter.cpp:5264) | **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:5264, `reserved_exit_qty` pine_adapter.cpp:11577) | **partial** — `PointBudget` native_order.hpp:379-382 and group `Reduce` native_order.hpp:220 give a generic reservation; no net-of-siblings claim for fraction sizes **Closed:** `ScopeClaim::NetOfSiblings` subtracts the units already claimed by live sibling reduces bound to the same scope, and `ScopeBasis::{AtMatch, AtAcceptance}` chooses when that scope is measured (`tests/test_native_sizing_bases.cpp`). | K+A | L3 | F:A14 S:S5 S:O10 O:A12 | F proposed nothing. R5-4: generic half → L3; the Pine ledger → SZ11, SZ12. | | SZ10 | *(quirk)* Ten-significant-digit money rule (`source_money_round` pine_adapter.cpp:558, `source_money_floor_lot` pine_adapter.cpp:658; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:5243) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1753 | 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:8520), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:10044), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:16235), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:16766, pine_adapter.cpp:16819) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:21686) | 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:3208), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:3227, `typed_entry_units` pine_adapter.cpp:3245), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:26659) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes and 202 famr tapes (two tests that live in the private fixtures); on a seven-digit tie (E 9998.99999996, 10 000 lots at tick(close) 0.9999) the rule admits while the kernel's conversion buys 9999.99999996 lots and binary64 gives E/Q 0.999899999996 (`tests/test_adapter_fill_qty_probe.cpp`, section 2). | -| SZ11 | *(quirk)* Same-bar `strategy.close` callsite batching, two-call provenance, entry-id ledger (`enqueue_pooc_fifo_close` pine_adapter.cpp:9171; ledger `close_batch_bar_` pine_adapter.hpp:2598; the kernel's comment-only residue was deleted by R5 lanes E6 and E10) | n/a | A | — | S:O8 F:A15 O:A15 | **Disagree:** S "generic → kernel" (its P2 `ReplaceByKey` / `Fifo`). R5-4: adapter. | -| SZ12 | *(quirk)* POOC reservation-growth market-entry population predicate (`select_reservation_growth_sources` src/compat/pine/reservation_expansion.cpp:9-34, called from the adapter, `select_reservation_growth_sources` pine_adapter.cpp:1322) | n/a | A | — | S:O10 | (single-source) R5-4 confirms S's own split: concept → SZ9, predicate A. | +| 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:8523), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:10047), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:16236), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:16767, pine_adapter.cpp:16820) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:21687) | 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:3210), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:3229, `typed_entry_units` pine_adapter.cpp:3247), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:26672) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes and 202 famr tapes (two tests that live in the private fixtures); on a seven-digit tie (E 9998.99999996, 10 000 lots at tick(close) 0.9999) the rule admits while the kernel's conversion buys 9999.99999996 lots and binary64 gives E/Q 0.999899999996 (`tests/test_adapter_fill_qty_probe.cpp`, section 2). | +| SZ11 | *(quirk)* Same-bar `strategy.close` callsite batching, two-call provenance, entry-id ledger (`enqueue_pooc_fifo_close` pine_adapter.cpp:9174; ledger `close_batch_bar_` pine_adapter.hpp:2599; the kernel's comment-only residue was deleted by R5 lanes E6 and E10) | n/a | A | — | S:O8 F:A15 O:A15 | **Disagree:** S "generic → kernel" (its P2 `ReplaceByKey` / `Fifo`). R5-4: adapter. | +| SZ12 | *(quirk)* POOC reservation-growth market-entry population predicate (`select_reservation_growth_sources` src/compat/pine/reservation_expansion.cpp:9-34, called from the adapter, `select_reservation_growth_sources` pine_adapter.cpp:1324) | n/a | A | — | S:O10 | (single-source) R5-4 confirms S's own split: concept → SZ9, predicate A. | ### 1.4 Margin, admission, liquidation, risk | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| MG1 | Allowed opening directions (`allowed_open_directions` pine_adapter.cpp:2859) | **yes** — `allowed_open_directions` native_run_spec.hpp:665 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:3733 | K | — | F:D4 O:D7 S:M1 | the one risk rule the adapter delegates (O) | +| MG1 | Allowed opening directions (`allowed_open_directions` pine_adapter.cpp:2861) | **yes** — `allowed_open_directions` native_run_spec.hpp:665 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:3733 | K | — | F:D4 O:D7 S:M1 | the one risk rule the adapter delegates (O) | | MG2 | Max absolute position size | **yes** — `max_abs_units` native_run_spec.hpp:663 tests the resulting book native_execution_consumer.cpp:3740 | 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:16514) is TV. | -| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:664 → `MaxOpenLots` native_execution_consumer.cpp:3745 | 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:7708, cap left unset `max_open_lots` pine_adapter.cpp:2858-2859) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (the private test `test_pyramiding_count_differential`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 13 (it keeps a slot a close or another entry's exit drained; since lane W5B-ENG-MARGIN-RESIDUAL's SB the later market entries of a flat bar are refused at the fill once the book holds the cap). The fifteenth, a resting entry accepted below the cap and filled at it, the kernel's cap refuses; the adapter books it since lane W8A-SIGSTATE-1's R-B, which also stopped the count from counting a resting LIMIT entry (a pending LIMIT entry does not count at a later call, and one the cap admitted fills over it: the private fixture set `pyramiding_open_order`). A recorded divergence, retained pending a re-lowering ruling. **Re-lowering tried by R5 lane H-THIN and not taken:** on the kernel's cap four corpus probes moved away from TradingView, for want of that first-eligible-point rule, so the count stays. The lane fixed the reservation divergence H-MEASURE measured beside it (P10): a default `strategy.exit(from_entry)` under FIFO reserved the whole book (200 in the adapter, 100 on TradingView's tape); it now reserves the entry's own quantity, the oldest lots first (`fifo_named_entry` pine_adapter.cpp:11598). | -| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:17327 and leaves the kernel gate unset pine_adapter.cpp:17346) | **yes** — `initial_margin_fraction` native_run_spec.hpp:666; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:3763 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:3609 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:650, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (the private test `test_adapter_margin_schedule_differential`). **Since R5 lane TAIL-C** an explicit-quantity market buy from flat is costed at its signal close's tick plus the slippage ticks its fill carries, off `process_orders_on_close` (the private test `test_explicit_market_admission_tapes`), and an infinite `strategy.entry` quantity trades the default quantity, as na does (`tests/test_nonfinite_entry_qty_tape.cpp`). | -| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:17580, `margin_pct` pine_adapter.cpp:17082) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:666 **Closed:** `NativeMarginModel::initial_long` / `initial_short` native_run_spec.hpp:242, opt-in and mutually exclusive with `initial_margin_fraction`; `initial_ == 0.0` is the maintenance-only spelling that leaves opening admission to the host at both of its gates (`tests/test_native_margin_model.cpp`). | K | L4 | F:D1 O:D2 | R5-1 | -| MG6 | Maintenance margin + forced liquidation (`submit_margin_call_slice` pine_adapter.cpp:17967, `submit_margin_call_units` pine_adapter.cpp:18000, `schedule_margin_call_path` pine_adapter.cpp:20245) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:666-667; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:3722 **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:17624) | **no** **Closed:** `NativeLiquidationSizing::{RestoreMinimum, ShortfallMultiple, Flatten}` with `shortfall_multiple` and `liquidation_min_units`, capped at the held quantity and floored onto `quantity_grid`; `resolve_margin_call_units` native_host.hpp:995 gives a host the last word, clamped into `(0, held]` (`tests/test_native_margin_model.cpp`, `tests/test_native_margin_hooks.cpp`). | K+A | L4 | O:D4 F:D2 S:M4 | **Disagree:** F generic knob `ShortfallMultiple` / O "×4 is A" / S quirk, yet a 4.0 default inside its generic model. R5-1: policy knob in the kernel, plain default (restore-minimum). **Decided by R5 lane F7 (the D2 question):** the adapter selects NO kernel knob — `project()` leaves `sizing` / `shortfall_multiple` / `liquidation_min_units` unset and its `resolve_margin_call_units` answers every call with TradingView's slice (the restore floored onto the lot grid BEFORE the ×4, floored again, the one-contract band, the dust gate). Measured: on a one-unit grid, 20 long at 100 on 1000 at 50 % reaching 93.9, TradingView books 4 lots and the kernel's own `ShortfallMultiple 4` books 5 (`tests/test_native_margin_hooks_twin.cpp` MG-F3, an executed run, no longer a comment); without a grid the two agree bit for bit (MG-F/H, 4.2105263157894735), but declaring the knob for those runs alone folds `NativeMarginModel::sizing` into every margin run's identity — an epoch decision F7 does not take. Since R5 lane H-THIN the pre-open admission slice (`schedule_preopen_margin_slice`) sizes through the same `source_margin_units`, where it kept its own copy of the ×4 without the band or the dust gates (AUDIT4 §6 G2-12; the private test `test_adapter_margin_schedule_differential` M10: band and fudge now 1 and 1, the kernel's and `ab9714be`'s). A four-times call on a whole-unit grid whose book's oldest lot is one unit is followed by one more unit at the next check point (`whole_unit_follow_up_due` pine_adapter.cpp:19152; R5 lane W5B-ENG-MARGIN-RESIDUAL, the private fixture set `margin_residual` w5b-es-*, w5b-sb-es-*, w5b-f-q1x12). With slippage the one-contract band takes its unit only where that unit restores the book at its own fill: the position's requirement at the liquidation's slipped print, less the equity at the mark, under one unit's margin (`source_margin_units` pine_adapter.cpp:17624; lane INT28-FIX rule OU, the private fixture set `slipped_short`). A one-unit call that leaves the book short at its own fill, by more than one unit's margin and two slippage steps with the book re-marked there, is followed by one more unit at the bar's next path point, where that point's own check calls nothing and its print is inside the call's fill (lane INT28-FIX rule OF). Under a percent commission a call of any size is followed so, on a fractional lot grid too: while the book the call left, re-marked at its fill, is short by more than two slippage steps for each unit called and the exit fees of those units and of one more, the next path point inside that fill takes four times the shortfall's lot floor over the fill, or one unit where that floors to none (`margin_follow_up_units` pine_adapter.cpp:19194, `follow_margin_call` pine_adapter.cpp:19243; R5 lane TAIL-I, the private fixture set `margin_open_print`). | +| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:664 → `MaxOpenLots` native_execution_consumer.cpp:3745 | 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:7711, cap left unset `max_open_lots` pine_adapter.cpp:2648) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (the private test `test_pyramiding_count_differential`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 13 (it keeps a slot a close or another entry's exit drained; since lane W5B-ENG-MARGIN-RESIDUAL's SB the later market entries of a flat bar are refused at the fill once the book holds the cap). The fifteenth, a resting entry accepted below the cap and filled at it, the kernel's cap refuses; the adapter books it since lane W8A-SIGSTATE-1's R-B, which also stopped the count from counting a resting LIMIT entry (a pending LIMIT entry does not count at a later call, and one the cap admitted fills over it: the private fixture set `pyramiding_open_order`). A recorded divergence, retained pending a re-lowering ruling. **Re-lowering tried by R5 lane H-THIN and not taken:** on the kernel's cap four corpus probes moved away from TradingView, for want of that first-eligible-point rule, so the count stays. The lane fixed the reservation divergence H-MEASURE measured beside it (P10): a default `strategy.exit(from_entry)` under FIFO reserved the whole book (200 in the adapter, 100 on TradingView's tape); it now reserves the entry's own quantity, the oldest lots first (`fifo_named_entry` pine_adapter.cpp:11601). | +| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:17328 and leaves the kernel gate unset pine_adapter.cpp:17347) | **yes** — `initial_margin_fraction` native_run_spec.hpp:666; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:3763 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:3609 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:650, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (the private test `test_adapter_margin_schedule_differential`). **Since R5 lane TAIL-C** an explicit-quantity market buy from flat is costed at its signal close's tick plus the slippage ticks its fill carries, off `process_orders_on_close` (the private test `test_explicit_market_admission_tapes`), and an infinite `strategy.entry` quantity trades the default quantity, as na does (`tests/test_nonfinite_entry_qty_tape.cpp`). | +| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:17581, `margin_pct` pine_adapter.cpp:17083) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:666 **Closed:** `NativeMarginModel::initial_long` / `initial_short` native_run_spec.hpp:242, opt-in and mutually exclusive with `initial_margin_fraction`; `initial_ == 0.0` is the maintenance-only spelling that leaves opening admission to the host at both of its gates (`tests/test_native_margin_model.cpp`). | K | L4 | F:D1 O:D2 | R5-1 | +| MG6 | Maintenance margin + forced liquidation (`submit_margin_call_slice` pine_adapter.cpp:17968, `submit_margin_call_units` pine_adapter.cpp:18001, `schedule_margin_call_path` pine_adapter.cpp:20246) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:666-667; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:3722 **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:17625) | **no** **Closed:** `NativeLiquidationSizing::{RestoreMinimum, ShortfallMultiple, Flatten}` with `shortfall_multiple` and `liquidation_min_units`, capped at the held quantity and floored onto `quantity_grid`; `resolve_margin_call_units` native_host.hpp:995 gives a host the last word, clamped into `(0, held]` (`tests/test_native_margin_model.cpp`, `tests/test_native_margin_hooks.cpp`). | K+A | L4 | O:D4 F:D2 S:M4 | **Disagree:** F generic knob `ShortfallMultiple` / O "×4 is A" / S quirk, yet a 4.0 default inside its generic model. R5-1: policy knob in the kernel, plain default (restore-minimum). **Decided by R5 lane F7 (the D2 question):** the adapter selects NO kernel knob — `project()` leaves `sizing` / `shortfall_multiple` / `liquidation_min_units` unset and its `resolve_margin_call_units` answers every call with TradingView's slice (the restore floored onto the lot grid BEFORE the ×4, floored again, the one-contract band, the dust gate). Measured: on a one-unit grid, 20 long at 100 on 1000 at 50 % reaching 93.9, TradingView books 4 lots and the kernel's own `ShortfallMultiple 4` books 5 (`tests/test_native_margin_hooks_twin.cpp` MG-F3, an executed run, no longer a comment); without a grid the two agree bit for bit (MG-F/H, 4.2105263157894735), but declaring the knob for those runs alone folds `NativeMarginModel::sizing` into every margin run's identity — an epoch decision F7 does not take. Since R5 lane H-THIN the pre-open admission slice (`schedule_preopen_margin_slice`) sizes through the same `source_margin_units`, where it kept its own copy of the ×4 without the band or the dust gates (AUDIT4 §6 G2-12; the private test `test_adapter_margin_schedule_differential` M10: band and fudge now 1 and 1, the kernel's and `ab9714be`'s). A four-times call on a whole-unit grid whose book's oldest lot is one unit is followed by one more unit at the next check point (`whole_unit_follow_up_due` pine_adapter.cpp:19153; R5 lane W5B-ENG-MARGIN-RESIDUAL, the private fixture set `margin_residual` w5b-es-*, w5b-sb-es-*, w5b-f-q1x12). With slippage the one-contract band takes its unit only where that unit restores the book at its own fill: the position's requirement at the liquidation's slipped print, less the equity at the mark, under one unit's margin (`source_margin_units` pine_adapter.cpp:17625; lane INT28-FIX rule OU, the private fixture set `slipped_short`). A one-unit call that leaves the book short at its own fill, by more than one unit's margin and two slippage steps with the book re-marked there, is followed by one more unit at the bar's next path point, where that point's own check calls nothing and its print is inside the call's fill (lane INT28-FIX rule OF). Under a percent commission a call of any size is followed so, on a fractional lot grid too: while the book the call left, re-marked at its fill, is short by more than two slippage steps for each unit called and the exit fees of those units and of one more, the next path point inside that fill takes four times the shortfall's lot floor over the fill, or one unit where that floors to none (`margin_follow_up_units` pine_adapter.cpp:19195, `follow_margin_call` pine_adapter.cpp:19244; R5 lane TAIL-I, the private fixture set `margin_open_print`). | | MG8 | Liquidation price query (`margin_liquidation_price` pine_strategy_host.cpp:168-184) | **no** **Closed:** `native_liquidation_price()` native_host.hpp:1320 answers the exact solved level, `nullopt` where none exists; `NativeLiquidationLevelBase` chooses whether the solve answers for the open entries' fees (`tests/test_native_margin_model.cpp`, `tests/test_native_open_lots.cpp`). | K+A | L4 | F:D3 S:R5 | **Disagree:** F generic / S adapter-only. R5-1: liquidation price is generic; the Pine spelling stays A. **Lowered by R5 lane H-THIN:** the Pine host's second solve (`compute_liquidation_price`) is gone; `strategy.margin_liquidation_price` reads `native_liquidation_price()`, which the adapter configures with `NativeLiquidationLevelBase::RealizedOnly` (the Pine formula's closed-money solve, bit for bit at every bar of the lane's 1,021 seeded margin-call runs), and keeps only TradingView's tick spelling: floored for a long, ceiled for a short. A run whose margin calls are switched off declares no margin model, so it reads na. | -| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:5688, `apply_fx_opening_margin_slice` pine_adapter.cpp:5700) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1202 but no margin re-check. **Closed by audit lane N6:** `NativeMarginCheckKind::FxRoll` — a step of the declared curve is a check point of its own, measured at the unchanged price before the first point the new rate converts (`tests/test_native_margin_fx_roll.cpp`). The adapter refuses the point by kind, for a measured reason since R5 lane F7: TradingView's rollover is a broker-open checkpoint at the open price, now at any positive margin (`apply_fx_open_margin_slice`, on the one TradingView money / units rule every other checkpoint uses), while the roll point measures at the remaining path's adverse mark and rests the slice there — a carried 1x long on a step bar O 100 L 97 books 0.4116 @ 97 on the adapter's run shape and 0.4036 @ 99, measured a bar early, on a strict calendar, against TradingView's 0.3996 @ 100 (`tests/test_native_margin_hooks_twin.cpp` MG-FX). Both N6 follow-ups are closed (§4.4). **Completed by R5 follow-up lane E3:** the conversion INSTANT is now generic too — every check kind converts at its own cursor's rate (`account_currency_fx_at(cursor.point.effective_time_ms)`) for the requirement, the equity and the level, in batch and stream, so a step landing between a fill's cursor and the clock the engine presents no longer breaches at the fill (`tests/test_native_margin_fx_clock.cpp`; N6's local clock swap inside `fx_roll_margin_check` is removed as redundant). **Extended by R5 follow-up lane E21** to the opening gate's placement site (MG4), and the rate-explicit equity E3 restated beside the engine's accessor is now that accessor's one implementation, `BacktestEngine::marked_equity_at` engine.hpp:730 | K | L4 → N6 → E3 → E21 | O:D8 | (single-source) | -| MG10 | Max drawdown halt (`update_risk_state` pine_adapter.cpp:17504) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1301 **Closed:** `NativeRiskLimits::max_drawdown` native_run_spec.hpp:311, a `NativeLossLimit{value, percent}` measured against the running peak equity at three points of every script bar, blocking openings for the rest of the run (`tests/test_native_risk_limits.cpp`). | K | L9 | F:D6 O:E1 S:M6 | R5-9 | -| MG11 | Max consecutive losing days (`SourceDayLedger` pine_adapter.hpp:1422-1430; gate `max_cons_loss_days` pine_adapter.cpp:16507) | **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:17537, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:20879) | **no** — observation only `intraday_loss_limit` include/pineforge/source/market_admission.hpp:51 **Closed:** `NativeRiskLimits::max_intraday_loss` with `NativeRiskAction::FlattenAndBlock`, under which the kernel issues one `Flatten` of its own at the breach point (`RequestOrigin::KernelRisk`) and latches the block before that flatten settles (`tests/test_native_risk_limits.cpp`). | K | L9 | F:D7 O:E3 S:M6 | R5-9 | -| MG13 | Max filled orders per day (`IntradayCap` intraday_cap.hpp:84-263, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:20922) | **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:17401) | **no** — the calendar exists `session_day_ordinal` native_calendar.hpp:416-419, there is no risk-day ledger **Closed:** `NativeRiskDay::{SessionDay, CalendarDayInTimezone}` keys the risk day, and `native_risk_state()` native_host.hpp:1329 reads the whole ledger back — block and reason, day, fills counted, loss-day streak, peak equity, day-opening equity (`tests/test_native_risk_limits.cpp`). | K+A | L9 | O:E5 S:M7 | The chart-day shortcut stays A; the kernel keys on session / calendar day | -| MG15 | *(quirk)* TV margin call: money rounding `source_margin_money` pine_adapter.cpp:17569, 4× shortfall `4.0 * minimum` pine_adapter.cpp:17658, POOC chronology exception `resting_order` pine_adapter.cpp:20265 (the commissioned carried short; the competing-entry and rounded-money exceptions retired by R5 lane W5B-ENG-MARGIN-RESIDUAL, the private fixture set `margin_residual` w5b-pc-*), 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:18562, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:18562) | n/a | A | — | F:D2 F:G4 O:D4 S:M4 | R5-1. **Measured (R5 lane H-MEASURE)** against the kernel's own points on the same bars and orders, the kernel's hooks answering TradingView's money and slice (the private test `test_adapter_margin_schedule_differential`): a gap-open breach executes at the open, sized on the open's money, where the kernel's mark check rests at the path's adverse extreme and `CalculationOnly` executes at the calculation (M8); the pre-open slice parts from the kernel's `AfterApplied` point, and from `ab9714be`, on the one-contract band and the restore's `+1e-6` floor (M10; the frozen units it also parted on are TradingView's book since R5 lane PAR-MARGIN froze a default-percent stop above 100 % as eight `lab tv` tapes, the private tapes `pm-m10-*` of `margin_entry_bar`, show TradingView does); the entry bar of a leveraged opening, where four `lab tv` tapes (the private fixture set `margin_entry_bar`) and the kernel's `AfterApplied` point book the margin call at that bar's low, is admitted by `on_applied` since R5 lane PAR-MARGIN on the run shapes `ab9714be` checked it on (the pre-open slice's own opening keeps that slice's verdict), and the adapter books the tapes' rows (M7; H-MEASURE had recorded it as a divergence); R5 lane PAR-MARGIN-2 extended it to limit openings, `process_orders_on_close`, `calc_on_order_fills` and adds to a carried book, and made the kernel's post-fill point reach the low a limit was filled on the way down to, or a short's high on its way up (twenty-eight `lab tv` tapes, the private tapes `pm2-*` of `margin_entry_bar`). Under the magnifier the kernel's `IntrabarSample` point (R5 lane PAR-MARGIN) is admitted on a leveraged long, plain, under `process_orders_on_close` or `calc_on_order_fills`, and on a short outside `process_orders_on_close` (PAR-MARGIN-2), where TradingView books its call at the first lower-timeframe low that crosses -- at its own 2-minute intrabars on a 15-minute chart, which a model reproduces on all 20 magnified tapes (the private fixture set `intrabar_margin`); six tapes part on the one-minute samples the host feeds, a recorded divergence of the magnifier's sampling. A margin call books the nearest tick of its print, as TradingView's market fills do (12.105 books 12.11; the private fixture set `half_tick_rounding`, PAR-MARGIN-2). | -| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:34 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:48); `admission_allocation` pine_adapter.cpp:4555, `record_market_review` pine_adapter.cpp:21178) | n/a | A | — | S:R2 | (single-source) | +| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:5691, `apply_fx_opening_margin_slice` pine_adapter.cpp:5703) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1202 but no margin re-check. **Closed by audit lane N6:** `NativeMarginCheckKind::FxRoll` — a step of the declared curve is a check point of its own, measured at the unchanged price before the first point the new rate converts (`tests/test_native_margin_fx_roll.cpp`). The adapter refuses the point by kind, for a measured reason since R5 lane F7: TradingView's rollover is a broker-open checkpoint at the open price, now at any positive margin (`apply_fx_open_margin_slice`, on the one TradingView money / units rule every other checkpoint uses), while the roll point measures at the remaining path's adverse mark and rests the slice there — a carried 1x long on a step bar O 100 L 97 books 0.4116 @ 97 on the adapter's run shape and 0.4036 @ 99, measured a bar early, on a strict calendar, against TradingView's 0.3996 @ 100 (`tests/test_native_margin_hooks_twin.cpp` MG-FX). Both N6 follow-ups are closed (§4.4). **Completed by R5 follow-up lane E3:** the conversion INSTANT is now generic too — every check kind converts at its own cursor's rate (`account_currency_fx_at(cursor.point.effective_time_ms)`) for the requirement, the equity and the level, in batch and stream, so a step landing between a fill's cursor and the clock the engine presents no longer breaches at the fill (`tests/test_native_margin_fx_clock.cpp`; N6's local clock swap inside `fx_roll_margin_check` is removed as redundant). **Extended by R5 follow-up lane E21** to the opening gate's placement site (MG4), and the rate-explicit equity E3 restated beside the engine's accessor is now that accessor's one implementation, `BacktestEngine::marked_equity_at` engine.hpp:730 | K | L4 → N6 → E3 → E21 | O:D8 | (single-source) | +| MG10 | Max drawdown halt (`update_risk_state` pine_adapter.cpp:17505) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1301 **Closed:** `NativeRiskLimits::max_drawdown` native_run_spec.hpp:311, a `NativeLossLimit{value, percent}` measured against the running peak equity at three points of every script bar, blocking openings for the rest of the run (`tests/test_native_risk_limits.cpp`). | K | L9 | F:D6 O:E1 S:M6 | R5-9 | +| MG11 | Max consecutive losing days (`SourceDayLedger` pine_adapter.hpp:1422-1430; gate `max_cons_loss_days` pine_adapter.cpp:16508) | **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:17538, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:20880) | **no** — observation only `intraday_loss_limit` include/pineforge/source/market_admission.hpp:51 **Closed:** `NativeRiskLimits::max_intraday_loss` with `NativeRiskAction::FlattenAndBlock`, under which the kernel issues one `Flatten` of its own at the breach point (`RequestOrigin::KernelRisk`) and latches the block before that flatten settles (`tests/test_native_risk_limits.cpp`). | K | L9 | F:D7 O:E3 S:M6 | R5-9 | +| MG13 | Max filled orders per day (`IntradayCap` intraday_cap.hpp:84-263, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:20923) | **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:17402) | **no** — the calendar exists `session_day_ordinal` native_calendar.hpp:416-419, there is no risk-day ledger **Closed:** `NativeRiskDay::{SessionDay, CalendarDayInTimezone}` keys the risk day, and `native_risk_state()` native_host.hpp:1329 reads the whole ledger back — block and reason, day, fills counted, loss-day streak, peak equity, day-opening equity (`tests/test_native_risk_limits.cpp`). | K+A | L9 | O:E5 S:M7 | The chart-day shortcut stays A; the kernel keys on session / calendar day | +| MG15 | *(quirk)* TV margin call: money rounding `source_margin_money` pine_adapter.cpp:17570, 4× shortfall `4.0 * minimum` pine_adapter.cpp:17659, POOC chronology exception `resting_order` pine_adapter.cpp:20266 (the commissioned carried short; the competing-entry and rounded-money exceptions retired by R5 lane W5B-ENG-MARGIN-RESIDUAL, the private fixture set `margin_residual` w5b-pc-*), 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:18563, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:18563) | n/a | A | — | F:D2 F:G4 O:D4 S:M4 | R5-1. **Measured (R5 lane H-MEASURE)** against the kernel's own points on the same bars and orders, the kernel's hooks answering TradingView's money and slice (the private test `test_adapter_margin_schedule_differential`): a gap-open breach executes at the open, sized on the open's money, where the kernel's mark check rests at the path's adverse extreme and `CalculationOnly` executes at the calculation (M8); the pre-open slice parts from the kernel's `AfterApplied` point, and from `ab9714be`, on the one-contract band and the restore's `+1e-6` floor (M10; the frozen units it also parted on are TradingView's book since R5 lane PAR-MARGIN froze a default-percent stop above 100 % as eight `lab tv` tapes, the private tapes `pm-m10-*` of `margin_entry_bar`, show TradingView does); the entry bar of a leveraged opening, where four `lab tv` tapes (the private fixture set `margin_entry_bar`) and the kernel's `AfterApplied` point book the margin call at that bar's low, is admitted by `on_applied` since R5 lane PAR-MARGIN on the run shapes `ab9714be` checked it on (the pre-open slice's own opening keeps that slice's verdict), and the adapter books the tapes' rows (M7; H-MEASURE had recorded it as a divergence); R5 lane PAR-MARGIN-2 extended it to limit openings, `process_orders_on_close`, `calc_on_order_fills` and adds to a carried book, and made the kernel's post-fill point reach the low a limit was filled on the way down to, or a short's high on its way up (twenty-eight `lab tv` tapes, the private tapes `pm2-*` of `margin_entry_bar`). Under the magnifier the kernel's `IntrabarSample` point (R5 lane PAR-MARGIN) is admitted on a leveraged long, plain, under `process_orders_on_close` or `calc_on_order_fills`, and on a short outside `process_orders_on_close` (PAR-MARGIN-2), where TradingView books its call at the first lower-timeframe low that crosses -- at its own 2-minute intrabars on a 15-minute chart, which a model reproduces on all 20 magnified tapes (the private fixture set `intrabar_margin`); six tapes part on the one-minute samples the host feeds, a recorded divergence of the magnifier's sampling. A margin call books the nearest tick of its print, as TradingView's market fills do (12.105 books 12.11; the private fixture set `half_tick_rounding`, PAR-MARGIN-2). | +| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:34 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:48); `admission_allocation` pine_adapter.cpp:4558, `record_market_review` pine_adapter.cpp:21179) | n/a | A | — | S:R2 | (single-source) | ### 1.5 Calculation timing | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | CT1 | Calculate at script-bar close | **yes** — `on_native_bar` native_host.hpp:893, dispatched by `invoke_callback` native_execution_consumer.cpp:8193 (call site `on_native_recalculate` native_execution_consumer.cpp:8221) | K | — | F:E1 O:F1 | — | -| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2816) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:8637 | K | — | F:E2 O:F6 S:C1 | — | -| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:22273) | **yes** — `on_native_bar_open` native_host.hpp:889 runs before the open match native_execution_consumer.cpp:8067; `execute_current` legal native_host.hpp:1104 | K | — | F:E3 O:F2 | hazard → CT4 | +| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2818) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:8637 | K | — | F:E2 O:F6 S:C1 | — | +| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:22274) | **yes** — `on_native_bar_open` native_host.hpp:889 runs before the open match native_execution_consumer.cpp:8067; `execute_current` legal native_host.hpp:1104 | K | — | F:E3 O:F2 | hazard → CT4 | | CT4 | Partial-bar view / open-bar lookahead guard | **no** — the open hook receives the *complete* script bar: `invoke_bar_open_callback(engine, bar, point)` native_execution_consumer.cpp:8010 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:8061 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1073 is the lookahead-free bar so far, through the last path point consumed (a fill inside a segment reads it at the segment's origin), and `NativeOpenBarView::OpenOnly` native_run_spec.hpp:109 masks `on_native_bar_open`'s bar to `H = L = C = open` as presentation only — the complete bar is restored before the open match (`tests/test_native_calc_timing.cpp`). | K | L5 | F:E3 | (single-source) The adapter needs the full bar, so the guard is opt-in. | | CT5 | Raw input observation before aggregation | **yes** — `on_native_input` native_host.hpp:875, `NativeInputContext` native_host.hpp:773-778 | K | — | O:F3 | (single-source row) | -| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:745, `coof_recalculation_due` pine_scheduler_native.cpp:711, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:866; `begin_coof_recalc` pine_adapter.cpp:6685, `flush_coof_tail` pine_adapter.cpp:7109, `end_coof_recalc` pine_adapter.cpp:6742) | **partial** — `on_native_applied` native_host.hpp:938 fires mid-path after each fill native_execution_consumer.cpp:7914, native_execution_consumer.cpp:7914; commands are legal there `commands_allowed` native_execution_consumer.cpp:1844; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:6551, `remaining_path_delivery` native_order.cpp:2082-2090. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | +| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:745, `coof_recalculation_due` pine_scheduler_native.cpp:711, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:866; `begin_coof_recalc` pine_adapter.cpp:6688, `flush_coof_tail` pine_adapter.cpp:7112, `end_coof_recalc` pine_adapter.cpp:6745) | **partial** — `on_native_applied` native_host.hpp:938 fires mid-path after each fill native_execution_consumer.cpp:7914, native_execution_consumer.cpp:7914; commands are legal there `commands_allowed` native_execution_consumer.cpp:1844; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:6551, `remaining_path_delivery` native_order.cpp:2082-2090. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | | CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:878 fires per accepted realtime print native_execution_consumer.cpp:8149; no per-tick calculation in batch; the guide pinned "close-only" (`docs/pages/native-engine.md`, "Batch OHLCV vs ticks vs quiet") **Closed:** `NativeCalculationTrigger::EveryModeledPoint` native_run_spec.hpp:97 adds one `Tick` recalculation at every modeled point of the delivered path and at every observed print, in batch and in a stream alike (`tests/test_native_calc_timing.cpp`). | K | L5 | F:E5 O:F4 S:C3 | **Disagree:** F yes / O partial / S no. R5-5: v1. Not an adapter feature either: absent from `PineStrategyConfig` pine_adapter.hpp:62-76, the runner rejects it (`calc_on_every_tick` main.cpp:259), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:295. | | CT8 | Historical intrabar calculation (per lower-TF sub-bar / magnifier sample) | **no** — with an `IntrabarPath` the callback still fires once per script bar native_execution_consumer.cpp:8744 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:928 fires once after each retained lower-timeframe sub-bar's whole matching path (`tests/test_native_calc_timing.cpp`). | K | L5 | F:E6 O:F8 S:C4 | — | -| CT9 | Intrabar (magnifier) matching path + path-order policy (`volume_weighted_cap` pine_adapter.cpp:2936) | **yes** — `IntrabarPath` native_run_spec.hpp:362-403, delivery `deliver_confirmed_script` native_execution_consumer.cpp:8296-8562; `NativePathOrder` native_run_spec.hpp:335-339 | K+A | — | F:E7 F:G5 O:F7 S:C4 | **Disagree:** S partial (it wants callback cadence → CT8). TV sampler choice is A: TradingView's own intrabars at its table's timeframe, each owned by the chart bar of its last minute, built from the finer feed (`tradingview_magnifier_bars` magnifier_intrabars.hpp:46, R5 lane MAG-INTRABAR). | +| CT9 | Intrabar (magnifier) matching path + path-order policy (`volume_weighted_cap` pine_adapter.cpp:2938) | **yes** — `IntrabarPath` native_run_spec.hpp:362-403, delivery `deliver_confirmed_script` native_execution_consumer.cpp:8296-8562; `NativePathOrder` native_run_spec.hpp:335-339 | K+A | — | F:E7 F:G5 O:F7 S:C4 | **Disagree:** S partial (it wants callback cadence → CT8). TV sampler choice is A: TradingView's own intrabars at its table's timeframe, each owned by the chart bar of its last minute, built from the finer feed (`tradingview_magnifier_bars` magnifier_intrabars.hpp:46, R5 lane MAG-INTRABAR). | | CT10 | Batch → warmup → realtime forward execution | **yes** — `stream_begin` engine.hpp:1772, `stream_push_bar` engine.hpp:1778, `stream_end` engine.hpp:1782, `NativeRunPhase` native_host.hpp:41, C ABI `strategy_stream_begin` c_abi.cpp:698 | K+A | — | F:E8 O:J3 S:C5 | **Disagree:** S partial. R5-1: the lifecycle is native; Pine trade-start suppression and realtime-tail / probe rules (`trading_window_active` pine_strategy_commands.cpp:15-21, `prepare_native_begin` pine_strategy_host.cpp:206-311) are A. S's "neutral tail + trade-window policy" is single-source and unassigned (§4 Q8). | -| CT11 | *(quirk)* COOF TV specifics: waypoint-only refill (`coof_next_waypoint` pine_adapter.cpp:6958), two-fills-at-open rule, script-state rollback (`PineScheduler` pine_scheduler_native.cpp:54-106; hooks `snapshot_script_state` engine.hpp:1333-1334), the first-open guard `kFirstOpenLoopGuard` pine_scheduler_native.cpp:866, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1964-1994 | 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:6961), two-fills-at-open rule, script-state rollback (`PineScheduler` pine_scheduler_native.cpp:54-106; hooks `snapshot_script_state` engine.hpp:1333-1334), the first-open guard `kFirstOpenLoopGuard` pine_scheduler_native.cpp:866, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1964-1994 | n/a | A | — | F:E4 O:F5 S:C2 | R5-5: the adapter keeps its path | ### 1.6 Higher timeframes and auxiliary feeds @@ -143,13 +143,13 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | FP1 | Trigger geometry on the modeled path; fill at the level or at the gap price; market-if-touched | **yes** — `native_matching` native_execution_consumer.cpp:6623, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:7213; `Limit::fill_through` native_order.hpp:239-244 enforced where the matched price is limit-protected (`protect_limit` native_execution_consumer.cpp:7209) and re-checked after host terms (`InvalidTerms` native_execution_consumer.cpp:5878) | K | — | F:G3 O:H1 O:H2 O:H3 | — | -| FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:652 applied once native_execution_consumer.cpp:5514, clamped by `protect_limit` native_execution_consumer.cpp:7209 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:15191) is A | +| FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:652 applied once native_execution_consumer.cpp:5514, clamped by `protect_limit` native_execution_consumer.cpp:7209 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:15192) is A | | FP3 | Commission: percent / cash per unit / cash per execution (`fee_kind_for` pine_adapter.cpp:669) | **yes** — `NativeFeeKind` native_run_spec.hpp:19-23; `quote_execution_commissions` engine_execution.cpp:1690 | K | — | F:G2 O:H8 S:F1 | — | -| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:274, `source_bar_fill` pine_adapter.cpp:15191, `directional_tick` pine_adapter.cpp:327) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:7191; `bar_fill_price` engine.hpp:772 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:7364; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_sizing_price_rule.cpp` quantizes the fill basis under `HalfUp`; `tests/test_native_acid_composite.cpp` books the `Directional` fills on the 0.25 ladder; the private test `test_native_price_grid` re-ran the pair under the overlay). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | +| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:274, `source_bar_fill` pine_adapter.cpp:15192, `directional_tick` pine_adapter.cpp:327) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:7191; `bar_fill_price` engine.hpp:772 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:7364; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_sizing_price_rule.cpp` quantizes the fill basis under `HalfUp`; `tests/test_native_acid_composite.cpp` books the `Directional` fills on the 0.25 ladder; the private test `test_native_price_grid` re-ran the pair under the overlay). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | | FP5 | Trigger tested against the tick-quantized bar | **no** — the kernel tests raw prices against raw levels **Closed:** `NativePriceGrid::QuantizeFillsAndTriggers` additionally tests and re-validates every activation against the tick-quantized path, one rule for every trigger kind (`tests/test_native_trail_best_seed.cpp` puts an off-ladder trail seed on the ladder; `tests/test_native_acid_composite.cpp` runs a trail's best and its exit level on it; the private test `test_native_price_grid` re-ran the pair under the overlay). | K+A | L8 | O:H5 F:G4 S:F3 | R5-3: the optional quantized trigger test is K; TV's exact half-tick rule (`source_trigger_threshold` pine_adapter.cpp:384) is A on top. | | PG | The Pine adapter on the kernel price grid: raw levels submitted, `project()` declaring `QuantizeFillsAndTriggers` + `HalfUp`, `source_trigger_threshold` deleted (lane R7; re-tested after L8b by gap lane N13) | **measured-infeasible for the Pine adapter** — B1 (the sub-tick cursor-print abort) is closed by L8b's `ActivationGrid`: 0 aborted runs under the trial (R7: 9 zero-offset-trail tapes + 6 POOC panels; now `test_zero_offset_trail_rides_l4c` 449/450, `test_pooc_short_close_tick_l4d` 183/183). B2 (TradingView quantizes per order kind, the grid is one run-wide rule) moved 30 pinned checks in 4 units when N13 last ran the trial, before lanes E14 and E16, with no adapter-side remedy: `test_coof_market_limit_recross_l4c` 24, `test_stop_tick_rounding_l4d` 3, `test_adapter_grid_relower` 2, `test_zero_offset_trail_rides_l4c` 1; 12 more checks in 3 units have an adapter-side cause (`test_trail_fill_snap_l4c` 2, `test_native_margin_hooks` 2, `test_adapter_brackets_relower` 8 book digests). Corpus: 5/312 probes differ, in the engine-only entry-incarnation column only (every probe runs a 0.01 tick on an on-grid feed, so it cannot arbitrate) | A | waived (R7, N13) | — | **Waiver.** No class of triggers is byte-identical on its own: the grid is a run-wide switch and a per-kind mask would spell TradingView's inconsistency into the kernel (not generic, R5-3). The adapter stays on `NativePriceGrid::None` and keeps `source_trigger_threshold` and its call sites; native-engine.md "The Pine adapter's grid re-lowering is waived". **Native-only by ruling (audit lane P6): §3.6.2**, with `examples/native/native_price_grid_strategy.cpp` and its C twin as the feature's hosts. | | FP6 | Account-currency FX | **partial** — scalar `account_fx` + immutable `NativeFxCurve` native_host.hpp:1202 in batch; the stream then refused a non-empty curve (today only tick-driven input is refused under one, `refuse_fx_curve_tick_input` native_execution_consumer.cpp:9997). **Closed by audit lane N6:** the declared curve is the run's immutable FX epoch and a confirmed-bar stream runs under it exactly as a batch does (stream twin in `tests/test_native_margin_fx_roll.cpp`); no realtime rate ingestion and no tick-driven input under a curve are stated as permanent limits with their reasons (native-engine.md, "Stream FX"). **Re-examined by R5 follow-up lane E3 and KEPT:** the tick refusal was never only about the margin check — it guards the SCRIPT-VISIBLE conversions (`open_trade_profit()`, `marked_equity()`, the equity curve), which read `active_account_currency_fx()`, i.e. the presented clock the observation hook has not yet moved (re-measured: a step at T+90s left the T+100s and T+110s prints converting at the pre-step rate). E3's cursor rule reaches the margin model only, so the refusal stands and is pinned in `tests/test_native_margin_fx_clock.cpp` | K | unassigned → N6 (E3: refusal kept) | F:D9 S:F4 | **Disagree:** F out of scope ("O7 clock") / S its P10, a stream-safe FX epoch. No R5 lane → §4 U4 (answered by N6: S's "same curve in batch and stream", without a new clock or callback). | -| FP7 | *(quirk)* TV tick conventions: half-tick threshold `source_bar_fill_tick` pine_adapter.cpp:295 (used `source_trigger_threshold` pine_adapter.cpp:384, `exit_limit_trigger` pine_adapter.cpp:14373), `source_bar_fill_tick` pine_adapter.cpp:295, raw-vs-booked spelling `source_same_point` pine_adapter.cpp:313, margin-call fill pricing `source_margin_fill_price` pine_adapter.cpp:17793 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:14867) | A | — | F:G4 O:H5 S:F3 | R5-1, R5-3 | +| FP7 | *(quirk)* TV tick conventions: half-tick threshold `source_bar_fill_tick` pine_adapter.cpp:295 (used `source_trigger_threshold` pine_adapter.cpp:384, `exit_limit_trigger` pine_adapter.cpp:14376), `source_bar_fill_tick` pine_adapter.cpp:295, raw-vs-booked spelling `source_same_point` pine_adapter.cpp:313, margin-call fill pricing `source_margin_fill_price` pine_adapter.cpp:17794 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:14868) | A | — | F:G4 O:H5 S:F3 | R5-1, R5-3 | ### 1.8 Excursion, metrics, report, hash @@ -171,7 +171,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | OT1 | Key / value run parameters (`set_input` engine.hpp:1872, refused in-run by `guard_native_mutation` engine.hpp:1877) | **partial** — staging allowed pre-run, getters protected | K (low) | — | F:J1 | (single-source) Nothing for v1: native hosts take constructor parameters. | -| OT2 | Symbol info, sessions, calendars, timezones (`timezone` pine_adapter.cpp:2737, `chart_timezone` pine_adapter.cpp:2780) | **yes** — spec fields `ticker` native_run_spec.hpp:636-646; `parse_session` native_calendar.hpp:180-467; intervals on every decision context `input_interval` market_driver.hpp:123-124 | K | — | F:J2 O:G-5 | — | +| OT2 | Symbol info, sessions, calendars, timezones (`timezone` pine_adapter.cpp:2739, `chart_timezone` pine_adapter.cpp:2782) | **yes** — spec fields `ticker` native_run_spec.hpp:636-646; `parse_session` native_calendar.hpp:180-467; intervals on every decision context `input_interval` market_driver.hpp:123-124 | K | — | F:J2 O:G-5 | — | | OT3 | Indicators, series, math, matrix, map, string utils | **yes** — `ta.hpp` includes only `na/series/window_sum` `window_sum` ta.hpp:2-8 | K | L11 (naming only) | F:J3 | (single-source row) TV-calibrated numerics → §2.ii b | | OT4 | Cooperative abort | **yes** — `request_abort` engine.hpp:2175, `NativeAbortReporting` native_run_spec.hpp:41-44 | K | — | F:J4 | (single-source) | | OT5 | C-level run configuration, stream, FX curve, contract probe | **yes** — `strategy_configure_native_v1` c_abi.cpp:980, FX curve `strategy_configure_native_fx_curve_v1` c_abi.cpp:1051, probe `strategy_execution_contract` c_abi.cpp:971, stream `strategy_stream_begin` c_abi.cpp:698 **Extended:** `pf_native_run_spec_ext_v1`'s policy tail now carries `intrabar`, `path_order`, `abort_reporting` and the slot-label / feed-tolerance policies (`PF_NATIVE_SPEC_EXT_INTRABAR`, `PF_NATIVE_SPEC_EXT_FEED_POLICY`), so nothing of `NativeRunSpec` but `identity` is unreachable from C (`tests/test_native_c_api.c`). | K | — | O:J4 | The v1 C spec `pf_native_run_spec_v1` pineforge.h:510 omitted `intrabar`, `path_order` and `abort_reporting`; they now travel in the extension's policy tail, `pf_native_abort_reporting_e` native_c_api.h:911 among them (F §2.iii) | @@ -204,13 +204,13 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa |---|---|---|---|---|---| | a | `source::StrategyOverrides` opaque pointer | Forward declarations in the kernel headers (since removed: no kernel header names the adapter type now); parameter of `run_rich` (execution_consumer.hpp:46-55, native_execution_consumer.hpp:110-119) and of the rich `BacktestEngine::run` overload (engine.hpp:1797-1802, engine_consumer.cpp:81-93). The kernel only forwards it as `NativeBeginArgs::overrides_opaque` native_host.hpp:758; the source host casts it back `overrides_opaque` pine_strategy_host.cpp:307 | NEUTRAL in behaviour; a consumer-vtable signature change, so it rides the shared `engine_script_run` bump | `const void*` in the kernel signatures (F `host_begin_extras`, O `overrides_opaque`), typed convenience overload in `source/pine_strategy_host.hpp`; then drop the checker's single whitelist entry `test_native_example_batch` check_native_include_independence.py:42-46. S's alternative (`NativeRunOverrides`, or removing the rich overload) not adopted. Unverified: generated code calls the typed overload (codegen repo is not in this tree — F). **Status (L11, `3f4fff5b`): done** — `run_rich` and the rich `BacktestEngine::run` take `const void*` overrides, the rich begin carries them as `overrides_opaque`, and the independence checker whitelists nothing (§2.vi row 6) | F O S | | b | `pine_float_compare.hpp` | 1e-10 absolute equality band, used by kernel TA only: `float_band_le` ta_misc.cpp:30 (percentrank), `float_band_gt` ta_oscillators.cpp:462-463 (MFI), `plus_dm` ta_volatility_trend.cpp:337-340 (DMI); no execution TU includes it (O) | NEUTRAL (rename only) | Rename to a neutral header (F `float_band_compare.hpp`, O `ta_compare_band.hpp`), keep `pine_float_*` inline aliases in a source-layer header for generated code. Numerics must not move. S's behavioural `ComparePolicy` split is not neutral → not adopted (§4 U6). **Status (L11 + N14): done** — `ta_compare_band.hpp` is the kernel header; the alias shim is `include/pineforge/source/pine_float_compare.hpp` (N14 moved it off the kernel include root; it has no in-tree includer, generated code emits its own comparator) | F O S | -| c | `NativeSlotLabelPolicy::LegacyTolerant`, `NativeLegacyTolerance` (now `NativeSlotLabelPolicy::FeedTolerant`, `NativeFeedTolerance`) | `NativeSlotLabelPolicy` native_run_spec.hpp:324-353; read by `legacy_tolerant_slot_labels` market_driver.cpp:55-56 and `native_feed_tolerance_enabled` market_driver.cpp:43-49, and by `legacy_tolerant_slot_labels` native_execution_consumer.hpp:1027-1029 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:2369; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:2386, `:10131`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2806. A feed-tolerance policy (raw strictly-increasing labels, zero / NaN-volume legacy bars), not TV semantics (O) | NEUTRAL | Rename with deprecated aliases (F `RawIncreasing` + `NativeInputTolerance::{StructuralBars, WarmupNonNegativeOHLC}`; O `NativeLabelPolicy::ProviderLabels`). Enumerator values stay 0/1 and 1/2: they are hashed `slot_label_policy` native_execution_consumer.cpp:273. **Status:** renamed as `FeedTolerant` / `NativeFeedTolerance` (the enumerator values unchanged), and the C transport carries them now, in the extension's feed-policy block (`PF_NATIVE_SPEC_EXT_FEED_POLICY` native_c_api.h:745) | F O S | +| c | `NativeSlotLabelPolicy::LegacyTolerant`, `NativeLegacyTolerance` (now `NativeSlotLabelPolicy::FeedTolerant`, `NativeFeedTolerance`) | `NativeSlotLabelPolicy` native_run_spec.hpp:324-353; read by `legacy_tolerant_slot_labels` market_driver.cpp:55-56 and `native_feed_tolerance_enabled` market_driver.cpp:43-49, and by `legacy_tolerant_slot_labels` native_execution_consumer.hpp:1027-1029 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:2369; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:2386, `:10131`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2808. 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:273. **Status:** renamed as `FeedTolerant` / `NativeFeedTolerance` (the enumerator values unchanged), and the C transport carries them now, in the extension's feed-policy block (`PF_NATIVE_SPEC_EXT_FEED_POLICY` native_c_api.h:745) | F O S | | d | Orphaned comments | The KI-62 block of `engine_orders.cpp` (*fresh*: comment-only, the bodies are deleted; S read it as live code) and a comment of `engine.hpp`; wider deleted-declaration blocks in `engine.hpp`; the money-rule essay in `engine.hpp` (the code is `source_money_round` pine_adapter.cpp:558) | NEUTRAL | Delete, or move to `docs/` next to the adapter code that implements them. **Status: done** — R5 lanes E6 and E10 deleted the detached comment residue (a census of 0 over the kernel's compile closure) | F O | | e | Dead helpers in the public kernel header | **Done.** All four are gone from `engine.hpp`: `round_to_mintick_directional` and `apply_slippage` were the pre-R5 spelling of the directional snap and the slippage step and the kernel now fills on `native_matching::grid_round_directional` / `native_matching::apply_slippage` src/native_matching.hpp:259 (the engine.hpp comment that named them went with R5 lane E10's residue sweep); `apply_limit_fill` and `reserve_percent_commission` are deleted outright, and only the adapter still spells the first two names, in its own comments. `round_to_mintick` engine.hpp:754 and `bar_fill_price` engine.hpp:772 stay, live through the call `bar_fill_price` native_execution_consumer.cpp:7365 | 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:1791 stays, and the declaration/definition pairing is still enforced by `scripts/test_native_source_guard.py` | NEUTRAL (removing the virtual changes the vtable → shared bump) | Delete both | O; F asked (its Q6) | | g | Legacy path resolver | **Done, and then deleted.** `src/engine_path_resolve.cpp` is 118 lines with five functions: `bar_path_uses_high_first`, both `first_touch_position` overloads, `fill_bar_path_points_ordered`, and `BacktestEngine::declare_opened_lot_entry_bar_mask`. Ten test files still mention `resolve_exit_path_fill` in historical comments; the resolver body and test-only oracle are gone. | NEUTRAL (pure move; verify with the link gate) | Keep the generic path helpers in the kernel TU; the old source resolver is not a live counterpart | F (single-source) | | | h | `pine_*`-named generic API | `timeframe_time` session_time.hpp:35-211 (live, e.g. `session_in_market`, which generated code reaches through `pine_session_ismarket` session_time.hpp:263), `str_utils.hpp`, `deterministic_random` math.hpp:9, `tz_util` timezone.hpp:8-30; the guard bans `pine_*` only inside `IDENTIFIER_ROOTS` (test_native_source_guard.py:34, test_native_source_guard.py:34-42) | NEUTRAL, M | Neutral aliases; `pine_*` kept as deprecated inlines for codegen. Large blast radius in generated code. **Status (L11 + N14): done** — session_time / str_utils / math / timezone in L11; N14 applied the same treatment to the two public matrix types: `PineMatrix` → `NumericMatrix`, `PineGenericMatrix` → `GenericMatrix`, old spellings kept as exact aliases (no version script pins either name) | O (single-source) | -| i | `pf_pending_order_v1_t` Pine-named fields | `tv_carry_qty` pending_order_mirror.hpp:73, `pine_birth_reach` pending_order_mirror.hpp:135, `pine_exit_activation_owner_cycle_at_birth` pending_order_mirror.hpp:147-156, `pine_frozen_market_instruction_kind` pending_order_mirror.hpp:164-169; enumerated by `PF_PO_FIELD` pending_order_mirror.cpp:144-178; written only by `pine_birth_reach` pine_adapter.cpp:27084 | 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:27085 | no change | Freeze (ABI). The neutral view arrives beside it (L7 / L13) | O (single-source) | | j | Pine-shaped lot flags | **Mostly done.** `market_pyramid_add` moved to the adapter side table the row asked for — `market_pyramid_adds_` src/source/pine_state_hash.cpp:692, whose own comment records that it was `PyramidEntry::market_pyramid_add` — and `ordinary_market_open`, `pooc_terminal_market_entry`, `ordinary_stop_open`, `bracket_slot_shadowed` and `entry_path_position` are all deleted. What is left on the lot is the intrabar-fill excursion mask, `skip_entry_bar_high` / `skip_entry_bar_low` engine.hpp:154, hashed (`skip_entry_bar_high` engine_state_hash.cpp:74) and, since R5 lane E6, written by no host at all: a host that owns lot excursions declares where its fill sat (`declare_opened_lot_entry_bar_mask` engine_path_resolve.cpp:95; the Pine host did until R5 lane H-THIN left its excursions to the kernel's sampler) and the kernel derives the pair | HASH-VISIBLE | Move the live flag to an adapter side table keyed by `entry_incarnation` (the adapter already keeps `PlacementTable` pine_adapter.hpp:355-675) and fold it through `hash_source_extension`; delete the dead ones; update the hash-coverage waivers. Whether a kernel TU beyond the hash reads them is unverified (O) | F O S | | k | `margin_call_enabled_` | **Done.** The generic kernel model is enabled by `NativeRunSpec::margin` native_run_spec.hpp:672. TradingView-specific toggling remains `source::PineStrategyHost::set_margin_call_enabled` pine_strategy_host.hpp:511; the frozen C symbol remains source-owned. | HASH-VISIBLE (R5-8) | Keep the source setter in the adapter; the kernel toggle is the presence of `spec.margin` | F O | | l | Kernel decodes adapter strings | **Done.** `closed_trade_close_cause` engine_trade_accessors.cpp:158 branches on no string at all: it reads `Trade::open_at_end`, then the typed `execution::CloseCause` the closer recorded on the row, then `exit_from_bracket`. A kernel-originated liquidation or risk flatten states its own cause through the settling fill, so a bare host reads `3` / `4` where the retired string derivation answered `1`. The public C contract it implements is `strategy_closed_trade_close_cause` pineforge.h:1309 | HASH-VISIBLE (adds durable state) | Kernel `enum class CloseCause` recorded on the `Trade` at settlement (the kernel sets `MarginCall` when L4 liquidates; a source virtual covers adapter causes). C numbers unchanged. After L4 | O (single-source) | @@ -326,7 +326,7 @@ Union of F §2.vi (9), O §2.vi (5 + 1) and S §2.6 (7), deduplicated. S had no | 4 | HTF "comes from the kernel feed `set_native_security_feed` / `prepare_native_security_feeds`" (ADR L84-89) | Not usable by a bare host: a partial boundary, not an absent kernel. `prepare_native_security_feeds` is protected with one caller; `configure_security_evaluators` has one caller; the public setter is inert pre-run and refused in-run | `prepare_native_security_feeds` engine.hpp:1640, `configure_security_evaluators` engine.hpp:1323, `set_native_security_feed` engine.hpp:1732, `guard_native_mutation` engine_aux_security.cpp:83 | F O S | | 5 | "`engine_aux_security.cpp` includes `source/pine_strategy_host.hpp`" (ADR L129-132) | False at `73817c1`: its includes are `engine_internal.hpp`, `ta.hpp` and std headers (*fresh* re-read; the run-failure codes added `run_failure.hpp`, a kernel header). No kernel TU includes a source header | `sha256:560a7f68b243d921382a69ed0390c398f28370ac066b84f30b5f96cbb385fb60` engine_aux_security.cpp:6-14 | F | | 6 | "Kernel TUs reference adapter types (`source::StrategyOverrides` …)" (ADR L129-131) | True only as a forward-declared opaque pointer the kernel never dereferences; the independence checker whitelists exactly that symbol | at `73817c1`; overtaken by `3f4fff5b` (§2.ii a), after which the rich begin carries `overrides_opaque` native_host.hpp:758 and the independence checker whitelists nothing | F O | -| 7 | `engine.hpp` hosts "the ten-significant-digit money rule, KI-62, the `strategy.close` batching + entry-id ledger, a TradingView margin-call toggle" (ADR L131-134) | Two of four survive as code. Money rule: a comment block; the arithmetic is in the adapter. KI-62: orphaned comments + one live, hashed lot flag. Close batching + id ledger: gone from the kernel (the ledger is in the adapter; the old names survive only in comments). Live: `margin_call_enabled_`, the KI-62 flag and — unmentioned by the ADR — string-sentinel decoding of adapter comments | the money-rule block was lines 927-961 of `engine.hpp` and R5 lane E6 deleted it (lines 73-180 were and are live code), the arithmetic `source_money_round` pine_adapter.cpp:558 and its twin `tv_money_round` pine_policy_support.hpp:9-15; `skip_entry_bar_high` engine.hpp:153, `skip_entry_bar_low` engine_state_hash.cpp:74, `declare_opened_lot_entry_bar_mask` engine_path_resolve.cpp:95; the ledger `close_logical_units_` pine_adapter.hpp:2583; `closed_trade_close_cause` engine_trade_accessors.cpp:158. **Overtaken:** the margin-call toggle and the string decoding are both gone — see ADR-0001, "Current state vs. target" | F O | +| 7 | `engine.hpp` hosts "the ten-significant-digit money rule, KI-62, the `strategy.close` batching + entry-id ledger, a TradingView margin-call toggle" (ADR L131-134) | Two of four survive as code. Money rule: a comment block; the arithmetic is in the adapter. KI-62: orphaned comments + one live, hashed lot flag. Close batching + id ledger: gone from the kernel (the ledger is in the adapter; the old names survive only in comments). Live: `margin_call_enabled_`, the KI-62 flag and — unmentioned by the ADR — string-sentinel decoding of adapter comments | the money-rule block was lines 927-961 of `engine.hpp` and R5 lane E6 deleted it (lines 73-180 were and are live code), the arithmetic `source_money_round` pine_adapter.cpp:558 and its twin `tv_money_round` pine_policy_support.hpp:9-15; `skip_entry_bar_high` engine.hpp:153, `skip_entry_bar_low` engine_state_hash.cpp:74, `declare_opened_lot_entry_bar_mask` engine_path_resolve.cpp:95; the ledger `close_logical_units_` pine_adapter.hpp:2584; `closed_trade_close_cause` engine_trade_accessors.cpp:158. **Overtaken:** the margin-call toggle and the string decoding are both gone — see ADR-0001, "Current state vs. target" | F O | | 8 | "No kernel-level C API. `c_abi.cpp` exports only version/descriptor symbols" (ADR L136-138) | 80 runtime `PF_API` symbols: native configure, the 12-symbol stream family, pending-order views, broker-state hash, security feeds, FX curve, reports. The 80 compiled-strategy runtime symbols in `c_abi.cpp` are distinct from the native C host API in `native_c_host.cpp`: the latter exposes submit, replace, cancel and a callback table to a C host. | `sha256:733786ed81c9137223b98e1887e28d01afdb3c64ba5e8ca52ce5161f2caf3f75` c_abi.cpp:325-1013, `sha256:6327f1829019fdd172cd76a96e3d8c9c0af1610ad0dc9878640dda0ca21a686b` check_c_abi_runtime.py:20-81, c_abi.cpp:946, `sha256:5ff4c65a56410fe29b21cd68bc21ac87bdb34b77344e4f75f90efa8887fe48e8` c_abi.cpp:664-788, `sha256:51fe087eb9d30e9960dc4f0ab3f7cc837531ebeaa71b5871a3f1cdc22fb37c2` c_abi.cpp:566-641, `sha256:f18d46ac323234f7e3082367d30840b704b8a7548f7ae95ee8cff8714f29fd50` c_abi.cpp:545-562, `sha256:6a9ef4e688f33dc10ea8297d9eaa8144ea7923ef6ee6f8fe9ff20f6af4e9acb3` c_abi.cpp:881-917, c_abi.cpp:1013 | F O S | | 9 | "No examples. `PINEFORGE_BUILD_EXAMPLES` is 'none yet'" (ADR L139) | The option is inert, but two Pine-free `NativeStrategyHost` examples exist, are compiled by the independence checker and tested through the live runner, only under `PINEFORGE_BUILD_LIVE_RUNNER`. Wording: "no top-level examples target" | at `73817c1` (overtaken; ADR-0001, "Current state vs. target") | F O S | | 10 | Follow-ups (1)-(4) (ADR L168-170) | Add native reporting, sizing, margin, HTF — and, per R5, calc-timing, price grid and order ergonomics. Without them "usable without Pine" is a build-system claim, not a feature claim | §3.5 | F | @@ -410,14 +410,14 @@ closure markers and `scripts/check_design_inventory.py`). |---|---|---|---| | L0 | — | docs only | — | | L1 | option `PINEFORGE_BUILD_SOURCE_LAYER`, targets `pineforge_kernel` + unchanged `pineforge` (§2.i) | same TUs, same flags; ctest + corpus | kernel-only link of the two examples + the 20 source-free native tests; installed-header closure | -| L2 | `NativeReportPolicy { HostRecorded, KernelRecorded }` (R1 appended a third value, `KernelRecordedAtHostMarks` native_run_spec.hpp:64), `report_open_position_at_end`, `hash_host_extension`, `native_metrics()` → A.1 | at L2 the adapter stayed `HostRecorded`; since R1 it declares `KernelRecordedAtHostMarks` pine_adapter.cpp:2855, so the kernel records the same series at the adapter's own report marks — still one recorder, no double recording, `fill_report` bytes unchanged on the corpus | native example: curve length == script bars, drawdown equals a hand walk (F); 50-bar host asserts a non-empty curve, a finite max drawdown and a range-end row (O); same generic strategy through both hosts → equal rows and curve (S) | +| L2 | `NativeReportPolicy { HostRecorded, KernelRecorded }` (R1 appended a third value, `KernelRecordedAtHostMarks` native_run_spec.hpp:64), `report_open_position_at_end`, `hash_host_extension`, `native_metrics()` → A.1 | at L2 the adapter stayed `HostRecorded`; since R1 it declares `KernelRecordedAtHostMarks` pine_adapter.cpp:2857, 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:6551) and `drain_applied_notifications` (native_execution_consumer.cpp:7234) are frozen; accessors are inert | COOF probe re-expressed natively: same fills where the TV waypoint rule is inactive (F); a host that flips on its own fill produces the adapter's cascade in shape (O); callback reason, cursor, newborn eligibility and event ordinals compared (S) | | L6 | `NativeTimeframeSubscription`, `on_native_timeframe_bar`, `native_series_bar` → A.5, §2.iv | `subscriptions` empty for the adapter; registration-gated pump; `engine_security.cpp` and pine_strategy_host.cpp:1091-1175 untouched; corpus diff before / after (O: highest parity risk of any lane) | HTF-filter probe on a 24x7 symbol: exact (F); `"D"` over a 15m feed equals a `TimeframeAggregator` baseline bar for bar (O); installed-feed and no-feed cases (S) | | L7 | `native_working_requests()`, `cancel_all()`, `cancel_where(label)`, `TriggerAnchor`, `TrailTicks`, zero offset, `ReplaceOptions`; toolkit `submit_bracket`, `OrderBook` → A.6 | new kinds unused by the adapter; anchor defaults to `Absolute`; the adapter's `tick * 0.5` sentinel keeps working | bracket + trailing probe through the builder: exact where TR5 / FP7 are inactive (on-grid levels) (F); the zero-offset trail test asserts the run no longer fails (O); parent-limit + stop sibling, parent rejection, replacement, parent-cycle revival (S) | | L8 | `NativePriceGrid { None, QuantizeFills, QuantizeFillsAndTriggers }`, `NativeGridRounding` → A.7 | `None` for the adapter, permanently — **native-only by ruling, audit lane P6: §3.6.2** | a host with `QuantizeFills` books on-grid prices; a second test pins that `QuantizeFillsAndTriggers` still differs from TV's half-tick threshold (FP5) (O) | -| L9 | `NativeRiskLimits`, `NativeRiskEvent`, `MatchRejectReason::RiskLimit` → A.8 | `risk` unset for the adapter; its ledger (`SourceDayLedger` pine_adapter.hpp:1426) untouched — **retained after measurement, audit lane N12; native-only by ruling, audit lane P6: §3.6.1** | risk probes: same halt bar (F); each limit + the day-boundary basis (O); breach, forced close, cancellation, next-day reset (S) | +| L9 | `NativeRiskLimits`, `NativeRiskEvent`, `MatchRejectReason::RiskLimit` → A.8 | `risk` unset for the adapter; its ledger (`SourceDayLedger` pine_adapter.hpp:1427) untouched — **retained after measurement, audit lane N12; native-only by ruling, audit lane P6: §3.6.1** | risk probes: same halt bar (F); each limit + the day-boundary basis (O); breach, forced close, cancellation, next-day reset (S) | | L10 | `PINEFORGE_EXPORT_NATIVE_STRATEGY(Class)` (§2.v) | build-only | the examples run in ctest; the independence checker compiles the relocated examples | | L11 | — (renames / moves, §2.ii a-i) | rename / move only; hashed enumerator values pinned; sweep unchanged | — | | L12 | — (§2.ii j-m) | **not neutral**: coordinated sweep, waiver updates, hash-domain plan (the `"pineforge-broker-state/v17"` literal of that wave, since moved to `"pineforge-broker-state/v19"` engine_state_hash.cpp:41 and pinned to one occurrence by check_broker_state_hash_coverage.py:223) | trades identical, hashes re-baselined once | @@ -523,12 +523,12 @@ line numbers are re-anchored to this tree (the measurement ran on 683a82f); kern | Pine rule | adapter mechanism | kernel counterpart | class | measured divergence (test scenario) | ruling | |---|---|---|---|---|---| -| `strategy.risk.allow_entry_in` | `risk_.direction`; `project()` already declares `spec.allowed_open_directions` (pine_adapter.cpp:2859); the forbidden opposite entry is reshaped to `CloseOpposite` in the adapter's own terms branch | per-opening cap `allowed_open_directions` | (i)+(ii), **already re-lowered** | none to measure: the cap is the kernel's, the close-only reshaping is the retained TV policy | done before N12; nothing to move | +| `strategy.risk.allow_entry_in` | `risk_.direction`; `project()` already declares `spec.allowed_open_directions` (pine_adapter.cpp:2861); 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:16514; 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:17504, `update_risk_state` pine_adapter.cpp:17504): 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:16506), 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:1422-1430): +1 per losing TRADE on a new chart day, reset to 0 by any winning trade at its fill (`consecutive_loss_days` pine_adapter.cpp:25744); 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:22513); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:20879), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:20911, `submit_intraday_loss_close` pine_adapter.cpp:20879), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:24701); 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:17499, `:17487`, `:17487`, `:17487`, `:17487`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:14600) | `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:17495); 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:17401, `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:0 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_drawdown` | `update_risk_state(bar.close)` once per script bar at the close mark (`update_risk_state(bar.close)` pine_adapter.cpp:17505, `update_risk_state` pine_adapter.cpp:17505): 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:16507), 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:1422-1430): +1 per losing TRADE on a new chart day, reset to 0 by any winning trade at its fill (`consecutive_loss_days` pine_adapter.cpp:25745); 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:22514); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:20880), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:20912, `submit_intraday_loss_close` pine_adapter.cpp:20880), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:24702); 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:17500, `:17500`, `:17500`, `:17500`, `:17500`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:14601) | `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:17496); 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:17402, `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:0 UTC) is a new chart day for the adapter and the same UTC day for the kernel. Corpus, kernel seeded with the declared cap: `cap-risk-gates-allow-max-intraday-01` identical 1464/1464 (the third fill of each day flattens the book, so block and latch coincide); `cap-max-intraday-filled-orders-isolate-01` 3840 of 3916 rows differ from row 77 (2025-04-08); `cap-gatekeeper-intraday-risk-01` 312 of 604 differ from row 293 (2025-10-25); `composite-bracket-cap-range-pending-stop-01` 2370 of 2384 differ from row 17 (2025-04-02 02:45), two extra rows | retained: the count alone is expressible, but the kernel's block would refuse fills the transfer admits and the close price is TV's; a report-only action would give the adapter a count it cannot use (charged slots ≠ applied fills) | **Proposed generic additions (not implemented — no consumer today).** A risk ticket (`label` / `comment` on `NativeRiskLimits`, folded only when set, @@ -1086,7 +1086,7 @@ row C). row, every placement snapshot the run had accepted (a sorted walk of every key, since replaced by the walk of the retained rows, `placement_` pine_state_hash.cpp:330) — one per accepted request, cleared only by -`reset_for_run` pine_adapter.cpp:2396 — and reflected the whole admission +`reset_for_run` pine_adapter.cpp:2398 — and reflected the whole admission journal (since replaced by a running digest of its events, `admission_journal` pine_state_hash.cpp:759). `PineScheduler::hash_state` pine_state_hash.cpp:797 re-folded the consumed source prefix too (`consumed` @@ -1133,10 +1133,10 @@ moving a single value, but not from inside lane F9's files: 1,339 rows each one callback old, an OCA re-price and a trail attach rows two callbacks old, and the row of an entry that filled at bar 1 is rewritten at bar 60, when its first exit is attached - (`has_full_entry_bracket` pine_adapter.cpp:11434). Full-table scans - rewrite rows as well (`suspend_brackets_for_reversal` pine_adapter.cpp:1965, - `revive_brackets_after_margin` pine_adapter.cpp:2200, - `preserved_by_close_all` pine_adapter.cpp:14667), and + (`has_full_entry_bracket` pine_adapter.cpp:11437). Full-table scans + rewrite rows as well (`suspend_brackets_for_reversal` pine_adapter.cpp:1967, + `revive_brackets_after_margin` pine_adapter.cpp:2202, + `preserved_by_close_all` pine_adapter.cpp:14668), 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. @@ -1221,16 +1221,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:2123), the revival skips a withdrawn +(`retire_cancelled_exits` pine_adapter.cpp:2125), 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:3648). Both shapes now cost ×1.89 to ×2.12 per doubling up +pine_adapter.cpp:3650). 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:4096); a lot opened +it off (`release_closed_cohort_origins` pine_adapter.cpp:4099); a lot opened by a replace successor keeps every older member of its roster while it is open, because its root is not a public fact. The flip then costs ×1.99, ×2.01, ×2.02 per doubling, and its rosters end with one member where they @@ -1600,10 +1600,10 @@ the rule gives 9.9 + 9.89). | # | Risk | Why it bites | Containment | Src | |---|---|---|---|---| | E1 | **Spec hashing.** `hash_spec` folded every field unconditionally (native_execution_consumer.cpp:235-257); every block added since folds only when set | Any new field, even defaulted, changes every continuation / broker-state hash, adapter runs included | Fold new blocks only when set, as a tagged extension (precedent `precommit_digest_` native_execution_consumer.hpp:1942); test that a default spec hashes to the pre-change constant | F | -| E2 | **Margin.** The TV margin call is built host-side from kernel primitives (a Stop-triggered `Reduce` bound to openings, or `execute_current`, which survives in `submit_margin_call_slice` pine_adapter.cpp:17967); its chronology lives at `schedule_margin_call_path` pine_adapter.cpp:20245 and its excursion sample differed by chronology (the Pine host's own excursion model, retired by R5 lane H-THIN: the kernel's sampler books it) | A kernel model under an adapter run would liquidate twice or reorder settlement; a checkpoint that reduces mid-path changes lot state exactly where the adapter's chronology lives | Superseded by R5: the adapter declares `spec.margin` — maintenance-only, so no opening is decided twice (`AdmitWithHostMargin`, MG4) — and the kernel owns the mechanism (check points, resting liquidation, receipt) while the adapter's hooks answer TradingView's scheduling, money and slice (ADR-0001 "Margin"). What stays host-side is named there: the chronology exceptions, the pre-open slice, the 1x-long money call and the broker-open FX rollover, whose kernel `FxRoll` point measures elsewhere (MG9). Corrected by R5 lane F7 | F O S | -| E3 | **Sizing.** TV sizes from the tick-rounded signal close, 10-digit-rounded equity and fee-reserved cash, frozen at signal, with fill-time exceptions in COOF paths (`default_sizing_shape` pine_adapter.cpp:3191, the COOF `at_fill` exception pine_adapter.cpp:8404) | One "generic" percent sizing serving both would break parity or import TV rounding into the kernel; the last ulp differs on lot-stepped instruments | Two paths on purpose: `Sized` (kernel, plain arithmetic, timing as a first-class field) vs `HostSized` + the adapter resolver. Never "simplify" the adapter onto the kernel basis. What the two paths share is the conversion: since R5 lanes N11 and F7 the division by price × point value × FX and the percentage fee reserve exist once, in the kernel (`native_sized_units`), for a default AND a typed (`qty_type` cash / percent) quantity. The adapter keeps what is measured TradingView: the money basis (signal-time equity, TradingView's margin equity at a typed fill, the hypothetical Flatten's balance for a typed reversal), the freeze, the fill-time exceptions and both lot floors (`tests/test_adapter_sizing_relower.cpp` cases 8, 9, 11: kernel 0.0392 against the percent floor's 0.0391 on a 0.0001 lot), and a staged FX series keeps `Sized` wherever the acceptance coordinate converts at the source's rate (case 12) | F O S | -| E4 | **Calc on fill.** Kernel suffix eligibility is continuous along the segment; TV refills only at O/H/L/C waypoints -- on a magnified path at each intrabar's open, twice, and its two extremes, never its close (R5 lane MAG-INTRABAR) (`PineScheduler::recalculate` pine_scheduler_native.cpp:745, `coof_next_waypoint` pine_adapter.cpp:6958) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:6551 and `drain_applied_notifications` (native_execution_consumer.cpp:7234); 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:22261) | The complete bar cannot be masked globally | Opt-in `OpenOnly` view (A.4); loud doc warning in L0 | F | +| E2 | **Margin.** The TV margin call is built host-side from kernel primitives (a Stop-triggered `Reduce` bound to openings, or `execute_current`, which survives in `submit_margin_call_slice` pine_adapter.cpp:17968); its chronology lives at `schedule_margin_call_path` pine_adapter.cpp:20246 and its excursion sample differed by chronology (the Pine host's own excursion model, retired by R5 lane H-THIN: the kernel's sampler books it) | A kernel model under an adapter run would liquidate twice or reorder settlement; a checkpoint that reduces mid-path changes lot state exactly where the adapter's chronology lives | Superseded by R5: the adapter declares `spec.margin` — maintenance-only, so no opening is decided twice (`AdmitWithHostMargin`, MG4) — and the kernel owns the mechanism (check points, resting liquidation, receipt) while the adapter's hooks answer TradingView's scheduling, money and slice (ADR-0001 "Margin"). What stays host-side is named there: the chronology exceptions, the pre-open slice, the 1x-long money call and the broker-open FX rollover, whose kernel `FxRoll` point measures elsewhere (MG9). Corrected by R5 lane F7 | F O S | +| E3 | **Sizing.** TV sizes from the tick-rounded signal close, 10-digit-rounded equity and fee-reserved cash, frozen at signal, with fill-time exceptions in COOF paths (`default_sizing_shape` pine_adapter.cpp:3193, the COOF `at_fill` exception pine_adapter.cpp:8407) | One "generic" percent sizing serving both would break parity or import TV rounding into the kernel; the last ulp differs on lot-stepped instruments | Two paths on purpose: `Sized` (kernel, plain arithmetic, timing as a first-class field) vs `HostSized` + the adapter resolver. Never "simplify" the adapter onto the kernel basis. What the two paths share is the conversion: since R5 lanes N11 and F7 the division by price × point value × FX and the percentage fee reserve exist once, in the kernel (`native_sized_units`), for a default AND a typed (`qty_type` cash / percent) quantity. The adapter keeps what is measured TradingView: the money basis (signal-time equity, TradingView's margin equity at a typed fill, the hypothetical Flatten's balance for a typed reversal), the freeze, the fill-time exceptions and both lot floors (`tests/test_adapter_sizing_relower.cpp` cases 8, 9, 11: kernel 0.0392 against the percent floor's 0.0391 on a 0.0001 lot), and a staged FX series keeps `Sized` wherever the acceptance coordinate converts at the source's rate (case 12) | F O S | +| E4 | **Calc on fill.** Kernel suffix eligibility is continuous along the segment; TV refills only at O/H/L/C waypoints -- on a magnified path at each intrabar's open, twice, and its two extremes, never its close (R5 lane MAG-INTRABAR) (`PineScheduler::recalculate` pine_scheduler_native.cpp:745, `coof_next_waypoint` pine_adapter.cpp:6961) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:6551 and `drain_applied_notifications` (native_execution_consumer.cpp:7234); 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:22274) | The complete bar cannot be masked globally | Opt-in `OpenOnly` view (A.4); loud doc warning in L0 | F | | E6 | **Double equity recording.** The source host already records per bar (history: only its cadence remains, `scheduler_mark_report_point` pine_strategy_host.cpp:2791) | Kernel recording would duplicate points and shift drawdown | Report policy / ownership (A.1); the adapter declares `KernelRecordedAtHostMarks` (R1): one recorder, at the adapter's own marks | F O | | E7 | **Price grid.** TV does not merely quantize: it tests triggers against a quantized bar while booking the fill on the *unrounded* level, so the adapter shifts the threshold (`source_trigger_threshold` pine_adapter.cpp:384) and books elsewhere | A kernel grid that quantizes both sides is correct and not TV; moving rounding into the kernel would change both native results and the adapter | `NativePriceGrid::None` for the adapter permanently; never "unify". Before L8: a byte-level experiment that the default price spelling on decimal ticks (one-ULP representations) does not move (S) | O S | | E8 | **HTF.** The evaluator machinery carries TV publication semantics (lookahead, boundary deferral, early close, `syminfo.type` branching engine_security.cpp:67-73). The feed store is TV-calibrated too: "W" / "M" from native daily bars, native stamps as the period partition (`set_native_security_feed` `sha256:eca3dbea0baa461982ba89f13d0a5c43277854069fb6eb2dd2f5c5471313204d` engine_aux_security.cpp:51-83, *fresh*). Two aggregators exist: script buckets use `native_calendar`, `request.security` uses `TimeframeAggregator`, whose completion rules are TV-pinned (timeframe.hpp:288-330) | Reuse risks changing the *order* of the Pine scheduler's pumps; a bare host inherits TV partition rules; native HTF may differ from Pine HTF on early-close sessions | Registration-gated pump; no change to `scheduler_feed_security_input` pine_strategy_host.cpp:1816; full corpus diff; supplied-bar provenance preserved, never silently replaced by chart aggregation (S); differences on early-close sessions are recorded, not "fixed" (F). The Pine scheduler keeps one more `TimeframeAggregator` pass, before the run (`PineScheduler::run_begin`, audit M26): the script-bar count it yields (`terminal_source_bar`, the probe-tail suppression) and the per-input completion flags it hands `on_native_input` are needed before the first input arrives, while the kernel seals script intervals only as inputs arrive and offers no pre-run interval query (`NativeExecutionConsumer::script_interval_at` is consumer-private). Retained by R5 lane F7: replacing it needs that kernel capability | F O S | @@ -1623,7 +1623,7 @@ the rule gives 9.9 + 9.89). | P5 | **Test asymmetry:** 430 adapter test files vs 14 native when this plan was written (on this tree 543 `tests/*.cpp` name `source::PineStrategyHost`, 110 `NativeStrategyHost`) | native-only tests in every lane (§3.1 b) | O | | P6 | **Stream vs batch.** The stream refuses monthly input, mixed modes, in-session gaps, security feeds and source setters, and tick input under an FX curve (any non-empty curve until audit lane N6) (`native-engine.md`, "Stream limitations (current)") | L5 and L6 must state which refusals are generic and which are deliberate source limits | S | | P7 | **C ABI variant growth** | §2.iii hardening rules | S | -| P8 | **Unverified in the inputs:** whether `execution::LifecycleEffects` (execution.hpp:136-139) is ever non-empty from the Pine path, else `PlacementSnapshot::legs` (pine_adapter.hpp:364) is the only live lifecycle (O); the `strategy.risk.allow_entry_in` setter chain beyond `set_pine_risk_direction` pine_strategy_host.cpp:886 → `set_risk_direction` pine_adapter.cpp:26486 (O); what the ABI receipts pin (F); the probe corpus itself lives outside this tree (F) | verify inside the lane that touches each | F O | +| P8 | **Unverified in the inputs:** whether `execution::LifecycleEffects` (execution.hpp:136-139) is ever non-empty from the Pine path, else `PlacementSnapshot::legs` (pine_adapter.hpp:364) is the only live lifecycle (O); the `strategy.risk.allow_entry_in` setter chain beyond `set_pine_risk_direction` pine_strategy_host.cpp:886 → `set_risk_direction` pine_adapter.cpp:26487 (O); what the ABI receipts pin (F); the probe corpus itself lives outside this tree (F) | verify inside the lane that touches each | F O | ### 4.3 Open design questions (supervisor level) @@ -1650,7 +1650,7 @@ All six are settled by what landed: U1 (native subscriptions take `lookahead` an | **U3** | **Do risk limits ship in v1?** | R5-9: in scope, not a blocker. S wanted them as a prerequisite; F accepts a toolkit stand-in; O defers. | | **U4** | **Stream-safe FX (FP6): in the roadmap or not?** It has no R5 lane. | F: out of scope here ("O7 clock"). S: its P10, an FX epoch consumed while flat, same curve in batch and stream. | | — | **U4, answered by audit lane N6.** In: the curve declared through `configure_native_fx_curve` is the FX epoch of the run, batch or stream; the account converts at the rate of each driver point's effective time, so the epoch is consumed by the walk itself, flat or not, and a confirmed-bar stream is the batch of the same bars. No `NativeFxEpoch` type and no `on_native_fx_epoch` callback were needed. Out, permanently: realtime rate ingestion (a new rate is a new run) and tick-driven input under a curve (its hooks and partial slots read a stale conversion clock — measured). | — | -| — | **N6 follow-ups (measured on the adapter; both closed by R5 lane F7).** (1) `apply_fx_open_margin_slice` (pine_adapter.cpp:5688) 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:3264) 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:5691) 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:3266) returns nullopt whenever any FX series is staged — even one point that restates the scalar rate — so an FX run silently loses the kernel `Sized` path R2b landed (measured: 1 kernel `Sized` request without a series, 0 with a one-point 1.0 series, same 50-unit book). **Closed by R5 lane F7.** (1) The throw is gone: the carried roll takes TradingView's broker-open checkpoint at any positive margin — its money at the open under the new rate and the side's own maintenance ratio, its lot-floored 4x restore, executed at the open before the step bar's script — and a survivor that is not a 1x long is re-checked over the rest of the bar by the kernel's liquidation. A 2x long or short of 15 at 100 on 1000 across 1 → 1.5 takes 6.666666666666667 at the open and keeps 8.33; at a 25 % side ratio it takes nothing; a constant curve never rolls (`tests/test_affordability_fx.cpp` F7 rows; G6's leveraged pin and its L0 oracle twin moved with an `expectation corrected:` note). The kernel's `FxRoll` point stays refused, measured (MG9). (2) `Sized` is withdrawn only where the kernel would convert at another rate than the source sized at — a curve step between the sub-bar open and the acceptance coordinate — so a one-point series lowers exactly as no series does, and on a stepped curve only the step's own bar keeps its host-resolved intent (`tests/test_adapter_sizing_relower.cpp` case 12). | — | | **U5** | **Open-bar view default.** Should `OpenOnly` be the default for hosts that are not `LegacyTolerant`? | F: safer for users, but a behaviour change for the ~20 existing native tests; default `Complete` proposed. | | **U6** | **TA comparison band.** Is TradingView's 1e-10 tie-breaking a feature of the TA library, or a default to make configurable? | F, O: rename only; a switch changes indicator outputs and needs its own lane. S: separate policies, hash the policy identity. | @@ -1673,7 +1673,7 @@ NativeMetricsView native_metrics() const; // S: net_pro ``` - `KernelRecorded`: the consumer calls the existing `update_equity_extremes()` (engine.hpp:1363) + `record_equity_point(script_open_ms)` (engine.hpp:1381) once per script calculation (after `invoke_callback` native_execution_consumer.cpp:8193, :8193, :8193, :8193) 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:71). -- `KernelRecordedAtHostMarks` (R1, KERNEL-EDGE): the same recording, at the points a C++ host names with `NativeStrategyHost::mark_native_report_point` instead of once per calculation; the consumer never records on its own initiative under it, so the continuation identity stays where `HostRecorded` leaves it. The Pine adapter's existing `mark_script_report_point` route has the same kernel producer. It is what the adapter declares (`KernelRecordedAtHostMarks` pine_adapter.cpp:2855), with `report_open_position_at_end == false`. +- `KernelRecordedAtHostMarks` (R1, KERNEL-EDGE): the same recording, at the points a C++ host names with `NativeStrategyHost::mark_native_report_point` instead of once per calculation; the consumer never records on its own initiative under it, so the continuation identity stays where `HostRecorded` leaves it. The Pine adapter's existing `mark_script_report_point` route has the same kernel producer. It is what the adapter declares (`KernelRecordedAtHostMarks` pine_adapter.cpp:2857), with `report_open_position_at_end == false`. - Publish read-only `closed_trade_*` accessors (RP6). - Alternative: F's virtual `owns_equity_recording()` (the A48 ownership pattern of `owns_lot_excursions` native_host.hpp:1023). Same semantics, but it costs a host-epoch bump, so the spec form was chosen; the virtual was never added, and `NativeReportPolicy` is what landed (§3.4 row 3). - TUs: `native_execution_consumer.cpp` (seal path, `seal_script` native_execution_consumer.cpp:9150), `engine_run.cpp`, `engine_report.cpp`, `engine_state_hash.cpp`, `native_run_spec.{hpp,cpp}`. @@ -1726,7 +1726,7 @@ virtual std::optional resolve_margin_call_units(const NativeMarginCallVi virtual void on_native_margin_call(const native_order::MarginCallEvent&) {} // O, S ``` -- Mechanism (F): a **kernel-originated request**. At each script-bar open and after each applied fill the kernel computes the liquidation level and rests a `Reduce` / `Flatten` with `Stop{level}` bound to the live openings — the shape the adapter builds host-side today (pine_adapter.cpp:20232). New hashed `RequestDefinition::origin { Host, KernelLiquidation, KernelRisk }`, new `CancelReason::Superseded` on re-price, `MarginCallEvent` joins `CommandEvent` (17 → 18, native_order.hpp:1308). O's alternative, a checkpoint on the excursion walk (`apply_excursion` native_execution_consumer.cpp:3392), 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:20233). New hashed `RequestDefinition::origin { Host, KernelLiquidation, KernelRisk }`, new `CancelReason::Superseded` on re-price, `MarginCallEvent` joins `CommandEvent` (17 → 18, native_order.hpp:1308). O's alternative, a checkpoint on the excursion walk (`apply_excursion` native_execution_consumer.cpp:3392), 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:3716 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). From eb3aef029c7911fd28ed17e35491fab73a9a0a5a Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Mon, 19 Oct 2026 06:35:15 +0800 Subject: [PATCH 26/35] docs: refresh refactor progress anchor --- docs/native-refactor-progress.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/native-refactor-progress.md b/docs/native-refactor-progress.md index bb804206e..6e547bcaa 100644 --- a/docs/native-refactor-progress.md +++ b/docs/native-refactor-progress.md @@ -173,7 +173,7 @@ them for the record only. "Linux aarch64" is the maintainers' Cortex-A725 host. | Item | What it is | Measured size | Deferred by | Prototype and evidence | Reopens when | | --- | --- | --- | --- | --- | --- | -| D2-B, the quiet-bar protocol and one receipt read per bar | PERF-D2's lane: a per-bar bar-open request, a bulk fold of quiet driver points, the adapter's quiet-open predicate with lazy open bookkeeping, and a gate before the receipt poll. D2-A landed the hook declarations and the O(1) `has_command_after` (`src/native_execution_consumer.hpp:321`). PERF-L4 gated the read inside `observe_terminal_receipts` (`src/source/pine_adapter.cpp:6457`), which still runs about three times a bar | PERF-D2, Linux aarch64 (Cortex-A725), callgrind and GBench: about ×0.88 on the 14 probes (an estimate from a measured −13 to −14 % floor bound for the bar-open request alone); after PB the receipt poll still costs about 250 instructions a bar on the no-order floor. On this tree the quiet-bar probe asks the receipt gate 40,996 times and each once-per-bar-open gate 13,248 times: 3.09 reads a bar, 82 % of them stopped at the gate (macOS arm64, AppleClang 17, Release) | "R5 PERF-D2 DONE 2026-09-24 19:56" (D2-B after V19-D); "R5 SUPERVISOR RULING 2026-09-25 00:15 Taipei": "D2-B (quiet-bar protocol) waits for PERF-D3's verdict"; TRIAGE1 §D | An unpublished prototype branch and its patches. PERF-D3's S1 (next row) prototyped the request and the lazy opening | A post-release performance epoch. Done when the receipt gate is asked as often as the once-per-bar gates | +| D2-B, the quiet-bar protocol and one receipt read per bar | PERF-D2's lane: a per-bar bar-open request, a bulk fold of quiet driver points, the adapter's quiet-open predicate with lazy open bookkeeping, and a gate before the receipt poll. D2-A landed the hook declarations and the O(1) `has_command_after` (`src/native_execution_consumer.hpp:321`). PERF-L4 gated the read inside `observe_terminal_receipts` (`src/source/pine_adapter.cpp:6460`), which still runs about three times a bar | PERF-D2, Linux aarch64 (Cortex-A725), callgrind and GBench: about ×0.88 on the 14 probes (an estimate from a measured −13 to −14 % floor bound for the bar-open request alone); after PB the receipt poll still costs about 250 instructions a bar on the no-order floor. On this tree the quiet-bar probe asks the receipt gate 40,996 times and each once-per-bar-open gate 13,248 times: 3.09 reads a bar, 82 % of them stopped at the gate (macOS arm64, AppleClang 17, Release) | "R5 PERF-D2 DONE 2026-09-24 19:56" (D2-B after V19-D); "R5 SUPERVISOR RULING 2026-09-25 00:15 Taipei": "D2-B (quiet-bar protocol) waits for PERF-D3's verdict"; TRIAGE1 §D | An unpublished prototype branch and its patches. PERF-D3's S1 (next row) prototyped the request and the lazy opening | A post-release performance epoch. Done when the receipt gate is asked as often as the once-per-bar gates | | S1a, kernel quiet-bar driving (PERF-D3) | A bar with nothing live, a flat book and a declined opening is driven in a quiet form: the kernel writes the bookkeeping its four price points would leave (ordinals, the last two driver marks, the floor, the point epochs) without walking them or calling the bar-open callback. Additive host cadence declarations; C hosts declare from their callback table; one C symbol under the PF_API checklist | PERF-D3, Linux aarch64 (Cortex-A725), GBench time and `perf stat` instructions, on the L3 tree `b33ea659`, S1a with S1b: no-order floor 0.764 / 0.660 in time (0.718 / 0.656 in instructions), 14 probes 0.958 / 0.921 (0.892 / 0.856), 100 slots 0.972 / 0.956 per slot. A bare kernel host that opts in: idle 0.545× (35 % fewer instructions a bar), market 0.81×, bracket 0.92×. Byte-identical: corpus 312/312, the 132-configuration battery with its hash columns | "FINDING PERF-D3 (filed for a post-release epoch per the owner's cap)" (ledger, 2026-09-24 16:58 UTC); "OWNER DECISION 2026-09-25 00:20 Taipei"; TRIAGE1 §D: "PERF-D3 S1/S2 (post-release epoch)" | An unpublished prototype branch (five commits on L3's tree) and its patches | The post-release epoch, with the owner calls PERF-D3 lists (the API under the existing ruling, the restated gate count) | | S1b, the Pine lazy opening | The Pine host records what its skipped opening and its uncalled input callback would have written, at the bar's first host callback. Member functions only, no layout change | Measured with S1a. A quiet bar then asks 15 gates, not 30, so the quiet-bar cost check `CHECK(asked >= 20 * o.boundary.size())` (`tests/test_adapter_quiet_bar.cpp:1054`) must be restated | As S1a | As S1a. The in-script hash witnesses caught the prototype's one miss (`awaiting_legacy_script_open_ms_`) | As S1a | | S1c, the held-position wake band | S1 for bars that hold a position with nothing live, 41 % of the median public slot's bars. The host declares a price band, and the kernel calls the opening only when the bar's range leaves it. The adapter derives the band from its margin model and declines when it cannot bound it | PERF-D3, a measurement-only upper bound without the band, which is not sound in general: 100 slots 0.776 / 0.716 per slot, 14 probes 0.830 / 0.761, cumulative against L3 with S1, S2 and D2-A | As S1a; the owner call "Fund the price band" | An unpublished measurement-only script | As S1a. Size L, parity risk high: TradingView's margin money rules | From 6baa158d45f4b0ddebab0d2f28b530b555ee5a9a Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Mon, 19 Oct 2026 08:51:18 +0800 Subject: [PATCH 27/35] fix: normalize non-working bracket cancellations --- src/source/pine_adapter.cpp | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/source/pine_adapter.cpp b/src/source/pine_adapter.cpp index 7cb4b1064..3f82f322b 100644 --- a/src/source/pine_adapter.cpp +++ b/src/source/pine_adapter.cpp @@ -14513,14 +14513,18 @@ void PineExecutionAdapter::exit_cancel_bracket(const SourceId& exit_id, found_family = true; } if (!found_family) return; - // Settled members no longer retain incarnation bytes. They have already - // left the working book, so a same-run sentinel is classified as - // NotWorking and emits the same compact cancel event while advancing the - // command ordinal once per settled member. + // Every member the kernel no longer works, including settled members, + // uses one same-run incarnation that no request is ever issued under. + constexpr std::uint64_t kNeverIssued = std::numeric_limits::max(); for (std::size_t index = 0; index < settled; ++index) - (void)require_host().cancel(native_order::RequestHandle{settled_run, 0}); + (void)require_host().cancel(native_order::RequestHandle{settled_run, kNeverIssued}); + IExecutionConsumer* consumer = bound_consumer(); for (const auto& handle : handles) { - const auto result = require_host().cancel(handle); + const bool working = consumer == nullptr + || as_native_consumer(*consumer).request_core().find_live(handle) != nullptr; + const auto target = working ? handle + : native_order::RequestHandle{handle.run, kNeverIssued}; + const auto result = require_host().cancel(target); if (result.status == native_order::CancelStatus::Cancelled) retire(handle); } } From 38914a61c65eb4de92005382997bb374274220b4 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Mon, 19 Oct 2026 08:54:16 +0800 Subject: [PATCH 28/35] docs: refresh shifted adapter anchors --- docs/adr/0001-kernel-adapter-boundary.md | 16 +++--- docs/design/native-feature-parity.md | 72 ++++++++++++------------ docs/native-refactor-progress.md | 2 +- docs/pages/native-engine.md | 2 +- 4 files changed, 46 insertions(+), 46 deletions(-) diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index 7e27fc0be..1bdc9c4b9 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -213,8 +213,8 @@ kernel and own these quirks, each at its site: - **Seams the adapter overrides.** The kernel is driven, not patched. `PineStrategyHost` overrides `resolve_execution_terms` (`pine_strategy_host.cpp:828`) for TV sizing and fill spelling; `validate_execution_precommit` (`pine_strategy_host.cpp:853`), whose - `validate_precommit` (`pine_adapter.cpp:16395`) returns `AdmitWithHostMargin` - (`pine_adapter.cpp:16590`) to own the opening margin decision; `on_native_tick` + `validate_precommit` (`pine_adapter.cpp:16399`) returns `AdmitWithHostMargin` + (`pine_adapter.cpp:16594`) to own the opening margin decision; `on_native_tick` (`pine_strategy_host.cpp:555`) / `on_native_applied` (`pine_strategy_host.cpp:627`) for `calc_on_order_fills` re-entry. It does not own lot excursions: since R5 lane H-THIN it keeps the kernel's `owns_lot_excursions()` default, so the kernel's sampler books every Pine lot's @@ -239,7 +239,7 @@ kernel and own these quirks, each at its site: - **Trail and tick conventions.** The half-tick arm threshold measured against tick-quantized extremes while the raw running best is retained (`has_trail_request` `pine_adapter.cpp:12511-12516`); the half-tick trigger threshold (`source_trigger_threshold` pine_adapter.cpp:384) and raw-vs-booked fill spelling, behind the - adapter's terms seam (`resolve_terms` `pine_adapter.cpp:14868`). + adapter's terms seam (`resolve_terms` `pine_adapter.cpp:14872`). - **Money arithmetic.** Ten-significant-digit half-up money (`source_money_round` `pine_adapter.cpp:558-564`, twin `tv_money_round` `pine_policy_support.hpp:9-15`). - **Quantity dust.** After every applied execution the Pine host erases any lot of at most @@ -260,8 +260,8 @@ kernel and own these quirks, each at its site: (`select_reservation_growth_sources` src/compat/pine/reservation_expansion.cpp:9). - **Margin.** The kernel owns the margin *mechanism* — the level solve, the check points, the kernel request, its re-pricing, the receipt — and the adapter answers its three policy hooks - with TradingView's: `margin_check_allowed` (`pine_adapter.cpp:17854`) for the scheduling, - `resolve_margin_requirement` (`pine_adapter.cpp:17923`) for the ten-significant-digit money, + with TradingView's: `margin_check_allowed` (`pine_adapter.cpp:17858`) for the scheduling, + `resolve_margin_requirement` (`pine_adapter.cpp:17927`) for the ten-significant-digit money, and `resolve_margin_call_units` for the lot-floored 4x restore. That hook answers every call on purpose (R5 lane F7): TradingView floors the restore onto the lot grid before the multiple, so on a gridded tape the kernel's own `ShortfallMultiple 4` books 5 lots where TradingView @@ -272,7 +272,7 @@ kernel and own these quirks, each at its site: margin since F7), while the roll point measures at the remaining path's adverse mark (MG-FX: 0.4116 @ 97 there against TradingView's 0.3996 @ 100). What has no kernel check point at all stays adapter-side: the `process_orders_on_close` chronology exception - (`non_pooc_commissioned_short` `pine_adapter.cpp:24189`) and + (`non_pooc_commissioned_short` `pine_adapter.cpp:24193`) and the 1x-long money call — plus the TV admission scopes (`explicit_pair_scope` `src/compat/pine/market_admission.cpp:34-47`) and the review fold they feed (`awaits_pair_review` `:68-73`, `fold_admission_history` `:83-140`). @@ -710,8 +710,8 @@ Every line number in this section names its symbol on this tree, which | `NativeRunSpec` field | ruling | what the adapter runs instead | measurement and ruling of record | executed native consumers | |---|---|---|---|---| | `price_grid`, `grid_rounding` | **native-only** | TradingView's per-order-kind tick rules on top of `None`: `source_trigger_threshold` (`pine_adapter.cpp:384`), `source_level_on_price_grid` (`:367`), `nearest_tick` (`:274`) / `source_bar_fill_tick` (`:295`) / `directional_tick` (`:327`), behind the terms seam | R5-3 and design risk E7 ruled `None` for the adapter before the lane ran; lanes R7 and N13 measured the alternative anyway (raw levels submitted, `QuantizeFillsAndTriggers` with `HalfUp` declared). L8b closed the first blocker (no run aborts). The second has no remedy on either side: TradingView quantizes per order kind (stop and limit legs and a trail's activation on the quantized bar; the trail stop, the running best, stop-limit entries and the `calc_on_order_fills` cursors raw), the grid is one rule for the run, and the old N13 trial moved 30 pinned checks in 4 units; this is historical evidence, not a current-head result; a per-kind mask would spell that inconsistency into the kernel. The corpus cannot arbitrate (every probe runs a 0.01 tick on an on-grid feed; 5 of 312 differ in an engine-only column). Permanent witness: `tests/test_adapter_grid_relower.cpp`, whose section 5 pins the trail stop the grid fires a bar early. Design row PG and §3.6 | `examples/native/native_price_grid_strategy.cpp`, `examples/native/native_price_grid_c.c`, the private test `test_native_price_grid` | -| `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:17505`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:20880`), `chart_day_key` (`pine_adapter.cpp:17402`), `compat::pine::IntradayCap` with `IntradayOrderBudget` (337 lines) | Structural first: Pine's risk calls are per-bar statements, so a limit reaches the adapter on script bar 0, after `project()` (`pine_strategy_host.cpp:350`) and the kernel's `configure` (`pine_strategy_host.cpp:359`) have fixed and digested the spec. In substance (lane N12, `tests/test_adapter_risk_relower.cpp`, 62 checks over nine paired scenarios): the drawdown latch samples at the close only and still admits a reversal; the loss-day streak counts trades, not days; the intraday loss closes at the path's adverse extreme, refuses every placement and withdraws the book; the fill cap charges slots, transfers quota and closes at the bar's better extreme on the chart timezone's day. With the kernel seeded on the corpus, 3 of 4 cap probes diverge (3840 of 3916, 312 of 604, 2370 of 2384 rows; re-derived by `scripts/check_seeded_risk_experiment.sh` since R5 lane H-MEASURE). `strategy.risk.allow_entry_in` is the one rule that is the kernel's already (`allowed_open_directions`). Design §3.6 | `examples/native/native_risk_limits_strategy.cpp`, `examples/native/native_trail_risk_strategy.cpp`, `tests/test_native_risk_limits.cpp`, `tests/test_native_c_api.c` | -| `max_abs_units` | **adapter-policy** | `strategy.risk.max_position_size` as a gate on the LIVE book before the fill (`max_position_size` `pine_adapter.cpp:16513-16514`): an entry is refused once the book already holds the limit | design row MG2 (R5-1): the resulting-book cap is the generic one. Measured by N12's scenario `PS` in `tests/test_adapter_risk_relower.cpp`: two-unit entries against a limit of 3 leave the adapter at 4 and the kernel cap at 2 | `tests/test_native_resting_matching_contract.cpp`, `tests/test_native_run_spec.cpp` | +| `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:17509`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:20884`), `chart_day_key` (`pine_adapter.cpp:17406`), `compat::pine::IntradayCap` with `IntradayOrderBudget` (337 lines) | Structural first: Pine's risk calls are per-bar statements, so a limit reaches the adapter on script bar 0, after `project()` (`pine_strategy_host.cpp:350`) and the kernel's `configure` (`pine_strategy_host.cpp:359`) have fixed and digested the spec. In substance (lane N12, `tests/test_adapter_risk_relower.cpp`, 62 checks over nine paired scenarios): the drawdown latch samples at the close only and still admits a reversal; the loss-day streak counts trades, not days; the intraday loss closes at the path's adverse extreme, refuses every placement and withdraws the book; the fill cap charges slots, transfers quota and closes at the bar's better extreme on the chart timezone's day. With the kernel seeded on the corpus, 3 of 4 cap probes diverge (3840 of 3916, 312 of 604, 2370 of 2384 rows; re-derived by `scripts/check_seeded_risk_experiment.sh` since R5 lane H-MEASURE). `strategy.risk.allow_entry_in` is the one rule that is the kernel's already (`allowed_open_directions`). Design §3.6 | `examples/native/native_risk_limits_strategy.cpp`, `examples/native/native_trail_risk_strategy.cpp`, `tests/test_native_risk_limits.cpp`, `tests/test_native_c_api.c` | +| `max_abs_units` | **adapter-policy** | `strategy.risk.max_position_size` as a gate on the LIVE book before the fill (`max_position_size` `pine_adapter.cpp:16517-16518`): an entry is refused once the book already holds the limit | design row MG2 (R5-1): the resulting-book cap is the generic one. Measured by N12's scenario `PS` in `tests/test_adapter_risk_relower.cpp`: two-unit entries against a limit of 3 leave the adapter at 4 and the kernel cap at 2 | `tests/test_native_resting_matching_contract.cpp`, `tests/test_native_run_spec.cpp` | | `max_open_lots` | **adapter-policy** | Pine pyramiding is a per-cycle entry count in the adapter's command policy; a resting source entry must not consume a physical-lot cap before it fills, so `project()` leaves the cap unset (`max_open_lots` `pine_adapter.cpp:2646-2648`) | design row MG3 (R5-1); the contract comment in `project()`. Measured against TradingView by R5 lane H-MEASURE (the private test `test_pyramiding_count_differential`, 15 scenarios on three `lab tv` tapes): TradingView checks an entry once, at its first eligible point, against the trades then open, and does not check a resting entry again at its fill; the kernel's cap books 14 of the 15 as TradingView does, the adapter's per-cycle count 13 (it keeps a slot a close or another entry's exit drained; since lane W8A-SIGSTATE-1's R-B a resting LIMIT entry no longer counts, and one the count admitted fills over the cap, and since lane W5B-ENG-MARGIN-RESIDUAL's SB the later market entries of a flat bar are refused at the fill once the book holds the cap, as on TradingView's tapes). A recorded divergence: the retention stands pending a re-lowering ruling. R5 lane H-THIN tried the lowering and kept the count: on the kernel's cap four corpus probes moved away from TradingView, for want of the first-eligible-point rule (H-MEASURE's P4b); it fixed the reservation divergence measured beside it (P10), so a default `strategy.exit(from_entry)` under FIFO now reserves its entry's own quantity, the oldest lots first, as TradingView does | `tests/test_native_resting_matching_contract.cpp`, `tests/test_native_margin_model.cpp` | | `initial_margin_fraction` | **adapter-policy** | TradingView's ten-significant-digit money admission against the signal-time tuple, answered as `AdmitWithHostMargin`; the `margin` model the adapter does declare is maintenance-only (`NativeMarginModel` `pine_adapter.cpp:2885-2893`, `margin.maintenance_long` `pine_adapter.cpp:2902`) | design row MG4 and the wave-4 ruling recorded in `project()`: a positive initial requirement would decline openings TradingView takes. Measured both ways by R5 lane H-MEASURE (the private test `test_adapter_margin_schedule_differential`, M11): the kernel's gate declines a 1x long whose entry fee the adapter admits (`InitialMargin`), and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times | `tests/test_native_precommit_view.cpp`, `tests/test_native_margin_model.cpp` | | `report_open_position_at_end` | **adapter-policy** | The Pine host owns TradingView's range-end shape: batch re-marks the stored final point and re-folds extremes, while REALTIME retains ordinary marks and `PineStrategyHost::present_report` re-marks only the caller-owned copy before generic metric derivation; the stream hash excludes that terminal extreme fold (`scheduler_record_range_end` pine_strategy_host.cpp:1987): report shape, not a mark-to-market row (`KernelRecordedAtHostMarks` pine_adapter.cpp:2857) | design row RP5; the kernel reads the field under `KernelRecorded` only, which `scripts/check_adapter_spec_shadowing.py` gates | `examples/native/native_sized_report_strategy.cpp`, `tests/test_native_report_truth.cpp` | diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index a61f11ff8..812122214 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -47,10 +47,10 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:3447; `entry()` pine_adapter.cpp:7171, `order()` pine_adapter.cpp:14634) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:6623; `submit` native_host.hpp:1213 | K | — | F:A1 O:A1 S:O2 | — | +| OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:3447; `entry()` pine_adapter.cpp:7171, `order()` pine_adapter.cpp:14638) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:6623; `submit` native_host.hpp:1213 | K | — | F:A1 O:A1 S:O2 | — | | OL2 | Intents: flatten, reduce by units, reduce owner-opened units, transact, reverse-to (`close_all()` pine_adapter.cpp:10555 → `Flatten{}` pine_adapter.cpp:10589; `Reduce{ExplicitUnits}` pine_adapter.cpp:9428; `ReverseTo` pine_adapter.cpp:7847) | **yes** — `OrderIntent` native_order.hpp:229, reduce sizes `ReductionSize` native_order.hpp:215, `ReverseTo` native_order.hpp:60-62, `OpeningShape` native_order.hpp:910-914, `Flatten` native_order.hpp:36 | K | — | F:A4 F:A8 O:A2 O:A3 S:O1 | — | | OL3 | Amend / cancel one working order (`submit_or_replace` pine_adapter.cpp:4375 → `host.replace` pine_adapter.cpp:4800) | **yes** — `replace` native_host.hpp:1218-1220, `cancel` native_host.hpp:1249; `ReplacedEvent` native_order.hpp:1017-1031; result and event types `SubmitStatus` native_order.hpp:835-861, `ReplacedEvent` native_order.hpp:1001-1067 | K | — | F:A2 O:A4 S:O3 | id → handle map is host bookkeeping (F) → OL4 | -| OL4 | Cancel all / cancel by label; label → handle book (`cancel(id)` pine_adapter.cpp:14528, `cancel_all` pine_adapter.cpp:14601; `live_by_source_key_` pine_adapter.hpp:2525; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4813) | **partial** — single-handle `cancel` native_host.hpp:1249; no enumeration (`WorkingRequestCore::live()` is consumer-internal native_order.hpp:1703); labels exist but are unindexed (`Request::label` native_order.hpp:485) **Closed:** `cancel_all` native_host.hpp:1267, `cancel_where(comment)` native_host.hpp:1272 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1272 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1263 is the enumeration; the id → handle book is `native_toolkit::OrderBook`, host-side and Pine-free (`tests/test_native_order_ergonomics.cpp`, `tests/test_native_anchored_legs.cpp`). | K+A | L7 | F:A3 F:A2 O:A5 O:A14 S:O3 | **Disagree:** F partial / O no (A5) + partial (A14) / S yes (folded into O3). R5-1: cancel-all / by-label is generic → kernel. TV state inheritance across a same-id re-issue stays A. The adapter's `strategy.cancel` / `strategy.cancel_all` keep their per-handle loops, measured by R5 lane F7 (audit M27): TradingView cancels in the SOURCE book, which holds commands of the same script call that have not reached the kernel yet (and a live `strategy.close` is labelled `__close__` there), so at the same point `cancel_where(id, NativeRequestField::Label)` and `cancel_all()` withdraw 0 — the close flattens at the next open, the reversal lands — where the source cancels withdraw them (`tests/test_adapter_brackets_relower.cpp`, M27 rows); each withdrawn source handle is also retired one by one. | +| OL4 | Cancel all / cancel by label; label → handle book (`cancel(id)` pine_adapter.cpp:14532, `cancel_all` pine_adapter.cpp:14605; `live_by_source_key_` pine_adapter.hpp:2525; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4813) | **partial** — single-handle `cancel` native_host.hpp:1249; no enumeration (`WorkingRequestCore::live()` is consumer-internal native_order.hpp:1703); labels exist but are unindexed (`Request::label` native_order.hpp:485) **Closed:** `cancel_all` native_host.hpp:1267, `cancel_where(comment)` native_host.hpp:1272 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1272 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1263 is the enumeration; the id → handle book is `native_toolkit::OrderBook`, host-side and Pine-free (`tests/test_native_order_ergonomics.cpp`, `tests/test_native_anchored_legs.cpp`). | K+A | L7 | F:A3 F:A2 O:A5 O:A14 S:O3 | **Disagree:** F partial / O no (A5) + partial (A14) / S yes (folded into O3). R5-1: cancel-all / by-label is generic → kernel. TV state inheritance across a same-id re-issue stays A. The adapter's `strategy.cancel` / `strategy.cancel_all` keep their per-handle loops, measured by R5 lane F7 (audit M27): TradingView cancels in the SOURCE book, which holds commands of the same script call that have not reached the kernel yet (and a live `strategy.close` is labelled `__close__` there), so at the same point `cancel_where(id, NativeRequestField::Label)` and `cancel_all()` withdraw 0 — the close flattens at the next open, the reversal lands — where the source cancels withdraw them (`tests/test_adapter_brackets_relower.cpp`, M27 rows); each withdrawn source handle is also retired one by one. | | OL5 | Per-point fill capacity, partial fills, residual working quantity (Pine uses only `ImmediateRemaining` pine_adapter.cpp:5019; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:9174) | **yes** — `Capacity` / `PointBudget` native_order.hpp:379-383, `Remaining` native_order.hpp:557-579, allowances `Allowance` native_order.hpp:662-675 | K | — | O:A16 S:O4 | — | | OL6 | Ownership: legs bound to a parent / opening(s) / cohort; close the lots of a given entry, FIFO or ANY (`owner_for_close` pine_adapter.cpp:6591, `cohort_for` pine_adapter.cpp:3021; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:9269) | **yes** — `Owner` native_order.hpp:464-479, `Authority` native_order.hpp:620-621, `CohortClose` native_order.hpp:617-619, `SelectedExposure` native_order.hpp:625-629, `cohort_open` native_host.hpp:1282, `CancelReason::OwnerGone` native_order.hpp:949 | K | — | F:A6 O:A8 O:A9 S:O5 | FIFO *ordering* is host-computed (O) | | OL7 | OCA cancel / reduce groups (`group_for` pine_adapter.cpp:3456) | **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 | — | @@ -58,8 +58,8 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | 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:11048; lowering `submit_one` pine_adapter.cpp:11926, `BindCohort` pine_adapter.cpp:12389, `submit_leg` pine_adapter.cpp:11682; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1940); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | | OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:11307; `materialize_relative_exits` pine_adapter.cpp:13997, pine_adapter.hpp:2262-2274) | **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:6551. `Trigger` itself is absolute-only native_order.hpp:296 **Closed:** `Request::anchor = FromOwnerFill{offset, ticks, rounding}` (`TriggerAnchor` native_order.hpp:336) defers the level to the owner's fill, the kernel materializes it once at the arm under `NativeAnchorRounding`, and `resolve_anchored_level` native_host.hpp:1012 is where a source language's own level arithmetic lives (`tests/test_native_anchored_legs.cpp`, `tests/test_native_arm_options.cpp`). | K | L7 | F:A11 O:A11 S:O6 | **Disagree:** F partial (toolkit is enough) / O no (kernel `TriggerAnchor`). R5-2: L7 delivers relative anchors; kernel anchor primary, toolkit materialization is the zero-kernel fallback. | -| OL12 | Working-order view for the host and outside observers (`strategy_pending_order_*`; `PendingIntentView` pine_adapter.hpp:1452-1487; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:27085) | **no** — base returns 0 / -1 `observe_last_bar_dual_entry_path_v1` engine_consumer.cpp:124-133; the POD is TV-shaped (`tv_carry_qty` pending_order_mirror.hpp:73, `pine_birth_reach` pending_order_mirror.hpp:135) **Closed for a bare host:** `native_working_requests()` native_host.hpp:1263 returns one owning `NativeWorkingRequest` native_host.hpp:681 per live request — definition, remaining, trigger state — with the C spelling `strategy_native_working_len_v1` / `_get_v1`. The TV-shaped POD stays frozen and stays the source layer's projection (`tests/test_native_limit_fill_through.cpp`, `tests/test_native_arm_options.cpp`, `tests/test_native_c_api.c`). | K+A | L7 (C++), L13 (C) | F:J5 O:J6 S:X4 | **Disagree:** O "partial + TV-shaped" / F, S no. A bare host reads zeros → no. R5-1: neutral view → kernel; POD shape and Pine journal retention stay A (POD frozen, §2.ii j). | -| OL13 | *(quirk)* Same-bar command batching and the seven deferred queues (`pending_bracket_legs_` pine_adapter.hpp:2518-2537); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1366); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:4206, `materialize_snapshot` pine_adapter.cpp:13971), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1360-1387, `DeferredOpenMarketableSell` pine_adapter.hpp:1848-1871, `CloseCallsiteState` pine_adapter.hpp:1986-2000; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:21639, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:21873) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:676-678 | A | — | F:A15 O:A15 S:O9 S:R3 | R5-1 names KI-62. S: the kernel exposes deterministic insertion order only. | +| OL12 | Working-order view for the host and outside observers (`strategy_pending_order_*`; `PendingIntentView` pine_adapter.hpp:1452-1487; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:27089) | **no** — base returns 0 / -1 `observe_last_bar_dual_entry_path_v1` engine_consumer.cpp:124-133; the POD is TV-shaped (`tv_carry_qty` pending_order_mirror.hpp:73, `pine_birth_reach` pending_order_mirror.hpp:135) **Closed for a bare host:** `native_working_requests()` native_host.hpp:1263 returns one owning `NativeWorkingRequest` native_host.hpp:681 per live request — definition, remaining, trigger state — with the C spelling `strategy_native_working_len_v1` / `_get_v1`. The TV-shaped POD stays frozen and stays the source layer's projection (`tests/test_native_limit_fill_through.cpp`, `tests/test_native_arm_options.cpp`, `tests/test_native_c_api.c`). | K+A | L7 (C++), L13 (C) | F:J5 O:J6 S:X4 | **Disagree:** O "partial + TV-shaped" / F, S no. A bare host reads zeros → no. R5-1: neutral view → kernel; POD shape and Pine journal retention stay A (POD frozen, §2.ii j). | +| OL13 | *(quirk)* Same-bar command batching and the seven deferred queues (`pending_bracket_legs_` pine_adapter.hpp:2518-2537); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1366); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:4206, `materialize_snapshot` pine_adapter.cpp:13971), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1360-1387, `DeferredOpenMarketableSell` pine_adapter.hpp:1848-1871, `CloseCallsiteState` pine_adapter.hpp:1986-2000; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:21643, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:21877) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:676-678 | A | — | F:A15 O:A15 S:O9 S:R3 | R5-1 names KI-62. S: the kernel exposes deterministic insertion order only. | | OL14 | *(quirk)* Exit activation / suspension / revival barriers, historical birth reach (`select_exit_activation` exit_activation.cpp:44-90, enforced `leg_activation` pine_adapter.cpp:1263; `select_exit_activation` exit_activation.cpp:20-90, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:1080) | 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:2125); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:2202). What `tests/test_adapter_margin_revival_cancel.cpp` replays on the NYSE:F 15m chart of seven lab tv tapes: badapter-v19dp1-control (no cancel: the margin call closes 928 through X) and v19fix-gapped-stop-control (X's stop at 10.15, gapped by the 05-01 open and parked: the same close), and badapter-v19dp1-cancel, v19fix-cancel-before-pair, v19fix-cancel-at-1000, v19fix-gapped-stop-cancel and v19fix-gapped-stop-cancel-after-pair (a cancel on 05-01 09:30 while X is dormant, on 04-30 15:45 before the reversal pair and on 04-30 10:0 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,16 +68,16 @@ 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:6632; `NativeTrailState` native_host.hpp:669-674, `trail_state` native_host.hpp:1092 | K | — | F:B1 O:B1 S:T1 | — | | TR2 | Trail activation / offset in ticks or points, relative to the entry fill (`trail_points_to_ticks` include/pineforge/compat/pine/trail_ticks.hpp:44 + grid snap exit_lifecycle.cpp:22-27; `trail_activation_level` pine_adapter.hpp:350-352) | **partial** — absolute prices and a raw price distance only **Closed:** `TrailTicks{n}` (`Trail::ticks` native_order.hpp:293) is resolved against `NativeRunSpec::price_tick` at acceptance, so a tick-spelled trail and its price-spelled equal behave identically bar for bar; a relative arm threshold is the `FromOwnerFill` anchor of OL11 (`tests/test_native_order_ergonomics.cpp`, `tests/test_native_anchored_legs.cpp`). | K | L7 | F:B2 O:B2 S:T2 | **Disagree:** S classes all trail operand conversion as TV quirk. R5-1: tick / relative spelling is generic. | -| TR3 | Zero-distance trail, "ride the best, exit on any adverse tick" (adapter substitutes `tick * 0.5` pine_adapter.cpp:15025) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:5367) **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:14942) stays A. | +| TR3 | Zero-distance trail, "ride the best, exit on any adverse tick" (adapter substitutes `tick * 0.5` pine_adapter.cpp:15029) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:5367) **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:14946) stays A. | | TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:4768) | **partial** — the read exists `trail_state` native_host.hpp:1092; `replace` always rebuilds trigger state **Closed:** `replace(handle, request, ReplaceOptions{/*retain_trigger_state=*/true})` carries the predecessor's live trigger state — a tracking trail's best, an already active stop — into the successor, refusing the replacement when the retained best leaves no representable level (`tests/test_native_order_ergonomics.cpp`, `tests/test_native_anchored_legs.cpp`). | K+A | L7 | O:B4 F:B3 | Status is single-source (O). R5-1: retain-trail replace is generic; TV's "retained best across re-issue" convention (`retained_trail_best` pine_adapter.hpp:353-355) is A. | -| TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:11170, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:14942, retained best `retained_trail_best` pine_adapter.hpp:353-355, operand / restart rules `source_trail_points` pine_adapter.cpp:11167, `trail_one_shot` pine_adapter.cpp:12513 | n/a | A | — | F:B3 S:T2 | R5-1 | +| TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:11170, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:14946, retained best `retained_trail_best` pine_adapter.hpp:353-355, operand / restart rules `source_trail_points` pine_adapter.cpp:11167, `trail_one_shot` pine_adapter.cpp:12513 | 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:7206, pine_adapter.cpp:7206) | **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:13737-14965) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:945-949, invoked `resolve_execution_terms` native_execution_consumer.cpp:5852, guarded by `consume_matched_request`'s `try` native_execution_consumer.cpp:5753-5787 | 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:13737-14969) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:945-949, invoked `resolve_execution_terms` native_execution_consumer.cpp:5852, guarded by `consume_matched_request`'s `try` native_execution_consumer.cpp:5753-5787 | 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:3210) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:5881 **Closed:** `Sized{Side, EquityFraction{f}}` (`OrderIntent` native_order.hpp:229) is the sixth intent — the kernel resolves the units from a fraction of marked equity at the sizing point, so a bare host needs no terms override (`tests/test_native_sizing_bases.cpp`). | K+A | L3 | F:C3 O:C2 S:S3 | TV parts stay A: the percentage's money rounded `source_money_round` pine_adapter.cpp:558 (not the equity: R5 lane W5B-ENG-MARGIN-RESIDUAL, the private fixture set `margin_residual` w5b-sz-*), money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:658 (→ SZ10) | | SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:3081) | **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:3229, `typed_entry_units` pine_adapter.cpp:3247; `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:3202; `percent_commission_live_equity` pine_adapter.cpp:5101) | **no** — only a dead primitive, `reserve_percent_commission` (since deleted), with no caller in `src/` **Closed:** `Sized::reserve_percent_fee` divides the sizing cash by `1 + fee_value / 100` under `NativeFeeKind::Percent` and is an exact no-op for every other fee kind; the dead `reserve_percent_commission` primitive is gone (`tests/test_native_sizing_bases.cpp`, `tests/test_native_sizing_price_rule.cpp`). | K | L3 | F:C3 O:C6 S:S3 | **Disagree:** F lists the reserve under quirk yet offers a knob / O kernel, "partial" / S knob. R5-1: fee-net option is generic. TradingView's cash-commission reserve stays adapter policy over the kernel's `CashValue`: a percent-of-equity default quantity under a cash-per-order or cash-per-contract fee takes its percentage of `strategy.equity` (`PineSizingSnapshot::strategy_equity`, the open entries' cash fees charged) and leaves out the fee its own order pays (`default_sizing_cash`, R5 lane PAR-CASHFEE, §3.10; `tests/test_cash_fee_sizing_tapes.cpp` on the tapes in the private fixture set `cash_fee_sizing`) | @@ -86,7 +86,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | SZ8 | Fractional reduce of the position / a cohort (`quantize_close_units` pine_adapter.cpp:5224, `compute_exit_reservation` pine_adapter.cpp:5264) | **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:5264, `reserved_exit_qty` pine_adapter.cpp:11577) | **partial** — `PointBudget` native_order.hpp:379-382 and group `Reduce` native_order.hpp:220 give a generic reservation; no net-of-siblings claim for fraction sizes **Closed:** `ScopeClaim::NetOfSiblings` subtracts the units already claimed by live sibling reduces bound to the same scope, and `ScopeBasis::{AtMatch, AtAcceptance}` chooses when that scope is measured (`tests/test_native_sizing_bases.cpp`). | K+A | L3 | F:A14 S:S5 S:O10 O:A12 | F proposed nothing. R5-4: generic half → L3; the Pine ledger → SZ11, SZ12. | | SZ10 | *(quirk)* Ten-significant-digit money rule (`source_money_round` pine_adapter.cpp:558, `source_money_floor_lot` pine_adapter.cpp:658; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:5243) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1753 | 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:8523), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:10047), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:16236), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:16767, pine_adapter.cpp:16820) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:21687) | 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:3210), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:3229, `typed_entry_units` pine_adapter.cpp:3247), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:26672) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes and 202 famr tapes (two tests that live in the private fixtures); on a seven-digit tie (E 9998.99999996, 10 000 lots at tick(close) 0.9999) the rule admits while the kernel's conversion buys 9999.99999996 lots and binary64 gives E/Q 0.999899999996 (`tests/test_adapter_fill_qty_probe.cpp`, section 2). | +| 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:8523), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:10047), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:16240), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:16771, pine_adapter.cpp:16824) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:21691) | 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:3210), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:3229, `typed_entry_units` pine_adapter.cpp:3247), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:26676) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes and 202 famr tapes (two tests that live in the private fixtures); on a seven-digit tie (E 9998.99999996, 10 000 lots at tick(close) 0.9999) the rule admits while the kernel's conversion buys 9999.99999996 lots and binary64 gives E/Q 0.999899999996 (`tests/test_adapter_fill_qty_probe.cpp`, section 2). | | SZ11 | *(quirk)* Same-bar `strategy.close` callsite batching, two-call provenance, entry-id ledger (`enqueue_pooc_fifo_close` pine_adapter.cpp:9174; ledger `close_batch_bar_` pine_adapter.hpp:2599; the kernel's comment-only residue was deleted by R5 lanes E6 and E10) | n/a | A | — | S:O8 F:A15 O:A15 | **Disagree:** S "generic → kernel" (its P2 `ReplaceByKey` / `Fifo`). R5-4: adapter. | | SZ12 | *(quirk)* POOC reservation-growth market-entry population predicate (`select_reservation_growth_sources` src/compat/pine/reservation_expansion.cpp:9-34, called from the adapter, `select_reservation_growth_sources` pine_adapter.cpp:1324) | n/a | A | — | S:O10 | (single-source) R5-4 confirms S's own split: concept → SZ9, predicate A. | @@ -95,21 +95,21 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | MG1 | Allowed opening directions (`allowed_open_directions` pine_adapter.cpp:2861) | **yes** — `allowed_open_directions` native_run_spec.hpp:665 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:3733 | K | — | F:D4 O:D7 S:M1 | the one risk rule the adapter delegates (O) | -| MG2 | Max absolute position size | **yes** — `max_abs_units` native_run_spec.hpp:663 tests the resulting book native_execution_consumer.cpp:3740 | 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:16514) is TV. | +| MG2 | Max absolute position size | **yes** — `max_abs_units` native_run_spec.hpp:663 tests the resulting book native_execution_consumer.cpp:3740 | 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:16518) is TV. | | MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:664 → `MaxOpenLots` native_execution_consumer.cpp:3745 | 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:7711, cap left unset `max_open_lots` pine_adapter.cpp:2648) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (the private test `test_pyramiding_count_differential`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 13 (it keeps a slot a close or another entry's exit drained; since lane W5B-ENG-MARGIN-RESIDUAL's SB the later market entries of a flat bar are refused at the fill once the book holds the cap). The fifteenth, a resting entry accepted below the cap and filled at it, the kernel's cap refuses; the adapter books it since lane W8A-SIGSTATE-1's R-B, which also stopped the count from counting a resting LIMIT entry (a pending LIMIT entry does not count at a later call, and one the cap admitted fills over it: the private fixture set `pyramiding_open_order`). A recorded divergence, retained pending a re-lowering ruling. **Re-lowering tried by R5 lane H-THIN and not taken:** on the kernel's cap four corpus probes moved away from TradingView, for want of that first-eligible-point rule, so the count stays. The lane fixed the reservation divergence H-MEASURE measured beside it (P10): a default `strategy.exit(from_entry)` under FIFO reserved the whole book (200 in the adapter, 100 on TradingView's tape); it now reserves the entry's own quantity, the oldest lots first (`fifo_named_entry` pine_adapter.cpp:11601). | -| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:17328 and leaves the kernel gate unset pine_adapter.cpp:17347) | **yes** — `initial_margin_fraction` native_run_spec.hpp:666; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:3763 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:3609 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:650, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (the private test `test_adapter_margin_schedule_differential`). **Since R5 lane TAIL-C** an explicit-quantity market buy from flat is costed at its signal close's tick plus the slippage ticks its fill carries, off `process_orders_on_close` (the private test `test_explicit_market_admission_tapes`), and an infinite `strategy.entry` quantity trades the default quantity, as na does (`tests/test_nonfinite_entry_qty_tape.cpp`). | -| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:17581, `margin_pct` pine_adapter.cpp:17083) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:666 **Closed:** `NativeMarginModel::initial_long` / `initial_short` native_run_spec.hpp:242, opt-in and mutually exclusive with `initial_margin_fraction`; `initial_ == 0.0` is the maintenance-only spelling that leaves opening admission to the host at both of its gates (`tests/test_native_margin_model.cpp`). | K | L4 | F:D1 O:D2 | R5-1 | -| MG6 | Maintenance margin + forced liquidation (`submit_margin_call_slice` pine_adapter.cpp:17968, `submit_margin_call_units` pine_adapter.cpp:18001, `schedule_margin_call_path` pine_adapter.cpp:20246) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:666-667; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:3722 **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:17625) | **no** **Closed:** `NativeLiquidationSizing::{RestoreMinimum, ShortfallMultiple, Flatten}` with `shortfall_multiple` and `liquidation_min_units`, capped at the held quantity and floored onto `quantity_grid`; `resolve_margin_call_units` native_host.hpp:995 gives a host the last word, clamped into `(0, held]` (`tests/test_native_margin_model.cpp`, `tests/test_native_margin_hooks.cpp`). | K+A | L4 | O:D4 F:D2 S:M4 | **Disagree:** F generic knob `ShortfallMultiple` / O "×4 is A" / S quirk, yet a 4.0 default inside its generic model. R5-1: policy knob in the kernel, plain default (restore-minimum). **Decided by R5 lane F7 (the D2 question):** the adapter selects NO kernel knob — `project()` leaves `sizing` / `shortfall_multiple` / `liquidation_min_units` unset and its `resolve_margin_call_units` answers every call with TradingView's slice (the restore floored onto the lot grid BEFORE the ×4, floored again, the one-contract band, the dust gate). Measured: on a one-unit grid, 20 long at 100 on 1000 at 50 % reaching 93.9, TradingView books 4 lots and the kernel's own `ShortfallMultiple 4` books 5 (`tests/test_native_margin_hooks_twin.cpp` MG-F3, an executed run, no longer a comment); without a grid the two agree bit for bit (MG-F/H, 4.2105263157894735), but declaring the knob for those runs alone folds `NativeMarginModel::sizing` into every margin run's identity — an epoch decision F7 does not take. Since R5 lane H-THIN the pre-open admission slice (`schedule_preopen_margin_slice`) sizes through the same `source_margin_units`, where it kept its own copy of the ×4 without the band or the dust gates (AUDIT4 §6 G2-12; the private test `test_adapter_margin_schedule_differential` M10: band and fudge now 1 and 1, the kernel's and `ab9714be`'s). A four-times call on a whole-unit grid whose book's oldest lot is one unit is followed by one more unit at the next check point (`whole_unit_follow_up_due` pine_adapter.cpp:19153; R5 lane W5B-ENG-MARGIN-RESIDUAL, the private fixture set `margin_residual` w5b-es-*, w5b-sb-es-*, w5b-f-q1x12). With slippage the one-contract band takes its unit only where that unit restores the book at its own fill: the position's requirement at the liquidation's slipped print, less the equity at the mark, under one unit's margin (`source_margin_units` pine_adapter.cpp:17625; lane INT28-FIX rule OU, the private fixture set `slipped_short`). A one-unit call that leaves the book short at its own fill, by more than one unit's margin and two slippage steps with the book re-marked there, is followed by one more unit at the bar's next path point, where that point's own check calls nothing and its print is inside the call's fill (lane INT28-FIX rule OF). Under a percent commission a call of any size is followed so, on a fractional lot grid too: while the book the call left, re-marked at its fill, is short by more than two slippage steps for each unit called and the exit fees of those units and of one more, the next path point inside that fill takes four times the shortfall's lot floor over the fill, or one unit where that floors to none (`margin_follow_up_units` pine_adapter.cpp:19195, `follow_margin_call` pine_adapter.cpp:19244; R5 lane TAIL-I, the private fixture set `margin_open_print`). | +| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:17332 and leaves the kernel gate unset pine_adapter.cpp:17351) | **yes** — `initial_margin_fraction` native_run_spec.hpp:666; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:3763 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:3609 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:650, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (the private test `test_adapter_margin_schedule_differential`). **Since R5 lane TAIL-C** an explicit-quantity market buy from flat is costed at its signal close's tick plus the slippage ticks its fill carries, off `process_orders_on_close` (the private test `test_explicit_market_admission_tapes`), and an infinite `strategy.entry` quantity trades the default quantity, as na does (`tests/test_nonfinite_entry_qty_tape.cpp`). | +| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:17585, `margin_pct` pine_adapter.cpp:17087) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:666 **Closed:** `NativeMarginModel::initial_long` / `initial_short` native_run_spec.hpp:242, opt-in and mutually exclusive with `initial_margin_fraction`; `initial_ == 0.0` is the maintenance-only spelling that leaves opening admission to the host at both of its gates (`tests/test_native_margin_model.cpp`). | K | L4 | F:D1 O:D2 | R5-1 | +| MG6 | Maintenance margin + forced liquidation (`submit_margin_call_slice` pine_adapter.cpp:17972, `submit_margin_call_units` pine_adapter.cpp:18005, `schedule_margin_call_path` pine_adapter.cpp:20250) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:666-667; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:3722 **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:17629) | **no** **Closed:** `NativeLiquidationSizing::{RestoreMinimum, ShortfallMultiple, Flatten}` with `shortfall_multiple` and `liquidation_min_units`, capped at the held quantity and floored onto `quantity_grid`; `resolve_margin_call_units` native_host.hpp:995 gives a host the last word, clamped into `(0, held]` (`tests/test_native_margin_model.cpp`, `tests/test_native_margin_hooks.cpp`). | K+A | L4 | O:D4 F:D2 S:M4 | **Disagree:** F generic knob `ShortfallMultiple` / O "×4 is A" / S quirk, yet a 4.0 default inside its generic model. R5-1: policy knob in the kernel, plain default (restore-minimum). **Decided by R5 lane F7 (the D2 question):** the adapter selects NO kernel knob — `project()` leaves `sizing` / `shortfall_multiple` / `liquidation_min_units` unset and its `resolve_margin_call_units` answers every call with TradingView's slice (the restore floored onto the lot grid BEFORE the ×4, floored again, the one-contract band, the dust gate). Measured: on a one-unit grid, 20 long at 100 on 1000 at 50 % reaching 93.9, TradingView books 4 lots and the kernel's own `ShortfallMultiple 4` books 5 (`tests/test_native_margin_hooks_twin.cpp` MG-F3, an executed run, no longer a comment); without a grid the two agree bit for bit (MG-F/H, 4.2105263157894735), but declaring the knob for those runs alone folds `NativeMarginModel::sizing` into every margin run's identity — an epoch decision F7 does not take. Since R5 lane H-THIN the pre-open admission slice (`schedule_preopen_margin_slice`) sizes through the same `source_margin_units`, where it kept its own copy of the ×4 without the band or the dust gates (AUDIT4 §6 G2-12; the private test `test_adapter_margin_schedule_differential` M10: band and fudge now 1 and 1, the kernel's and `ab9714be`'s). A four-times call on a whole-unit grid whose book's oldest lot is one unit is followed by one more unit at the next check point (`whole_unit_follow_up_due` pine_adapter.cpp:19157; R5 lane W5B-ENG-MARGIN-RESIDUAL, the private fixture set `margin_residual` w5b-es-*, w5b-sb-es-*, w5b-f-q1x12). With slippage the one-contract band takes its unit only where that unit restores the book at its own fill: the position's requirement at the liquidation's slipped print, less the equity at the mark, under one unit's margin (`source_margin_units` pine_adapter.cpp:17629; lane INT28-FIX rule OU, the private fixture set `slipped_short`). A one-unit call that leaves the book short at its own fill, by more than one unit's margin and two slippage steps with the book re-marked there, is followed by one more unit at the bar's next path point, where that point's own check calls nothing and its print is inside the call's fill (lane INT28-FIX rule OF). Under a percent commission a call of any size is followed so, on a fractional lot grid too: while the book the call left, re-marked at its fill, is short by more than two slippage steps for each unit called and the exit fees of those units and of one more, the next path point inside that fill takes four times the shortfall's lot floor over the fill, or one unit where that floors to none (`margin_follow_up_units` pine_adapter.cpp:19199, `follow_margin_call` pine_adapter.cpp:19248; R5 lane TAIL-I, the private fixture set `margin_open_print`). | | MG8 | Liquidation price query (`margin_liquidation_price` pine_strategy_host.cpp:168-184) | **no** **Closed:** `native_liquidation_price()` native_host.hpp:1320 answers the exact solved level, `nullopt` where none exists; `NativeLiquidationLevelBase` chooses whether the solve answers for the open entries' fees (`tests/test_native_margin_model.cpp`, `tests/test_native_open_lots.cpp`). | K+A | L4 | F:D3 S:R5 | **Disagree:** F generic / S adapter-only. R5-1: liquidation price is generic; the Pine spelling stays A. **Lowered by R5 lane H-THIN:** the Pine host's second solve (`compute_liquidation_price`) is gone; `strategy.margin_liquidation_price` reads `native_liquidation_price()`, which the adapter configures with `NativeLiquidationLevelBase::RealizedOnly` (the Pine formula's closed-money solve, bit for bit at every bar of the lane's 1,021 seeded margin-call runs), and keeps only TradingView's tick spelling: floored for a long, ceiled for a short. A run whose margin calls are switched off declares no margin model, so it reads na. | | MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:5691, `apply_fx_opening_margin_slice` pine_adapter.cpp:5703) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1202 but no margin re-check. **Closed by audit lane N6:** `NativeMarginCheckKind::FxRoll` — a step of the declared curve is a check point of its own, measured at the unchanged price before the first point the new rate converts (`tests/test_native_margin_fx_roll.cpp`). The adapter refuses the point by kind, for a measured reason since R5 lane F7: TradingView's rollover is a broker-open checkpoint at the open price, now at any positive margin (`apply_fx_open_margin_slice`, on the one TradingView money / units rule every other checkpoint uses), while the roll point measures at the remaining path's adverse mark and rests the slice there — a carried 1x long on a step bar O 100 L 97 books 0.4116 @ 97 on the adapter's run shape and 0.4036 @ 99, measured a bar early, on a strict calendar, against TradingView's 0.3996 @ 100 (`tests/test_native_margin_hooks_twin.cpp` MG-FX). Both N6 follow-ups are closed (§4.4). **Completed by R5 follow-up lane E3:** the conversion INSTANT is now generic too — every check kind converts at its own cursor's rate (`account_currency_fx_at(cursor.point.effective_time_ms)`) for the requirement, the equity and the level, in batch and stream, so a step landing between a fill's cursor and the clock the engine presents no longer breaches at the fill (`tests/test_native_margin_fx_clock.cpp`; N6's local clock swap inside `fx_roll_margin_check` is removed as redundant). **Extended by R5 follow-up lane E21** to the opening gate's placement site (MG4), and the rate-explicit equity E3 restated beside the engine's accessor is now that accessor's one implementation, `BacktestEngine::marked_equity_at` engine.hpp:730 | K | L4 → N6 → E3 → E21 | O:D8 | (single-source) | -| MG10 | Max drawdown halt (`update_risk_state` pine_adapter.cpp:17505) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1301 **Closed:** `NativeRiskLimits::max_drawdown` native_run_spec.hpp:311, a `NativeLossLimit{value, percent}` measured against the running peak equity at three points of every script bar, blocking openings for the rest of the run (`tests/test_native_risk_limits.cpp`). | K | L9 | F:D6 O:E1 S:M6 | R5-9 | -| MG11 | Max consecutive losing days (`SourceDayLedger` pine_adapter.hpp:1422-1430; gate `max_cons_loss_days` pine_adapter.cpp:16508) | **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:17538, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:20880) | **no** — observation only `intraday_loss_limit` include/pineforge/source/market_admission.hpp:51 **Closed:** `NativeRiskLimits::max_intraday_loss` with `NativeRiskAction::FlattenAndBlock`, under which the kernel issues one `Flatten` of its own at the breach point (`RequestOrigin::KernelRisk`) and latches the block before that flatten settles (`tests/test_native_risk_limits.cpp`). | K | L9 | F:D7 O:E3 S:M6 | R5-9 | -| MG13 | Max filled orders per day (`IntradayCap` intraday_cap.hpp:84-263, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:20923) | **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:17402) | **no** — the calendar exists `session_day_ordinal` native_calendar.hpp:416-419, there is no risk-day ledger **Closed:** `NativeRiskDay::{SessionDay, CalendarDayInTimezone}` keys the risk day, and `native_risk_state()` native_host.hpp:1329 reads the whole ledger back — block and reason, day, fills counted, loss-day streak, peak equity, day-opening equity (`tests/test_native_risk_limits.cpp`). | K+A | L9 | O:E5 S:M7 | The chart-day shortcut stays A; the kernel keys on session / calendar day | -| MG15 | *(quirk)* TV margin call: money rounding `source_margin_money` pine_adapter.cpp:17570, 4× shortfall `4.0 * minimum` pine_adapter.cpp:17659, POOC chronology exception `resting_order` pine_adapter.cpp:20266 (the commissioned carried short; the competing-entry and rounded-money exceptions retired by R5 lane W5B-ENG-MARGIN-RESIDUAL, the private fixture set `margin_residual` w5b-pc-*), 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:18563, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:18563) | n/a | A | — | F:D2 F:G4 O:D4 S:M4 | R5-1. **Measured (R5 lane H-MEASURE)** against the kernel's own points on the same bars and orders, the kernel's hooks answering TradingView's money and slice (the private test `test_adapter_margin_schedule_differential`): a gap-open breach executes at the open, sized on the open's money, where the kernel's mark check rests at the path's adverse extreme and `CalculationOnly` executes at the calculation (M8); the pre-open slice parts from the kernel's `AfterApplied` point, and from `ab9714be`, on the one-contract band and the restore's `+1e-6` floor (M10; the frozen units it also parted on are TradingView's book since R5 lane PAR-MARGIN froze a default-percent stop above 100 % as eight `lab tv` tapes, the private tapes `pm-m10-*` of `margin_entry_bar`, show TradingView does); the entry bar of a leveraged opening, where four `lab tv` tapes (the private fixture set `margin_entry_bar`) and the kernel's `AfterApplied` point book the margin call at that bar's low, is admitted by `on_applied` since R5 lane PAR-MARGIN on the run shapes `ab9714be` checked it on (the pre-open slice's own opening keeps that slice's verdict), and the adapter books the tapes' rows (M7; H-MEASURE had recorded it as a divergence); R5 lane PAR-MARGIN-2 extended it to limit openings, `process_orders_on_close`, `calc_on_order_fills` and adds to a carried book, and made the kernel's post-fill point reach the low a limit was filled on the way down to, or a short's high on its way up (twenty-eight `lab tv` tapes, the private tapes `pm2-*` of `margin_entry_bar`). Under the magnifier the kernel's `IntrabarSample` point (R5 lane PAR-MARGIN) is admitted on a leveraged long, plain, under `process_orders_on_close` or `calc_on_order_fills`, and on a short outside `process_orders_on_close` (PAR-MARGIN-2), where TradingView books its call at the first lower-timeframe low that crosses -- at its own 2-minute intrabars on a 15-minute chart, which a model reproduces on all 20 magnified tapes (the private fixture set `intrabar_margin`); six tapes part on the one-minute samples the host feeds, a recorded divergence of the magnifier's sampling. A margin call books the nearest tick of its print, as TradingView's market fills do (12.105 books 12.11; the private fixture set `half_tick_rounding`, PAR-MARGIN-2). | -| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:34 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:48); `admission_allocation` pine_adapter.cpp:4558, `record_market_review` pine_adapter.cpp:21179) | n/a | A | — | S:R2 | (single-source) | +| MG10 | Max drawdown halt (`update_risk_state` pine_adapter.cpp:17509) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1301 **Closed:** `NativeRiskLimits::max_drawdown` native_run_spec.hpp:311, a `NativeLossLimit{value, percent}` measured against the running peak equity at three points of every script bar, blocking openings for the rest of the run (`tests/test_native_risk_limits.cpp`). | K | L9 | F:D6 O:E1 S:M6 | R5-9 | +| MG11 | Max consecutive losing days (`SourceDayLedger` pine_adapter.hpp:1422-1430; gate `max_cons_loss_days` pine_adapter.cpp:16512) | **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:17542, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:20884) | **no** — observation only `intraday_loss_limit` include/pineforge/source/market_admission.hpp:51 **Closed:** `NativeRiskLimits::max_intraday_loss` with `NativeRiskAction::FlattenAndBlock`, under which the kernel issues one `Flatten` of its own at the breach point (`RequestOrigin::KernelRisk`) and latches the block before that flatten settles (`tests/test_native_risk_limits.cpp`). | K | L9 | F:D7 O:E3 S:M6 | R5-9 | +| MG13 | Max filled orders per day (`IntradayCap` intraday_cap.hpp:84-263, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:20927) | **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:17406) | **no** — the calendar exists `session_day_ordinal` native_calendar.hpp:416-419, there is no risk-day ledger **Closed:** `NativeRiskDay::{SessionDay, CalendarDayInTimezone}` keys the risk day, and `native_risk_state()` native_host.hpp:1329 reads the whole ledger back — block and reason, day, fills counted, loss-day streak, peak equity, day-opening equity (`tests/test_native_risk_limits.cpp`). | K+A | L9 | O:E5 S:M7 | The chart-day shortcut stays A; the kernel keys on session / calendar day | +| MG15 | *(quirk)* TV margin call: money rounding `source_margin_money` pine_adapter.cpp:17574, 4× shortfall `4.0 * minimum` pine_adapter.cpp:17663, POOC chronology exception `resting_order` pine_adapter.cpp:20270 (the commissioned carried short; the competing-entry and rounded-money exceptions retired by R5 lane W5B-ENG-MARGIN-RESIDUAL, the private fixture set `margin_residual` w5b-pc-*), 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:18567, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:18567) | n/a | A | — | F:D2 F:G4 O:D4 S:M4 | R5-1. **Measured (R5 lane H-MEASURE)** against the kernel's own points on the same bars and orders, the kernel's hooks answering TradingView's money and slice (the private test `test_adapter_margin_schedule_differential`): a gap-open breach executes at the open, sized on the open's money, where the kernel's mark check rests at the path's adverse extreme and `CalculationOnly` executes at the calculation (M8); the pre-open slice parts from the kernel's `AfterApplied` point, and from `ab9714be`, on the one-contract band and the restore's `+1e-6` floor (M10; the frozen units it also parted on are TradingView's book since R5 lane PAR-MARGIN froze a default-percent stop above 100 % as eight `lab tv` tapes, the private tapes `pm-m10-*` of `margin_entry_bar`, show TradingView does); the entry bar of a leveraged opening, where four `lab tv` tapes (the private fixture set `margin_entry_bar`) and the kernel's `AfterApplied` point book the margin call at that bar's low, is admitted by `on_applied` since R5 lane PAR-MARGIN on the run shapes `ab9714be` checked it on (the pre-open slice's own opening keeps that slice's verdict), and the adapter books the tapes' rows (M7; H-MEASURE had recorded it as a divergence); R5 lane PAR-MARGIN-2 extended it to limit openings, `process_orders_on_close`, `calc_on_order_fills` and adds to a carried book, and made the kernel's post-fill point reach the low a limit was filled on the way down to, or a short's high on its way up (twenty-eight `lab tv` tapes, the private tapes `pm2-*` of `margin_entry_bar`). Under the magnifier the kernel's `IntrabarSample` point (R5 lane PAR-MARGIN) is admitted on a leveraged long, plain, under `process_orders_on_close` or `calc_on_order_fills`, and on a short outside `process_orders_on_close` (PAR-MARGIN-2), where TradingView books its call at the first lower-timeframe low that crosses -- at its own 2-minute intrabars on a 15-minute chart, which a model reproduces on all 20 magnified tapes (the private fixture set `intrabar_margin`); six tapes part on the one-minute samples the host feeds, a recorded divergence of the magnifier's sampling. A margin call books the nearest tick of its print, as TradingView's market fills do (12.105 books 12.11; the private fixture set `half_tick_rounding`, PAR-MARGIN-2). | +| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:34 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:48); `admission_allocation` pine_adapter.cpp:4558, `record_market_review` pine_adapter.cpp:21183) | n/a | A | — | S:R2 | (single-source) | ### 1.5 Calculation timing @@ -117,7 +117,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa |---|---|---|---|---|---|---| | CT1 | Calculate at script-bar close | **yes** — `on_native_bar` native_host.hpp:893, dispatched by `invoke_callback` native_execution_consumer.cpp:8193 (call site `on_native_recalculate` native_execution_consumer.cpp:8221) | K | — | F:E1 O:F1 | — | | CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2818) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:8637 | K | — | F:E2 O:F6 S:C1 | — | -| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:22274) | **yes** — `on_native_bar_open` native_host.hpp:889 runs before the open match native_execution_consumer.cpp:8067; `execute_current` legal native_host.hpp:1104 | K | — | F:E3 O:F2 | hazard → CT4 | +| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:22278) | **yes** — `on_native_bar_open` native_host.hpp:889 runs before the open match native_execution_consumer.cpp:8067; `execute_current` legal native_host.hpp:1104 | K | — | F:E3 O:F2 | hazard → CT4 | | CT4 | Partial-bar view / open-bar lookahead guard | **no** — the open hook receives the *complete* script bar: `invoke_bar_open_callback(engine, bar, point)` native_execution_consumer.cpp:8010 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:8061 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1073 is the lookahead-free bar so far, through the last path point consumed (a fill inside a segment reads it at the segment's origin), and `NativeOpenBarView::OpenOnly` native_run_spec.hpp:109 masks `on_native_bar_open`'s bar to `H = L = C = open` as presentation only — the complete bar is restored before the open match (`tests/test_native_calc_timing.cpp`). | K | L5 | F:E3 | (single-source) The adapter needs the full bar, so the guard is opt-in. | | CT5 | Raw input observation before aggregation | **yes** — `on_native_input` native_host.hpp:875, `NativeInputContext` native_host.hpp:773-778 | K | — | O:F3 | (single-source row) | | CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:745, `coof_recalculation_due` pine_scheduler_native.cpp:711, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:866; `begin_coof_recalc` pine_adapter.cpp:6688, `flush_coof_tail` pine_adapter.cpp:7112, `end_coof_recalc` pine_adapter.cpp:6745) | **partial** — `on_native_applied` native_host.hpp:938 fires mid-path after each fill native_execution_consumer.cpp:7914, native_execution_consumer.cpp:7914; commands are legal there `commands_allowed` native_execution_consumer.cpp:1844; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:6551, `remaining_path_delivery` native_order.cpp:2082-2090. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | @@ -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:6623, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:7213; `Limit::fill_through` native_order.hpp:239-244 enforced where the matched price is limit-protected (`protect_limit` native_execution_consumer.cpp:7209) and re-checked after host terms (`InvalidTerms` native_execution_consumer.cpp:5878) | K | — | F:G3 O:H1 O:H2 O:H3 | — | -| FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:652 applied once native_execution_consumer.cpp:5514, clamped by `protect_limit` native_execution_consumer.cpp:7209 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:15192) is A | +| FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:652 applied once native_execution_consumer.cpp:5514, clamped by `protect_limit` native_execution_consumer.cpp:7209 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:15196) is A | | FP3 | Commission: percent / cash per unit / cash per execution (`fee_kind_for` pine_adapter.cpp:669) | **yes** — `NativeFeeKind` native_run_spec.hpp:19-23; `quote_execution_commissions` engine_execution.cpp:1690 | K | — | F:G2 O:H8 S:F1 | — | -| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:274, `source_bar_fill` pine_adapter.cpp:15192, `directional_tick` pine_adapter.cpp:327) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:7191; `bar_fill_price` engine.hpp:772 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:7364; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_sizing_price_rule.cpp` quantizes the fill basis under `HalfUp`; `tests/test_native_acid_composite.cpp` books the `Directional` fills on the 0.25 ladder; the private test `test_native_price_grid` re-ran the pair under the overlay). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | +| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:274, `source_bar_fill` pine_adapter.cpp:15196, `directional_tick` pine_adapter.cpp:327) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:7191; `bar_fill_price` engine.hpp:772 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:7364; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_sizing_price_rule.cpp` quantizes the fill basis under `HalfUp`; `tests/test_native_acid_composite.cpp` books the `Directional` fills on the 0.25 ladder; the private test `test_native_price_grid` re-ran the pair under the overlay). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | | FP5 | Trigger tested against the tick-quantized bar | **no** — the kernel tests raw prices against raw levels **Closed:** `NativePriceGrid::QuantizeFillsAndTriggers` additionally tests and re-validates every activation against the tick-quantized path, one rule for every trigger kind (`tests/test_native_trail_best_seed.cpp` puts an off-ladder trail seed on the ladder; `tests/test_native_acid_composite.cpp` runs a trail's best and its exit level on it; the private test `test_native_price_grid` re-ran the pair under the overlay). | K+A | L8 | O:H5 F:G4 S:F3 | R5-3: the optional quantized trigger test is K; TV's exact half-tick rule (`source_trigger_threshold` pine_adapter.cpp:384) is A on top. | | PG | The Pine adapter on the kernel price grid: raw levels submitted, `project()` declaring `QuantizeFillsAndTriggers` + `HalfUp`, `source_trigger_threshold` deleted (lane R7; re-tested after L8b by gap lane N13) | **measured-infeasible for the Pine adapter** — B1 (the sub-tick cursor-print abort) is closed by L8b's `ActivationGrid`: 0 aborted runs under the trial (R7: 9 zero-offset-trail tapes + 6 POOC panels; now `test_zero_offset_trail_rides_l4c` 449/450, `test_pooc_short_close_tick_l4d` 183/183). B2 (TradingView quantizes per order kind, the grid is one run-wide rule) moved 30 pinned checks in 4 units when N13 last ran the trial, before lanes E14 and E16, with no adapter-side remedy: `test_coof_market_limit_recross_l4c` 24, `test_stop_tick_rounding_l4d` 3, `test_adapter_grid_relower` 2, `test_zero_offset_trail_rides_l4c` 1; 12 more checks in 3 units have an adapter-side cause (`test_trail_fill_snap_l4c` 2, `test_native_margin_hooks` 2, `test_adapter_brackets_relower` 8 book digests). Corpus: 5/312 probes differ, in the engine-only entry-incarnation column only (every probe runs a 0.01 tick on an on-grid feed, so it cannot arbitrate) | A | waived (R7, N13) | — | **Waiver.** No class of triggers is byte-identical on its own: the grid is a run-wide switch and a per-kind mask would spell TradingView's inconsistency into the kernel (not generic, R5-3). The adapter stays on `NativePriceGrid::None` and keeps `source_trigger_threshold` and its call sites; native-engine.md "The Pine adapter's grid re-lowering is waived". **Native-only by ruling (audit lane P6): §3.6.2**, with `examples/native/native_price_grid_strategy.cpp` and its C twin as the feature's hosts. | | FP6 | Account-currency FX | **partial** — scalar `account_fx` + immutable `NativeFxCurve` native_host.hpp:1202 in batch; the stream then refused a non-empty curve (today only tick-driven input is refused under one, `refuse_fx_curve_tick_input` native_execution_consumer.cpp:9997). **Closed by audit lane N6:** the declared curve is the run's immutable FX epoch and a confirmed-bar stream runs under it exactly as a batch does (stream twin in `tests/test_native_margin_fx_roll.cpp`); no realtime rate ingestion and no tick-driven input under a curve are stated as permanent limits with their reasons (native-engine.md, "Stream FX"). **Re-examined by R5 follow-up lane E3 and KEPT:** the tick refusal was never only about the margin check — it guards the SCRIPT-VISIBLE conversions (`open_trade_profit()`, `marked_equity()`, the equity curve), which read `active_account_currency_fx()`, i.e. the presented clock the observation hook has not yet moved (re-measured: a step at T+90s left the T+100s and T+110s prints converting at the pre-step rate). E3's cursor rule reaches the margin model only, so the refusal stands and is pinned in `tests/test_native_margin_fx_clock.cpp` | K | unassigned → N6 (E3: refusal kept) | F:D9 S:F4 | **Disagree:** F out of scope ("O7 clock") / S its P10, a stream-safe FX epoch. No R5 lane → §4 U4 (answered by N6: S's "same curve in batch and stream", without a new clock or callback). | -| FP7 | *(quirk)* TV tick conventions: half-tick threshold `source_bar_fill_tick` pine_adapter.cpp:295 (used `source_trigger_threshold` pine_adapter.cpp:384, `exit_limit_trigger` pine_adapter.cpp:14376), `source_bar_fill_tick` pine_adapter.cpp:295, raw-vs-booked spelling `source_same_point` pine_adapter.cpp:313, margin-call fill pricing `source_margin_fill_price` pine_adapter.cpp:17794 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:14868) | A | — | F:G4 O:H5 S:F3 | R5-1, R5-3 | +| FP7 | *(quirk)* TV tick conventions: half-tick threshold `source_bar_fill_tick` pine_adapter.cpp:295 (used `source_trigger_threshold` pine_adapter.cpp:384, `exit_limit_trigger` pine_adapter.cpp:14376), `source_bar_fill_tick` pine_adapter.cpp:295, raw-vs-booked spelling `source_same_point` pine_adapter.cpp:313, margin-call fill pricing `source_margin_fill_price` pine_adapter.cpp:17798 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:14872) | A | — | F:G4 O:H5 S:F3 | R5-1, R5-3 | ### 1.8 Excursion, metrics, report, hash @@ -210,7 +210,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | f | Dead legacy stream path | **Done.** `stream_feed_input_bar` and `dispatch_source_stream_script_bar` are both gone — neither name is in `src/` or `include/` — and the virtual left with the epoch that carried it. `stream_state_hash` engine.hpp:1791 stays, and the declaration/definition pairing is still enforced by `scripts/test_native_source_guard.py` | NEUTRAL (removing the virtual changes the vtable → shared bump) | Delete both | O; F asked (its Q6) | | g | Legacy path resolver | **Done, and then deleted.** `src/engine_path_resolve.cpp` is 118 lines with five functions: `bar_path_uses_high_first`, both `first_touch_position` overloads, `fill_bar_path_points_ordered`, and `BacktestEngine::declare_opened_lot_entry_bar_mask`. Ten test files still mention `resolve_exit_path_fill` in historical comments; the resolver body and test-only oracle are gone. | NEUTRAL (pure move; verify with the link gate) | Keep the generic path helpers in the kernel TU; the old source resolver is not a live counterpart | F (single-source) | | | h | `pine_*`-named generic API | `timeframe_time` session_time.hpp:35-211 (live, e.g. `session_in_market`, which generated code reaches through `pine_session_ismarket` session_time.hpp:263), `str_utils.hpp`, `deterministic_random` math.hpp:9, `tz_util` timezone.hpp:8-30; the guard bans `pine_*` only inside `IDENTIFIER_ROOTS` (test_native_source_guard.py:34, test_native_source_guard.py:34-42) | NEUTRAL, M | Neutral aliases; `pine_*` kept as deprecated inlines for codegen. Large blast radius in generated code. **Status (L11 + N14): done** — session_time / str_utils / math / timezone in L11; N14 applied the same treatment to the two public matrix types: `PineMatrix` → `NumericMatrix`, `PineGenericMatrix` → `GenericMatrix`, old spellings kept as exact aliases (no version script pins either name) | O (single-source) | -| i | `pf_pending_order_v1_t` Pine-named fields | `tv_carry_qty` pending_order_mirror.hpp:73, `pine_birth_reach` pending_order_mirror.hpp:135, `pine_exit_activation_owner_cycle_at_birth` pending_order_mirror.hpp:147-156, `pine_frozen_market_instruction_kind` pending_order_mirror.hpp:164-169; enumerated by `PF_PO_FIELD` pending_order_mirror.cpp:144-178; written only by `pine_birth_reach` pine_adapter.cpp:27085 | 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:27089 | no change | Freeze (ABI). The neutral view arrives beside it (L7 / L13) | O (single-source) | | j | Pine-shaped lot flags | **Mostly done.** `market_pyramid_add` moved to the adapter side table the row asked for — `market_pyramid_adds_` src/source/pine_state_hash.cpp:692, whose own comment records that it was `PyramidEntry::market_pyramid_add` — and `ordinary_market_open`, `pooc_terminal_market_entry`, `ordinary_stop_open`, `bracket_slot_shadowed` and `entry_path_position` are all deleted. What is left on the lot is the intrabar-fill excursion mask, `skip_entry_bar_high` / `skip_entry_bar_low` engine.hpp:154, hashed (`skip_entry_bar_high` engine_state_hash.cpp:74) and, since R5 lane E6, written by no host at all: a host that owns lot excursions declares where its fill sat (`declare_opened_lot_entry_bar_mask` engine_path_resolve.cpp:95; the Pine host did until R5 lane H-THIN left its excursions to the kernel's sampler) and the kernel derives the pair | HASH-VISIBLE | Move the live flag to an adapter side table keyed by `entry_incarnation` (the adapter already keeps `PlacementTable` pine_adapter.hpp:355-675) and fold it through `hash_source_extension`; delete the dead ones; update the hash-coverage waivers. Whether a kernel TU beyond the hash reads them is unverified (O) | F O S | | k | `margin_call_enabled_` | **Done.** The generic kernel model is enabled by `NativeRunSpec::margin` native_run_spec.hpp:672. TradingView-specific toggling remains `source::PineStrategyHost::set_margin_call_enabled` pine_strategy_host.hpp:511; the frozen C symbol remains source-owned. | HASH-VISIBLE (R5-8) | Keep the source setter in the adapter; the kernel toggle is the presence of `spec.margin` | F O | | l | Kernel decodes adapter strings | **Done.** `closed_trade_close_cause` engine_trade_accessors.cpp:158 branches on no string at all: it reads `Trade::open_at_end`, then the typed `execution::CloseCause` the closer recorded on the row, then `exit_from_bracket`. A kernel-originated liquidation or risk flatten states its own cause through the settling fill, so a bare host reads `3` / `4` where the retired string derivation answered `1`. The public C contract it implements is `strategy_closed_trade_close_cause` pineforge.h:1309 | HASH-VISIBLE (adds durable state) | Kernel `enum class CloseCause` recorded on the `Trade` at settlement (the kernel sets `MarginCall` when L4 liquidates; a source virtual covers adapter causes). C numbers unchanged. After L4 | O (single-source) | @@ -524,11 +524,11 @@ line numbers are re-anchored to this tree (the measurement ran on 683a82f); kern | Pine rule | adapter mechanism | kernel counterpart | class | measured divergence (test scenario) | ruling | |---|---|---|---|---|---| | `strategy.risk.allow_entry_in` | `risk_.direction`; `project()` already declares `spec.allowed_open_directions` (pine_adapter.cpp:2861); 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:16514; 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:17505, `update_risk_state` pine_adapter.cpp:17505): 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:16507), 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:1422-1430): +1 per losing TRADE on a new chart day, reset to 0 by any winning trade at its fill (`consecutive_loss_days` pine_adapter.cpp:25745); 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:22514); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:20880), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:20912, `submit_intraday_loss_close` pine_adapter.cpp:20880), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:24702); 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:17500, `:17500`, `:17500`, `:17500`, `:17500`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:14601) | `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:17496); 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:17402, `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:0 UTC) is a new chart day for the adapter and the same UTC day for the kernel. Corpus, kernel seeded with the declared cap: `cap-risk-gates-allow-max-intraday-01` identical 1464/1464 (the third fill of each day flattens the book, so block and latch coincide); `cap-max-intraday-filled-orders-isolate-01` 3840 of 3916 rows differ from row 77 (2025-04-08); `cap-gatekeeper-intraday-risk-01` 312 of 604 differ from row 293 (2025-10-25); `composite-bracket-cap-range-pending-stop-01` 2370 of 2384 differ from row 17 (2025-04-02 02:45), two extra rows | retained: the count alone is expressible, but the kernel's block would refuse fills the transfer admits and the close price is TV's; a report-only action would give the adapter a count it cannot use (charged slots ≠ applied fills) | +| `strategy.risk.max_position_size` | precommit refuses a flat/same-side ENTRY at its fill when the LIVE book already holds ≥ the limit (`max_position_size` pine_adapter.cpp:16518; 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:17509, `update_risk_state` pine_adapter.cpp:17509): 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:16511), 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:1422-1430): +1 per losing TRADE on a new chart day, reset to 0 by any winning trade at its fill (`consecutive_loss_days` pine_adapter.cpp:25749); 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:22518); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:20884), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:20916, `submit_intraday_loss_close` pine_adapter.cpp:20884), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:24706); 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:17504, `:17504`, `:17504`, `:17504`, `:17504`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:14605) | `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:17500); 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:17406, `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:0 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, @@ -1136,7 +1136,7 @@ moving a single value, but not from inside lane F9's files: (`has_full_entry_bracket` pine_adapter.cpp:11437). Full-table scans rewrite rows as well (`suspend_brackets_for_reversal` pine_adapter.cpp:1967, `revive_brackets_after_margin` pine_adapter.cpp:2202, - `preserved_by_close_all` pine_adapter.cpp:14668), and + `preserved_by_close_all` pine_adapter.cpp:14672), 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. @@ -1600,10 +1600,10 @@ the rule gives 9.9 + 9.89). | # | Risk | Why it bites | Containment | Src | |---|---|---|---|---| | E1 | **Spec hashing.** `hash_spec` folded every field unconditionally (native_execution_consumer.cpp:235-257); every block added since folds only when set | Any new field, even defaulted, changes every continuation / broker-state hash, adapter runs included | Fold new blocks only when set, as a tagged extension (precedent `precommit_digest_` native_execution_consumer.hpp:1942); test that a default spec hashes to the pre-change constant | F | -| E2 | **Margin.** The TV margin call is built host-side from kernel primitives (a Stop-triggered `Reduce` bound to openings, or `execute_current`, which survives in `submit_margin_call_slice` pine_adapter.cpp:17968); its chronology lives at `schedule_margin_call_path` pine_adapter.cpp:20246 and its excursion sample differed by chronology (the Pine host's own excursion model, retired by R5 lane H-THIN: the kernel's sampler books it) | A kernel model under an adapter run would liquidate twice or reorder settlement; a checkpoint that reduces mid-path changes lot state exactly where the adapter's chronology lives | Superseded by R5: the adapter declares `spec.margin` — maintenance-only, so no opening is decided twice (`AdmitWithHostMargin`, MG4) — and the kernel owns the mechanism (check points, resting liquidation, receipt) while the adapter's hooks answer TradingView's scheduling, money and slice (ADR-0001 "Margin"). What stays host-side is named there: the chronology exceptions, the pre-open slice, the 1x-long money call and the broker-open FX rollover, whose kernel `FxRoll` point measures elsewhere (MG9). Corrected by R5 lane F7 | F O S | +| E2 | **Margin.** The TV margin call is built host-side from kernel primitives (a Stop-triggered `Reduce` bound to openings, or `execute_current`, which survives in `submit_margin_call_slice` pine_adapter.cpp:17972); its chronology lives at `schedule_margin_call_path` pine_adapter.cpp:20250 and its excursion sample differed by chronology (the Pine host's own excursion model, retired by R5 lane H-THIN: the kernel's sampler books it) | A kernel model under an adapter run would liquidate twice or reorder settlement; a checkpoint that reduces mid-path changes lot state exactly where the adapter's chronology lives | Superseded by R5: the adapter declares `spec.margin` — maintenance-only, so no opening is decided twice (`AdmitWithHostMargin`, MG4) — and the kernel owns the mechanism (check points, resting liquidation, receipt) while the adapter's hooks answer TradingView's scheduling, money and slice (ADR-0001 "Margin"). What stays host-side is named there: the chronology exceptions, the pre-open slice, the 1x-long money call and the broker-open FX rollover, whose kernel `FxRoll` point measures elsewhere (MG9). Corrected by R5 lane F7 | F O S | | E3 | **Sizing.** TV sizes from the tick-rounded signal close, 10-digit-rounded equity and fee-reserved cash, frozen at signal, with fill-time exceptions in COOF paths (`default_sizing_shape` pine_adapter.cpp:3193, the COOF `at_fill` exception pine_adapter.cpp:8407) | One "generic" percent sizing serving both would break parity or import TV rounding into the kernel; the last ulp differs on lot-stepped instruments | Two paths on purpose: `Sized` (kernel, plain arithmetic, timing as a first-class field) vs `HostSized` + the adapter resolver. Never "simplify" the adapter onto the kernel basis. What the two paths share is the conversion: since R5 lanes N11 and F7 the division by price × point value × FX and the percentage fee reserve exist once, in the kernel (`native_sized_units`), for a default AND a typed (`qty_type` cash / percent) quantity. The adapter keeps what is measured TradingView: the money basis (signal-time equity, TradingView's margin equity at a typed fill, the hypothetical Flatten's balance for a typed reversal), the freeze, the fill-time exceptions and both lot floors (`tests/test_adapter_sizing_relower.cpp` cases 8, 9, 11: kernel 0.0392 against the percent floor's 0.0391 on a 0.0001 lot), and a staged FX series keeps `Sized` wherever the acceptance coordinate converts at the source's rate (case 12) | F O S | | E4 | **Calc on fill.** Kernel suffix eligibility is continuous along the segment; TV refills only at O/H/L/C waypoints -- on a magnified path at each intrabar's open, twice, and its two extremes, never its close (R5 lane MAG-INTRABAR) (`PineScheduler::recalculate` pine_scheduler_native.cpp:745, `coof_next_waypoint` pine_adapter.cpp:6961) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:6551 and `drain_applied_notifications` (native_execution_consumer.cpp:7234); 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:22274) | The complete bar cannot be masked globally | Opt-in `OpenOnly` view (A.4); loud doc warning in L0 | F | +| E5 | **Open-bar lookahead.** The adapter reads the full bar in `on_bar_open` (margin path scheduling uses H/L, pine_adapter.cpp:22278) | The complete bar cannot be masked globally | Opt-in `OpenOnly` view (A.4); loud doc warning in L0 | F | | E6 | **Double equity recording.** The source host already records per bar (history: only its cadence remains, `scheduler_mark_report_point` pine_strategy_host.cpp:2791) | Kernel recording would duplicate points and shift drawdown | Report policy / ownership (A.1); the adapter declares `KernelRecordedAtHostMarks` (R1): one recorder, at the adapter's own marks | F O | | E7 | **Price grid.** TV does not merely quantize: it tests triggers against a quantized bar while booking the fill on the *unrounded* level, so the adapter shifts the threshold (`source_trigger_threshold` pine_adapter.cpp:384) and books elsewhere | A kernel grid that quantizes both sides is correct and not TV; moving rounding into the kernel would change both native results and the adapter | `NativePriceGrid::None` for the adapter permanently; never "unify". Before L8: a byte-level experiment that the default price spelling on decimal ticks (one-ULP representations) does not move (S) | O S | | E8 | **HTF.** The evaluator machinery carries TV publication semantics (lookahead, boundary deferral, early close, `syminfo.type` branching engine_security.cpp:67-73). The feed store is TV-calibrated too: "W" / "M" from native daily bars, native stamps as the period partition (`set_native_security_feed` `sha256:eca3dbea0baa461982ba89f13d0a5c43277854069fb6eb2dd2f5c5471313204d` engine_aux_security.cpp:51-83, *fresh*). Two aggregators exist: script buckets use `native_calendar`, `request.security` uses `TimeframeAggregator`, whose completion rules are TV-pinned (timeframe.hpp:288-330) | Reuse risks changing the *order* of the Pine scheduler's pumps; a bare host inherits TV partition rules; native HTF may differ from Pine HTF on early-close sessions | Registration-gated pump; no change to `scheduler_feed_security_input` pine_strategy_host.cpp:1816; full corpus diff; supplied-bar provenance preserved, never silently replaced by chart aggregation (S); differences on early-close sessions are recorded, not "fixed" (F). The Pine scheduler keeps one more `TimeframeAggregator` pass, before the run (`PineScheduler::run_begin`, audit M26): the script-bar count it yields (`terminal_source_bar`, the probe-tail suppression) and the per-input completion flags it hands `on_native_input` are needed before the first input arrives, while the kernel seals script intervals only as inputs arrive and offers no pre-run interval query (`NativeExecutionConsumer::script_interval_at` is consumer-private). Retained by R5 lane F7: replacing it needs that kernel capability | F O S | @@ -1623,7 +1623,7 @@ the rule gives 9.9 + 9.89). | P5 | **Test asymmetry:** 430 adapter test files vs 14 native when this plan was written (on this tree 543 `tests/*.cpp` name `source::PineStrategyHost`, 110 `NativeStrategyHost`) | native-only tests in every lane (§3.1 b) | O | | P6 | **Stream vs batch.** The stream refuses monthly input, mixed modes, in-session gaps, security feeds and source setters, and tick input under an FX curve (any non-empty curve until audit lane N6) (`native-engine.md`, "Stream limitations (current)") | L5 and L6 must state which refusals are generic and which are deliberate source limits | S | | P7 | **C ABI variant growth** | §2.iii hardening rules | S | -| P8 | **Unverified in the inputs:** whether `execution::LifecycleEffects` (execution.hpp:136-139) is ever non-empty from the Pine path, else `PlacementSnapshot::legs` (pine_adapter.hpp:364) is the only live lifecycle (O); the `strategy.risk.allow_entry_in` setter chain beyond `set_pine_risk_direction` pine_strategy_host.cpp:886 → `set_risk_direction` pine_adapter.cpp:26487 (O); what the ABI receipts pin (F); the probe corpus itself lives outside this tree (F) | verify inside the lane that touches each | F O | +| P8 | **Unverified in the inputs:** whether `execution::LifecycleEffects` (execution.hpp:136-139) is ever non-empty from the Pine path, else `PlacementSnapshot::legs` (pine_adapter.hpp:364) is the only live lifecycle (O); the `strategy.risk.allow_entry_in` setter chain beyond `set_pine_risk_direction` pine_strategy_host.cpp:886 → `set_risk_direction` pine_adapter.cpp:26491 (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) @@ -1726,7 +1726,7 @@ virtual std::optional resolve_margin_call_units(const NativeMarginCallVi virtual void on_native_margin_call(const native_order::MarginCallEvent&) {} // O, S ``` -- Mechanism (F): a **kernel-originated request**. At each script-bar open and after each applied fill the kernel computes the liquidation level and rests a `Reduce` / `Flatten` with `Stop{level}` bound to the live openings — the shape the adapter builds host-side today (pine_adapter.cpp:20233). New hashed `RequestDefinition::origin { Host, KernelLiquidation, KernelRisk }`, new `CancelReason::Superseded` on re-price, `MarginCallEvent` joins `CommandEvent` (17 → 18, native_order.hpp:1308). O's alternative, a checkpoint on the excursion walk (`apply_excursion` native_execution_consumer.cpp:3392), 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:20237). New hashed `RequestDefinition::origin { Host, KernelLiquidation, KernelRisk }`, new `CancelReason::Superseded` on re-price, `MarginCallEvent` joins `CommandEvent` (17 → 18, native_order.hpp:1308). O's alternative, a checkpoint on the excursion walk (`apply_excursion` native_execution_consumer.cpp:3392), 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:3716 per side; scheduling next to `invoke_bar_open_callback` / `drain_after_applied`), `native_order.*`, `engine_state_hash.cpp` + the coverage checker, `c_abi.cpp` + `pineforge.h` (`pf_native_run_spec_v2` or an extension struct; `CLAUDE.md` SOP). @@ -1836,7 +1836,7 @@ std::optional risk; // NativeRunSpec - Breach: `MatchRejectReason::RiskLimit` for openings (O; F: `RiskHalted`), an optional kernel-issued flatten through A.3's kernel-originated request (`origin = KernelRisk`, F), and a `NativeRiskEvent` in `native_events` (native_host.hpp:1335) (O) or a callback `on_native_risk_event` (S — a virtual, so only inside an epoch batch). The day key uses the existing calendar (`session_day_ordinal` native_calendar.hpp:416), not a chart clock. - S's `directions` / `max_units` / `max_position_units` are existing spec fields (MG1-MG3), not duplicated. -- Interim (F): a C++ host can build all four today from `native_marked_equity` + `on_native_applied`, which is what the adapter does (pine_adapter.cpp:17333-17341); 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:17337-17345); 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:3716 and the per-point walk), `native_order.hpp`, the state hash. ### A.9 Single-source proposals not adopted diff --git a/docs/native-refactor-progress.md b/docs/native-refactor-progress.md index 9d278faa4..b80837f5b 100644 --- a/docs/native-refactor-progress.md +++ b/docs/native-refactor-progress.md @@ -183,7 +183,7 @@ them for the record only. "Linux aarch64" is the maintainers' Cortex-A725 host. | Compact journal records and a history ring buffer (L3) | Smaller command records and a ring buffer for the request core's history. `WorkingRequestCore::history` (`include/pineforge/native_order.hpp:1709`) hands out the whole vector and the core holds `history_` (`include/pineforge/native_order.hpp:2280`) inside `native_order_v7`, so both are a layout and API change | L3, Linux aarch64 (Cortex-A725), `perf` call graphs, inclusive shares: at most 3.68 % of bracket, 11.99 % of re-issue, 4.26 % of market and 3.42 % of the 14 probes (retiring the history alone: 1.64, 4.51, 2.53 and 1.00 %); only part is realisable, since records must still be written | "R5 L3 DONE + ACCEPTED 2026-09-24 22:10 Taipei", ruling 1: "DEFERRED to the PERF-D3 structural plan"; TRIAGE1 §D: "compact journal records" | Lane L3's code, merged by INT22 (#285); its profiles are unpublished | A `native_order` layout epoch | | A checked-values handoff for executions (L3) | `execution_values` runs twice, in `check_execution` (`src/native_order.cpp:4887`) and again in `apply_execution` (`src/native_order.cpp:4910`); handing the checked values across needs a public type, an API addition with no layout change | L3, as above: 2.21 % of bracket for the two together, about 1.1 % recoverable | L3 ruling 2: "a public checked-values handoff for executions (~1.1% bracket) -> DEFERRED" | As above | An owner decision on the API addition | | A chunked `trades_` store (L3) | Closed trades in chunks rather than one vector; `trades_` (`include/pineforge/engine.hpp:547`) is protected `BacktestEngine` state, so script ABI | L3, as above: on bracket, vector growth 0.64 %, `Trade` moves 0.74 % and `record_close_trade` 0.43 % (at most about 1.4 %); at most 0.35 % on the 14 probes | L3 ruling 4: "chunked trades_ store (ABI) -> DEFERRED" | As above | An `engine_script_run` epoch that changes the engine's protected layout | -| A zoned chart-day memo (D2-D finding 4) | On a chart with a timezone, `libc_chart_day_key` (`src/source/pine_adapter.cpp:17375`) takes the process timezone lock through `ScopedTimezone` and calls `localtime_r` on every bar, because `on_bar_open` (`src/source/pine_adapter.cpp:22274`) reads `chart_day_key` (`src/source/pine_adapter.cpp:22511`) unconditionally. The non-UTC monthly Sharpe/Sortino walk, `month_key_local` (`src/engine_metrics.cpp:46`), pays one `localtime_r` per equity point. UTC charts use D2-D's memo, `civil_chart_day_key` (`src/source/pine_adapter.cpp:17359`) | Not measured on a zoned chart. The UTC path cost 134 instructions a bar before D2-D's memo and 68 after (D2-D, Linux aarch64, callgrind on the floor). PERF-K1: `ScopedTimezone` copies the zone name, which allocates for names of 16 or more characters under libstdc++ | TRIAGE1 §D: "D2-D finding 4 (the timezone `localtime_r` memo)", under the owner's cap. D2-D's report: an exact zoned memo "would need the zone's transition table, which belongs in `timezone.cpp`" | None; the UTC memo's witness is `tests/test_chart_day_memo.cpp` | A lane that owns `src/timezone.cpp` and builds an exact zone-transition memo (PERF-K1's Sitka case shows why a shortcut is not exact), byte-identical, with the zoned keys of `test_chart_day_memo` still equal to the computed ones | +| A zoned chart-day memo (D2-D finding 4) | On a chart with a timezone, `libc_chart_day_key` (`src/source/pine_adapter.cpp:17379`) takes the process timezone lock through `ScopedTimezone` and calls `localtime_r` on every bar, because `on_bar_open` (`src/source/pine_adapter.cpp:22278`) reads `chart_day_key` (`src/source/pine_adapter.cpp:22515`) unconditionally. The non-UTC monthly Sharpe/Sortino walk, `month_key_local` (`src/engine_metrics.cpp:46`), pays one `localtime_r` per equity point. UTC charts use D2-D's memo, `civil_chart_day_key` (`src/source/pine_adapter.cpp:17363`) | Not measured on a zoned chart. The UTC path cost 134 instructions a bar before D2-D's memo and 68 after (D2-D, Linux aarch64, callgrind on the floor). PERF-K1: `ScopedTimezone` copies the zone name, which allocates for names of 16 or more characters under libstdc++ | TRIAGE1 §D: "D2-D finding 4 (the timezone `localtime_r` memo)", under the owner's cap. D2-D's report: an exact zoned memo "would need the zone's transition table, which belongs in `timezone.cpp`" | None; the UTC memo's witness is `tests/test_chart_day_memo.cpp` | A lane that owns `src/timezone.cpp` and builds an exact zone-transition memo (PERF-K1's Sitka case shows why a shortcut is not exact), byte-identical, with the zoned keys of `test_chart_day_memo` still equal to the computed ones | | Thread-local reads that remain per call | D2-C moved the three scopes' writes onto the pump's block, `pump_ambient` (`src/native_execution_consumer.hpp:496`). A reader still reaches the block through `tl_runtime_ambient` (`src/ta_extremes_volume.cpp:29`): `bar_context` (`src/ta_extremes_volume.cpp:82`), read by every `ExtremeRing::update` (`src/ta_extremes_volume.cpp:117`), which backs `ta.highest`, `ta.lowest`, `ta.highestbars` and `ta.lowestbars` and, through an embedded `Highest` and `Lowest`, `ta.stoch`, `ta.wpr` and `ta.range`; and `matching_day_partition` (`src/timeframe.cpp:628`), read by the default-anchored `ta.vwap` through `session_day_index`, by `crosses_boundary` on a DAY period (`timeframe.change("D")`) and by the session-period helpers. `decompose_ms_local` (`src/session_time.cpp:250`) reads two thread-locals per call on a zoned clock, and the prepared order storage's `free_blocks` (`src/native_order.cpp:620`) one per prepared mutation. `ema_na_warmup_flag` is read once per EMA, at its first compute | One thread-local access per reader call. AUDIT4, macOS arm64, a dlopen'd tutorial-MACD probe, counted by `_tlv_get_addr` breakpoints and by descriptor patching: 1.000 a bar for one `ta::Highest` and for one default `ta::VWAP`, 1.999 for one `pine_hour` on a New York clock, 0.000 on the no-order floor and with orders. On Linux a TLS-descriptor call costs about 20 instructions: D2-C's 13 → 4 calls a bar was 260 → 80 instructions (Linux aarch64, GCC 13, callgrind). INT23: 2.00 a bar on the zoned probe 064, 0.08 on probe 008 | None for the TA and day-partition readers: "RULING: INT23 ACCEPTED" (ledger, 2026-09-24 23:30 UTC) reads "dlopen TLS 13->0/bar", and INT23's report names only the timezone and order-core residuals. This row is their record | Unpublished (AUDIT4's scratch) | A byte-identical lane that hands these readers the pump's block without a thread-local read. `ta::bar_context()` is script ABI, so its public declaration stays as it is | | "audit-off mode", "LTO", "data-oriented layout" | Named in "OWNER DECISION 2026-09-25 00:20 Taipei" among the post-release structural items: "S1 bulk quiet bars, S2 per-strategy specialization, audit-off mode, LTO+PGO, data-oriented layout". No engine or campaign document defines, designs or measures the three | None | The owner decision | None | Struck from this inventory until a design note defines one of them | diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 68a8fdc5a..3c196ce2b 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -1496,7 +1496,7 @@ adapter answers `margin_check_allowed` with TradingView's scheduling — which includes the post-exit re-size: when a priced bracket leg of the script bar fills, the slice resting at that bar's adverse extreme was sized on the pre-exit book, and the legacy broker cancelled and re-scheduled it there -(`margin_check_allowed` `pine_adapter.cpp:17853-17886`), so the adapter admits +(`margin_check_allowed` `pine_adapter.cpp:17857-17890`), 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 From 862f5752dccb70b7ddd448cd3c9c1b728883cdb6 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Mon, 19 Oct 2026 09:32:37 +0800 Subject: [PATCH 29/35] build(cr150): move L15b registration into lane fragment --- tests/CMakeLists.txt | 4 ---- tests/cr150.d/core-cr150-l15b.cmake | 2 ++ 2 files changed, 2 insertions(+), 4 deletions(-) create mode 100644 tests/cr150.d/core-cr150-l15b.cmake diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e29e448bb..bad64a536 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -7107,10 +7107,6 @@ if(PINEFORGE_BUILD_SOURCE_LAYER) pineforge_add_unit_test(test_magnifier_path_source_l16) endif() -# CR-150 L15b: append after every existing line-pinned registration. -list(APPEND TEST_SOURCES test_native_bar_views_l15b) -pineforge_add_unit_test(test_native_bar_views_l15b) - # CR-150 L8a: keep the existing CMake line pins stable. list(APPEND TEST_SOURCES test_trade_ledger) pineforge_add_unit_test(test_trade_ledger) diff --git a/tests/cr150.d/core-cr150-l15b.cmake b/tests/cr150.d/core-cr150-l15b.cmake new file mode 100644 index 000000000..e7e6bd032 --- /dev/null +++ b/tests/cr150.d/core-cr150-l15b.cmake @@ -0,0 +1,2 @@ +list(APPEND TEST_SOURCES test_native_bar_views_l15b) +pineforge_add_unit_test(test_native_bar_views_l15b) From 680221b714638260b032cc87f5dc10a95881ef02 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Mon, 19 Oct 2026 09:53:47 +0800 Subject: [PATCH 30/35] test(cr150): apply CORE-TOP's 09:48 F0a ruling, option (a) Remove only the out-of-contract final recorded-row/scalar CHECK, its sole stderr diagnostic, and the now-unused cinttypes include. Preserve every other CHECK and all runtime/hash recording cadence. Base evidence quoted in REPORT.md Fix rounds 2-4: Round-2 Debug ctest.log:1863-1864 and Release ctest.log:1866-1867, rj-20261018t173132-91c857: "hash boundary: recorded=4c5b127f7df116d4 scalar_after_run=9718642416a18a0b scalar_after_report=9718642416a18a0b" "whole.broker_state_hash[63] == on.broker_state_hash()" These observations already differed before F0a. CORE-TOP ruling 09:48, option (a), excludes cadence changes from CR-150 and authorizes removing this one assertion. --- tests/test_admission_journal_window.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/test_admission_journal_window.cpp b/tests/test_admission_journal_window.cpp index 112a0a84a..d588f4cb2 100644 --- a/tests/test_admission_journal_window.cpp +++ b/tests/test_admission_journal_window.cpp @@ -5,7 +5,6 @@ #include #include -#include #include #include #include @@ -143,11 +142,6 @@ void recording_and_prefix_identity() { if (whole.broker_state_hash_len == 64 && part.broker_state_hash_len == 32) { for (int i = 0; i < 32; ++i) CHECK(part.broker_state_hash[i] == whole.broker_state_hash[i]); - if (whole.broker_state_hash[63] != on.broker_state_hash()) - std::fprintf(stderr, "hash boundary: recorded=%016" PRIx64 - " scalar_after_run=%016" PRIx64 " scalar_after_report=%016" PRIx64 "\n", - whole.broker_state_hash[63], before_report, on.broker_state_hash()); - CHECK(whole.broker_state_hash[63] == on.broker_state_hash()); } CHECK(on.broker_state_hash() == before_report); const auto digest = on.broker_state_hash(); From b4bbca0ccd1527107cabae6d34c48395d739428c Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Mon, 19 Oct 2026 10:03:21 +0800 Subject: [PATCH 31/35] fix(cr150): re-pin transcript digests under fixture authority 07:49 rj-20261019t010941-4bc9d7 --- ...st_adapter_live_state_equivalence_data.hpp | 206 +++++++++--------- 1 file changed, 103 insertions(+), 103 deletions(-) diff --git a/tests/test_adapter_live_state_equivalence_data.hpp b/tests/test_adapter_live_state_equivalence_data.hpp index 30b0ce5a0..bc72c16ba 100644 --- a/tests/test_adapter_live_state_equivalence_data.hpp +++ b/tests/test_adapter_live_state_equivalence_data.hpp @@ -415,112 +415,112 @@ // brackets seed 314188: 5626204446991468979 -> 13444318351679181243 // chains seed 314189: 18266502754563434276 -> 300926759266208037 constexpr std::uint64_t kTranscriptDigests[] = { - 1790576907302357908ull, // reversals seed 104729, 124 trades, 397 commands, 444 rows placed - 10024640008279748788ull, // reversals seed 209458, 124 trades, 379 commands, 418 rows placed - 18108469526809334996ull, // reversals seed 314187, 220 trades, 710 commands, 708 rows placed - 8603512707567609453ull, // reversals seed 418916, 106 trades, 372 commands, 373 rows placed + 5812684589312154871ull, // reversals seed 104729, 124 trades, 397 commands, 444 rows placed + 4683315889309806675ull, // reversals seed 209458, 124 trades, 379 commands, 418 rows placed + 11192938340988648442ull, // reversals seed 314187, 220 trades, 710 commands, 708 rows placed + 9447733487864338841ull, // reversals seed 418916, 106 trades, 372 commands, 373 rows placed 16125481158763653444ull, // reversals seed 523645, 104 trades, 417 commands, 380 rows placed - 11954619287366675874ull, // reversals seed 628374, 132 trades, 411 commands, 403 rows placed - 10949395400367693376ull, // reversals seed 733103, 138 trades, 387 commands, 412 rows placed - 2687164286151755975ull, // reversals seed 837832, 90 trades, 401 commands, 369 rows placed - 6131133790994226055ull, // reversals seed 942561, 98 trades, 408 commands, 367 rows placed - 12699224912122031164ull, // reversals seed 1047290, 137 trades, 390 commands, 399 rows placed + 6599795193700557484ull, // reversals seed 628374, 132 trades, 411 commands, 403 rows placed + 4925702266421634684ull, // reversals seed 733103, 138 trades, 387 commands, 412 rows placed + 11651815776502670583ull, // reversals seed 837832, 90 trades, 401 commands, 369 rows placed + 385235826901295142ull, // reversals seed 942561, 98 trades, 408 commands, 367 rows placed + 2560334546094961294ull, // reversals seed 1047290, 137 trades, 390 commands, 399 rows placed 10245107961889113516ull, // reversals seed 1152019, 141 trades, 392 commands, 444 rows placed 10671261055934450290ull, // reversals seed 1256748, 117 trades, 401 commands, 373 rows placed - 3043805792067546930ull, // reversals seed 1361477, 100 trades, 409 commands, 390 rows placed - 11441719398179537978ull, // reversals seed 1466206, 156 trades, 387 commands, 440 rows placed - 18024148252823409663ull, // reversals seed 1570935, 69 trades, 344 commands, 269 rows placed - 13300180944349916279ull, // reversals seed 1675664, 120 trades, 404 commands, 439 rows placed - 2813325518340450122ull, // reversals seed 1780393, 177 trades, 669 commands, 628 rows placed - 2275854726471319605ull, // reversals seed 1885122, 127 trades, 396 commands, 402 rows placed - 11265413284249527184ull, // reversals seed 1989851, 171 trades, 429 commands, 451 rows placed - 8444874965488324792ull, // reversals seed 2094580, 92 trades, 384 commands, 350 rows placed - 7434878463000589134ull, // reversals seed 2199309, 123 trades, 401 commands, 391 rows placed - 16340996993447103239ull, // reversals seed 2304038, 162 trades, 409 commands, 437 rows placed - 2756298568903753126ull, // reversals seed 2408767, 138 trades, 418 commands, 419 rows placed - 8654109025937810515ull, // reversals seed 2513496, 102 trades, 393 commands, 399 rows placed - 12615617675046204564ull, // reversals seed 2618225, 105 trades, 412 commands, 400 rows placed - 13574645891582671747ull, // reversals seed 2722954, 139 trades, 398 commands, 445 rows placed + 5536466409891028260ull, // reversals seed 1361477, 100 trades, 409 commands, 390 rows placed + 5402302340520265648ull, // reversals seed 1466206, 156 trades, 387 commands, 440 rows placed + 1616424144157930691ull, // reversals seed 1570935, 69 trades, 344 commands, 269 rows placed + 7511139969801259153ull, // reversals seed 1675664, 120 trades, 404 commands, 439 rows placed + 1601257824703151799ull, // reversals seed 1780393, 177 trades, 669 commands, 628 rows placed + 1890500574835062377ull, // reversals seed 1885122, 127 trades, 396 commands, 402 rows placed + 7085816489193635920ull, // reversals seed 1989851, 171 trades, 429 commands, 451 rows placed + 6023548670256492480ull, // reversals seed 2094580, 92 trades, 384 commands, 350 rows placed + 16692781169495489153ull, // reversals seed 2199309, 123 trades, 401 commands, 391 rows placed + 16272109601722490480ull, // reversals seed 2304038, 162 trades, 409 commands, 437 rows placed + 4049421656702198461ull, // reversals seed 2408767, 138 trades, 418 commands, 419 rows placed + 13178968189170620141ull, // reversals seed 2513496, 102 trades, 393 commands, 399 rows placed + 17670434204484139401ull, // reversals seed 2618225, 105 trades, 412 commands, 400 rows placed + 1633357297523434718ull, // reversals seed 2722954, 139 trades, 398 commands, 445 rows placed 11406439758950114475ull, // reversals seed 2827683, 45 trades, 340 commands, 197 rows placed - 11829483237718104981ull, // reversals seed 2932412, 92 trades, 366 commands, 327 rows placed - 4678338477382770081ull, // reversals seed 3037141, 114 trades, 405 commands, 409 rows placed + 16858162214369946201ull, // reversals seed 2932412, 92 trades, 366 commands, 327 rows placed + 10003877012992753894ull, // reversals seed 3037141, 114 trades, 405 commands, 409 rows placed 9350461461386796741ull, // reversals seed 3141870, 66 trades, 347 commands, 260 rows placed - 18025952178484281790ull, // reversals seed 3246599, 156 trades, 467 commands, 537 rows placed - 6358143307834022586ull, // reversals seed 3351328, 106 trades, 414 commands, 363 rows placed - 12370377506510123879ull, // reversals seed 3456057, 87 trades, 373 commands, 351 rows placed - 4523288504224647910ull, // reversals seed 3560786, 153 trades, 403 commands, 452 rows placed - 14246593818834397991ull, // reversals seed 3665515, 136 trades, 393 commands, 424 rows placed - 5669930245333977489ull, // reversals seed 3770244, 34 trades, 321 commands, 226 rows placed - 7562978076140294353ull, // brackets seed 104730, 84 trades, 472 commands, 586 rows placed - 918119817585072101ull, // brackets seed 209459, 79 trades, 588 commands, 642 rows placed - 13444318351679181243ull, // brackets seed 314188, 120 trades, 788 commands, 950 rows placed - 345551147133871958ull, // brackets seed 418917, 94 trades, 524 commands, 629 rows placed - 3598556121825612327ull, // brackets seed 523646, 73 trades, 524 commands, 523 rows placed - 10787524866046025632ull, // brackets seed 628375, 92 trades, 580 commands, 741 rows placed - 12647837381492911624ull, // brackets seed 733104, 105 trades, 546 commands, 689 rows placed - 9142292326310325449ull, // brackets seed 837833, 60 trades, 471 commands, 504 rows placed - 1575526034554365613ull, // brackets seed 942562, 96 trades, 541 commands, 581 rows placed - 3479520250185906772ull, // brackets seed 1047291, 84 trades, 508 commands, 578 rows placed - 14362220529398004157ull, // brackets seed 1152020, 71 trades, 559 commands, 681 rows placed - 18185683915043225509ull, // brackets seed 1256749, 85 trades, 510 commands, 578 rows placed - 12383757188086100027ull, // brackets seed 1361478, 87 trades, 538 commands, 598 rows placed - 13395291126983886538ull, // brackets seed 1466207, 86 trades, 571 commands, 692 rows placed - 17661091441404936538ull, // brackets seed 1570936, 66 trades, 475 commands, 665 rows placed - 12371414418607433001ull, // brackets seed 1675665, 74 trades, 501 commands, 551 rows placed - 5509523710559050159ull, // brackets seed 1780394, 92 trades, 671 commands, 752 rows placed - 17554269931279056335ull, // brackets seed 1885123, 93 trades, 540 commands, 738 rows placed - 14174662451420240772ull, // brackets seed 1989852, 77 trades, 558 commands, 683 rows placed - 4473743503459375456ull, // brackets seed 2094581, 73 trades, 520 commands, 601 rows placed - 5399518682810858268ull, // brackets seed 2199310, 76 trades, 452 commands, 575 rows placed - 12383750543640558931ull, // brackets seed 2304039, 89 trades, 548 commands, 663 rows placed - 5478903712594868650ull, // brackets seed 2408768, 71 trades, 527 commands, 614 rows placed - 5629628733882308382ull, // brackets seed 2513497, 72 trades, 507 commands, 525 rows placed - 14281581543217611485ull, // brackets seed 2618226, 73 trades, 508 commands, 558 rows placed - 6533341922887312293ull, // brackets seed 2722955, 93 trades, 490 commands, 657 rows placed - 11549075198333019681ull, // brackets seed 2827684, 66 trades, 557 commands, 594 rows placed - 15594624435547718078ull, // brackets seed 2932413, 73 trades, 509 commands, 552 rows placed - 8625746513086268806ull, // brackets seed 3037142, 64 trades, 507 commands, 528 rows placed - 9112443912224141062ull, // brackets seed 3141871, 95 trades, 558 commands, 645 rows placed - 1519353229550935462ull, // brackets seed 3246600, 98 trades, 629 commands, 829 rows placed - 8445431132239256351ull, // brackets seed 3351329, 86 trades, 543 commands, 615 rows placed - 17096340679869233820ull, // brackets seed 3456058, 73 trades, 530 commands, 558 rows placed - 5173892073438342031ull, // brackets seed 3560787, 96 trades, 506 commands, 588 rows placed - 13707670575754008369ull, // brackets seed 3665516, 75 trades, 559 commands, 694 rows placed - 11684879077355395000ull, // brackets seed 3770245, 79 trades, 510 commands, 573 rows placed - 4961901654803425155ull, // chains seed 104731, 67 trades, 318 commands, 348 rows placed - 11415452759840164455ull, // chains seed 209460, 71 trades, 379 commands, 424 rows placed - 300926759266208037ull, // chains seed 314189, 79 trades, 477 commands, 480 rows placed - 4820568811517812619ull, // chains seed 418918, 44 trades, 293 commands, 349 rows placed - 146053814318818740ull, // chains seed 523647, 47 trades, 314 commands, 327 rows placed - 17614793864897068161ull, // chains seed 628376, 73 trades, 342 commands, 350 rows placed - 4167100239005267285ull, // chains seed 733105, 67 trades, 369 commands, 415 rows placed - 1658255311856021412ull, // chains seed 837834, 60 trades, 368 commands, 418 rows placed - 6339945337346473638ull, // chains seed 942563, 56 trades, 343 commands, 361 rows placed - 730910484327473289ull, // chains seed 1047292, 35 trades, 363 commands, 402 rows placed - 7776152202237345240ull, // chains seed 1152021, 72 trades, 351 commands, 401 rows placed - 15009504967454947641ull, // chains seed 1256750, 64 trades, 314 commands, 315 rows placed - 14955372577578561066ull, // chains seed 1361479, 65 trades, 309 commands, 326 rows placed - 13676647888332124949ull, // chains seed 1466208, 79 trades, 388 commands, 405 rows placed - 7515518570456744171ull, // chains seed 1570937, 76 trades, 358 commands, 358 rows placed - 3770602179437410615ull, // chains seed 1675666, 64 trades, 314 commands, 346 rows placed - 4910402023661469991ull, // chains seed 1780395, 57 trades, 436 commands, 463 rows placed - 15441784586956253521ull, // chains seed 1885124, 60 trades, 362 commands, 336 rows placed - 7007249540920133426ull, // chains seed 1989853, 76 trades, 351 commands, 381 rows placed - 9887369544729338515ull, // chains seed 2094582, 37 trades, 310 commands, 353 rows placed - 2275122436928983111ull, // chains seed 2199311, 56 trades, 321 commands, 328 rows placed - 8018172237702962449ull, // chains seed 2304040, 64 trades, 326 commands, 339 rows placed - 10601587719066905856ull, // chains seed 2408769, 63 trades, 349 commands, 377 rows placed - 10758446752353699061ull, // chains seed 2513498, 50 trades, 344 commands, 354 rows placed - 5354971660748769171ull, // chains seed 2618227, 51 trades, 339 commands, 386 rows placed - 2693529084803416692ull, // chains seed 2722956, 59 trades, 355 commands, 391 rows placed - 8753155635371523673ull, // chains seed 2827685, 74 trades, 341 commands, 400 rows placed - 18215172146061067714ull, // chains seed 2932414, 62 trades, 336 commands, 373 rows placed - 1905765453874616921ull, // chains seed 3037143, 61 trades, 344 commands, 412 rows placed - 15456267325861807530ull, // chains seed 3141872, 64 trades, 366 commands, 416 rows placed - 13715974753572872612ull, // chains seed 3246601, 96 trades, 464 commands, 498 rows placed - 16665891776065465266ull, // chains seed 3351330, 45 trades, 301 commands, 354 rows placed - 17080496727403308679ull, // chains seed 3456059, 43 trades, 310 commands, 268 rows placed - 3614987093622666609ull, // chains seed 3560788, 61 trades, 363 commands, 426 rows placed - 11464826053545175105ull, // chains seed 3665517, 72 trades, 347 commands, 376 rows placed - 6257524543885771248ull, // chains seed 3770246, 63 trades, 346 commands, 364 rows placed + 1186645452882449094ull, // reversals seed 3246599, 156 trades, 467 commands, 537 rows placed + 5914138696329396694ull, // reversals seed 3351328, 106 trades, 414 commands, 363 rows placed + 8146859408863277364ull, // reversals seed 3456057, 87 trades, 373 commands, 351 rows placed + 9379975246217913177ull, // reversals seed 3560786, 153 trades, 403 commands, 452 rows placed + 14545178887694502241ull, // reversals seed 3665515, 136 trades, 393 commands, 424 rows placed + 7086626550256878961ull, // reversals seed 3770244, 34 trades, 321 commands, 226 rows placed + 10094645971911037415ull, // brackets seed 104730, 84 trades, 472 commands, 586 rows placed + 16256026640831797918ull, // brackets seed 209459, 79 trades, 588 commands, 642 rows placed + 13546768592260066367ull, // brackets seed 314188, 120 trades, 788 commands, 950 rows placed + 12993433230084037637ull, // brackets seed 418917, 94 trades, 524 commands, 629 rows placed + 11427830104576186523ull, // brackets seed 523646, 73 trades, 524 commands, 523 rows placed + 4752327840989693556ull, // brackets seed 628375, 92 trades, 580 commands, 741 rows placed + 8641804402859422695ull, // brackets seed 733104, 105 trades, 546 commands, 689 rows placed + 12725092510992792916ull, // brackets seed 837833, 60 trades, 471 commands, 504 rows placed + 18099586500469040260ull, // brackets seed 942562, 96 trades, 541 commands, 581 rows placed + 5430943859018843351ull, // brackets seed 1047291, 84 trades, 508 commands, 578 rows placed + 15664286658355663301ull, // brackets seed 1152020, 71 trades, 559 commands, 681 rows placed + 17291524785719174957ull, // brackets seed 1256749, 85 trades, 510 commands, 578 rows placed + 13255643617480355408ull, // brackets seed 1361478, 87 trades, 538 commands, 598 rows placed + 4116778928699750137ull, // brackets seed 1466207, 86 trades, 571 commands, 692 rows placed + 5689563574579797076ull, // brackets seed 1570936, 66 trades, 475 commands, 665 rows placed + 1819048845802213517ull, // brackets seed 1675665, 74 trades, 501 commands, 551 rows placed + 7746502993297993918ull, // brackets seed 1780394, 92 trades, 671 commands, 752 rows placed + 3797372730031200791ull, // brackets seed 1885123, 93 trades, 540 commands, 738 rows placed + 1372439258430626077ull, // brackets seed 1989852, 77 trades, 558 commands, 683 rows placed + 5014847110111286973ull, // brackets seed 2094581, 73 trades, 520 commands, 601 rows placed + 9382330743978314590ull, // brackets seed 2199310, 76 trades, 452 commands, 575 rows placed + 10266477138525481284ull, // brackets seed 2304039, 89 trades, 548 commands, 663 rows placed + 11630251291204579616ull, // brackets seed 2408768, 71 trades, 527 commands, 614 rows placed + 9681995728843047391ull, // brackets seed 2513497, 72 trades, 507 commands, 525 rows placed + 15487479544172750296ull, // brackets seed 2618226, 73 trades, 508 commands, 558 rows placed + 1900433198298071990ull, // brackets seed 2722955, 93 trades, 490 commands, 657 rows placed + 6173378355269621210ull, // brackets seed 2827684, 66 trades, 557 commands, 594 rows placed + 5622828643190822534ull, // brackets seed 2932413, 73 trades, 509 commands, 552 rows placed + 8432201514020882056ull, // brackets seed 3037142, 64 trades, 507 commands, 528 rows placed + 2324736937362423583ull, // brackets seed 3141871, 95 trades, 558 commands, 645 rows placed + 12132151619639363890ull, // brackets seed 3246600, 98 trades, 629 commands, 829 rows placed + 6983498278829102952ull, // brackets seed 3351329, 86 trades, 543 commands, 615 rows placed + 4135864409621548592ull, // brackets seed 3456058, 73 trades, 530 commands, 558 rows placed + 15575942321470447436ull, // brackets seed 3560787, 96 trades, 506 commands, 588 rows placed + 3151622594274226776ull, // brackets seed 3665516, 75 trades, 559 commands, 694 rows placed + 3588279018791132754ull, // brackets seed 3770245, 79 trades, 510 commands, 573 rows placed + 3254791467963586893ull, // chains seed 104731, 67 trades, 318 commands, 348 rows placed + 663207094553151764ull, // chains seed 209460, 71 trades, 379 commands, 424 rows placed + 9283772566969702802ull, // chains seed 314189, 79 trades, 477 commands, 480 rows placed + 2033162760298087391ull, // chains seed 418918, 44 trades, 293 commands, 349 rows placed + 12221320464222137166ull, // chains seed 523647, 47 trades, 314 commands, 327 rows placed + 2047618321397170201ull, // chains seed 628376, 73 trades, 342 commands, 350 rows placed + 921697445748184875ull, // chains seed 733105, 67 trades, 369 commands, 415 rows placed + 17976626526517250951ull, // chains seed 837834, 60 trades, 368 commands, 418 rows placed + 16934523334156189547ull, // chains seed 942563, 56 trades, 343 commands, 361 rows placed + 10373090182271315002ull, // chains seed 1047292, 35 trades, 363 commands, 402 rows placed + 16074989119573095012ull, // chains seed 1152021, 72 trades, 351 commands, 401 rows placed + 2862011423919039425ull, // chains seed 1256750, 64 trades, 314 commands, 315 rows placed + 6616125162615994474ull, // chains seed 1361479, 65 trades, 309 commands, 326 rows placed + 424855751488845231ull, // chains seed 1466208, 79 trades, 388 commands, 405 rows placed + 17665150425097115693ull, // chains seed 1570937, 76 trades, 358 commands, 358 rows placed + 12680252031151115087ull, // chains seed 1675666, 64 trades, 314 commands, 346 rows placed + 11300291163130859423ull, // chains seed 1780395, 57 trades, 436 commands, 463 rows placed + 4433442514945879207ull, // chains seed 1885124, 60 trades, 362 commands, 336 rows placed + 6667986704571303442ull, // chains seed 1989853, 76 trades, 351 commands, 381 rows placed + 17468462564703429683ull, // chains seed 2094582, 37 trades, 310 commands, 353 rows placed + 6883161397953621370ull, // chains seed 2199311, 56 trades, 321 commands, 328 rows placed + 17043463892635610333ull, // chains seed 2304040, 64 trades, 326 commands, 339 rows placed + 1105769744799573605ull, // chains seed 2408769, 63 trades, 349 commands, 377 rows placed + 2883434956168009346ull, // chains seed 2513498, 50 trades, 344 commands, 354 rows placed + 14725999688024910753ull, // chains seed 2618227, 51 trades, 339 commands, 386 rows placed + 9558404090892926781ull, // chains seed 2722956, 59 trades, 355 commands, 391 rows placed + 6376350046543796098ull, // chains seed 2827685, 74 trades, 341 commands, 400 rows placed + 4663592643653421001ull, // chains seed 2932414, 62 trades, 336 commands, 373 rows placed + 12118628373320771408ull, // chains seed 3037143, 61 trades, 344 commands, 412 rows placed + 10452998186580074403ull, // chains seed 3141872, 64 trades, 366 commands, 416 rows placed + 16075779900850779479ull, // chains seed 3246601, 96 trades, 464 commands, 498 rows placed + 2759418038614498470ull, // chains seed 3351330, 45 trades, 301 commands, 354 rows placed + 9300963237862288069ull, // chains seed 3456059, 43 trades, 310 commands, 268 rows placed + 12230891639422690187ull, // chains seed 3560788, 61 trades, 363 commands, 426 rows placed + 15029286602622171676ull, // chains seed 3665517, 72 trades, 347 commands, 376 rows placed + 17709499046001609036ull, // chains seed 3770246, 63 trades, 346 commands, 364 rows placed }; From 4cc8faf0bb36b249f81a3c573bc36f2a905bb5aa Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Mon, 19 Oct 2026 11:12:15 +0800 Subject: [PATCH 32/35] docs(cr150): refresh m11 parity anchors --- docs/design/native-feature-parity.md | 42 ++++++++++++++-------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 58a54eaee..45446fde7 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -124,15 +124,15 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:878 fires per accepted realtime print native_execution_consumer.cpp:8149; no per-tick calculation in batch; the guide pinned "close-only" (`docs/pages/native-engine.md`, "Batch OHLCV vs ticks vs quiet") **Closed:** `NativeCalculationTrigger::EveryModeledPoint` native_run_spec.hpp:97 adds one `Tick` recalculation at every modeled point of the delivered path and at every observed print, in batch and in a stream alike (`tests/test_native_calc_timing.cpp`). | K | L5 | F:E5 O:F4 S:C3 | **Disagree:** F yes / O partial / S no. R5-5: v1. Not an adapter feature either: absent from `PineStrategyConfig` pine_adapter.hpp:62-76, the runner rejects it (`calc_on_every_tick` main.cpp:259), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:297. | | 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:8744 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:928 fires once after each retained lower-timeframe sub-bar's whole matching path (`tests/test_native_calc_timing.cpp`). | K | L5 | F:E6 O:F8 S:C4 | — | | CT9 | Intrabar (magnifier) matching path + path-order policy (`volume_weighted_cap` pine_adapter.cpp:2936) | **yes** — `IntrabarPath` native_run_spec.hpp:362-403, delivery `deliver_confirmed_script` native_execution_consumer.cpp:8296-8562; `NativePathOrder` native_run_spec.hpp:335-339 | K+A | — | F:E7 F:G5 O:F7 S:C4 | **Disagree:** S partial (it wants callback cadence → CT8). TV sampler choice is A: TradingView's own intrabars at its table's timeframe, each owned by the chart bar of its last minute, built from the finer feed (`tradingview_magnifier_bars` magnifier_intrabars.hpp:46, R5 lane MAG-INTRABAR). | -| CT10 | Batch → warmup → realtime forward execution | **yes** — `stream_begin` engine.hpp:1772, `stream_push_bar` engine.hpp:1778, `stream_end` engine.hpp:1782, `NativeRunPhase` native_host.hpp:41, C ABI `strategy_stream_begin` c_abi.cpp:698 | K+A | — | F:E8 O:J3 S:C5 | **Disagree:** S partial. R5-1: the lifecycle is native; Pine trade-start suppression and realtime-tail / probe rules (`trading_window_active` pine_strategy_commands.cpp:15-21, `prepare_native_begin` pine_strategy_host.cpp:206-311) are A. S's "neutral tail + trade-window policy" is single-source and unassigned (§4 Q8). | +| CT10 | Batch → warmup → realtime forward execution | **yes** — `stream_begin` engine.hpp:1772, `stream_push_bar` engine.hpp:1778, `stream_end` engine.hpp:1782, `NativeRunPhase` native_host.hpp:41, C ABI `strategy_stream_begin` c_abi.cpp:699 | K+A | — | F:E8 O:J3 S:C5 | **Disagree:** S partial. R5-1: the lifecycle is native; Pine trade-start suppression and realtime-tail / probe rules (`trading_window_active` pine_strategy_commands.cpp:15-21, `prepare_native_begin` pine_strategy_host.cpp:206-311) are A. S's "neutral tail + trade-window policy" is single-source and unassigned (§4 Q8). | | CT11 | *(quirk)* COOF TV specifics: waypoint-only refill (`coof_next_waypoint` pine_adapter.cpp:6958), two-fills-at-open rule, script-state rollback (`PineScheduler` pine_scheduler_native.cpp:54-106; hooks `snapshot_script_state` engine.hpp:1333-1334), the first-open guard `kFirstOpenLoopGuard` pine_scheduler_native.cpp:866, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1961-1991 | n/a | A | — | F:E4 O:F5 S:C2 | R5-5: the adapter keeps its path | ### 1.6 Higher timeframes and auxiliary feeds | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| HT1 | HTF series of the same symbol (`request.security`): evaluators registered `scheduler_configure_security_evaluators` pine_strategy_host.cpp:1524, pumped from the scheduler `scheduler_prepare_security_sequence` pine_strategy_host.cpp:1534-1833; state lives in `BacktestEngine` engine.hpp:357-559, engine.hpp:357-403, engine_security.cpp:27 | **no** sanctioned path — `configure_security_evaluators` is an empty virtual engine.hpp:1323 whose only caller is pine_strategy_host.cpp:1525 **Closed:** `NativeRunSpec::subscriptions` (`NativeTimeframeSubscription` native_run_spec.hpp:491), or `declare_timeframe_subscriptions` native_host.hpp:1136 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:885 and `native_series_bar(index)` native_host.hpp:1119 reading them back; the kernel registers the declared series after that callback returns, so a host's own evaluators survive (`tests/test_native_htf_subscriptions.cpp`, `tests/test_native_htf_subscriptions_stream.cpp`). | K | L6 | F:F1 O:G-1 S:H1 | **Disagree:** S partial (it counts kernel-side storage → HT2); F, S v1 / O deferrable. R5-6: v1. | -| HT2 | Authoritative (exchange) HTF bars substituted into a series (`set_native_security_feed` engine.hpp:1732, engine_aux_security.cpp:79-133, C ABI `strategy_set_native_security_feed` c_abi.cpp:937) | **partial (inert)** — pre-run staging is accepted `refuse_source_mutation` native_execution_consumer.cpp:1879 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1640, its sole call `prepare_native_security_feeds` pine_strategy_host.cpp:1656; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1897 **Closed:** a bare host's subscriptions consume authoritative bars: `NativeTimeframeSubscription::authoritative_bars` native_run_spec.hpp:493 is staged into the feed store by the kernel's own `prepare_native_security_feeds` native_execution_consumer.cpp:9553 call at begin, so a declared series is the venue's own bars (`tests/test_native_htf_subscriptions.cpp`). | K | L6 | F:F2 O:G-2 S:H1 | F says "no" on the same facts. "native" in these names means TradingView's native-timeframe bars, not the native host (the feed store's comment `never from the chart's intraday prints` `sha256:3cc5174fa9a60fc0e2ca7870ef1cccd875abaf5d8e11248e633922612c18d9d8` engine_aux_security.cpp:51-61). | +| HT1 | HTF series of the same symbol (`request.security`): evaluators registered `scheduler_configure_security_evaluators` pine_strategy_host.cpp:1533, pumped from the scheduler `scheduler_prepare_security_sequence` pine_strategy_host.cpp:1534-1833; state lives in `BacktestEngine` engine.hpp:357-559, engine.hpp:357-403, engine_security.cpp:27 | **no** sanctioned path — `configure_security_evaluators` is an empty virtual engine.hpp:1323 whose only caller is pine_strategy_host.cpp:1534 **Closed:** `NativeRunSpec::subscriptions` (`NativeTimeframeSubscription` native_run_spec.hpp:491), or `declare_timeframe_subscriptions` native_host.hpp:1136 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:885 and `native_series_bar(index)` native_host.hpp:1119 reading them back; the kernel registers the declared series after that callback returns, so a host's own evaluators survive (`tests/test_native_htf_subscriptions.cpp`, `tests/test_native_htf_subscriptions_stream.cpp`). | K | L6 | F:F1 O:G-1 S:H1 | **Disagree:** S partial (it counts kernel-side storage → HT2); F, S v1 / O deferrable. R5-6: v1. | +| HT2 | Authoritative (exchange) HTF bars substituted into a series (`set_native_security_feed` engine.hpp:1732, engine_aux_security.cpp:79-133, C ABI `strategy_set_native_security_feed` c_abi.cpp:938) | **partial (inert)** — pre-run staging is accepted `refuse_source_mutation` native_execution_consumer.cpp:1879 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1640, its sole call `prepare_native_security_feeds` pine_strategy_host.cpp:1665; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1897 **Closed:** a bare host's subscriptions consume authoritative bars: `NativeTimeframeSubscription::authoritative_bars` native_run_spec.hpp:493 is staged into the feed store by the kernel's own `prepare_native_security_feeds` native_execution_consumer.cpp:9553 call at begin, so a declared series is the venue's own bars (`tests/test_native_htf_subscriptions.cpp`). | K | L6 | F:F2 O:G-2 S:H1 | F says "no" on the same facts. "native" in these names means TradingView's native-timeframe bars, not the native host (the feed store's comment `never from the chart's intraday prints` `sha256:3cc5174fa9a60fc0e2ca7870ef1cccd875abaf5d8e11248e633922612c18d9d8` engine_aux_security.cpp:51-61). | | HT3 | Lower-timeframe arrays (`request.security_lower_tf`; `register_security_lower_tf_eval` include/pineforge/source/pine_strategy_host.hpp:832) | **no** — `IntrabarPath::lower_tf` drives matching only **Closed:** `on_native_sub_bar` native_host.hpp:928 delivers every retained `lower_tf` sub-bar after its matching path, so a host that put the lower bars in the path reads them bar by bar (`tests/test_native_calc_timing.cpp`); the series form is the auxiliary feed of HT4 (`tests/test_native_auxiliary_feed.cpp`). | K | L5 (sub-bar hook); series form optional in L6 | F:F4 O:G-3 S:H2 | **Disagree on lane:** F via `on_native_sub_bar` (the host already owns the lower bars it put in the path) / O, S via the series API. v1 = the L5 hook (R5-5), landed; the array form Pine returns stays the adapter's. | | HT4 | Auxiliary finer feed driving `request.security` (`set_aux_security_feed` pine_aux_security.cpp:23-241, `feed_aux_security_for_chart_bar` pine_aux_security.cpp:262-488; the kernel base returns false `set_aux_security_feed` engine_consumer.cpp:158-161) | **no** **Closed:** `NativeRunSpec::auxiliary_feed` (`NativeAuxiliaryFeed` native_run_spec.hpp:524) plus `NativeSeriesSource::AuxiliaryFeed` build a series from the host's finer bars, routed by time alone, with `declare_auxiliary_feed` at begin and `append_auxiliary_bars` on a realtime stream (`tests/test_native_auxiliary_feed.cpp`, `tests/test_native_auxiliary_feed_stream.cpp`). | K+A | L6 | F:F3 S:H2 | **Disagree:** F quirk (TV split-feed harness construct) / S generic. R5-1 + R5-6: aux feed registration is generic; chart-slice mapping and deferred first-bucket publication stay A. **Closed (lane INT28-FIX, rule RC):** a `calc_on_order_fills` recalculation reads the calling bar's own slice, as the bar's close calculation does and as TradingView's does after the open fill or a later fill (a lookahead_on site through the slice's first bucket; `lab tv` tapes (the private fixture set `coof_recalc_security`): the bar's first recalculation before its close callback feeds it (`scheduler_feed_aux_security` pine_scheduler_native.cpp:790). The close callback fed it alone (`scheduler_feed_aux_security` pine_scheduler_native.cpp:652, now only when no recalculation did), after the bar's recalculations had read the previous bar's; a recalculation after that callback feeds nothing. | | HT5 | Publication mode: lookahead, gaps, session / early-close boundaries (`prepare_historical_security_lookahead_projections` pine_scheduler.cpp:42-132, `calling_bar_complete` pine_aux_security.cpp:282-397) | **no** for a bare host **Closed for a bare host:** `NativeTimeframeSubscription::lookahead` and `::gaps` are the two delivery rules, and the calendar boundaries — session close, early close by instrument class, the lazy seal of a clipped script interval — are the kernel's own (`tests/test_native_htf_subscriptions.cpp`). | K+A | L6 | S:H3 F:F1 O:G-1 | **Disagree:** F "no `lookahead_on`, by construction" / O `lookahead = false` default, available / S `lookahead_on` field, status partial. R5-6: user question (§4 U1); the exact TV projection stays A. | @@ -163,7 +163,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | RP6 | Per-trade accessors | **partial** — `get_trade` is public; the `closed_trade_*` family is protected engine.hpp:1392 **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:1850 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:1390, enabled `stream_observe_actions_` native_execution_consumer.cpp:10729, C ABI `strategy_stream_order_actions_len` c_abi.cpp:737-754 | K | — | F:H5 | (single-source) | | RP8 | Continuation hash | **yes** — `native_continuation_hash` native_host.hpp:1376, native_execution_consumer.cpp:12298; the spec is folded by `hash_spec` native_execution_consumer.cpp:239-261 | K | — | F:I1 O:J2 S:X3 | — | -| RP9 | Broker-state hash: scalar + per-bar recording | **partial** — scalar `broker_state_hash` engine_state_hash.cpp:22-118, C ABI `strategy_broker_state_hash` c_abi.cpp:593; per-bar rows are appended only by the source scheduler `scheduler_record_broker_hash` pine_strategy_host.cpp:2795 **Closed:** under `KernelRecorded` with `set_broker_state_hash_recording(true)` the consumer appends one `broker_state_hash()` row after each report point, so `broker_state_hash_len == equity_curve_len == script_bars_processed` on a completed run in batch and across a stream's warmup and realtime legs. A Failed or Aborted run can expose a shorter prefix. A row is the run's continuation identity at that bar, per driving mode — the broker half alone is mode-invariant (`tests/test_native_report_truth.cpp`). | K | L2 | F:I2 O:J1 S:X3 | O's "yes" covers the scalar only. §3.9 records the old quadratic adapter fold and closed-row kernel walk as history; V19-E and V19-A replaced them with running digests. | +| 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:594; per-bar rows are appended only by the source scheduler `scheduler_record_broker_hash` pine_strategy_host.cpp:2804 **Closed:** under `KernelRecorded` with `set_broker_state_hash_recording(true)` the consumer appends one `broker_state_hash()` row after each report point, so `broker_state_hash_len == equity_curve_len == script_bars_processed` on a completed run in batch and across a stream's warmup and realtime legs. A Failed or Aborted run can expose a shorter prefix. A row is the run's continuation identity at that bar, per driving mode — the broker half alone is mode-invariant (`tests/test_native_report_truth.cpp`). | K | L2 | F:I2 O:J1 S:X3 | O's "yes" covers the scalar only. §3.9 records the old quadratic adapter fold and closed-row kernel walk as history; V19-E and V19-A replaced them with running digests. | | RP10 | Fold the host's own durable state into the hash | **partial** — the protected virtual `hash_source_extension` engine.hpp:412 is overridable but source-named; the kernel default folds `"source:none"` engine_state_hash.cpp:19-21 **Closed:** `hash_host_extension(BrokerStateHashSink&)` engine.hpp:406 is the generic seam — protected virtual on `BacktestEngine`, called once per hash, last — and `hash_source_extension` engine.hpp:412 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,10 +174,10 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | OT2 | Symbol info, sessions, calendars, timezones (`timezone` pine_adapter.cpp:2737, `chart_timezone` pine_adapter.cpp:2780) | **yes** — spec fields `ticker` native_run_spec.hpp:636-646; `parse_session` native_calendar.hpp:180-467; intervals on every decision context `input_interval` market_driver.hpp:123-124 | K | — | F:J2 O:G-5 | — | | 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:2175, `NativeAbortReporting` native_run_spec.hpp:41-44 | K | — | F:J4 | (single-source) | -| OT5 | C-level run configuration, stream, FX curve, contract probe | **yes** — `strategy_configure_native_v1` c_abi.cpp:980, FX curve `strategy_configure_native_fx_curve_v1` c_abi.cpp:1051, probe `strategy_execution_contract` c_abi.cpp:971, stream `strategy_stream_begin` c_abi.cpp:698 **Extended:** `pf_native_run_spec_ext_v1`'s policy tail now carries `intrabar`, `path_order`, `abort_reporting` and the slot-label / feed-tolerance policies (`PF_NATIVE_SPEC_EXT_INTRABAR`, `PF_NATIVE_SPEC_EXT_FEED_POLICY`), so nothing of `NativeRunSpec` but `identity` is unreachable from C (`tests/test_native_c_api.c`). | K | — | O:J4 | The v1 C spec `pf_native_run_spec_v1` pineforge.h:510 omitted `intrabar`, `path_order` and `abort_reporting`; they now travel in the extension's policy tail, `pf_native_abort_reporting_e` native_c_api.h:911 among them (F §2.iii) | +| OT5 | C-level run configuration, stream, FX curve, contract probe | **yes** — `strategy_configure_native_v1` c_abi.cpp:981, FX curve `strategy_configure_native_fx_curve_v1` c_abi.cpp:1052, probe `strategy_execution_contract` c_abi.cpp:972, stream `strategy_stream_begin` c_abi.cpp:699 **Extended:** `pf_native_run_spec_ext_v1`'s policy tail now carries `intrabar`, `path_order`, `abort_reporting` and the slot-label / feed-tolerance policies (`PF_NATIVE_SPEC_EXT_INTRABAR`, `PF_NATIVE_SPEC_EXT_FEED_POLICY`), so nothing of `NativeRunSpec` but `identity` is unreachable from C (`tests/test_native_c_api.c`). | K | — | O:J4 | The v1 C spec `pf_native_run_spec_v1` pineforge.h:510 omitted `intrabar`, `path_order` and `abort_reporting`; they now travel in the extension's policy tail, `pf_native_abort_reporting_e` native_c_api.h:911 among them (F §2.iii) | | OT6 | C-level order submission + strategy callbacks | **no** — none of the original runtime `PF_API` symbols; then listed as a refusal on native-engine.md **Closed:** `strategy_native_host_create_v1` takes a C callback table and `strategy_native_submit_v1` / `_replace_v1` / `_cancel_v1` / `_cancel_all_v1` / `_cancel_where_v1` / `_execute_current_v1` command the same kernel under the same legality rule; the 41 `strategy_native_*` declarations plus two `strategy_configure_native_ext_*` declarations are a second, disjoint inventory beside the 81 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:341-376, `chart_index` pine_scheduler_native.cpp:588-721) | n/a | A | — | S:R1 | (single-source) **Closed (E25, lane E20's finding 2; E26, E25's findings 1 and 3):** `session.islastbar` reads one script bar ahead on every driving path: the chart timeframe's next retained bar, an aggregated chart's next bucket (a scheduler lookahead, deleted when the rule moved into the kernel), the calendar for the live tail's bar and for every bar of a stream, warmup included — a stream continues past its replay, so only a batch run's final bar reads "last" from having no successor. Since R4 slice C an aggregated or realtime bar read every in-session bar as last. **What TradingView does:** it ends a session at the session DAY — the flag belongs to the last chart bar whose successor belongs to another session day, in session or not, and `session.isfirstbar` is its dual, the bar after a session's last one. The ordinal is `internal::session_trading_day_index` timeframe.hpp:145, the exchange-timezone day rolling at the symbol's day stamp (09:30 ET on NYSE RTH, midnight on 24x7, 17:00 ET on a 1700-1700 forex session), not local midnight; the unmerged form, since a fused holiday daily bar still holds two intraday sessions. The three `lab tv` tapes (the private fixture set `session_islastbar`) give that rule bar for bar with no disagreement, and the engine now holds 255/255 NYSE:F last bars, 255/255 first bars and 370/370 ETH last bars on the chart-timeframe AND the aggregated path (it held 1, 0 and 1 before). Early closes come with it: the three NYSE:F half days flag 12:45 ET without a holiday calendar, because the next bar is the next session day's. **Generic since R5 lane F5:** the kernel computes that reading for every host — `NativeDecisionContext::in_session` market_driver.hpp:161 and the facts after it (`opens_session_day`, `closes_session_day`, `closes_session_day_open_ended`), in C the session bytes of `pf_native_decision_v1` native_c_api.h:1485 — from the run's own calendar and input (`present_session_day` native_execution_consumer.cpp:8273), so a bare host reproduces the tapes flag for flag (the private test `test_native_session_day_facts`, kernel-only). The adapter computes no regular-day rule of its own: `scheduler_update_session_state` pine_strategy_host.cpp:2228 selects the kernel's facts into the flags, and only widens the plain `session.isfirstbar` / `islastbar` pair to the chart's day on an extended-hours chart (R5 lane K-SESSION-WINDOWS F2: TradingView's `isfirstbar` / `islastbar` span the pre- and post-market bars there while the `_regular` pair keeps the regular day, `tests/test_session_extended_day_tape.cpp`), which are `PineStrategyHost` members now (`session_ismarket_` pine_strategy_host.hpp:1186; `BacktestEngine` holds no Pine session state), and only its live-probe tail reads the open-ended close at its batch's final bar (the private test `test_session_day_facts_adapter`). Selecting the kernel also ended a calc_on_order_fills defect of the adapter's lookahead, which read two bars ahead on a bar a fill recalculation had already published (a fill on a day's second-to-last bar flagged that bar last and the day's real last bar first). **Ruled, not open:** a bar with nothing held after it — a stream's bar, a live-tail bar — steps the calendar one width on, so it cannot see an early close the session string does not declare (ADR-0001, "Session-day facts at a bar with nothing held after it"). **Closed (F1):** a `calc_on_order_fills` recalculation reads the flags of the bar it runs on, not the previous bar's (the kernel presents the current bar's facts on that recalculation), and the close callback after it no longer looks one retained bar too far ahead (§3.8, AG3). | -| OT8 | *(quirk)* Live-tail / probe-suppress harness flags (`realtime_tail_` pine_strategy_host.cpp:300) | n/a — **contained since N14** (the flags were `BacktestEngine` members until then; now `source::PineStrategyHost` state behind two kernel virtual seams, §2.ii q) | A | N14 | F:J7 S:C5 | parity-campaign tooling | +| 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:341-376, `chart_index` pine_scheduler_native.cpp:588-721) | n/a | A | — | S:R1 | (single-source) **Closed (E25, lane E20's finding 2; E26, E25's findings 1 and 3):** `session.islastbar` reads one script bar ahead on every driving path: the chart timeframe's next retained bar, an aggregated chart's next bucket (a scheduler lookahead, deleted when the rule moved into the kernel), the calendar for the live tail's bar and for every bar of a stream, warmup included — a stream continues past its replay, so only a batch run's final bar reads "last" from having no successor. Since R4 slice C an aggregated or realtime bar read every in-session bar as last. **What TradingView does:** it ends a session at the session DAY — the flag belongs to the last chart bar whose successor belongs to another session day, in session or not, and `session.isfirstbar` is its dual, the bar after a session's last one. The ordinal is `internal::session_trading_day_index` timeframe.hpp:145, the exchange-timezone day rolling at the symbol's day stamp (09:30 ET on NYSE RTH, midnight on 24x7, 17:00 ET on a 1700-1700 forex session), not local midnight; the unmerged form, since a fused holiday daily bar still holds two intraday sessions. The three `lab tv` tapes (the private fixture set `session_islastbar`) give that rule bar for bar with no disagreement, and the engine now holds 255/255 NYSE:F last bars, 255/255 first bars and 370/370 ETH last bars on the chart-timeframe AND the aggregated path (it held 1, 0 and 1 before). Early closes come with it: the three NYSE:F half days flag 12:45 ET without a holiday calendar, because the next bar is the next session day's. **Generic since R5 lane F5:** the kernel computes that reading for every host — `NativeDecisionContext::in_session` market_driver.hpp:161 and the facts after it (`opens_session_day`, `closes_session_day`, `closes_session_day_open_ended`), in C the session bytes of `pf_native_decision_v1` native_c_api.h:1485 — from the run's own calendar and input (`present_session_day` native_execution_consumer.cpp:8273), so a bare host reproduces the tapes flag for flag (the private test `test_native_session_day_facts`, kernel-only). The adapter computes no regular-day rule of its own: `scheduler_update_session_state` pine_strategy_host.cpp:2237 selects the kernel's facts into the flags, and only widens the plain `session.isfirstbar` / `islastbar` pair to the chart's day on an extended-hours chart (R5 lane K-SESSION-WINDOWS F2: TradingView's `isfirstbar` / `islastbar` span the pre- and post-market bars there while the `_regular` pair keeps the regular day, `tests/test_session_extended_day_tape.cpp`), which are `PineStrategyHost` members now (`session_ismarket_` pine_strategy_host.hpp:1186; `BacktestEngine` holds no Pine session state), and only its live-probe tail reads the open-ended close at its batch's final bar (the private test `test_session_day_facts_adapter`). Selecting the kernel also ended a calc_on_order_fills defect of the adapter's lookahead, which read two bars ahead on a bar a fill recalculation had already published (a fill on a day's second-to-last bar flagged that bar last and the day's real last bar first). **Ruled, not open:** a bar with nothing held after it — a stream's bar, a live-tail bar — steps the calendar one width on, so it cannot see an early close the session string does not declare (ADR-0001, "Session-day facts at a bar with nothing held after it"). **Closed (F1):** a `calc_on_order_fills` recalculation reads the flags of the bar it runs on, not the previous bar's (the kernel presents the current bar's facts on that recalculation), and the close callback after it no longer looks one retained bar too far ahead (§3.8, AG3). | +| OT8 | *(quirk)* Live-tail / probe-suppress harness flags (`realtime_tail_` pine_strategy_host.cpp:302) | 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,7 +202,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | # | Item | Where / what it is used for | Class | Plan | Src | |---|---|---|---|---|---| -| a | `source::StrategyOverrides` opaque pointer | Forward declarations in the kernel headers (since removed: no kernel header names the adapter type now); parameter of `run_rich` (execution_consumer.hpp:46-55, native_execution_consumer.hpp:110-119) and of the rich `BacktestEngine::run` overload (engine.hpp:1797-1802, engine_consumer.cpp:81-93). The kernel only forwards it as `NativeBeginArgs::overrides_opaque` native_host.hpp:758; the source host casts it back `overrides_opaque` pine_strategy_host.cpp:307 | NEUTRAL in behaviour; a consumer-vtable signature change, so it rides the shared `engine_script_run` bump | `const void*` in the kernel signatures (F `host_begin_extras`, O `overrides_opaque`), typed convenience overload in `source/pine_strategy_host.hpp`; then drop the checker's single whitelist entry `test_native_example_batch` check_native_include_independence.py:42-46. S's alternative (`NativeRunOverrides`, or removing the rich overload) not adopted. Unverified: generated code calls the typed overload (codegen repo is not in this tree — F). **Status (L11, `3f4fff5b`): done** — `run_rich` and the rich `BacktestEngine::run` take `const void*` overrides, the rich begin carries them as `overrides_opaque`, and the independence checker whitelists nothing (§2.vi row 6) | F O S | +| a | `source::StrategyOverrides` opaque pointer | Forward declarations in the kernel headers (since removed: no kernel header names the adapter type now); parameter of `run_rich` (execution_consumer.hpp:46-55, native_execution_consumer.hpp:110-119) and of the rich `BacktestEngine::run` overload (engine.hpp:1797-1802, engine_consumer.cpp:81-93). The kernel only forwards it as `NativeBeginArgs::overrides_opaque` native_host.hpp:758; the source host casts it back `overrides_opaque` pine_strategy_host.cpp:308 | NEUTRAL in behaviour; a consumer-vtable signature change, so it rides the shared `engine_script_run` bump | `const void*` in the kernel signatures (F `host_begin_extras`, O `overrides_opaque`), typed convenience overload in `source/pine_strategy_host.hpp`; then drop the checker's single whitelist entry `test_native_example_batch` check_native_include_independence.py:42-46. S's alternative (`NativeRunOverrides`, or removing the rich overload) not adopted. Unverified: generated code calls the typed overload (codegen repo is not in this tree — F). **Status (L11, `3f4fff5b`): done** — `run_rich` and the rich `BacktestEngine::run` take `const void*` overrides, the rich begin carries them as `overrides_opaque`, and the independence checker whitelists nothing (§2.vi row 6) | F O S | | b | `pine_float_compare.hpp` | 1e-10 absolute equality band, used by kernel TA only: `float_band_le` ta_misc.cpp:30 (percentrank), `float_band_gt` ta_oscillators.cpp:462-463 (MFI), `plus_dm` ta_volatility_trend.cpp:337-340 (DMI); no execution TU includes it (O) | NEUTRAL (rename only) | Rename to a neutral header (F `float_band_compare.hpp`, O `ta_compare_band.hpp`), keep `pine_float_*` inline aliases in a source-layer header for generated code. Numerics must not move. S's behavioural `ComparePolicy` split is not neutral → not adopted (§4 U6). **Status (L11 + N14): done** — `ta_compare_band.hpp` is the kernel header; the alias shim is `include/pineforge/source/pine_float_compare.hpp` (N14 moved it off the kernel include root; it has no in-tree includer, generated code emits its own comparator) | F O S | | c | `NativeSlotLabelPolicy::LegacyTolerant`, `NativeLegacyTolerance` (now `NativeSlotLabelPolicy::FeedTolerant`, `NativeFeedTolerance`) | `NativeSlotLabelPolicy` native_run_spec.hpp:324-353; read by `legacy_tolerant_slot_labels` market_driver.cpp:55-56 and `native_feed_tolerance_enabled` market_driver.cpp:43-49, and by `legacy_tolerant_slot_labels` native_execution_consumer.hpp:1027-1029 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:2369; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:2386, `:10131`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2806. A feed-tolerance policy (raw strictly-increasing labels, zero / NaN-volume legacy bars), not TV semantics (O) | NEUTRAL | Rename with deprecated aliases (F `RawIncreasing` + `NativeInputTolerance::{StructuralBars, WarmupNonNegativeOHLC}`; O `NativeLabelPolicy::ProviderLabels`). Enumerator values stay 0/1 and 1/2: they are hashed `slot_label_policy` native_execution_consumer.cpp:273. **Status:** renamed as `FeedTolerant` / `NativeFeedTolerance` (the enumerator values unchanged), and the C transport carries them now, in the extension's feed-policy block (`PF_NATIVE_SPEC_EXT_FEED_POLICY` native_c_api.h:745) | F O S | | d | Orphaned comments | The KI-62 block of `engine_orders.cpp` (*fresh*: comment-only, the bodies are deleted; S read it as live code) and a comment of `engine.hpp`; wider deleted-declaration blocks in `engine.hpp`; the money-rule essay in `engine.hpp` (the code is `source_money_round` pine_adapter.cpp:558) | NEUTRAL | Delete, or move to `docs/` next to the adapter code that implements them. **Status: done** — R5 lanes E6 and E10 deleted the detached comment residue (a census of 0 over the kernel's compile closure) | F O | @@ -230,7 +230,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa 1.0 C surface still does not expose is the 1.0 C boundary table of `docs/pages/native-engine.md`. "Today" below is the tree L13 read.* -Today a C host can configure (`strategy_configure_native_v1` c_abi.cpp:980-1045), stream (`strategy_stream_*` c_abi.cpp:623-732) 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:981-1045), stream (`strategy_stream_*` c_abi.cpp:623-732) and read results, but strategy logic is C++-only and the batch entry points (`strategy_create`, `run_backtest*`) are per-strategy symbols a native author hand-writes. Design retained from F (most complete), with O's event polling and S's hardening rules: ```c typedef struct pf_native_decision_v1 { uint64_t ordinal; int32_t interval_index; /* SCRIPT-bar index */ int64_t effective_time_ms, @@ -327,7 +327,7 @@ Union of F §2.vi (9), O §2.vi (5 + 1) and S §2.6 (7), deduplicated. S had no | 5 | "`engine_aux_security.cpp` includes `source/pine_strategy_host.hpp`" (ADR L129-132) | False at `73817c1`: its includes are `engine_internal.hpp`, `ta.hpp` and std headers (*fresh* re-read; the run-failure codes added `run_failure.hpp`, a kernel header). No kernel TU includes a source header | `sha256:560a7f68b243d921382a69ed0390c398f28370ac066b84f30b5f96cbb385fb60` engine_aux_security.cpp:6-14 | F | | 6 | "Kernel TUs reference adapter types (`source::StrategyOverrides` …)" (ADR L129-131) | True only as a forward-declared opaque pointer the kernel never dereferences; the independence checker whitelists exactly that symbol | at `73817c1`; overtaken by `3f4fff5b` (§2.ii a), after which the rich begin carries `overrides_opaque` native_host.hpp:758 and the independence checker whitelists nothing | F O | | 7 | `engine.hpp` hosts "the ten-significant-digit money rule, KI-62, the `strategy.close` batching + entry-id ledger, a TradingView margin-call toggle" (ADR L131-134) | Two of four survive as code. Money rule: a comment block; the arithmetic is in the adapter. KI-62: orphaned comments + one live, hashed lot flag. Close batching + id ledger: gone from the kernel (the ledger is in the adapter; the old names survive only in comments). Live: `margin_call_enabled_`, the KI-62 flag and — unmentioned by the ADR — string-sentinel decoding of adapter comments | the money-rule block was lines 927-961 of `engine.hpp` and R5 lane E6 deleted it (lines 73-180 were and are live code), the arithmetic `source_money_round` pine_adapter.cpp:558 and its twin `tv_money_round` pine_policy_support.hpp:9-15; `skip_entry_bar_high` engine.hpp:153, `skip_entry_bar_low` engine_state_hash.cpp:74, `declare_opened_lot_entry_bar_mask` engine_path_resolve.cpp:95; the ledger `close_logical_units_` pine_adapter.hpp:2580; `closed_trade_close_cause` engine_trade_accessors.cpp:158. **Overtaken:** the margin-call toggle and the string decoding are both gone — see ADR-0001, "Current state vs. target" | F O | -| 8 | "No kernel-level C API. `c_abi.cpp` exports only version/descriptor symbols" (ADR L136-138) | 81 runtime `PF_API` symbols: native configure, the 12-symbol stream family, pending-order views, broker-state hash, security feeds, FX curve, reports. The 81 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:a60fa3c03ba568fefb8995ec149560ca7aac3eadcd2a4485315470484edefca4` c_abi.cpp:325-1013, `sha256:6327f1829019fdd172cd76a96e3d8c9c0af1610ad0dc9878640dda0ca21a686b` check_c_abi_runtime.py:20-81, c_abi.cpp:946, `sha256:5ff4c65a56410fe29b21cd68bc21ac87bdb34b77344e4f75f90efa8887fe48e8` c_abi.cpp:664-788, `sha256:051fe087eb9d30e9960dc4f0ab3f7cc837531ebeaa71b5871a3f1cdc22fb37c2` c_abi.cpp:566-641, `sha256:f18d46ac323234f7e3082367d30840b704b8a7548f7ae95ee8cff8714f29fd50` c_abi.cpp:545-562, `sha256:6a9ef4e688f33dc10ea8297d9eaa8144ea7923ef6ee6f8fe9ff20f6af4e9acb3` c_abi.cpp:881-917, c_abi.cpp:1013 | F O S | +| 8 | "No kernel-level C API. `c_abi.cpp` exports only version/descriptor symbols" (ADR L136-138) | 81 runtime `PF_API` symbols: native configure, the 12-symbol stream family, pending-order views, broker-state hash, security feeds, FX curve, reports. The 81 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:4f2fb1b736dfbd9de9477e82c45bfe8e1791b94cc2f2ab0d522cdea316b007d7` c_abi.cpp:325-1013, `sha256:6327f1829019fdd172cd76a96e3d8c9c0af1610ad0dc9878640dda0ca21a686b` check_c_abi_runtime.py:20-81, c_abi.cpp:946, `sha256:5ff4c65a56410fe29b21cd68bc21ac87bdb34b77344e4f75f90efa8887fe48e8` c_abi.cpp:664-788, `sha256:051fe087eb9d30e9960dc4f0ab3f7cc837531ebeaa71b5871a3f1cdc22fb37c2` c_abi.cpp:566-641, `sha256:f18d46ac323234f7e3082367d30840b704b8a7548f7ae95ee8cff8714f29fd50` c_abi.cpp:545-562, `sha256:6a9ef4e688f33dc10ea8297d9eaa8144ea7923ef6ee6f8fe9ff20f6af4e9acb3` c_abi.cpp:881-917, c_abi.cpp:1013 | F O S | | 9 | "No examples. `PINEFORGE_BUILD_EXAMPLES` is 'none yet'" (ADR L139) | The option is inert, but two Pine-free `NativeStrategyHost` examples exist, are compiled by the independence checker and tested through the live runner, only under `PINEFORGE_BUILD_LIVE_RUNNER`. Wording: "no top-level examples target" | at `73817c1` (overtaken; ADR-0001, "Current state vs. target") | F O S | | 10 | Follow-ups (1)-(4) (ADR L168-170) | Add native reporting, sizing, margin, HTF — and, per R5, calc-timing, price grid and order ergonomics. Without them "usable without Pine" is a build-system claim, not a feature claim | §3.5 | F | | 11 | Build layout | One `pineforge` library always appends the source list; a kernel-only target does not exist yet; `compat/pine/market_admission.cpp` sits in the main list, a boundary bug. **Overtaken:** `PINEFORGE_KERNEL_SOURCES` CMakeLists.txt:115 and `add_library` CMakeLists.txt:161 build `PineForge::kernel`, and the compat unit is in `PINEFORGE_SOURCE_LAYER_SOURCES` CMakeLists.txt:91 | `add_library` CMakeLists.txt:161 | S (F, O agree in §2.i) | @@ -414,7 +414,7 @@ closure markers and `scripts/check_design_inventory.py`). | L3 | `Sized{side, basis, time, grid_policy, reserve_percent_fee}` as the 6th `OrderIntent`; `ScopeFraction{fraction, claim}` in `ReductionSize` → A.2 | at L3 the adapter emitted neither. Since R2 it lowers a default cash / percent opening frozen at the signal onto `Sized{CashValue, AtAcceptance, SignalOnTick, ExplicitUnits}`, since N11 its placement-time quantity is the kernel's `native_sized_units`, and since R5 lane F7 a typed quantity (`qty_type` cash / percent) converts through the same query and a staged FX series keeps `Sized` wherever the acceptance coordinate converts at the source's rate (`tests/test_adapter_sizing_relower.cpp` cases 1-4, 8, 11, 12). It still never emits `ScopeFraction` (case 5) | percent-of-equity probe, zero fee, no grid: exact; with grid / fee: differences explained by SZ10 only (F). Differential: `Sized` vs a host-resolved `HostSized` on the same facts → identical `ExecutionAppliedEvent` (O). Gap, reversal, fee-bearing first lot; two 50 % siblings, partial close, pending parent, replacement (S) | | L4 | `NativeMarginModel`, `native_liquidation_price()`, `resolve_margin_call_units`, `on_native_margin_call`, `RequestDefinition::origin`, `MarginCallEvent` → A.3 | at L4 the adapter left `margin` unset. Since R5 `project()` declares a maintenance-only model (per-side `initial_*` 0, `maintenance_*` = `margin_long` / `margin_short`, `PathAdverseExtremeMark`, TradingView's equity basis) and answers its three hooks with TradingView's scheduling, money and slice (MG6, MG7), while every opening is still `AdmitWithHostMargin` (MG4) | first a plain maintenance breach in both hosts (S); then a margin-call probe reproduced with `ShortfallMultiple 4`: same bar, same side; quantity deltas itemized against MG15 (F, O) | | L5 | `NativeCalculationTrigger`, `on_native_recalculate(…, reason, cause)`, `on_native_sub_bar`, `current_partial_bar()`, `NativeOpenBarView` → A.4 | the adapter keeps `BarClose` + its own cascade; `born_on_remaining_path` (native_execution_consumer.cpp:6551) and `drain_applied_notifications` (native_execution_consumer.cpp:7234) 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 the host's `scheduler_prepare_security_sequence` (pine_strategy_host.cpp:1636) 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 the host's `scheduler_prepare_security_sequence` (pine_strategy_host.cpp:1645) untouched; corpus diff before / after (O: highest parity risk of any lane) | HTF-filter probe on a 24x7 symbol: exact (F); `"D"` over a 15m feed equals a `TimeframeAggregator` baseline bar for bar (O); installed-feed and no-feed cases (S) | | L7 | `native_working_requests()`, `cancel_all()`, `cancel_where(label)`, `TriggerAnchor`, `TrailTicks`, zero offset, `ReplaceOptions`; toolkit `submit_bracket`, `OrderBook` → A.6 | new kinds unused by the adapter; anchor defaults to `Absolute`; the adapter's `tick * 0.5` sentinel keeps working | bracket + trailing probe through the builder: exact where TR5 / FP7 are inactive (on-grid levels) (F); the zero-offset trail test asserts the run no longer fails (O); parent-limit + stop sibling, parent rejection, replacement, parent-cycle revival (S) | | L8 | `NativePriceGrid { None, QuantizeFills, QuantizeFillsAndTriggers }`, `NativeGridRounding` → A.7 | `None` for the adapter, permanently — **native-only by ruling, audit lane P6: §3.6.2** | a host with `QuantizeFills` books on-grid prices; a second test pins that `QuantizeFillsAndTriggers` still differs from TV's half-tick threshold (FP5) (O) | | L9 | `NativeRiskLimits`, `NativeRiskEvent`, `MatchRejectReason::RiskLimit` → A.8 | `risk` unset for the adapter; its ledger (`SourceDayLedger` pine_adapter.hpp:1423) untouched — **retained after measurement, audit lane N12; native-only by ruling, audit lane P6: §3.6.1** | risk probes: same halt bar (F); each limit + the day-boundary basis (O); breach, forced close, cancellation, next-day reset (S) | @@ -501,8 +501,8 @@ inside the per-bar body (corpus 442d497, `set_pine_risk_direction(1)` / `set_pine_risk_max_position_size(2)` / `set_pine_risk_max_intraday_filled_orders(3)`), so the adapter first sees a limit on script bar 0, from `PineStrategyHost::set_pine_risk_*` -(pine_strategy_host.cpp:886) — after `prepare_native_begin` has already -projected the spec (pine_strategy_host.cpp:243) and `configure_native` has +(pine_strategy_host.cpp:895) — 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 `max_drawdown=0 cap.active=0` for every risk probe. `NativeRunSpec::risk` is a @@ -557,8 +557,8 @@ Pine run needs the source change the patch makes, and the in-process pins remain the five probes' baseline rows and the `SW` check above. **The P6 decision: native-only.** Re-verified on main `b8e7976e`: the ordering -stands (`project()` at pine_strategy_host.cpp:350, the kernel's `configure` at :359, -the `set_pine_risk_*` setters (`set_pine_risk_direction` pine_strategy_host.cpp:886), and corpus 442d497 +stands (`project()` at pine_strategy_host.cpp:359, the kernel's `configure` at :368, +the `set_pine_risk_*` setters (`set_pine_risk_direction` pine_strategy_host.cpp:895), 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 @@ -1288,7 +1288,7 @@ engine.hpp:1668) is not. The other is a contract that the six folded fields of a booked row are never rewritten. In the tree they are not; the only writer after booking, the Pine host, rewrites `close_cause`, `exit_from_bracket`, `exit_comment` and `entry_incarnation` only; its -writes are `close_cause` pine_strategy_host.cpp:639, `exit_comment` :768, `close_cause` :782-787, `exit_from_bracket` :1288 and `entry_incarnation` :1469-1477. +writes are `close_cause` pine_strategy_host.cpp:648, `exit_comment` :777, `close_cause` :791-796, `exit_from_bracket` :1297 and `entry_incarnation` :1476-1486. But `trades_` engine.hpp:547 is a protected member any host can write, so a lane that takes the consumer-side digest has to make that contract explicit. Folding `(count, digest)` in place of the rows saves the 256-entry table but @@ -1320,8 +1320,8 @@ the bars cost 3.8 times the CPU with a round trip every ten bars script point, not after the run's teardown, so that it is one value with recording on or off (`broker_state_hash_projection` pine_strategy_host.cpp:166). `capture_script_continuation_hash` -pine_strategy_host.cpp:532 takes that snapshot at the last batch bar -(`last_batch` pine_strategy_host.cpp:613), and taking it is one full +pine_strategy_host.cpp:541 takes that snapshot at the last batch bar +(`last_batch` pine_strategy_host.cpp:622), 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 @@ -1604,9 +1604,9 @@ the rule gives 9.9 + 9.89). | E3 | **Sizing.** TV sizes from the tick-rounded signal close, 10-digit-rounded equity and fee-reserved cash, frozen at signal, with fill-time exceptions in COOF paths (`default_sizing_shape` pine_adapter.cpp:3191, the COOF `at_fill` exception pine_adapter.cpp:8404) | One "generic" percent sizing serving both would break parity or import TV rounding into the kernel; the last ulp differs on lot-stepped instruments | Two paths on purpose: `Sized` (kernel, plain arithmetic, timing as a first-class field) vs `HostSized` + the adapter resolver. Never "simplify" the adapter onto the kernel basis. What the two paths share is the conversion: since R5 lanes N11 and F7 the division by price × point value × FX and the percentage fee reserve exist once, in the kernel (`native_sized_units`), for a default AND a typed (`qty_type` cash / percent) quantity. The adapter keeps what is measured TradingView: the money basis (signal-time equity, TradingView's margin equity at a typed fill, the hypothetical Flatten's balance for a typed reversal), the freeze, the fill-time exceptions and both lot floors (`tests/test_adapter_sizing_relower.cpp` cases 8, 9, 11: kernel 0.0392 against the percent floor's 0.0391 on a 0.0001 lot), and a staged FX series keeps `Sized` wherever the acceptance coordinate converts at the source's rate (case 12) | F O S | | E4 | **Calc on fill.** Kernel suffix eligibility is continuous along the segment; TV refills only at O/H/L/C waypoints -- on a magnified path at each intrabar's open, twice, and its two extremes, never its close (R5 lane MAG-INTRABAR) (`PineScheduler::recalculate` pine_scheduler_native.cpp:745, `coof_next_waypoint` pine_adapter.cpp:6958) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:6551 and `drain_applied_notifications` (native_execution_consumer.cpp:7234); 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:22261) | The complete bar cannot be masked globally | Opt-in `OpenOnly` view (A.4); loud doc warning in L0 | F | -| E6 | **Double equity recording.** The source host already records per bar (history: only its cadence remains, `scheduler_mark_report_point` pine_strategy_host.cpp:2791) | Kernel recording would duplicate points and shift drawdown | Report policy / ownership (A.1); the adapter declares `KernelRecordedAtHostMarks` (R1): one recorder, at the adapter's own marks | F O | +| E6 | **Double equity recording.** The source host already records per bar (history: only its cadence remains, `scheduler_mark_report_point` pine_strategy_host.cpp:2800) | Kernel recording would duplicate points and shift drawdown | Report policy / ownership (A.1); the adapter declares `KernelRecordedAtHostMarks` (R1): one recorder, at the adapter's own marks | F O | | E7 | **Price grid.** TV does not merely quantize: it tests triggers against a quantized bar while booking the fill on the *unrounded* level, so the adapter shifts the threshold (`source_trigger_threshold` pine_adapter.cpp:384) and books elsewhere | A kernel grid that quantizes both sides is correct and not TV; moving rounding into the kernel would change both native results and the adapter | `NativePriceGrid::None` for the adapter permanently; never "unify". Before L8: a byte-level experiment that the default price spelling on decimal ticks (one-ULP representations) does not move (S) | O S | -| E8 | **HTF.** The evaluator machinery carries TV publication semantics (lookahead, boundary deferral, early close, `syminfo.type` branching engine_security.cpp:67-73). The feed store is TV-calibrated too: "W" / "M" from native daily bars, native stamps as the period partition (`set_native_security_feed` `sha256:eca3dbea0baa461982ba89f13d0a5c43277854069fb6eb2dd2f5c5471313204d` engine_aux_security.cpp:51-83, *fresh*). Two aggregators exist: script buckets use `native_calendar`, `request.security` uses `TimeframeAggregator`, whose completion rules are TV-pinned (timeframe.hpp:288-330) | Reuse risks changing the *order* of the Pine scheduler's pumps; a bare host inherits TV partition rules; native HTF may differ from Pine HTF on early-close sessions | Registration-gated pump; no change to `scheduler_feed_security_input` pine_strategy_host.cpp:1816; full corpus diff; supplied-bar provenance preserved, never silently replaced by chart aggregation (S); differences on early-close sessions are recorded, not "fixed" (F). The Pine scheduler keeps one more `TimeframeAggregator` pass, before the run (`PineScheduler::run_begin`, audit M26): the script-bar count it yields (`terminal_source_bar`, the probe-tail suppression) and the per-input completion flags it hands `on_native_input` are needed before the first input arrives, while the kernel seals script intervals only as inputs arrive and offers no pre-run interval query (`NativeExecutionConsumer::script_interval_at` is consumer-private). Retained by R5 lane F7: replacing it needs that kernel capability | F O S | +| E8 | **HTF.** The evaluator machinery carries TV publication semantics (lookahead, boundary deferral, early close, `syminfo.type` branching engine_security.cpp:67-73). The feed store is TV-calibrated too: "W" / "M" from native daily bars, native stamps as the period partition (`set_native_security_feed` `sha256:eca3dbea0baa461982ba89f13d0a5c43277854069fb6eb2dd2f5c5471313204d` engine_aux_security.cpp:51-83, *fresh*). Two aggregators exist: script buckets use `native_calendar`, `request.security` uses `TimeframeAggregator`, whose completion rules are TV-pinned (timeframe.hpp:288-330) | Reuse risks changing the *order* of the Pine scheduler's pumps; a bare host inherits TV partition rules; native HTF may differ from Pine HTF on early-close sessions | Registration-gated pump; no change to `scheduler_feed_security_input` pine_strategy_host.cpp:1825; full corpus diff; supplied-bar provenance preserved, never silently replaced by chart aggregation (S); differences on early-close sessions are recorded, not "fixed" (F). The Pine scheduler keeps one more `TimeframeAggregator` pass, before the run (`PineScheduler::run_begin`, audit M26): the script-bar count it yields (`terminal_source_bar`, the probe-tail suppression) and the per-input completion flags it hands `on_native_input` are needed before the first input arrives, while the kernel seals script intervals only as inputs arrive and offers no pre-run interval query (`NativeExecutionConsumer::script_interval_at` is consumer-private). Retained by R5 lane F7: replacing it needs that kernel capability | F O S | | E9 | **TA numerics are TV-calibrated** (1e-10 band in DMI / MFI / percentrank; conditional-call extremum ring `ExtremeRing` ta.hpp:134) | Non-Pine users may expect IEEE semantics; changing them breaks parity | Rename only (§2.ii b). For an every-bar caller the ring equals a positional window (the ring comment `sha256:fd1f92206558863ad8df33fd3ef3b8cc8b9bd6678a25fc98ca866168c1a81aca` ta.hpp:87-88). Document. → U6 | F O S | | E10 | **Dead-but-hashed lot flags** (§2.ii j) | Deleting them changes `broker_state_hash` | Separate, explicitly non-neutral lane (L12) | F O | | E11 | **OCA and ownership.** Pine's default-sized OCA and `from_entry` reservation rules are source policies | Source IDs, callsite tokens or Pine labels leaking into `WorkingRequestCore` | Opaque keys and receipts only (R5-4) | S | @@ -1623,7 +1623,7 @@ the rule gives 9.9 + 9.89). | P5 | **Test asymmetry:** 430 adapter test files vs 14 native when this plan was written (on this tree 543 `tests/*.cpp` name `source::PineStrategyHost`, 110 `NativeStrategyHost`) | native-only tests in every lane (§3.1 b) | O | | P6 | **Stream vs batch.** The stream refuses monthly input, mixed modes, in-session gaps, security feeds and source setters, and tick input under an FX curve (any non-empty curve until audit lane N6) (`native-engine.md`, "Stream limitations (current)") | L5 and L6 must state which refusals are generic and which are deliberate source limits | S | | P7 | **C ABI variant growth** | §2.iii hardening rules | S | -| P8 | **Unverified in the inputs:** whether `execution::LifecycleEffects` (execution.hpp:136-139) is ever non-empty from the Pine path, else `PlacementSnapshot::legs` (pine_adapter.hpp:364) is the only live lifecycle (O); the `strategy.risk.allow_entry_in` setter chain beyond `set_pine_risk_direction` pine_strategy_host.cpp:886 → `set_risk_direction` pine_adapter.cpp:26474 (O); what the ABI receipts pin (F); the probe corpus itself lives outside this tree (F) | verify inside the lane that touches each | F O | +| P8 | **Unverified in the inputs:** whether `execution::LifecycleEffects` (execution.hpp:136-139) is ever non-empty from the Pine path, else `PlacementSnapshot::legs` (pine_adapter.hpp:364) is the only live lifecycle (O); the `strategy.risk.allow_entry_in` setter chain beyond `set_pine_risk_direction` pine_strategy_host.cpp:895 → `set_risk_direction` pine_adapter.cpp:26474 (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) From 5e412f1f0693221fe336b613c3cca3eb150f37ff Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Mon, 19 Oct 2026 11:16:01 +0800 Subject: [PATCH 33/35] docs(cr150): retain valid m11 anchor --- 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 45446fde7..c7cfffd2d 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -1288,7 +1288,7 @@ engine.hpp:1668) is not. The other is a contract that the six folded fields of a booked row are never rewritten. In the tree they are not; the only writer after booking, the Pine host, rewrites `close_cause`, `exit_from_bracket`, `exit_comment` and `entry_incarnation` only; its -writes are `close_cause` pine_strategy_host.cpp:648, `exit_comment` :777, `close_cause` :791-796, `exit_from_bracket` :1297 and `entry_incarnation` :1476-1486. +writes are `close_cause` pine_strategy_host.cpp:648, `exit_comment` :777, `close_cause` :791-796, `exit_from_bracket` :1297 and `entry_incarnation` :1469-1477. But `trades_` engine.hpp:547 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 From 03405b622e73028bf2338b02298de27911f8c9a9 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Mon, 19 Oct 2026 11:22:49 +0800 Subject: [PATCH 34/35] docs(cr150): correct on_applied anchor for L19E fix round 6 The l19e-m10 doc-anchors log identifies the sole stale citation at native-feature-parity.md:1839. PineExecutionAdapter::on_applied starts at pine_adapter.cpp:24410 after the 16-line F0a insertion. Change only 24394 to 24410; preserve the row and all code/tests. --- 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 aa3da1e15..772729429 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -1836,7 +1836,7 @@ std::optional risk; // NativeRunSpec - Breach: `MatchRejectReason::RiskLimit` for openings (O; F: `RiskHalted`), an optional kernel-issued flatten through A.3's kernel-originated request (`origin = KernelRisk`, F), and a `NativeRiskEvent` in `native_events` (native_host.hpp:1335) (O) or a callback `on_native_risk_event` (S — a virtual, so only inside an epoch batch). The day key uses the existing calendar (`session_day_ordinal` native_calendar.hpp:416), not a chart clock. - S's `directions` / `max_units` / `max_position_units` are existing spec fields (MG1-MG3), not duplicated. -- Interim (F): a C++ host can build all four today from `native_marked_equity` + `on_native_applied`, which is what the adapter does (`update_risk_state` pine_adapter.cpp:17492-17500; `on_applied` pine_adapter.cpp:24394); 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 (`update_risk_state` pine_adapter.cpp:17492-17500; `on_applied` pine_adapter.cpp:24410); 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:3716 and the per-point walk), `native_order.hpp`, the state hash. ### A.9 Single-source proposals not adopted From 2dc35f0bb6c707e192a3c76cd25f7236b99caa5d Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Mon, 19 Oct 2026 11:32:22 +0800 Subject: [PATCH 35/35] docs: repair current native parity anchors --- docs/design/native-feature-parity.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index fe72d2183..8ec02312e 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -56,7 +56,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | OL7 | OCA cancel / reduce groups (`group_for` pine_adapter.cpp:3456) | **yes** — `Group` / `GroupEffect` native_order.hpp:466-473, `CancelReason::Group` native_order.hpp:948-953, `ReservationReducedEvent` native_order.hpp:1191-1201 (deferred form `DeferredGroupAdjustmentEvent` native_order.hpp:1211-1221) | K | — | F:A12 O:A6 O:A7 S:O7 | — | | OL8 | Immediate execution at the current point (`immediately=true`) | **yes** — `inspect_current_execution` / `execute_current` native_host.hpp:1104-1105 | K | — | F:A9 | (single-source row; O:F2 cites the same call) | | OL9 | Birth eligibility: an order accepted at bar N cannot fill on N's already-delivered points | **yes** — `point_eligible` native_order.hpp:786-790, the generic birth record `OrderBirth` include/pineforge/order_birth.hpp:59, contract `docs/pages/native-engine.md` ("Acceptance is not a fill") | K+A | — | F:A13 O:A10 S:R4 | **Disagree:** F yes / O partial / S "no, TV quirk". R5-1: the generic core (birth cursor) is native; TV's bar-granular first-bar rule is the A half → OL14. | -| OL10 | TP/SL bracket per entry, armed before the entry fills, OCA between legs (`exit()` pine_adapter.cpp:11048; lowering `submit_one` pine_adapter.cpp:11926, `BindCohort` pine_adapter.cpp:12389, `submit_leg` pine_adapter.cpp:11682; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1940); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | +| OL10 | TP/SL bracket per entry, armed before the entry fills, OCA between legs (`exit()` pine_adapter.cpp:11048; lowering `submit_one` pine_adapter.cpp:11926, `BindCohort` pine_adapter.cpp:12389, `submit_leg` pine_adapter.cpp:11682; 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:1953); 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:11307; `materialize_relative_exits` pine_adapter.cpp:13997, pine_adapter.hpp:2262-2274) | **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:6551. `Trigger` itself is absolute-only native_order.hpp:296 **Closed:** `Request::anchor = FromOwnerFill{offset, ticks, rounding}` (`TriggerAnchor` native_order.hpp:336) defers the level to the owner's fill, the kernel materializes it once at the arm under `NativeAnchorRounding`, and `resolve_anchored_level` native_host.hpp:1012 is where a source language's own level arithmetic lives (`tests/test_native_anchored_legs.cpp`, `tests/test_native_arm_options.cpp`). | K | L7 | F:A11 O:A11 S:O6 | **Disagree:** F partial (toolkit is enough) / O no (kernel `TriggerAnchor`). R5-2: L7 delivers relative anchors; kernel anchor primary, toolkit materialization is the zero-kernel fallback. | | OL12 | Working-order view for the host and outside observers (`strategy_pending_order_*`; `PendingIntentView` pine_adapter.hpp:1452-1487; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:27089) | **no** — base returns 0 / -1 `observe_last_bar_dual_entry_path_v1` engine_consumer.cpp:124-133; the POD is TV-shaped (`tv_carry_qty` pending_order_mirror.hpp:73, `pine_birth_reach` pending_order_mirror.hpp:135) **Closed for a bare host:** `native_working_requests()` native_host.hpp:1263 returns one owning `NativeWorkingRequest` native_host.hpp:681 per live request — definition, remaining, trigger state — with the C spelling `strategy_native_working_len_v1` / `_get_v1`. The TV-shaped POD stays frozen and stays the source layer's projection (`tests/test_native_limit_fill_through.cpp`, `tests/test_native_arm_options.cpp`, `tests/test_native_c_api.c`). | K+A | L7 (C++), L13 (C) | F:J5 O:J6 S:X4 | **Disagree:** O "partial + TV-shaped" / F, S no. A bare host reads zeros → no. R5-1: neutral view → kernel; POD shape and Pine journal retention stay A (POD frozen, §2.ii j). | | OL13 | *(quirk)* Same-bar command batching and the seven deferred queues (`pending_bracket_legs_` pine_adapter.hpp:2518-2537); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1366); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:4206, `materialize_snapshot` pine_adapter.cpp:13971), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1360-1387, `DeferredOpenMarketableSell` pine_adapter.hpp:1848-1871, `CloseCallsiteState` pine_adapter.hpp:1986-2000; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:21643, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:21877) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:676-678 | A | — | F:A15 O:A15 S:O9 S:R3 | R5-1 names KI-62. S: the kernel exposes deterministic insertion order only. | @@ -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:22278) | **yes** — `on_native_bar_open` native_host.hpp:889 runs before the open match native_execution_consumer.cpp:8067; `execute_current` legal native_host.hpp:1104 | K | — | F:E3 O:F2 | hazard → CT4 | | CT4 | Partial-bar view / open-bar lookahead guard | **no** — the open hook receives the *complete* script bar: `invoke_bar_open_callback(engine, bar, point)` native_execution_consumer.cpp:8010 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:8061 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1073 is the lookahead-free bar so far, through the last path point consumed (a fill inside a segment reads it at the segment's origin), and `NativeOpenBarView::OpenOnly` native_run_spec.hpp:109 masks `on_native_bar_open`'s bar to `H = L = C = open` as presentation only — the complete bar is restored before the open match (`tests/test_native_calc_timing.cpp`). | K | L5 | F:E3 | (single-source) The adapter needs the full bar, so the guard is opt-in. | | CT5 | Raw input observation before aggregation | **yes** — `on_native_input` native_host.hpp:875, `NativeInputContext` native_host.hpp:773-778 | K | — | O:F3 | (single-source row) | -| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:745, `coof_recalculation_due` pine_scheduler_native.cpp:711, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:866; `begin_coof_recalc` pine_adapter.cpp:6688, `flush_coof_tail` pine_adapter.cpp:7112, `end_coof_recalc` pine_adapter.cpp:6745) | **partial** — `on_native_applied` native_host.hpp:938 fires mid-path after each fill native_execution_consumer.cpp:7914, native_execution_consumer.cpp:7914; commands are legal there `commands_allowed` native_execution_consumer.cpp:1844; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:6551, `remaining_path_delivery` native_order.cpp:2082-2090. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | +| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:745, `coof_recalculation_due` pine_scheduler_native.cpp:711, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:866; `begin_coof_recalc` pine_adapter.cpp:6688, `flush_coof_tail` pine_adapter.cpp:7112, `end_coof_recalc` pine_adapter.cpp:6745) | **partial** — `on_native_applied` native_host.hpp:938 fires mid-path after each fill native_execution_consumer.cpp:7914, native_execution_consumer.cpp:7914; commands are legal there `commands_allowed` native_execution_consumer.cpp:1844; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:6551, `remaining_path_delivery` native_order.cpp:2094-2102. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | | CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:878 fires per accepted realtime print native_execution_consumer.cpp:8149; no per-tick calculation in batch; the guide pinned "close-only" (`docs/pages/native-engine.md`, "Batch OHLCV vs ticks vs quiet") **Closed:** `NativeCalculationTrigger::EveryModeledPoint` native_run_spec.hpp:97 adds one `Tick` recalculation at every modeled point of the delivered path and at every observed print, in batch and in a stream alike (`tests/test_native_calc_timing.cpp`). | K | L5 | F:E5 O:F4 S:C3 | **Disagree:** F yes / O partial / S no. R5-5: v1. Not an adapter feature either: absent from `PineStrategyConfig` pine_adapter.hpp:62-76, the runner rejects it (`calc_on_every_tick` main.cpp:259), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:295. | | CT8 | Historical intrabar calculation (per lower-TF sub-bar / magnifier sample) | **no** — with an `IntrabarPath` the callback still fires once per script bar native_execution_consumer.cpp:8744 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:928 fires once after each retained lower-timeframe sub-bar's whole matching path (`tests/test_native_calc_timing.cpp`). | K | L5 | F:E6 O:F8 S:C4 | — | | CT9 | Intrabar (magnifier) matching path + path-order policy (`volume_weighted_cap` pine_adapter.cpp:2938) | **yes** — `IntrabarPath` native_run_spec.hpp:362-403, delivery `deliver_confirmed_script` native_execution_consumer.cpp:8296-8562; `NativePathOrder` native_run_spec.hpp:335-339 | K+A | — | F:E7 F:G5 O:F7 S:C4 | **Disagree:** S partial (it wants callback cadence → CT8). TV sampler choice is A: TradingView's own intrabars at its table's timeframe, each owned by the chart bar of its last minute, built from the finer feed (`tradingview_magnifier_bars` magnifier_intrabars.hpp:46, R5 lane MAG-INTRABAR). | @@ -1836,7 +1836,7 @@ std::optional risk; // NativeRunSpec - Breach: `MatchRejectReason::RiskLimit` for openings (O; F: `RiskHalted`), an optional kernel-issued flatten through A.3's kernel-originated request (`origin = KernelRisk`, F), and a `NativeRiskEvent` in `native_events` (native_host.hpp:1335) (O) or a callback `on_native_risk_event` (S — a virtual, so only inside an epoch batch). The day key uses the existing calendar (`session_day_ordinal` native_calendar.hpp:416), not a chart clock. - S's `directions` / `max_units` / `max_position_units` are existing spec fields (MG1-MG3), not duplicated. -- Interim (F): a C++ host can build all four today from `native_marked_equity` + `on_native_applied`, which is what the adapter does (`update_risk_state` pine_adapter.cpp:17509; `on_applied` pine_adapter.cpp:24411; the opening admission block pine_adapter.cpp:17337-17345); 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 (`update_risk_state` pine_adapter.cpp:17509; `on_applied` pine_adapter.cpp:24411; the `opening_margin_checkpoint` block pine_adapter.cpp:17337-17345); 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:3716 and the per-point walk), `native_order.hpp`, the state hash. ### A.9 Single-source proposals not adopted