From 208f2b456a6ec83e5f0a8f858a2216db4cf01dd5 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sun, 18 Oct 2026 18:26:17 +0800 Subject: [PATCH 1/6] test(cr150): pin L8a ledger and retaining native behavior Red-first witness: the static assertion requires BacktestEngine closed rows to be TradeLedger; the P00 base still stores std::vector. Runtime cases cover the frozen API, global windows, ownership, tail rewrites, native reports and handle reuse. Expected failure is source-derived; no compile or test was run under the CORE-TOP Mac restriction. --- tests/CMakeLists.txt | 4 + tests/test_trade_ledger.cpp | 299 ++++++++++++++++++++++++++++++++++++ 2 files changed, 303 insertions(+) create mode 100644 tests/test_trade_ledger.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d11ecee74..4b2ec73ec 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 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/test_trade_ledger.cpp b/tests/test_trade_ledger.cpp new file mode 100644 index 000000000..7978854b9 --- /dev/null +++ b/tests/test_trade_ledger.cpp @@ -0,0 +1,299 @@ +// CR-150 L8a: exercise the frozen ledger contract and its retaining native path. +#include +#include +#include "native_current_fixture.hpp" + +#include +#include +#include +#include + +using namespace r4_test; + +namespace { + +struct LedgerProbe : Host { + using ClosedRows = decltype(trades_); + static_assert(std::is_same_v, + "L8a must replace the closed-row vector with TradeLedger"); + const auto& ledger() const { return trades_; } +}; + +bool same_double(double a, double b) { + return std::memcmp(&a, &b, sizeof(double)) == 0; +} + +Trade row(int n) { + Trade t{}; + t.entry_time = T + n * 60000LL; + t.exit_time = t.entry_time + 60000; + t.entry_price = 100.0 + n; + t.exit_price = 101.0 + n; + t.qty = 0.25 * (n + 1); + t.pnl = n % 3 == 0 ? -0.0 : (n % 3 == 1 ? -1.5 : 2.5); + t.pnl_pct = t.pnl / t.entry_price; + t.is_long = n % 2 == 0; + t.entry_bar_index = n; + t.exit_bar_index = n + 1; + t.entry_id = "entry-" + std::to_string(n) + std::string(40, 'a'); + t.entry_comment = std::string("entry\0comment", 13); + t.exit_id = "exit-" + std::to_string(n); + t.exit_comment = std::string(70, 'x') + std::to_string(n); + t.exit_from_bracket = n % 2 != 0; + t.max_runup = n + 0.5; + t.max_drawdown = n + 0.25; + t.commission = n * 0.125; + t.entry_incarnation = 1000 + n; + t.open_at_end = n % 5 == 0; + t.close_cause = ex::CloseCause::Bracket; + return t; +} + +void same_trade(const Trade& a, const Trade& b) { + CHECK(a.entry_time == b.entry_time); + CHECK(a.exit_time == b.exit_time); + CHECK(same_double(a.entry_price, b.entry_price)); + CHECK(same_double(a.exit_price, b.exit_price)); + CHECK(same_double(a.qty, b.qty)); + CHECK(same_double(a.pnl, b.pnl)); + CHECK(same_double(a.pnl_pct, b.pnl_pct)); + CHECK(a.is_long == b.is_long); + CHECK(a.entry_bar_index == b.entry_bar_index); + CHECK(a.exit_bar_index == b.exit_bar_index); + CHECK(a.entry_id == b.entry_id); + CHECK(a.entry_comment == b.entry_comment); + CHECK(a.exit_id == b.exit_id); + CHECK(a.exit_comment == b.exit_comment); + CHECK(a.exit_from_bracket == b.exit_from_bracket); + CHECK(same_double(a.max_runup, b.max_runup)); + CHECK(same_double(a.max_drawdown, b.max_drawdown)); + CHECK(same_double(a.commission, b.commission)); + CHECK(a.entry_incarnation == b.entry_incarnation); + CHECK(a.open_at_end == b.open_at_end); + CHECK(a.close_cause == b.close_cause); +} + +template +void invariant(F action) { + try { + action(); + CHECK(false); + } catch (const std::logic_error& error) { + CHECK(dynamic_cast(&error) != nullptr); + CHECK(classify_run_failure(error).code == RunFailureCode::engine_invariant); + } +} + +void matches(const TradeLedger& ledger, const std::vector& expected, + std::size_t base = 0) { + CHECK(ledger.base() == base); + REQUIRE(ledger.size() == expected.size()); + CHECK(ledger.empty() == expected.empty()); + std::size_t index = base; + for (const Trade& t : ledger) { + REQUIRE(index < expected.size()); + same_trade(t, expected[index]); + same_trade(ledger[index], expected[index]); + same_trade(ledger.at(index), expected[index]); + ++index; + } + CHECK(index == expected.size()); + if (base < expected.size()) same_trade(ledger.back(), expected.back()); +} + +void retaining_tail() { + TradeLedger ledger; + std::vector expected; + CHECK(ledger.empty()); + CHECK(ledger.begin() == ledger.end()); + ledger.reserve(8); + for (int n = 0; n < 513; ++n) { + Trade t = row(n); + expected.push_back(t); + if (n % 2 == 0) ledger.push_back(t); + else ledger.push_back(std::move(t)); + } + matches(ledger, expected); + ledger.at(64).exit_comment = expected.at(64).exit_comment = "amended"; + ledger[65].exit_time = expected[65].exit_time = T; + std::size_t index = 0; + for (Trade& t : ledger) t.exit_id = expected[index++].exit_id = "sorted-tail"; + ledger.resize(400); + expected.resize(400); + for (int n = 600; n < 630; ++n) { + ledger.push_back(row(n)); + expected.push_back(row(n)); + } + ledger.resize(435); + expected.resize(435); + matches(ledger, expected); + same_trade(ledger.back(), expected.back()); + ledger.clear(); + CHECK(ledger.size() == 0); + CHECK(ledger.base() == 0); + CHECK(ledger.empty()); + ledger.push_back(row(7)); + matches(ledger, {row(7)}); +} + +void global_window() { + TradeLedger ledger; + std::vector expected; + for (int n = 0; n < 9; ++n) { + ledger.push_back(row(n)); + expected.push_back(row(n)); + } + ledger.seal(3); + matches(ledger, expected, 3); + ledger.seal(3); + matches(ledger, expected, 3); + const TradeLedger& read = ledger; + invariant([&] { (void)ledger[2]; }); + invariant([&] { (void)read[2]; }); + invariant([&] { (void)ledger.at(2); }); + invariant([&] { (void)read.at(2); }); + invariant([&] { (void)ledger.at(9); }); + invariant([&] { (void)read.at(9); }); + invariant([&] { (void)ledger[9]; }); + invariant([&] { (void)read[9]; }); + invariant([&] { ledger.resize(2); }); + invariant([&] { ledger.seal(2); }); + invariant([&] { ledger.seal(10); }); + matches(ledger, expected, 3); + ledger.resize(5); + expected.resize(5); + ledger.push_back(row(20)); + expected.push_back(row(20)); + ledger.resize(7); + expected.resize(7); + matches(ledger, expected, 3); + ledger.seal(ledger.size()); + CHECK(ledger.size() == 7); + CHECK(ledger.base() == 7); + CHECK(!ledger.empty()); + CHECK(ledger.begin() == ledger.end()); + CHECK(read.begin() == read.end()); + invariant([&] { (void)ledger.back(); }); + invariant([&] { (void)read.back(); }); + ledger.reserve(4); + ledger.push_back(row(21)); + expected.push_back(row(21)); + matches(ledger, expected, 7); + ledger.clear(); + CHECK(ledger.base() == 0); + CHECK(ledger.empty()); + invariant([&] { (void)ledger.at(0); }); + invariant([&] { (void)ledger.back(); }); + ledger.resize(1); + matches(ledger, std::vector(1)); +} + +void copies_and_moves() { + TradeLedger original; + std::vector expected; + for (int n = 0; n < 5; ++n) { + original.push_back(row(n)); + expected.push_back(row(n)); + } + original.seal(2); + TradeLedger copy(original); + original.at(2).entry_id = "original changed"; + matches(copy, expected, 2); + TradeLedger assigned; + assigned.push_back(row(99)); + assigned = copy; + assigned = assigned; + matches(assigned, expected, 2); + copy[3].exit_comment = "copy changed"; + matches(assigned, expected, 2); + TradeLedger moved(std::move(assigned)); + matches(moved, expected, 2); + assigned.clear(); + assigned.push_back(row(50)); + matches(assigned, {row(50)}); + TradeLedger destination; + destination.push_back(row(100)); + destination = std::move(moved); + matches(destination, expected, 2); + moved.clear(); + moved.reserve(4); + moved.push_back(row(51)); + matches(moved, {row(51)}); + destination = std::move(destination); + matches(destination, expected, 2); +} + +struct Snapshot { + std::vector rows; + std::uint64_t hash; +}; + +Snapshot native_run(LedgerProbe& host, std::uint64_t run_number) { + host.beginning = [](Host& h) { h.calculations = 0; h.notified.clear(); }; + host.calculation = [](Host& h) { + apply(h, put(h, h.calculations % 2 == 1 ? tx(1.0) : flat())); + }; + auto config = spec("cr150-l8a"); + config.identity.run_number = run_number; + REQUIRE(host.configure_native(config).status == NativeSetupStatus::Applied); + std::vector bars; + for (int n = 0; n < 600; ++n) { + const double price = 100.0 + n; + bars.push_back({price, price, price, price, 1.0, T + n * 60000LL}); + } + host.run(bars.data(), static_cast(bars.size())); + completed(host); + CHECK(host.ledger().base() == 0); + REQUIRE(host.ledger().size() == 300); + const auto hash = host.broker_state_hash(); + ReportC first{}, repeated{}; + host.fill_report(&first); + host.fill_report(&repeated); + REQUIRE(first.total_trades == 300 && first.trades_len == 300); + REQUIRE(repeated.total_trades == 300 && repeated.trades_len == 300); + CHECK(first.net_profit == 300.0); + CHECK(repeated.net_profit == first.net_profit); + CHECK(first.equity_curve_len == static_cast(bars.size())); + CHECK(repeated.equity_curve_len == first.equity_curve_len); + for (int n = 0; n < first.trades_len; ++n) { + const Trade& t = host.ledger().at(static_cast(n)); + CHECK(first.trades[n].entry_time == t.entry_time); + CHECK(first.trades[n].exit_time == t.exit_time); + CHECK(same_double(first.trades[n].entry_price, t.entry_price)); + CHECK(same_double(first.trades[n].exit_price, t.exit_price)); + CHECK(same_double(first.trades[n].qty, t.qty)); + CHECK(same_double(first.trades[n].pnl, t.pnl)); + CHECK(same_double(first.trades[n].commission, t.commission)); + CHECK(same_double(repeated.trades[n].pnl, first.trades[n].pnl)); + CHECK(!t.open_at_end); + CHECK(t.entry_bar_index == 2 * n); + CHECK(t.exit_bar_index == 2 * n + 1); + } + CHECK(host.broker_state_hash() == hash); + BacktestEngine::free_report(&repeated); + BacktestEngine::free_report(&first); + return {{host.ledger().begin(), host.ledger().end()}, hash}; +} + +void native_retention_and_reuse() { + LedgerProbe reused, fresh; + const auto first = native_run(reused, 1); + const auto second = native_run(reused, 2); + const auto reference = native_run(fresh, 2); + CHECK(first.rows.size() == second.rows.size()); + REQUIRE(second.rows.size() == reference.rows.size()); + for (std::size_t n = 0; n < second.rows.size(); ++n) + same_trade(second.rows[n], reference.rows[n]); + CHECK(second.hash == reference.hash); +} + +} // namespace + +int main() { + test("retaining tail matches vector", retaining_tail); + test("global resident window", global_window); + test("copy and move ownership", copies_and_moves); + test("sink-off native retention and reuse", native_retention_and_reuse); + std::printf("test_trade_ledger: %d checks, %d failures\n", checks, failures); + return failures ? 1 : 0; +} From 8d24ff0e750e142ebbdbd6f99445ef268c1f1b73 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sun, 18 Oct 2026 18:33:51 +0800 Subject: [PATCH 2/6] feat(cr150): implement the retaining TradeLedger container Implement the frozen P00 ledger in engine_orders.cpp, swap the closed-row owner, and adapt digest/sort signatures without changing their folds or ordering. Keep production retention unchanged: no seal call is installed. Explicitly select kernel-recorded equity in the new native report witness. The out-of-row engine_execution.cpp reserve_effects vector caller and frozen oracle compatibility remain reported OPEN; no local compile or test was run. --- include/pineforge/engine.hpp | 3 +- src/engine_orders.cpp | 137 ++++++++++++++++++++++++++++++ src/native_execution_consumer.cpp | 4 +- src/native_execution_consumer.hpp | 4 +- src/source/pine_strategy_host.cpp | 6 +- tests/test_trade_ledger.cpp | 1 + 6 files changed, 147 insertions(+), 8 deletions(-) diff --git a/include/pineforge/engine.hpp b/include/pineforge/engine.hpp index 47f7f4ffa..511325489 100644 --- a/include/pineforge/engine.hpp +++ b/include/pineforge/engine.hpp @@ -34,6 +34,7 @@ #include "timeframe.hpp" #include "magnifier.hpp" #include "session_time.hpp" +#include // Suppress per-strategy function declarations (strategy_create, run_backtest, // etc.) whose pf_*_t parameter types conflict with the internal C++ types // used in codegen-emitted extern "C" blocks that include this header. @@ -543,7 +544,7 @@ class BacktestEngine { // else today (reset_run_state writes the NaN), so that fold never fires; // it stays because it is hashed broker state (engine_state_hash.cpp). double fold_exit_trail_peak_ = std::numeric_limits::quiet_NaN(); - std::vector trades_; + TradeLedger trades_; // Report-only rows for a position still open when the feed ends, produced // by NativeExecutionConsumer::record_open_position_report_rows when the // spec asks for them (NativeRunSpec::report_open_position_at_end). They diff --git a/src/engine_orders.cpp b/src/engine_orders.cpp index f82e84751..aabb37d39 100644 --- a/src/engine_orders.cpp +++ b/src/engine_orders.cpp @@ -12,6 +12,7 @@ #include "engine_internal.hpp" #include +#include #include #include @@ -325,4 +326,140 @@ void BacktestEngine::open_quoted_position(PositionSide requested, PyramidEntry l // at one point is the consumer's acceptance and incarnation ordinals // (ADR-0001, detached comment residue). +inline namespace engine_script_run_v19 { + +struct TradeLedger::State { + std::vector rows; + size_type first = 0; + + size_type offset(size_type global) const { + if (global < first || global - first >= rows.size()) + throw coded(RunFailureCode::engine_invariant, {}, + "trade ledger: row outside resident window"); + return global - first; + } +}; + +TradeLedger::TradeLedger() : state_(std::make_unique()) {} +TradeLedger::~TradeLedger() = default; + +TradeLedger::TradeLedger(const TradeLedger& other) + : state_(std::make_unique(*other.state_)) {} + +TradeLedger& TradeLedger::operator=(const TradeLedger& other) { + if (this != &other) { + auto next = std::make_unique(*other.state_); + state_.swap(next); + } + return *this; +} + +TradeLedger::TradeLedger(TradeLedger&& other) : TradeLedger() { + state_.swap(other.state_); +} + +TradeLedger& TradeLedger::operator=(TradeLedger&& other) { + if (this != &other) { + TradeLedger next(std::move(other)); + state_.swap(next.state_); + } + return *this; +} + +void TradeLedger::clear() { + state_->rows.clear(); + state_->first = 0; +} + +void TradeLedger::reserve(size_type count) { + // The hint is a resident row count. Keep the settlement preallocation's + // geometric growth inside the opaque storage, including its overflow guard. + auto& rows = state_->rows; + if (count > rows.max_size()) + throw std::length_error("execution effect capacity exhausted"); + if (count <= rows.capacity()) return; + const size_type grown = rows.capacity() > rows.max_size() / 2 + ? rows.max_size() : rows.capacity() * 2; + rows.reserve(std::max(count, grown)); +} + +TradeLedger::size_type TradeLedger::size() const { + return state_->first + state_->rows.size(); +} + +TradeLedger::size_type TradeLedger::base() const { return state_->first; } +bool TradeLedger::empty() const { return size() == 0; } + +Trade& TradeLedger::operator[](size_type global) { return at(global); } +const Trade& TradeLedger::operator[](size_type global) const { return at(global); } + +Trade& TradeLedger::at(size_type global) { + return state_->rows[state_->offset(global)]; +} + +const Trade& TradeLedger::at(size_type global) const { + return state_->rows[state_->offset(global)]; +} + +Trade& TradeLedger::back() { + if (state_->rows.empty()) + throw coded(RunFailureCode::engine_invariant, {}, + "trade ledger: no resident last row"); + return state_->rows.back(); +} + +const Trade& TradeLedger::back() const { + if (state_->rows.empty()) + throw coded(RunFailureCode::engine_invariant, {}, + "trade ledger: no resident last row"); + return state_->rows.back(); +} + +void TradeLedger::push_back(const Trade& trade) { + if (size() == std::numeric_limits::max()) + throw std::length_error("trade ledger: logical end exhausted"); + state_->rows.push_back(trade); +} + +void TradeLedger::push_back(Trade&& trade) { + if (size() == std::numeric_limits::max()) + throw std::length_error("trade ledger: logical end exhausted"); + state_->rows.push_back(std::move(trade)); +} + +void TradeLedger::resize(size_type global_end) { + if (global_end < state_->first) + throw coded(RunFailureCode::engine_invariant, {}, + "trade ledger: rewind below resident window"); + state_->rows.resize(global_end - state_->first); +} + +TradeLedger::iterator TradeLedger::begin() { return state_->rows.data(); } +TradeLedger::const_iterator TradeLedger::begin() const { return state_->rows.data(); } + +TradeLedger::iterator TradeLedger::end() { + auto& rows = state_->rows; + return rows.empty() ? rows.data() : rows.data() + rows.size(); +} + +TradeLedger::const_iterator TradeLedger::end() const { + const auto& rows = state_->rows; + return rows.empty() ? rows.data() : rows.data() + rows.size(); +} + +void TradeLedger::seal(size_type global_end) { + if (global_end < state_->first || global_end > size()) + throw coded(RunFailureCode::engine_invariant, {}, + "trade ledger: seal outside resident window"); + const size_type count = global_end - state_->first; + if (count == 0) return; + auto& rows = state_->rows; + rows.erase(rows.begin(), rows.begin() + + static_cast::difference_type>(count)); + state_->first = global_end; + if (rows.empty()) std::vector().swap(rows); +} + +} // inline namespace engine_script_run_v19 + } // namespace pineforge diff --git a/src/native_execution_consumer.cpp b/src/native_execution_consumer.cpp index e21d17b0e..6b034772f 100644 --- a/src/native_execution_consumer.cpp +++ b/src/native_execution_consumer.cpp @@ -11779,7 +11779,7 @@ void fold_closed_row(BrokerStateHashSink& f, const Trade& row) noexcept { } // namespace uint64_t NativeExecutionConsumer::closed_rows_digest( - const std::vector& rows) const { + const TradeLedger& rows) const { const std::size_t final_rows = std::min(closed_rows_final_, rows.size()); if (closed_rows_digested_ > final_rows) rewind_closed_rows(final_rows); BrokerStateHashSink f; @@ -11804,7 +11804,7 @@ void NativeExecutionConsumer::rewind_closed_rows(std::size_t first) const noexce } bool NativeExecutionConsumer::closed_rows_digest_holds( - const std::vector& rows) const noexcept { + const TradeLedger& rows) const noexcept { if (closed_rows_digested_ > rows.size()) return false; BrokerStateHashSink f; for (std::size_t index = 0; index < closed_rows_digested_; ++index) { diff --git a/src/native_execution_consumer.hpp b/src/native_execution_consumer.hpp index 7536b74b9..8cf94b6aa 100644 --- a/src/native_execution_consumer.hpp +++ b/src/native_execution_consumer.hpp @@ -362,8 +362,8 @@ class NativeExecutionConsumer final : public IExecutionConsumer { // definition for the finality rule. closed_rows_digest_holds re-folds the // rows the digest has taken and answers whether they still match: the // Debug check a run's end makes, and a witness's probe. - uint64_t closed_rows_digest(const std::vector& rows) const; - bool closed_rows_digest_holds(const std::vector& rows) const noexcept; + uint64_t closed_rows_digest(const TradeLedger& rows) const; + bool closed_rows_digest_holds(const TradeLedger& rows) const noexcept; // NativeStrategyHost::native_closed_rows_amended: the rows from `first` on // changed after they were final; the next read folds them again. void closed_rows_amended(std::size_t first) noexcept { rewind_closed_rows(first); } diff --git a/src/source/pine_strategy_host.cpp b/src/source/pine_strategy_host.cpp index 3d5f7cf65..7170915f1 100644 --- a/src/source/pine_strategy_host.cpp +++ b/src/source/pine_strategy_host.cpp @@ -1878,7 +1878,7 @@ void source::PineStrategyHost::scheduler_finish_security_sequence() { #endif } -static std::optional sort_same_bar_exit_trades(std::vector&, +static std::optional sort_same_bar_exit_trades(TradeLedger&, source::PineExecutionAdapter&); // sort_same_bar_exit_trades' first test, asked before the call (R5 lane D2-D): @@ -1887,7 +1887,7 @@ static std::optional sort_same_bar_exit_trades(std::vector&, // the group is one row, the sort moves nothing and the call answers nullopt. // A quiet-bar gate (pine_quiet_bar.hpp): the ungated reference calls the sort // at every publication. -static bool same_bar_exit_group_possible(const std::vector& trades) noexcept { +static bool same_bar_exit_group_possible(const TradeLedger& trades) noexcept { #if PINEFORGE_PINE_QUIET_BAR_GATES if (trades.size() < 2) return false; const Trade& last = trades[trades.size() - 1]; @@ -2178,7 +2178,7 @@ void source::PineStrategyHost::order_global_exit_children( // returned), so a sort that moves one answers the first row it moved, which // the caller hands to the kernel before the next hash read // (NativeStrategyHost::native_closed_rows_amended). -static std::optional sort_same_bar_exit_trades(std::vector& trades, +static std::optional sort_same_bar_exit_trades(TradeLedger& trades, source::PineExecutionAdapter& adapter) { if (trades.size() < 2) return std::nullopt; const std::size_t end = trades.size(); diff --git a/tests/test_trade_ledger.cpp b/tests/test_trade_ledger.cpp index 7978854b9..7e2078016 100644 --- a/tests/test_trade_ledger.cpp +++ b/tests/test_trade_ledger.cpp @@ -235,6 +235,7 @@ Snapshot native_run(LedgerProbe& host, std::uint64_t run_number) { }; auto config = spec("cr150-l8a"); config.identity.run_number = run_number; + config.report_policy = NativeReportPolicy::KernelRecorded; REQUIRE(host.configure_native(config).status == NativeSetupStatus::Applied); std::vector bars; for (int n = 0; n < 600; ++n) { From 01f8972ebb988bf0df60cbf81bb56702e9d55e26 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sun, 18 Oct 2026 18:34:02 +0800 Subject: [PATCH 3/6] test(cr150): adapt non-frozen helpers to TradeLedger Keep live helper references on the ledger and preserve snapshot copies through iterator ranges. Replace front reads with resident iteration and update the closed-row digest probe type. Keep numeric assertion literals, fixture values, oracle sources and hash pins unchanged. This is a mechanical caller migration under SCOPE-RULING-1818; no local compilation or tests were run. --- tests/native_current_fixture.hpp | 2 +- tests/test_adapter_fill_qty_probe.cpp | 2 +- tests/test_adapter_quiet_bar.cpp | 4 ++-- tests/test_adapter_sizing_relower.cpp | 4 ++-- tests/test_adapter_typed_entry_admission.cpp | 2 +- tests/test_carried_pooc_short_margin_state.cpp | 6 +++--- tests/test_carried_pooc_short_margin_state_l4a.cpp | 6 +++--- tests/test_close_id_retires_ledger.cpp | 2 +- tests/test_coof_chart_tick_touch.cpp | 2 +- tests/test_coof_chart_tick_touch_l4c.cpp | 2 +- tests/test_default_qty_signal_freeze.cpp | 6 +++--- tests/test_exit_leg_lifecycle_integration.cpp | 2 +- tests/test_explicit_qty_fill_admission.cpp | 2 +- tests/test_explicit_qty_fill_admission_l4b.cpp | 2 +- tests/test_famag_opening_money.cpp | 2 +- tests/test_famag_opening_money_l4b.cpp | 2 +- tests/test_frozen_flat_gap_reject.cpp | 2 +- tests/test_frozen_flat_gap_reject_l4d.cpp | 2 +- tests/test_high_value_price_admission.cpp | 4 ++-- tests/test_high_value_price_admission_l4d.cpp | 4 ++-- tests/test_high_value_signal_cost.cpp | 4 ++-- tests/test_high_value_signal_cost_l4d.cpp | 4 ++-- tests/test_integer_short_margin_state.cpp | 4 ++-- tests/test_intraday_order_session_day.cpp | 2 +- tests/test_intraday_order_session_day_l4a.cpp | 2 +- tests/test_l9a_carried_pooc_competing_checkpoint.cpp | 2 +- tests/test_margin_admission_gate.cpp | 2 +- tests/test_native_calc_identity.cpp | 2 +- tests/test_native_fx_activation.cpp | 4 ++-- tests/test_native_queued_reverse_to.cpp | 2 +- tests/test_native_state_continuation.cpp | 6 +++--- tests/test_open_money_before_priced_exit.cpp | 4 ++-- tests/test_open_money_before_priced_exit_l4b.cpp | 4 ++-- tests/test_pending_order_identity.cpp | 2 +- tests/test_percent_equity_open_entry_fee.cpp | 8 ++++---- tests/test_pine_transaction_settlement.cpp | 2 +- tests/test_placement_rejection_bracket_ownership.cpp | 2 +- tests/test_placement_rejection_bracket_ownership_l4c.cpp | 2 +- tests/test_pooc_long_money_before_trail.cpp | 4 ++-- tests/test_pooc_long_money_before_trail_l4b.cpp | 4 ++-- tests/test_pooc_money_admission.cpp | 2 +- tests/test_pooc_money_admission_l4b.cpp | 2 +- tests/test_pooc_open_money_event.cpp | 4 ++-- tests/test_pooc_open_money_event_l4b.cpp | 4 ++-- tests/test_range_end_close.cpp | 2 +- tests/test_range_end_close_l4d.cpp | 2 +- tests/test_replaced_percent_short_market.cpp | 2 +- tests/test_reversal_admission_float_guard.cpp | 2 +- tests/test_reversal_admission_float_guard_l4c.cpp | 2 +- tests/test_rounded_carried_short_trail.cpp | 4 ++-- tests/test_rounded_carried_short_trail_l4c.cpp | 4 ++-- tests/test_short_margin_script_state.cpp | 4 ++-- tests/test_sizing_basis_mintick.cpp | 2 +- tests/test_sizing_basis_mintick_l4b.cpp | 2 +- tests/test_small_money_margin_residual.cpp | 2 +- tests/test_stop_open_margin_script_state.cpp | 4 ++-- tests/test_taro_mc_close_residue.cpp | 2 +- tests/test_taro_price_gap_admission.cpp | 2 +- tests/test_taro_price_gap_admission_l4d.cpp | 2 +- 59 files changed, 88 insertions(+), 88 deletions(-) diff --git a/tests/native_current_fixture.hpp b/tests/native_current_fixture.hpp index cd7f85410..3126af1ff 100644 --- a/tests/native_current_fixture.hpp +++ b/tests/native_current_fixture.hpp @@ -43,7 +43,7 @@ struct Host : NativeStrategyHost { --depth; } const std::vector& lots() const { return pyramid_entries_; } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } int64_t cycle() const { return position_cycle_seq_; } double net() const { return net_profit_sum_; } double balance() const { return initial_capital_+net_profit_sum_; } diff --git a/tests/test_adapter_fill_qty_probe.cpp b/tests/test_adapter_fill_qty_probe.cpp index 603efc775..65e1b0421 100644 --- a/tests/test_adapter_fill_qty_probe.cpp +++ b/tests/test_adapter_fill_qty_probe.cpp @@ -121,7 +121,7 @@ class Probe : public pineforge::source::PineStrategyHost { rc = probe_fill_qty(index, row_.level, &probed, &close_only, &partition); } double held() const { return physical_position().signed_units; } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } private: Row row_; }; diff --git a/tests/test_adapter_quiet_bar.cpp b/tests/test_adapter_quiet_bar.cpp index 56a5ce327..27b247f9e 100644 --- a/tests/test_adapter_quiet_bar.cpp +++ b/tests/test_adapter_quiet_bar.cpp @@ -229,7 +229,7 @@ class QuietHost final : public source::PineStrategyHost { return execution_consumer().continuation_hash(); } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } std::vector boundary; std::vector quiet_allocations; @@ -793,7 +793,7 @@ Observed observe(const Scenario& s, bool magnifier, bool recording, bool portabl BacktestEngine::free_report(&report); out.final_hash = host.broker_state_hash(); if (!portable) out.continuation = host.continuation(); - out.trades = host.rows(); + out.trades.assign(host.rows().begin(), host.rows().end()); return out; } diff --git a/tests/test_adapter_sizing_relower.cpp b/tests/test_adapter_sizing_relower.cpp index fb40b7be6..d9603d82e 100644 --- a/tests/test_adapter_sizing_relower.cpp +++ b/tests/test_adapter_sizing_relower.cpp @@ -272,7 +272,7 @@ class PineProbe : public pineforge::source::PineStrategyHost { } using BacktestEngine::position_qty_; using BacktestEngine::position_side_; - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } private: double current_close_ = kNaN; }; @@ -1015,7 +1015,7 @@ class TypedProbe : public pineforge::source::PineStrategyHost { } if (bar_index_ == close_bar) strategy_close_all(); } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } }; // The kernel's conversion of `cash` at `price`, raw (ExplicitUnits), on the diff --git a/tests/test_adapter_typed_entry_admission.cpp b/tests/test_adapter_typed_entry_admission.cpp index 357befd4e..cd70eed26 100644 --- a/tests/test_adapter_typed_entry_admission.cpp +++ b/tests/test_adapter_typed_entry_admission.cpp @@ -144,7 +144,7 @@ class Host : public pineforge::source::PineStrategyHost { } if (bar_index_ == 4) strategy_close_all(); } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } private: Case case_; bool per_call_; diff --git a/tests/test_carried_pooc_short_margin_state.cpp b/tests/test_carried_pooc_short_margin_state.cpp index 8cd80c676..e76189775 100644 --- a/tests/test_carried_pooc_short_margin_state.cpp +++ b/tests/test_carried_pooc_short_margin_state.cpp @@ -52,7 +52,7 @@ class CarriedShort : public pineforge::source::PineStrategyHost { if (parked_entry && bar_index_ == 0) strategy_entry("Parked", true, 1.0, qnan, 0.001); if (bar_index_ == 3) { final_view = signed_position_size(); strategy_close_all(); } } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } }; const std::vector bars = { @@ -148,7 +148,7 @@ class FreshShort : public pineforge::source::PineStrategyHost { } if (bar_index_ == 1) { view = signed_position_size(); strategy_close_all(); } } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } private: bool stop_; }; @@ -206,7 +206,7 @@ class FullReplacement : public pineforge::source::PineStrategyHost { } if (bar_index_ == 3) strategy_close_all(); } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } }; void test_full_liquidation_retires_only_the_old_owned_bracket() { diff --git a/tests/test_carried_pooc_short_margin_state_l4a.cpp b/tests/test_carried_pooc_short_margin_state_l4a.cpp index e127e3557..cd65d9fc0 100644 --- a/tests/test_carried_pooc_short_margin_state_l4a.cpp +++ b/tests/test_carried_pooc_short_margin_state_l4a.cpp @@ -57,7 +57,7 @@ class CarriedShort : public pineforge::source::PineStrategyHost { if (parked_entry && bar_index_ == 0) strategy_entry("Parked", true, 1.0, qnan, 0.001); if (bar_index_ == 3) { final_view = signed_position_size(); strategy_close_all(); } } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } }; const std::vector bars = { @@ -153,7 +153,7 @@ class FreshShort : public pineforge::source::PineStrategyHost { } if (bar_index_ == 1) { view = signed_position_size(); strategy_close_all(); } } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } private: bool stop_; }; @@ -211,7 +211,7 @@ class FullReplacement : public pineforge::source::PineStrategyHost { } if (bar_index_ == 3) strategy_close_all(); } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } }; void test_full_liquidation_retires_only_the_old_owned_bracket() { diff --git a/tests/test_close_id_retires_ledger.cpp b/tests/test_close_id_retires_ledger.cpp index c25056170..2666bb62a 100644 --- a/tests/test_close_id_retires_ledger.cpp +++ b/tests/test_close_id_retires_ledger.cpp @@ -198,7 +198,7 @@ class Probe : public pineforge::source::PineStrategyHost { bool is_long() const { return position_side_ == PositionSide::LONG; } double pos() const { return position_qty_; } bool is_flat() const { return position_side_ == PositionSide::FLAT; } - const std::vector& all_trades() const { return trades_; } + const TradeLedger& all_trades() const { return trades_; } // Records of the ledger / position AFTER each bar's broker point. std::vector ledger_l35_after_bar; std::vector pos_after_bar; diff --git a/tests/test_coof_chart_tick_touch.cpp b/tests/test_coof_chart_tick_touch.cpp index 1cfccb2bf..b1fe76b96 100644 --- a/tests/test_coof_chart_tick_touch.cpp +++ b/tests/test_coof_chart_tick_touch.cpp @@ -65,7 +65,7 @@ class TickProbe : public pineforge::source::PineStrategyHost { if (bar_index_ == end_) strategy_close("", "END"); } void extra_order() { extra_order_ = true; } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } double remaining() const { return position_qty_; } int observed_after_exit = 0; int exit_seen_bar = -1; diff --git a/tests/test_coof_chart_tick_touch_l4c.cpp b/tests/test_coof_chart_tick_touch_l4c.cpp index d8e7e8aaf..aa2c0fb43 100644 --- a/tests/test_coof_chart_tick_touch_l4c.cpp +++ b/tests/test_coof_chart_tick_touch_l4c.cpp @@ -69,7 +69,7 @@ class TickProbe : public pineforge::source::PineStrategyHost { if (bar_index_ == end_) strategy_close("", "END"); } void extra_order() { extra_order_ = true; } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } double remaining() const { return position_qty_; } int observed_after_exit = 0; int exit_seen_bar = -1; diff --git a/tests/test_default_qty_signal_freeze.cpp b/tests/test_default_qty_signal_freeze.cpp index ba5eb7cbb..1e0707492 100644 --- a/tests/test_default_qty_signal_freeze.cpp +++ b/tests/test_default_qty_signal_freeze.cpp @@ -111,7 +111,7 @@ class Probe : public pineforge::source::PineStrategyHost { } using BacktestEngine::position_qty_; using BacktestEngine::position_side_; - const std::vector& all_trades() const { return trades_; } + const TradeLedger& all_trades() const { return trades_; } }; // A. Percent-of-equity reversal, close(S) != open(S+1). @@ -285,7 +285,7 @@ class OcaProbe : public pineforge::source::PineStrategyHost { } } using BacktestEngine::position_side_; - const std::vector& all_trades() const { return trades_; } + const TradeLedger& all_trades() const { return trades_; } }; void test_oca_default_sibling_cancelled() { @@ -337,7 +337,7 @@ class ReversalBindProbe : public pineforge::source::PineStrategyHost { } } using BacktestEngine::position_side_; - const std::vector& all_trades() const { return trades_; } + const TradeLedger& all_trades() const { return trades_; } }; void test_reversal_bracket_binding_survives_freeze() { diff --git a/tests/test_exit_leg_lifecycle_integration.cpp b/tests/test_exit_leg_lifecycle_integration.cpp index 54a13435d..b2984488d 100644 --- a/tests/test_exit_leg_lifecycle_integration.cpp +++ b/tests/test_exit_leg_lifecycle_integration.cpp @@ -29,7 +29,7 @@ class Book:public pineforge::source::PineStrategyHost { void suspend(const char* id){lifecycle_fixture::suspend(get(id));} void margin(double p){++bar_index_;current_bar_={p,p,p,p,1,bar_index_*60000LL};process_margin_call(current_bar_);} double qty()const{return position_qty_;} - const std::vector& rows()const{return trades_;} + const TradeLedger& rows()const{return trades_;} private:bool buy_; }; void old_stop(bool buy,bool marketable,bool remove_current){ diff --git a/tests/test_explicit_qty_fill_admission.cpp b/tests/test_explicit_qty_fill_admission.cpp index adc6d5a14..34b744104 100644 --- a/tests/test_explicit_qty_fill_admission.cpp +++ b/tests/test_explicit_qty_fill_admission.cpp @@ -147,7 +147,7 @@ class Probe : public pineforge::source::PineStrategyHost { using BacktestEngine::position_side_; double position_size() const { return signed_position_size(); } std::string exit_comment(int i) const { return closed_trade_exit_comment(i); } - const std::vector& all_trades() const { return trades_; } + const TradeLedger& all_trades() const { return trades_; } }; // RED-1. Flat explicit all-in (qty = equity/close = 100), zero commission, no diff --git a/tests/test_explicit_qty_fill_admission_l4b.cpp b/tests/test_explicit_qty_fill_admission_l4b.cpp index d32f83f71..cc10674b9 100644 --- a/tests/test_explicit_qty_fill_admission_l4b.cpp +++ b/tests/test_explicit_qty_fill_admission_l4b.cpp @@ -148,7 +148,7 @@ class Probe : public pineforge::source::PineStrategyHost { using BacktestEngine::position_side_; double position_size() const { return signed_position_size(); } std::string exit_comment(int i) const { return closed_trade_exit_comment(i); } - const std::vector& all_trades() const { return trades_; } + const TradeLedger& all_trades() const { return trades_; } }; // RED-1. Flat explicit all-in (qty = equity/close = 100), zero commission, no diff --git a/tests/test_famag_opening_money.cpp b/tests/test_famag_opening_money.cpp index a48df1b6d..ad25985df 100644 --- a/tests/test_famag_opening_money.cpp +++ b/tests/test_famag_opening_money.cpp @@ -78,7 +78,7 @@ class OpeningMoneyProbe : public pineforge::source::PineStrategyHost { if (bar_index_ == flatten_bar_) strategy_close("Next", "END"); } - std::vector rows() const { return trades_; } + std::vector rows() const { return {trades_.begin(), trades_.end()}; } double remaining_position() const { return signed_position_size(); } private: diff --git a/tests/test_famag_opening_money_l4b.cpp b/tests/test_famag_opening_money_l4b.cpp index 0fd15a498..2423f7ec9 100644 --- a/tests/test_famag_opening_money_l4b.cpp +++ b/tests/test_famag_opening_money_l4b.cpp @@ -79,7 +79,7 @@ class OpeningMoneyProbe : public pineforge::source::PineStrategyHost { if (bar_index_ == flatten_bar_) strategy_close("Next", "END"); } - std::vector rows() const { return trades_; } + std::vector rows() const { return {trades_.begin(), trades_.end()}; } double remaining_position() const { return signed_position_size(); } private: diff --git a/tests/test_frozen_flat_gap_reject.cpp b/tests/test_frozen_flat_gap_reject.cpp index adfca2f0b..a8c35f534 100644 --- a/tests/test_frozen_flat_gap_reject.cpp +++ b/tests/test_frozen_flat_gap_reject.cpp @@ -123,7 +123,7 @@ class Probe : public pineforge::source::PineStrategyHost { std::string exit_comment(int i) const { return closed_trade_exit_comment(i); } - const std::vector& all_trades() const { return trades_; } + const TradeLedger& all_trades() const { return trades_; } }; // RED-4. SHORT true-flat, zero commission, gap ABOVE the signal close (open diff --git a/tests/test_frozen_flat_gap_reject_l4d.cpp b/tests/test_frozen_flat_gap_reject_l4d.cpp index c912b100f..76f921909 100644 --- a/tests/test_frozen_flat_gap_reject_l4d.cpp +++ b/tests/test_frozen_flat_gap_reject_l4d.cpp @@ -136,7 +136,7 @@ class Probe : public pineforge::source::PineStrategyHost { std::string exit_comment(int i) const { return closed_trade_exit_comment(i); } - const std::vector& all_trades() const { return trades_; } + const TradeLedger& all_trades() const { return trades_; } }; // RED-4. SHORT true-flat, zero commission, gap ABOVE the signal close (open diff --git a/tests/test_high_value_price_admission.cpp b/tests/test_high_value_price_admission.cpp index 362a05c88..c05a087f8 100644 --- a/tests/test_high_value_price_admission.cpp +++ b/tests/test_high_value_price_admission.cpp @@ -53,7 +53,7 @@ class Reversal : public pineforge::source::PineStrategyHost { } if (bar_index_ == 2) { after = signed_position_size(); strategy_close_all(); } } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } }; void execute(Reversal& engine) { @@ -136,7 +136,7 @@ class Flat : public pineforge::source::PineStrategyHost { if (bar_index_ == 0) strategy_entry("New", is_long); if (bar_index_ == 1) { after = signed_position_size(); strategy_close_all(); } } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } }; void test_flat_across_price_scales() { diff --git a/tests/test_high_value_price_admission_l4d.cpp b/tests/test_high_value_price_admission_l4d.cpp index 861b0f2dc..cd26c1d81 100644 --- a/tests/test_high_value_price_admission_l4d.cpp +++ b/tests/test_high_value_price_admission_l4d.cpp @@ -66,7 +66,7 @@ class Reversal : public pineforge::source::PineStrategyHost { } if (bar_index_ == 2) { after = signed_position_size(); strategy_close_all(); } } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } }; void execute(Reversal& engine) { @@ -149,7 +149,7 @@ class Flat : public pineforge::source::PineStrategyHost { if (bar_index_ == 0) strategy_entry("New", is_long); if (bar_index_ == 1) { after = signed_position_size(); strategy_close_all(); } } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } }; void test_flat_across_price_scales() { diff --git a/tests/test_high_value_signal_cost.cpp b/tests/test_high_value_signal_cost.cpp index 48893acb0..ec47a4d8a 100644 --- a/tests/test_high_value_signal_cost.cpp +++ b/tests/test_high_value_signal_cost.cpp @@ -46,7 +46,7 @@ class Reversal : public pineforge::source::PineStrategyHost { } if (bar_index_ == 2) { observed = signed_position_size(); strategy_close_all(); } } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } }; void test_reversal_signal_cost_boundary() { @@ -123,7 +123,7 @@ class Flat : public pineforge::source::PineStrategyHost { } if (bar_index_ == 1) { observed = signed_position_size(); strategy_close_all(); } } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } }; void test_flat_signal_cost_across_price_scales() { diff --git a/tests/test_high_value_signal_cost_l4d.cpp b/tests/test_high_value_signal_cost_l4d.cpp index a572c840d..de0600361 100644 --- a/tests/test_high_value_signal_cost_l4d.cpp +++ b/tests/test_high_value_signal_cost_l4d.cpp @@ -59,7 +59,7 @@ class Reversal : public pineforge::source::PineStrategyHost { } if (bar_index_ == 2) { observed = signed_position_size(); strategy_close_all(); } } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } }; void test_reversal_signal_cost_boundary() { @@ -136,7 +136,7 @@ class Flat : public pineforge::source::PineStrategyHost { } if (bar_index_ == 1) { observed = signed_position_size(); strategy_close_all(); } } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } }; void test_flat_signal_cost_across_price_scales() { diff --git a/tests/test_integer_short_margin_state.cpp b/tests/test_integer_short_margin_state.cpp index 56b8c60f4..578bb6d2d 100644 --- a/tests/test_integer_short_margin_state.cpp +++ b/tests/test_integer_short_margin_state.cpp @@ -89,7 +89,7 @@ class IntegerScript : public pineforge::source::PineStrategyHost { } if (bar_index_ == 5) strategy_close_all(); } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } }; const std::vector bars = { @@ -271,7 +271,7 @@ class DormantCheckpoint : public pineforge::source::PineStrategyHost { void on_source_bar(const Bar&) override {} void checkpoint() { process_short_margin_before_script(current_bar_); } void late_margin() { process_margin_call(current_bar_); } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } double quantity() const { return position_qty_; } std::size_t pending() const { return pending_orders_.size(); } }; diff --git a/tests/test_intraday_order_session_day.cpp b/tests/test_intraday_order_session_day.cpp index 77dc3c12f..9d3ecab39 100644 --- a/tests/test_intraday_order_session_day.cpp +++ b/tests/test_intraday_order_session_day.cpp @@ -44,7 +44,7 @@ class SessionOrders : public pineforge::source::PineStrategyHost { strategy_exit("X", "L", 101.0, std::numeric_limits::quiet_NaN()); } } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } }; std::vector session_bars(int64_t day, int reset_hour) { diff --git a/tests/test_intraday_order_session_day_l4a.cpp b/tests/test_intraday_order_session_day_l4a.cpp index 4873ee6d9..a613a6f88 100644 --- a/tests/test_intraday_order_session_day_l4a.cpp +++ b/tests/test_intraday_order_session_day_l4a.cpp @@ -47,7 +47,7 @@ class SessionOrders : public pineforge::source::PineStrategyHost { strategy_exit("X", "L", 101.0, std::numeric_limits::quiet_NaN()); } } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } }; std::vector session_bars(int64_t day, int reset_hour) { diff --git a/tests/test_l9a_carried_pooc_competing_checkpoint.cpp b/tests/test_l9a_carried_pooc_competing_checkpoint.cpp index dcea443d3..6368e8bf7 100644 --- a/tests/test_l9a_carried_pooc_competing_checkpoint.cpp +++ b/tests/test_l9a_carried_pooc_competing_checkpoint.cpp @@ -68,7 +68,7 @@ class CarriedShort : public pineforge::source::PineStrategyHost { if (parked_entry && bar_index_ == 0) strategy_entry("Parked", true, 1.0, qnan, 0.001); if (bar_index_ == 3) { final_view = signed_position_size(); strategy_close_all(); } } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } }; const std::vector bars = { diff --git a/tests/test_margin_admission_gate.cpp b/tests/test_margin_admission_gate.cpp index 80643620f..ce9fdd29a 100644 --- a/tests/test_margin_admission_gate.cpp +++ b/tests/test_margin_admission_gate.cpp @@ -132,7 +132,7 @@ class Probe : public pineforge::source::PineStrategyHost { using BacktestEngine::initial_capital_; using BacktestEngine::margin_long_; using BacktestEngine::margin_short_; - const std::vector& all_trades() const { return trades_; } + const TradeLedger& all_trades() const { return trades_; } }; static void run_constant_100_script(Probe& eng, const std::string& script) { diff --git a/tests/test_native_calc_identity.cpp b/tests/test_native_calc_identity.cpp index 0c969493d..e34cf5115 100644 --- a/tests/test_native_calc_identity.cpp +++ b/tests/test_native_calc_identity.cpp @@ -78,7 +78,7 @@ class Host final : public NativeStrategyHost { if (bar == 4) submit_market({execution::Flatten{}, "x", ""}); } NativeExecutionConsumer& consumer() { return as_native_consumer(execution_consumer()); } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } }; std::vector minute_bars(int n) { diff --git a/tests/test_native_fx_activation.cpp b/tests/test_native_fx_activation.cpp index c482f579e..8c9faf791 100644 --- a/tests/test_native_fx_activation.cpp +++ b/tests/test_native_fx_activation.cpp @@ -239,7 +239,7 @@ void a_f3_fee_and_a_f4_batch_rate_facts() { REQUIRE(rates.size() >= 2); CHECK(rates[0] == 2.0 && rates[1] == 2.0); CHECK(host.rows().size() == 1); - CHECK(host.rows().front().commission > 0.0); + CHECK(host.rows().begin()->commission > 0.0); struct PercentRow { double at_entry = 0.0; @@ -278,7 +278,7 @@ void a_f3_fee_and_a_f4_batch_rate_facts() { percent.run(&bar, 1); completed(percent); REQUIRE(percent.rows().size() == 1); - out.commission = percent.rows().front().commission; + out.commission = percent.rows().begin()->commission; return out; }; diff --git a/tests/test_native_queued_reverse_to.cpp b/tests/test_native_queued_reverse_to.cpp index fe5b3ca6a..7d639b724 100644 --- a/tests/test_native_queued_reverse_to.cpp +++ b/tests/test_native_queued_reverse_to.cpp @@ -225,7 +225,7 @@ void a_s2_absorbed_close_opposite_flattens_entire_roster() { CHECK(bits(applied.closed_units) == bits(1e16)); CHECK(bits(applied.filled_working) == bits(1e16)); REQUIRE(h.rows().size() == 1); - CHECK(bits(h.rows().front().qty) == bits(1e16)); + CHECK(bits(h.rows().begin()->qty) == bits(1e16)); REQUIRE(h.lots().size() == 1); CHECK(h.lots().front().entry_incarnation == 222); CHECK(bits(h.lots().front().qty) == bits(.1)); diff --git a/tests/test_native_state_continuation.cpp b/tests/test_native_state_continuation.cpp index 1995944ca..1ec557a83 100644 --- a/tests/test_native_state_continuation.cpp +++ b/tests/test_native_state_continuation.cpp @@ -388,7 +388,7 @@ struct NativeExecutionConsumerProbe { } static std::uint64_t precommit_count(const C& c) { return c.precommit_digest_.count; } - static bool holds(const C& c, const std::vector& rows) { + static bool holds(const C& c, const TradeLedger& rows) { return c.closed_rows_digest_holds(rows); } }; @@ -465,8 +465,8 @@ struct Host : NativeStrategyHost { return verdict; } NativeExecutionConsumer& consumer() { return as_native_consumer(execution_consumer()); } - const std::vector& rows() const { return trades_; } - std::vector& mutable_rows() { return trades_; } + const TradeLedger& rows() const { return trades_; } + TradeLedger& mutable_rows() { return trades_; } }; bool run(Host& host, const NativeRunSpec& spec, int bars) { diff --git a/tests/test_open_money_before_priced_exit.cpp b/tests/test_open_money_before_priced_exit.cpp index ba6045879..03f304c19 100644 --- a/tests/test_open_money_before_priced_exit.cpp +++ b/tests/test_open_money_before_priced_exit.cpp @@ -73,7 +73,7 @@ class Probe : public pineforge::source::PineStrategyHost { } if (bar_index_ == 5 && mode_ == Mode::Half) strategy_close_all(); } - const std::vector& closed() const { return trades_; } + const TradeLedger& closed() const { return trades_; } }; bool near(double a, double b, double eps = 1e-8) { return std::abs(a-b) < eps; } void check_priced_exit(Mode mode, bool call, double price) { @@ -147,7 +147,7 @@ class BoundaryProbe : public pineforge::source::PineStrategyHost { pyramid_entries_.push_back(entry); strategy_exit("Resting", "Boundary", 11.0, 9.0); } - const std::vector& closed() const { return trades_; } + const TradeLedger& closed() const { return trades_; } double position() const { return signed_position_size(); } }; void check_original_money_scope_is_preserved() { diff --git a/tests/test_open_money_before_priced_exit_l4b.cpp b/tests/test_open_money_before_priced_exit_l4b.cpp index a69a5c1e2..49c01ae91 100644 --- a/tests/test_open_money_before_priced_exit_l4b.cpp +++ b/tests/test_open_money_before_priced_exit_l4b.cpp @@ -74,7 +74,7 @@ class Probe : public pineforge::source::PineStrategyHost { } if (bar_index_ == 5 && mode_ == Mode::Half) strategy_close_all(); } - const std::vector& closed() const { return trades_; } + const TradeLedger& closed() const { return trades_; } }; bool near(double a, double b, double eps = 1e-8) { return std::abs(a-b) < eps; } void check_priced_exit(Mode mode, bool call, double price) { @@ -148,7 +148,7 @@ class BoundaryProbe : public pineforge::source::PineStrategyHost { pyramid_entries_.push_back(entry); strategy_exit("Resting", "Boundary", 11.0, 9.0); } - const std::vector& closed() const { return trades_; } + const TradeLedger& closed() const { return trades_; } double position() const { return signed_position_size(); } }; void check_original_money_scope_is_preserved() { diff --git a/tests/test_pending_order_identity.cpp b/tests/test_pending_order_identity.cpp index 10730c776..1f9b2f728 100644 --- a/tests/test_pending_order_identity.cpp +++ b/tests/test_pending_order_identity.cpp @@ -173,7 +173,7 @@ class Book final : public pineforge::source::PineStrategyHost { uint64_t fills() const { return broker_fill_event_seq_; } double size() const { return position_qty_; } PositionSide side() const { return position_side_; } - const std::vector& closed() const { return trades_; } + const TradeLedger& closed() const { return trades_; } void expect_A(bool long_side) const { REQUIRE(position_side_ == PositionSide::FLAT); REQUIRE(broker_fill_event_seq_ == 2); diff --git a/tests/test_percent_equity_open_entry_fee.cpp b/tests/test_percent_equity_open_entry_fee.cpp index 55f6768a9..ae93a5fab 100644 --- a/tests/test_percent_equity_open_entry_fee.cpp +++ b/tests/test_percent_equity_open_entry_fee.cpp @@ -98,10 +98,10 @@ class FeeEquityProbe : public pineforge::source::PineStrategyHost { int closed_count() const { return static_cast(trades_.size()); } double first_closed_qty() const { return trades_.empty() ? std::numeric_limits::quiet_NaN() - : trades_.front().qty; + : trades_.begin()->qty; } std::string first_exit_comment() const { - return trades_.empty() ? std::string() : trades_.front().exit_comment; + return trades_.empty() ? std::string() : trades_.begin()->exit_comment; } private: @@ -236,11 +236,11 @@ class Ki56TvOracleProbe : public pineforge::source::PineStrategyHost { int closed_count() const { return static_cast(trades_.size()); } double first_closed_qty() const { - return trades_.empty() ? kNaN : trades_.front().qty; + return trades_.empty() ? kNaN : trades_.begin()->qty; } double remaining_qty() const { return position_qty_; } std::string first_exit_comment() const { - return trades_.empty() ? std::string() : trades_.front().exit_comment; + return trades_.empty() ? std::string() : trades_.begin()->exit_comment; } private: diff --git a/tests/test_pine_transaction_settlement.cpp b/tests/test_pine_transaction_settlement.cpp index d862a12d1..639953756 100644 --- a/tests/test_pine_transaction_settlement.cpp +++ b/tests/test_pine_transaction_settlement.cpp @@ -55,7 +55,7 @@ class Book : public pineforge::source::PineStrategyHost { : position_side_ == PositionSide::LONG ? position_qty_ : -position_qty_; } uint64_t fills() const { return broker_fill_event_seq_; } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } const std::vector& lots() const { return pyramid_entries_; } }; diff --git a/tests/test_placement_rejection_bracket_ownership.cpp b/tests/test_placement_rejection_bracket_ownership.cpp index 3f3ec00a1..d381c761c 100644 --- a/tests/test_placement_rejection_bracket_ownership.cpp +++ b/tests/test_placement_rejection_bracket_ownership.cpp @@ -73,7 +73,7 @@ class Probe : public pineforge::source::PineStrategyHost { if (mode_ != Mode::NoReversal) strategy_entry("Attempt", true); } } - const std::vector& closed() const { return trades_; } + const TradeLedger& closed() const { return trades_; } double position() const { return signed_position_size(); } }; bool near(double a, double b, double eps = 1e-8) { diff --git a/tests/test_placement_rejection_bracket_ownership_l4c.cpp b/tests/test_placement_rejection_bracket_ownership_l4c.cpp index f0e71342c..02ba88872 100644 --- a/tests/test_placement_rejection_bracket_ownership_l4c.cpp +++ b/tests/test_placement_rejection_bracket_ownership_l4c.cpp @@ -77,7 +77,7 @@ class Probe : public pineforge::source::PineStrategyHost { if (mode_ != Mode::NoReversal) strategy_entry("Attempt", true); } } - const std::vector& closed() const { return trades_; } + const TradeLedger& closed() const { return trades_; } double position() const { return signed_position_size(); } }; bool near(double a, double b, double eps = 1e-8) { diff --git a/tests/test_pooc_long_money_before_trail.cpp b/tests/test_pooc_long_money_before_trail.cpp index 29d7fd450..e11a5c273 100644 --- a/tests/test_pooc_long_money_before_trail.cpp +++ b/tests/test_pooc_long_money_before_trail.cpp @@ -56,7 +56,7 @@ class LongTrail : public pineforge::source::PineStrategyHost { if (foreign) strategy_exit("SX", "S", qnan, qnan, 0.001, 0.001); if (bar_index_ == 3) strategy_close_all(); } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } }; void test_rounding_slice_precedes_resolved_trail() { @@ -142,7 +142,7 @@ void test_later_waypoint_cannot_precede_the_trail_fill() { engine.run(bars.data(), static_cast(bars.size())); CHECK(engine.rows().size() == (no_exit ? 2u : 1u)); if (engine.rows().empty()) continue; - const auto& first = engine.rows().front(); + const auto& first = *engine.rows().begin(); CHECK(first.exit_time == 2000); if (no_exit) { CHECK(first.exit_id == "__margin_call__"); diff --git a/tests/test_pooc_long_money_before_trail_l4b.cpp b/tests/test_pooc_long_money_before_trail_l4b.cpp index efca24967..d3ba64ef6 100644 --- a/tests/test_pooc_long_money_before_trail_l4b.cpp +++ b/tests/test_pooc_long_money_before_trail_l4b.cpp @@ -57,7 +57,7 @@ class LongTrail : public pineforge::source::PineStrategyHost { if (foreign) strategy_exit("SX", "S", qnan, qnan, 0.001, 0.001); if (bar_index_ == 3) strategy_close_all(); } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } }; void test_rounding_slice_precedes_resolved_trail() { @@ -143,7 +143,7 @@ void test_later_waypoint_cannot_precede_the_trail_fill() { engine.run(bars.data(), static_cast(bars.size())); CHECK(engine.rows().size() == (no_exit ? 2u : 1u)); if (engine.rows().empty()) continue; - const auto& first = engine.rows().front(); + const auto& first = *engine.rows().begin(); CHECK(first.exit_time == 2000); if (no_exit) { CHECK(first.exit_id == "__margin_call__"); diff --git a/tests/test_pooc_money_admission.cpp b/tests/test_pooc_money_admission.cpp index 6af5f503e..8be329f22 100644 --- a/tests/test_pooc_money_admission.cpp +++ b/tests/test_pooc_money_admission.cpp @@ -62,7 +62,7 @@ class Probe : public pineforge::source::PineStrategyHost { if (bar_index_>0 && position_side_!=PositionSide::FLAT) strategy_close("L"); } double live_qty() const { return position_qty_; } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } }; void run_case(const Case& c, int cap, QtyType defaults) { Probe p(c,cap,defaults); diff --git a/tests/test_pooc_money_admission_l4b.cpp b/tests/test_pooc_money_admission_l4b.cpp index ffde5e0e9..a57516ac5 100644 --- a/tests/test_pooc_money_admission_l4b.cpp +++ b/tests/test_pooc_money_admission_l4b.cpp @@ -63,7 +63,7 @@ class Probe : public pineforge::source::PineStrategyHost { if (bar_index_>0 && position_side_!=PositionSide::FLAT) strategy_close("L"); } double live_qty() const { return position_qty_; } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } }; void run_case(const Case& c, int cap, QtyType defaults) { Probe p(c,cap,defaults); diff --git a/tests/test_pooc_open_money_event.cpp b/tests/test_pooc_open_money_event.cpp index 5c1b884a3..982272de3 100644 --- a/tests/test_pooc_open_money_event.cpp +++ b/tests/test_pooc_open_money_event.cpp @@ -81,7 +81,7 @@ class Probe : public pineforge::source::PineStrategyHost { strategy_close("", "SURVIVOR"); } } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } double remaining() const { return position_qty_; } void literal_shortfall_at_entry() { // Deliberately seed a post-admission book with a real cash deficit. @@ -137,7 +137,7 @@ void covered_control(bool coof, bool funded, int slip = 2, int pyramid_limit = 0 CHECK(script.recalc == (coof && fire)); CHECK(script.at_open == (coof && fire)); if (fire) { - const auto& call = p.rows().front(); + const auto& call = *p.rows().begin(); CHECK(call.exit_comment == "Margin call"); CHECK(call.exit_id == "__margin_call__"); CHECK(call.qty == 1.0); diff --git a/tests/test_pooc_open_money_event_l4b.cpp b/tests/test_pooc_open_money_event_l4b.cpp index 7f26f90a2..cc14b167f 100644 --- a/tests/test_pooc_open_money_event_l4b.cpp +++ b/tests/test_pooc_open_money_event_l4b.cpp @@ -94,7 +94,7 @@ class Probe : public pineforge::source::PineStrategyHost { strategy_close("", "SURVIVOR"); } } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } double remaining() const { return position_qty_; } void literal_shortfall_at_entry() { // Drive the base fixture's post-admission deficit through a public RAW @@ -139,7 +139,7 @@ void covered_control(bool coof, bool funded, int slip = 2, int pyramid_limit = 0 CHECK(script.recalc == (coof && fire)); CHECK(script.at_open == (coof && fire)); if (fire) { - const auto& call = p.rows().front(); + const auto& call = *p.rows().begin(); CHECK(call.exit_comment == "Margin call"); CHECK(call.exit_id == "__margin_call__"); CHECK(call.qty == 1.0); diff --git a/tests/test_range_end_close.cpp b/tests/test_range_end_close.cpp index dc7901018..64d01719f 100644 --- a/tests/test_range_end_close.cpp +++ b/tests/test_range_end_close.cpp @@ -133,7 +133,7 @@ class Probe : public pineforge::source::PineStrategyHost { } using BacktestEngine::position_side_; using BacktestEngine::position_qty_; - const std::vector& all_trades() const { return trades_; } + const TradeLedger& all_trades() const { return trades_; } const std::vector& range_end_rows() const { return range_end_trades_; } const std::vector& curve() const { return equity_curve_; } double max_dd() const { return max_drawdown_; } diff --git a/tests/test_range_end_close_l4d.cpp b/tests/test_range_end_close_l4d.cpp index 3aca47cbb..27467f790 100644 --- a/tests/test_range_end_close_l4d.cpp +++ b/tests/test_range_end_close_l4d.cpp @@ -146,7 +146,7 @@ class Probe : public pineforge::source::PineStrategyHost { } using BacktestEngine::position_side_; using BacktestEngine::position_qty_; - const std::vector& all_trades() const { return trades_; } + const TradeLedger& all_trades() const { return trades_; } const std::vector& range_end_rows() const { return range_end_trades_; } const std::vector& curve() const { return equity_curve_; } double max_dd() const { return max_drawdown_; } diff --git a/tests/test_replaced_percent_short_market.cpp b/tests/test_replaced_percent_short_market.cpp index 03e55a56d..44ad2e643 100644 --- a/tests/test_replaced_percent_short_market.cpp +++ b/tests/test_replaced_percent_short_market.cpp @@ -87,7 +87,7 @@ class Probe : public pineforge::source::PineStrategyHost { seen.push_back({position_side_,position_qty_, pyramid_entries_.empty() ? "" : pyramid_entries_.front().entry_id, trades_.size()}); - closed = trades_; + closed.assign(trades_.begin(), trades_.end()); } }; std::vector feed(bool touch = false) { diff --git a/tests/test_reversal_admission_float_guard.cpp b/tests/test_reversal_admission_float_guard.cpp index 73043bcb3..b96a01251 100644 --- a/tests/test_reversal_admission_float_guard.cpp +++ b/tests/test_reversal_admission_float_guard.cpp @@ -140,7 +140,7 @@ class Probe : public pineforge::source::PineStrategyHost { using BacktestEngine::position_qty_; using BacktestEngine::position_side_; using BacktestEngine::qty_step_; - const std::vector& all_trades() const { return trades_; } + const TradeLedger& all_trades() const { return trades_; } }; // Shared reversal fixture. Long all-in fills at 100 on bar 1 and the opposite diff --git a/tests/test_reversal_admission_float_guard_l4c.cpp b/tests/test_reversal_admission_float_guard_l4c.cpp index bb7bb65c0..535a4e14f 100644 --- a/tests/test_reversal_admission_float_guard_l4c.cpp +++ b/tests/test_reversal_admission_float_guard_l4c.cpp @@ -144,7 +144,7 @@ class Probe : public pineforge::source::PineStrategyHost { using BacktestEngine::position_qty_; using BacktestEngine::position_side_; using BacktestEngine::qty_step_; - const std::vector& all_trades() const { return trades_; } + const TradeLedger& all_trades() const { return trades_; } }; // Shared reversal fixture. Long all-in fills at 100 on bar 1 and the opposite diff --git a/tests/test_rounded_carried_short_trail.cpp b/tests/test_rounded_carried_short_trail.cpp index d86118c16..36ef9e7da 100644 --- a/tests/test_rounded_carried_short_trail.cpp +++ b/tests/test_rounded_carried_short_trail.cpp @@ -75,7 +75,7 @@ class RoundedShort : public pineforge::source::PineStrategyHost { if (bar_index_ == 4) after_action_view = signed_position_size(); if (bar_index_ == 6) strategy_close_all(); } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } }; void test_margin_is_visible_before_script_actions() { @@ -164,7 +164,7 @@ class ExcursionShort : public pineforge::source::PineStrategyHost { strategy_exit("SX", "S", qnan, qnan, qnan, 1.0, 60000.0); if (bar_index_ == 2) strategy_close_all(); } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } }; void test_margin_excursion_samples_only_the_traversed_prefix() { diff --git a/tests/test_rounded_carried_short_trail_l4c.cpp b/tests/test_rounded_carried_short_trail_l4c.cpp index f44a665bf..9f3e79570 100644 --- a/tests/test_rounded_carried_short_trail_l4c.cpp +++ b/tests/test_rounded_carried_short_trail_l4c.cpp @@ -79,7 +79,7 @@ class RoundedShort : public pineforge::source::PineStrategyHost { if (bar_index_ == 4) after_action_view = signed_position_size(); if (bar_index_ == 6) strategy_close_all(); } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } }; void test_margin_is_visible_before_script_actions() { @@ -168,7 +168,7 @@ class ExcursionShort : public pineforge::source::PineStrategyHost { strategy_exit("SX", "S", qnan, qnan, qnan, 1.0, 60000.0); if (bar_index_ == 2) strategy_close_all(); } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } }; void test_margin_excursion_samples_only_the_traversed_prefix() { diff --git a/tests/test_short_margin_script_state.cpp b/tests/test_short_margin_script_state.cpp index 3c0e26637..b2c29f903 100644 --- a/tests/test_short_margin_script_state.cpp +++ b/tests/test_short_margin_script_state.cpp @@ -60,7 +60,7 @@ class ScriptView : public pineforge::source::PineStrategyHost { } if (bar_index_ == 3) strategy_close_all(); } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } }; const std::vector bars = { @@ -182,7 +182,7 @@ class CarriedView : public pineforge::source::PineStrategyHost { } if (bar_index_ == 5) strategy_close_all(); } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } }; void test_carried_liquidation_script_state() { diff --git a/tests/test_sizing_basis_mintick.cpp b/tests/test_sizing_basis_mintick.cpp index 02bc88477..75c17f5e2 100644 --- a/tests/test_sizing_basis_mintick.cpp +++ b/tests/test_sizing_basis_mintick.cpp @@ -102,7 +102,7 @@ class Probe : public pineforge::source::PineStrategyHost { double nearest(double p) const { return round_to_mintick(p); } using BacktestEngine::position_qty_; using BacktestEngine::position_side_; - const std::vector& all_trades() const { return trades_; } + const TradeLedger& all_trades() const { return trades_; } }; // A. Invented midpoint: 64.035 / 0.01 = 6403.499999999999, so the diff --git a/tests/test_sizing_basis_mintick_l4b.cpp b/tests/test_sizing_basis_mintick_l4b.cpp index 7f1b8e3b7..b37a9051e 100644 --- a/tests/test_sizing_basis_mintick_l4b.cpp +++ b/tests/test_sizing_basis_mintick_l4b.cpp @@ -117,7 +117,7 @@ class Probe : public pineforge::source::PineStrategyHost { double nearest(double p) const { return round_to_mintick(p); } using BacktestEngine::position_qty_; using BacktestEngine::position_side_; - const std::vector& all_trades() const { return trades_; } + const TradeLedger& all_trades() const { return trades_; } }; // A. Invented midpoint: 64.035 / 0.01 = 6403.499999999999, so the diff --git a/tests/test_small_money_margin_residual.cpp b/tests/test_small_money_margin_residual.cpp index 05f079810..04a801815 100644 --- a/tests/test_small_money_margin_residual.cpp +++ b/tests/test_small_money_margin_residual.cpp @@ -68,7 +68,7 @@ class ResidualProbe : public pineforge::source::PineStrategyHost { } if (bar_index_ == 1) strategy_close("L", "survivor"); } - const std::vector& closed() const { return trades_; } + const TradeLedger& closed() const { return trades_; } private: double realized_; double quantity_; diff --git a/tests/test_stop_open_margin_script_state.cpp b/tests/test_stop_open_margin_script_state.cpp index 2f935072a..b77973399 100644 --- a/tests/test_stop_open_margin_script_state.cpp +++ b/tests/test_stop_open_margin_script_state.cpp @@ -68,7 +68,7 @@ class StopBook : public pineforge::source::PineStrategyHost { if (carried_half && bar_index_ == 2) strategy_close("Short", "carry half", qnan, 50.0); if (bar_index_ == 4) strategy_close_all(); } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } }; void test_full_stop_liquidation_and_replacement() { @@ -157,7 +157,7 @@ class PathAndLifetime : public pineforge::source::PineStrategyHost { } if (preserve && signed_position_size() > 0) strategy_close_all(); } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } }; void test_prior_high_and_pending_entry_lifetime() { diff --git a/tests/test_taro_mc_close_residue.cpp b/tests/test_taro_mc_close_residue.cpp index 7c2fd9e36..309a05b1e 100644 --- a/tests/test_taro_mc_close_residue.cpp +++ b/tests/test_taro_mc_close_residue.cpp @@ -51,7 +51,7 @@ class Probe:public pineforge::source::PineStrategyHost{ } if(bar_index_==(cfg.shape==Shape::Earlier?5:3))strategy_close("S","END"); } - const std::vector& rows()const{return trades_;} + const TradeLedger& rows()const{return trades_;} // Consume the real queued order after a synthetic between-event state // change. The initial two-bar run performs the actual margin call. void consume(const Bar& b,int index){bar_index_=index;current_bar_=b;process_pending_orders(b);} diff --git a/tests/test_taro_price_gap_admission.cpp b/tests/test_taro_price_gap_admission.cpp index 6fe687a8e..629e19b42 100644 --- a/tests/test_taro_price_gap_admission.cpp +++ b/tests/test_taro_price_gap_admission.cpp @@ -84,7 +84,7 @@ class Probe : public pineforge::source::PineStrategyHost { } if (bar_index_ == cfg_.signal_bar+1) strategy_close_all(); } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } double position() const { return signed_position_size(); } bool magnifier() const { return cfg_.magnifier; } private: diff --git a/tests/test_taro_price_gap_admission_l4d.cpp b/tests/test_taro_price_gap_admission_l4d.cpp index d9bff8944..39b0a1434 100644 --- a/tests/test_taro_price_gap_admission_l4d.cpp +++ b/tests/test_taro_price_gap_admission_l4d.cpp @@ -97,7 +97,7 @@ class Probe : public pineforge::source::PineStrategyHost { } if (bar_index_ == cfg_.signal_bar+1) strategy_close_all(); } - const std::vector& rows() const { return trades_; } + const TradeLedger& rows() const { return trades_; } double position() const { return signed_position_size(); } bool magnifier() const { return cfg_.magnifier; } private: From 72637a795563e1ea8ffb1ae180ff1e35107e3554 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sun, 18 Oct 2026 20:52:03 +0800 Subject: [PATCH 4/6] test(cr150): adapt frozen oracle carriers through snapshots Add a test-only vector snapshot accessor to OraclePineNativeHost and map trades_ only around the two affected non-frozen carrier includes. Refresh same-size snapshots in place to preserve row references across repeated reads. Leave frozen oracle bytes, pins and P00 declarations unchanged. No build, compilation, test or guard was run. --- tests/oracle_fixture_accessors.hpp | 16 ++++++++++++++++ tests/test_native_oracle_frozen_size_full_l2.cpp | 2 ++ ...ative_oracle_reversal_replaced_percent_l2.cpp | 2 ++ 3 files changed, 20 insertions(+) diff --git a/tests/oracle_fixture_accessors.hpp b/tests/oracle_fixture_accessors.hpp index c91c9de85..0c319aa98 100644 --- a/tests/oracle_fixture_accessors.hpp +++ b/tests/oracle_fixture_accessors.hpp @@ -2,6 +2,9 @@ #include +#include +#include + namespace pineforge::source { // The L0 oracle calls the source-language position accessor, whose value can @@ -14,6 +17,19 @@ class OraclePineNativeHost : public PineNativeHost { double oracle_script_position_size() const { return PineNativeHost::signed_position_size(); } + + // Keep the frozen oracle's vector view in this fixture. Refresh existing + // row objects in place so repeated same-size reads preserve references. + const std::vector& oracle_trade_rows() const { + auto& snapshot = oracle_trade_snapshot_; + snapshot.resize(trades_.size() - trades_.base()); + if (!snapshot.empty()) + std::copy(trades_.begin(), trades_.end(), snapshot.begin()); + return snapshot; + } + +private: + mutable std::vector oracle_trade_snapshot_; }; } // namespace pineforge::source diff --git a/tests/test_native_oracle_frozen_size_full_l2.cpp b/tests/test_native_oracle_frozen_size_full_l2.cpp index 92912a856..1b1e431fe 100644 --- a/tests/test_native_oracle_frozen_size_full_l2.cpp +++ b/tests/test_native_oracle_frozen_size_full_l2.cpp @@ -8,7 +8,9 @@ #define signed_position_size oracle_script_position_size #define id_unclosed_qty_ source_id_ledger_view() #include "oracle_fixture_config_shim.hpp" +#define trades_ oracle_trade_rows() #include "oracle/test_oracle_frozen_size.cpp" +#undef trades_ #undef id_unclosed_qty_ #undef signed_position_size #undef PineStrategyHost diff --git a/tests/test_native_oracle_reversal_replaced_percent_l2.cpp b/tests/test_native_oracle_reversal_replaced_percent_l2.cpp index 7e8217f05..2ff57b906 100644 --- a/tests/test_native_oracle_reversal_replaced_percent_l2.cpp +++ b/tests/test_native_oracle_reversal_replaced_percent_l2.cpp @@ -6,6 +6,8 @@ #define PineStrategyHost OraclePineNativeHost #define signed_position_size oracle_script_position_size #include "oracle_fixture_config_shim.hpp" +#define trades_ oracle_trade_rows() #include "oracle/test_oracle_reversal_replaced_percent.cpp" +#undef trades_ #undef signed_position_size #undef PineStrategyHost From cfaf8524a23032dee276b12b2a17f01a3b40f6ff Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sun, 18 Oct 2026 20:52:26 +0800 Subject: [PATCH 5/6] fix(cr150): reserve settlement effects through TradeLedger Add the CORE-TOP-approved TradeLedger overload immediately after the vector helper. Guard the logical end and reserve the resident count through the frozen API. Preserve both existing call sites, the vector helper and P00 declarations. No build, compilation, test or guard was run. --- src/engine_execution.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/engine_execution.cpp b/src/engine_execution.cpp index b94e23099..e97bc1889 100644 --- a/src/engine_execution.cpp +++ b/src/engine_execution.cpp @@ -68,6 +68,13 @@ void reserve_effects(std::vector& values, size_t extra) { values.reserve(std::max(required, grown)); } +void reserve_effects(TradeLedger& values, size_t extra) { + if (extra > std::numeric_limits::max() - values.size()) + throw std::length_error("execution effect capacity exhausted"); + const size_t resident = values.size() - values.base(); + values.reserve(resident + extra); +} + struct CloseScopeInspection { execution::Status status = execution::Status::Applied; double held = 0.0; From 327425ddb61f75e7b9df7648ef66caac2c0d9226 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sun, 18 Oct 2026 21:51:07 +0800 Subject: [PATCH 6/6] docs(cr150): refresh every L8a source citation Repair the source citations shifted by the L8a header/include and allocation-overload edits. Search docs for all 70 changed-file basenames, covering 516 mentions in 18 documentation files, rather than only the 140 lab findings. Keep exact declaration/occurrence targets for ambiguous symbols, preserve the existing comment hash while shifting its range, and name the trail-peak carry sites explicitly. Identify archived deleted-source coordinates as historical records instead of shifting them onto unrelated live code. Preserve document line counts. Doc-revert/source-line attribution: the edited documentation lines were introduced or last re-anchored by the following commits; the prose sources of the rewritten passages are named as well: - 51a19899ef178f72e04db4ba862d6637634b6a04 - d0935605cf40ef96b4b19573bc6a65d67ac03c5c - 06d4107c004fd1ee5a34a81093608fe78a7230e1 - 44b17c185e1bc76b21b9f7670376f88318eeba61 - 868aa65f1a7bd9db75c50e14e16c5f24fc04ac15 - 925f1c9b73098954a220ccd2ebde1263f2a948a2 - a744bee88a358f1eb0fcfe3b0ec647d31a1dda4b - f48b7c8c3777b6eea39ea4ef61824da7d7087308 - 0411892c2d437943f3a85799cd58193f8535a1c0 - 47184e5c6e1a10c17b81eb8c6543096afbae3a86 - 0d76a099cdd6a042601abf14a8f2beb50d5c8241 - 65c69b078808ebfa3c42299a4e4870deb75a39aa - b16e11f69fbcd32088dfdc8632aab173b6e141b7 - caa65e47c7adffa60acc42fb9d80a27d9f1f6f8b - b97c4095bc99c0a92aa63d217cc742fc3b9863ab - 3372b5d9605d80f1b23434caffe7e65e39dbe778 - b2a578ce776b75b85c24f46ccdcbb3dcbe227a64 These include L14-A/Doxygen (51a19899), L11 archived notes (44b17c185, 06d4107c0), Q6 trail-peak discussion (b16e11f69, caa65e47c, 868aa65f1), and the archival paragraph's V19-A/DOCS-ENGINE history (3372b5d96, b2a578ce7). No behavior ruling is reverted. No source/header/test or pin changes in this commit. No build, compilation, tests, documentation guards, preflight, parity or corpus command was run under the CORE-TOP Mac restriction. --- docs/Doxyfile | 8 +- docs/adr/0001-kernel-adapter-boundary.md | 46 +++++----- docs/design/native-feature-parity.md | 72 +++++++-------- docs/native-refactor-progress.md | 2 +- docs/pages/native-engine.md | 20 ++--- docs/pages/pine-to-native.md | 108 +++++++++++------------ docs/pine-adapter-kernel-notes.md | 10 +-- 7 files changed, 133 insertions(+), 133 deletions(-) diff --git a/docs/Doxyfile b/docs/Doxyfile index 5388815f2..d3e5c8330 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -101,10 +101,10 @@ WARN_LOGFILE = site/doxygen-warnings.log # RECURSIVE reaches include/pineforge/source and include/pineforge/compat; # EXCLUDE below keeps the generated and vendored trees out. # -# pineforge.h is named FIRST, before the directory that also contains it, for -# the reason engine.hpp:37 states about C translation units: engine.hpp defines -# PINEFORGE_NO_STRATEGY_DECLS, and Doxygen's preprocessor carries a define -# across the whole run. Parsed after engine.hpp, pineforge.h loses +# pineforge.h is named FIRST, before the directory that also contains it, because +# engine.hpp defines `PINEFORGE_NO_STRATEGY_DECLS` at engine.hpp:44, and Doxygen's +# preprocessor carries that define across the whole run. Parsed after engine.hpp, +# pineforge.h loses # strategy_create / strategy_free / run_backtest / run_backtest_full / # report_free, and the ~50 `#run_backtest` references in the headers and the # pages resolve to nothing. Order is the fix, exactly as it is in src/c_abi.cpp. diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index 7110412c6..97905cf62 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -86,7 +86,7 @@ summary of it. `NativeCoordinate::interval_index` (`market_driver.hpp:74`) is the sequential SCRIPT-bar index even when `input_tf` is finer than `script_tf`. The same script coordinate books `PyramidEntry::entry_bar_index`, trade-row indices and - the inclusive bar metrics (`engine_orders.cpp:113-114`, `engine_metrics.cpp:140`). + the inclusive bar metrics (`entry_bar_index` / `exit_bar_index` `engine_orders.cpp:114-115`, `engine_metrics.cpp:140`). `NativeCoordinate::input_interval_index` (`market_driver.hpp:79`) names the input slot that supplied the point for a host that needs input cadence. Both coordinate values are folded by `hash_coordinate` @@ -149,10 +149,10 @@ summary of it. the field comments say so, and `scripts/check_adapter_spec_shadowing.py` holds the adapter to the fields it actually needs. - **Feeding data.** Bars enter through the engine's own entry points: `run(bars, n)` - (`engine.hpp:1710`), the timeframe `run` (`engine.hpp:1712`), the rich begin - (`run` `engine.hpp:1800`), or `stream_begin` (`engine.hpp:1771`) / `stream_push_bar` - (`engine.hpp:1777`) / `stream_push_tick(s)` / `stream_advance_time` / `stream_end` - (`engine.hpp:1781`). `market_driver.hpp` is driver *types* (`NativeCoordinate` `market_driver.hpp:72`), not a feed API; read + (`engine.hpp:1711`), the timeframe `run` (`engine.hpp:1713`), the rich begin + (`run` `engine.hpp:1801`), or `stream_begin` (`engine.hpp:1772`) / `stream_push_bar` + (`engine.hpp:1778`) / `stream_push_tick(s)` / `stream_advance_time` / `stream_end` + (`engine.hpp:1782`). `market_driver.hpp` is driver *types* (`NativeCoordinate` `market_driver.hpp:72`), not a feed API; read `examples/native/native_market_strategy.cpp` for a host that drives both a batch and a stream. - **Data / HTF.** The magnifier (`magnifier.hpp`) reconstructs intrabar fills. `request.security`-style series **are** reachable from a bare host, as declared series of the @@ -165,18 +165,18 @@ summary of it. `NativeSeriesSource::AuxiliaryFeed` for a series finer than the input. The kernel owns the aggregation, the `lookahead`/`gaps` delivery rules and the lazy-seal chronology; TradingView's `request.security` *semantics* are not in it. `prepare_native_security_feeds` stays protected - (`engine.hpp:1639`) and `configure_security_evaluators` stays an empty virtual - (`engine.hpp:1322`) — a host does not register an evaluator by hand — while - `set_native_security_feed` (`engine.hpp:1731`) is the pre-run door for a series' + (`engine.hpp:1640`) and `configure_security_evaluators` stays an empty virtual + (`engine.hpp:1323`) — a host does not register an evaluator by hand — while + `set_native_security_feed` (`engine.hpp:1732`) is the pre-run door for a series' authoritative bars and still throws in-run (`guard_native_mutation` `engine_aux_security.cpp:83`). Indicators are `ta_*.cpp`, session/calendar `session_time.cpp` / `native_calendar.cpp`, FX `configure_native_fx_curve` (`native_host.hpp:1202`). -- **Reporting.** `fill_report` (`engine.hpp:1859`, `src/engine_report.cpp:56`) gives a bare host +- **Reporting.** `fill_report` (`engine.hpp:1860`, `src/engine_report.cpp:56`) gives a bare host trade stats, and the equity curve is a run-spec choice rather than a host chore. `NativeReportPolicy::HostRecorded` (the default) leaves the series to the host, whose `record_equity_point` / `update_equity_extremes` remain protected - (`update_equity_extremes` `engine.hpp:1362`, `record_equity_point` `engine.hpp:1380`); + (`update_equity_extremes` `engine.hpp:1363`, `record_equity_point` `engine.hpp:1381`); `KernelRecorded` has the consumer mark one point per script calculation, and `KernelRecordedAtHostMarks` records the same series at the host's own marks. `report_open_position_at_end` books the still-open position as a mark-to-market row under @@ -194,13 +194,13 @@ summary of it. hosts ever agreed. A tzdata release that rewrites the zone's rules still moves the digest, because the run then read different rules (`tests/test_native_continuation_portable.cpp`, measured equal on macOS/arm64 and Linux/aarch64 at tzdata 2026c). - `broker_state_hash()` (`engine.hpp:2043`) + `broker_state_hash()` (`engine.hpp:2044`) folds that hash with position and lot state under the pinned domain `pineforge-broker-state/v19` (`engine_state_hash.cpp:41`), the closed rows as their count and a running digest each final row folds into once; it is the one exported to C (`c_abi.cpp:538`). A host folds its **own** durable state into it through - `hash_host_extension` (`engine.hpp:405`), whose deprecated spelling - `hash_source_extension` (`engine.hpp:411`) the generic default forwards to. Per-bar rows of + `hash_host_extension` (`engine.hpp:406`), whose deprecated spelling + `hash_source_extension` (`engine.hpp:412`) the generic default forwards to. Per-bar rows of the same hash are recorded under `KernelRecorded` with the recording switch on. ## How the source-adapter reflects Pine for parity @@ -353,7 +353,7 @@ kernel and own these quirks, each at its site: `tests/test_session_ismarket_tape.cpp`). A reading at the bar's open also puts a bar that opens inside a break out of market, which TradingView and the kernel's fact hold in market (`tests/test_session_break_open_bar_tape.cpp`); `barstate_islast_`, still a - `BacktestEngine` member (`engine.hpp:378`) only this host writes; the live-tail / + `BacktestEngine` member (`engine.hpp:379`) only this host writes; the live-tail / probe-suppress overrides (`set_realtime_tail` `pine_strategy_host.hpp:533`, `set_probe_suppress_tail_logic` `pine_strategy_host.hpp:559`), whose live-probe protocol is also a Pine policy over the session flags: its batch's final bar reads the kernel's @@ -401,7 +401,7 @@ citations, 264 are in `src/source/` and its headers; 6 sit in kernel files `closed_trade_close_cause` (`src/engine_trade_accessors.cpp:158`) reads a typed `execution::CloseCause` off the row, compares no string, and answers `3` for a *kernel* liquidation as readily as for the adapter's. What is live is one pair of hashed Pine-shaped - lot flags: `skip_entry_bar_high` / `skip_entry_bar_low` (`engine.hpp:153`), hashed + lot flags: `skip_entry_bar_high` / `skip_entry_bar_low` (`engine.hpp:154`), hashed (`skip_entry_bar_high` `src/engine_state_hash.cpp:74`) and, since R5 lane E6, set by no host at all: a host that owns lot excursions declares where its fill sat (`declare_opened_lot_entry_bar_mask` `src/engine_path_resolve.cpp:95`; no in-tree host has @@ -416,7 +416,7 @@ citations, 264 are in `src/source/` and its headers; 6 sit in kernel files the two generic functions (`bar_path_uses_high_first`, `entry_stop_first_touch` is the source layer's own). The Pine `request.security` *semantics* left the kernel for `src/source/pine_security_eval.cpp`; what stays is the generic evaluator registry - (`SecurityEvalState` `engine.hpp:1163`), ruled by design §2.ii row m (the kernel keeps the + (`SecurityEvalState` `engine.hpp:1164`), ruled by design §2.ii row m (the kernel keeps the registry, the aggregator and one generic step), and the authoritative-feed store, ruled below (Section B's authoritative-feed row, with design §2.ii row s). What remains, each with a ruling in this document: the comparison band in kernel TA @@ -757,7 +757,7 @@ number names its symbol on this tree. | `cohort_add` (`native_host.hpp:1285`) / `cohort_remove` answer `void` (E12 f7) | **retained; the typed receipt is a named follow-up** | The kernel judges every enrolment into a `CohortReceipt` (Applied, InvalidHandle, UnknownOrigin, TerminalOrigin) and folds it into the continuation (`hash_cohort_receipt` `native_execution_consumer.cpp:929`, since v19 once, at the enrolment, into the running digest the continuation carries), so a replay that diverges there diverges in the hash. Nothing that matches or settles reads it: a roster is a relation the close reads at its match, and a refused enrolment is a member the close does not take. A host that needs the receipt AT the call needs `NativeStrategyHost::cohort_add_result` first, a kernel-header change outside this lane; its C spelling would then be an `_ext_v1` with a receipt word. Until then `strategy_native_cohort_add_v1`'s `PF_NATIVE_OK` means the enrolment was issued. | | `native_sized_units` (`native_host.hpp:1314`) / `native_liquidation_price` answer `std::optional` (E12 f7) | **retained** | Observation queries, not commands: each empty is documented cause by cause in its own comment (unconfigured, non-positive money or denominator, a below-one-step quotient; no margin model, no maintenance fraction for the side, a flat book, no finite solution), and every cause is a fact the host can read itself. A typed reason would name what the caller already knows. C answers `PF_NATIVE_ABSENT` and NaN (`strategy_native_liquidation_price_v1` `native_c_api.h:3087`). | | `pf_native_working_v1` without the leg's anchor or owner relation (E13 f4) | **carried** | The C++ host reads them off `NativeWorkingRequest::definition`, so the C readout's relation tail carries them: `anchor` (`native_c_api.h:1718`) through `arm_scope`, the request as the kernel holds it now (an armed leg reads ABSOLUTE with its installed level). | -| `hash_source_extension` (`engine.hpp:411`) | **C++-only** | The deprecated spelling of `hash_host_extension`, kept so an existing C++ subclass compiles and folds unchanged. A C host has only ever had the current spelling, `on_hash_extension`. | +| `hash_source_extension` (`engine.hpp:412`) | **C++-only** | The deprecated spelling of `hash_host_extension`, kept so an existing C++ subclass compiles and folds unchanged. A C host has only ever had the current spelling, `on_hash_extension`. | | `NativeReportPolicy::KernelRecordedAtHostMarks` (`native_run_spec.hpp:64`) | **C++-only** (lane E22's ruling, restated) | Under it the host names each report point from inside its own callbacks, and the C table has no call that marks one: a C host could only declare a series nobody records. `pf_native_report_policy_e` (`native_c_api.h:661`) leaves its value unnamed, and the enum guard holds that exclusion. A C++ host now reaches the same kernel producer through `NativeStrategyHost::mark_native_report_point`; a mark outside a running HostMarks run returns false. | | `strategy_set_path_order` (`pineforge.h: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. | @@ -819,7 +819,7 @@ The table records, at the tree the doc wave read, what each block described and | real-end and chart-close completion (`Real-end completion` `src/timeframe.cpp:1311-1332`, `Chart-close completion` `:1356-1375`) | a thin or session-clipped intraday bucket completes on the bar whose end reaches the bucket end, or on the session's last bar | both are "the period is over" tests over feed data; a gap-free 24x7 feed is bit-identical either way, which the comment states and the tests pin | a completion-mode field over a bucket the calendar already closes | `tests/test_calendar_aggregation_wm.cpp`, `tests/test_native_htf_subscriptions.cpp` | | the native period partition installed by `set_native_periods` `src/timeframe.cpp:1769` (the holiday-pause `TradingView` comment `sha256:7bd87c1e6dfe78729221d412952249a8cb427934ef498d3a4deb6812f7af05d7` :1536-1541) | a session that pauses and reopens the same day is one session day | the rule reads the feed's own stamps; the holiday is data | a holiday table in the kernel | `tests/test_calendar_aggregation_wm.cpp` | | `bar_path_uses_high_first` `src/engine_path_resolve.cpp:28`, `compute_ohlc_path_legs` `src/magnifier.cpp:97`, `entry_stop_first_touch` `src/engine_internal.hpp:101-109` | with only OHLC the intrabar walk order is unknowable, so the open-proximity heuristic picks the first leg | **and the knob exists**: `NativeRunSpec::path_order` (`NativePathOrder` `native_run_spec.hpp:335`) states `HighFirst` / `LowFirst` explicitly | already spelled, and `Auto` is the default that moves no identity | `tests/test_native_run_spec.cpp`, the private test `test_native_price_grid` | -| `round_to_mintick` `include/pineforge/engine.hpp:753` (the `TradingView` comment `sha256:ba90d695d99260d555d1f44bd05110f4977eeb1e54323e6362ea6bd451572399` :737-750) | nearest-tick rounding, ties away from zero | a tick ladder is the instrument's, declared as `price_tick`; the generic surface over it is `NativePriceGrid` + `NativeGridRounding` | TradingView's **per-order-kind** quantization is exactly the knob that would not be generic — it is the measured content of the `price_grid` native-only ruling above | the private test `test_native_price_grid` | +| `round_to_mintick` `include/pineforge/engine.hpp:754` (the `TradingView` comment `sha256:ba90d695d99260d555d1f44bd05110f4977eeb1e54323e6362ea6bd451572399` :738-751) | nearest-tick rounding, ties away from zero | a tick ladder is the instrument's, declared as `price_tick`; the generic surface over it is `NativePriceGrid` + `NativeGridRounding` | TradingView's **per-order-kind** quantization is exactly the knob that would not be generic — it is the measured content of the `price_grid` native-only ruling above | the private test `test_native_price_grid` | | `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` | @@ -827,7 +827,7 @@ The table records, at the tree the doc wave read, what each block described and | `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` | -| `skip_entry_bar_high` / `skip_entry_bar_low` `include/pineforge/engine.hpp:153` | the intrabar-fill excursion mask: the part of a bar traversed before a priced entry filled is not that trade's excursion | the mask is generic — an excursion is measured from the fill — and since R5 lane E6 no host raises the flags: the excursion owner declares where its fill sat and the kernel derives the pair from the bar's path (`declare_opened_lot_entry_bar_mask` `src/engine_path_resolve.cpp:95`); with no declaration the kernel samples the whole bar | already spelled the generic way: `owns_lot_excursions()` + `closed_lot_excursion` hand the whole measurement to a host | `tests/test_l11a_host_excursion.cpp` | +| `skip_entry_bar_high` / `skip_entry_bar_low` `include/pineforge/engine.hpp:154` | the intrabar-fill excursion mask: the part of a bar traversed before a priced entry filled is not that trade's excursion | the mask is generic — an excursion is measured from the fill — and since R5 lane E6 no host raises the flags: the excursion owner declares where its fill sat and the kernel derives the pair from the bar's path (`declare_opened_lot_entry_bar_mask` `src/engine_path_resolve.cpp:95`); with no declaration the kernel samples the whole bar | already spelled the generic way: `owns_lot_excursions()` + `closed_lot_excursion` hand the whole measurement to a host | `tests/test_l11a_host_excursion.cpp` | | the frozen `pf_pending_order_v1_t` field names | the reflection table of an append-only C ABI POD | already ruled: the residual tables above, held by `scripts/check_kernel_residuals.py` | renaming is an ABI break | `scripts/test_check_kernel_residuals.py` (the gate's own suite, CTest row `test_kernel_residuals`) | | the C ABI's documented vocabulary — `src/c_abi.cpp`, `include/pineforge/pineforge.h`, `src/engine_metrics.cpp`, `src/engine_report.cpp`, `src/engine_trade_accessors.cpp` | the exports are the contract; the comments say what a Pine consumer called the same number | already ruled: the `inputs_` / `get_input_*` / `syminfo_metadata_` row above. No kernel decision reads a Pine name | the export names are frozen | `scripts/check_c_abi_runtime.py`, `tests/test_native_c_api.c` | | the generated-code runtime's spellings — `session_time.hpp`, `str_utils.hpp`, `math.hpp`, `matrix.hpp`, `generic_matrix.hpp`; `map.hpp`, `color.hpp`, `drawing.hpp`, `log.hpp` | two cases. The first five headers have neutral primaries with exact deprecated `pine_*` forwards or aliases for generated code. The last four keep Pine-named primaries with no neutral alias (`PineMap`, `pine_color`, `pine_drawing_error`, `pine_log_*`) | already ruled, name by name, by the residual table's two rows above: the `pine_time` row (deprecated spellings) and the `PineMap` row (primaries, whose neutral spelling is an open codegen decision, AUDIT3-opus GAP-15) | dropping an alias or renaming a primary is a codegen change | the ruled-name gate | @@ -839,13 +839,13 @@ The table records, at the tree the doc wave read, what each block described and | 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) | +| `PhysicalExecutionContext::account_fx` `include/pineforge/execution.hpp:92`, read by `build_close_trade_with_costs` `src/engine_orders.cpp:71-88` and handed by `append_open_position_report_rows` `src/native_execution_consumer.cpp:8893-8914` | a closing or mark-to-market row converts at the account-currency rate its execution names -- a mark at a report point's instant, a fill at its own cursor -- and at the presented clock's rate when it names none, as before (R5 lane B-ADAPTER, item 3) | the rate is data a host of any venue supplies with the row it asks for; nothing branches on a platform, and the row's currency is the basis its owner's magnitudes are recorded in | none: no spec field; a caller names the rate per call, and a call naming none converts exactly as before | `tests/test_adapter_range_end_fx.cpp` (a stepped FX curve at seven instants around the range end, plain and aggregated charts) | | `NativeExecutionConsumer::execute_current` `src/native_execution_consumer.cpp:7440` | a current execution converts at its cursor's rate, threaded through its terms facts, inspection, preview and settlement contexts, and neither writes nor restores the host's presented clock, so its own hooks and the host after it see the frame's clock; its preview `inspect_current_execution` still converts at the presented clock (R5 lane B-ADAPTER, item 5) | an execution converting at its own instant's rate is the same for every host and venue, and the host's presented clock stays its own | none: the rate is the one the run already chose (the cursor's instant), carried as data | `tests/test_native_current_execution_rate.cpp` (kernel-only) | | `NativeExecutionConsumer::validate_current_execution` `src/native_execution_consumer.cpp:7163` | a current execution refuses a request bound to a host roster (`BindCohort`) as `UnsupportedRequest`, before its point is taken and whatever the roster holds: it admits exactly the owners the evaluation's current shape executes (`current_shape` `src/native_order.cpp:383`: `Independent`, `BindOpening`, `BindOpenings`). Since R4 slice C it had admitted `BindCohort` as well, which the evaluation then left unevaluated, so a roster holding a live lot failed the run with "native current evaluated allowance mismatch" and an empty one answered `UnreadyOwner`; its read-only preview (`inspect_current_execution`) settled what the execution could not (R5 lane PAR-ORDERS-2) | one legality rule, the evaluation's own, decides a command before any point is taken, the same for every host and for the C surface (`strategy_native_execute_current_v1` answers through this validation); a roster read at the match is not a membership fixed at the command, which is what the documented contract names (`docs/pages/native-engine.md` "Selected exposure and current execution") | none: no spec field or hook. A refused request stays queued and fills at the next match like any market close; a host that wants a cohort's lots closed at once fixes its roster first (`BindOpenings`), as the Pine adapter's immediate closes do | `tests/test_native_current_cohort_refusal.cpp` (kernel-only: a live and an empty roster refused, then filled as queued closes; a fixed roster executing at once) , `tests/test_native_cohort_sliced_close.cpp` (its preview rows: the roster close's preview is refused, the same selection's settles) and `tests/test_adapter_live_state_equivalence.cpp` (reversals seed 3246599, which the failure used to end) | -| the chart symbol's daily partition -- `prepare_chart_day_partition` `src/engine_aux_security.cpp:358`, kept in `chart_day_partition_` `include/pineforge/engine.hpp:1218` and presented to the language runtime through `RuntimeAmbient::day_partition` `src/runtime_ambient.hpp:38` (the TradingView comment above `NativeDayPartition` in `include/pineforge/timeframe.hpp`) | on an intraday chart whose run installed its own symbol's `D` feed, the chart-level D period follows that feed's stamps: the exchange's trade-date bars, a holiday-merged day included. It governs `time("D")`, `timeframe.change("1D")`, `ta.change(time("D"))`, the default-anchored `ta.vwap` and `crosses_boundary(DAY)`. Without the feed, on a 1D chart or on another symbol's clock every rule is the nominal session day | the stamps are the venue's own data, as for the authoritative-feed partition above: another venue is driven by installing its own daily bars, and nothing branches on a platform | the policy knob is installing the feed or not, so a "which daily partition" field would restate it. Only the Pine host builds it (`prepare_chart_day_partition` `pine_strategy_host.cpp:1646`, `:1687`) and presents it for each bar it publishes (`AmbientDayPartitionScope` `pine_strategy_host.cpp:2389`). A bare host never calls the builder, so its calendar stays nominal | the private test `test_o_close_pct_day_anchor` (the partition is built from the D feed, and none without it) and `tests/test_chart_day_memo.cpp`; the tape is `lab tv` `o-cme-dayanchor-full`, recorded at the site | -| 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 chart symbol's daily partition -- `prepare_chart_day_partition` `src/engine_aux_security.cpp:358`, kept in `chart_day_partition_` `include/pineforge/engine.hpp:1219` 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` `include/pineforge/engine.hpp:1067` and `_bar_hour` through `_bar_weekofyear` `include/pineforge/engine.hpp:1087-1094` | 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` | +| the grid and lot helpers -- `tick_grid_price` `include/pineforge/engine.hpp:800`, `price_grid_decimals` `include/pineforge/engine.hpp:834`, `level_on_price_grid` `include/pineforge/engine.hpp:844`, `apply_qty_step` `include/pineforge/engine.hpp:933`, `apply_exit_qty_step` `include/pineforge/engine.hpp:953` (their TradingView calibration comments sit above each) | nearest-tick and price-grid quantization and the lot-increment floors, each calibrated on TradingView tapes | **not a kernel mechanism**: no kernel or source-layer code calls any of them (`tests/test_f6_dead_kernel_members.cpp`, witness 2). The adapter runs its own copies of TradingView's per-order-kind tick rules (ruled native-only in the `price_grid` row above) and sizes its own lots; a native host declares a price grid and a quantity grid in its run spec | nothing to spell. They are rule 5's legacy, kept only because the twin-parity freeze (`scripts/check_twin_parity.py`, base `ab9714be`) holds the checks of the suites that reach them, and they leave with those checks | `tests/test_stop_tick_rounding_l4d.cpp`, `tests/test_level_grid_snap_l4d.cpp`, `tests/test_qty_step_epsilon_floor_l4b.cpp` | The rows above account for the whole population. R5 lane H-DOCGATES re-took it on its own tree with the search this section describes: **369** comment lines in 44 of the 92 files of the diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 2442b75c8..d5cad2be9 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -85,7 +85,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | 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 | +| 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: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: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. | | 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. | @@ -97,12 +97,12 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | 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`). | +| 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: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: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`). | | 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) | +| 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:730 | 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 | @@ -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: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 | +| 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-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: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 | ### 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:1519, pumped from the scheduler `scheduler_prepare_security_sequence` pine_strategy_host.cpp:1529-1828; 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: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: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:1871 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1640, 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). | | 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. | @@ -144,8 +144,8 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa |---|---|---|---|---|---|---| | FP1 | Trigger geometry on the modeled path; fill at the level or at the gap price; market-if-touched | **yes** — `native_matching` native_execution_consumer.cpp: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 | -| 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. | +| 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: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:772 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:7252; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_sizing_price_rule.cpp` quantizes the fill basis under `HalfUp`; `tests/test_native_acid_composite.cpp` books the `Directional` fills on the 0.25 ladder; the private test `test_native_price_grid` re-ran the pair under the overlay). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | | FP5 | Trigger tested against the tick-quantized bar | **no** — the kernel tests raw prices against raw levels **Closed:** `NativePriceGrid::QuantizeFillsAndTriggers` additionally tests and re-validates every activation against the tick-quantized path, one rule for every trigger kind (`tests/test_native_trail_best_seed.cpp` puts an off-ladder trail seed on the ladder; `tests/test_native_acid_composite.cpp` runs a trail's best and its exit level on it; the private test `test_native_price_grid` re-ran the pair under the overlay). | K+A | L8 | O:H5 F:G4 S:F3 | R5-3: the optional quantized trigger test is K; TV's exact half-tick rule (`source_trigger_threshold` pine_adapter.cpp:384) is A on top. | | PG | The Pine adapter on the kernel price grid: raw levels submitted, `project()` declaring `QuantizeFillsAndTriggers` + `HalfUp`, `source_trigger_threshold` deleted (lane R7; re-tested after L8b by gap lane N13) | **measured-infeasible for the Pine adapter** — B1 (the sub-tick cursor-print abort) is closed by L8b's `ActivationGrid`: 0 aborted runs under the trial (R7: 9 zero-offset-trail tapes + 6 POOC panels; now `test_zero_offset_trail_rides_l4c` 449/450, `test_pooc_short_close_tick_l4d` 183/183). B2 (TradingView quantizes per order kind, the grid is one run-wide rule) moved 30 pinned checks in 4 units when N13 last ran the trial, before lanes E14 and E16, with no adapter-side remedy: `test_coof_market_limit_recross_l4c` 24, `test_stop_tick_rounding_l4d` 3, `test_adapter_grid_relower` 2, `test_zero_offset_trail_rides_l4c` 1; 12 more checks in 3 units have an adapter-side cause (`test_trail_fill_snap_l4c` 2, `test_native_margin_hooks` 2, `test_adapter_brackets_relower` 8 book digests). Corpus: 5/312 probes differ, in the engine-only entry-incarnation column only (every probe runs a 0.01 tick on an on-grid feed, so it cannot arbitrate) | A | waived (R7, N13) | — | **Waiver.** No class of triggers is byte-identical on its own: the grid is a run-wide switch and a per-kind mask would spell TradingView's inconsistency into the kernel (not generic, R5-3). The adapter stays on `NativePriceGrid::None` and keeps `source_trigger_threshold` and its call sites; native-engine.md "The Pine adapter's grid re-lowering is waived". **Native-only by ruling (audit lane P6): §3.6.2**, with `examples/native/native_price_grid_strategy.cpp` and its C twin as the feature's hosts. | | FP6 | Account-currency FX | **partial** — scalar `account_fx` + immutable `NativeFxCurve` native_host.hpp:1202 in batch; the stream then refused a non-empty curve (today only tick-driven input is refused under one, `refuse_fx_curve_tick_input` native_execution_consumer.cpp:9866). **Closed by audit lane N6:** the declared curve is the run's immutable FX epoch and a confirmed-bar stream runs under it exactly as a batch does (stream twin in `tests/test_native_margin_fx_roll.cpp`); no realtime rate ingestion and no tick-driven input under a curve are stated as permanent limits with their reasons (native-engine.md, "Stream FX"). **Re-examined by R5 follow-up lane E3 and KEPT:** the tick refusal was never only about the margin check — it guards the SCRIPT-VISIBLE conversions (`open_trade_profit()`, `marked_equity()`, the equity curve), which read `active_account_currency_fx()`, i.e. the presented clock the observation hook has not yet moved (re-measured: a step at T+90s left the T+100s and T+110s prints converting at the pre-step rate). E3's cursor rule reaches the margin model only, so the refusal stands and is pinned in `tests/test_native_margin_fx_clock.cpp` | K | unassigned → N6 (E3: refusal kept) | F:D9 S:F4 | **Disagree:** F out of scope ("O7 clock") / S its P10, a stream-safe FX epoch. No R5 lane → §4 U4 (answered by N6: S's "same curve in batch and stream", without a new clock or callback). | @@ -155,25 +155,25 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| RP1 | Per-lot MFE / MAE, kernel-sampled or host-owned | **yes** — sampler `apply_excursion` native_execution_consumer.cpp: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`. | -| 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 | +| 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:158-172; `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:1793-1830, `compute_trade_stats` engine_metrics.cpp:122 | K | — | F:H2 O:I3 S:X2 | — | +| RP3 | Equity curve | **no** — `update_equity_extremes` engine.hpp:1363 / `record_equity_point` engine.hpp:1381 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. | -| 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) | +| 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:1832 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. | +| 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: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. | -| 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 | — | +| 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 | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| OT1 | Key / value run parameters (`set_input` engine.hpp:1871, refused in-run by `guard_native_mutation` engine.hpp:1876) | **partial** — staging allowed pre-run, getters protected | K (low) | — | F:J1 | (single-source) Nothing for v1: native hosts take constructor parameters. | +| 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 | — | | 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) | +| 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) | | 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). | @@ -202,19 +202,19 @@ 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: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:303 | NEUTRAL in behaviour; a consumer-vtable signature change, so it rides the shared `engine_script_run` bump | `const void*` in the kernel signatures (F `host_begin_extras`, O `overrides_opaque`), typed convenience overload in `source/pine_strategy_host.hpp`; then drop the checker's single whitelist entry `test_native_example_batch` check_native_include_independence.py:42-46. S's alternative (`NativeRunOverrides`, or removing the rich overload) not adopted. Unverified: generated code calls the typed overload (codegen repo is not in this tree — F). **Status (L11, `3f4fff5b`): done** — `run_rich` and the rich `BacktestEngine::run` take `const void*` overrides, the rich begin carries them as `overrides_opaque`, and the independence checker whitelists nothing (§2.vi row 6) | F O S | | b | `pine_float_compare.hpp` | 1e-10 absolute equality band, used by kernel TA only: `float_band_le` ta_misc.cpp:30 (percentrank), `float_band_gt` ta_oscillators.cpp:462-463 (MFI), `plus_dm` ta_volatility_trend.cpp:337-340 (DMI); no execution TU includes it (O) | NEUTRAL (rename only) | Rename to a neutral header (F `float_band_compare.hpp`, O `ta_compare_band.hpp`), keep `pine_float_*` inline aliases in a source-layer header for generated code. Numerics must not move. S's behavioural `ComparePolicy` split is not neutral → not adopted (§4 U6). **Status (L11 + N14): done** — `ta_compare_band.hpp` is the kernel header; the alias shim is `include/pineforge/source/pine_float_compare.hpp` (N14 moved it off the kernel include root; it has no in-tree includer, generated code emits its own comparator) | F O S | | c | `NativeSlotLabelPolicy::LegacyTolerant`, `NativeLegacyTolerance` (now `NativeSlotLabelPolicy::FeedTolerant`, `NativeFeedTolerance`) | `NativeSlotLabelPolicy` native_run_spec.hpp:324-353; read by `legacy_tolerant_slot_labels` market_driver.cpp:55-56 and `native_feed_tolerance_enabled` market_driver.cpp:43-49, and by `legacy_tolerant_slot_labels` native_execution_consumer.hpp:1027-1029 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:2318; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:2335, `:9996`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2806. A feed-tolerance policy (raw strictly-increasing labels, zero / NaN-volume legacy bars), not TV semantics (O) | NEUTRAL | Rename with deprecated aliases (F `RawIncreasing` + `NativeInputTolerance::{StructuralBars, WarmupNonNegativeOHLC}`; O `NativeLabelPolicy::ProviderLabels`). Enumerator values stay 0/1 and 1/2: they are hashed `slot_label_policy` native_execution_consumer.cpp:272. **Status:** renamed as `FeedTolerant` / `NativeFeedTolerance` (the enumerator values unchanged), and the C transport carries them now, in the extension's feed-policy block (`PF_NATIVE_SPEC_EXT_FEED_POLICY` native_c_api.h:745) | F O S | | d | Orphaned comments | The KI-62 block of `engine_orders.cpp` (*fresh*: comment-only, the bodies are deleted; S read it as live code) and a comment of `engine.hpp`; wider deleted-declaration blocks in `engine.hpp`; the money-rule essay in `engine.hpp` (the code is `source_money_round` pine_adapter.cpp:558) | NEUTRAL | Delete, or move to `docs/` next to the adapter code that implements them. **Status: done** — R5 lanes E6 and E10 deleted the detached comment residue (a census of 0 over the kernel's compile closure) | F O | -| e | Dead helpers in the public kernel header | **Done.** All four are gone from `engine.hpp`: `round_to_mintick_directional` and `apply_slippage` were the pre-R5 spelling of the directional snap and the slippage step and the kernel now fills on `native_matching::grid_round_directional` / `native_matching::apply_slippage` src/native_matching.hpp:259 (the engine.hpp comment that named them went with R5 lane E10's residue sweep); `apply_limit_fill` and `reserve_percent_commission` are deleted outright, and only the adapter still spells the first two names, in its own comments. `round_to_mintick` engine.hpp:753 and `bar_fill_price` engine.hpp:771 stay, live through the call `bar_fill_price` native_execution_consumer.cpp:7253 | NEUTRAL | Reuse as the reference arithmetic of the price-grid and sizing lanes, delete the rest | O (single-source) | -| f | Dead legacy stream path | **Done.** `stream_feed_input_bar` and `dispatch_source_stream_script_bar` are both gone — neither name is in `src/` or `include/` — and the virtual left with the epoch that carried it. `stream_state_hash` engine.hpp:1790 stays, and the declaration/definition pairing is still enforced by `scripts/test_native_source_guard.py` | NEUTRAL (removing the virtual changes the vtable → shared bump) | Delete both | O; F asked (its Q6) | +| 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:7253 | NEUTRAL | Reuse as the reference arithmetic of the price-grid and sizing lanes, delete the rest | O (single-source) | +| f | Dead legacy stream path | **Done.** `stream_feed_input_bar` and `dispatch_source_stream_script_bar` are both gone — neither name is in `src/` or `include/` — and the virtual left with the epoch that carried it. `stream_state_hash` engine.hpp: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:27061 | 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 | +| 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) | -| m | `request.security` machinery | `register_security_eval` engine_security.cpp:27, `set_native_security_feed` engine_aux_security.cpp:77-131, `SecurityEvalState` engine.hpp:1156-1360 carry Pine semantics (lookahead, gaps, Heikin-Ashi, KI-55 range start) and are driven only by the source layer | HASH-VISIBLE / measured path | F planned to move both TUs and the state into the source host after native HTF. **R5-6 narrows this:** L6 reuses the feed store and routing, so those stay kernel; only the Pine-only evaluator semantics remain candidates. Do last. **Status (lane L12b): done** — the evaluator semantics are `source::PineSecurityEvalState` + `src/source/pine_security_eval.cpp`; the kernel keeps the registry, aggregator, feed store and one generic step | F S | +| m | `request.security` machinery | `register_security_eval` engine_security.cpp:27, `set_native_security_feed` engine_aux_security.cpp:77-131, `SecurityEvalState` engine.hpp:1157-1361 carry Pine semantics (lookahead, gaps, Heikin-Ashi, KI-55 range start) and are driven only by the source layer | HASH-VISIBLE / measured path | F planned to move both TUs and the state into the source host after native HTF. **R5-6 narrows this:** L6 reuses the feed store and routing, so those stay kernel; only the Pine-only evaluator semantics remain candidates. Do last. **Status (lane L12b): done** — the evaluator semantics are `source::PineSecurityEvalState` + `src/source/pine_security_eval.cpp`; the kernel keeps the registry, aggregator, feed store and one generic step | F S | | n | S-only proposals, not adopted | A generic `FillModel` interface with a TV model installed by the adapter (for the TradingView money / tick / path comment blocks in `engine.hpp` and the dead helpers of item e); "emit generic FIFO facts and let an adapter policy produce the KI-62 scratch" | — | Superseded by R5-3 (price-grid lane) and R5-1 (TV fill specifics stay behind `resolve_execution_terms`); the dead-helper part is item e; there is no scratch code left to re-lower (item d) | S | | o | `pineforge::admission` (market-admission journal) — **N14 addition** | was `include/pineforge/market_admission.hpp` (275 lines, installed by a kernel-only build) + `src/market_admission.cpp` (kernel TU) until N14 moved both under `source/` (`include/pineforge/source/market_admission.hpp`, `src/source/market_admission.cpp`): the observation journal of TradingView admission reviews; every `Configuration` field is a `strategy()` declaration parameter. No kernel TU consumed it (engine.hpp included it and used nothing; the hash helper in `src/broker_state_hash_internal.hpp` had one caller, `src/source/pine_state_hash.cpp`) | NEUTRAL (pure move; the fold is unchanged, so no hash domain moves) | **Status (N14): done** — moved whole to `include/pineforge/source/market_admission.hpp` + `src/source/market_admission.cpp` (`PINEFORGE_SOURCE_LAYER_SOURCES`); `hash_admission_field` moved verbatim into the source hash TU; `check_market_admission_schema.py` re-pointed; `MarketAdmissionDraft` / `MarketAdmissionJournal` spellings and the `market_admission_v2` inline namespace unchanged. ADR-0001 no longer calls it a generic stem | audit (Opus N14) | | p | TradingView trail tick arithmetic — **N14 addition** | `src/engine_internal.hpp`: `kTrailPointsCeilEps` (5e-5 tolerant ceil of `trail_points`, kept as the fallback without a usable mintick; the live tolerance is `kTrailPointsCeilMintickScale` × mintick ticks), `trail_points_to_ticks`, `trail_offset_to_ticks`, `snap_trail_level_to_tick_grid`; every value a `lab tv` calibration; readers only in `src/source/` and `src/compat/pine/` (the kernel's trail is `TrailTicks`, L7) | NEUTRAL (bodies byte-identical) | **Status (N14): done** — moved verbatim to `include/pineforge/compat/pine/trail_ticks.hpp` (`pineforge::compat::pine`); nine call sites re-qualified; `test_trail_fill_snap_l4c` includes the compat header (CHECK texts unchanged, twin parity OK) and is therefore a source-layer row | audit (Opus N14) | @@ -287,7 +287,7 @@ Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the | Lookahead | none, by construction | `lookahead = false` default, available | `lookahead_on = false`, `gaps_on = true` | open 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`. +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:1296-1298, *fresh*); install `authoritative_bars` into the existing store (`set_native_security_feed` engine.hpp:1732, engine_aux_security.cpp:79-133); call `prepare_native_security_feeds` (engine.hpp:1640, 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:1313-1315, *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. 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. @@ -323,10 +323,10 @@ Union of F §2.vi (9), O §2.vi (5 + 1) and S §2.6 (7), deduplicated. S had no | 1 | Orders are "Market / Limit / Stop" (ADR L72-75) | Also StopLimit and Trail; five intents, five owner kinds, groups, `PointBudget` | `Trigger` native_order.hpp:296, `OrderIntent` native_order.hpp:229, `Owner` native_order.hpp:464, `Group` native_order.hpp:473, `PointBudget` native_order.hpp:379 | F | | 2 | "The kernel matches and fills … with sizing, slippage, fees, margin and settlement" (ADR L74-75) | "with slippage, fees, opening admission and settlement; sizing is resolved by the host". `NativeRunSpec` owns fees, FX, close timing, direction / caps and intrabar paths; it does not own percent / cash sizing, maintenance margin, calc-on-fill, every-tick calculation or bare-host security registration | at `73817c1` (overtaken; ADR-0001, "Current state vs. target") | O S | | 3 | "Read results via `metrics.hpp`" (ADR L76-80) | Trade stats yes. Equity curve and equity metrics are empty for a bare host, silently, and an open final position is missing from the report | at `73817c1` (overtaken; ADR-0001, "Current state vs. target") | F O | -| 4 | HTF "comes from the kernel feed `set_native_security_feed` / `prepare_native_security_feeds`" (ADR L84-89) | Not usable by a bare host: a partial boundary, not an absent kernel. `prepare_native_security_feeds` is protected with one caller; `configure_security_evaluators` has one caller; the public setter is inert pre-run and refused in-run | `prepare_native_security_feeds` engine.hpp:1639, `configure_security_evaluators` engine.hpp:1322, `set_native_security_feed` engine.hpp:1731, `guard_native_mutation` engine_aux_security.cpp:83 | F O S | +| 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:152, `skip_entry_bar_low` engine_state_hash.cpp:74, `declare_opened_lot_entry_bar_mask` engine_path_resolve.cpp:95; the ledger `close_logical_units_` pine_adapter.hpp:2580; `closed_trade_close_cause` engine_trade_accessors.cpp:158. **Overtaken:** the margin-call toggle and the string decoding are both gone — see ADR-0001, "Current state vs. target" | F O | +| 7 | `engine.hpp` hosts "the ten-significant-digit money rule, KI-62, the `strategy.close` batching + entry-id ledger, a TradingView margin-call toggle" (ADR L131-134) | Two of four survive as code. Money rule: a comment block; the arithmetic is in the adapter. KI-62: orphaned comments + one live, hashed lot flag. Close batching + id ledger: gone from the kernel (the ledger is in the adapter; the old names survive only in comments). Live: `margin_call_enabled_`, the KI-62 flag and — unmentioned by the ADR — string-sentinel decoding of adapter comments | the money-rule block was lines 927-961 of `engine.hpp` and R5 lane E6 deleted it (lines 73-180 were and are live code), the arithmetic `source_money_round` pine_adapter.cpp:558 and its twin `tv_money_round` pine_policy_support.hpp:9-15; `skip_entry_bar_high` engine.hpp: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) | 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 | @@ -868,13 +868,13 @@ bare host on its close-execution point, so every `@hour` label is one bar apart between the halves. The mark itself is the same bar on both. **A dead branch found and reported, not removed.** `fold_exit_trail_peak_` -(engine.hpp:545) is initialised NaN, reset to NaN at run start +(engine.hpp:546) is initialised NaN, reset to NaN at run start (`fold_exit_trail_peak_` engine_run.cpp:109) and **never assigned a finite value anywhere in the -tree**, so all four `if (!std::isnan(fold_exit_trail_peak_))` carries -(engine_execution.cpp:1113/:493, the former adapter loop, the shared producer) -are inert. The shared producer keeps the carry so it folds exactly what the -kernel's settling path folds; removing the member is an epoch question (it is -hashed at engine_state_hash.cpp:72) and is left to a lane that budgets one. +tree**, so the `fold_exit_trail_peak_` checks +(engine_execution.cpp:1120, :1135; `fold_exit_trail_peak_` native_execution_consumer.cpp:8903) +and the shared producer's `preceding_exit_trail_peak` check (engine_orders.cpp:168) +are inert. The shared producer keeps the carry so it folds exactly what the kernel's settling path folds; removing the member is an epoch question (it is +hashed as `fold_exit_trail_peak_` at engine_state_hash.cpp:80) and is left to a lane that budgets one. **Acceptance evidence.** `tests/test_adapter_range_end_relower.cpp`: 12 checks, the adapter half reproduced bit for bit against the `b8e7976e` @@ -1112,7 +1112,7 @@ 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: 1. **No epoch is needed.** The sink is FNV-1a 64 (`BrokerStateHashSink` - engine.hpp:350), and FNV-1a over a fixed byte segment S is an affine map + engine.hpp:351), and FNV-1a over a fixed byte segment S is an affine map with a low-byte-indexed offset, T_S(h) = p^|S|·h + C_S[h mod 256] (mod 2^64); two such maps compose in 256 steps. A row folded once can be applied in O(1) to whatever state the fold arrives with, byte for byte. @@ -1284,12 +1284,12 @@ but it needs two things the kernel does not have. One is storage for the composed transform: a `BacktestEngine` member changes the layout of the script ABI (then `engine_script_run_v18`, since advanced), which is an epoch decision, while a member of the heap-owned execution consumer (`ExecutionConsumerSlot` -engine.hpp:1667) is not. The other is a contract that the six folded fields of +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:634, `exit_comment` :763, `close_cause` :777-782, `exit_from_bracket` :1283 and `entry_incarnation` :1464-1472. -But `trades_` engine.hpp:546 is a protected member any host can write, so a +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 still needs the storage and the contract, and it moves every broker hash: an @@ -1672,7 +1672,7 @@ virtual void hash_host_extension(BrokerStateHashSink&) const {} // F (S: hash_ NativeMetricsView native_metrics() const; // S: net_profit, equity, closed_trades, open_lots; non-virtual ``` -- `KernelRecorded`: the consumer calls the existing `update_equity_extremes()` (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:1363) + `record_equity_point(script_open_ms)` (engine.hpp:1381) 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: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`. - 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). diff --git a/docs/native-refactor-progress.md b/docs/native-refactor-progress.md index fd9c33a3f..82fce2650 100644 --- a/docs/native-refactor-progress.md +++ b/docs/native-refactor-progress.md @@ -182,7 +182,7 @@ them for the record only. "Linux aarch64" is the maintainers' Cortex-A725 host. | PGO | Profile-guided optimisation of the library objects: an instrumented build, a training run over the bench strategies, a `-fprofile-use` rebuild and the identity gates, per compiler and target, with a staleness gate on the profile | LAYOUT1, Linux aarch64 (Cortex-A725), GCC 13.3, `perf stat`, trained on 86 slots and timed on the 14 held-out probes: 14.5 % fewer cycles, CPU time 0.865, 7 % fewer instructions, 26 % fewer L1i refills, 36 % fewer front-end stalls, layout spread 9.0 % → 5.1 %, fingerprints identical | "R5 LAYOUT1 DONE + ACCEPTED 2026-09-24 21:45 Taipei": "PGO APPROVED as a lane (PGO1) scheduled LAST in the perf wave after the code lanes (profile staleness)"; the supervisor's cap: "PGO folds into the release lane"; the owner: "release with PGO in the release build"; TRIAGE1 §D | Unpublished. The tree carries only LAYOUT1's `-falign-functions=64` | The release lane. BENCH4 timed the standard Release build without PGO, and neither BENCH4's nor REL10-ENGINE's brief names it | | Compact journal records and a history ring buffer (L3) | Smaller command records and a ring buffer for the request core's history. `WorkingRequestCore::history` (`include/pineforge/native_order.hpp: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 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: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 | | 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 acedc6c43..36ad5af76 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -468,7 +468,7 @@ byte-identical** to the spec. Conflicting values are a preflight refusal: 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 +overload (`engine.hpp:1801-1813`) is **not** refused as a source mutation: it reaches `NativeExecutionConsumer::run_rich` (`native_execution_consumer.cpp:10429-10471`), which admits the begin, checks the timeframe arguments against the spec, preflights and pumps the batch exactly @@ -2081,10 +2081,10 @@ host that marks its own equity keeps this default and owns the whole series. **The scalar extremes are not part of that bargain.** `max_drawdown_`, `max_runup_` and `max_contracts_held_all_` / `_long_` / `_short_` — read back -through `max_drawdown_percent()` (`engine.hpp:1507`), `max_runup_percent()` -(`engine.hpp:972`) and `max_contracts_held_all/long/short()` -(`max_contracts_held_all` `engine.hpp:1852-1854`) — are a property of the RUN: what it drew down, what -it ran up, the most it ever held. The kernel folds them (`update_equity_extremes`, `engine.hpp:1362`) at every script +through `max_drawdown_percent()` (`engine.hpp:1508`), `max_runup_percent()` +(`engine.hpp:973`) and `max_contracts_held_all/long/short()` +(`max_contracts_held_all` `engine.hpp:1853-1855`) — are a property of the RUN: what it drew down, what +it ran up, the most it ever held. The kernel folds them (`update_equity_extremes`, `engine.hpp:1363`) at every script calculation under `HostRecorded` and `KernelRecorded`. Under `KernelRecordedAtHostMarks`, it folds them at the host-mark callbacks instead, so the host controls the cadence. A `HostRecorded` host reads them truthfully without @@ -2106,7 +2106,7 @@ drawdown/run-up walk, and metrics computed over a real series. The per-bar **broker-state hash** is a row of that same report, so `KernelRecorded` records it too. It stays behind the recording switch it always had — `set_broker_state_hash_recording(true)` -(`engine.hpp:2143`; C: `strategy_set_broker_state_hash_recording`), off by +(`engine.hpp:2144`; C: `strategy_set_broker_state_hash_recording`), off by default, set while no run is active — because each row is a full `broker_state_hash()` over the lots and the closed rows (since v19 a row costs the live state, not the run's length: the closed rows enter through a running @@ -2238,7 +2238,7 @@ bare host's own rows get theirs: the margin model's liquidation books under the ticket the model or the run named. A host running its own forced close states the cause on the row it produced. -`closed_trade_close_cause(i)` (`engine.hpp:1849`) is the C++ read and +`closed_trade_close_cause(i)` (`engine.hpp:1850`) is the C++ read and `strategy_closed_trade_close_cause` (`pineforge.h:1309`) the C one, with the same numbering: `-1` for a bad index or a NULL handle, `0` UNKNOWN, `1` SCRIPT, `2` BRACKET, `3` MARGIN_CALL, `4` INTRADAY_LOSS_CAP, `5` @@ -2270,11 +2270,11 @@ struct RegimeHost : pineforge::NativeStrategyHost { }; ``` -- `hash_host_extension` (`engine.hpp:405`, protected virtual on +- `hash_host_extension` (`engine.hpp:406`, protected virtual on `BacktestEngine`) is called exactly once per hash, last, after the kernel's fold. What it writes is part of the scalar `broker_state_hash()`, of every per-bar row a `KernelRecorded` run records, and of `stream_state_hash()`. -- `BrokerStateHashSink` (`engine.hpp:350`) is a complete public type: FNV-1a +- `BrokerStateHashSink` (`engine.hpp:351`) is a complete public type: FNV-1a over a canonical byte spelling — `d` (a double; `-0.0` folds as `0.0`, every NaN as one quiet NaN), `i`, `u`, `b`, `s` (length, then bytes), `bytes`. - An override **replaces** the default. A host that overrides nothing folds @@ -3093,7 +3093,7 @@ declared subscription does: an `authoritative_bars` feed, the `gaps` and series' place in the run's continuous identity. Prefer `subscriptions` unless you want none of those. -`set_native_security_feed` (`engine.hpp:1731`) is the host ingress for +`set_native_security_feed` (`engine.hpp:1732`) is the host ingress for `authoritative_bars` installed before a run — see *Authoritative bars* above — and not a way to register a series: registration is `NativeRunSpec::subscriptions` or `declare_timeframe_subscriptions`. In-run the diff --git a/docs/pages/pine-to-native.md b/docs/pages/pine-to-native.md index e6d1f192a..7b9bf7c18 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -125,7 +125,7 @@ rather than defaulted. | `initial_capital` | `initial_capital` native_run_spec.hpp:648 | `initial_capital` pineforge.h:501 | `hello_kernel.cpp` | Required, positive. | | `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. | +| `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:1690. 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. | @@ -483,11 +483,11 @@ migration decision: 2. **The protected accessors your host inherits.** `NativeStrategyHost` derives from `BacktestEngine`, so the statistics Pine exposes as `strategy.*` are ordinary protected member functions of your own class: `net_profit` - engine.hpp:967, `gross_profit` engine.hpp:968, `gross_loss` - engine.hpp:969, `current_equity` engine.hpp:970 and the rest. They are + engine.hpp:968, `gross_profit` engine.hpp:969, `gross_loss` + engine.hpp:970, `current_equity` engine.hpp:971 and the rest. They are reachable from inside your host exactly as they are from inside a generated Pine strategy, and `native_open_lots_strategy.cpp` reads every one of them. -3. **The report, after the run** — `fill_report` engine.hpp:1859 into a +3. **The report, after the run** — `fill_report` engine.hpp:1860 into a `pf_report_t` pineforge.h:431, which is what a C host reads. One rule governs the whole table, in two scopes. The equity **series** is @@ -535,30 +535,30 @@ range-end reporting is off. | `strategy.account_currency` | `currency` native_run_spec.hpp:639 | `currency` pineforge.h:499 | `hello_kernel.cpp` / `native_fx_roll_strategy.cpp` | Declared, never inferred. `account_fx` native_run_spec.hpp:650 is the one positive scalar that converts quote to account; a timestamped curve is `configure_native_fx_curve` native_host.hpp:1202. The FX-roll example runs a JPY account on a USD-quoted stock through such a curve, and a step of it is a margin check point of its own. | | `strategy.initial_capital` | `initial_capital` native_run_spec.hpp:648 | `initial_capital` pineforge.h:501 | `hello_kernel.cpp` | The value you declared, unchanged by the run. | | `strategy.equity` | `native_marked_equity(mark)` native_host.hpp:1301; on the Pine host `current_equity() + open_profit(mark)` pine_strategy_host.hpp:688 | `strategy_native_marked_equity_v1` native_c_api.h:3079 | `native_open_lots_strategy.cpp` | Pine marks at the current `close`. The native marked call takes the mark explicitly and returns balance plus open lots' **net** `unrealized_pnl` native_host.hpp:341, which is Pine's equity: TradingView charges the open lots' paid entry fees. | -| `strategy.netprofit` | on the Pine host `net_profit` pine_strategy_host.hpp:687; `net_profit` engine.hpp:967 is the closed trades' figure | `net_profit` pineforge.h:374, less open lots' `entry_commission` | `native_open_lots_strategy.cpp` | The closed trades' net profit less the entry fees the open lots have paid. The report's figure is the closed trades'. | +| `strategy.netprofit` | on the Pine host `net_profit` pine_strategy_host.hpp:687; `net_profit` engine.hpp:968 is the closed trades' figure | `net_profit` pineforge.h:374, less open lots' `entry_commission` | `native_open_lots_strategy.cpp` | The closed trades' net profit less the entry fees the open lots have paid. The report's figure is the closed trades'. | | `strategy.netprofit_percent` | derive: `net_profit` pine_strategy_host.hpp:687 over `initial_capital` native_run_spec.hpp:648 | `pf_metrics_t` pineforge.h:320 | `native_sized_report_strategy.cpp` | The kernel keeps the facts, not the ratio. | -| `strategy.grossprofit` | `gross_profit` engine.hpp:968 | `pf_trade_stats_t` pineforge.h:269 | `native_open_lots_strategy.cpp` | | -| `strategy.grossprofit_percent` | `grossprofit_percent` engine.hpp:975 | `pf_metrics_t` pineforge.h:320 | `native_sized_report_strategy.cpp` | Of initial capital. | -| `strategy.grossloss` | `gross_loss` engine.hpp:969 | `pf_trade_stats_t::gross_loss` pineforge.h:224 | `native_open_lots_strategy.cpp` | For booked closed trades the protected C++ accessor is signed (negative for losses), while the C report stores a positive loss magnitude. With range-end reporting off, `-C.gross_loss == gross_loss()` and `netprofit == grossprofit - C.gross_loss`. With it on, C trade statistics include the report-only range-end rows; filter `open_at_end == 0` before comparing to Pine's closed-trade statistics. | -| `strategy.grossloss_percent` | `grossloss_percent` engine.hpp:978 | `pf_metrics_t` pineforge.h:320 | `native_sized_report_strategy.cpp` | | -| `strategy.openprofit` | `open_profit` engine.hpp:1025 | `strategy_native_marked_equity_v1` native_c_api.h:3079 less realized balance, plus open lots' `entry_commission` | `native_open_lots_strategy.cpp` | Pine's open profit is gross of remaining entry fees. The native marked equity and open-lot `unrealized_pnl` are net of them; use the same explicit mark for every term. | -| `strategy.openprofit_percent` | derive from `open_profit` engine.hpp:1025 | — | `native_open_lots_strategy.cpp` | Pine's denominator is the initial capital plus `strategy.netprofit`, the open lots' paid entry fees charged. | -| `strategy.max_drawdown` | `max_drawdown_` engine.hpp:581 | `max_equity_drawdown` pineforge.h:274 | `native_open_lots_strategy.cpp` | Folded under **every** report policy — see the rule above. The scalar is the run's own; the `pf_equity_stats_t` figure derived from the recorded curve still needs `KernelRecorded` native_run_spec.hpp:63. | -| `strategy.max_drawdown_percent` | `max_drawdown_percent` engine.hpp:1507 | `max_equity_drawdown_pct` pineforge.h:274 | `native_sized_report_strategy.cpp` | Same rule. | -| `strategy.max_runup` | `max_runup_` engine.hpp:582 | `max_equity_runup` pineforge.h:278 | `native_open_lots_strategy.cpp` | Same rule. | -| `strategy.max_runup_percent` | `max_runup_percent` engine.hpp:972 | `max_equity_runup_pct` pineforge.h:278 | `native_sized_report_strategy.cpp` | Same rule. | -| `strategy.avg_trade` | `avg_trade` engine.hpp:981 | `pf_trade_stats_t` pineforge.h:269 | `native_open_lots_strategy.cpp` | | -| `strategy.avg_trade_percent` | `avg_trade_percent` engine.hpp:985 | `pf_trade_stats_t` pineforge.h:269 | `native_sized_report_strategy.cpp` | | -| `strategy.avg_winning_trade` | `avg_winning_trade` engine.hpp:992 | `pf_trade_stats_t` pineforge.h:269 | `native_sized_report_strategy.cpp` | | -| `strategy.avg_winning_trade_percent` | `avg_winning_trade_percent` engine.hpp:998 | `pf_trade_stats_t` pineforge.h:269 | `native_sized_report_strategy.cpp` | | -| `strategy.avg_losing_trade` | `avg_losing_trade` engine.hpp:995 | `pf_trade_stats_t` pineforge.h:269 | `native_sized_report_strategy.cpp` | | -| `strategy.avg_losing_trade_percent` | `avg_losing_trade_percent` engine.hpp:1007 | `pf_trade_stats_t` pineforge.h:269 | `native_sized_report_strategy.cpp` | | -| `strategy.wintrades` | `count_wintrades` engine.hpp:1042 | `pf_trade_stats_t` pineforge.h:269 | `native_open_lots_strategy.cpp` | | -| `strategy.losstrades` | `count_losstrades` engine.hpp:1043 | `pf_trade_stats_t` pineforge.h:269 | `native_open_lots_strategy.cpp` | | -| `strategy.eventrades` | `eventrades` engine.hpp:1857 | `pf_trade_stats_t` pineforge.h:269 | `native_open_lots_strategy.cpp` | Public, not protected: a zero-profit closed row. Win + loss + even is the closed-row count, which the example asserts. | -| `strategy.max_contracts_held_all` | `max_contracts_held_all` engine.hpp:1852 | — | `native_open_lots_strategy.cpp` | `pf_trade_stats_t` has no max-contracts-held field. A C host must derive the peak from position/open-lot observations; there is no direct C report counterpart. | -| `strategy.max_contracts_held_long` | `max_contracts_held_long` engine.hpp:1853 | — | `tests/test_native_report_truth.cpp` | No direct C report counterpart; derive the long-side peak from position/open-lot observations. | -| `strategy.max_contracts_held_short` | `max_contracts_held_short` engine.hpp:1854 | — | `tests/test_native_report_truth.cpp` | No direct C report counterpart; derive the short-side peak from position/open-lot observations. | +| `strategy.grossprofit` | `gross_profit` engine.hpp:969 | `pf_trade_stats_t` pineforge.h:269 | `native_open_lots_strategy.cpp` | | +| `strategy.grossprofit_percent` | `grossprofit_percent` engine.hpp:976 | `pf_metrics_t` pineforge.h:320 | `native_sized_report_strategy.cpp` | Of initial capital. | +| `strategy.grossloss` | `gross_loss` engine.hpp:970 | `pf_trade_stats_t::gross_loss` pineforge.h:224 | `native_open_lots_strategy.cpp` | For booked closed trades the protected C++ accessor is signed (negative for losses), while the C report stores a positive loss magnitude. With range-end reporting off, `-C.gross_loss == gross_loss()` and `netprofit == grossprofit - C.gross_loss`. With it on, C trade statistics include the report-only range-end rows; filter `open_at_end == 0` before comparing to Pine's closed-trade statistics. | +| `strategy.grossloss_percent` | `grossloss_percent` engine.hpp:979 | `pf_metrics_t` pineforge.h:320 | `native_sized_report_strategy.cpp` | | +| `strategy.openprofit` | `open_profit` engine.hpp:1026 | `strategy_native_marked_equity_v1` native_c_api.h:3079 less realized balance, plus open lots' `entry_commission` | `native_open_lots_strategy.cpp` | Pine's open profit is gross of remaining entry fees. The native marked equity and open-lot `unrealized_pnl` are net of them; use the same explicit mark for every term. | +| `strategy.openprofit_percent` | derive from `open_profit` engine.hpp:1026 | — | `native_open_lots_strategy.cpp` | Pine's denominator is the initial capital plus `strategy.netprofit`, the open lots' paid entry fees charged. | +| `strategy.max_drawdown` | `max_drawdown_` engine.hpp:582 | `max_equity_drawdown` pineforge.h:274 | `native_open_lots_strategy.cpp` | Folded under **every** report policy — see the rule above. The scalar is the run's own; the `pf_equity_stats_t` figure derived from the recorded curve still needs `KernelRecorded` native_run_spec.hpp:63. | +| `strategy.max_drawdown_percent` | `max_drawdown_percent` engine.hpp:1508 | `max_equity_drawdown_pct` pineforge.h:274 | `native_sized_report_strategy.cpp` | Same rule. | +| `strategy.max_runup` | `max_runup_` engine.hpp:583 | `max_equity_runup` pineforge.h:278 | `native_open_lots_strategy.cpp` | Same rule. | +| `strategy.max_runup_percent` | `max_runup_percent` engine.hpp:973 | `max_equity_runup_pct` pineforge.h:278 | `native_sized_report_strategy.cpp` | Same rule. | +| `strategy.avg_trade` | `avg_trade` engine.hpp:982 | `pf_trade_stats_t` pineforge.h:269 | `native_open_lots_strategy.cpp` | | +| `strategy.avg_trade_percent` | `avg_trade_percent` engine.hpp:986 | `pf_trade_stats_t` pineforge.h:269 | `native_sized_report_strategy.cpp` | | +| `strategy.avg_winning_trade` | `avg_winning_trade` engine.hpp:993 | `pf_trade_stats_t` pineforge.h:269 | `native_sized_report_strategy.cpp` | | +| `strategy.avg_winning_trade_percent` | `avg_winning_trade_percent` engine.hpp:999 | `pf_trade_stats_t` pineforge.h:269 | `native_sized_report_strategy.cpp` | | +| `strategy.avg_losing_trade` | `avg_losing_trade` engine.hpp:996 | `pf_trade_stats_t` pineforge.h:269 | `native_sized_report_strategy.cpp` | | +| `strategy.avg_losing_trade_percent` | `avg_losing_trade_percent` engine.hpp:1008 | `pf_trade_stats_t` pineforge.h:269 | `native_sized_report_strategy.cpp` | | +| `strategy.wintrades` | `count_wintrades` engine.hpp:1043 | `pf_trade_stats_t` pineforge.h:269 | `native_open_lots_strategy.cpp` | | +| `strategy.losstrades` | `count_losstrades` engine.hpp:1044 | `pf_trade_stats_t` pineforge.h:269 | `native_open_lots_strategy.cpp` | | +| `strategy.eventrades` | `eventrades` engine.hpp:1858 | `pf_trade_stats_t` pineforge.h:269 | `native_open_lots_strategy.cpp` | Public, not protected: a zero-profit closed row. Win + loss + even is the closed-row count, which the example asserts. | +| `strategy.max_contracts_held_all` | `max_contracts_held_all` engine.hpp:1853 | — | `native_open_lots_strategy.cpp` | `pf_trade_stats_t` has no max-contracts-held field. A C host must derive the peak from position/open-lot observations; there is no direct C report counterpart. | +| `strategy.max_contracts_held_long` | `max_contracts_held_long` engine.hpp:1854 | — | `tests/test_native_report_truth.cpp` | No direct C report counterpart; derive the long-side peak from position/open-lot observations. | +| `strategy.max_contracts_held_short` | `max_contracts_held_short` engine.hpp:1855 | — | `tests/test_native_report_truth.cpp` | No direct C report counterpart; derive the short-side peak from position/open-lot observations. | | `strategy.position_size` | `NativePhysicalPosition::signed_units` native_host.hpp:297 | `strategy_native_position_v1` native_c_api.h:2855 | `hello_kernel.cpp` | Signed: no separate direction field. | | `strategy.position_avg_price` | `NativePhysicalPosition::average_price` native_host.hpp:298 | `strategy_native_position_v1` native_c_api.h:2855 | `native_selected_strategy.cpp` | Volume-weighted over the open lots. | | `strategy.position_entry_name` | `native_open_lots` native_host.hpp:1297 then the last row's `entry_label` native_host.hpp:333 | `strategy_native_open_lot_get_v1` native_c_api.h:2896 | `native_open_lots_strategy.cpp` | Pine reports the newest entry's id; the native book lets you read any lot's. | @@ -601,7 +601,7 @@ native_order.hpp:445 names. | `strategy.opentrades.max_runup_percent()` | derive from `favorable_excursion` native_host.hpp:342 | — | `native_open_lots_strategy.cpp` | The snapshot carries the facts, not the ratio. | | `strategy.opentrades.max_drawdown()` | `adverse_excursion` native_host.hpp:343 | `pf_native_open_lot_v1` native_c_api.h:1767 | `native_open_lots_strategy.cpp` | Largest move against the lot, likewise; both excursions are magnitudes `>= 0`. | | `strategy.opentrades.max_drawdown_percent()` | derive from `adverse_excursion` native_host.hpp:343 | — | `native_open_lots_strategy.cpp` | | -| `strategy.opentrades.capital_held` | `open_trades_capital_held` engine.hpp:1016 | — | `native_open_lots_strategy.cpp` | `pf_trade_stats_t` carries no capital-held field. Derive it from the open lots and declared price/value/FX terms, or read the protected C++ accessor. | +| `strategy.opentrades.capital_held` | `open_trades_capital_held` engine.hpp:1017 | — | `native_open_lots_strategy.cpp` | `pf_trade_stats_t` carries no capital-held field. Derive it from the open lots and declared price/value/FX terms, or read the protected C++ accessor. | A NaN `mark` keeps every booking fact, leaves `unrealized_pnl` native_host.hpp:341 NaN and folds nothing into the excursions — the example @@ -611,9 +611,9 @@ run the two excursion fields fold `mark` alone. ## Closed trades {#pine_to_native_map_closed_trades} -`closed_trade_count` engine.hpp:1823 and `closed_trade` engine.hpp:1824 answer -the `Trade` engine.hpp:172 rows this run booked, in booking order; -`report_trade_count` engine.hpp:1831 and `get_report_trade` engine.hpp:1834 +`closed_trade_count` engine.hpp:1824 and `closed_trade` engine.hpp:1825 answer +the `Trade` engine.hpp:173 rows this run booked, in booking order; +`report_trade_count` engine.hpp:1832 and `get_report_trade` engine.hpp:1835 span the same rows followed by the range-end rows `report_open_position_at_end` native_run_spec.hpp:683 adds. In C the report's `pf_report_t::trades` pineforge.h:371 carries the numeric fields, and the @@ -625,29 +625,29 @@ rows of that array. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `strategy.closedtrades` | `closed_trade_count` engine.hpp:1823 | `total_trades` pineforge.h:368 | `native_open_lots_strategy.cpp` | `closed_trade_count` counts booked closed rows. The C report's `total_trades` also includes range-end rows when `report_open_position_at_end` is enabled; filter `pf_trade_t::open_at_end == 0` to match Pine `strategy.closedtrades`. | +| `strategy.closedtrades` | `closed_trade_count` engine.hpp:1824 | `total_trades` pineforge.h:368 | `native_open_lots_strategy.cpp` | `closed_trade_count` counts booked closed rows. The C report's `total_trades` also includes range-end rows when `report_open_position_at_end` is enabled; filter `pf_trade_t::open_at_end == 0` to match Pine `strategy.closedtrades`. | | `strategy.closedtrades.first_index` | none — the kernel keeps every row | — | `native_open_lots_strategy.cpp` | TradingView drops old rows past a 9000-trade cap and advances `first_index` when it does. The kernel caps nothing, so the first index is always 0 and codegen emits the literal. | -| `strategy.closedtrades.entry_id()` | `entry_id` engine.hpp:183 | `strategy_closed_trade_entry_id` pineforge.h:1227 | `native_open_lots_strategy.cpp` | The same string the lot carried as `entry_label` native_host.hpp:333. | -| `strategy.closedtrades.entry_comment()` | `entry_comment` engine.hpp:184 | — | `native_open_lots_strategy.cpp` | No C accessor: `strategy_closed_trade_entry_id` pineforge.h:1227 answers the id, not the comment, and `pf_trade_t` carries no strings. A C host that needs it reads the open lot's `entry_comment` (`pf_native_open_lot_v1` native_c_api.h:1767) before the close. | -| `strategy.closedtrades.entry_bar_index()` | `entry_bar_index` engine.hpp:181 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | Copied from the lot. | -| `strategy.closedtrades.entry_time()` | `entry_time` engine.hpp:173 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | Unix ms. | -| `strategy.closedtrades.entry_price()` | `entry_price` engine.hpp:175 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | | -| `strategy.closedtrades.exit_id()` | `exit_id` engine.hpp:186 | `strategy_closed_trade_exit_id` pineforge.h:1240 | `native_margin_strategy.cpp` | For a kernel liquidation this is the model's own `liquidation_label` native_run_spec.hpp:251, which is how a reporting layer classifies the row. | -| `strategy.closedtrades.exit_comment()` | `exit_comment` engine.hpp:185 | `strategy_closed_trade_exit_comment` pineforge.h:1242 | `native_margin_strategy.cpp` | | -| `strategy.closedtrades.exit_bar_index()` | `exit_bar_index` engine.hpp:182 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | | -| `strategy.closedtrades.exit_time()` | `exit_time` engine.hpp:174 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | | -| `strategy.closedtrades.exit_price()` | `exit_price` engine.hpp:176 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | | -| `strategy.closedtrades.size()` | `qty` engine.hpp:177 with `is_long` engine.hpp:180 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | Unsigned, with the direction beside it. A partial close books a row for the closed slice only. | -| `strategy.closedtrades.profit()` | `pnl` engine.hpp:178 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | Net of `commission` engine.hpp:201. `pnl + commission` is the gross move, which equals the lot's `unrealized_pnl + entry_commission` at a mark equal to the exit price, scaled to the closed slice. | -| `strategy.closedtrades.profit_percent()` | `pnl_pct` engine.hpp:179 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | Net return on entry cost. | -| `strategy.closedtrades.commission()` | `commission` engine.hpp:201 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | Entry share plus exit share. One cash-per-execution ticket is split by units over every slice of that execution. | -| `strategy.closedtrades.max_runup()` | `max_runup` engine.hpp:199 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | TradingView's net-open-profit basis: the lot's gross favorable excursion less the entry share, floored at zero. The open-lot field is the gross value it is derived from. | -| `strategy.closedtrades.max_runup_percent()` | derive from `max_runup` engine.hpp:199 | — | `native_open_lots_strategy.cpp` | Over `entry_price * qty * point_value`. | -| `strategy.closedtrades.max_drawdown()` | `max_drawdown` engine.hpp:200 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | The adverse excursion plus the entry share. | -| `strategy.closedtrades.max_drawdown_percent()` | derive from `max_drawdown` engine.hpp:200 | — | `native_open_lots_strategy.cpp` | | +| `strategy.closedtrades.entry_id()` | `entry_id` engine.hpp:184 | `strategy_closed_trade_entry_id` pineforge.h:1227 | `native_open_lots_strategy.cpp` | The same string the lot carried as `entry_label` native_host.hpp:333. | +| `strategy.closedtrades.entry_comment()` | `entry_comment` engine.hpp:185 | — | `native_open_lots_strategy.cpp` | No C accessor: `strategy_closed_trade_entry_id` pineforge.h:1227 answers the id, not the comment, and `pf_trade_t` carries no strings. A C host that needs it reads the open lot's `entry_comment` (`pf_native_open_lot_v1` native_c_api.h:1767) before the close. | +| `strategy.closedtrades.entry_bar_index()` | `entry_bar_index` engine.hpp:182 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | Copied from the lot. | +| `strategy.closedtrades.entry_time()` | `entry_time` engine.hpp:174 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | Unix ms. | +| `strategy.closedtrades.entry_price()` | `entry_price` engine.hpp:176 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | | +| `strategy.closedtrades.exit_id()` | `exit_id` engine.hpp:187 | `strategy_closed_trade_exit_id` pineforge.h:1240 | `native_margin_strategy.cpp` | For a kernel liquidation this is the model's own `liquidation_label` native_run_spec.hpp:251, which is how a reporting layer classifies the row. | +| `strategy.closedtrades.exit_comment()` | `exit_comment` engine.hpp:186 | `strategy_closed_trade_exit_comment` pineforge.h:1242 | `native_margin_strategy.cpp` | | +| `strategy.closedtrades.exit_bar_index()` | `exit_bar_index` engine.hpp:183 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | | +| `strategy.closedtrades.exit_time()` | `exit_time` engine.hpp:175 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | | +| `strategy.closedtrades.exit_price()` | `exit_price` engine.hpp:177 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | | +| `strategy.closedtrades.size()` | `qty` engine.hpp:178 with `is_long` engine.hpp:181 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | Unsigned, with the direction beside it. A partial close books a row for the closed slice only. | +| `strategy.closedtrades.profit()` | `pnl` engine.hpp:179 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | Net of `commission` engine.hpp:202. `pnl + commission` is the gross move, which equals the lot's `unrealized_pnl + entry_commission` at a mark equal to the exit price, scaled to the closed slice. | +| `strategy.closedtrades.profit_percent()` | `pnl_pct` engine.hpp:180 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | Net return on entry cost. | +| `strategy.closedtrades.commission()` | `commission` engine.hpp:202 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | Entry share plus exit share. One cash-per-execution ticket is split by units over every slice of that execution. | +| `strategy.closedtrades.max_runup()` | `max_runup` engine.hpp:200 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | TradingView's net-open-profit basis: the lot's gross favorable excursion less the entry share, floored at zero. The open-lot field is the gross value it is derived from. | +| `strategy.closedtrades.max_runup_percent()` | derive from `max_runup` engine.hpp:200 | — | `native_open_lots_strategy.cpp` | Over `entry_price * qty * point_value`. | +| `strategy.closedtrades.max_drawdown()` | `max_drawdown` engine.hpp:201 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | The adverse excursion plus the entry share. | +| `strategy.closedtrades.max_drawdown_percent()` | derive from `max_drawdown` engine.hpp:201 | — | `native_open_lots_strategy.cpp` | | Pine has no word for *why* a row closed. The native row does: -`closed_trade_close_cause` engine.hpp:1849 (C: +`closed_trade_close_cause` engine.hpp:1850 (C: `strategy_closed_trade_close_cause` pineforge.h:1309) distinguishes a script close, a liquidation, a risk flatten and the range end — the kernel states the last three itself. The bracket-leg cause (`2`) is the kernel's too for a close @@ -683,7 +683,7 @@ the same numerics the adapter uses, and the header pulls only `na`, `series` and `window_sum` ta.hpp:2-4, so it is engine-free. `pineforge::Series` series.hpp:94 is Pine's history operator as a fixed-capacity ring you push what you want to keep into. Pine's `input.*` becomes ordinary constructor -parameters: `set_input` engine.hpp:1871 exists but its getters are protected, +parameters: `set_input` engine.hpp:1872 exists but its getters are protected, and a native host takes its parameters in C++. ## One strategy, three ways {#pine_to_native_three_ways} @@ -730,7 +730,7 @@ class HelloKernel : public pineforge::NativeStrategyHost { The host is a class; the run is one `NativeRunSpec` native_run_spec.hpp:622 naming the clock, instrument, account and fees; `configure_native` -native_host.hpp:1196 applies it and `run` engine.hpp:1710 drives the bars. +native_host.hpp:1196 applies it and `run` engine.hpp:1711 drives the bars. **C**, against the same kernel — `examples/native/hello_kernel_c.c:36` is this callback and `examples/native/hello_kernel_c.c:66` the run spec: @@ -995,7 +995,7 @@ closes the stop at `101 - 20 × 0.25 = 96`. `fee_value = 0.1`, and a `NativeLossLimit` whose `percent` native_run_spec.hpp:261 is set, so `20.0` is twenty percent. 5. **The equity curve is opt-in; the rest of the report is not.** `trade_count` - engine.hpp:1814 and `get_trade` engine.hpp:1815 are always complete, and so + engine.hpp:1815 and `get_trade` engine.hpp:1816 are always complete, and so are the equity extremes and the position-size peaks, which every report policy folds. Only the recorded equity **series**, and the `pf_equity_stats_t` figures walked out of it, arrive with diff --git a/docs/pine-adapter-kernel-notes.md b/docs/pine-adapter-kernel-notes.md index 558c80b4c..5f1e2821c 100644 --- a/docs/pine-adapter-kernel-notes.md +++ b/docs/pine-adapter-kernel-notes.md @@ -13,7 +13,7 @@ ## 1. TradingView's ten-significant-digit money rule -Was `include/pineforge/engine.hpp:918-952`, deleted by R5 lane E6 (the `:73-180` span earlier +Was `include/pineforge/engine.hpp`, historical lines 918-952 recorded by `06d4107c0`, deleted by R5 lane E6 (the span at lines 73-180 earlier drafts named is and was live code: `ClosedLotExcursionFacts`, `PyramidEntry`, `Trade`). The arithmetic lives in the adapter (`tv_money_round` / `tv_money_floor_lot` in @@ -714,7 +714,7 @@ runs on fractional grids only (§1.8). ## 2. `strategy.close`: the per-entry-id ledger and same-bar batching -Was `include/pineforge/engine.hpp:503-565`, next to storage that is the +Was `include/pineforge/engine.hpp`, historical lines 503-565 recorded by `44b17c185`, next to storage that is the adapter's since the `engine_script_run_v18` relocation (`PineStrategyHost::id_unclosed_qty_`, which R4 slice C, #254, replaced with `PineExecutionAdapter::close_logical_units_`, and the same-bar close batches, see @@ -798,8 +798,8 @@ id-only maps above without any behavior change. ## 3. KI-62: same-bar pyramid-add scratch after a priced bracket exit -Was `src/engine_orders.cpp:198-208` (comment-only — the helper it documented -is gone) and, in short form, `include/pineforge/engine.hpp:2659-2663`. The +Was `src/engine_orders.cpp`, historical lines 198-208 recorded by `44b17c185` (comment-only — the helper it documented +is gone) and, in short form, `include/pineforge/engine.hpp`, historical lines 2659-2663. The scratch is produced by the adapter today. ```text @@ -818,7 +818,7 @@ this exit's fill price), matching TV's per-pyramid scratch reporting. ## 4. Other orphaned fill / bracket / margin notes -Was the contiguous comment-only block `include/pineforge/engine.hpp:2646-2845`, +Was the contiguous comment-only block `include/pineforge/engine.hpp`, historical lines 2646-2845 recorded by `44b17c185`, in source order. Each paragraph documented a private member that no longer exists; that block's KI-62 paragraph is section 3 above.