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