PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
engine.hpp
Go to the documentation of this file.
1#pragma once
2#include <atomic>
3#include <deque>
4#include <vector>
5#include <string>
6#include <cstddef>
7#include <cstdint>
8#include <cmath>
9#include <ctime>
10#include <iterator>
11#include <limits>
12#include <memory>
13#include <utility>
14#include <set>
15#include <algorithm>
16#include <unordered_map>
17#include <unordered_set>
18#include <stdexcept>
19#include <optional>
20#include <functional>
22#include "na.hpp"
23#include "bar.hpp"
24#include "broker_events.hpp"
25#include "quantity_intent.hpp"
26#include "execution.hpp"
33#include "leg_activation.hpp"
35#include "order_birth.hpp"
36#include "series.hpp"
37#include "timeframe.hpp"
38#include "magnifier.hpp"
39#include "session_time.hpp"
42// Suppress per-strategy function declarations (strategy_create, run_backtest, etc.) whose pf_*_t parameter types conflict with the internal C++ types
43// used in codegen-emitted extern "C" blocks that include this header.
44// NOTE: this macro leaks into every TU that includes engine.hpp; include
45// pineforge.h FIRST in any TU that needs the per-strategy declarations
46// (see src/c_abi.cpp).
47#define PINEFORGE_NO_STRATEGY_DECLS
48// Angle-bracket form is the installed public path (deliberate).
49#include <pineforge/pineforge.h>
55// Generated script lifecycle users must be rebuilt against this runtime.
56// These internal C++ capabilities change no public C POD or C signature.
57#define PINEFORGE_HAS_TRADE_HISTORY_POLICY_V1 1
58#define PINEFORGE_HAS_SCRIPT_RUN_PREPARE_V1 1
59#define PINEFORGE_HAS_NATIVE_LIVE_V1 1
60#define PINEFORGE_HAS_EQUITY_METRICS_FOLD_V1 1
61namespace pineforge {
62struct CacheCapacity;
63struct CacheDefaults;
64namespace metrics { struct ScratchCurveRegion; }
65enum class PositionSide { FLAT, LONG, SHORT };
66namespace report_sink { class ScratchCurveWriter; }
67namespace report {
68class SelectedReportStream;
69class ReportReplay;
70enum class EquityBasis : std::uint8_t;
71}
72// Physical emulator lot entry/exit in execution order; no range-end report rows. A reversal may produce several exits followed by an entry.
74 uint64_t sequence = 0;
75 int64_t timestamp_ms = 0;
76 int32_t bar_index = 0;
77 bool is_entry = false;
78 bool is_long = false;
79 double quantity = 0.0;
80 double price = 0.0;
81 std::string order_id;
82 std::string comment;
83 uint64_t entry_incarnation = 0;
84 size_t closed_trade_index = static_cast<size_t>(-1); // engine-private provenance
85};
86
87// Forward declaration of an internal enum used by some BacktestEngine
88// method signatures. The full definition lives in src/engine_internal.hpp
89// (private to libruntime); only the underlying-type pin is needed here.
90namespace internal {
91enum class DualEntryStopPathWinner : int;
92}
93
94// ────────────────────────────────────────────────────────────────────
95// Host-owned per-lot excursion accounting (RULING A48, one generic
96// capability). A host that declares ownership of a lot's favorable/adverse
97// excursion supplies the closed-lot result from its own sampler; the kernel
98// then neither samples excursion at matched trigger prices nor folds bar-path
99// extremes into the closing row. Everything below is source-blind: the facts
100// are the lot's own booking coordinates plus the carried extremes, and the
101// result is the two magnitudes the row reports, recorded exactly as answered
102// (the row's account currency; any reporting basis is the owner's to apply).
103// ────────────────────────────────────────────────────────────────────
105 uint64_t entry_incarnation = 0;
106 int64_t entry_time_ms = 0;
107 double entry_price = 0.0;
108 double lot_qty = 0.0;
109 double closed_qty = 0.0;
110 double fill_price = 0.0;
111 double carried_favorable = 0.0;
112 double carried_adverse = 0.0;
113 bool is_long = true;
116 // The lot's own entry-bar excursion masks (PyramidEntry): the booking
117 // facts of a priced intrabar fill, carried across the boundary so the
118 // owner never has to be reachable from the closing row.
121 // The closed slice's share of the lot's entry commission, in account
122 // currency: the entry half of the closing row's `commission`.
123 double entry_commission = 0.0;
124 // The account-currency rate the closing row converts at: the rate its
125 // P&L and its own excursion columns take, so an owner answering in the
126 // row's currency converts at it too.
127 double account_fx = 1.0;
128};
129
131 double favorable = 0.0;
132 double adverse = 0.0;
133};
134
136
137// Where an opened lot's own fill sits on its entry bar's modeled path — the
138// entry-side half of the excursion capability above, and the only thing the
139// owner of a lot's excursion has to say about it. OnPath names a fill at a
140// price the path reaches, so the kernel derives which end (if either) the
141// path had already reached before it; AfterPath names a fill at the bar's
142// closing point, after the whole path, so both ends precede it. The geometry
143// is the kernel's: which leg is walked first and where a price is first
144// touched are the same rules the matcher uses, and a host that forces the leg
145// order does it once, through NativeRunSpec::path_order.
146enum class OpenedLotFillPoint : std::uint8_t {
149};
150
152 double price;
153 int64_t time;
154 double qty;
155 std::string entry_id;
157 std::string entry_comment;
158 double max_runup = 0.0;
159 double max_drawdown = 0.0;
160 // The lot's entry-bar excursion mask: which end of its entry bar the
161 // modeled path had already reached before the lot's own opening fill.
162 // The kernel derives both flags from the bar's path, walked in the leg
163 // order the run declares, when the lot's excursion owner declares where
164 // its fill sat (declare_opened_lot_entry_bar_mask), and hands them back
165 // on the closing row's ClosedLotExcursionFacts; no kernel sampler reads
166 // them. Both default false (nothing declared masks nothing).
168 bool skip_entry_bar_low = false;
169 // Entry-leg commission in account currency at this slice's actual fill
170 // boundary. Percentage commission depends on quote->account FX, so an
171 // effective-time provider must not retroactively reprice this already-paid
172 // fee when a later rate becomes active. Partial realization allocates this
173 // paid cost proportionally and leaves only the unconsumed cost on the lot,
174 // independently of the current fee schedule. NaN is reserved for legacy/
175 // synthetic injection; every production entry path captures a real quote.
177 std::numeric_limits<double>::quiet_NaN();
178 // Monotonic per-run identity of the request record object whose broker fill
179 // created this physical lot. Unlike Pine's user-visible entry_id, an
180 // incarnation is never reused by same-id replacements or later calls.
181 // Every partial-close fragment copied from this lot therefore retains the
182 // same physical-entry provenance. Zero is reserved for legacy/test-only
183 // synthetic lots that were not created by a request record.
184 uint64_t entry_incarnation = 0;
185};
186
187struct Trade {
188 int64_t entry_time;
189 int64_t exit_time;
192 double qty;
193 double pnl;
194 double pnl_pct;
198 std::string entry_id;
199 std::string entry_comment;
200 std::string exit_comment;
201 std::string exit_id;
202 // True when this trade's exit fill came from a REAL strategy.exit
203 // bracket leg (stop/limit/trail/profit/loss), as opposed to a
204 // strategy.close/close_all market close, a reversal-driven close, a
205 // margin-call slice, or an intraday-cap close. The kernel never sets it:
206 // its one writer is the Pine adapter, which labels the rows an exit-leg
207 // fill closed (source::PineStrategyHost::adapter_label_bracket_trades). A
208 // bare host's bracket leg reads BRACKET through close_cause below instead,
209 // which the kernel records when the leg's owner armed it.
210 // ABI v4 task 9: closed_trade_close_cause() reads this to distinguish
211 // BRACKET (2) from SCRIPT (1), after the row's recorded close_cause, which
212 // a margin-call or intraday-cap row carries instead.
213 bool exit_from_bracket = false;
214 double max_runup = 0.0;
215 double max_drawdown = 0.0;
216 double commission = 0.0;
217 // Physical-entry provenance copied from PyramidEntry. This is deliberately
218 // separate from entry_id: Pine permits user-visible IDs to be reused.
219 uint64_t entry_incarnation = 0;
220 // True for the range-end close of a position still open after the final
221 // bar -- a report-only mark-to-market row
222 // (NativeExecutionConsumer::append_open_position_report_rows); false for
223 // every script-driven or bracket exit. Mirrors pf_trade_t::open_at_end.
224 bool open_at_end = false;
225 // Why this row exited, when the closer knew. A kernel-originated
226 // liquidation or risk flatten carries its own cause through the settling
227 // execution::Fill, and so does a close its owner's fill armed (Bracket;
228 // the relation native_toolkit::submit_bracket builds); a host that runs
229 // its own forced-close policy records its cause on the row. Unspecified
230 // leaves closed_trade_close_cause() to the generic facts above
231 // (open_at_end, exit_from_bracket).
233};
234
235struct TradeC {
236 int64_t entry_time;
237 int64_t exit_time;
240 double pnl;
241 double pnl_pct;
243 // Max Adverse/Favorable Excursion for the whole trade in account currency,
244 // net of entry fees. max_runup is the peak favorable amount;
245 // max_drawdown is the peak adverse amount.
246 double max_runup;
248 double qty;
249 double commission; // mirrors pf_trade_t tail; semantics documented in pineforge.h
252 int32_t open_at_end; // ABI v3: 1 on the range-end close row (pineforge.h)
253};
254
261
262// Per-bar runtime trace entry. The transpiler's ``trace`` pragma emits
263// ``trace(name, value)`` calls inside the generated ``on_bar`` so the
264// validator can replay engine-internal series alongside TradingView's own
265// per-bar plot data and pinpoint the bar/filter where divergence appears.
266//
267// ``name_id`` indexes into ``ReportC::trace_names`` — names are interned
268// once per unique label so the per-call cost is push_back of a 24-byte POD
269// rather than a string copy.
271 int64_t timestamp;
272 int32_t bar_index;
273 int32_t name_id;
274 double value;
275};
276
277struct ReportC {
296 // Per-bar trace records emitted by ``BacktestEngine::trace`` calls. Both
297 // arrays are heap-allocated by ``fill_report`` and freed by
298 // ``free_report``; both are nullptr / 0-length when tracing was disabled
299 // or no calls were made. ``trace_names`` is a flat name table — each
300 // ``TraceEntryC.name_id`` indexes into it. Pointers in ``trace_names``
301 // are stable C-strings owned by the live ``BacktestEngine`` instance
302 // (its ``trace_names_`` vector); they remain valid until ``strategy_free``.
305 const char** trace_names;
310 // ABI v4: per-script-bar broker-state hash (empty unless recording enabled).
313};
314
315// Same words/values as docs/design/o1-report-contract.md in o1w-contract.
316// The envelope lane owns the versioned provider API and its eligibility gate.
317// O1 report envelope v1. Data and method declarations do not advertise
318// runtime support; ReportC retains the layout introduced in ABI 4.
324
329
330// Engine-side admission input only. CGI emits the versioned module
331// declaration alongside strategy.ids.freshPerBar; the loader binds that
332// declaration to the actual module/presenter/mark implementation. A missing
333// or unknown declaration, or an overridden presenter/mark, means PLAIN=no
334// and today's byte-identical curve route.
336 std::uint32_t struct_size = sizeof(MetricsPlainDeclarationV1);
337 std::uint32_t version = 1;
338 std::uint32_t builtin_presenter = 0;
339 std::uint32_t builtin_marks = 0;
340 std::uint32_t builtin_scheduler = 0;
341 std::uint32_t security_sites = 0;
342 std::uint32_t lower_tf_sites = 0;
343 std::uint32_t magnifier_declared = 0;
344 std::uint32_t calculation_trigger = 0; // NativeCalculationTrigger word
345 std::uint32_t calc_on_every_history_tick = 0;
346};
349 std::uint32_t module_contract_bound = 0;
350 std::uint32_t historical_batch = 0;
351 std::uint32_t equal_resolved_timeframes = 0;
352 std::uint32_t timeframe_undetected = 0;
353 std::uint32_t begin_bar_magnifier = 0;
354 std::uint32_t intrabar_none = 0;
355 std::uint32_t pending_selected_window = 0;
356 std::uint32_t admitted_selected_window = 0;
357 std::uint32_t realtime_tail = 0;
358 std::uint32_t probe_suppress_tail_logic = 0;
359 std::uint32_t builtin_host_marks = 0;
360 std::uint32_t feed_tolerant_passthrough = 0;
361 std::uint32_t calculation_trigger = 0;
362 std::uint32_t final_admitted_view_bound = 0;
363};
364
365static_assert(sizeof(ReportC) == sizeof(pf_report_t),
366 "legacy C and C++ report sizes must match");
367static_assert(offsetof(ReportEnvelopeV1, report)
368 == offsetof(pf_report_envelope_v1, report),
369 "versioned report payload offsets must match");
370static_assert(sizeof(ReportEnvelopeV1) == sizeof(pf_report_envelope_v1),
371 "versioned C and C++ report sizes must match");
372
373// Reopened here so the row source and the engine it borrows live in the same
374// ABI epoch (the inline namespace is the same one that holds BacktestEngine).
375inline namespace engine_script_run_v19 {
376class BacktestEngine;
377
378// O1 R12: the terminal range-end row source.
379//
380// A run that ends with P physical lots open reports one row per lot. The mark
381// resolves a host's excursion answers once, retains those numeric facts in
382// bounded scratch, and validates every row before publishing the generation.
383// Every reader then rebuilds one row at a time from the lot, terminal mark,
384// account FX and retained answer through the same builder a full close uses.
385// Resident storage is constant in P: no row array and no per-lot string copy.
386//
387// materialize() is the legacy borrowed-pointer boundary. Only the C accessors
388// that hand out a string pointer (materialize_report_trade and its callers)
389// need a row that outlives the call; it keeps exactly those rows, as
390// the old vector did, and is documented non-flat. Report construction, folds
391// and sink emission never call it.
393public:
395 explicit OpenReportRowSource(const BacktestEngine* owner) : owner_(owner) {}
398 // A new run's terminal mark replaces the previous one; the C accessors'
399 // retained rows belong to the replaced generation.
400 void clear() noexcept;
401 void begin(double mark_price, std::int64_t mark_time_ms, int interval_index,
402 double account_fx, bool was_long, std::size_t count);
403 // The exact left fold append_open_position_report_rows returned.
404 void set_pnl(double pnl) noexcept { pnl_ = pnl; }
405 bool empty() const noexcept { return size() == 0; }
406 std::size_t size() const noexcept { return present_ ? count_ : synthetic_.size(); }
407 double pnl() const noexcept { return pnl_; }
408 // One detached row, built exactly as the range-end append builds it.
409 Trade row(std::size_t index) const;
410 // Legacy borrowed-pointer materialization, for the C string accessors and
411 // for tests that seed a row with no live lot.
412 const Trade& materialize(std::size_t index) const;
413 const Trade& operator[](std::size_t index) const { return materialize(index); }
414 // Reference-oracle seeding only: a synthetic report row with no live lot.
415 // Production writers pin a terminal mark instead; the two never coexist
416 // because begin() replaces the generation.
417 void push_back(const Trade& row) { synthetic_.push_back(row); pnl_ += row.pnl; }
418 void push_back(Trade&& row) { pnl_ += row.pnl; synthetic_.push_back(std::move(row)); }
420 public:
421 using iterator_category = std::forward_iterator_tag;
423 using difference_type = std::ptrdiff_t;
425 using pointer = void;
426 const_iterator() = default;
427 Trade operator*() const { return source_->row(index_); }
428 const_iterator& operator++() { ++index_; return *this; }
429 const_iterator operator++(int) { auto old = *this; ++*this; return old; }
430 bool operator==(const const_iterator& other) const {
431 return source_ == other.source_ && index_ == other.index_;
432 }
433 bool operator!=(const const_iterator& other) const { return !(*this == other); }
434 private:
436 const_iterator(const OpenReportRowSource* source, std::size_t index)
437 : source_(source), index_(index) {}
438 const OpenReportRowSource* source_ = nullptr;
439 std::size_t index_ = 0;
440 };
441 const_iterator begin() const { return const_iterator(this, 0); }
442 const_iterator end() const { return const_iterator(this, size()); }
443private:
444 struct OwnedExcursions;
445 Trade build_row(std::size_t index, bool consult_excursion_hook) const;
446 const BacktestEngine* owner_ = nullptr;
447 bool present_ = false;
448 bool was_long_ = false;
449 double mark_price_ = 0.0;
450 std::int64_t mark_time_ms_ = 0;
451 int interval_index_ = 0;
452 double account_fx_ = 1.0;
453 std::size_t count_ = 0;
454 double pnl_ = 0.0;
455 // Published generations share immutable report facts, not continuation
456 // state. Only a host-owned excursion needs this external numeric store.
457 std::shared_ptr<const OwnedExcursions> excursions_;
458 // A deque gives every retained row a stable address, so a reference a C
459 // accessor handed out survives the next row's materialization, as a
460 // reference into the old vector did.
461 mutable std::deque<Trade> materialized_;
462 // Reference-oracle rows with no live lot (tests only; production leaves
463 // this empty).
464 std::vector<Trade> synthetic_;
465};
466} // inline namespace engine_script_run_v19
467
469
470// default_qty_type constants (matches TradingView)
471enum class QtyType { FIXED = 0, PERCENT_OF_EQUITY = 1, CASH = 2 };
472
473// commission_type constants
475
476// Pine user enum → str.tostring (field payloads). Transpiler enforces enum decl before
477// input.enum; this clamps the index so bad values never read past the table.
478struct SymInfo {
479 std::string ticker = "UNKNOWN";
480 std::string tickerid = "UNKNOWN";
481 std::string currency = "USD";
482 std::string basecurrency = "";
483 std::string type = "crypto";
484 std::string timezone = "UTC";
485 std::string session = "24x7";
486 std::string volumetype = "base";
487 std::string description = "";
488 double mintick = 0.01;
489 double pointvalue = 1.0;
490 // Per-instrument quantity step (syminfo.* "qty_step" — the smallest
491 // tradable lot increment, e.g. 0.0004 for BINANCE:ETHUSDT.P). 0 = disabled
492 // (the engine default), so no quantity quantization is applied — corpus
493 // instruments leave this 0 and are byte-identical. A host that sizes in
494 // lots declares it as the run spec's quantity_grid, where admission
495 // requires on-grid units and the kernel's own liquidation floors an
496 // off-grid slice onto it (kernel_submit_liquidation).
497 double qty_step = 0.0;
498};
499
500// Value-owned runtime input transport shared by the public rich run overload
501// and the generic native pre-begin provider. It contains no frontend policy.
502using InputsMap = std::unordered_map<std::string, std::string>;
503
504// The C++ subclass contract is internal, unlike pineforge.h's stable C ABI.
505// Changing its layout or vtable requires all generated/native C++ objects to be rebuilt.
506// v6 adds explicit owner-bound exit-leg activation and Pine placement evidence.
507// Version the mangled class name so older headers' member offsets/vtable cannot
508// silently bind out-of-line members of this different object layout.
509inline namespace engine_script_run_v19 {
510// The fold broker_state_hash() is built on: FNV-1a over a canonical byte
511// spelling of each value (-0.0 folds as 0.0, every NaN as one quiet NaN, a
512// string as its length then its bytes). It is a public, complete type because
513// a host writes into it: BacktestEngine::hash_host_extension receives the sink
514// the kernel has already folded its own broker state into, and whatever the
515// host folds after that is part of every scalar, per-bar and stream hash of
516// the run. Begin an extension with a domain tag of your own (`sink.s(...)`),
517// then fold each durable value in a fixed order.
519public:
520 uint64_t h = 1469598103934665603ULL;
521
522 void bytes(const void* p, size_t n) {
523 const unsigned char* c = static_cast<const unsigned char*>(p);
524 for (size_t i = 0; i < n; ++i) { h ^= c[i]; h *= 1099511628211ULL; }
525 }
526
527 void d(double v) {
528 if (v == 0.0) v = 0.0;
529 if (v != v) v = std::numeric_limits<double>::quiet_NaN();
530 bytes(&v, sizeof v);
531 }
532
533 void i(int64_t v) { bytes(&v, sizeof v); }
534 void u(uint64_t v) { bytes(&v, sizeof v); }
535 void b(bool v) { const unsigned char c = v ? 1 : 0; bytes(&c, 1); }
536 void s(const std::string& v) { u(v.size()); bytes(v.data(), v.size()); }
537};
538// Optional frontend projection interface retained for source compatibility.
539// Broker dispatch itself is virtual on BacktestEngine and never discovers a
540// host kind with RTTI.
542public:
543 virtual ~BrokerStateHashProvider() = default;
544 virtual std::uint64_t broker_state_hash_projection() const = 0;
545};
547protected:
548 // The consumer is the kernel's own execution authority, so this
549 // friendship is also the access path for the request.security feed
550 // machinery a native higher-timeframe subscription drives:
551 // register_security_eval, prepare_native_security_feeds and
552 // feed_security_eval_state stay protected members of this class and are
553 // still out of reach of host code, while the consumer calls them at
554 // begin and from its accepted-input path. No member moved and no
555 // behaviour changed for this.
557 friend class NativeStrategyHost;
558 // The terminal range-end row source rebuilds rows through the engine's
559 // non-mutating close builder and reads the live lots.
561 struct NativeConsumerBindTag { explicit NativeConsumerBindTag() = default; };
565 // The host's own durable state, folded last into broker_state_hash():
566 // called exactly once per hash, after the kernel's generic broker state.
567 // A host whose next decision depends on state the kernel does not own — a
568 // regime, a counter, a model — overrides this and folds it, under its own
569 // domain tag, so a replay that diverges there diverges in the hash. The
570 // override replaces the default; call BacktestEngine::hash_host_extension
571 // first to keep the default's bytes and append to them. A host that
572 // overrides nothing folds exactly what it always folded: the default
573 // forwards to the deprecated spelling below, whose default is the
574 // "source:none" marker.
575 // @host-seam (native_c_api.h, COVERAGE / BASE-CLASS SEAMS)
577 // Deprecated spelling of hash_host_extension, named by the source layer
578 // before a bare host could extend the fold. Still folded when it is the
579 // only one overridden, so an existing subclass compiles and hashes
580 // unchanged; the kernel itself calls hash_host_extension only.
581 // @host-seam (native_c_api.h, COVERAGE / BASE-CLASS SEAMS)
583 virtual std::uint64_t broker_state_hash_projection() const;
584 std::uint64_t broker_state_hash_from_execution_hash(std::uint64_t) const;
585 // --- Position state ---
586 // @broker-state begin
588 double position_entry_price_ = 0.0; // volume-weighted average (for strategy calculations)
589 // Owned, consumable post-fill checkpoint. The shared successful-fill
590 // dispatcher supplies its producer and position identities; economic
591 // eligibility is decided there, independently of this lifecycle model.
594 // Position is FLAT until the first opening fill. The canonical accessor
595 // ``signed_position_size`` already reads 0 when FLAT whatever this holds,
596 // but the settlement and every host that reads position_qty_ directly
597 // must see the same 0 on the first call of a run, so the default is 0
598 // and the two reads stay aligned.
599 double position_qty_ = 0.0;
600 int position_entry_count_ = 0; // number of entries in current direction (for pyramiding)
601 int position_open_bar_ = -1; // bar_index_ when position was opened (for exit delay)
602 // Exact position-instance provenance for pending orders whose semantics
603 // depend on the position cycle in which they were placed. A fresh open or
604 // reversal gets a new nonzero id; same-direction pyramid adds retain it.
607 std::vector<PyramidEntry> pyramid_entries_; // individual entries for trade reporting
608
609 // --- Strategy parameters (set from strategy() declaration) ---
610 double initial_capital_ = 1000000.0;
611 // Detached on bare native construction. Only the explicit Pine frontend
612 // attachment can select its source-shape priority interpretation.
614 double commission_value_ = 0.0;
615 int slippage_ = 0; // slippage in ticks
616 double syminfo_mintick_ = 0.01; // tick size for slippage calculation
617 // Per-instrument lot-size step (the instrument's quantity increment).
618 // 0 = disabled (default; corpus no-op). Fed via the syminfo_metadata
619 // channel ("qty_step") or the SymInfo struct on the explicit run() path.
620 // The kernel's own readers are the apply_*qty_step floors below; a host
621 // that sizes in lots declares the step as the run spec's quantity_grid
622 // (the source host projects this field there).
623 double qty_step_ = 0.0;
624 // Account-currency FX multiplier for every quote->account money path:
625 // equity is denominated in the account currency and a position's notional
626 // in the quote currency, so commission, P&L and the margin comparison
627 // convert the notional at this rate (a Pine ``currency=currency.XXX``
628 // that differs from the symbol's quote currency, e.g. currency.INR on a
629 // USDT-quoted perp, is how the adapter comes to set it). The engine
630 // otherwise assumes account == quote (FX 1.0). Injected via the syminfo metadata
631 // channel (key "account_currency_fx"); defaults to 1.0 so every corpus
632 // strategy (which never sets it) is byte-identical. A timestamped provider
633 // may override it as bars advance; the configured scalar remains the
634 // fallback before the provider's first effective point and across reruns.
637 std::vector<double> account_currency_fx_rates_;
638 // Per-run broker clock for timestamped FX. The epoch is the number of
639 // provider points effective at the current script-bar open (0 means the
640 // scalar fallback). Consuming an epoch even while flat prevents a later
641 // entry from being mistaken for a carried position when the rate has not
642 // changed again.
646
647 int64_t trade_start_time_ = std::numeric_limits<int64_t>::min();
648
649
650 // --- SymInfo + Input injection ---
652 int64_t last_bar_time_ = 0;
654 // Forced intrabar path order (ABI v4 live-runtime surface, task 4): 0
655 // AUTO, 1 HIGH_FIRST, 2 LOW_FIRST. Any other value is clamped to AUTO by
656 // set_path_order() -- this member is always one of {0,1,2}. Persistent
657 // configuration -- reset_run_state() does not touch it. See
658 // set_path_order(). No kernel path reads it: native-bound source hosts
659 // project it into NativeRunSpec::path_order at begin, so the native
660 // driver owns the active batch/stream path order.
662 // Retained last-array-bar visibility flag (waived from the fold). The
663 // probe tail suppression it once gated is the source host's (see
664 // set_probe_suppress_tail_logic below); no kernel path reads it.
665 bool is_tail_bar_ = false;
666 // Chart's display timezone — separate from ``syminfo_.timezone`` (the
667 // exchange TZ). Set by ``set_chart_timezone`` / the C ABI's
668 // ``strategy_set_chart_timezone``. See the doc on ``set_chart_timezone``
669 // for why these two TZ slots must NOT alias.
670 std::string chart_timezone_;
671 std::unordered_map<std::string, std::string> inputs_;
672
673 // Injected symbol metadata (syminfo.shares_outstanding_*,
674 // recommendations_*, target_price_*, pricescale, minmove, …). These
675 // have no source in an OHLCV feed, so the engine returns na<double>()
676 // unless a data feed pushes a value via ``set_syminfo_metadata``. Keyed
677 // by the Pine member name (e.g. "shares_outstanding_total").
678 std::unordered_map<std::string, double> syminfo_metadata_;
679
680 // Input injection helpers for generated code
681 double get_input_double(const std::string& key, double default_val) const;
682 int get_input_int(const std::string& key, int default_val) const;
683 int64_t get_input_int64(const std::string& key, int64_t default_val) const;
684 bool get_input_bool(const std::string& key, bool default_val) const;
685 std::string get_input_string(const std::string& key, const std::string& default_val) const;
686
687 // syminfo.* fundamental/exchange metadata that has no OHLCV source.
688 // Returns the value injected via ``set_syminfo_metadata`` for ``key``,
689 // or na<double>() when none was injected. Codegen routes the
690 // na-by-default SYMINFO_MEMBER_MAP double fields here.
691 double get_syminfo_metadata(const std::string& key) const {
692 auto it = syminfo_metadata_.find(key);
693 return it != syminfo_metadata_.end() ? it->second : na<double>();
694 }
695
696 // --- Runtime state ---
698 int bar_index_ = 0;
699 // Timestamp of the input bar that FOLLOWS the one being fed to the
700 // request.security evaluators; 0 = unknown (streams, the feed's last
701 // bar). A historical run holds its whole feed, and the calendar
702 // aggregator uses the hint to finalize a D/W/M bucket on the period's
703 // actual last chart bar -- early closes and exchange holidays included
704 // (TimeframeAggregator::feed(bar, next_input_ms)). Set by whoever pumps
705 // the evaluators, for each bar it feeds; never for a stream's live input.
708 // Transient companion for trail exits: the trail's best (peak) price at
709 // fill time, a pre-fill favorable excursion of the closing trade that
710 // first_touch_position cannot place on the bar path (the level is only
711 // active after the peak). The settlement hands it to
712 // build_close_trade_with_costs as the context's preceding_exit_trail_peak.
713 // NaN = not a trail fill -- and nothing in the tree assigns it anything
714 // else today (reset_run_state writes the NaN), so that fold never fires;
715 // it stays because it is hashed broker state (engine_state_hash.cpp).
716 double fold_exit_trail_peak_ = std::numeric_limits<double>::quiet_NaN();
718 // Report-only rows for a position still open when the feed ends, produced
719 // by NativeExecutionConsumer::record_open_position_report_rows when the
720 // spec asks for them (NativeRunSpec::report_open_position_at_end). They
721 // are merged behind trades_ by fill_trades_section and never enter
722 // trades_, the realized sums, or the live position (a stream continues it).
723 // R12: the source stores only the terminal mark; see OpenReportRowSource.
725
726 // Best favorable price is updated by the physical open/add helpers for
727 // every host. Source policy may consume the same value through
728 // inheritance, while the public C observer uses the virtual projection.
729 double trail_best_price_ = std::numeric_limits<double>::quiet_NaN();
730
731 // Generic synchronous close obligation. Pine quota/cause/beneficiary
732 // state remains exclusively in the compatibility facade above.
734
735 // --- Cached trade metrics (updated incrementally by record_close_trade) ---
736 double net_profit_sum_ = 0.0;
737 // Conservative absolute roundoff accumulated by additions to the cached
738 // net-profit sum. This is numerical provenance only: reported PnL and
739 // equity continue to use the unchanged sum above. Infinity means a
740 // narrower margin comparison cannot be justified from this history.
742 // The net value whose additions this bound tracked. Direct/synthetic or
743 // future restore writes without matching provenance cannot narrow the
744 // established margin comparison; the next trade makes them unbounded.
746 double gross_profit_sum_ = 0.0;
747 double gross_loss_sum_ = 0.0;
750
751 // --- Equity extremes for max_drawdown / max_runup ---
752 double max_equity_ = 0.0; // peak equity for drawdown
753 double max_drawdown_ = 0.0; // maximum drawdown (positive number)
754 double max_runup_ = 0.0; // maximum runup (positive number)
755 double min_equity_ = 0.0; // trough equity for runup
756
757 // --- Per-script-bar equity curve (metrics + pf_report_t exposure) ---
758 // SUMMARY_ONLY uses exactly one editable point; the fixed prefix lives
759 // outside the broker-state region below.
760 std::vector<pf_equity_point_t> equity_curve_;
761 int64_t bars_in_market_ = 0; // script bars with an open position at close
762 double first_bar_open_ = std::numeric_limits<double>::quiet_NaN(); // buy&hold basis
763
764 // --- Per-script-bar broker-state hash recording (ABI v4 live-runtime
765 // surface, task 6). Default off: empty vector, empty report array,
766 // every historical run byte-identical to before this flag existed. See
767 // set_broker_state_hash_recording() and broker_state_hash(). ---
769 std::vector<uint64_t> broker_state_hashes_;
770
771 // --- Position-size extremes (strategy.max_contracts_held_*) ---
775
776 // --- Even-trade counter (strategy.eventrades) ---
778
779 // @broker-state end
780
781 // Output-only O1W state, deliberately outside the broker-state region.
782 // Like equity_curve_, it never supplies an execution decision. The
783 // derived broker-prefix helper restores the ALREADY HASHED four extrema;
784 // it adds no broker leaf and changes no existing hash domain.
789 mutable bool summary_equity_snapshot_valid_ = false;
790 mutable bool summary_equity_ratios_ready_ = false;
791 std::optional<metrics::EquityRatioResult> summary_equity_ratios_;
793
794 // Output-only streaming-ratio state, like the summary fold above: no broker decision
795 // reads it and report policy must not change continuation hashes. The
796 // fold witnesses its ordered FINAL points. Copies inherit preferences,
797 // never another attempt's admission, provider, fold or receipt.
825 BacktestEngine& engine_;
826 public:
827 explicit MetricsStreamingAttemptV1(BacktestEngine& engine) : engine_(engine) {}
829 // A refused preflight also consumes the external attempt binding.
830 // Construction inspects no plan/feed and changes no failure order.
831 auto& s = engine_.metrics_streaming_;
832 s.pending_plain = s.prepared = false;
833 s.pending_plan = {};
834 s.pending_failed_plain_inputs = 0;
835 }
838 };
839 void begin_metrics_streaming_v1(bool admitted_shape);
843 template<class Source>
844 void observe_metrics_curve_v1(const Source& curve, std::int64_t count,
845 pf_equity_stats_t* equity = nullptr) const {
847 {const_cast<Source*>(&curve), count,
848 [](void* context, std::int64_t index) {
849 return (*static_cast<const Source*>(context))[index];
850 }}, equity);
851 }
852
853public:
854 // Separate equity-mode helper; set_report_mode_v1 does not call it. The envelope owner must
855 // first establish replay/presentation eligibility and all OTHER sequence
856 // policies. This helper alone does not advertise a complete mode provider.
857 // Status values follow the versioned contract: accepted=0, invalid=-1,
858 // busy=-3, conflict=-4, replay unavailable=-5 (SUMMARY_ONLY with a
859 // registered execution observer). A refusal preserves the pending mode.
860 // Configuration takes effect at the next run begin.
865 std::int64_t equity_logical_point_count() const {
867 || metrics_streaming_.no_curve
868 ? summary_equity_fold_.first_pass().point_count
869 : static_cast<std::int64_t>(equity_curve_.size());
870 }
871 // Frozen presented first pass from the latest equity-metric assembly.
872 // The envelope copies its count with captured=0/external=0. No curve
873 // pointer is exposed. A pending second pass cannot certify completion.
885
886protected:
887 // Consumer terminal/range-end integration calls this BEFORE its legacy
888 // full-array refold. No history is reread. The prefix excludes the held
889 // point, including when a host just rewrote last_equity_point().
900
901 // Host-installed excursion capability (RULING A48). Transient run
902 // wiring, not durable broker state: reset_run_state clears it and the
903 // consumer reinstalls it once per run when the host declares ownership.
905 // The entry-side declaration of that capability: the owner of a lot's
906 // excursion names where the lot's opening fill sits on its entry bar, and
907 // the kernel derives the two entry-bar mask flags of every lot booked
908 // under `entry_incarnation` from the bar's path, walked in the leg order
909 // the run declares (NativeRunSpec::path_order, the order the matcher
910 // walks). A fill the path never reaches leaves that lot's flags as they
911 // were. The flags are durable lot state: they are folded into the run
912 // hash and handed back to the owner on the closing row's
913 // ClosedLotExcursionFacts.
914 // @host-seam (native_c_api.h, COVERAGE / BASE-CLASS SEAMS)
915 void declare_opened_lot_entry_bar_mask(uint64_t entry_incarnation,
916 const Bar& entry_bar,
917 OpenedLotFillPoint fill_point);
918 // Continuation digest at the last script point. Native batch teardown
919 // moves the consumer into Completed and would otherwise change the scalar
920 // relative to the recorded array; source state is still folded live so
921 // post-run mutations remain visible.
924 // --- Per-trade extreme tracking ---
926 const execution::Action& action, const execution::Fill& fill,
929 const execution::Action& action, const execution::Fill& fill,
930 const execution::LifecycleEffects& lifecycle,
933 const execution::Action& action, const execution::Fill& fill) const;
934 // Synchronous selected-close extensions. Book preserves the original
935 // action semantics; OpeningExposure permits only Flatten/Reduce and is
936 // revalidated against the current physical book on every call.
938 const execution::Action& action, const execution::Fill& fill,
939 execution::CloseScope scope) const;
940 // The same inspection at an explicit account-currency rate: the resulting
941 // notional and the quoted charges convert at `fx` and no clock is read.
942 // inspect_native_settlement_scoped() is this at the presented clock's rate.
944 const execution::Action& action, const execution::Fill& fill,
945 execution::CloseScope scope, double fx) const;
947 const execution::Action& action, const execution::Fill& fill,
950 // --- Additive selected close (not a CloseScope overload) ---
952 const execution::Action& action,
953 const execution::Fill& fill,
954 const execution::SelectedOpeningSet& selection) const;
955 // The selected inspection at an explicit account-currency rate, as
956 // inspect_native_settlement_scoped_at is the scoped one.
958 const execution::Action& action,
959 const execution::Fill& fill,
960 const execution::SelectedOpeningSet& selection, double fx) const;
962 const execution::Action& action,
963 const execution::Fill& fill,
965 const execution::SelectedOpeningSet& selection);
966 // Source lifecycle coordinator: current_bar_/fold context, source-day
967 // preflight, selected commit, then observation of its committed close rows.
968 // Empty lifecycle is valid. Do not fold selection into LifecycleEffects.
970 const execution::Action& action,
971 const execution::Fill& fill,
972 const execution::LifecycleEffects& lifecycle,
973 const execution::SelectedOpeningSet& selection);
974 // --- Additive non-applying account/effect projection ---
976 const execution::Action& action,
977 const execution::Fill& fill) const;
978 // The same projection at an explicit account-currency rate: its rows, its
979 // quoted charges, its notional and its marked equity convert at `fx`, and
980 // no clock is read. project_native_settlement_v1() is this at the
981 // presented clock's rate.
983 const execution::Action& action,
984 const execution::Fill& fill, double fx) const;
986 const execution::Action& action,
987 const execution::Fill& fill,
988 execution::CloseScope scope) const;
990 const execution::Action& action,
991 const execution::Fill& fill,
992 const execution::SelectedOpeningSet& selection) const;
993 // Opposite-book reversal to an exact signed exposure. Unlike Transact,
994 // the opening is not a remainder of transaction units minus held units.
995 // These synchronous extensions share the settlement owner and do not
996 // introduce a queued native request or saved commit authority.
998 const execution::ReverseTo& reversal,
999 const execution::Fill& fill) const;
1000 // The reversal inspection at an explicit account-currency rate.
1002 const execution::ReverseTo& reversal,
1003 const execution::Fill& fill, double fx) const;
1005 const execution::ReverseTo& reversal,
1006 const execution::Fill& fill) const;
1008 const execution::ReverseTo& reversal,
1009 const execution::Fill& fill,
1011 // Source reversal coordinator with current chart context and source days.
1013 const execution::ReverseTo& reversal,
1014 const execution::Fill& fill,
1015 const execution::LifecycleEffects& lifecycle);
1016 // Native account value: realized balance plus marked physical lots minus
1017 // their remaining paid entry costs, for every fee type. No Pine sizing or
1018 // end-of-range reporting convention participates in this value.
1019 double marked_equity(double price) const;
1020 // The same value at an explicit account-currency rate, and its only
1021 // implementation: marked_equity(price) is this at the presented clock's
1022 // rate, a margin check point calls it at its own cursor's rate.
1023 double marked_equity_at(double price, double fx) const;
1024
1025 // --- Fill rounding helpers ---
1026 // Nearest-tick rounding, floor(price / mintick + 0.5) * mintick with NO
1027 // epsilon: a tie goes where the binary quotient puts it, never to a
1028 // decimal half-up rule (ADR-0001 Section B, `round_to_mintick`; the
1029 // instrument's ladder is its declared tick).
1030 //
1031 // Calibration record (finding-446): TradingView's NASDAQ:AAPL and
1032 // OANDA:EURUSD series carry sub-tick prints (x.xx5 and a few 4-dp
1033 // values), and every fill taken at one of those RAW BAR PRICES is
1034 // exactly this function of it: 24,582 /
1035 // 24,582 half-cent AAPL closes (22,122 rounded up, 2,460 rounded DOWN
1036 // because the binary quotient lands just under the midpoint —
1037 // 228.765 / 0.01 = 22876.499999999996 -> 228.76, while
1038 // 214.385 / 0.01 = 21438.5 -> 214.39) and 142,938 / 142,938 EURUSD
1039 // fills at tick 1e-5. Any epsilon nudge (floor(r + 0.5 + 1e-6)) or a
1040 // decimal half-up rule forces every binary midpoint up and breaks the
1041 // 2,460 down cases. For r >= 1 the form below is bit-identical to
1042 // std::round(r) (adding 0.5 is exact inside one binade), so the legacy
1043 // std::round shape was already the right function; it is spelled out so
1044 // the code reads as the census formula it was fitted to.
1045 // Money is not rounded here at all: ten-significant-digit money is the
1046 // source adapter's own arithmetic, and the kernel's money is exact.
1047 double round_to_mintick(double price) const {
1048 if (std::isnan(price) || syminfo_mintick_ <= 0.0) return price;
1049 return std::floor(price / syminfo_mintick_ + 0.5) * syminfo_mintick_;
1050 }
1051
1052 // A fill taken AT A RAW BAR PRICE — a market fill at a bar point (the
1053 // close, or the next open), a resting stop/limit the open gapped
1054 // through, a stop-limit whose limit is already marketable at an OHLC
1055 // path point, a liquidation slice at the open or adverse extreme — books
1056 // the raw print rounded to the NEAREST tick and only then carries
1057 // slippage ticks. The FEED is never quantized (indicators consume the raw
1058 // sub-tick values); only the fill is on-tick. The directional snap
1059 // (native_matching::grid_round_directional, src/native_matching.hpp) is
1060 // reserved for COMPUTED stop / limit LEVELS that fall between ticks;
1061 // applying it to a raw bar price was the finding-432/446 defect (sells
1062 // floored, buys ceiled — 43 AAPL slugs off by one tick). The result is
1063 // on-tick, so the matcher's directional snap downstream is an identity
1064 // on it (its ladder-exactness test absorbs the n*tick/tick FP residue).
1065 double bar_fill_price(double raw_bar_price) const {
1066 return round_to_mintick(raw_bar_price);
1067 }
1068
1069 // The nearest-tick grid point of a price (the finding-446 formula),
1070 // materialized as k / (1/mintick) when 1/mintick is integral (every
1071 // decimal tick) — the double a decimal literal on that tick parses to,
1072 // so an on-grid level compares EQUAL to a quantized price bit-for-bit
1073 // (14.04 vs k*0.01 = 14.040000000000001 would not). Non-decimal ticks
1074 // fall back to k*mintick.
1075 //
1076 // No production code calls it. Testing a resting stop / limit against
1077 // the bar quantized to the tick is TradingView's per-order-kind rule,
1078 // and the Pine adapter owns it (its tick rules in
1079 // src/source/pine_adapter.cpp; ADR-0001's `price_grid` native-only
1080 // ruling), while a native host declares its ladder through the run
1081 // spec's price grid. It stays because the twin-parity-frozen
1082 // test_stop_tick_rounding_l4d pins it. Calibration record
1083 // (design-stop-tick-rounding, round 6; NYSE:F 1D `lab tv` tapes
1084 // stopround-*, 2026-09-04): sell-stops 13.74624 / 13.7451 / 13.7449 /
1085 // 13.745 skip 02-02 (low 13.745 -> 13.75) and fill 02-03 @13.74 while
1086 // 13.3449 fills 01-26 (low 13.3448 -> 13.34); buy-stops fill on the high
1087 // rounded to NEAREST (14.0351 -> 14.04; 13.225 skips a 13.2202 high); the
1088 // open is quantized too (13.776 over a 13.775 open fills at 13.77); the
1089 // exit-limit, entry-stop / -limit and order-stop tapes reproduce, and
1090 // stopround-ohlc-0/1 show the prints themselves are raw, so the
1091 // quantization is the broker's. Trail legs, stop-limit entries and the
1092 // after-close / fill-recalculation cursors were not pinned and stay raw.
1093 double tick_grid_price(double price) const {
1094 if (std::isnan(price) || syminfo_mintick_ <= 0.0) return price;
1095 const double k = std::floor(price / syminfo_mintick_ + 0.5);
1096 const double inv = 1.0 / syminfo_mintick_;
1097 const double inv_int = std::floor(inv + 0.5);
1098 if (inv_int > 0.0 && std::abs(inv - inv_int) <= 1e-6 * inv_int) {
1099 return k / inv_int;
1100 }
1101 return k * syminfo_mintick_;
1102 }
1103 // A level on the instrument's PRICE GRID: multiples of 1 / pricescale,
1104 // pricescale = 10^(the tick's decimals). A level within
1105 // kLevelGridBandPoints / pricescale^2 of a grid price IS that grid
1106 // price, materialized as k / pricescale — the double the decimal literal
1107 // parses to, bit-for-bit equal to tick_grid_price's output — which
1108 // absorbs the residue a level computed as avg_price +/- k * mintick
1109 // carries (9.99 + 0.05 = 10.040000000000001); a level outside the band
1110 // is returned untouched. A binary tick (1/128 = 0.0078125) resolves to a
1111 // 7-decimal grid whose band is 1e-16: effectively untouched; a tick with
1112 // no short decimal expansion has no grid and is untouched.
1113 //
1114 // No production code calls it. Storing a resting level on the grid is
1115 // TradingView's rule, and the Pine adapter owns it (its own copy,
1116 // source_level_on_price_grid, src/source/pine_adapter.cpp; ADR-0001's
1117 // `price_grid` native-only ruling). It stays because the
1118 // twin-parity-frozen test_level_grid_snap_l4d pins it. Calibration
1119 // record (round 8 family T, 40 `lab tv` sensor tapes on NYSE:F /
1120 // CME_MINI:ES1! / OANDA:EURUSD 15m, 2026-09-05): the band is 1e-6 on a
1121 // 2-decimal symbol (10.040001 IS 10.04; a sell limit at 10.0400012 is not
1122 // and fills at 10.05), the same on ES1! (tick 0.25, pricescale 100) and
1123 // 1e-12 on EURUSD (pricescale 1e5); comparing the raw level against the
1124 // tick-quantized bar instead cost 148 of the 179 exit-time mismatches of
1125 // the F@15 lane.
1126 static constexpr double kLevelGridBandPoints = 0.01; // x 1/pricescale
1128 if (syminfo_mintick_ <= 0.0) return -1;
1129 double scaled = syminfo_mintick_;
1130 for (int n = 0; n <= 10; ++n) {
1131 const double k = std::floor(scaled + 0.5);
1132 if (k >= 1.0 && std::abs(scaled - k) <= 1e-6 * k) return n;
1133 scaled *= 10.0;
1134 }
1135 return -1;
1136 }
1137 double level_on_price_grid(double level) const {
1138 if (std::isnan(level) || !std::isfinite(level)) return level;
1139 const int n = price_grid_decimals();
1140 if (n < 0) return level;
1141 double pricescale = 1.0;
1142 for (int i = 0; i < n; ++i) pricescale *= 10.0;
1143 // Points as level / pointsize (pointsize = 1 / pricescale as a
1144 // double): of the candidate arithmetics this is the one that
1145 // reproduces every tape at the band's FP boundary (EURUSD
1146 // 1.13556 + 1e-12 snaps, + 2e-12 does not; ES 5513.75 + 5e-7
1147 // snaps, + 1e-6 does not; F 10.04 + 1e-6 snaps, + 1.2e-6 does not).
1148 const double pointsize = 1.0 / pricescale;
1149 const double p = level / pointsize;
1150 const double k = std::floor(p + 0.5);
1151 if (std::abs(p - k) <= kLevelGridBandPoints / pricescale) {
1152 return k / pricescale;
1153 }
1154 return level;
1155 }
1156 // --- Commission helper ---
1157 // PERCENT commission is a % of the order's notional value. The notional
1158 // (fill_price × qty × pointvalue) is in the symbol's QUOTE currency; the
1159 // commission a strategy() reports is in ACCOUNT currency, so it needs the
1160 // same instrument->account conversion as the margin gate below
1161 // (account_currency_fx_, default 1.0 — no-op for the corpus). Cash-per-
1162 // order / cash-per-contract are already account-currency-native (a
1163 // trader configures "$20 per contract" in their own currency), so they
1164 // are untouched.
1165 double calc_commission(double fill_price, double qty) const {
1166 // Only a percentage converts, so only it reads the presented clock.
1167 return calc_commission_at(fill_price, qty,
1169 ? active_account_currency_fx() : 1.0);
1170 }
1171 // calc_commission at an explicit account-currency rate `fx`: what a row
1172 // that converts at a rate of its own charges.
1173 double calc_commission_at(double fill_price, double qty, double fx) const {
1174 switch (commission_type_) {
1176 return fill_price * qty * syminfo_.pointvalue
1177 * fx
1178 * (commission_value_ / 100.0);
1180 return commission_value_;
1182 return commission_value_ * qty;
1183 }
1184 return 0.0;
1185 }
1186
1187 // Read the paid entry fee for a still-open pyramid slice. Production entry
1188 // paths always initialize the snapshot. The fallback keeps hand-constructed
1189 // PyramidEntry fixtures source-compatible without weakening real lifecycle
1190 // behavior.
1191 double open_entry_commission(const PyramidEntry& pe) const {
1192 return std::isfinite(pe.entry_commission_account)
1194 : calc_commission(pe.price, pe.qty);
1195 }
1196
1197 double allocated_entry_commission(const PyramidEntry& pe, double units) const {
1198 if (units <= 0.0) return 0.0;
1199 const double paid = open_entry_commission(pe);
1200 return units >= pe.qty ? paid : paid * (units / pe.qty);
1201 }
1202
1206
1207 // --- Lot-increment floors ---
1208 // Floor a quantity to the instrument's lot increment (qty_step_). A
1209 // quotient that is only binary64 residue below an integer is treated as
1210 // that integer (a 1e-6-of-a-step tolerance), so an on-grid request such
1211 // as 1 / 0.00001 does not lose a whole lot because the quotient
1212 // materializes as 99999.999999..., while a genuinely off-grid request
1213 // still floors. When the floor would be a no-op the original double is
1214 // returned, so the tolerance never increases a requested quantity;
1215 // qty_step_ == 0 (the corpus default) leaves every quantity untouched,
1216 // and a quantity may floor to zero (no floor-to-one-step fallback).
1217 //
1218 // No production code calls either floor: the Pine adapter sizes and
1219 // floors its own lots (src/source/pine_adapter.cpp, measured by
1220 // test_adapter_sizing_relower), and a native host sizes with Sized /
1221 // native_sized_units over the run spec's quantity_grid. They stay
1222 // because twin-parity-frozen suites pin them
1223 // (test_qty_step_epsilon_floor_l4b, the frozen-size oracle rows).
1224 // Calibration record: the floor-not-round rule was verified row-for-row
1225 // on TradingView's DCA / safety-order quantities (baseOrderSize/close).
1226 double apply_qty_step(double qty) const {
1227 if (qty_step_ <= 0.0 || !std::isfinite(qty) || qty <= 0.0) return qty;
1228 double floored = std::floor(qty / qty_step_ + 1e-6) * qty_step_;
1229 return floored < qty ? floored : qty;
1230 }
1231
1232 // The same floor for a percent-derived exit leg: 50% of an on-grid
1233 // position is often exactly on-grid in real numbers but lands one ulp
1234 // below the grid ratio in doubles (2.7051/0.0001 = 27050.999999…), and a
1235 // plain floor would knock such a leg a FULL step down; the tolerance
1236 // (1e-6 of a step) sits far above double representation error at
1237 // realistic qty/step magnitudes yet far below any genuine sub-step
1238 // remainder. When the floor is a no-op the ORIGINAL double is returned:
1239 // reconstructing it as floor(...)*step lands one ulp away (0.3 ->
1240 // 0.30000000000000004), jitter that would leak into printed P&L.
1241 // Calibration record: TradingView's BINANCE:ETHUSDT.P (qty_step 0.0001)
1242 // fills a qty_percent=50/50 short bracket over 5.4103 as 2.7051 + 2.7051
1243 // and leaves a 0.0001 dust short open (39 of
1244 // stockhunter2025-btcusd-4h-ema-swing-strategy's 56 unmatched trades were
1245 // such rows); officialjackofalltrades' exact 30%-of-1 legs pin the no-op.
1246 double apply_exit_qty_step(double qty) const {
1247 if (qty_step_ <= 0.0 || !std::isfinite(qty) || qty <= 0.0) return qty;
1248 double floored = std::floor(qty / qty_step_ + 1e-6) * qty_step_;
1249 return floored < qty ? floored : qty;
1250 }
1251
1252 // The basis of a default-sized request is the request's own: SizeTime says
1253 // WHEN it resolves (AtMatch / AtAcceptance) and SizePrice says WHICH price
1254 // it converts at (Resolved / Signal / SignalOnTick), so this class freezes
1255 // no sizing of its own (ADR-0001 rule 5).
1256 // When a forced liquidation is tested is the spec's NativeLiquidationCheck;
1257 // this class keeps no intrabar-liquidation rule of its own.
1258 // What a script-visible position accessor reads around a close is decided
1259 // by NativeCloseExecution, not by a freeze in this class.
1260 // --- Strategy variable accessors ---
1261 double net_profit() const { return net_profit_sum_; }
1262 double gross_profit() const { return gross_profit_sum_; }
1263 double gross_loss() const { return gross_loss_sum_; }
1265
1266 double max_runup_percent() const {
1267 return (initial_capital_ > 0.0) ? (max_runup_ / initial_capital_) * 100.0 : 0.0;
1268 }
1269 double grossprofit_percent() const {
1270 return (initial_capital_ > 0.0) ? (gross_profit_sum_ / initial_capital_) * 100.0 : 0.0;
1271 }
1272 double grossloss_percent() const {
1273 return (initial_capital_ > 0.0) ? (gross_loss_sum_ / initial_capital_) * 100.0 : 0.0;
1274 }
1275 double avg_trade() const {
1276 int n = (int)trades_.size();
1277 return (n > 0) ? (net_profit_sum_ / (double)n) : 0.0;
1278 }
1279 double avg_trade_percent() const {
1280 double s = trade_percent_prefix_.all_sum;
1281 std::size_t n = trade_percent_prefix_.all_count;
1282 // Continue the original left fold over the resident rows.
1283 for (const auto& t : trades_) { s += t.pnl_pct; ++n; }
1284 return n > 0 ? s / static_cast<double>(n) : 0.0;
1285 }
1286 double avg_winning_trade() const {
1287 return (win_trades_count_ > 0) ? (gross_profit_sum_ / (double)win_trades_count_) : 0.0;
1288 }
1289 double avg_losing_trade() const {
1290 return (loss_trades_count_ > 0) ? (gross_loss_sum_ / (double)loss_trades_count_) : 0.0;
1291 }
1293 if (win_trades_count_ <= 0) return 0.0;
1294 double s = trade_percent_prefix_.win_sum;
1295 std::size_t c = trade_percent_prefix_.win_count;
1296 for (const auto& t : trades_) {
1297 if (t.pnl > 0.0) { s += t.pnl_pct; ++c; }
1298 }
1299 return (c > 0) ? (s / (double)c) : 0.0;
1300 }
1302 if (loss_trades_count_ <= 0) return 0.0;
1303 double s = trade_percent_prefix_.loss_sum;
1304 std::size_t c = trade_percent_prefix_.loss_count;
1305 for (const auto& t : trades_) {
1306 if (t.pnl < 0.0) { s += t.pnl_pct; ++c; }
1307 }
1308 return (c > 0) ? (s / (double)c) : 0.0;
1309 }
1311 if (position_side_ == PositionSide::FLAT) return 0.0;
1312 return std::abs(position_qty_ * position_entry_price_) * syminfo_.pointvalue;
1313 }
1314
1315 // Mark-to-market open profit in account currency. The point-value
1316 // multiplier keeps this consistent with realized PnL
1317 // (build_close_trade_with_costs)
1318 // so equity = capital + net_profit + open_profit stays in one unit.
1319 double open_profit(double current_price) const {
1320 if (position_side_ == PositionSide::FLAT) return 0.0;
1321 return open_profit_at(current_price, active_account_currency_fx());
1322 }
1323 // open_profit at an explicit account-currency rate `fx`.
1324 double open_profit_at(double current_price, double fx) const {
1325 if (position_side_ == PositionSide::FLAT) return 0.0;
1326 double diff = (position_side_ == PositionSide::LONG)
1327 ? (current_price - position_entry_price_)
1328 : (position_entry_price_ - current_price);
1329 // Account-currency, matching build_close_trade_with_costs /
1330 // open_trade_profit —
1331 // callers combine this with initial_capital_ + net_profit_sum_ (both
1332 // account-currency) to get total equity. fx=1.0 is a no-op.
1333 return diff * position_qty_ * syminfo_.pointvalue * fx;
1334 }
1335
1336 int count_wintrades() const { return win_trades_count_; }
1337 int count_losstrades() const { return loss_trades_count_; }
1338
1339 // --- Time/date extraction from bar timestamp ---
1340 // The current bar's time components in UTC, the engine's storage
1341 // timezone, memoized per stamp. Protected, so generated code could call
1342 // them, but the codegen transpiler does not: it spells Pine's bare
1343 // ``hour`` / ``minute`` / ``dayofweek`` ... as pine_hour(current_bar_.
1344 // timestamp, syminfo_.timezone) and its siblings (session_time.hpp), on
1345 // the symbol's exchange timezone, and no generated corpus strategy names
1346 // one of these. Their callers are the tests that pin the UTC reading
1347 // (test_chart_timezone, test_intraday_rollover_chart_tz_l4a).
1351
1352 // Single-entry memo: generated scripts commonly read several time
1353 // components per bar (hour + minute + dayofweek); decompose once per
1354 // distinct bar timestamp instead of per accessor. Keyed on the raw
1355 // timestamp, so no per-run invalidation is needed (same ts -> same
1356 // UTC decomposition, run-independent).
1357 mutable int64_t bar_time_memo_ts_ = std::numeric_limits<int64_t>::min();
1359
1361 if (current_bar_.timestamp == bar_time_memo_ts_) return bar_time_memo_;
1362 time_t secs = (time_t)(current_bar_.timestamp / 1000);
1363 struct tm tm_buf;
1364 gmtime_r(&secs, &tm_buf);
1365 BarTime bt;
1366 bt.year = tm_buf.tm_year + 1900;
1367 bt.month = tm_buf.tm_mon + 1;
1368 bt.dayofmonth = tm_buf.tm_mday;
1369 bt.hour = tm_buf.tm_hour;
1370 bt.minute = tm_buf.tm_min;
1371 bt.second = tm_buf.tm_sec;
1372 bt.dayofweek = tm_buf.tm_wday + 1;
1373 bt.weekofyear = (tm_buf.tm_yday + 7 - ((tm_buf.tm_wday + 6) % 7)) / 7;
1374 bar_time_memo_ts_ = current_bar_.timestamp;
1375 bar_time_memo_ = bt;
1376 return bt;
1377 }
1378
1379 // The components of _decompose_bar_time(), one accessor each (UTC).
1380 int _bar_hour() const { return _decompose_bar_time().hour; }
1381 int _bar_minute() const { return _decompose_bar_time().minute; }
1382 int _bar_second() const { return _decompose_bar_time().second; }
1385 int _bar_month() const { return _decompose_bar_time().month; }
1386 int _bar_year() const { return _decompose_bar_time().year; }
1388
1389 // --- Bar magnifier state ---
1391 bool barstate_islast_ = false;
1394 // When true, the intrabar path scales per-sub-bar sample count by
1395 // (sub_bar.volume / mean_sub_bar_volume) within each script bar — dense
1396 // tick approximation on high-volume sub-bars without real tick data.
1397 // The flag reaches the host as NativeBeginArgs::magnifier_volume_weighted;
1398 // a host that projects the magnifier into the run's IntrabarPath carries
1399 // it as that path's volume_weighted, which the execution consumer's
1400 // deliver_intrabar_script samples by (sample_price_path_volume_weighted).
1402
1403 // @broker-state begin
1404 // Monotonic cross-bar fill sequence counter, folded with the broker
1405 // state. The Pine adapter advances it (PineStrategyHost, one step per
1406 // applied broker instruction) and compares its placement fact
1407 // `signal_close_mc_fill_seq` against it in fill-time gates that cross the
1408 // bar boundary; the kernel only resets and folds it.
1410 // @broker-state end
1411
1412 // Session-day facts are the native decision context's
1413 // (NativeDecisionContext::in_session and its neighbours, market_driver.hpp);
1414 // a host that keeps flags of its own stores them itself.
1415 // --- Timeframe state ---
1416 std::string input_tf_;
1417 std::string script_tf_;
1418 // Cached tf_to_seconds(script_tf_). MUST be refreshed immediately after
1419 // every assignment to script_tf_ (the one writer is the Pine host's
1420 // scheduler_prepare_script_run). Avoids a string parse per read.
1424
1425 // --- Historical -> realtime stream lifecycle ---
1426 // stream_begin() executes the historical warmup through the normal run()
1427 // path exactly once, then these fields carry the SAME broker, Pine series,
1428 // TA and timeframe-aggregator state forward while normalized trades arrive.
1429 enum class StreamPhase { IDLE, REALTIME, ENDED };
1432 // A host that keeps the legacy no-op refusal for in-run C-ABI setters
1433 // sets this for the handle lifetime so
1434 // guard_native_mutation stays a no-op (ab9714be LegacyCompatibilityConsumer::refuse).
1435 // Native hosts leave it false; their in-run setter still throws.
1439 int64_t stream_clock_ms_ = 0;
1454 std::vector<StreamOrderAction> stream_order_actions_;
1455
1456 // --- request.security state ---
1458 int sec_id = 0;
1459 std::string tf;
1463 int64_t feed_count = 0;
1466 // Requested-context bar index of the latest dispatch_security_eval()
1467 // (the ring address its TA members saw, see ta::bar_context()); -1
1468 // before the first dispatch. A host that replays its latest
1469 // evaluation re-dispatches the same bar under the same index.
1470 int64_t ta_bar_index = -1;
1471 // Native higher-timeframe feed routing, rebuilt per run by
1472 // prepare_native_security_feeds(): the index into
1473 // native_security_feeds_ serving this state's requested timeframe
1474 // (-1: none, the aggregate stands), and that feed's bars keyed by
1475 // the label this state's aggregator stamps on the same bucket --
1476 // TimeframeAggregator::bar_label_ms of the native bar's covered
1477 // session instant -- so a completed bucket finds its exchange bar by
1478 // the timestamp the aggregate already carries. The same call installs
1479 // the feed's stamps into this state's aggregator as its period
1480 // partition (TimeframeAggregator::set_native_periods): the bucket's
1481 // span, label and completion bar are the native bar's own.
1483 std::unordered_map<int64_t, Bar> native_bars_by_label;
1484 };
1485
1486 std::vector<SecurityEvalState> security_eval_states_;
1487 // The raw feed used by security aggregators in the active run. This is the
1488 // chart input TF on the legacy path and the auxiliary TF on the split path.
1490
1491 // Native higher-timeframe request.security feeds: the exchange's own
1492 // bars of one timeframe each (TradingView's "D" bar on an intraday chart
1493 // is the settlement / official close, not the last intraday close). A
1494 // completed request.security bucket of a fed timeframe takes the native
1495 // bar's OHLCV; timing, other timeframes, chart and broker are untouched.
1496 // Layout-unconditional like the aux feed's siblings above.
1498 std::string tf;
1499 int seconds = 0;
1500 std::vector<Bar> bars;
1501 };
1502 std::vector<NativeSecurityFeed> native_security_feeds_;
1505 // The chart symbol's own daily partition, built per run from the native
1506 // "D" feed's stamps on an intraday chart (prepare_chart_day_partition)
1507 // and installed for the run's bar loop (NativeDayPartitionScope) so the
1508 // chart-level D consumers -- time("D") / time_close("D"),
1509 // timeframe.change("1D"), ta.change(time("D")), ta.vwap's daily anchor --
1510 // read TradingView's trade-date daily bars (timeframe.hpp). Empty on a
1511 // run without the feed or on a calendar chart: every rule nominal.
1513
1514 // --- Runtime trace state ---
1515 // Gated by ``trace_enabled_`` (default false) so production strategies
1516 // pay zero cost; the validator flips it on per-strategy when it needs
1517 // engine-internal per-bar values for TV cross-referencing.
1518 //
1519 // ``trace_buffer_`` is a flat vector of POD records (no string allocs
1520 // per call); each entry holds a ``name_id`` indexing into
1521 // ``trace_names_``, which is the unique-name table built by
1522 // ``intern_trace_name``. A first-time name pays one ``unordered_map``
1523 // insert + ``string`` push_back; subsequent calls with the same name
1524 // are a single map lookup.
1525 bool trace_enabled_ = false;
1526
1527 // Live-runtime surface (ABI v4). All default off/zero; historical runs are
1528 // byte-identical when untouched (tests/test_live_flags_off_identity.cpp).
1529 int last_run_status_ = 0; // 0 completed, 1 NOT_COMPLETED (abort)
1530
1531 // Cooperative abort (spec §3.5): set from any thread via request_abort();
1532 // consumed by the run in progress at the top of each bar-loop iteration
1533 // via check_abort(), which unwinds the run with AbortRequested. Cleared
1534 // at every run() entry, so a request made while idle is a no-op.
1535 //
1536 // AbortFlag wraps std::atomic<bool> in a copy/move-constructible shell:
1537 // std::atomic itself has its copy/move members deleted, and some tests
1538 // (e.g. tests/test_pooc_global_full_exit.cpp's ``run_case``) return a
1539 // BacktestEngine subclass by value, which needs the class to stay
1540 // implicitly copyable. Copying/moving never carries an in-flight abort
1541 // request across — there is no "run in progress" on a copy — so the
1542 // copy always starts cleared.
1543 struct AbortFlag {
1544 std::atomic<bool> value{false};
1545 AbortFlag() = default;
1546 AbortFlag(const AbortFlag&) noexcept {}
1547 AbortFlag(AbortFlag&&) noexcept {}
1548 AbortFlag& operator=(const AbortFlag&) noexcept {
1549 value.store(false, std::memory_order_relaxed);
1550 return *this;
1551 }
1553 value.store(false, std::memory_order_relaxed);
1554 return *this;
1555 }
1556 bool load(std::memory_order order) const { return value.load(order); }
1557 void store(bool v, std::memory_order order) { value.store(v, order); }
1558 };
1560 struct AbortRequested {}; // thrown inside the bar loops only
1562 if (abort_requested_.load(std::memory_order_relaxed)) throw AbortRequested{};
1563 }
1564
1565 std::vector<TraceEntryC> trace_buffer_;
1566 std::vector<std::string> trace_names_;
1567 std::unordered_map<std::string, int32_t> trace_name_index_;
1568
1569 int32_t intern_trace_name(const std::string& name);
1570
1577
1578 // Captured by the public run() wrappers when the underlying engine logic
1579 // throws. Cleared at the start of every run(). Surfaces through
1580 // last_error() / strategy_get_last_error() so the C ABI never
1581 // unwinds a C++ exception across the extern "C" boundary.
1582 std::string last_error_;
1583
1584 // Registers one aggregating evaluator: the requested timeframe's buckets
1585 // built from the run's evaluator input timeframe, passthrough when the
1586 // two are equal. Publication modes are not the kernel's: a source host
1587 // keeps its own per-sec_id semantics beside this state and a native
1588 // subscription's modes are the consumer's delivery rules.
1589 void register_security_eval(int sec_id, const std::string& requested_tf,
1590 const std::string& input_tf);
1591 // The one path to evaluate_security(): installs the requested context's
1592 // bar index for the evaluator's TA members (ta::bar_context()) for the
1593 // duration of the dispatch. `bar_index` is the 0-based index of the
1594 // requested-context bar being evaluated — the just-completed bucket for a
1595 // complete evaluation (eval_complete_count - 1), the in-progress bucket
1596 // for a partial one (eval_complete_count) — so every
1597 // compute()/recompute() dispatch of one requested bar rewrites the same
1598 // ring slot, and a conditional window call inside the security expression
1599 // is addressed exactly like TradingView addresses it.
1601 bool publish, int64_t bar_index);
1602 // The generic evaluator step for one input bar: aggregate, take the
1603 // native bar of a completed bucket where a feed serves the timeframe,
1604 // and dispatch the completed bucket. Publication at the last contributing
1605 // input is the consumer's; a source host composes its own step.
1606 void feed_security_eval_state(SecurityEvalState& state, const Bar& input_bar);
1607
1608 // A new batch run (including stream_begin's historical warmup) starts a
1609 // fresh script lifecycle. Called once after broker reset, before any
1610 // requested/chart computation or per-bar checkpoint. Generated subclasses
1611 // reset their own persistent state here, then optionally prepare the
1612 // current run's static TA cache. Config/inputs/feeds belong to the engine
1613 // and survive. Streaming ticks and fill recalculations never call this.
1614 virtual void prepare_script_run(const Bar*, int, bool) {}
1615
1617 virtual void evaluate_security(int sec_id, const Bar& bar, bool is_complete) {}
1618 virtual void clear_security(int sec_id) {}
1619
1620 // Generated-state transaction hooks for calc_on_order_fills. Snapshot is
1621 // called once before the broker walks a historical bar; restore precedes
1622 // every fill recalc and the ordinary close execution. The completed
1623 // ordinary-close execution becomes the committed checkpoint. Historical
1624 // post-C fill recalculations start from it, recompute its current-bar
1625 // history slot, and are rolled back after their broker effects persist.
1626 virtual void snapshot_script_state() {}
1627 virtual void restore_script_state() {}
1628 virtual void commit_script_state() {}
1629
1630 // Magnifier helpers
1631
1632
1633 virtual void finalize_bar() {}
1634
1635 // --- Equity extremes update (called after each on_bar) ---
1636 // NOTE: the dd/runup walk in src/engine_metrics.cpp (compute_equity_stats)
1637 // MUST mirror this trough-reset logic; keep in lockstep. The fold is one
1638 // per script calculation and the kernel performs it under EVERY report
1639 // policy (native_execution_consumer.cpp): these scalars measure the run,
1640 // not the report. A policy that also records the curve folds and appends
1641 // at one instant, so a re-walk of the curve through fold_equity_extreme
1642 // reproduces the scalars bit for bit (scheduler_record_range_end uses it).
1643 void fold_equity_extreme(double eq) {
1644 if (eq > max_equity_) {
1645 max_equity_ = eq;
1646 min_equity_ = eq; // reset trough on new peak
1647 }
1648 if (eq < min_equity_) {
1649 min_equity_ = eq;
1650 }
1651 double dd = max_equity_ - eq;
1652 if (dd > max_drawdown_) max_drawdown_ = dd;
1653 double ru = eq - min_equity_;
1654 if (ru > max_runup_) max_runup_ = ru;
1655 }
1658
1659 // --- Update max_contracts_held_* running peaks ---
1660 double abs_qty = std::abs(position_qty_);
1662 if (abs_qty > max_contracts_held_all_) max_contracts_held_all_ = abs_qty;
1664 max_contracts_held_long_ = abs_qty;
1666 max_contracts_held_short_ = abs_qty;
1667 }
1668 }
1669
1670 int report_equity_month_key(std::int64_t time_ms) const {
1671 // An out-of-domain no-curve attempt is unpublishable: the monthly
1672 // fold requests whole-report recovery. Avoid invoking the legacy
1673 // int month-key expression speculatively on that attempt.
1675 return 0;
1677 }
1678
1679 // Record one equity point per SCRIPT bar. ``script_bar_ts`` must be the
1680 // script-bar open timestamp captured BEFORE dispatch — current_bar_.timestamp
1681 // is overwritten by the magnifier sub-bar walk (engine_run.cpp), which would
1682 // make the curve differ between magnifier on/off.
1683 void record_equity_point(int64_t script_bar_ts) {
1684 // M05/M06 may have edited the preceding held slot since its append.
1685 // Consume that edit BEFORE either storage path releases the point.
1688 || metrics_streaming_.no_curve;
1689 if (summary ? equity_curve_.empty() : prepare_equity_point())
1692 p.time_ms = script_bar_ts;
1695 if (summary) {
1696 if (!equity_curve_.empty()) {
1697 const auto& held = equity_curve_.back();
1698 summary_equity_fold_.replace_last(
1699 held, report_equity_month_key(held.time_ms));
1700 auto& prefix = summary_broker_equity_prefix_;
1701 const double eq = held.equity;
1702 if (eq > prefix[0]) prefix[0] = prefix[1] = eq;
1703 if (eq < prefix[1]) prefix[1] = eq;
1704 const double dd = prefix[0] - eq;
1705 if (dd > prefix[2]) prefix[2] = dd;
1706 const double ru = eq - prefix[1];
1707 if (ru > prefix[3]) prefix[3] = ru;
1708 }
1712 if (equity_curve_.empty()) equity_curve_.push_back(p);
1713 else equity_curve_.back() = p;
1715 summary_equity_ratios_.reset();
1716 } else {
1718 }
1719 if (metrics_streaming_.fold) {
1720 metrics_streaming_.fold->append(
1722 ++metrics_streaming_.points;
1723 metrics_streaming_.receipt.stop_boundary.completed_marks = metrics_streaming_.points;
1724 metrics_streaming_.receipt.stop_boundary.kind =
1726 }
1727 metrics_streaming_.receipt_final = false;
1729 }
1730
1731 // --- Trade history accessors (for strategy.closedtrades.*) ---
1732 double closed_trade_profit(int index) const {
1733 if (index >= 0 && index < (int)trades_.size())
1734 return trades_.pnl_at(index);
1735 return 0.0;
1736 }
1737 double closed_trade_profit_percent(int idx) const {
1738 if (idx < 0 || idx >= (int)trades_.size()) return std::numeric_limits<double>::quiet_NaN();
1739 return trades_.pnl_pct_at(idx);
1740 }
1741 double closed_trade_commission(int idx) const {
1742 if (idx < 0 || idx >= (int)trades_.size()) return std::numeric_limits<double>::quiet_NaN();
1743 return trades_.commission_at(idx);
1744 }
1745 int closed_trade_entry_bar_index(int idx) const {
1746 if (idx < 0 || idx >= (int)trades_.size()) return na<int>();
1747 return trades_.entry_bar_index_at(idx);
1748 }
1749 int closed_trade_exit_bar_index(int idx) const {
1750 if (idx < 0 || idx >= (int)trades_.size()) return na<int>();
1751 return trades_.exit_bar_index_at(idx);
1752 }
1753 std::string closed_trade_entry_comment(int idx) const {
1754 if (idx < 0 || idx >= (int)trades_.size()) return std::string();
1755 return trades_.entry_comment_at(idx);
1756 }
1757 std::string closed_trade_exit_comment(int idx) const {
1758 if (idx < 0 || idx >= (int)trades_.size()) return std::string();
1759 return trades_.exit_comment_at(idx);
1760 }
1761 std::string closed_trade_entry_id(int idx) const {
1762 if (idx < 0 || idx >= (int)trades_.size()) return std::string();
1763 return trades_.entry_id_at(idx);
1764 }
1765 std::string closed_trade_exit_id(int idx) const {
1766 if (idx < 0 || idx >= (int)trades_.size()) return std::string();
1767 return trades_.exit_id_at(idx);
1768 }
1769 uint64_t closed_trade_entry_incarnation(int idx) const {
1770 if (idx < 0 || idx >= (int)trades_.size()) return 0;
1771 return trades_.entry_incarnation_at(idx);
1772 }
1773 double closed_trade_entry_price(int idx) const {
1774 if (idx < 0 || idx >= (int)trades_.size()) return std::numeric_limits<double>::quiet_NaN();
1775 return trades_.entry_price_at(idx);
1776 }
1777 double closed_trade_exit_price(int idx) const {
1778 if (idx < 0 || idx >= (int)trades_.size()) return std::numeric_limits<double>::quiet_NaN();
1779 return trades_.exit_price_at(idx);
1780 }
1781 int64_t closed_trade_entry_time(int idx) const {
1782 if (idx < 0 || idx >= (int)trades_.size()) return 0;
1783 return trades_.entry_time_at(idx);
1784 }
1785 int64_t closed_trade_exit_time(int idx) const {
1786 if (idx < 0 || idx >= (int)trades_.size()) return 0;
1787 return trades_.exit_time_at(idx);
1788 }
1789 double closed_trade_size(int idx) const {
1790 if (idx < 0 || idx >= (int)trades_.size()) return std::numeric_limits<double>::quiet_NaN();
1791 return trades_.qty_at(idx);
1792 }
1793 double closed_trade_max_runup(int idx) const {
1794 if (idx < 0 || idx >= (int)trades_.size()) return 0.0;
1795 return trades_.max_runup_at(idx);
1796 }
1797 // Percent excursions: trade.max_runup / max_drawdown are stored in
1798 // account currency (× pointvalue, see build_close_trade_with_costs), so the entry
1799 // cost denominator must be in currency too (entry × qty × pointvalue).
1800 // pointvalue=1 cancels out and matches the legacy ratio bit-for-bit.
1801 double closed_trade_max_runup_percent(int idx) const {
1802 if (idx < 0 || idx >= (int)trades_.size()) return 0.0;
1803 double cost = trades_.entry_price_at(idx) * trades_.qty_at(idx) * syminfo_.pointvalue;
1804 return (cost > 0.0) ? (trades_.max_runup_at(idx) / cost) * 100.0 : 0.0;
1805 }
1806 double closed_trade_max_drawdown(int idx) const {
1807 if (idx < 0 || idx >= (int)trades_.size()) return 0.0;
1808 return trades_.max_drawdown_at(idx);
1809 }
1811 if (idx < 0 || idx >= (int)trades_.size()) return 0.0;
1812 double cost = trades_.entry_price_at(idx) * trades_.qty_at(idx) * syminfo_.pointvalue;
1813 return (cost > 0.0) ? (trades_.max_drawdown_at(idx) / cost) * 100.0 : 0.0;
1814 }
1815
1816 // --- Direction accessors ---
1817 std::string closed_trade_direction(int idx) const {
1818 if (idx < 0 || idx >= (int)trades_.size()) return "";
1819 return trades_.is_long_at(idx) ? "long" : "short";
1820 }
1821 std::string open_trade_direction(int idx) const {
1822 if (position_side_ == PositionSide::FLAT || idx < 0 || idx >= (int)pyramid_entries_.size()) return "";
1823 return (position_side_ == PositionSide::LONG) ? "long" : "short";
1824 }
1825
1826 // --- Open position trade accessors (strategy.opentrades.*) ---
1827 double open_trade_profit(int idx) const;
1828 double open_trade_profit_percent(int idx) const;
1829 double open_trade_commission(int idx) const;
1830 int open_trade_entry_bar_index(int idx) const;
1831 std::string open_trade_entry_comment(int idx) const;
1832 std::string open_trade_entry_id(int idx) const;
1833 double open_trade_entry_price(int idx) const;
1834 int64_t open_trade_entry_time(int idx) const;
1835 double open_trade_size(int idx) const;
1836 double open_trade_max_drawdown(int idx) const;
1837 double open_trade_max_drawdown_percent(int idx) const;
1838 double open_trade_max_runup(int idx) const;
1839 double open_trade_max_runup_percent(int idx) const;
1840
1841 std::string position_entry_name() const {
1842 if (position_side_ == PositionSide::FLAT || pyramid_entries_.empty()) return "";
1843 return pyramid_entries_.back().entry_id;
1844 }
1845
1846 double max_drawdown_percent() const {
1847 return (initial_capital_ > 0.0) ? (max_drawdown_ / initial_capital_) * 100.0 : 0.0;
1848 }
1849
1850protected:
1852 const execution::Action& action, const execution::Fill& fill,
1853 const execution::LifecycleEffects& lifecycle,
1855 execution::CloseScope scope);
1857 const execution::Action& action, const execution::Fill& fill,
1858 const execution::LifecycleEffects& lifecycle,
1860 const execution::SelectedOpeningSet& selection);
1862 const execution::Action& action,
1863 const execution::Fill& fill,
1864 execution::CloseScope book_or_opening,
1865 const execution::SelectedOpeningSet* selected, double fx) const;
1867 const execution::Action& action,
1868 const execution::Fill& fill,
1869 execution::CloseScope book_or_opening,
1870 const execution::SelectedOpeningSet* selected, double fx) const;
1872 const execution::Action& action, const execution::Fill& fill,
1873 const execution::LifecycleEffects& lifecycle,
1875 execution::CloseScope book_or_opening,
1876 const execution::SelectedOpeningSet* selected);
1877 // Runtime-private preview of the exact existing pre-source prefix. The
1878 // caller pins the quoted ticket before entering this method.
1880 const execution::Action& action, const execution::Fill& fill,
1883 execution::AccountEffectProjection& account, std::vector<double>& row_pnl) const;
1885 const execution::ReverseTo& reversal, const execution::Fill& fill,
1887 execution::AccountEffectProjection& account, std::vector<double>& row_pnl) const;
1888 struct NativeSettlementStage;
1889 struct NativeSettlementRows;
1892 NativeSettlementStage& stage,
1893 const execution::CloseScope& book_or_opening,
1894 double& remaining) const;
1896 NativeSettlementStage& stage, const execution::Fill& fill) const;
1898 const NativeSettlementStage& stage, const execution::Fill& fill) const;
1900 const NativeSettlementStage& stage, const execution::Fill& fill) const;
1902 NativeSettlementStage& stage, const execution::Fill& fill,
1903 const execution::LifecycleEffects& lifecycle,
1906 const NativeSettlementStage& stage, const execution::Fill& fill,
1908 NativeSettlementRows& rows) const;
1910 const NativeSettlementStage& stage, const execution::Fill& fill,
1912 NativeSettlementRows& rows) const;
1914 const NativeSettlementStage& stage,
1915 const execution::LifecycleEffects& lifecycle,
1916 const NativeSettlementRows& rows);
1918 NativeSettlementStage& stage, const execution::Fill& fill,
1919 const execution::LifecycleEffects& lifecycle,
1921 NativeSettlementRows& rows);
1923 NativeSettlementStage& stage, const execution::Fill& fill,
1924 const execution::LifecycleEffects& lifecycle,
1927 NativeSettlementStage& stage,
1928 const execution::Action& action,
1929 const execution::Fill& fill,
1930 execution::CloseScope book_or_opening,
1931 const execution::SelectedOpeningSet* selected,
1932 const execution::LifecycleEffects* lifecycle, double fx) const;
1934 NativeSettlementStage& stage,
1935 const execution::ReverseTo& reversal,
1936 const execution::Fill& fill,
1937 const execution::LifecycleEffects* lifecycle, double fx) const;
1938 void append_quoted_lot(PyramidEntry lot, double total_qty, double average_price);
1939 // Opens a new position cycle with `lot` as its only lot: a fresh cycle
1940 // id, the per-position state reset, and the stream observation recorded.
1942
1944 void validate_close_trade_counters(const Trade* rows, size_t count) const;
1945 // Quote one resolved execution's current charges, a percent schedule at
1946 // the account-currency rate `fx` its stage converts at. Entry costs on the
1947 // closed rows are historical allocations. Returns close shares in FIFO
1948 // order followed by the opening share (zero when there is no opening).
1949 std::vector<double> quote_execution_commissions(
1950 const std::vector<double>& closed_units, double opening_units,
1951 const execution::Fill& fill, double fx) const;
1952 // The Trade row a close of ``close_qty`` of ``pe`` at ``fill_price`` on
1953 // the current bar would record (pnl, pnl_pct, commission, excursions, bar
1954 // indexes), without its bookkeeping: the settlement builds it here and
1955 // commits it through record_close_trade; the range-end report row builds
1956 // only.
1957
1959 double fill_price, bool was_long, double entry_commission,
1960 double exit_commission,
1961 const execution::PhysicalExecutionContext& context) const;
1962 // A terminal-row read supplies its retained answer (or the kernel model)
1963 // and never consults the host again. Settlement uses the overload above.
1965 double fill_price, bool was_long, double entry_commission,
1966 double exit_commission,
1968 const ClosedLotExcursion* retained_excursion, bool consult_excursion_hook) const;
1970 // Reset ALL per-run state (trades, accumulators, position, pending orders,
1971 // equity extremes, risk latches, intraday/day counters, source-series
1972 // history) so a reused handle's run N is bit-identical to a fresh handle's
1973 // run 1. Preserves configuration (initial_capital_, slippage_,
1974 // commission_*, syminfo_, inputs_) — those are set before run() and must
1975 // survive it. Called at the top of every
1976 // run() loop entrypoint. See tests/test_handle_reuse_reset.cpp.
1978 double account_currency_fx_at(int64_t timestamp_ms) const;
1980
1981 // Native HTF feed routing (engine_aux_security.cpp): per-state label maps
1982 // built after the evaluators' aggregators exist for this run, and the
1983 // substitution a completed bucket applies. Returns whether `bar` was
1984 // replaced by its native sibling.
1985 void prepare_native_security_feeds(const Bar* input_bars, int n_input);
1986 void prepare_chart_day_partition(const Bar* input_bars, int n_input);
1987 const std::string& report_scratch_directory() const noexcept { return report_sink_state_.scratch_dir; }
1989 bool count_miss = true);
1990
1991#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
1993 virtual void source_aux_security_input_view(const Bar*& bars, int& n) const;
1997#endif
1998
2000 virtual void source_stream_entry_comment(const PyramidEntry&, std::string&) const;
2001 void stream_observe_exit(size_t trade_index);
2002 void stream_refresh_action_metadata(size_t first_action, size_t first_trade);
2003
2004 // fill_report helpers (defined in engine_report.cpp).
2006 virtual void present_report(ReportC*) const {}
2007 // Host declaration, not an override detector: true covers the engine's
2008 // empty presenter and is inherited by native subclasses. Any host that
2009 // overrides present_report MUST also override this hook: return false
2010 // until its exact bounded-state adapter supports non-FULL capture.
2011 // Eligibility must be known before begin; inheriting true does not prove
2012 // an arbitrary whole-array presenter eligible.
2013 virtual bool report_presenter_supports_non_full() const noexcept { return true; }
2016 void fill_trace_section(ReportC* out) const;
2017
2018 void guard_native_mutation(const char* operation);
2019
2021 bool native = false;
2022 mutable std::unique_ptr<IExecutionConsumer> ptr;
2025 : native(other.native) {}
2027 if (this != &other) {
2028 native = other.native;
2029 ptr.reset();
2030 }
2031 return *this;
2032 }
2033 ExecutionConsumerSlot(ExecutionConsumerSlot&& other) noexcept : native(other.native) {
2034 other.ptr.reset();
2035 }
2037 if (this != &other) {
2038 native = other.native;
2039 ptr.reset();
2040 other.ptr.reset();
2041 }
2042 return *this;
2043 }
2044 };
2046
2047public:
2050 bool native_bound() const;
2051 virtual void on_bar(const Bar& bar) = 0;
2052
2053 // All run() overloads preflight the entire chart array before modifying
2054 // configuration, resetting state, preparing scripts or dispatching bars.
2055 // Require n >= 0, a non-null pointer when n > 0, finite enclosed OHLC,
2056 // volume that is finite >= 0 or NaN (unavailable), and strictly increasing
2057 // timestamps with representable positive int64 deltas. Finite signed/zero
2058 // prices, off-grid prices and gaps are structurally admitted, without a
2059 // guarantee about their financial or extreme-calendar arithmetic.
2060 // Rejection sets last_error() and preserves state except entry-time error,
2061 // run-status and abort bookkeeping. n == 0 retains empty-new-run semantics.
2062 // Processing exceptions after preflight do not imply state rollback.
2063 void run(const Bar* bars, int n);
2064
2065 void run(const Bar* input_bars, int n_input,
2066 const std::string& input_tf,
2067 const std::string& script_tf,
2068 bool bar_magnifier = false,
2069 int magnifier_samples = 4,
2071
2072#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
2073 // Copies a finer request.security-only feed for subsequent historical
2074 // runs. n == 0 clears it. Validation that depends on native chart bars is
2075 // intentionally deferred to run(), where failures reach last_error().
2076 virtual bool set_aux_security_feed(const Bar* bars, int n,
2077 const std::string& input_tf);
2078#endif
2079
2080 // Copies the exchange's own bars of one higher timeframe for the
2081 // request.security evaluators that request exactly it (see
2082 // strategy_set_native_security_feed in pineforge.h). n == 0 clears that
2083 // timeframe's feed. Routing is built per run once the evaluators exist.
2084 bool set_native_security_feed(const std::string& timeframe,
2085 const Bar* bars, int n);
2087 return !native_security_feeds_.empty();
2088 }
2089 // Per-run diagnostics: completed buckets that took a native bar, and
2090 // completed buckets of a fed timeframe that found none (kept aggregate).
2094 int64_t native_security_misses() const {
2096 }
2097 // Whether the last run built the chart symbol's native daily partition
2098 // (an intraday chart with a native "D" feed installed).
2100 return !chart_day_partition_.empty();
2101 }
2102
2103 // Another symbol's bars, facts and recorded request values, for a source
2104 // host whose request sites name them (strategy_set_symbol_feed and its
2105 // three siblings in pineforge.h). These are that host's doors: the kernel
2106 // base keeps none of it and answers false without a word, as it does for
2107 // set_aux_security_feed. A host with no source layer installs another
2108 // instrument's bars in its run spec (NativeRunSpec::instrument_feeds).
2109 virtual bool set_symbol_feed(const std::string& key, const std::string& timeframe,
2110 const Bar* bars, const int64_t* close_ms, int n);
2111 virtual bool set_symbol_feed_column(const std::string& key, const std::string& timeframe,
2112 const std::string& name, const double* values, int n);
2113 virtual bool set_symbol_facts(const std::string& key, const std::string& field,
2114 const std::string& value);
2115 virtual bool set_recorded_series(const std::string& key, const int64_t* chart_open_ms,
2116 const double* values, int n);
2117
2118 // Execute confirmed historical bars, then keep this exact instance alive
2119 // for realtime trade updates. The warmup feed must contain at least one
2120 // complete input-timeframe bar. Normalized ticks begin at or after the next
2121 // input bar's open; in-session gaps are materialized as zero-volume
2122 // carry-forward bars when a later tick or stream_advance_time() crosses
2123 // their close boundary. Configured out-of-session intervals are skipped.
2124 bool stream_begin(const Bar* warmup_bars, int n_warmup,
2125 const std::string& input_tf,
2126 const std::string& script_tf = "");
2127 // Confirmed input bars use the same OHLC broker kernel as batch. The first
2128 // tick/bar locks the input mode; mixing them is rejected. Missing in-session
2129 // bars are rejected rather than padded. Closed-session gaps may be skipped.
2130 bool stream_push_bar(const Bar& bar);
2131 bool stream_push_tick(const TradeTick& tick);
2132 bool stream_push_ticks(const TradeTick* ticks, int n);
2133 bool stream_advance_time(int64_t timestamp_ms);
2134 bool stream_end(bool finalize_partial_input_bar = false);
2136 int stream_order_actions_len() const { return static_cast<int>(stream_order_actions_.size()); }
2138 return stream_order_actions_.at(static_cast<size_t>(i));
2139 }
2141 // Observable broker + stream cursors/forming bars, not a serialization of
2142 // arbitrary strategy members. Deterministic strategy code is required.
2143 uint64_t stream_state_hash() const;
2144
2145 // Install an effective-time FX curve (account-currency units per one unit
2146 // of symbol quote currency). Points are copied and must have strictly
2147 // increasing epoch-ms timestamps plus positive finite rates. The latest
2148 // point whose timestamp is <= the current broker event is active. Passing
2149 // n=0 clears the curve and restores the scalar metadata fallback.
2150 bool set_account_currency_fx_series(const int64_t* timestamps_ms,
2151 const double* rates, int n);
2152
2153 void run(const Bar* input_bars, int n_input,
2154 const std::string& input_tf,
2155 const std::string& script_tf,
2156 const std::unordered_map<std::string, std::string>& inputs,
2157 const SymInfo& syminfo,
2158 // Opaque host-override handle: the kernel only forwards it to
2159 // prepare_native_begin() as NativeBeginArgs::overrides_opaque
2160 // and never dereferences it. A host layer that defines its own
2161 // override record passes its address and casts it back there.
2162 const void* overrides = nullptr,
2163 bool bar_magnifier = false,
2164 int magnifier_samples = 4,
2166
2167 int trade_count() const { return (int)trades_.size(); }
2168 Trade get_trade(int i) const { return trades_.read(static_cast<std::size_t>(i)); }
2169 // Read-only run diagnostics, outside reports and broker-state hashes.
2170 int report_cache_stats(std::uint64_t* resident_bytes,
2171 std::uint64_t* allocation_bypass_count) const noexcept;
2172 // The same closed rows under the `closed_trade_*` name a host reads them
2173 // by (RP6). The per-field `closed_trade_*` family below stays protected
2174 // Pine plumbing: one owning row value answers all of it, and a host
2175 // that owns its report needs the row, not twenty wrappers. Range-end
2176 // report rows are NOT here — report_trade_count() / get_report_trade()
2177 // below span both spaces. Index against closed_trade_count(); reads below
2178 // base refuse with engine_invariant only on an admitted retiring sink run.
2179 std::size_t closed_trade_count() const noexcept { return trades_.size(); }
2180 Trade closed_trade(std::size_t i) const { return trades_.read(i); }
2181 // Newest first, preserving global indices across resident and external
2182 // rows. Start at closed_trade_count(); crossing a retired base refuses.
2183 bool previous_closed_trade(std::size_t& cursor, Trade& value) const {
2184 return trades_.previous(cursor, value);
2185 }
2186 // The REPORT's row space: trades_ followed by range_end_trades_, in the
2187 // order fill_trades_section lays pf_report_t::trades out. trade_count()
2188 // / get_trade() stay the Pine-visible closed trades (strategy.closedtrades
2189 // never sees a range-end row); the report-indexed C ABI accessors
2190 // (strategy_closed_trade_entry_incarnation) must index THIS space, or
2191 // every range-end row reads as index-out-of-range (round-4b F3).
2193 return (int)(trades_.size() + range_end_trades_.size());
2194 }
2195 // A range-end row is rebuilt from its terminal lot mark on every value read.
2197 const int n_closed = (int)trades_.size();
2198 return i < n_closed ? trades_[(size_t)i]
2199 : range_end_trades_.row((std::size_t)(i - n_closed));
2200 }
2201 // The legacy C string ABI promises pointer lifetime until the next run.
2202 // Asking for that borrowed storage explicitly materializes the requested
2203 // row. Ordinary report/value readers never enter this compatibility path.
2204 const Trade& materialize_report_trade(int i) const {
2205 const int n_closed = static_cast<int>(trades_.size());
2206 return i < n_closed ? trades_.materialize(static_cast<std::size_t>(i))
2207 : range_end_trades_.materialize(static_cast<std::size_t>(i - n_closed));
2208 }
2209 // Why a report row closed, from the row's own recorded facts; the report
2210 // index form below adds the row-space bounds. Kept together so a caller
2211 // holding a row value does not need a reference into the row space.
2212 static int classify_report_close_cause(const Trade& row);
2213
2214 // ABI v4 live-runtime surface (task 9): classify why a REPORT-row
2215 // closed trade exited -- report-row scope (spans trades_ then
2216 // range_end_trades_, like get_report_trade above), so this cannot reuse
2217 // the existing protected closed_trade_* names below (strategy.
2218 // closedtrades.* scope: trades_ only, std::string returns). 0 UNKNOWN
2219 // (bad index), 1 SCRIPT, 2 BRACKET, 3 MARGIN_CALL, 4 INTRADAY_LOSS_CAP,
2220 // 5 INTRADAY_FILL_CAP, 6 RANGE_END. Defined in engine_trade_accessors.cpp;
2221 // see strategy_closed_trade_close_cause (pineforge.h) for the exact
2222 // derivation order.
2224
2225 // --- Position-size extremes (strategy.max_contracts_held_*) ---
2229
2230 // --- Even-trade count (strategy.eventrades) ---
2231 int eventrades() const { return eventrades_count_; }
2232
2233 void fill_report(ReportC* out) const;
2235
2236 // O1 report envelope v1 (include/pineforge/report_mode.h). Select before
2237 // execution; inspect the returned status constant (PF_REPORT_MODE_ACCEPTED
2238 // on success). Rejection is atomic. SUMMARY_ONLY must never fall back to
2239 // FULL.
2240 //
2241 // SUMMARY_ONLY and STREAMED keep the logical trade total and the exact
2242 // folds, and return no resident TradeC array: trade rows are rebuilt one
2243 // at a time from the closed ledger and the terminal lot mark.
2245 // Streaming-ratio report-only routing and diagnostics.
2246 // New engines default to streaming Sharpe On. Explicit Off keeps the
2247 // exact curve route when SharpeBar is needed; Returns always keeps it.
2248 // Preferences apply at the next begin; an existing receipt stays bound
2249 // to the configuration of the attempt it describes.
2252 // The receipt also observes the final metric equity mark when nonempty;
2253 // this observation never participates in the planned extent's identity.
2259 // Recovery is a separate host proof, never implied by PLAIN. A prepared
2260 // no-curve adapter MUST resolve a LegacyRerun receipt before publication;
2261 // that speculative attempt exposes no owning report until resolution.
2265 // Initialize metadata.struct_size/version before reading. A successful
2266 // read assembles through fill_report; without an execution observer or selected generation, FULL and STREAMED present on every read; the first STREAMED read after a completed run writes the sink.
2267 // Release the owned payload with free_report(&out->report).
2269
2270 // Returns the error message captured by the most recent run() if it
2271 // failed, or an empty string if the run completed normally. Cleared at
2272 // the start of every run(). The C ABI exposes this via
2273 // strategy_get_last_error().
2274 const std::string& last_error() const { return last_error_; }
2275
2276 // Per-input override (title -> serialized value). Must be set before run()
2277 // so get_input_*() lookups pick up the TV-tester value rather than the
2278 // Pine default.
2279 void set_input(const std::string& key, const std::string& value) {
2280 guard_native_mutation("set_input");
2281 inputs_[key] = value;
2282 }
2284 guard_native_mutation("clear_inputs");
2285 inputs_.clear();
2286 }
2287 void set_trade_start_time(int64_t timestamp_ms) {
2288 guard_native_mutation("set_trade_start_time");
2289 trade_start_time_ = timestamp_ms;
2290 }
2291
2292 // Set the chart's display timezone. Stored in a dedicated slot so it
2293 // does NOT clobber ``syminfo_.timezone`` (the symbol/exchange TZ).
2294 //
2295 // Pre-fix this method wrote the chart TZ into ``syminfo_.timezone``,
2296 // which the codegen reads as the default tz argument of the 1-arg
2297 // ``hour(time)`` / ``minute(time)`` / ``dayofweek(time)`` form. That
2298 // conflated two distinct TV concepts and silently shifted the result
2299 // by the chart-vs-exchange offset (e.g. Asia/Taipei vs UTC = +8h)
2300 // for crypto symbols. The shift cascaded into ``hour``-bucketed
2301 // accumulators — see
2302 // ``validation_typed_matrix/typed-matrix-probe-01-bool-regime-mask``,
2303 // whose 24x7 ``matrix<bool>`` regime mask filled in 8 hours earlier
2304 // than TV and produced ~9% trade-count divergence (TV 773, engine 714
2305 // before this fix; ~778 after).
2306 //
2307 // TV semantics (Pine v6 reference docs):
2308 // * Bare variable ``hour`` / ``minute`` / ``dayofweek``: exchange
2309 // timezone (``syminfo.timezone``). Already correct via
2310 // ``_decompose_bar_time()``'s hardcoded ``gmtime_r``, which
2311 // matches the corpus' ETH-USDT (UTC) data.
2312 // * 1-arg function form ``hour(time)``: defaults its tz arg to
2313 // ``syminfo.timezone`` (NOT the chart display TZ). With this
2314 // change, ``syminfo_.timezone`` retains its constructor default
2315 // ("UTC") and the codegen lambda lands on the cheap gmtime_r
2316 // branch — matching TV.
2317 // * 2-arg function form ``hour(time, tz)``: honours the explicit
2318 // argument, unchanged by this fix.
2319 void set_chart_timezone(const std::string& tz) {
2320 guard_native_mutation("set_chart_timezone");
2321 chart_timezone_ = tz;
2322 }
2323 const std::string& chart_timezone() const { return chart_timezone_; }
2324
2325 // --- Symbol metadata injection (data feed → syminfo.*) ---
2326 // The exchange timezone + session feed session.ismarket / time(session)
2327 // predicates. They default to UTC / 24x7 (crypto); a data feed pushes
2328 // the real values via these setters before run().
2329 //
2330 // The source-layer intraday cap consumes a valid explicit session and this
2331 // timezone through its risk-day policy. Other risk-day rules keep
2332 // chart_timezone_, as do continuous/unconfigured order-counter clocks;
2333 // the existing crypto-on-shifted-chart contract therefore remains intact.
2334 void set_syminfo_timezone(const std::string& tz) {
2335 guard_native_mutation("set_syminfo_timezone");
2336 syminfo_.timezone = tz;
2337 }
2338 void set_syminfo_session(const std::string& s) {
2339 guard_native_mutation("set_syminfo_session");
2340 syminfo_.session = s;
2341 }
2342 // ``syminfo.type`` ("crypto" default; "forex" / "stock" / "futures" /
2343 // "index" / "fund" / "cfd" per TradingView). Scripts branch on it for
2344 // instrument conventions — the canonical one being the pip size
2345 // (``syminfo.type == "forex" ? 0.0001 : syminfo.mintick``), which on a
2346 // 5-digit FX symbol under the crypto default computed every pip-scaled
2347 // stop/target 10x too tight (finding 454). Empty is ignored.
2348 void set_syminfo_type(const std::string& t) {
2349 guard_native_mutation("set_syminfo_type");
2350 if (!t.empty()) syminfo_.type = t;
2351 }
2352 /// Whether TradingView's session template for this symbol carries the
2353 /// exchange's early closes and holidays, so a D/W/M request.security
2354 /// bucket completes on a shortened session's actual last chart bar
2355 /// (TimeframeAggregator::set_early_close_completes): exchange-listed
2356 /// kinds (stock, futures, index, fund, dr, ...) yes -- NYSE:F's 12:45 ET
2357 /// half-day bar and CME's 11:45 CT early-close bar are pinned; the OTC
2358 /// quote streams -- forex, cfd, crypto -- no: their period ends at the
2359 /// nominal close and a session ending early completes lazily on the
2360 /// next period's first bar (OANDA:XAUUSD 15m, lab tv oanda pin, ledger
2361 /// log-20260905t034240z-30be11fe). syminfo.type comes from the harness
2362 /// (strategy_set_syminfo_type <- PINEFORGE_VERIFY_SYMTYPE); the
2363 /// constructor default "crypto" keeps an untyped run on the lazy rule.
2365 // Generic string-field injection for the remaining OHLCV-less syminfo
2366 // members (ticker / tickerid / currency / basecurrency / description /
2367 // volumetype / type). Unknown keys and empty values are ignored; returns
2368 // true when a field was set.
2369 bool set_syminfo_string(const std::string& key, const std::string& value) {
2370 guard_native_mutation("set_syminfo_string");
2371 if (value.empty()) return false;
2372 if (key == "type") { syminfo_.type = value; return true; }
2373 if (key == "ticker") { syminfo_.ticker = value; return true; }
2374 if (key == "tickerid") { syminfo_.tickerid = value; return true; }
2375 if (key == "currency") { syminfo_.currency = value; return true; }
2376 if (key == "basecurrency") { syminfo_.basecurrency = value; return true; }
2377 if (key == "description") { syminfo_.description = value; return true; }
2378 if (key == "volumetype") { syminfo_.volumetype = value; return true; }
2379 return false;
2380 }
2381 // Runtime syminfo injection (by design — the engine stores no instrument
2382 // metadata of its own; the harness supplies it per run). mintick drives the
2383 // directional fill snap + slippage*tick economics; pointvalue is the
2384 // futures $-per-point multiplier applied to every money path (realized
2385 // PnL + excursions, open profit / equity, percent/cash sizing, percent
2386 // commission notionals, margin check — see tests/test_pointvalue.cpp).
2387 // Both default to crypto/equity values (0.01 / 1.0) and only matter when the
2388 // harness sets a non-default instrument.
2389 void set_syminfo_mintick(double m) {
2390 guard_native_mutation("set_syminfo_mintick");
2391 if (m > 0.0) { syminfo_.mintick = m; syminfo_mintick_ = m; }
2392 }
2393 void set_syminfo_pointvalue(double pv) {
2394 guard_native_mutation("set_syminfo_pointvalue");
2395 if (pv > 0.0) { syminfo_.pointvalue = pv; }
2396 }
2397
2398 virtual void set_syminfo_metadata(const std::string& key, double value);
2399
2400 // Returns the script's active timeframe string (e.g. "15" for 15-minute,
2401 // "D" for daily). Backs timeframe.main_period in generated Pine v6 code.
2402 const std::string& main_period() const { return script_tf_; }
2403
2404 // Host run-mode overrides behind the frozen C setters
2405 // strategy_set_realtime_tail / strategy_set_probe_suppress_tail_logic
2406 // (pineforge.h). The kernel keeps no forming-tail state of its own: a
2407 // batch is complete input and the forward path is the stream, so
2408 // "the last fed bar is still forming" and "run only the broker steps on
2409 // the last bar" are a host's live-probe protocol, not a kernel mode. A
2410 // host that models one (source::PineStrategyHost, for the live runner's
2411 // probe / settle cycle) overrides both, owns the state and answers true.
2412 // The kernel default keeps the C ingress contract these setters always
2413 // had on a bare host -- accepted before begin, guarded like every other
2414 // setter, and inert (no kernel path ever read the flags on a native
2415 // host) -- so it stores nothing, leaves last_error() untouched and
2416 // answers false, meaning "no such mode here". R5 lane N14: the flags
2417 // were BacktestEngine members until then.
2418 virtual bool set_realtime_tail(bool on, int horizon_bars);
2419 virtual bool set_probe_suppress_tail_logic(bool on);
2420
2421 // Force this run's intrabar path order (ABI v4 live-runtime surface,
2422 // task 4): 0 AUTO (the unchanged |H-O| vs |O-L| rule), 1 HIGH_FIRST
2423 // (O -> H -> L -> C), 2 LOW_FIRST (O -> L -> H -> C). Any other value is
2424 // clamped to AUTO. A live probe runs the SAME forming bar under both
2425 // forced orders and keeps only the fills that agree between the two --
2426 // a fill that depends on which leg TradingView's own still-forming bar
2427 // will resolve to is path-dependent and must be suppressed rather than
2428 // guessed.
2429 // Persistent configuration, like set_realtime_tail -- stays set until a
2430 // caller passes mode=0. It only stores the mode: a native-bound source
2431 // provider projects it into NativeRunSpec::path_order at begin (a bare
2432 // native host declares that field itself), and the native driver walks
2433 // the spec field, handing the declared order to the intrabar sampler
2434 // itself (internal::sample_price_path_ordered).
2435 // Default AUTO (mode=0): every historical run stays byte-identical to
2436 // before this flag existed.
2437 void set_path_order(int mode) {
2438 guard_native_mutation("set_path_order");
2439 path_order_mode_ = (mode == 1 || mode == 2) ? mode : 0;
2440 }
2441
2442 // Live runtime G1 (spec §3.4): a deterministic, order-independent
2443 // FNV-1a 64 hash over every piece of broker state that decides the next
2444 // bar's fills (position, book, pyramid lots, trail scalars, cycle/
2445 // intraday/risk latches, frozen sizing, equity sums). Two engines with
2446 // equal broker state hash equally regardless of unordered-container
2447 // insertion history; any difference in that state changes the hash.
2448 // Implemented in engine_state_hash.cpp; coverage of the marked
2449 // broker-state region(s) below (grep this file for "broker-state") is
2450 // enforced by scripts/check_broker_state_hash_coverage.py.
2451 uint64_t broker_state_hash() const;
2452
2453 // The live position's volume-weighted average entry price
2454 // (position_entry_price_; 0 when flat -- the engine keeps 0 there, the
2455 // C ABI reports NaN when flat), its cycle id (position_cycle_seq_; 0
2456 // when flat, a fresh nonzero id per open/reversal, kept across
2457 // same-direction adds) and the trail extreme the exit trail legs ride
2458 // (trail_best_price_; NaN until a position fills).
2460 int64_t position_cycle_seq() const { return position_cycle_seq_; }
2461 double trail_best_price() const { return trail_best_price_; }
2462 // ABI v4 live-runtime surface (task 9): public forwarders for the C
2463 // ABI, which -- being extern "C" free functions -- cannot reach the
2464 // protected signed_position_size() / current_equity() above.
2465 // signed_position_size() is strategy.position_size (freeze-aware:
2466 // reads the pre-close position while a same-bar POOC close is frozen).
2467 // current_equity() is initial capital plus realized net profit
2468 // (strategy.initial_capital + strategy.netprofit). NOT Pine's
2469 // strategy.equity, which adds open profit on top of this (see the
2470 // sizing_equity formula and the equity-curve remark below, both
2471 // current_equity() + open_profit(...)).
2472 double initial_capital() const;
2473 virtual double live_position_size() const {
2476 return 0.0;
2477 }
2478 // The live-runtime observation seam (ABI v4, spec 3.6): read-only views
2479 // of the arbitration this run's last bar decided, of the resting-order
2480 // book in force for the next bar and of the engine-computed derived
2481 // values of one resting order. Every one is a pure const read, so a run
2482 // is byte-identical whether or not a caller reads it. The kernel's own
2483 // answer is "not available" (0 / -1 / NaN): the host that owns the book
2484 // overrides them, and the C surface documents what each value means
2485 // (strategy_last_bar_dual_entry_path, strategy_pending_orders_len,
2486 // strategy_pending_order_get, strategy_pending_order_fill_qty,
2487 // strategy_pending_order_level_resolved,
2488 // strategy_pending_order_effective_levels, strategy_trail_best_price --
2489 // include/pineforge/pineforge.h).
2491 virtual int observe_pending_count_v1() const;
2492 virtual int observe_pending_copy_v1(int index, pf_pending_order_v1_t* out) const;
2493 virtual int observe_probe_fill_qty(int index, double fill_price, double* qty,
2494 int* close_only, int* partition) const;
2495 virtual int observe_pending_level_resolved(int index) const;
2496 virtual int observe_pending_effective_levels(int index, double* stop,
2497 double* limit,
2498 double* trail_activation) const;
2499 virtual double observe_trail_best_price_v1() const; virtual bool set_symbol_feed_borrowed(const std::string&, const std::string&, const Bar*, const int64_t*, int) { guard_native_mutation("set_symbol_feed_borrowed"); return false; }
2500 double live_current_equity() const { return current_equity(); }
2501 // ABI v4 live-runtime surface (task 9): total SCRIPT bars dispatched by
2502 // the most recent run() (mirrors pf_report_t::script_bars_processed,
2503 // engine_report.cpp), including the stream warmup leg and every
2504 // realtime tick-driven bar after strategy_stream_begin.
2506
2507 // ABI v4 live-runtime surface (task 6): when on, every report point
2508 // appends broker_state_hash() to broker_state_hashes_
2509 // immediately after that bar's record_equity_point() call -- under
2510 // KernelRecorded the execution consumer's record_script_report_point,
2511 // which every script calculation it delivers reaches (confirmed,
2512 // intrabar and aggregated bars, in a batch run and across a stream's
2513 // warmup and realtime legs); a host that records or marks its own report
2514 // points appends its own row at each -- so a completed run's recorded
2515 // array has one row per report point (and, under KernelRecorded, matches
2516 // script_bars_processed and pf_report_t::broker_state_hash_len 1:1) --
2517 // including on strategy_stream_fill_report, whose report is the
2518 // cumulative warmup + realtime run. Default off: broker_state_hashes_
2519 // stays empty, fill_report emits a null/zero-length array, and every
2520 // historical run stays byte-identical to before this flag existed.
2521 // reset_run_state() clears the recorded array on every run() (the
2522 // warmup leg of stream_begin included) regardless of this flag's
2523 // value; the flag itself is persistent configuration, like
2524 // set_realtime_tail, so it must be set BEFORE stream_begin to also
2525 // cover the warmup bars.
2526 //
2527 // What a row is, and what it is for. A row is the run's CONTINUATION
2528 // IDENTITY at that bar, not its trade outcome: broker_state_hash() is
2529 // broker_state_hash_from_execution_hash(continuation_hash()), so it folds
2530 // the kernel's broker state AND, ahead of it, the state a resume would
2531 // continue from. NativeRunPhase (Batch/Warmup/Realtime, readable as
2532 // native_state().phase) is part of that continuation on purpose, because a
2533 // consumer mid-warmup and one mid-realtime are not interchangeable
2534 // continuations. So the array is a replay check WITHIN one driving mode
2535 // and deliberately not across modes:
2536 // * same driving -- two runs driven the same way record the same rows,
2537 // and the row after bar k is the last row of a run driven the same way
2538 // that ended at bar k (a batch and a stream at any warmup split);
2539 // * different driving -- run(), stream_begin(warmup=1)+push and
2540 // stream_begin(warmup=all) over the same bars booking the same trades
2541 // record different rows from index 0; only the length identity above
2542 // survives. Without subscriptions, streams share their Warmup prefix.
2543 // * the broker half alone IS driving-mode invariant: factor the
2544 // continuation out with broker_state_hash_from_execution_hash(fixed)
2545 // and the remaining fold is identical at every bar in every driving.
2546 // The batch<->stream oracle is therefore the OUTCOME, not this array:
2547 // tests/test_native_margin_fx_roll.cpp section 8 and
2548 // tests/test_native_report_truth.cpp are the kernel witnesses;
2549 // tests/test_streaming.cpp is the source-layer companion, and
2550 // scripts/check_corpus_parity.sh covers the Pine adapter.
2551 // Both directions are pinned in tests/test_native_report_truth.cpp.
2553 guard_native_mutation("set_broker_state_hash_recording");
2555 }
2556
2557 // Toggle volume-weighted per-sub-bar sampling of the intrabar path (see
2558 // magnifier_volume_weighted_). Has no effect unless bar magnifier is
2559 // enabled.
2561 guard_native_mutation("set_magnifier_volume_weighted");
2563 }
2564
2565 // --- Runtime trace API ---
2566 // Default off so existing strategies pay zero cost. The validator
2567 // flips this on per-strategy via ``strategy_set_trace_enabled`` (the
2568 // FFI shim defined in c_abi.cpp) before running a backtest whose
2569 // per-bar values it wants to cross-reference against TradingView.
2570 void set_trace_enabled(bool on) {
2571 guard_native_mutation("set_trace_enabled");
2572 trace_enabled_ = on;
2573 }
2574 bool trace_enabled() const { return trace_enabled_; }
2575
2576 // --- Live-runtime status API (ABI v4) ---
2577 int last_run_status() const { return last_run_status_; }
2578
2579 // Request cooperative abort of the run in progress on this handle (a
2580 // live runtime supersedes an in-flight probe run). Safe to call from any
2581 // thread; consumed by the running loop at its next bar. A request made
2582 // while idle is cleared at the next run() entry and is a no-op.
2583 void request_abort() { abort_requested_.store(true, std::memory_order_relaxed); }
2584
2585 // Push a typed per-bar value into the trace buffer. Cheap when
2586 // disabled — a single bool branch and return. When enabled, name
2587 // interning amortises to a single hash lookup per call after the
2588 // first occurrence; the actual record is a 24-byte POD push_back.
2589 //
2590 // The bool / int overloads internally cast to double so the
2591 // transpiler pragma can emit a single call shape regardless of the
2592 // source variable's Pine type — keeping codegen rewrites trivial.
2593 void trace(const std::string& name, double value);
2594 void trace(const std::string& name, bool value) { trace(name, value ? 1.0 : 0.0); }
2595 void trace(const std::string& name, int value) { trace(name, static_cast<double>(value)); }
2596
2597 // --- Recorded outputs (docs/outputs.md) ---
2598 // Named slots of doubles kept per bar, values kept once per run, and
2599 // events with an optional text, written by a generated module or a C++
2600 // host and read through the strategy_outputs_* C exports. Off until a
2601 // caller enables recording; no matching, fill, margin, sizing, report or
2602 // hash path reads the record, and none of it is hashed. The host states
2603 // the shape once (declare_outputs), the boundary of every run
2604 // (output_run_begin) and the identity of every row (output_bar): a row is
2605 // a bar the host published, keyed by its open time. A violated
2606 // precondition throws std::logic_error coded engine_invariant; a cap the
2607 // run reached (outputs_limit) or a bar recalculated after its events were
2608 // cleared (outputs_rejected) throws std::runtime_error. Either fails the
2609 // run inside a callback with its text and code. See src/engine_report.cpp.
2610public:
2612 uint64_t sequence = 0; // 1-based per run; a cleared one is never issued again
2613 int32_t output_index = 0;
2614 int32_t bar_index = 0; // the row it was recorded on
2615 int64_t bar_open_ms = 0;
2616 int64_t bar_close_ms = 0;
2617 uint32_t ordinal_in_bar = 0; // per output, from 0 within the row
2618 uint32_t phase = 0; // the run's NativeRunPhase when recorded
2619 uint32_t confirmed = 1; // 0 on a calculation of a partially finalized bar
2620 double value = 0.0;
2621 uint64_t message_hash64 = 0; // FNV-1a 64 of the message bytes; 0 without one
2622 bool has_message = false;
2623 std::string message;
2624 };
2625
2626 bool outputs_declared() const { return outputs_declared_; }
2627 // false: refused (nothing declared, or a change while the host is Running);
2628 // last_error() says which unless it holds a failed run's error, and a
2629 // success leaves it as it is. A change of the flag clears the record.
2630 bool set_outputs_enabled(bool on);
2631 // The readers behind the C exports. While recording is off the record is
2632 // empty and the counts are 0.
2633 int output_series_count() const { return outputs_enabled_ ? outputs_slots_ : 0; }
2634 int64_t output_bars_len() const { return static_cast<int64_t>(output_open_ms_.size()); }
2635 bool output_bar_times_copy(int64_t from_bar, int64_t* open_ms, int64_t* close_ms,
2636 int64_t capacity, int64_t* written) const;
2637 bool output_series_copy(int slot, int64_t from_bar, double* out, int64_t capacity,
2638 int64_t* written) const;
2639 int output_events_len() const { return static_cast<int>(output_events_.size()); }
2640 const OutputEvent* output_event_at(int index) const;
2641 // Drops the queued events; rows, ordinals and the sequence stay.
2643 int output_constants_count() const { return outputs_enabled_ ? outputs_constants_ : 0; }
2644 // NaN for na, for a value not written this run, or out of range.
2645 double output_constant_value(int index) const;
2646
2647protected:
2648 // Read by generated code before each recorder call, as trace_enabled_ is.
2649 bool outputs_enabled_ = false;
2650 // Once, before the first run: the slots per row, the outputs an event may
2651 // name and the values kept per run. Repeating it repeats the same counts.
2652 // @host-seam (native_c_api.h, COVERAGE / BASE-CLASS SEAMS)
2653 void declare_outputs(int series_slots, int outputs, int run_constants = 0);
2654 // At every run's beginning (a C++ host: on_native_run_begin): an empty
2655 // record, the sequence from 1, every run value unwritten. Idempotent.
2656 // @host-seam (native_c_api.h, COVERAGE / BASE-CLASS SEAMS)
2658 // Opens the row of the bar being calculated: a new row above the last
2659 // row's open, the last row again (a recalculation) at an equal one; a
2660 // lower open throws. The writers below return at once while recording is
2661 // off.
2662 // @host-seam (native_c_api.h, COVERAGE / BASE-CLASS SEAMS)
2663 void output_bar(int64_t open_ms, int64_t close_ms);
2664 // @host-seam (native_c_api.h, COVERAGE / BASE-CLASS SEAMS)
2665 void output_value(int slot, double value);
2666 // @host-seam (native_c_api.h, COVERAGE / BASE-CLASS SEAMS)
2667 void output_event(int output, double value);
2668 // @host-seam (native_c_api.h, COVERAGE / BASE-CLASS SEAMS)
2669 void output_event(int output, double value, const std::string& message);
2670 // The run's first write of an index stores it; a later one must be equal.
2671 // @host-seam (native_c_api.h, COVERAGE / BASE-CLASS SEAMS)
2672 void output_constant(int index, double value);
2673
2674private:
2675 void clear_output_record();
2676 bool output_host_running() const;
2677 void record_output_event(int output, double value, const std::string* message);
2678 int outputs_slots_ = 0;
2679 int outputs_count_ = 0;
2680 int outputs_constants_ = 0;
2681 bool outputs_declared_ = false;
2682 bool output_run_begun_ = false;
2683 std::vector<double> output_series_; // row-major, rows x slots; NaN unwritten
2684 std::vector<int64_t> output_open_ms_;
2685 std::vector<int64_t> output_close_ms_;
2686 std::vector<OutputEvent> output_events_; // since the run began or the last clear
2687 uint64_t output_sequence_ = 0; // the last sequence issued
2688 uint64_t output_row_seq_base_ = 0; // its value when the last row opened
2689 uint64_t output_cleared_sequence_ = 0; // the highest sequence a clear dropped
2690 std::vector<uint32_t> output_ordinals_; // per output, within the last row
2691 std::vector<double> output_constants_;
2692 std::vector<uint8_t> output_constant_written_;
2693
2694private:
2695 // The run spec's quantity tolerance (NativeRunSpec::quantity_tolerance),
2696 // 0 when the run declares none, which keeps the settlement exact. The FIFO
2697 // close walk and the opening beside a surviving book read it
2698 // (src/engine_execution.cpp); the execution consumer, a friend, projects
2699 // it from the spec at every begin, and the spec digest folds the spec's
2700 // value. Private, so no host writes it (R5 lane K-ULP4).
2701 double native_quantity_tolerance_ = 0.0;
2702
2703 // Physical scratch policy and run ownership, independent of the report
2704 // sink. Neither participates in trading decisions or durable hash leaves;
2705 // the logical tables keep their existing folds. Copies retain only policy.
2706 detail::ScratchTableConfig scratch_table_config_{};
2707 struct ScratchTableRun {
2708 std::shared_ptr<detail::ScratchSlab> slab;
2709 ScratchTableRun() = default;
2710 ScratchTableRun(const ScratchTableRun&) noexcept {}
2711 ScratchTableRun& operator=(const ScratchTableRun& other) noexcept {
2712 if (this != &other) slab.reset();
2713 return *this;
2714 }
2715 ScratchTableRun(ScratchTableRun&&) noexcept = default;
2716 ScratchTableRun& operator=(ScratchTableRun&&) noexcept = default;
2717 };
2718 ScratchTableRun scratch_table_run_;
2719 void prepare_scratch_table_run(const detail::ScratchTableConfig& config);
2720
2721 // Reporting configuration and run-local output ownership. Copies retain
2722 // configuration, never a writer, receipt, or an active-call barrier.
2723 struct ReportSinkState {
2724 bool enabled = false;
2725 bool trade_history_streamable = false;
2726 std::string path, run_id, scratch_dir;
2727 int fd = -1; // borrowed stream; -1 selects the owned path writer
2728 std::uint64_t fd_device = 0;
2729 std::uint64_t fd_inode = 0;
2730 bool fd_identity_valid = false;
2731 std::uint32_t active_calls = 0;
2732 std::uint32_t stream_calls = 0;
2733 std::unique_ptr<report_sink::LineWriter> writer;
2734 // Shared ordinary/selected record-time curve. Selected capture moves
2735 // this owner to its immutable presentation snapshot.
2736 std::unique_ptr<report_sink::ScratchCurveWriter> equity_scratch;
2737 bool equity_tail_stored = false; // scratch includes the held tail
2738 pf_report_sink_receipt_v1 receipt{};
2739 pf_report_sink_summary_v1 summary{}; // report output, shares the sink hash waiver
2740 // Derived report-tail prefix, in max/min/drawdown/runup order. The
2741 // resulting extrema already have their broker-state folds; this
2742 // reconstructible prefix shares report_sink_state_'s output waiver.
2743 double equity_prefix[4]{};
2744 // Report cursor only, covered by report_sink_state_'s existing
2745 // broker-hash waiver. No execution decision reads it; copies reset it.
2746 std::int64_t output_base = 0;
2747 // Selected report validation only, under report_sink_state_'s output
2748 // waiver. Released FINAL points are latched here, never broker inputs.
2749 std::unique_ptr<report::SelectedReportStream> selected_equity_stream;
2750 // Independent full-run reference for selected tail refolds, including
2751 // observations outside [T, E) that the selected scratch must omit.
2752 // Always present so test/module and library NDEBUG settings agree on
2753 // the C++ layout; only Debug updates/reads these report-only values.
2754 double selected_released_extremes[4]{};
2755 ReportSinkState();
2756 ~ReportSinkState();
2757 ReportSinkState(const ReportSinkState& other);
2758 ReportSinkState& operator=(const ReportSinkState& other);
2759 ReportSinkState(ReportSinkState&&) noexcept;
2760 ReportSinkState& operator=(ReportSinkState&&) noexcept;
2761 };
2762 // @broker-state begin
2763 mutable ReportSinkState report_sink_state_;
2764 // @broker-state end
2765 // O1 capture policy for reports built after it is set. Deliberately
2766 // outside the broker-state region: it changes no fill, fold, hash leaf or
2767 // continuation digest (two runs over the same input with different report
2768 // modes must hash identically), and reset_run_state leaves it in place
2769 // like the sink descriptor.
2770 ReportMode report_mode_ = ReportMode::FULL;
2771
2772 class ReportSinkCallScope {
2773 ReportSinkState& state;
2774 bool stream;
2775 public:
2776 explicit ReportSinkCallScope(const BacktestEngine& engine, bool is_stream = false)
2777 : state(engine.report_sink_state_), stream(is_stream) {
2778 ++state.active_calls;
2779 if (stream) ++state.stream_calls;
2780 }
2781 ~ReportSinkCallScope() {
2782 if (stream) --state.stream_calls;
2783 --state.active_calls;
2784 }
2785 ReportSinkCallScope(const ReportSinkCallScope&) = delete;
2786 ReportSinkCallScope& operator=(const ReportSinkCallScope&) = delete;
2787 };
2788 int report_sink_setter_status() const;
2789 void begin_report_sink();
2790 void finish_report_sink(ReportC* out) const;
2791 void stage_report_sink_equity_point(const pf_equity_point_t& point) const;
2792 void stage_report_sink_equity_tail(const ReportC* out) const;
2793 void compute_report_sink_metrics(ReportC* out, std::int64_t equity_count,
2794 CacheCapacity equity_capacity) const;
2795 void compute_report_sink_trade_metrics(ReportC* out,
2796 const TradeC* open_rows, int count) const;
2797 void write_report_sink_trades(const TradeC* open_rows, int count) const;
2798 void fill_report_sink_metrics_section(ReportC* out) const;
2799 template<class Write> void report_sink_record(Write write) const;
2800 bool stream_report_sink_trace(const std::string& name, const TraceEntryC& entry);
2801 void reset_report_sink_outputs();
2802 std::int64_t report_sink_output_row() const;
2803 void write_report_sink_output_row() const;
2804 void write_report_sink_events() const;
2805 void retire_report_sink_output_row();
2806 void write_report_sink_hashes() const;
2807 void retire_report_sink_hashes();
2808 void begin_report_replay();
2809 void finish_report_replay(const ReportC* out) const;
2810 void record_report_replay_close(std::size_t global);
2811 void record_report_replay_point(const pf_equity_point_t& point);
2812 void write_report_replay_equity(const pf_equity_point_t& point) const;
2813 void write_report_replay_trade(std::size_t global, bool terminal) const;
2814 void retire_report_replay_trades(std::size_t global_end);
2815
2816public:
2817 // The C descriptor is copied only after the lifecycle/shape checks. The
2818 // existing idle barrier covers begin callbacks even when the sink is OFF.
2820 if (descriptor && (descriptor->struct_size != sizeof(*descriptor)
2821 || descriptor->version != 1 || descriptor->reserved != 0
2822 || descriptor->backend > static_cast<std::uint32_t>(
2824 || descriptor->V > 27)) return -1;
2825 const int status = report_sink_setter_status();
2826 if (status != 0) return status;
2828 if (descriptor) {
2829 next.backend = static_cast<detail::ScratchTableBackend>(descriptor->backend);
2830 next.manifest = {descriptor->K_q, descriptor->B_q,
2831 descriptor->K_e, descriptor->B_e,
2832 descriptor->K_c, descriptor->B_c,
2833 descriptor->U, descriptor->B_U,
2834 descriptor->V, descriptor->B_V, descriptor->R};
2835 }
2836 scratch_table_config_ = next;
2837 return 0;
2838 }
2840 return scratch_table_config_;
2841 }
2842
2843 // Physical policy lives behind the opaque consumer, so these additive
2844 // methods do not change the engine's layout or virtual interface.
2846 void set_cache_policy(const CacheDefaults& policy);
2849
2850 // The additive C ABI delegates here; receipt queries never bind a consumer.
2856 int set_report_replay_v1(const char* path, const char* run_id);
2857 double raw_net_profit_sum() const noexcept { return net_profit_sum_; }
2858 double report_pointvalue() const noexcept;
2859 // Codegen opts in only for modules that do not read closedtrades.*.
2860 // L6B1 still retains every row; L8c consumes this policy at sealing.
2861 void set_trade_history_streamable(bool streamable);
2862 bool trade_history_streamable() const { return report_sink_state_.trade_history_streamable; }
2863
2864protected:
2865 // A private configuration tag uses the existing pending scalar, never
2866 // object padding or a public recorder mode. The setter admits only the
2867 // three public values; begin strips the tag before activating the mode.
2868 // OFF hooks perform no registry lookup, lock, allocation or replay call.
2869 static constexpr std::uint32_t report_replay_flag = UINT32_C(0x80000000);
2871 return static_cast<ReportMode>(
2872 static_cast<std::uint32_t>(pending_equity_report_mode_) & ~report_replay_flag);
2873 }
2875 return static_cast<ReportMode>(static_cast<std::uint32_t>(mode)
2876 | (static_cast<std::uint32_t>(pending_equity_report_mode_) & report_replay_flag));
2877 }
2878 report::ReportReplay* report_replay_state() const noexcept {
2879 if (!(static_cast<std::uint32_t>(pending_equity_report_mode_) & report_replay_flag))
2880 return nullptr;
2881 return find_report_replay();
2882 }
2883 report::ReportReplay* find_report_replay() const noexcept;
2884 void amend_report_replay_point(std::int64_t time, report::EquityBasis basis,
2885 double term) const;
2886 // Begin-only source binder input. Backends copy the owner, not this
2887 // reference; no byte pointer is exposed to a strategy or a hot callback.
2888 const std::shared_ptr<detail::ScratchSlab>& scratch_tables_for_run() const noexcept {
2889 return scratch_table_run_.slab;
2890 }
2891
2892 // In-place closed-trade text comparisons (see TradeLedger::*_matches): an
2893 // invalid index compares as the empty string, as the owning getters return.
2894 bool closed_trade_entry_id_equals(int idx, std::string_view rhs) const {
2895 return trades_.entry_id_matches(idx, rhs);
2896
2897 }
2898 bool closed_trade_exit_id_equals(int idx, std::string_view rhs) const {
2899 return trades_.exit_id_matches(idx, rhs);
2900
2901 }
2902 bool closed_trade_entry_comment_equals(int idx, std::string_view rhs) const {
2903 return trades_.entry_comment_matches(idx, rhs);
2904
2905 }
2906 bool closed_trade_exit_comment_equals(int idx, std::string_view rhs) const {
2907 return trades_.exit_comment_matches(idx, rhs);
2908
2909 }
2910
2911 // The last point stays editable until its successor is recorded. Sinks
2912 // using the kernel recorder keep only this tail; sink OFF keeps the array.
2914 const pf_equity_point_t* last_equity_point() const noexcept;
2915 // Sink execution prefix, excluding the editable held point. A selected
2916 // prefix starts with its anchor and contains only released window points.
2917 // The returned descriptor is borrowed; observing flushes pending writes.
2918 metrics::ScratchCurveRegion report_sink_equity_scratch_region() const;
2920 bool prepare_equity_point(); // release the prior point; true for the first
2921 void fold_report_equity_extreme(double equity);
2924 // Sealed closed rows only; range-end report rows never enter these sums.
2925 // Each accessor continues its own sum one resident row at a time, so a
2926 // separately accumulated tail cannot change floating-point grouping.
2928 double all_sum = 0.0;
2929 double win_sum = 0.0;
2930 double loss_sum = 0.0;
2931 std::size_t all_count = 0;
2932 std::size_t win_count = 0;
2933 std::size_t loss_count = 0;
2934 };
2935 // @broker-state begin
2937 // @broker-state end
2938
2939 // The sealing caller folds each final row exactly once, in global row
2940 // order, immediately before retiring it from trades_. This operation
2941 // does not change storage, logical indices or the win/loss guards.
2943 // The streaming owner clears this prefix with the run's closed rows.
2945 // A sink hook receives only range-end rows. Re-summing an edited tail
2946 // starts with this closed-row left fold, never a separately summed tail.
2948
2949 // Optional sequence retention. The default is
2950 // the historical owning-array path, byte-identical to before. SummaryOnly
2951 // keeps one fixed ordered witness per sequence instead, so trial memory
2952 // does not grow with the bar, trace, row or event population. A report
2953 // sink streams the complete sequence externally in either mode.
2954public:
2957
2958private:
2959 void reset_optional_sequence_witness();
2960 void fold_pending_hashes_into_witness() const;
2961 void fold_current_output_row_witness() const;
2962 void finalize_summary_output_row();
2963 static void fold_hash_leaf(report_sequence::SequenceWitness& witness,
2964 std::uint64_t hash);
2965 static void fold_trace_leaf(report_sequence::SequenceWitness& witness,
2966 std::int64_t timestamp, std::int32_t bar_index,
2967 const std::string& name, double value);
2968 static void fold_row_leaf(report_sequence::SequenceWitness& witness,
2969 std::int64_t row, std::int64_t open_ms,
2970 std::int64_t close_ms, const double* values,
2971 std::int64_t count);
2972 static void fold_event_leaf(report_sequence::SequenceWitness& witness,
2973 const OutputEvent& event);
2974 report_sequence::ReportMode optional_report_mode_ =
2976 // The sink writers and fill_report are const; the witness is the only
2977 // state they add, and no reader observes it as part of the run's report.
2978 mutable report_sequence::OptionalSequenceSummary sequence_witness_;
2979 mutable std::size_t sequence_hash_folded_ = 0;
2980 mutable std::uint64_t sequence_hash_folded_leaf_ = 0;
2981 mutable bool sequence_row_folded_ = false;
2982 report_sequence::SequenceWitness output_row_witness_base_;
2983 report_sequence::SequenceWitness output_event_row_witness_;
2984
2985public:
2986 // Configure this thread's next hosts BEFORE their constructors declare
2987 // outputs. nullptr means unlimited; a negative value is a checked refusal.
2988 static int set_output_series_limit_checked(const std::int64_t* maximum,
2989 char* error, std::size_t capacity) noexcept;
2990 // 1: a declaration exceeded its operator limit since the last successful
2991 // setter; write that maximum. 0: none; -1: null destination. No allocation.
2992 static int output_series_limit_failure(std::int64_t* maximum) noexcept;
2993
2994private:
2995 static std::optional<std::int64_t> default_output_series_limit() noexcept;
2996 // Recorder admission only, outside execution state and its hashes, like
2997 // the output shape and recording switch. Existing hosts keep their setting.
2998 std::optional<std::int64_t> max_output_series_ = default_output_series_limit();
2999};
3000
3001inline constexpr std::size_t kReportSinkHeldEquityPoints = 1;
3002
3003} // inline namespace engine_script_run_v19
3004} // namespace pineforge
3005
3006// Optional metrics C entry points, implemented in engine_report.cpp and
3007// declared here independently of pineforge.h's static-runtime inventory.
3008// Native modules advertise their certified execution adapter separately via
3009// strategy_metrics_only_capability_v1 (native_module.hpp).
3010// mode: 0=OFF, 1=ON (default);
3011// metrics_needed is the OR of kMetricsNeed*V1. The getter is an additive
3012// diagnostic transport; it does not extend pf_report_t.
3014 void* strategy, std::uint32_t streaming_sharpe,
3015 std::uint64_t metrics_needed) noexcept;
3017 const void* strategy, pineforge::metrics::MetricsExtentReceiptV1* out) noexcept;
MetricsStreamingAttemptV1 & operator=(const MetricsStreamingAttemptV1 &)=delete
double get_syminfo_metadata(const std::string &key) const
Definition engine.hpp:691
double allocated_entry_commission(const PyramidEntry &pe, double units) const
Definition engine.hpp:1197
static int output_series_limit_failure(std::int64_t *maximum) noexcept
metrics::MetricsStreamingStatusV1 metrics_extent_receipt_v1(metrics::MetricsExtentReceiptV1 *) const
void append_quoted_lot(PyramidEntry lot, double total_qty, double average_price)
int report_sink_diagnostics_v1(pf_report_sink_diagnostics_v1 *out) const noexcept
virtual void prepare_script_run(const Bar *, int, bool)
Definition engine.hpp:1614
std::unordered_map< std::string, std::string > inputs_
Definition engine.hpp:671
broker::PositionCloseObligation position_close_obligation_
Definition engine.hpp:733
void open_quoted_position(PositionSide requested, PyramidEntry lot)
double open_profit_at(double current_price, double fx) const
Definition engine.hpp:1324
const StreamOrderAction & stream_order_action_at(int i) const
Definition engine.hpp:2137
int set_optional_report_mode(report_sequence::ReportMode mode)
execution::Status validate_native_settlement_book(double &held) const
void set_chart_timezone(const std::string &tz)
Definition engine.hpp:2319
void record_equity_point(int64_t script_bar_ts)
Definition engine.hpp:1683
void feed_security_eval_state(SecurityEvalState &state, const Bar &input_bar)
virtual int observe_pending_effective_levels(int index, double *stop, double *limit, double *trail_activation) const
std::string open_trade_entry_comment(int idx) const
double closed_trade_max_drawdown_percent(int idx) const
Definition engine.hpp:1810
execution::Status prepare_native_settlement_commit(const NativeSettlementStage &stage, const execution::Fill &fill, const execution::PhysicalExecutionContext &context, NativeSettlementRows &rows) const
execution::Status preview_native_settlement_commit(const execution::ReverseTo &reversal, const execution::Fill &fill, const execution::PhysicalExecutionContext &context, execution::AccountEffectProjection &account, std::vector< double > &row_pnl) const
execution::SettlementInspection inspect_native_settlement_stage(const NativeSettlementStage &stage, const execution::Fill &fill) const
execution::SettlementInspection inspect_native_settlement_scoped(const execution::Action &action, const execution::Fill &fill, execution::CloseScope scope) const
execution::Result settle_with_context_scoped(const execution::Action &action, const execution::Fill &fill, const execution::LifecycleEffects &lifecycle, const execution::PhysicalExecutionContext &context, execution::CloseScope scope)
execution::Status preview_native_settlement_commit(const execution::Action &action, const execution::Fill &fill, const execution::PhysicalExecutionContext &context, execution::CloseScope scope, const execution::SelectedOpeningSet *selected, execution::AccountEffectProjection &account, std::vector< double > &row_pnl) const
execution::Status preflight_native_settlement_effects(const NativeSettlementStage &stage, const execution::LifecycleEffects &lifecycle, const NativeSettlementRows &rows)
bool output_bar_times_copy(int64_t from_bar, int64_t *open_ms, int64_t *close_ms, int64_t capacity, int64_t *written) const
static int set_output_series_limit_checked(const std::int64_t *maximum, char *error, std::size_t capacity) noexcept
int report_equity_month_key(std::int64_t time_ms) const
Definition engine.hpp:1670
double get_input_double(const std::string &key, double default_val) const
bool closed_trade_exit_comment_equals(int idx, std::string_view rhs) const
Definition engine.hpp:2906
void register_security_eval(int sec_id, const std::string &requested_tf, const std::string &input_tf)
pf_equity_point_t * last_equity_point() noexcept
void set_syminfo_type(const std::string &t)
Definition engine.hpp:2348
std::vector< PyramidEntry > pyramid_entries_
Definition engine.hpp:607
void snapshot_entry_commission(PyramidEntry &pe) const
Definition engine.hpp:1203
execution::Result settle_source_staged_execution(NativeSettlementStage &stage, const execution::Fill &fill, const execution::LifecycleEffects &lifecycle, const execution::PhysicalExecutionContext &context)
bool stream_advance_time(int64_t timestamp_ms)
execution::SettlementInspection inspect_native_settlement(const execution::Action &action, const execution::Fill &fill) const
double open_entry_commission(const PyramidEntry &pe) const
Definition engine.hpp:1191
execution::Result commit_prepared_native_settlement_stage(NativeSettlementStage &stage, const execution::Fill &fill, const execution::LifecycleEffects &lifecycle, const execution::PhysicalExecutionContext &context, NativeSettlementRows &rows)
static constexpr std::uint32_t report_replay_flag
Definition engine.hpp:2869
virtual bool set_recorded_series(const std::string &key, const int64_t *chart_open_ms, const double *values, int n)
void prepare_chart_day_partition(const Bar *input_bars, int n_input)
int fill_report_envelope_v1(ReportEnvelopeV1 *out) const
void finish_native_settlement_stage(NativeSettlementStage &stage, const execution::Fill &fill) const
execution::SettlementInspection inspect_native_settlement_selected(const execution::Action &action, const execution::Fill &fill, const execution::SelectedOpeningSet &selection) const
void dispatch_security_eval(SecurityEvalState &state, const Bar &bar, bool publish, int64_t bar_index)
std::string get_input_string(const std::string &key, const std::string &default_val) const
virtual void source_stream_entry_comment(const PyramidEntry &, std::string &) const
int set_report_sink_fd_v1(const pf_report_sink_fd_v1 *sink)
std::string closed_trade_direction(int idx) const
Definition engine.hpp:1817
execution::Result settle_native_execution_selected_at(const execution::Action &action, const execution::Fill &fill, const execution::PhysicalExecutionContext &context, const execution::SelectedOpeningSet &selection)
double account_currency_fx_at(int64_t timestamp_ms) const
const OutputEvent * output_event_at(int index) const
Trade build_close_trade_with_costs(const PyramidEntry &pe, double close_qty, double fill_price, bool was_long, double entry_commission, double exit_commission, const execution::PhysicalExecutionContext &context) const
execution::Result settle_execution_selected_with_lifecycle(const execution::Action &action, const execution::Fill &fill, const execution::LifecycleEffects &lifecycle, const execution::SelectedOpeningSet &selection)
int get_input_int(const std::string &key, int default_val) const
bool stream_end(bool finalize_partial_input_bar=false)
static int classify_report_close_cause(const Trade &row)
void set_input(const std::string &key, const std::string &value)
Definition engine.hpp:2279
bool stream_push_ticks(const TradeTick *ticks, int n)
std::size_t closed_trade_count() const noexcept
Definition engine.hpp:2179
void fill_security_diag_section(ReportC *out) const
virtual void present_report(ReportC *) const
Definition engine.hpp:2006
uint64_t closed_trade_entry_incarnation(int idx) const
Definition engine.hpp:1769
int set_report_replay_v1(const char *path, const char *run_id)
bool closed_trade_entry_id_equals(int idx, std::string_view rhs) const
Definition engine.hpp:2894
std::optional< metrics::EquityRatioResult > summary_equity_ratios_
Definition engine.hpp:791
void prepare_native_security_feeds(const Bar *input_bars, int n_input)
virtual std::uint64_t broker_state_hash_projection() const
void amend_report_replay_point(std::int64_t time, report::EquityBasis basis, double term) const
virtual void evaluate_security(int sec_id, const Bar &bar, bool is_complete)
Definition engine.hpp:1617
void trace(const std::string &name, double value)
bool set_native_security_feed(const std::string &timeframe, const Bar *bars, int n)
const std::string & report_scratch_directory() const noexcept
Definition engine.hpp:1987
execution::SettlementInspection inspect_native_settlement_selected_at(const execution::Action &action, const execution::Fill &fill, const execution::SelectedOpeningSet &selection, double fx) const
metrics::MetricsStreamingStatusV1 admit_metrics_plain_v1(const MetricsPlainInputsV1 &, const metrics::MetricsExtentPlanV1 &, metrics::MetricsExtentReceiptV1 *)
int64_t get_input_int64(const std::string &key, int64_t default_val) const
bool stream_begin(const Bar *warmup_bars, int n_warmup, const std::string &input_tf, const std::string &script_tf="")
void output_event(int output, double value, const std::string &message)
void stage_native_settlement(NativeSettlementStage &stage, const execution::ReverseTo &reversal, const execution::Fill &fill, const execution::LifecycleEffects *lifecycle, double fx) const
std::vector< SecurityEvalState > security_eval_states_
Definition engine.hpp:1486
void declare_outputs(int series_slots, int outputs, int run_constants=0)
void build_native_settlement_close_rows(const NativeSettlementStage &stage, const execution::Fill &fill, const execution::PhysicalExecutionContext &context, NativeSettlementRows &rows) const
execution::AccountEffectProjection project_native_settlement_v1(const execution::Action &action, const execution::Fill &fill) const
std::vector< StreamOrderAction > stream_order_actions_
Definition engine.hpp:1454
void set_cache_policy(const CacheDefaults &policy)
void output_event(int output, double value)
bool closed_trade_exit_id_equals(int idx, std::string_view rhs) const
Definition engine.hpp:2898
virtual int observe_probe_fill_qty(int index, double fill_price, double *qty, int *close_only, int *partition) const
metrics::MetricsStreamingStatusV1 prepare_metrics_streaming_v1(const metrics::MetricsStreamingConfigV1 &, const metrics::MetricsExtentPlanV1 &)
double level_on_price_grid(double level) const
Definition engine.hpp:1137
void observe_metrics_curve_v1(const Source &curve, std::int64_t count, pf_equity_stats_t *equity=nullptr) const
Definition engine.hpp:844
execution::AccountEffectProjection project_with_membership(const execution::Action &action, const execution::Fill &fill, execution::CloseScope book_or_opening, const execution::SelectedOpeningSet *selected, double fx) const
const std::string & chart_timezone() const
Definition engine.hpp:2323
virtual void source_aux_security_input_view(const Bar *&bars, int &n) const
virtual int observe_pending_level_resolved(int index) const
bool set_account_currency_fx_series(const int64_t *timestamps_ms, const double *rates, int n)
bool previous_closed_trade(std::size_t &cursor, Trade &value) const
Definition engine.hpp:2183
void trace(const std::string &name, bool value)
Definition engine.hpp:2594
execution::Result settle_reversal_with_lifecycle_v1(const execution::ReverseTo &reversal, const execution::Fill &fill, const execution::LifecycleEffects &lifecycle)
void set_syminfo_session(const std::string &s)
Definition engine.hpp:2338
const IExecutionConsumer & execution_consumer() const
virtual bool set_symbol_feed_borrowed(const std::string &, const std::string &, const Bar *, const int64_t *, int)
Definition engine.hpp:2499
int set_scratch_tables_v1(const pf_scratch_tables_v1 *descriptor)
Definition engine.hpp:2819
std::unordered_map< std::string, double > syminfo_metadata_
Definition engine.hpp:678
report_sequence::OptionalSequenceSummary optional_sequence_summary() const
virtual void set_syminfo_metadata(const std::string &key, double value)
execution::Result settle_native_reversal_at_v1(const execution::ReverseTo &reversal, const execution::Fill &fill, const execution::PhysicalExecutionContext &context)
void run(const Bar *input_bars, int n_input, const std::string &input_tf, const std::string &script_tf, bool bar_magnifier=false, int magnifier_samples=4, MagnifierDistribution magnifier_dist=MagnifierDistribution::ENDPOINTS)
void stream_observe_entry(const PyramidEntry &pe)
bool get_input_bool(const std::string &key, bool default_val) const
void validate_close_trade_counters(const Trade *rows, size_t count) const
int report_sink_receipt_v1(pf_report_sink_receipt_v1 *out) const noexcept
bool session_template_knows_early_close() const
Whether TradingView's session template for this symbol carries the exchange's early closes and holida...
execution::Status allocate_native_settlement_closes(NativeSettlementStage &stage, const execution::CloseScope &book_or_opening, double &remaining) const
virtual bool set_aux_security_feed(const Bar *bars, int n, const std::string &input_tf)
execution::SettlementInspection inspect_native_reversal_at(const execution::ReverseTo &reversal, const execution::Fill &fill, double fx) const
report::ReportReplay * find_report_replay() const noexcept
virtual bool report_presenter_supports_non_full() const noexcept
Definition engine.hpp:2013
double open_profit(double current_price) const
Definition engine.hpp:1319
execution::SettlementInspection inspect_with_membership(const execution::Action &action, const execution::Fill &fill, execution::CloseScope book_or_opening, const execution::SelectedOpeningSet *selected, double fx) const
execution::Result settle_native_execution_scoped_at(const execution::Action &action, const execution::Fill &fill, const execution::PhysicalExecutionContext &context, execution::CloseScope scope)
std::vector< NativeSecurityFeed > native_security_feeds_
Definition engine.hpp:1502
int set_cache_policy_v1(const pf_cache_policy_v1 *descriptor)
ReportMode pending_equity_report_mode() const noexcept
Definition engine.hpp:2870
void stream_refresh_action_metadata(size_t first_action, size_t first_trade)
virtual bool set_realtime_tail(bool on, int horizon_bars)
execution::AccountEffectProjection project_native_settlement_selected_v1(const execution::Action &action, const execution::Fill &fill, const execution::SelectedOpeningSet &selection) const
void store_equity_point(const pf_equity_point_t &point)
execution::Result settle_with_membership(const execution::Action &action, const execution::Fill &fill, const execution::LifecycleEffects &lifecycle, const execution::PhysicalExecutionContext &context, execution::CloseScope book_or_opening, const execution::SelectedOpeningSet *selected)
const CacheDefaults & cache_policy_config() const
void fold_trade_percent_prefix(const Trade &trade)
int report_sink_summary_v1(pf_report_sink_summary_v1 *out) const noexcept
std::string closed_trade_entry_comment(int idx) const
Definition engine.hpp:1753
ReportMode with_report_replay_flag(ReportMode mode) const noexcept
Definition engine.hpp:2874
const detail::ScratchTableConfig & scratch_table_config() const noexcept
Definition engine.hpp:2839
bool install_equity_ratio_result(const metrics::EquityRatioResult &result)
Definition engine.hpp:878
execution::AccountEffectProjection project_native_settlement_at(const execution::Action &action, const execution::Fill &fill, double fx) const
int32_t intern_trace_name(const std::string &name)
report::ReportReplay * report_replay_state() const noexcept
Definition engine.hpp:2878
virtual bool set_symbol_feed_column(const std::string &key, const std::string &timeframe, const std::string &name, const double *values, int n)
void finish_metrics_streaming_v1(metrics::MetricsRatioCurveV1, pf_equity_stats_t *=nullptr) const
std::string open_trade_direction(int idx) const
Definition engine.hpp:1821
virtual void hash_host_extension(BrokerStateHashSink &) const
virtual int observe_pending_copy_v1(int index, pf_pending_order_v1_t *out) const
double round_to_mintick(double price) const
Definition engine.hpp:1047
const Trade & materialize_report_trade(int i) const
Definition engine.hpp:2204
int report_cache_stats(std::uint64_t *resident_bytes, std::uint64_t *allocation_bypass_count) const noexcept
std::uint64_t broker_state_hash_from_execution_hash(std::uint64_t) const
bool output_series_copy(int slot, int64_t from_bar, double *out, int64_t capacity, int64_t *written) const
std::vector< double > quote_execution_commissions(const std::vector< double > &closed_units, double opening_units, const execution::Fill &fill, double fx) const
int set_report_sink_v1(const pf_report_sink_v1 *sink)
bool closed_trade_entry_comment_equals(int idx, std::string_view rhs) const
Definition engine.hpp:2902
metrics::EquityMetricsFold summary_equity_fold_
Definition engine.hpp:787
virtual void hash_source_extension(BrokerStateHashSink &) const
std::string closed_trade_entry_id(int idx) const
Definition engine.hpp:1761
double marked_equity_at(double price, double fx) const
bool set_syminfo_string(const std::string &key, const std::string &value)
Definition engine.hpp:2369
void set_syminfo_timezone(const std::string &tz)
Definition engine.hpp:2334
void trace(const std::string &name, int value)
Definition engine.hpp:2595
bool stream_push_tick(const TradeTick &tick)
const std::shared_ptr< detail::ScratchSlab > & scratch_tables_for_run() const noexcept
Definition engine.hpp:2888
const metrics::EquityFoldSnapshot * equity_summary_snapshot() const
Definition engine.hpp:874
void begin_metrics_streaming_v1(bool admitted_shape)
void run(const Bar *input_bars, int n_input, const std::string &input_tf, const std::string &script_tf, const std::unordered_map< std::string, std::string > &inputs, const SymInfo &syminfo, const void *overrides=nullptr, bool bar_magnifier=false, int magnifier_samples=4, MagnifierDistribution magnifier_dist=MagnifierDistribution::ENDPOINTS)
execution::SettlementInspection inspect_native_settlement_scoped_at(const execution::Action &action, const execution::Fill &fill, execution::CloseScope scope, double fx) const
metrics::ScratchCurveRegion report_sink_equity_scratch_region() const
execution::Result settle_native_execution_at(const execution::Action &action, const execution::Fill &fill, const execution::PhysicalExecutionContext &context)
std::string open_trade_entry_id(int idx) const
std::string closed_trade_exit_id(int idx) const
Definition engine.hpp:1765
execution::Result settle_with_context_selected(const execution::Action &action, const execution::Fill &fill, const execution::LifecycleEffects &lifecycle, const execution::PhysicalExecutionContext &context, const execution::SelectedOpeningSet &selection)
double calc_commission(double fill_price, double qty) const
Definition engine.hpp:1165
const CacheDefaults & cache_policy_for_run() const
bool substitute_native_security_bar(SecurityEvalState &state, Bar &bar, bool count_miss=true)
broker::OpeningObligations opening_obligations_
Definition engine.hpp:592
std::vector< pf_equity_point_t > equity_curve_
Definition engine.hpp:760
execution::AccountEffectProjection project_native_settlement_scoped_v1(const execution::Action &action, const execution::Fill &fill, execution::CloseScope scope) const
virtual bool set_symbol_facts(const std::string &key, const std::string &field, const std::string &value)
execution::Result commit_native_settlement_stage(NativeSettlementStage &stage, const execution::Fill &fill, const execution::LifecycleEffects &lifecycle, const execution::PhysicalExecutionContext &context)
double bar_fill_price(double raw_bar_price) const
Definition engine.hpp:1065
std::unordered_map< std::string, int32_t > trace_name_index_
Definition engine.hpp:1567
std::vector< int64_t > account_currency_fx_timestamps_
Definition engine.hpp:636
virtual bool set_symbol_feed(const std::string &key, const std::string &timeframe, const Bar *bars, const int64_t *close_ms, int n)
void present_report_equity_tail(ReportC *out) const
Trade build_close_trade_with_costs(const PyramidEntry &pe, double close_qty, double fill_price, bool was_long, double entry_commission, double exit_commission, const execution::PhysicalExecutionContext &context, const ClosedLotExcursion *retained_excursion, bool consult_excursion_hook) const
void output_constant(int index, double value)
std::string closed_trade_exit_comment(int idx) const
Definition engine.hpp:1757
metrics::EquityFoldSnapshot summary_equity_snapshot_
Definition engine.hpp:788
execution::AccountEffectProjection project_native_reversal_v1(const execution::ReverseTo &reversal, const execution::Fill &fill) const
double calc_commission_at(double fill_price, double qty, double fx) const
Definition engine.hpp:1173
void output_bar(int64_t open_ms, int64_t close_ms)
execution::SettlementInspection inspect_native_reversal_v1(const execution::ReverseTo &reversal, const execution::Fill &fill) const
void set_trade_start_time(int64_t timestamp_ms)
Definition engine.hpp:2287
execution::Result settle_with_context(const execution::Action &action, const execution::Fill &fill, const execution::LifecycleEffects &lifecycle, const execution::PhysicalExecutionContext &context)
execution::AccountEffectProjection project_native_settlement_stage(const NativeSettlementStage &stage, const execution::Fill &fill) const
void guard_native_mutation(const char *operation)
metrics::MetricsStreamingStatusV1 set_metrics_streaming_v1(metrics::StreamingSharpeV1, metrics::MetricsNeedMaskV1)
void declare_opened_lot_entry_bar_mask(uint64_t entry_incarnation, const Bar &entry_bar, OpenedLotFillPoint fill_point)
void stage_native_settlement(NativeSettlementStage &stage, const execution::Action &action, const execution::Fill &fill, execution::CloseScope book_or_opening, const execution::SelectedOpeningSet *selected, const execution::LifecycleEffects *lifecycle, double fx) const
virtual std::uint64_t broker_state_hash_projection() const =0
OpenReportRowSource & operator=(const OpenReportRowSource &)=default
OpenReportRowSource(const OpenReportRowSource &)=default
void begin(double mark_price, std::int64_t mark_time_ms, int interval_index, double account_fx, bool was_long, std::size_t count)
OpenReportRowSource(const BacktestEngine *owner)
Definition engine.hpp:395
const Trade & materialize(std::size_t index) const
const Trade & operator[](std::size_t index) const
Definition engine.hpp:413
std::int32_t strategy_set_metrics_streaming_v1(void *strategy, std::uint32_t streaming_sharpe, std::uint64_t metrics_needed) noexcept
std::int32_t strategy_metrics_extent_receipt_v1(const void *strategy, pineforge::metrics::MetricsExtentReceiptV1 *out) noexcept
constexpr std::size_t kReportSinkHeldEquityPoints
Definition engine.hpp:3001
std::variant< Book, OpeningExposure > CloseScope
std::variant< Flatten, order_action::Reduce, order_action::Transact > Action
Definition execution.hpp:17
std::int64_t equity_month_key(std::int64_t time_ms, const std::string &chart_timezone)
std::uint64_t MetricsNeedMaskV1
bool monthly_replay_time_supported_v1(std::int64_t time_ms)
exit_legs::Lifecycle ExitLegLifecycle
Definition engine.hpp:468
MagnifierDistribution
Definition magnifier.hpp:7
double na< double >()
Definition na.hpp:64
OpenedLotFillPoint
Definition engine.hpp:146
std::function< ClosedLotExcursion(const ClosedLotExcursionFacts &)> LotExcursionHook
Definition engine.hpp:135
std::unordered_map< std::string, std::string > InputsMap
Definition engine.hpp:502
int na< int >()
Definition na.hpp:65
#define PF_API
Definition pineforge.h:83
@ PF_REPORT_MODE_SUMMARY_ONLY
FULL metric bits without presentation overrides; no captured sequences; refused with an observer,...
Definition report_mode.h:28
@ PF_REPORT_MODE_FULL
Legacy owning report sequences.
Definition report_mode.h:27
@ PF_REPORT_MODE_STREAMED
Complete sequences in an external sink.
Definition report_mode.h:29
Versioned resident cache policy.
Definition pineforge.h:976
Single per-script-bar equity point.
Definition pineforge.h:326
double open_profit
Mark-to-market open P&L at bar close.
Definition pineforge.h:329
double equity
initial_capital + net_profit + open_profit.
Definition pineforge.h:328
int64_t time_ms
Script-bar OPEN timestamp (Unix ms).
Definition pineforge.h:327
Equity-curve-derived statistics (all-trades only, like TV).
Definition pineforge.h:272
Composite metrics container: trade stats (all / long / short) + equity-curve stats.
Definition pineforge.h:317
Opt-in envelope, separate from the report layout introduced in ABI 4.
Definition report_mode.h:80
Fixed-size report metadata.
Definition report_mode.h:61
Fixed-size annualization provenance from the emitted v1 records.
Borrowed sequential stream sink descriptor.
Definition report_sink.h:61
Report-only T-row summary, independent of the 144-byte trailer receipt above.
Path sink descriptor.
Definition report_sink.h:26
Backtest report filled by run_backtest / run_backtest_full.
Definition pineforge.h:366
Version 1 scratch-table admission maxima, in keys/rows and key bytes.
Definition pineforge.h:1023
std::uint32_t module_contract_bound
Definition engine.hpp:349
std::uint32_t begin_bar_magnifier
Definition engine.hpp:353
std::uint32_t equal_resolved_timeframes
Definition engine.hpp:351
std::uint32_t pending_selected_window
Definition engine.hpp:355
std::uint32_t probe_suppress_tail_logic
Definition engine.hpp:358
std::uint32_t feed_tolerant_passthrough
Definition engine.hpp:360
std::uint32_t final_admitted_view_bound
Definition engine.hpp:362
std::uint32_t calculation_trigger
Definition engine.hpp:361
std::uint32_t admitted_selected_window
Definition engine.hpp:356
std::uint32_t timeframe_undetected
Definition engine.hpp:352
const MetricsPlainDeclarationV1 * declaration
Definition engine.hpp:348
std::string entry_comment
Definition engine.hpp:157
double entry_commission_account
Definition engine.hpp:176
int64_t magnifier_sample_ticks_total
Definition engine.hpp:288
pf_metrics_t metrics
Definition engine.hpp:307
SecurityDiagC * security_diag
Definition engine.hpp:294
int64_t equity_curve_len
Definition engine.hpp:309
int64_t security_feeds_total
Definition engine.hpp:284
int64_t magnifier_sub_bars_total
Definition engine.hpp:287
pf_equity_point_t * equity_curve
Definition engine.hpp:308
int64_t broker_state_hash_len
Definition engine.hpp:312
TraceEntryC * trace
Definition engine.hpp:303
int64_t input_bars_processed
Definition engine.hpp:282
int64_t security_eval_complete_total
Definition engine.hpp:285
uint64_t * broker_state_hash
Definition engine.hpp:311
const char ** trace_names
Definition engine.hpp:305
int64_t script_bars_processed
Definition engine.hpp:283
int64_t security_eval_partial_total
Definition engine.hpp:286
pf_report_metadata_v1 metadata
Definition engine.hpp:326
std::string timezone
Definition engine.hpp:484
std::string tickerid
Definition engine.hpp:480
std::string ticker
Definition engine.hpp:479
std::string basecurrency
Definition engine.hpp:482
std::string volumetype
Definition engine.hpp:486
std::string type
Definition engine.hpp:483
std::string description
Definition engine.hpp:487
std::string currency
Definition engine.hpp:481
std::string session
Definition engine.hpp:485
int32_t open_at_end
Definition engine.hpp:252
int32_t exit_bar_index
Definition engine.hpp:251
double max_drawdown
Definition engine.hpp:247
int64_t entry_time
Definition engine.hpp:236
int64_t exit_time
Definition engine.hpp:237
int32_t entry_bar_index
Definition engine.hpp:250
bool exit_from_bracket
Definition engine.hpp:213
double commission
Definition engine.hpp:216
std::string exit_comment
Definition engine.hpp:200
execution::CloseCause close_cause
Definition engine.hpp:232
uint64_t entry_incarnation
Definition engine.hpp:219
int64_t exit_time
Definition engine.hpp:189
std::string entry_id
Definition engine.hpp:198
double exit_price
Definition engine.hpp:191
std::string exit_id
Definition engine.hpp:201
std::string entry_comment
Definition engine.hpp:199
int64_t entry_time
Definition engine.hpp:188
double entry_price
Definition engine.hpp:190
double max_drawdown
Definition engine.hpp:215
void store(bool v, std::memory_order order)
Definition engine.hpp:1557
AbortFlag & operator=(const AbortFlag &) noexcept
Definition engine.hpp:1548
ExecutionConsumerSlot & operator=(ExecutionConsumerSlot &&other) noexcept
Definition engine.hpp:2036
ExecutionConsumerSlot(const ExecutionConsumerSlot &other) noexcept
Definition engine.hpp:2024
ExecutionConsumerSlot & operator=(const ExecutionConsumerSlot &other) noexcept
Definition engine.hpp:2026
MetricsStreamingStateV1(MetricsStreamingStateV1 &&) noexcept=default
MetricsStreamingStateV1 & operator=(const MetricsStreamingStateV1 &other)
Definition engine.hpp:816
std::unique_ptr< metrics::StreamingEquityRatiosV1 > fold
Definition engine.hpp:807
A region of the engine-private scratch file (3.8:902-909).