PineForge v0.13.1-379-g9b50973
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 <vector>
4#include <string>
5#include <cstddef>
6#include <cstdint>
7#include <cmath>
8#include <ctime>
9#include <limits>
10#include <memory>
11#include <set>
12#include <algorithm>
13#include <unordered_map>
14#include <unordered_set>
15#include <stdexcept>
16#include <optional>
17#include <functional>
19#include "na.hpp"
20#include "bar.hpp"
21#include "broker_events.hpp"
22#include "quantity_intent.hpp"
23#include "execution.hpp"
30#include "leg_activation.hpp"
32#include "order_birth.hpp"
33#include "series.hpp"
34#include "timeframe.hpp"
35#include "magnifier.hpp"
36#include "session_time.hpp"
37// Suppress per-strategy function declarations (strategy_create, run_backtest,
38// etc.) whose pf_*_t parameter types conflict with the internal C++ types
39// used in codegen-emitted extern "C" blocks that include this header.
40// NOTE: this macro leaks into every TU that includes engine.hpp; include
41// pineforge.h FIRST in any TU that needs the per-strategy declarations
42// (see src/c_abi.cpp).
43#define PINEFORGE_NO_STRATEGY_DECLS
44// Angle-bracket form is the installed public path (deliberate).
45#include <pineforge/pineforge.h>
46
47// Generated modules using the full script lifecycle reset must be rebuilt
48// against a runtime providing this hook. This is an internal C++ capability;
49// it does not change any public C POD or exported C function signature.
50#define PINEFORGE_HAS_SCRIPT_RUN_PREPARE_V1 1
51#define PINEFORGE_HAS_NATIVE_LIVE_V1 1
52namespace pineforge {
53
54enum class PositionSide { FLAT, LONG, SHORT };
55
56// One physical emulator lot entry/exit, in actual execution order. A reversal
57// may produce several exits followed by an entry. No range-end report rows.
59 uint64_t sequence = 0;
60 int64_t timestamp_ms = 0;
61 int32_t bar_index = 0;
62 bool is_entry = false;
63 bool is_long = false;
64 double quantity = 0.0;
65 double price = 0.0;
66 std::string order_id;
67 std::string comment;
68 uint64_t entry_incarnation = 0;
69 size_t closed_trade_index = static_cast<size_t>(-1); // engine-private provenance
70};
71
72// Forward declaration of an internal enum used by some BacktestEngine
73// method signatures. The full definition lives in src/engine_internal.hpp
74// (private to libruntime); only the underlying-type pin is needed here.
75namespace internal {
76enum class DualEntryStopPathWinner : int;
77}
78
79// ────────────────────────────────────────────────────────────────────
80// Host-owned per-lot excursion accounting (RULING A48, one generic
81// capability). A host that declares ownership of a lot's favorable/adverse
82// excursion supplies the closed-lot result from its own sampler; the kernel
83// then neither samples excursion at matched trigger prices nor folds bar-path
84// extremes into the closing row. Everything below is source-blind: the facts
85// are the lot's own booking coordinates plus the carried extremes, and the
86// result is the two price-difference x quantity magnitudes the row reports.
87// ────────────────────────────────────────────────────────────────────
89 uint64_t entry_incarnation = 0;
90 int64_t entry_time_ms = 0;
91 double entry_price = 0.0;
92 double lot_qty = 0.0;
93 double closed_qty = 0.0;
94 double fill_price = 0.0;
95 double carried_favorable = 0.0;
96 double carried_adverse = 0.0;
97 bool is_long = true;
100 // The lot's own entry-bar excursion masks (PyramidEntry): the booking
101 // facts of a priced intrabar fill, carried across the boundary so the
102 // owner never has to be reachable from the closing row.
105};
106
108 double favorable = 0.0;
109 double adverse = 0.0;
110};
111
113
115 double price;
116 int64_t time;
117 double qty;
118 std::string entry_id;
120 std::string entry_comment;
121 double max_runup = 0.0;
122 double max_drawdown = 0.0;
123 // Intrabar-fill excursion masks: when a priced (stop/limit) entry fills
124 // mid-bar, the portion of the bar's range traversed BEFORE the fill is
125 // not part of the trade's excursion (TV convention). On the assumed
126 // OHLC path (bar_path_uses_high_first), an extreme that occurs before
127 // the fill position is excluded from update_per_trade_extremes sampling
128 // for the fill bar only. Both default false (market/open fills sample
129 // the full bar).
131 bool skip_entry_bar_low = false;
132 // Entry-leg commission in account currency at this slice's actual fill
133 // boundary. Percentage commission depends on quote->account FX, so an
134 // effective-time provider must not retroactively reprice this already-paid
135 // fee when a later rate becomes active. Partial realization allocates this
136 // paid cost proportionally and leaves only the unconsumed cost on the lot,
137 // independently of the current fee schedule. NaN is reserved for legacy/
138 // synthetic injection; every production entry path captures a real quote.
140 std::numeric_limits<double>::quiet_NaN();
141 // Monotonic per-run identity of the request record object whose broker fill
142 // created this physical lot. Unlike Pine's user-visible entry_id, an
143 // incarnation is never reused by same-id replacements or later calls.
144 // Every partial-close fragment copied from this lot therefore retains the
145 // same physical-entry provenance. Zero is reserved for legacy/test-only
146 // synthetic lots that were not created by a request record.
147 uint64_t entry_incarnation = 0;
148};
149
150struct Trade {
151 int64_t entry_time;
152 int64_t exit_time;
155 double qty;
156 double pnl;
157 double pnl_pct;
161 std::string entry_id;
162 std::string entry_comment;
163 std::string exit_comment;
164 std::string exit_id;
165 // True when this trade's exit fill came from a REAL strategy.exit
166 // bracket leg (stop/limit/trail/profit/loss), as opposed to a
167 // strategy.close/close_all market close, a reversal-driven close, a
168 // margin-call slice, or an intraday-cap close. Set at two sites:
169 // 1. The native applied-event projection classifies a live EXIT request
170 // whose id is not the adapter's internal "__close__" close command.
171 // 2. Adapter receipt reconciliation preserves the classification for a
172 // bracket that survives a margin reduction.
173 // ABI v4 task 9: closed_trade_close_cause() reads this to distinguish
174 // BRACKET (2) from SCRIPT (1); it is never set on a margin-call /
175 // intraday-cap row (those stay false and are classified from exit_id /
176 // exit_comment instead).
177 bool exit_from_bracket = false;
178 double max_runup = 0.0;
179 double max_drawdown = 0.0;
180 double commission = 0.0;
181 // Physical-entry provenance copied from PyramidEntry. This is deliberately
182 // separate from entry_id: Pine permits user-visible IDs to be reused.
183 uint64_t entry_incarnation = 0;
184 // True for the range-end close of a position still open after the final
185 // bar (record_range_end_close_trades); false for every script-driven
186 // or bracket exit. Mirrors pf_trade_t::open_at_end.
187 bool open_at_end = false;
188 // Why this row exited, when the closer knew. A kernel-originated
189 // liquidation or risk flatten carries its own cause through the settling
190 // execution::Fill; a host that runs its own forced-close policy records
191 // its cause on the row. Unspecified leaves closed_trade_close_cause() to
192 // the generic facts above (open_at_end, exit_from_bracket).
194};
195
196struct TradeC {
197 int64_t entry_time;
198 int64_t exit_time;
201 double pnl;
202 double pnl_pct;
204 // Max Adverse/Favorable Excursion expressed as $ move per unit qty.
205 // max_runup is peak favorable move (price travel in direction of trade).
206 // max_drawdown is peak adverse move (price travel against trade).
207 double max_runup;
209 double qty;
210 double commission; // mirrors pf_trade_t tail; semantics documented in pineforge.h
213 int32_t open_at_end; // ABI v3: 1 on the range-end close row (pineforge.h)
214};
215
222
223// Per-bar runtime trace entry. The transpiler's ``trace`` pragma emits
224// ``trace(name, value)`` calls inside the generated ``on_bar`` so the
225// validator can replay engine-internal series alongside TradingView's own
226// per-bar plot data and pinpoint the bar/filter where divergence appears.
227//
228// ``name_id`` indexes into ``ReportC::trace_names`` — names are interned
229// once per unique label so the per-call cost is push_back of a 24-byte POD
230// rather than a string copy.
232 int64_t timestamp;
233 int32_t bar_index;
234 int32_t name_id;
235 double value;
236};
237
238struct ReportC {
257 // Per-bar trace records emitted by ``BacktestEngine::trace`` calls. Both
258 // arrays are heap-allocated by ``fill_report`` and freed by
259 // ``free_report``; both are nullptr / 0-length when tracing was disabled
260 // or no calls were made. ``trace_names`` is a flat name table — each
261 // ``TraceEntryC.name_id`` indexes into it. Pointers in ``trace_names``
262 // are stable C-strings owned by the live ``BacktestEngine`` instance
263 // (its ``trace_names_`` vector); they remain valid until ``strategy_free``.
266 const char** trace_names;
271 // ABI v4: per-script-bar broker-state hash (empty unless recording enabled).
274};
275
277
278// default_qty_type constants (matches TradingView)
279enum class QtyType { FIXED = 0, PERCENT_OF_EQUITY = 1, CASH = 2 };
280
281// commission_type constants
283
284// Pine user enum → str.tostring (field payloads). Transpiler enforces enum decl before
285// input.enum; this clamps the index so bad values never read past the table.
286struct SymInfo {
287 std::string ticker = "UNKNOWN";
288 std::string tickerid = "UNKNOWN";
289 std::string currency = "USD";
290 std::string basecurrency = "";
291 std::string type = "crypto";
292 std::string timezone = "UTC";
293 std::string session = "24x7";
294 std::string volumetype = "base";
295 std::string description = "";
296 double mintick = 0.01;
297 double pointvalue = 1.0;
298 // Per-instrument quantity step (syminfo.* "qty_step" — the smallest
299 // tradable lot increment, e.g. 0.0004 for BINANCE:ETHUSDT.P). 0 = disabled
300 // (the engine default), so no quantity quantization is applied — corpus
301 // instruments leave this 0 and are byte-identical. Only the forced-
302 // liquidation (margin call) path floors its computed lot to this step to
303 // mirror TradingView, which nibbles the position in exact lot multiples.
304 double qty_step = 0.0;
305};
306
307// Value-owned runtime input transport shared by the public rich run overload
308// and the generic native pre-begin provider. It contains no frontend policy.
309using InputsMap = std::unordered_map<std::string, std::string>;
310
311// The C++ subclass contract is internal, unlike pineforge.h's stable C ABI.
312// Changing its layout or vtable requires all generated/native C++ objects to be rebuilt.
313// v6 adds explicit owner-bound exit-leg activation and Pine placement evidence.
314// Version the mangled class name so older headers' member offsets/vtable cannot
315// silently bind out-of-line members of this different object layout.
316inline namespace engine_script_run_v18 {
317// The fold broker_state_hash() is built on: FNV-1a over a canonical byte
318// spelling of each value (-0.0 folds as 0.0, every NaN as one quiet NaN, a
319// string as its length then its bytes). It is a public, complete type because
320// a host writes into it: BacktestEngine::hash_host_extension receives the sink
321// the kernel has already folded its own broker state into, and whatever the
322// host folds after that is part of every scalar, per-bar and stream hash of
323// the run. Begin an extension with a domain tag of your own (`sink.s(...)`),
324// then fold each durable value in a fixed order.
326public:
327 uint64_t h = 1469598103934665603ULL;
328
329 void bytes(const void* p, size_t n) {
330 const unsigned char* c = static_cast<const unsigned char*>(p);
331 for (size_t i = 0; i < n; ++i) { h ^= c[i]; h *= 1099511628211ULL; }
332 }
333
334 void d(double v) {
335 if (v == 0.0) v = 0.0;
336 if (v != v) v = std::numeric_limits<double>::quiet_NaN();
337 bytes(&v, sizeof v);
338 }
339
340 void i(int64_t v) { bytes(&v, sizeof v); }
341 void u(uint64_t v) { bytes(&v, sizeof v); }
342 void b(bool v) { const unsigned char c = v ? 1 : 0; bytes(&c, 1); }
343 void s(const std::string& v) { u(v.size()); bytes(v.data(), v.size()); }
344};
345// Optional frontend projection interface retained for source compatibility.
346// Broker dispatch itself is virtual on BacktestEngine and never discovers a
347// host kind with RTTI.
349public:
350 virtual ~BrokerStateHashProvider() = default;
351 virtual std::uint64_t broker_state_hash_projection() const = 0;
352};
354protected:
355 // The consumer is the kernel's own execution authority, so this
356 // friendship is also the access path for the request.security feed
357 // machinery a native higher-timeframe subscription drives:
358 // register_security_eval, prepare_native_security_feeds and
359 // feed_security_eval_state stay protected members of this class and are
360 // still out of reach of host code, while the consumer calls them at
361 // begin and from its accepted-input path. No member moved and no
362 // behaviour changed for this.
364 friend class NativeStrategyHost;
365 struct NativeConsumerBindTag { explicit NativeConsumerBindTag() = default; };
369 // The host's own durable state, folded last into broker_state_hash():
370 // called exactly once per hash, after the kernel's generic broker state.
371 // A host whose next decision depends on state the kernel does not own — a
372 // regime, a counter, a model — overrides this and folds it, under its own
373 // domain tag, so a replay that diverges there diverges in the hash. The
374 // override replaces the default; call BacktestEngine::hash_host_extension
375 // first to keep the default's bytes and append to them. A host that
376 // overrides nothing folds exactly what it always folded: the default
377 // forwards to the deprecated spelling below, whose default is the
378 // "source:none" marker.
380 // Deprecated spelling of hash_host_extension, named by the source layer
381 // before a bare host could extend the fold. Still folded when it is the
382 // only one overridden, so an existing subclass compiles and hashes
383 // unchanged; the kernel itself calls hash_host_extension only.
385 virtual std::uint64_t broker_state_hash_projection() const;
386 std::uint64_t broker_state_hash_from_execution_hash(std::uint64_t) const;
387 // --- Position state ---
388 // @broker-state begin
390 double position_entry_price_ = 0.0; // volume-weighted average (for strategy calculations)
391 // Owned, consumable post-fill checkpoint. The shared successful-fill
392 // dispatcher supplies its producer and position identities; economic
393 // eligibility is decided there, independently of this lifecycle model.
396 // Position is FLAT until the first entry fires; the canonical
397 // accessor ``signed_position_size`` already reads as 0 when FLAT
398 // regardless of this default, but several internal carry- and
399 // risk-gating reads (strategy_entry's tv_carry_qty capture,
400 // check_risk_allow_entry's max-position check) read position_qty_
401 // directly. A non-zero default leaks into those reads on the very
402 // first call of any session, producing phantom carry growth (probe
403 // 62 trade #1 fired qty=2 from a default-leaked carry=1) and
404 // spuriously blocked entries when ``risk_max_position_size_=1``.
405 // Initialising to 0 keeps the canonical and direct reads aligned.
406 double position_qty_ = 0.0;
407 int position_entry_count_ = 0; // number of entries in current direction (for pyramiding)
408 int position_open_bar_ = -1; // bar_index_ when position was opened (for exit delay)
409 // Exact position-instance provenance for pending orders whose semantics
410 // depend on the position cycle in which they were placed. A fresh open or
411 // reversal gets a new nonzero id; same-direction pyramid adds retain it.
414 std::vector<PyramidEntry> pyramid_entries_; // individual entries for trade reporting
415
416 // --- Strategy parameters (set from strategy() declaration) ---
417 double initial_capital_ = 1000000.0;
418 // Detached on bare native construction. Only the explicit Pine frontend
419 // attachment can select its source-shape priority interpretation.
421 double commission_value_ = 0.0;
422 int slippage_ = 0; // slippage in ticks
423 double syminfo_mintick_ = 0.01; // tick size for slippage calculation
424 // Per-instrument lot-size step for forced-liquidation quantization.
425 // 0 = disabled (default; corpus no-op). Fed via the syminfo_metadata
426 // channel ("qty_step") or the SymInfo struct on the explicit run() path.
427 // process_margin_call floors each liquidation lot DOWN to a multiple of
428 // this, matching TradingView's per-instrument margin-call lot sizing.
429 double qty_step_ = 0.0;
430 // Account-currency FX multiplier for every quote->account money path. When a
431 // strategy declares ``currency=currency.XXX`` differing from the symbol's
432 // quote currency (e.g. currency.INR on a USDT-quoted perp), TradingView
433 // denominates equity in the account currency but the position notional in
434 // the quote currency, converting the latter via the account-currency FX
435 // rate before the ``required_margin <= equity`` check. The engine otherwise
436 // assumes account == quote (FX 1.0). Injected via the syminfo metadata
437 // channel (key "account_currency_fx"); defaults to 1.0 so every corpus
438 // strategy (which never sets it) is byte-identical. A timestamped provider
439 // may override it as bars advance; the configured scalar remains the
440 // fallback before the provider's first effective point and across reruns.
443 std::vector<double> account_currency_fx_rates_;
444 // Per-run broker clock for timestamped FX. The epoch is the number of
445 // provider points effective at the current script-bar open (0 means the
446 // scalar fallback). Consuming an epoch even while flat prevents a later
447 // entry from being mistaken for a carried position when the rate has not
448 // changed again.
452
453 int64_t trade_start_time_ = std::numeric_limits<int64_t>::min();
454
455
456 // --- SymInfo + Input injection ---
458 int64_t last_bar_time_ = 0;
460 // Forced intrabar path order (ABI v4 live-runtime surface, task 4): 0
461 // AUTO, 1 HIGH_FIRST, 2 LOW_FIRST. Any other value is clamped to AUTO by
462 // set_path_order() -- this member is always one of {0,1,2}. Persistent
463 // configuration -- reset_run_state() does not touch it. See
464 // set_path_order() and the
465 // legacy PathOrderScope guard in engine_run.cpp. Native-bound source
466 // hosts project it into NativeRunSpec::path_order at begin, so the native
467 // driver owns the active batch/stream path order.
469 // Retained last-array-bar visibility flag (waived from the fold). The
470 // probe tail suppression it once gated is the source host's (see
471 // set_probe_suppress_tail_logic below); no kernel path reads it.
472 bool is_tail_bar_ = false;
473 // Chart's display timezone — separate from ``syminfo_.timezone`` (the
474 // exchange TZ). Set by ``set_chart_timezone`` / the C ABI's
475 // ``strategy_set_chart_timezone``. See the doc on ``set_chart_timezone``
476 // for why these two TZ slots must NOT alias.
477 std::string chart_timezone_;
478 std::unordered_map<std::string, std::string> inputs_;
479
480 // Injected symbol metadata (syminfo.shares_outstanding_*,
481 // recommendations_*, target_price_*, pricescale, minmove, …). These
482 // have no source in an OHLCV feed, so the engine returns na<double>()
483 // unless a data feed pushes a value via ``set_syminfo_metadata``. Keyed
484 // by the Pine member name (e.g. "shares_outstanding_total").
485 std::unordered_map<std::string, double> syminfo_metadata_;
486
487 // Input injection helpers for generated code
488 double get_input_double(const std::string& key, double default_val) const;
489 int get_input_int(const std::string& key, int default_val) const;
490 int64_t get_input_int64(const std::string& key, int64_t default_val) const;
491 bool get_input_bool(const std::string& key, bool default_val) const;
492 std::string get_input_string(const std::string& key, const std::string& default_val) const;
493 // input.source: resolve a runtime override string ("open"/"high"/"low"/
494 // "close"/"volume"/"hl2"/"hlc3"/"ohlc4"/"hlcc4") to the matching native
495 // source series. Returns ``default_series`` (the codegen-resolved defval
496 // series) when the key is absent OR the override string is non-native —
497 // the analyzer hard-rejects non-native defvals, so a non-native override
498 // can only arrive from an operator-supplied input value; never crash on it.
499
500
501 // syminfo.* fundamental/exchange metadata that has no OHLCV source.
502 // Returns the value injected via ``set_syminfo_metadata`` for ``key``,
503 // or na<double>() when none was injected. Codegen routes the
504 // na-by-default SYMINFO_MEMBER_MAP double fields here.
505 double get_syminfo_metadata(const std::string& key) const {
506 auto it = syminfo_metadata_.find(key);
507 return it != syminfo_metadata_.end() ? it->second : na<double>();
508 }
509
510 // --- Runtime state ---
512 int bar_index_ = 0;
513 // Timestamp of the input bar that FOLLOWS the one being fed to the
514 // request.security evaluators; 0 = unknown (streams, the feed's last
515 // bar). A historical run holds its whole feed, and the calendar
516 // aggregator uses the hint to finalize a D/W/M bucket on the period's
517 // actual last chart bar -- early closes and exchange holidays included
518 // (TimeframeAggregator::feed(bar, next_input_ms)). Set by whoever pumps
519 // the evaluators, for each bar it feeds; never for a stream's live input.
522 // TV: at most one priced ENTRY "open" event per bar; persists across
523 // multiple native matching calls (bar magnifier) and dual-pass
524 // opposing-stop resolution (see NativeExecutionConsumer).
525
526 // Transient companion for TRAIL exits: the trail's best (peak) price at
527 // fill time. The peak that armed the trailing stop is by definition a
528 // pre-fill favorable excursion of the closing trade (TV reports
529 // MFE == fill + offset == peak), but first_touch_position can't place a
530 // trail fill on the bar path (the level is only active after the peak),
531 // so emit_close_trade folds the peak directly. NaN = not a trail fill.
532 double fold_exit_trail_peak_ = std::numeric_limits<double>::quiet_NaN();
533 // Set by evaluate_fill_price: the just-evaluated exit fill fired on the
534 // TRAIL leg (vs stop/limit/gap). Consumed by apply_filled_order_to_state
535 // to reconstruct the trail peak above.
536 std::vector<Trade> trades_;
537 // TradingView's range-end accounting (record_range_end_close_trades,
538 // engine_orders.cpp): the rows that close a position still open after
539 // the final script bar, at that bar's close. Report-only — they are
540 // merged behind trades_ by fill_trades_section and never enter trades_,
541 // the realized sums, or the live position (a stream continues it).
542 std::vector<Trade> range_end_trades_;
543 // A rejected strategy.entry call leaves no request record behind. The exact
544 // collision gate can consume only the immediately preceding source bar, so
545 // one scalar tombstone is sufficient and cannot grow with feed length.
546
547 // Actual command/review/sizing causes; policy history is a transient fold.
548 // Evaluation-scoped tombstones for live priced ENTRY objects actually
549 // removed by strategy.cancel(id). invoke_chart_on_bar clears the map
550 // before each script execution; the first fresh same-id strategy.entry
551 // consumes the unique cancelled incarnation.
552
553 // strategy.exit partial orders are one-shot per open position for a given id
554
555 // Reusable scratchpad for the per-call opposing-stop deferral set in
556 // request matching. Holds the ids of flat-issued entry stops that
557 // lost the intra-bar path race in pass 0 and are reconsidered in pass 1.
558 // Cleared at the start of each request matching call; the retained
559 // capacity avoids a fresh heap allocation 2-4x per bar. Typically tiny
560 // (0-1 entries). Not state — must be empty across calls.
561
562 // Reusable scratch for request matching (capacity persists across
563 // calls, mirroring scratch_skip_ids_). Incarnations survive OCA erasure;
564 // vector indices and retained replacement priorities do not identify an
565 // object. Always cleared before use; never persistent cancellation state.
566
567 // Per-PASS dual-entry-stop arbitration winner (a flat position resting
568 // one long stop-only ENTRY + one short stop-only ENTRY, both touched
569 // this bar -- dual_entry_stop_path_winner, engine_path_resolve.cpp).
570 // Reset to None at the top of every request matching CALL (a
571 // close-timing mode script bar calls it twice per bar -- old-
572 // order settlement, then new-order fills -- and each pass re-derives
573 // its own flat-position winner) and written where that arbitration is
574 // decided. This is working state, NOT the public accessor's value --
575 // it goes back to None the moment the winning side fills (position no
576 // longer FLAT) or its admission is declined (the release at the
577 // `path_winner_stop_margin_decline` site below), even though a real
578 // arbitration happened this bar. last_bar_dual_entry_path() reads
579 // last_bar_dual_entry_decision_ (below) instead, precisely to survive
580 // that. Only the standard (non-calc_on_order_fills) dispatch path
581 // updates this; the COOF scheduler's process_next_pending_order keeps
582 // its own unrelated local of the same computation and does not persist
583 // it here.
584 // Per-BAR snapshot of the above: the last non-None value
585 // dual_entry_path_ took during this bar, surviving whatever
586 // dual_entry_path_ itself does afterward (a fill, a declined admission
587 // release, or the next request matching call's reset). Reset to
588 // None once per bar -- at the top of dispatch_bar() and, for the bar
589 // magnifier (which never reaches dispatch_bar), where bar_index_
590 // advances for each emitted script bar in run_aggregation_bar_loop --
591 // and written ONLY alongside dual_entry_path_'s own arbitration write
592 // (the native applied-event projection), never at the declined-admission release. ABI v4
593 // live-runtime surface (task 4): this is what last_bar_dual_entry_path()
594 // returns, so a live probe (or an ordinary POOC run, tail-suppressed or
595 // not) reads the bar's real arbitration even if the winning order later
596 // filled, was declined, or the working state otherwise moved on. Same
597 // fill-recalculation mode caveat as dual_entry_path_ above.
598
599 // --- Trailing stop state ---
600 // Best favorable price since position entry (for trailing stop computation)
601 // The script bar on which a strategy.exit re-issue restarted
602 // trail_best_price_ from the bar's CLOSE under process_orders_on_close
603 // (round 9 family Z's restart rule, round 10 family Y's bar rule). The
604 // restarted extreme is the NEW order's, and that order's path starts at
605 // the next bar's open: the same bar's high/low must not be folded into
606 // it by the close-time request matching that follows the script
607 // body (update_trail_best_for_bar_open skips this bar). -1 = none.
608 // The position's running extreme as it stood BEFORE the current bar's
609 // high / low were folded in (update_trail_best_for_bar_open), and the
610 // bar it was captured on: a trail leg killed by a declined reversal on
611 // this bar restarts from it (round 10 family AE,
612 // adapter placement fact `dormant_trail_best`).
613 // The ordinary POOC close scan may revisit a retained trail with that
614 // same pre-bar extreme only while the carried position is unchanged.
615 // A new cycle, add, reduction or close-time trail restart keeps its own
616 // established path state instead of inheriting an earlier position's.
617
618 // Best favorable price is updated by the physical open/add helpers for
619 // every host. Source policy may consume the same value through
620 // inheritance, while the public C observer uses the virtual projection.
621 double trail_best_price_ = std::numeric_limits<double>::quiet_NaN();
622
623 // Generic synchronous close obligation. Pine quota/cause/beneficiary
624 // state remains exclusively in the compatibility facade above.
626
627 // Temporary legacy adapter: only value facts cross into Pine policy.
628
629 // --- Cached trade metrics (updated incrementally in execute_market_exit) ---
630 double net_profit_sum_ = 0.0;
631 // Conservative absolute roundoff accumulated by additions to the cached
632 // net-profit sum. This is numerical provenance only: reported PnL and
633 // equity continue to use the unchanged sum above. Infinity means a
634 // narrower margin comparison cannot be justified from this history.
636 // The net value whose additions this bound tracked. Direct/synthetic or
637 // future restore writes without matching provenance cannot narrow the
638 // established margin comparison; the next trade makes them unbounded.
640 double gross_profit_sum_ = 0.0;
641 double gross_loss_sum_ = 0.0;
644
645 // --- Equity extremes for max_drawdown / max_runup ---
646 double max_equity_ = 0.0; // peak equity for drawdown
647 double max_drawdown_ = 0.0; // maximum drawdown (positive number)
648 double max_runup_ = 0.0; // maximum runup (positive number)
649 double min_equity_ = 0.0; // trough equity for runup
650
651 // --- Per-script-bar equity curve (metrics + pf_report_t exposure) ---
652 std::vector<pf_equity_point_t> equity_curve_;
653 int64_t bars_in_market_ = 0; // script bars with an open position at close
654 double first_bar_open_ = std::numeric_limits<double>::quiet_NaN(); // buy&hold basis
655
656 // --- Per-script-bar broker-state hash recording (ABI v4 live-runtime
657 // surface, task 6). Default off: empty vector, empty report array,
658 // every historical run byte-identical to before this flag existed. See
659 // set_broker_state_hash_recording() and broker_state_hash(). ---
661 std::vector<uint64_t> broker_state_hashes_;
662
663 // --- Position-size extremes (strategy.max_contracts_held_*) ---
667
668 // --- Even-trade counter (strategy.eventrades) ---
670
671 // --- Risk management (strategy.risk.*) ---
672
673 // Risk state tracking
674
675 // TradingView's strategy.risk.max_intraday_loss (round 7 family M
676 // mechanism 5b, pinned 2026-09-05 by lab tv m45-risk-t1/t6/t9/t3b on
677 // BINANCE:BTCUSDT 1D and the JOAT probe's threshold ladder
678 // m45-joat-risk-*): the rule keeps the DAY-START equity E_ds = realized
679 // + the open position marked at the chart-tz day's first tick, and at
680 // every emulated tick compares loss = E_ds - (realized + open P&L at
681 // the tick) with pct% of E_ds (percent_of_equity) or the absolute
682 // value. A fill that CLOSES position quantity is checked with the
683 // position already gone but its own realized P&L NOT yet booked, so a
684 // profitable exit whose day-start open profit was >= the threshold
685 // fires (the JOAT 2026-02-06 short: open profit 2513.6 = 2.452% of
686 // 102513.6 fires at 2.45, not at 2.46; the +2699 exit is a gain by every
687 // other measure). Realized P&L booked earlier in the day counts at later
688 // ticks (t9). Firing closes the position at the tick as "Close Position
689 // (Max intraday Loss)", cancels every pending order and blocks every
690 // order placement until the chart-tz day changes (t1: the close-calc
691 // order of the fired day is dropped, the next day's fills). The rule
692 // never latches risk_halted_ (that stays with max_drawdown /
693 // max_cons_loss_days).
694 // A fire inside a fill loop defers the pending-order cancel to the
695 // loop's safe point (finish_intraday_loss_cancel); the loop itself
696 // removes every order it has not yet applied.
697 // @broker-state end
698 // Host-installed excursion capability (RULING A48). Transient run
699 // wiring, not durable broker state: reset_run_state clears it and the
700 // consumer reinstalls it once per run when the host declares ownership.
702 // Continuation digest at the last script point. Native batch teardown
703 // moves the consumer into Completed and would otherwise change the scalar
704 // relative to the recorded array; source state is still folded live so
705 // post-run mutations remain visible.
708 // --- Per-trade extreme tracking ---
710 const execution::Action& action, const execution::Fill& fill,
713 const execution::Action& action, const execution::Fill& fill,
714 const execution::LifecycleEffects& lifecycle,
717 const execution::Action& action, const execution::Fill& fill) const;
718 // Synchronous selected-close extensions. Book preserves the original
719 // action semantics; OpeningExposure permits only Flatten/Reduce and is
720 // revalidated against the current physical book on every call.
722 const execution::Action& action, const execution::Fill& fill,
723 execution::CloseScope scope) const;
725 const execution::Action& action, const execution::Fill& fill,
728 // --- Additive selected close (not a CloseScope overload) ---
730 const execution::Action& action,
731 const execution::Fill& fill,
732 const execution::SelectedOpeningSet& selection) const;
734 const execution::Action& action,
735 const execution::Fill& fill,
737 const execution::SelectedOpeningSet& selection);
738 // Source lifecycle coordinator: current_bar_/fold context, source-day
739 // preflight, selected commit, then observation of its committed close rows.
740 // Empty lifecycle is valid. Do not fold selection into LifecycleEffects.
742 const execution::Action& action,
743 const execution::Fill& fill,
744 const execution::LifecycleEffects& lifecycle,
745 const execution::SelectedOpeningSet& selection);
746 // --- Additive non-applying account/effect projection ---
748 const execution::Action& action,
749 const execution::Fill& fill) const;
751 const execution::Action& action,
752 const execution::Fill& fill,
753 execution::CloseScope scope) const;
755 const execution::Action& action,
756 const execution::Fill& fill,
757 const execution::SelectedOpeningSet& selection) const;
758 // Opposite-book reversal to an exact signed exposure. Unlike Transact,
759 // the opening is not a remainder of transaction units minus held units.
760 // These synchronous extensions share the settlement owner and do not
761 // introduce a queued native request or saved commit authority.
763 const execution::ReverseTo& reversal,
764 const execution::Fill& fill) const;
766 const execution::ReverseTo& reversal,
767 const execution::Fill& fill) const;
769 const execution::ReverseTo& reversal,
770 const execution::Fill& fill,
772 // Source reversal coordinator with current chart context and source days.
774 const execution::ReverseTo& reversal,
775 const execution::Fill& fill,
776 const execution::LifecycleEffects& lifecycle);
777 // Native account value: realized balance plus marked physical lots minus
778 // their remaining paid entry costs, for every fee type. No Pine sizing or
779 // end-of-range reporting convention participates in this value.
780 double marked_equity(double price) const;
781
782 // --- Strategy order commands ---
783 // NOTE: prior to v0.2 the runtime accepted a leading `double market_price`
784 // positional after `is_long`. The implementation never read it; every
785 // fill price came from `current_bar_.close` inside the function body,
786 // and every closed-transpiler call site passed `current_bar_.close`
787 // verbatim. Parameter dropped to match TradingView's `strategy.entry()`
788 // surface. Consumer codegen must be regenerated alongside this commit.
789
790
791 // Keep the historical five-argument symbol above; regenerated legacy
792 // sources continue to bind token 0. This does not promise that arbitrary
793 // objects compiled against an older BacktestEngine class layout can be
794 // relinked without rebuilding. New codegen supplies a stable nonzero token
795 // for the syntactic strategy.close source site.
796
797
798
799
800
801
802
803
804
805 // TradingView forced-liquidation (margin call). Finite-price liquidation
806 // paths use the bar's adverse extreme. A 100%-margin long instead uses
807 // opening affordability and the separately scoped rounded-money checks.
808
809 // Settle an opening money restore before a later eligible owned exit,
810 // retaining the actual chart bar for financial-class eligibility.
811
812 // Ordinary subcontract shorts and integer MARKET lots expose completed
813 // liquidation to the close-time script (R23/R25/R28 TV controls).
814
815 // An unchanged carried POOC short finishes its adverse-path margin event
816 // before the close script observes or reverses it. The caller proves no
817 // resting order filled earlier on this bar.
818
819 // finding-308: chronological pre-exit forced-liquidation slice. Called
820 // from the request matching fill loop immediately BEFORE a priced
821 // exit of the live position is applied. Fires only when (a) no margin
822 // call was booked on this bar yet, (b) the bar's adverse extreme comes
823 // STRICTLY earlier on the synthesized intrabar path than the exit's
824 // fill (a tie — the exit filling exactly at the extreme — keeps the
825 // exit first), and (c) the pre-fill position is already in margin
826 // deficit at that extreme. The slice mirrors the adverse-cascade
827 // trigger/slice arithmetic of process_margin_call byte-for-byte.
828 // Returns true when a "Margin call" row was booked; the triggering exit
829 // then fills the reduced remainder.
830
831 // finding-325 (1x-long entry-fill affordability chronology): TV runs the
832 // 1x-long (margin_long=100) opening-affordability check AT THE ENTRY
833 // FILL, chronologically before the same bar's intrabar exits. When a
834 // priced exit of a just-opened 1x long is about to fill on the entry's
835 // own bar and the floor-sized opening cost exceeds post-close equity,
836 // the one-shot opening event books its trim FIRST — the ordinary
837 // floor-before-4x quantity (including the sub-lot one-contract
838 // fallback), filled at the RAW matched entry base, tagged "Margin call"
839 // — and the exit then closes the reduced remainder. Consumes the
840 // pending opening event; returns true when a slice was booked.
841
842 // A timestamped FX rollover is a broker-open event, not an end-of-bar
843 // adverse-price check. Cell A1 supports carried 1x full-margin long and
844 // short in ordinary historical dispatch; leveraged shapes stay fail-closed.
845 // Returns true when it emits a broker liquidation row.
846
847 // finding-430: forced liquidation at the bar OPEN. A carried position
848 // with a finite liquidation price that already breaches the margin
849 // requirement at the open is sliced AT THE OPEN (quantity computed at
850 // the open price), before any resting order is evaluated there; the
851 // survivor keeps its ordinary adverse-extreme check, so one bar can book
852 // an open slice AND an extreme slice. Bars whose open does not breach
853 // are untouched. Returns true when a "Margin call" row was booked.
854
855 // Round 7 family H residual (NYSE:F 1D short admission tape 2025-04-23 /
856 // 2026-04-08): a strategy.close / close_all MARKET order for the WHOLE
857 // position resting for this bar's open fills before the open's margin
858 // evaluation — TradingView closes 1025 @9.84 through the pending close
859 // where the engine sliced 48 @9.84 first. True when such an order rests
860 // in the book (created on a prior bar, no priced leg, covers the whole
861 // position) AND no opposite-side entry order rests for the same open:
862 // a close paired with a reversal entry is voided when TradingView
863 // declines that reversal by admission (pin log-20260905t111645z-
864 // e1783b94), so the open slice must stand (round-8 regression on the
865 // all-in reversal scripts: amandaborgeson06 F@15 2025-05-01 13:30Z,
866 // hexatrades AAPL@15 2025-07-29 13:30Z). margin_call_slice_at_bar_open
867 // then stands down only for the unconditional close. A partial close or
868 // a priced exit the open gapped through keeps the open slice (unpinned).
869
870 // Round 7 family L (campaign pin log-20260905t093952z-0c4938cb; lab tv
871 // tapes scratchpad/r7/pins/xau15-mcpath-{a,b}, fresh-touch-once): on the
872 // bar a position OPENS, TradingView marks the forced liquidation only
873 // over the part of the synthesized O-H-L-C / O-L-H-C path AFTER the
874 // entry fill — a bearish bar whose stop fill lies below the open never
875 // sees that bar's high (asian-box 04-01 15:45Z: no slice; xau15-mcpath-a:
876 // the slice comes on the next bar at its high), the bar CLOSE is a mark
877 // point (fresh-touch-once: 8 @11.25 = the entry bar's close), and a fill
878 // at the open (market, or a stop the open gapped through) sees the whole
879 // bar (xau15-mcpath-b, mdfe3757 04-08 13:30Z: same-bar slice at the
880 // high). Carried bars keep the whole-bar extreme.
881 //
882 // True when the just-opened position leaves a path suffix on `bar`:
883 // *out_mark is the suffix's adverse extreme (RAW price — the waypoints
884 // after the fill, the close included; the caller applies the cascade's
885 // own mintick rounding), *out_pos its path position (waypoint index, in
886 // first_touch_position's units). An unrouted fill coordinate (market /
887 // open fills, stop-limit, raw orders) reads as the open, i.e. the whole
888 // bar as before; a fill at the close has no suffix (false).
889
890 // The dispatch shapes the entry-bar path rule is pinned for: the position
891 // opened on this bar under ordinary historical dispatch (no
892 // process_orders_on_close, calc_on_order_fills, bar magnifier or
893 // streaming). Everything else keeps the whole-bar extreme.
894
895
896 // --- Fill rounding helpers ---
897 // Nearest-tick rounding: TradingView's exact double-precision function
898 // floor(price / mintick + 0.5) * mintick, with NO epsilon.
899 //
900 // finding-446: TV's own NASDAQ:AAPL and OANDA:EURUSD series carry
901 // sub-tick prints (x.xx5 and a few 4-dp values). Every TV fill taken at
902 // one of those RAW BAR PRICES is exactly this function of it: 24,582 /
903 // 24,582 half-cent AAPL closes (22,122 rounded up, 2,460 rounded DOWN
904 // because the binary quotient lands just under the midpoint —
905 // 228.765 / 0.01 = 22876.499999999996 -> 228.76, while
906 // 214.385 / 0.01 = 21438.5 -> 214.39) and 142,938 / 142,938 EURUSD
907 // fills at tick 1e-5. Any epsilon nudge (floor(r + 0.5 + 1e-6)) or a
908 // decimal half-up rule forces every binary midpoint up and breaks the
909 // 2,460 down cases. For r >= 1 the form below is bit-identical to
910 // std::round(r) (adding 0.5 is exact inside one binade), so the legacy
911 // std::round shape was already the right function; it is spelled out so
912 // the code reads as the census formula it was fitted to.
913 double round_to_mintick(double price) const {
914 if (std::isnan(price) || syminfo_mintick_ <= 0.0) return price;
915 return std::floor(price / syminfo_mintick_ + 0.5) * syminfo_mintick_;
916 }
917
918 // round 8 family R scope: the 10-significant-digit money arithmetic
919 // (tv_money_round) is applied where it was pinned and where it can move a
920 // lot — a lot-stepped instrument whose lot is worth less than one unit of
921 // account currency at the sizing price (OANDA:EURUSD 0.01 x 1.1 = 0.011;
922 // BINANCE:BTCUSDT 1e-5 x 85,000 = 0.85; ETHUSDT.P 1e-4 x 2,500 = 0.25).
923 // On integer-lot instruments (F, AAPL, ES, NQ, NIFTY: a lot is 10..250k)
924 // and on the corpus' continuous qty_step 0 the exact arithmetic stays:
925 // there the rounding could only ever bite on a synthetic exact tie.
926 // POOC flat-parent controls: cost rounding also precedes admission at the
927 // terminal close. Named child brackets created after the sole pending
928 // parent have no live owner yet; they cannot compete for its opening cash.
929 // Same-bar POOC money admission for one true-flat long MARKET parent.
930 // Quantity sizing keeps its existing slipped divisor. The money checks
931 // use the recorded signal mark and a tick-built slipped price instead.
932
933 // R24 signal-cost controls: a fractional lot worth >=1 account unit can
934 // still cross the rounded-money admission boundary. BTC Q9.36259 at
935 // 112380.33 costs 1052170.9538547, rounded to 1052170.954; exact signal
936 // equity 1052170.9536054998 therefore keeps only a reversal's close leg.
937 // XAU Q300 at 3443.625 likewise rejects capital cost-0.0001, while exact
938 // cost and cost+0.0001 admit, despite the cheaper next opening price.
939 // R39 BTC/XAU controls also pin the independent price-scale check for
940 // this ordinary market book. Share its scope while keeping the separately
941 // pinned POOC signal-cost extension out of the price-scale extension.
942 // Rule 1's scope (round 10 family AE): the ten-digit equity and the raw
943 // lot floor size EVERY lot-stepped instrument — integer shares included
944 // (NASDAQ:AAPL 2025-09-05 16:15Z: 1094521.68 / 238.77 floors to 4583 on
945 // TradingView, 4584 from the float-accumulated ledger or a nudged
946 // floor). Only a continuous qty_step 0 keeps the exact arithmetic.
947 // The broker's required margin at a mark is money at ten significant
948 // digits where that can move a lot (famr-adm-rev-01000: the 4x trim of
949 // the fill bar's short is 3900.60 from the ROUNDED 1000527.321 required
950 // margin, 3900.56 from the exact 1000527.3207023 — one lot of restore
951 // quantity across the 0.01 floor; rev-00800 / -01079 / revb / S3 keep
952 // their trims either way). Exact outside the scope.
953
954 // A fill taken AT A RAW BAR PRICE — a market order at the bar close
955 // (process_orders_on_close) or at the next open, a resting stop/limit
956 // the open gapped through, a stop-limit whose limit is already
957 // marketable at an OHLC path point, a margin-call slice at the open or
958 // adverse extreme, a strategy.close at the close / COOF bar-point
959 // cursor — books the raw print rounded to the NEAREST tick and only then
960 // carries slippage ticks. The FEED is never quantized (indicators consume
961 // the raw sub-tick values); only the fill and the broker's default-sizing
962 // snapshot (calc_qty / frozen_sizing_price, same nearest-tick form) are
963 // on-tick. The directional snap
964 // (native_matching::grid_round_directional, src/native_matching.hpp) is
965 // reserved for COMPUTED stop / limit LEVELS that fall between ticks;
966 // applying it to a raw bar price was the finding-432/446 defect (sells
967 // floored, buys ceiled — 43 AAPL slugs off by one tick). The result is
968 // on-tick, so the matcher's directional snap downstream is an identity
969 // on it (its ladder-exactness test absorbs the n*tick/tick FP residue).
970 double bar_fill_price(double raw_bar_price) const {
971 return round_to_mintick(raw_bar_price);
972 }
973
974 // design-stop-tick-rounding (round 6): the broker emulator TESTS a
975 // resting stop / limit against the bar's OHLC quantized to the tick
976 // (nearest, the finding-446 formula), while the order LEVEL stays raw;
977 // the fill keeps its existing directional / limit-or-better snap.
978 //
979 // Pinned on NYSE:F 1D (mintick 0.01, sub-penny prints; lab tv tapes
980 // scratchpad/r6/pins/stopround-*, 2026-09-04):
981 // long sell-stops 13.74624 / 13.7451 / 13.7449 / 13.745 all SKIP
982 // 2026-02-02 (low 13.745 -> 13.75) and fill 02-03 @13.74, while
983 // 13.3449 fills 01-26 (low 13.3448 -> 13.34) @13.34 — neither a raw
984 // compare (02-02 would fill) nor a floored/ceiled level (01-26
985 // would not) explains both; only the quantized low does;
986 // short buy-stops 14.0349 / 14.03505 / 14.0352 all fill 02-03 (high
987 // 14.0351 -> 14.04) @14.04; 13.225 skips 12-09 (high 13.2202 ->
988 // 13.22): the high rounds to NEAREST, not up;
989 // sell-stop 13.776 over the 02-20 open 13.775 (-> 13.78) fills at the
990 // level 13.77, not at the open: the open is quantized too;
991 // the same bars/levels reproduce for strategy.exit(limit=),
992 // strategy.entry(stop=) / (limit=) and strategy.order(stop=), long
993 // and short (stopround-xl-*, -es-*, -el-*, -eo-*);
994 // stopround-ohlc-0/1 encode Pine's own low/high in the trade qty:
995 // 13.745, 13.3448, 14.0351 — the RAW prints, identical to the feed,
996 // so the quantization lives in the broker, not the data.
997 // The trail leg is NOT covered (stopround-xt-L-trail: trail_points 20 /
998 // trail_offset 3 over the 14.035 high exits at the next open, the raw-
999 // extreme behaviour the engine already has), so the trail keeps walking
1000 // the raw path; stop-limit entries, the process_orders_on_close close
1001 // compares and the calc_on_order_fills cursors were not pinned either
1002 // and stay raw.
1003 //
1004 // The grid point is materialized as k / (1/mintick) when 1/mintick is
1005 // integral (every decimal tick), which is the double a Pine literal on
1006 // that tick parses to — so an on-grid level compares EQUAL to a
1007 // quantized bar price bit-for-bit (14.04 vs k*0.01 = 14.040000000000001
1008 // would not). Non-decimal ticks fall back to k*mintick.
1009 double tick_grid_price(double price) const {
1010 if (std::isnan(price) || syminfo_mintick_ <= 0.0) return price;
1011 const double k = std::floor(price / syminfo_mintick_ + 0.5);
1012 const double inv = 1.0 / syminfo_mintick_;
1013 const double inv_int = std::floor(inv + 0.5);
1014 if (inv_int > 0.0 && std::abs(inv - inv_int) <= 1e-6 * inv_int) {
1015 return k / inv_int;
1016 }
1017 return k * syminfo_mintick_;
1018 }
1019 // round 8 family T (NYSE:F@15, pinned by 40 `lab tv` sensor tapes on
1020 // NYSE:F / CME_MINI:ES1! / OANDA:EURUSD 15m, scratchpad famT/pins,
1021 // 2026-09-05): TradingView stores a resting stop / limit LEVEL on the
1022 // symbol's PRICE GRID (multiples of 1 / pricescale, pricescale =
1023 // 10^decimals of the tick) whenever the level sits within
1024 // 0.01 / pricescale^2 of a grid price — the residue a level computed
1025 // as avg_price +/- k * mintick carries (9.99 + 0.05 =
1026 // 10.040000000000001, 11.86 - 0.05 = 11.809999999999999) and anything
1027 // up to 1e-6 on a 2-decimal symbol (10.040001 IS 10.04; 10.0400012 is
1028 // not and takes the directional snap: a sell limit at 10.0400012 fills
1029 // at 10.05). ES1! (tick 0.25, pricescale 100) has the same 1e-6 band
1030 // (5513.7500005 IS 5513.75, 5513.750001 is not); EURUSD (pricescale
1031 // 1e5) snaps only within 1e-12 (1.135560000001 IS 1.13556,
1032 // 1.135560000002 is not). The engine used to compare the RAW level
1033 // against the tick-quantized bar, so a bar whose quantized extreme
1034 // EQUALS the level (h 10.04, or h 10.035 -> 10.04, vs
1035 // 10.040000000000001) did not fill and the exit landed bars later at
1036 // the same snapped price — 148 of the 179 exit-time mismatches on the
1037 // F@15 lane (masayanfx-scalping 102, latibonit 17, jos-protrader 8,
1038 // vasudevshenoy 6, lukeborgerding, drakkhon, rhyme17, hariss369,
1039 // colasbreugnon, fast-scalper, JOAT aureate). Applied where a level is
1040 // stored on a request record (strategy.entry / exit / order, and the
1041 // profit / loss tick conversion), so every trigger test, gap test,
1042 // marketable-at-placement test and fill snap reads the grid value —
1043 // materialized as k / pricescale, the double the decimal literal
1044 // parses to, bit-for-bit equal to tick_grid_price's output. A level
1045 // outside the band is returned untouched (the directional fill snap
1046 // and the exact trigger compare keep TV's sub-tick behaviour, round 6).
1047 // A binary tick (1/128 = 0.0078125) resolves to a 7-decimal grid whose
1048 // band is 1e-16: effectively untouched; a tick with no short decimal
1049 // expansion has no grid and is untouched.
1050 static constexpr double kLevelGridBandPoints = 0.01; // x 1/pricescale
1052 if (syminfo_mintick_ <= 0.0) return -1;
1053 double scaled = syminfo_mintick_;
1054 for (int n = 0; n <= 10; ++n) {
1055 const double k = std::floor(scaled + 0.5);
1056 if (k >= 1.0 && std::abs(scaled - k) <= 1e-6 * k) return n;
1057 scaled *= 10.0;
1058 }
1059 return -1;
1060 }
1061 double level_on_price_grid(double level) const {
1062 if (std::isnan(level) || !std::isfinite(level)) return level;
1063 const int n = price_grid_decimals();
1064 if (n < 0) return level;
1065 double pricescale = 1.0;
1066 for (int i = 0; i < n; ++i) pricescale *= 10.0;
1067 // Points as level / pointsize (pointsize = 1 / pricescale as a
1068 // double): of the candidate arithmetics this is the one that
1069 // reproduces every tape at the band's FP boundary (EURUSD
1070 // 1.13556 + 1e-12 snaps, + 2e-12 does not; ES 5513.75 + 5e-7
1071 // snaps, + 1e-6 does not; F 10.04 + 1e-6 snaps, + 1.2e-6 does not).
1072 const double pointsize = 1.0 / pricescale;
1073 const double p = level / pointsize;
1074 const double k = std::floor(p + 0.5);
1075 if (std::abs(p - k) <= kLevelGridBandPoints / pricescale) {
1076 return k / pricescale;
1077 }
1078 return level;
1079 }
1080 Bar broker_tick_bar(const Bar& bar) const {
1081 Bar b = bar;
1082 b.open = tick_grid_price(bar.open);
1083 b.high = tick_grid_price(bar.high);
1084 b.low = tick_grid_price(bar.low);
1085 b.close = tick_grid_price(bar.close);
1086 return b;
1087 }
1088 // The bar the stop / limit trigger tests run on. A synthetic bar — the
1089 // calc_on_order_fills scheduler's point / monotonic-segment bars and the
1090 // KI-67 cascade waypoint bar — is a slice of an already-decided path and
1091 // is compared raw, exactly as before; a real chart / lower-TF bar is
1092 // quantized.
1093
1094 // --- Commission helper ---
1095 // PERCENT commission is a % of the order's notional value. The notional
1096 // (fill_price × qty × pointvalue) is in the symbol's QUOTE currency; the
1097 // commission a strategy() reports is in ACCOUNT currency, so it needs the
1098 // same instrument->account conversion as the margin gate below
1099 // (account_currency_fx_, default 1.0 — no-op for the corpus). Cash-per-
1100 // order / cash-per-contract are already account-currency-native (a
1101 // trader configures "$20 per contract" in their own currency), so they
1102 // are untouched.
1103 double calc_commission(double fill_price, double qty) const {
1104 switch (commission_type_) {
1106 return fill_price * qty * syminfo_.pointvalue
1108 * (commission_value_ / 100.0);
1110 return commission_value_;
1112 return commission_value_ * qty;
1113 }
1114 return 0.0;
1115 }
1116
1117 // Read the paid entry fee for a still-open pyramid slice. Production entry
1118 // paths always initialize the snapshot. The fallback keeps hand-constructed
1119 // PyramidEntry fixtures source-compatible without weakening real lifecycle
1120 // behavior.
1121 double open_entry_commission(const PyramidEntry& pe) const {
1122 return std::isfinite(pe.entry_commission_account)
1124 : calc_commission(pe.price, pe.qty);
1125 }
1126
1127 double allocated_entry_commission(const PyramidEntry& pe, double units) const {
1128 if (units <= 0.0) return 0.0;
1129 const double paid = open_entry_commission(pe);
1130 return units >= pe.qty ? paid : paid * (units / pe.qty);
1131 }
1132
1136
1137 // --- Position sizing helper ---
1138 // PERCENT_OF_EQUITY / CASH size a budget that is denominated in ACCOUNT
1139 // currency (equity, and a strategy.cash default_qty_value are both
1140 // account-currency-native — see emit_close_trade / current_equity()),
1141 // then convert it into a quantity of the instrument, whose price is in
1142 // QUOTE currency. Divide the account-currency cash by account_currency_fx_
1143 // first (the inverse of the instrument->account multiply used for
1144 // commission/PnL/margin) so the division by fill_price stays dimensionally
1145 // consistent; default 1.0 leaves the corpus untouched.
1146 // Floor an order quantity to the instrument's tradable lot increment
1147 // (qty_step_). TradingView applies this to EVERY order it sends to the
1148 // exchange, not just forced liquidations — verified row-for-row: a
1149 // computed DCA/safety-order quantity (e.g. baseOrderSize/close) is
1150 // floored, not rounded, before it ever contributes to cost basis or a
1151 // fill (the source adapter applies the same rule to liquidation lots).
1152 // qty_step_ == 0 (corpus default) leaves qty
1153 // untouched. A quotient that is only binary64 residue below an integer is
1154 // treated as that integer, using the same 1e-6-of-a-step tolerance as
1155 // percent-derived exits below. This keeps an on-grid request such as
1156 // 1 / 0.00001 from losing a whole lot because the quotient materializes as
1157 // 99999.999999..., while a genuinely off-grid request still floors. When
1158 // quantization would be a no-op, preserve the original double so the
1159 // tolerance never increases a requested quantity. Unlike the liquidation
1160 // path, a regular entry legitimately CAN floor to zero (an under-funded
1161 // order is simply not placed), so there is no "never stall"
1162 // floor-to-one-step fallback here.
1163 double apply_qty_step(double qty) const {
1164 if (qty_step_ <= 0.0 || !std::isfinite(qty) || qty <= 0.0) return qty;
1165 double floored = std::floor(qty / qty_step_ + 1e-6) * qty_step_;
1166 return floored < qty ? floored : qty;
1167 }
1168
1169 // Percent-derived strategy.exit lots are floored to the same lot
1170 // increment (TV evidence, BINANCE:ETHUSDT.P qty_step 0.0001: a
1171 // qty_percent=50/50 short bracket over a 5.4103 position fills
1172 // 2.7051 + 2.7051, leaving a 0.0001 dust short OPEN until the next
1173 // reversal/close/margin-call — 39 of stockhunter2025-btcusd-4h-ema-
1174 // swing-strategy's 56 unmatched TV trades were exactly such dust
1175 // rows). Unlike apply_qty_step this floor is epsilon-tolerant: 50%
1176 // of an on-grid position is often exactly on-grid in real numbers
1177 // but lands one ulp below the grid ratio in doubles (2.7051/0.0001
1178 // = 27050.999999…), and a plain floor would knock such a leg a FULL
1179 // step down, inventing dust TV does not have. The tolerance (1e-6 of
1180 // a step) sits far above double representation error at realistic
1181 // qty/step magnitudes yet far below any genuine sub-step remainder.
1182 // When the floor is a no-op (qty already on-grid) the ORIGINAL double
1183 // is returned unchanged: reconstructing it as floor(...)*step lands
1184 // one ulp away (0.3 -> 0.30000000000000004) and that representation
1185 // jitter leaks into printed PnL at the 1e-6 digit for strategies whose
1186 // percent legs were already exact (officialjackofalltrades' 30%-of-1
1187 // legs) — a pure artifact this fix must not introduce.
1188 double apply_exit_qty_step(double qty) const {
1189 if (qty_step_ <= 0.0 || !std::isfinite(qty) || qty <= 0.0) return qty;
1190 double floored = std::floor(qty / qty_step_ + 1e-6) * qty_step_;
1191 return floored < qty ? floored : qty;
1192 }
1193
1194 // Integer-lot symbols keep one minimum contract/share for any positive
1195 // percent-derived strategy.exit request when at least one whole step is
1196 // still unreserved. TradingView evidence on one-contract ES/NQ/NIFTY
1197 // positions shows a pair of qty_percent=50 siblings reserving 1 + 0, not
1198 // 0 + 0. Fractional-lot symbols retain the floor/dust rule above.
1199 //
1200 // This helper is intentionally exit-percent-specific: explicit exit qty,
1201 // full-percent exits, entries and other broker quantities must not acquire
1202 // a minimum-one-step fallback.
1203 double apply_percent_exit_qty_step(double requested_qty,
1204 double available_qty) const {
1205 double gridded = apply_exit_qty_step(requested_qty);
1206 if (qty_step_ >= 1.0
1207 && requested_qty > 0.0
1208 && requested_qty < qty_step_
1209 && available_qty >= qty_step_) {
1210 return qty_step_;
1211 }
1212 return gridded;
1213 }
1214
1215 // The broker's sizing arithmetic runs ON-TICK. Both the price the budget
1216 // is divided by and the price the open position is marked at for the
1217 // equity term are round_to_mintick() of their raw inputs; the FEED itself
1218 // stays raw (ta.*, crossovers, plots and every strategy.* metric TV
1219 // reports on raw values keep reading current_bar_.close directly — only
1220 // this sizing snapshot and the fill are quantized). The evidence is the
1221 // same tape family that pinned the signal-bar freeze below:
1222 //
1223 // - taro-s-c-c-ma-simplified-2-color replayed over the NYSE:F and
1224 // NASDAQ:AAPL tapes: qty = floor(E / tick(close_S)) with E marked at
1225 // tick(close_S) reproduces 674/674 F and 832/832 AAPL reversals; the
1226 // raw-close divisor fits only 476/674 on F. The same replay matches
1227 // 675/675 TV entries by RAW-close crossovers, which is why the signal
1228 // path is left alone.
1229 // - drgunjan-F trade 1: a 9.565 signal close, TV qty 10460 =
1230 // floor(100000 / 9.56) (9.565 / 0.01 = 956.49999... rounds DOWN under
1231 // the census formula, exactly as 228.765 does); the raw divisor gave
1232 // floor(100000 / 9.565) = 10454.
1233 // - The raw basis also DECLINED entries TV filled: when an x.xx5 close
1234 // rounds UP at the fill (bar_fill_price) while the quantity was
1235 // floored against the raw close, qty * fill exceeds the sizing
1236 // equity by ~qty * mintick/2 and the true-flat gap-reject / reversal
1237 // float-guard arms in apply_filled_order_to_state saw a phantom gap.
1238 // 463/463 missing taro-F entries were predicted by that mechanism
1239 // with 0 counterexamples; 26/26 drgunjan-F and 6/6 mazi-F missing
1240 // entries had sub-penny signal closes.
1241 //
1242 // On a price that is already n*tick (bar_fill_price output, a
1243 // directionally-snapped level, or the slipped sizing price
1244 // frozen_sizing_price builds) round_to_mintick is identical to within
1245 // ONE ULP — not an identity: double(tick) is inexact for every decimal
1246 // tick, so floor(x/0.01 + 0.5)*0.01 != x for 6,951 of 49,900 decimal-
1247 // parsed 2dp prices 1.00..500.00 (always +1 ulp, double(0.01) > 0.01),
1248 // for 35,736 of 70,000 5dp prices at tick 1e-5, and for 0 of 12,000 at
1249 // the binary-exact 0.25. The ulp is absorbed by apply_qty_step's 1e-6
1250 // nudge (0 floor flips of floor(100000/x) across all 49,900 prices) and
1251 // by the 1e-9 / 1e-12 float guards on every admission arm, so no
1252 // quantity or verdict moves on an on-tick feed: the fill-time legacy
1253 // callers and the frozen path reproduce the pre-fix numbers there, and
1254 // only sub-tick prints move — to TV's number.
1255 //
1256 // QtyType::CASH follows percent_of_equity by construction (the same
1257 // broker division, only the numerator differs) and is UNPINNED: every
1258 // census above is default percent_of_equity sizing, and no strategy.cash
1259 // tape discriminating round(close) from close has been replayed. A
1260 // future CASH mismatch on a sub-tick feed is traced here first.
1261
1262 // TradingView freezes DEFAULT (qty=na) market-order sizing at the SIGNAL
1263 // bar — the bar whose on_bar issued the strategy.entry/strategy.order
1264 // call — not at the fill:
1265 //
1266 // tick(x) = round_to_mintick(x) // nearest tick, census form
1267 // equity_S = initial_capital + realized net profit
1268 // + open_profit(tick(close(S))) // position may still be OPEN
1269 // sizing_price = tick(close(S)) + slippage*mintick*(+1 buy / -1 sell)
1270 // qty = floor_step( commission_reserved(budget)
1271 // / fx / (sizing_price * pointvalue) )
1272 // // commission_reserved divides by (1 + commRate) for a
1273 // // PERCENT commission and is the identity otherwise;
1274 // // the adapter owns it (docs/pine-adapter-kernel-notes.md)
1275 //
1276 // The market order then fills at the NEXT bar's open carrying this frozen
1277 // quantity. calc_qty(price) implements exactly that shape when evaluated
1278 // AT SIGNAL TIME (current_bar_ IS the signal bar: open_profit marks at
1279 // tick(close(S)) and the divisor is the rounded argument), so the freeze
1280 // is simply calc_qty(slipped rounded signal close) captured at
1281 // placement. Evaluating the same expression at FILL time — the
1282 // pre-freeze behavior — was wrong in
1283 // three separable ways on a reversal/gap: it double-counted the just-
1284 // closed position's PnL (current_equity() already realized the exit while
1285 // position_* still held the stale lot for open_profit), it marked open
1286 // profit at the FILL bar's close (a look-ahead: that close is unknown
1287 // when the order fills at the open), and it divided by the fill price
1288 // instead of the signal close. Freezing at placement removes all three.
1289 //
1290 // Only PERCENT_OF_EQUITY / CASH default sizing is price/equity-dependent;
1291 // FIXED default sizing stays qty=NaN at placement (identical value at
1292 // fill, and keeping NaN preserves the isnan(order.qty)-keyed semantics
1293 // elsewhere, e.g. the OCA "fully filled" heuristic).
1294 //
1295 // Priced (limit/stop) entries are NOT frozen: TV's sizing basis for an
1296 // order armed one or more bars before its fill is not empirically
1297 // established, so they conservatively keep the legacy fill-time sizing.
1298 // The sizing price of the frozen rule above, exposed separately so the
1299 // placement sites can retain it as an adapter placement fact (`sizing_price`)
1300 // for the fill-time margin-admission re-check.
1301 //
1302 // The basis is the mintick-ROUNDED signal close. Rounding happens BEFORE
1303 // the slippage ticks are added so the result is n*tick for any feed
1304 // print, exactly as a bar_fill_price fill carries its slippage: TV's
1305 // broker never sees the sub-tick close Pine sees (674/674 F, 832/832
1306 // AAPL reversals on the taro tapes fit tick(close_S); the raw close fits
1307 // 476/674 — see calc_qty). A feed that is already on-tick is unaffected
1308 // in every quantity and verdict, though not bit-for-bit: round_to_mintick
1309 // returns the n*tick double to within one ulp (double(tick) is inexact
1310 // for decimal ticks — the measurement is in calc_qty's comment), and
1311 // that ulp is absorbed by apply_qty_step's 1e-6 nudge and by the
1312 // admission arms' float guards.
1313
1314
1315 // round 7 (family M, JOAT BTC@1D; campaign note log-20260905t121513z-
1316 // 50167cb8, CORRECTING the m1d-coof-ctx pin): a DEFAULT-sized
1317 // (percent_of_equity / cash) MARKET order that a calc_on_order_fills
1318 // FILL RECALC places is sized by TradingView at ITS OWN FILL, not at the
1319 // signal bar's close and not at the recalc's cursor:
1320 //
1321 // lab tv scratchpad/pins/m1d-coof-size-btc (BINANCE:BTCUSDT 1D,
1322 // 2025-10-01..12-31, tv-tape-m1d-coof-size-btc-7ee8712b): "B", born in
1323 // the SECOND recalc at the 10-02 open and filled at W1 = the low
1324 // 118279.31, has qty 845.4564 = 10% x 1e9 / 118279.31 (cursor O
1325 // 118594.99 -> 843.2; the bar's close 120529.35 -> 829.7); thirteen
1326 // entries born in a first-O recalc and filled at O size at O, never at
1327 // the finals close. The probe itself: TV 4 0.09245 = 9802.56 /
1328 // (106011.13 x 1.0001) at the 11-11 open fill (the engine froze 0.0951
1329 // at the 11-11 close 103058.99); TV 10 0.14674 at its W2 fill 69988.83
1330 // (cursor W1 63913.27 -> 0.16069, close 67988.04 -> 0.15106).
1331 //
1332 // The script context of such a recalc is unchanged — TradingView runs it
1333 // on the CURRENT bar's finals (scratchpad/pins/m1d-coof-ctx2-{btc,f}:
1334 // 103/103 encoded firings read bar k's high/low/close/volume, bar_index k,
1335 // barstate.isconfirmed true, close[1] = bar k-1) — exactly what
1336 // execute_coof_script_body presents. Only the SIZING moment differs from
1337 // an ordinary close-calc placement: the placement freeze above is skipped
1338 // for recalc-born default market orders (strategy.entry MARKET and
1339 // strategy.order RAW alike), frozen_default_qty stays NaN, and the fill
1340 // kernels size with calc_qty(slipped fill) — open lots marked at the fill
1341 // (current_bar_ is the scheduler's point bar there). No KI-54 / gap-reject
1342 // / gross-admission snapshot is taken for them: those gates are pinned on
1343 // close-calc placements (their frozen invariant qty * sizing_price <=
1344 // sizing_equity has no meaning at a fill-time size); the zero-lot decline
1345 // and the affordability gate read the fill-time quantity. FIXED default
1346 // sizing and explicit quantities are untouched (never frozen); priced
1347 // (stop/limit) entries keep their own paths. Ordinary close executions
1348 // (coof_fill_recalc_active_ false) freeze exactly as before, so a script
1349 // without calc_on_order_fills is byte-identical.
1350
1351 // KI-54 defect fix: the frozen sizing snapshot must see POST-liquidation
1352 // equity. TradingView liquidates intrabar, BEFORE the bar-close script
1353 // body runs; the engine's process_margin_call runs at the END of
1354 // dispatch_bar, AFTER on_bar placed (and froze) this bar's default-sized
1355 // market orders. When a margin call fires on the placement bar, the
1356 // frozen qty was computed on pre-liquidation equity — over-sized, so the
1357 // next bar's fill opens a position whose notional exceeds equity and the
1358 // long_full_margin branch of process_margin_call then emits a phantom
1359 // LONG margin call TV does not have. Rather than moving process_margin_call
1360 // (which would change what strategy.equity reads inside on_bar for every
1361 // strategy), the dispatch loop calls this refresh right after a margin
1362 // call actually liquidated something: every still-pending frozen
1363 // default-sized market order placed on THIS bar is re-frozen on the
1364 // post-liquidation state. Strict no-op on bars without a margin call
1365 // (the caller checks), and bit-identical recompute for untouched state.
1366
1367 // --- Strategy variable accessors ---
1368
1369
1370 // KI-64: freeze the pre-close position for the script-visible position
1371 // accessor before an ordinary POOC strategy.close/close_all fills in-line
1372 // this bar. Capture-once per on_bar (a second same-bar close keeps the
1373 // FIRST pre-close snapshot). Caller guards close-timing mode &&
1374 // !immediately; this reads position_side_/position_qty_ while they still
1375 // hold the pre-close values (execute_immediate_close has not run yet).
1376
1377 // KI-64: release the freeze so the next script-visible read returns the real
1378 // (post-close) position. Called at the top of flush_same_bar_close(), i.e.
1379 // immediately after every POOC on_bar returns.
1380
1381
1382 double net_profit() const { return net_profit_sum_; }
1383 double gross_profit() const { return gross_profit_sum_; }
1384 double gross_loss() const { return gross_loss_sum_; }
1386
1387 double max_runup_percent() const {
1388 return (initial_capital_ > 0.0) ? (max_runup_ / initial_capital_) * 100.0 : 0.0;
1389 }
1390 double grossprofit_percent() const {
1391 return (initial_capital_ > 0.0) ? (gross_profit_sum_ / initial_capital_) * 100.0 : 0.0;
1392 }
1393 double grossloss_percent() const {
1394 return (initial_capital_ > 0.0) ? (gross_loss_sum_ / initial_capital_) * 100.0 : 0.0;
1395 }
1396 double avg_trade() const {
1397 int n = (int)trades_.size();
1398 return (n > 0) ? (net_profit_sum_ / (double)n) : 0.0;
1399 }
1400 double avg_trade_percent() const {
1401 int n = (int)trades_.size();
1402 if (n <= 0) return 0.0;
1403 double s = 0.0;
1404 for (const auto& t : trades_) s += t.pnl_pct;
1405 return s / (double)n;
1406 }
1407 double avg_winning_trade() const {
1408 return (win_trades_count_ > 0) ? (gross_profit_sum_ / (double)win_trades_count_) : 0.0;
1409 }
1410 double avg_losing_trade() const {
1411 return (loss_trades_count_ > 0) ? (gross_loss_sum_ / (double)loss_trades_count_) : 0.0;
1412 }
1414 if (win_trades_count_ <= 0) return 0.0;
1415 double s = 0.0;
1416 int c = 0;
1417 for (const auto& t : trades_) {
1418 if (t.pnl > 0.0) { s += t.pnl_pct; ++c; }
1419 }
1420 return (c > 0) ? (s / (double)c) : 0.0;
1421 }
1423 if (loss_trades_count_ <= 0) return 0.0;
1424 double s = 0.0;
1425 int c = 0;
1426 for (const auto& t : trades_) {
1427 if (t.pnl < 0.0) { s += t.pnl_pct; ++c; }
1428 }
1429 return (c > 0) ? (s / (double)c) : 0.0;
1430 }
1432 if (position_side_ == PositionSide::FLAT) return 0.0;
1433 return std::abs(position_qty_ * position_entry_price_) * syminfo_.pointvalue;
1434 }
1435
1436 // Mark-to-market open profit in account currency. The point-value
1437 // multiplier keeps this consistent with realized PnL (emit_close_trade)
1438 // so equity = capital + net_profit + open_profit stays in one unit.
1439 double open_profit(double current_price) const {
1440 if (position_side_ == PositionSide::FLAT) return 0.0;
1441 double diff = (position_side_ == PositionSide::LONG)
1442 ? (current_price - position_entry_price_)
1443 : (position_entry_price_ - current_price);
1444 // Account-currency, matching emit_close_trade / open_trade_profit —
1445 // callers combine this with initial_capital_ + net_profit_sum_ (both
1446 // account-currency) to get total equity. fx=1.0 is a no-op.
1447 return diff * position_qty_ * syminfo_.pointvalue * active_account_currency_fx();
1448 }
1449
1450 int count_wintrades() const { return win_trades_count_; }
1451 int count_losstrades() const { return loss_trades_count_; }
1452
1453 // --- Time/date extraction from bar timestamp ---
1454 // Pine's bare ``hour`` / ``minute`` / ``dayofweek`` (the variable form,
1455 // not the 1-arg function form) returns the wall-clock for the **exchange
1456 // timezone** of the symbol (per TV reference docs). For crypto symbols
1457 // like ETH-USDT the exchange TZ is UTC, which matches the engine's
1458 // storage TZ — so the cheap ``gmtime_r`` path is correct for the
1459 // overwhelming majority of strategies in the corpus.
1460 //
1461 // The 1-arg function form ``hour(time)`` is handled separately by the
1462 // codegen (see codegen/visit_call.py) and DOES honour
1463 // ``syminfo_.timezone`` (set via ``strategy_set_chart_timezone``) since
1464 // TV's reference says the function form defaults its tz arg to
1465 // ``syminfo.timezone``, which TV harnesses commonly set to the chart's
1466 // display TZ for cross-exchange / multi-zone work.
1470
1471 // Single-entry memo: generated scripts commonly read several time
1472 // components per bar (hour + minute + dayofweek); decompose once per
1473 // distinct bar timestamp instead of per accessor. Keyed on the raw
1474 // timestamp, so no per-run invalidation is needed (same ts -> same
1475 // UTC decomposition, run-independent).
1476 mutable int64_t bar_time_memo_ts_ = std::numeric_limits<int64_t>::min();
1478
1480 if (current_bar_.timestamp == bar_time_memo_ts_) return bar_time_memo_;
1481 time_t secs = (time_t)(current_bar_.timestamp / 1000);
1482 struct tm tm_buf;
1483 gmtime_r(&secs, &tm_buf);
1484 BarTime bt;
1485 bt.year = tm_buf.tm_year + 1900;
1486 bt.month = tm_buf.tm_mon + 1;
1487 bt.dayofmonth = tm_buf.tm_mday;
1488 bt.hour = tm_buf.tm_hour;
1489 bt.minute = tm_buf.tm_min;
1490 bt.second = tm_buf.tm_sec;
1491 bt.dayofweek = tm_buf.tm_wday + 1;
1492 bt.weekofyear = (tm_buf.tm_yday + 7 - ((tm_buf.tm_wday + 6) % 7)) / 7;
1493 bar_time_memo_ts_ = current_bar_.timestamp;
1494 bar_time_memo_ = bt;
1495 return bt;
1496 }
1497
1498 // Chart-timezone-aware decomposition for the existing loss-day clocks
1499 // and the continuous/unconfigured-session order-counter fallback. The
1500 // order counter on an explicitly timed session instead consumes
1501 // source-layer intraday-cap risk-day policy, which follows the symbol's trading day.
1502 //
1503 // Falls back to plain ``_decompose_bar_time()`` (UTC) when no chart
1504 // timezone has been set, preserving the legacy fast path for
1505 // engine consumers that don't call ``set_chart_timezone``.
1506 //
1507 // Defined out-of-line in src/engine_risk.cpp so we can use the
1508 // private ``ScopedTimezone`` helper without leaking its header into
1509 // the public engine.hpp surface.
1510 int _bar_hour() const { return _decompose_bar_time().hour; }
1511 int _bar_minute() const { return _decompose_bar_time().minute; }
1512 int _bar_second() const { return _decompose_bar_time().second; }
1515 int _bar_month() const { return _decompose_bar_time().month; }
1516 int _bar_year() const { return _decompose_bar_time().year; }
1518
1519 // --- Bar magnifier state ---
1521 bool barstate_islast_ = false;
1522 // Independent from barstate.isnew. False only when a COOF execution
1523 // restores a completed ordinary-close checkpoint that already contains
1524 // the current bar's one committed history slot.
1527 // When true, run_magnified_bar scales per-sub-bar sample count by
1528 // (sub_bar.volume / mean_sub_bar_volume) within each script bar — dense
1529 // tick approximation on high-volume sub-bars without real tick data.
1531
1532 // KI-60 scheduler transients. Script executions see the complete
1533 // historical bar, while direct POOC/immediate market closes use the
1534 // monotonic broker cursor price held here.
1535 // finding-446: true when coof_cursor_price_ is a RAW OHLC path point /
1536 // magnifier tick (a broker-price fill there is nearest-tick rounded via
1537 // bar_fill_price); false when it is a resolved fill price (a bar-point
1538 // fill is already rounded, a level fill keeps its directional snap).
1539 // KI-67: true only while the active fill recalc owns the FIRST fill event
1540 // at the bar-open tick (O). Orders placed while this holds keep STANDARD
1541 // exact-level semantics. Later fills at that same O, like fills at every
1542 // other path point, are MID-BAR cascades (the Pine historical cascade permission).
1543 // True only while executing a fill recalc triggered by a later fill event
1544 // at O, after the first O fill has already consumed bar-open provenance.
1545 // Such a recalc is mid-bar for KI-67 and resumes on leg 0 (O->W1). This bit
1546 // lets strategy.exit apply the one pinned exception: a marketable LIMIT may
1547 // resume at W1, while marketable STOP suppression remains whole-entry-bar.
1548 // Round15: identify the MARKET opening whose first callback is active.
1549 // A direct close/partial/reentry in that body changes the serial and must
1550 // not inherit the original fill's permission to arm a recrossing limit.
1551 // KI-67: true only during a point-bar evaluation that sits AT an extreme
1552 // waypoint (W1 or W2) of the historical 4-tick path. Cascade orders born
1553 // this bar may fill only while this holds; on segments, at O, at C, and on
1554 // the ordinary-close / POOC-C / margin passes it is false so cascade orders
1555 // are held (they convert to ordinary resting orders at bar end). Set only by
1556 // the historical dispatch; the magnifier path never sets it.
1557 // KI-67 exit cascade: the historical dispatch publishes its current path
1558 // position here for the strategy.exit cascade gate. coof_hist_is_segment_
1559 // marks a segment (vs point) evaluation; coof_hist_path_index_ is the LEG
1560 // index (0..2) on a segment, or the path WAYPOINT index (0..3, cursor =
1561 // path[index]) on a point. Meaningful only while coof_scheduler_active_ on
1562 // the non-magnifier historical path; the POOC-C / margin passes publish the
1563 // C waypoint (index 3) so cascade exits are held there.
1564 // KI-67 exit cascade: the in-flight leg index (0..2) the CURRENT fill recalc
1565 // was triggered on — the leg the dispatch cursor traverses next after the
1566 // triggering fill. Published by the loop right before each recalc so a
1567 // strategy.exit placed in that recalc records its seg_i from the loop's real
1568 // position ("a fill AT a waypoint starts the NEXT leg"), rather than
1569 // re-deriving it from the fill price (ambiguous exactly at waypoints). -1 (or
1570 // >=3) outside a mid-bar historical recalc / at the terminal C tick.
1571 // KI-67 exit cascade: set by the gate immediately before evaluate_fill_price
1572 // so the exit fill evaluation runs its open-gap shortcut on the in-flight
1573 // leg-end waypoint POINT even when is_entry_bar (entry + exit share a bar).
1574 // Reset right after that evaluation; never set on the magnifier path.
1575 // Direct strategy.close / POOC fills can occur inside on_bar rather than
1576 // through process_next_pending_order. The scheduler refreshes this budget
1577 // before every speculative execution so those fills consume the same
1578 // finite historical/magnifier event budget as every other broker fill.
1579 // @broker-state begin
1580 // Monotonic cross-bar fill sequence counter; compared against
1581 // trail_best_before_bar_fill_seq_ (hashed above) and against
1582 // adapter placement fact `signal_close_mc_fill_seq` by fill-time gates
1583 // that cross the bar boundary.
1585 // @broker-state end
1586
1587 // --- Session predicate bar-state tracking ---
1588 // Tracks whether the previous bar was inside the regular session.
1589 // Used to compute session.isfirstbar (in_session && !prev_in_session_)
1590 // and session.islastbar (prev_in_session_ && !in_session).
1591 bool prev_in_session_ = false;
1592 // Current-bar session predicates — recomputed at the start of each bar
1593 // by set_session_bar_state() (engine_run.cpp) on every bar pump.
1594 bool session_ismarket_ = false;
1597
1598 // session.ismarket of the CHART bar stamped bar_ms on the symbol's
1599 // session clock — the chart-timeframe-aware rule of session_time.hpp:
1600 // every bar of a daily-or-higher chart is the regular-session bar,
1601 // intraday bars keep the time-of-day test.
1602 bool chart_bar_ismarket(int64_t bar_ms) const;
1603 // Set the three per-bar predicates for the chart bar being dispatched.
1604 // in_session is chart_bar_ismarket(that bar); intraday_islastbar is the
1605 // pump's own lookahead verdict for an intraday chart (peek at the next
1606 // bar, barstate.islast, or never in magnifier mode). On a D/W/M chart
1607 // the bar IS the whole session — its own first and last bar — so
1608 // session.isfirstbar and session.islastbar both equal in_session there
1609 // and the prev/next bookkeeping does not apply.
1610
1611
1612 // --- Timeframe state ---
1613 std::string input_tf_;
1614 std::string script_tf_;
1615 // Cached tf_to_seconds(script_tf_). MUST be refreshed immediately after
1616 // every assignment to script_tf_ (both sites live in engine_run.cpp).
1617 // Avoids a string parse per strategy.* call.
1621
1622 // --- Historical -> realtime stream lifecycle ---
1623 // stream_begin() executes the historical warmup through the normal run()
1624 // path exactly once, then these fields carry the SAME broker, Pine series,
1625 // TA and timeframe-aggregator state forward while normalized trades arrive.
1626 enum class StreamPhase { IDLE, REALTIME, ENDED };
1629 // A host that keeps the legacy no-op refusal for in-run C-ABI setters
1630 // sets this for the handle lifetime so
1631 // guard_native_mutation stays a no-op (ab9714be LegacyCompatibilityConsumer::refuse).
1632 // Native hosts leave it false; their in-run setter still throws.
1636 int64_t stream_clock_ms_ = 0;
1651 std::vector<StreamOrderAction> stream_order_actions_;
1652
1653 // --- request.security state ---
1655 int sec_id = 0;
1656 std::string tf;
1660 int64_t feed_count = 0;
1663 // Requested-context bar index of the latest dispatch_security_eval()
1664 // (the ring address its TA members saw, see ta::bar_context()); -1
1665 // before the first dispatch. A host that replays its latest
1666 // evaluation re-dispatches the same bar under the same index.
1667 int64_t ta_bar_index = -1;
1668 // Native higher-timeframe feed routing, rebuilt per run by
1669 // prepare_native_security_feeds(): the index into
1670 // native_security_feeds_ serving this state's requested timeframe
1671 // (-1: none, the aggregate stands), and that feed's bars keyed by
1672 // the label this state's aggregator stamps on the same bucket --
1673 // TimeframeAggregator::bar_label_ms of the native bar's covered
1674 // session instant -- so a completed bucket finds its exchange bar by
1675 // the timestamp the aggregate already carries. The same call installs
1676 // the feed's stamps into this state's aggregator as its period
1677 // partition (TimeframeAggregator::set_native_periods): the bucket's
1678 // span, label and completion bar are the native bar's own.
1680 std::unordered_map<int64_t, Bar> native_bars_by_label;
1681 };
1682
1683 std::vector<SecurityEvalState> security_eval_states_;
1684 // The raw feed used by security aggregators in the active run. This is the
1685 // chart input TF on the legacy path and the auxiliary TF on the split path.
1687
1688 // Native higher-timeframe request.security feeds: the exchange's own
1689 // bars of one timeframe each (TradingView's "D" bar on an intraday chart
1690 // is the settlement / official close, not the last intraday close). A
1691 // completed request.security bucket of a fed timeframe takes the native
1692 // bar's OHLCV; timing, other timeframes, chart and broker are untouched.
1693 // Layout-unconditional like the aux feed's siblings above.
1695 std::string tf;
1696 int seconds = 0;
1697 std::vector<Bar> bars;
1698 };
1699 std::vector<NativeSecurityFeed> native_security_feeds_;
1702 // The chart symbol's own daily partition, built per run from the native
1703 // "D" feed's stamps on an intraday chart (prepare_chart_day_partition)
1704 // and installed for the run's bar loop (NativeDayPartitionScope) so the
1705 // chart-level D consumers -- time("D") / time_close("D"),
1706 // timeframe.change("1D"), ta.change(time("D")), ta.vwap's daily anchor --
1707 // read TradingView's trade-date daily bars (timeframe.hpp). Empty on a
1708 // run without the feed or on a calendar chart: every rule nominal.
1710
1711 // --- Runtime trace state ---
1712 // Gated by ``trace_enabled_`` (default false) so production strategies
1713 // pay zero cost; the validator flips it on per-strategy when it needs
1714 // engine-internal per-bar values for TV cross-referencing.
1715 //
1716 // ``trace_buffer_`` is a flat vector of POD records (no string allocs
1717 // per call); each entry holds a ``name_id`` indexing into
1718 // ``trace_names_``, which is the unique-name table built by
1719 // ``intern_trace_name``. A first-time name pays one ``unordered_map``
1720 // insert + ``string`` push_back; subsequent calls with the same name
1721 // are a single map lookup.
1722 bool trace_enabled_ = false;
1723
1724 // Live-runtime surface (ABI v4). All default off/zero; historical runs are
1725 // byte-identical when untouched (tests/test_live_flags_off_identity.cpp).
1726 int last_run_status_ = 0; // 0 completed, 1 NOT_COMPLETED (abort)
1727
1728 // Cooperative abort (spec §3.5): set from any thread via request_abort();
1729 // consumed by the run in progress at the top of each bar-loop iteration
1730 // via check_abort(), which unwinds the run with AbortRequested. Cleared
1731 // at every run() entry, so a request made while idle is a no-op.
1732 //
1733 // AbortFlag wraps std::atomic<bool> in a copy/move-constructible shell:
1734 // std::atomic itself has its copy/move members deleted, and some tests
1735 // (e.g. tests/test_pooc_global_full_exit.cpp's ``run_case``) return a
1736 // BacktestEngine subclass by value, which needs the class to stay
1737 // implicitly copyable. Copying/moving never carries an in-flight abort
1738 // request across — there is no "run in progress" on a copy — so the
1739 // copy always starts cleared.
1740 struct AbortFlag {
1741 std::atomic<bool> value{false};
1742 AbortFlag() = default;
1743 AbortFlag(const AbortFlag&) noexcept {}
1744 AbortFlag(AbortFlag&&) noexcept {}
1745 AbortFlag& operator=(const AbortFlag&) noexcept {
1746 value.store(false, std::memory_order_relaxed);
1747 return *this;
1748 }
1750 value.store(false, std::memory_order_relaxed);
1751 return *this;
1752 }
1753 bool load(std::memory_order order) const { return value.load(order); }
1754 void store(bool v, std::memory_order order) { value.store(v, order); }
1755 };
1757 struct AbortRequested {}; // thrown inside the bar loops only
1759 if (abort_requested_.load(std::memory_order_relaxed)) throw AbortRequested{};
1760 }
1761
1762 std::vector<TraceEntryC> trace_buffer_;
1763 std::vector<std::string> trace_names_;
1764 std::unordered_map<std::string, int32_t> trace_name_index_;
1765
1766 int32_t intern_trace_name(const std::string& name);
1767
1774
1775 // Captured by the public run() wrappers when the underlying engine logic
1776 // throws. Cleared at the start of every run(). Surfaces through
1777 // last_error() / pf_strategy_get_last_error() so the C ABI never
1778 // unwinds a C++ exception across the extern "C" boundary.
1779 std::string last_error_;
1780
1781 // Registers one aggregating evaluator: the requested timeframe's buckets
1782 // built from the run's evaluator input timeframe, passthrough when the
1783 // two are equal. Publication modes are not the kernel's: a source host
1784 // keeps its own per-sec_id semantics beside this state and a native
1785 // subscription's modes are the consumer's delivery rules.
1786 void register_security_eval(int sec_id, const std::string& requested_tf,
1787 const std::string& input_tf);
1788 // The one path to evaluate_security(): installs the requested context's
1789 // bar index for the evaluator's TA members (ta::bar_context()) for the
1790 // duration of the dispatch. `bar_index` is the 0-based index of the
1791 // requested-context bar being evaluated — the just-completed bucket for a
1792 // complete evaluation (eval_complete_count - 1), the in-progress bucket
1793 // for a partial one (eval_complete_count) — so every
1794 // compute()/recompute() dispatch of one requested bar rewrites the same
1795 // ring slot, and a conditional window call inside the security expression
1796 // is addressed exactly like TradingView addresses it.
1798 bool publish, int64_t bar_index);
1799 // The generic evaluator step for one input bar: aggregate, take the
1800 // native bar of a completed bucket where a feed serves the timeframe,
1801 // and dispatch the completed bucket. Publication at the last contributing
1802 // input is the consumer's; a source host composes its own step.
1803 void feed_security_eval_state(SecurityEvalState& state, const Bar& input_bar);
1804
1805 // A new batch run (including stream_begin's historical warmup) starts a
1806 // fresh script lifecycle. Called once after broker reset, before any
1807 // requested/chart computation or per-bar checkpoint. Generated subclasses
1808 // reset their own persistent state here, then optionally prepare the
1809 // current run's static TA cache. Config/inputs/feeds belong to the engine
1810 // and survive. Streaming ticks and fill recalculations never call this.
1811 virtual void prepare_script_run(const Bar*, int, bool) {}
1812
1814 virtual void evaluate_security(int sec_id, const Bar& bar, bool is_complete) {}
1815 virtual void clear_security(int sec_id) {}
1816
1817 // Generated-state transaction hooks for calc_on_order_fills. Snapshot is
1818 // called once before the broker walks a historical bar; restore precedes
1819 // every fill recalc and the ordinary close execution. The completed
1820 // ordinary-close execution becomes the committed checkpoint. Historical
1821 // post-C fill recalculations start from it, recompute its current-bar
1822 // history slot, and are rolled back after their broker effects persist.
1823 virtual void snapshot_script_state() {}
1824 virtual void restore_script_state() {}
1825 virtual void commit_script_state() {}
1826
1827 // Magnifier helpers
1828
1829
1830 virtual void finalize_bar() {}
1831
1832 // --- Equity extremes update (called after each on_bar) ---
1833 // NOTE: the dd/runup walk in src/engine_metrics.cpp (compute_equity_stats)
1834 // MUST mirror this trough-reset logic; keep in lockstep. The fold is
1835 // exactly one per script bar and always paired with record_equity_point,
1836 // so the curve holds the very values folded here and a re-walk of the
1837 // curve through fold_equity_extreme reproduces the scalars bit for bit
1838 // — record_range_end_close_trades (engine_orders.cpp) relies on that
1839 // when it re-marks the last point.
1840 void fold_equity_extreme(double eq) {
1841 if (eq > max_equity_) {
1842 max_equity_ = eq;
1843 min_equity_ = eq; // reset trough on new peak
1844 }
1845 if (eq < min_equity_) {
1846 min_equity_ = eq;
1847 }
1848 double dd = max_equity_ - eq;
1849 if (dd > max_drawdown_) max_drawdown_ = dd;
1850 double ru = eq - min_equity_;
1851 if (ru > max_runup_) max_runup_ = ru;
1852 }
1855
1856 // --- Update max_contracts_held_* running peaks ---
1857 double abs_qty = std::abs(position_qty_);
1859 if (abs_qty > max_contracts_held_all_) max_contracts_held_all_ = abs_qty;
1861 max_contracts_held_long_ = abs_qty;
1863 max_contracts_held_short_ = abs_qty;
1864 }
1865 }
1866
1867 // Record one equity point per SCRIPT bar. ``script_bar_ts`` must be the
1868 // script-bar open timestamp captured BEFORE dispatch — current_bar_.timestamp
1869 // is overwritten by the magnifier sub-bar walk (engine_run.cpp), which would
1870 // make the curve differ between magnifier on/off.
1871 void record_equity_point(int64_t script_bar_ts) {
1872 if (equity_curve_.empty()) first_bar_open_ = current_bar_.open;
1874 p.time_ms = script_bar_ts;
1877 equity_curve_.push_back(p);
1879 }
1880
1881 // --- Trade history accessors (for strategy.closedtrades.*) ---
1882 double closed_trade_profit(int index) const {
1883 if (index >= 0 && index < (int)trades_.size())
1884 return trades_[index].pnl;
1885 return 0.0;
1886 }
1887 double closed_trade_profit_percent(int idx) const {
1888 if (idx < 0 || idx >= (int)trades_.size()) return std::numeric_limits<double>::quiet_NaN();
1889 return trades_[idx].pnl_pct;
1890 }
1891 double closed_trade_commission(int idx) const {
1892 if (idx < 0 || idx >= (int)trades_.size()) return std::numeric_limits<double>::quiet_NaN();
1893 return trades_[idx].commission;
1894 }
1895 int closed_trade_entry_bar_index(int idx) const {
1896 if (idx < 0 || idx >= (int)trades_.size()) return na<int>();
1897 return trades_[idx].entry_bar_index;
1898 }
1899 int closed_trade_exit_bar_index(int idx) const {
1900 if (idx < 0 || idx >= (int)trades_.size()) return na<int>();
1901 return trades_[idx].exit_bar_index;
1902 }
1903 std::string closed_trade_entry_comment(int idx) const {
1904 if (idx < 0 || idx >= (int)trades_.size()) return std::string();
1905 return trades_[idx].entry_comment;
1906 }
1907 std::string closed_trade_exit_comment(int idx) const {
1908 if (idx < 0 || idx >= (int)trades_.size()) return std::string();
1909 return trades_[idx].exit_comment;
1910 }
1911 std::string closed_trade_entry_id(int idx) const {
1912 if (idx < 0 || idx >= (int)trades_.size()) return std::string();
1913 return trades_[idx].entry_id;
1914 }
1915 std::string closed_trade_exit_id(int idx) const {
1916 if (idx < 0 || idx >= (int)trades_.size()) return std::string();
1917 return trades_[idx].exit_id;
1918 }
1919 uint64_t closed_trade_entry_incarnation(int idx) const {
1920 if (idx < 0 || idx >= (int)trades_.size()) return 0;
1921 return trades_[idx].entry_incarnation;
1922 }
1923 double closed_trade_entry_price(int idx) const {
1924 if (idx < 0 || idx >= (int)trades_.size()) return std::numeric_limits<double>::quiet_NaN();
1925 return trades_[idx].entry_price;
1926 }
1927 double closed_trade_exit_price(int idx) const {
1928 if (idx < 0 || idx >= (int)trades_.size()) return std::numeric_limits<double>::quiet_NaN();
1929 return trades_[idx].exit_price;
1930 }
1931 int64_t closed_trade_entry_time(int idx) const {
1932 if (idx < 0 || idx >= (int)trades_.size()) return 0;
1933 return trades_[idx].entry_time;
1934 }
1935 int64_t closed_trade_exit_time(int idx) const {
1936 if (idx < 0 || idx >= (int)trades_.size()) return 0;
1937 return trades_[idx].exit_time;
1938 }
1939 double closed_trade_size(int idx) const {
1940 if (idx < 0 || idx >= (int)trades_.size()) return std::numeric_limits<double>::quiet_NaN();
1941 return trades_[idx].qty;
1942 }
1943 double closed_trade_max_runup(int idx) const {
1944 if (idx < 0 || idx >= (int)trades_.size()) return 0.0;
1945 return trades_[idx].max_runup;
1946 }
1947 // Percent excursions: trade.max_runup / max_drawdown are stored in
1948 // account currency (× pointvalue, see emit_close_trade), so the entry
1949 // cost denominator must be in currency too (entry × qty × pointvalue).
1950 // pointvalue=1 cancels out and matches the legacy ratio bit-for-bit.
1951 double closed_trade_max_runup_percent(int idx) const {
1952 if (idx < 0 || idx >= (int)trades_.size()) return 0.0;
1953 const Trade& t = trades_[idx];
1954 double cost = t.entry_price * t.qty * syminfo_.pointvalue;
1955 return (cost > 0.0) ? (t.max_runup / cost) * 100.0 : 0.0;
1956 }
1957 double closed_trade_max_drawdown(int idx) const {
1958 if (idx < 0 || idx >= (int)trades_.size()) return 0.0;
1959 return trades_[idx].max_drawdown;
1960 }
1962 if (idx < 0 || idx >= (int)trades_.size()) return 0.0;
1963 const Trade& t = trades_[idx];
1964 double cost = t.entry_price * t.qty * syminfo_.pointvalue;
1965 return (cost > 0.0) ? (t.max_drawdown / cost) * 100.0 : 0.0;
1966 }
1967
1968 // --- Direction accessors ---
1969 std::string closed_trade_direction(int idx) const {
1970 if (idx < 0 || idx >= (int)trades_.size()) return "";
1971 return trades_[idx].is_long ? "long" : "short";
1972 }
1973 std::string open_trade_direction(int idx) const {
1974 if (position_side_ == PositionSide::FLAT || idx < 0 || idx >= (int)pyramid_entries_.size()) return "";
1975 return (position_side_ == PositionSide::LONG) ? "long" : "short";
1976 }
1977
1978 // --- Open position trade accessors (strategy.opentrades.*) ---
1979 double open_trade_profit(int idx) const;
1980 double open_trade_profit_percent(int idx) const;
1981 double open_trade_commission(int idx) const;
1982 int open_trade_entry_bar_index(int idx) const;
1983 std::string open_trade_entry_comment(int idx) const;
1984 std::string open_trade_entry_id(int idx) const;
1985 double open_trade_entry_price(int idx) const;
1986 int64_t open_trade_entry_time(int idx) const;
1987 double open_trade_size(int idx) const;
1988 double open_trade_max_drawdown(int idx) const;
1989 double open_trade_max_drawdown_percent(int idx) const;
1990 double open_trade_max_runup(int idx) const;
1991 double open_trade_max_runup_percent(int idx) const;
1992
1993 std::string position_entry_name() const {
1994 if (position_side_ == PositionSide::FLAT || pyramid_entries_.empty()) return "";
1995 return pyramid_entries_.back().entry_id;
1996 }
1997
1998 double max_drawdown_percent() const {
1999 return (initial_capital_ > 0.0) ? (max_drawdown_ / initial_capital_) * 100.0 : 0.0;
2000 }
2001
2002 // Internal sizing helper; protected (alongside calc_qty) so the sizing-guard
2003 // test can exercise the fill_price<=0 / NaN rejection path directly. See
2004 // tests/test_adversarial_ohlcv.cpp.
2005protected:
2007 const execution::Action& action, const execution::Fill& fill,
2008 const execution::LifecycleEffects& lifecycle,
2010 execution::CloseScope scope);
2012 const execution::Action& action, const execution::Fill& fill,
2013 const execution::LifecycleEffects& lifecycle,
2015 const execution::SelectedOpeningSet& selection);
2017 const execution::Action& action,
2018 const execution::Fill& fill,
2019 execution::CloseScope book_or_opening,
2020 const execution::SelectedOpeningSet* selected) const;
2022 const execution::Action& action,
2023 const execution::Fill& fill,
2024 execution::CloseScope book_or_opening,
2025 const execution::SelectedOpeningSet* selected) const;
2027 const execution::Action& action, const execution::Fill& fill,
2028 const execution::LifecycleEffects& lifecycle,
2030 execution::CloseScope book_or_opening,
2031 const execution::SelectedOpeningSet* selected);
2032 // Runtime-private preview of the exact existing pre-source prefix. The
2033 // caller pins the quoted ticket before entering this method.
2035 const execution::Action& action, const execution::Fill& fill,
2038 execution::AccountEffectProjection& account, std::vector<double>& row_pnl) const;
2040 const execution::ReverseTo& reversal, const execution::Fill& fill,
2042 execution::AccountEffectProjection& account, std::vector<double>& row_pnl) const;
2043 struct NativeSettlementStage;
2044 struct NativeSettlementRows;
2047 NativeSettlementStage& stage,
2048 const execution::CloseScope& book_or_opening,
2049 double& remaining) const;
2051 NativeSettlementStage& stage, const execution::Fill& fill) const;
2053 const NativeSettlementStage& stage, const execution::Fill& fill) const;
2055 const NativeSettlementStage& stage, const execution::Fill& fill) const;
2057 NativeSettlementStage& stage, const execution::Fill& fill,
2058 const execution::LifecycleEffects& lifecycle,
2061 const NativeSettlementStage& stage, const execution::Fill& fill,
2063 NativeSettlementRows& rows) const;
2065 const NativeSettlementStage& stage, const execution::Fill& fill,
2067 NativeSettlementRows& rows) const;
2069 const NativeSettlementStage& stage,
2070 const execution::LifecycleEffects& lifecycle,
2071 const NativeSettlementRows& rows);
2073 NativeSettlementStage& stage, const execution::Fill& fill,
2074 const execution::LifecycleEffects& lifecycle,
2076 NativeSettlementRows& rows);
2078 NativeSettlementStage& stage, const execution::Fill& fill,
2079 const execution::LifecycleEffects& lifecycle,
2082 NativeSettlementStage& stage,
2083 const execution::Action& action,
2084 const execution::Fill& fill,
2085 execution::CloseScope book_or_opening,
2086 const execution::SelectedOpeningSet* selected,
2087 const execution::LifecycleEffects* lifecycle) const;
2089 NativeSettlementStage& stage,
2090 const execution::ReverseTo& reversal,
2091 const execution::Fill& fill,
2092 const execution::LifecycleEffects* lifecycle) const;
2094 SCRIPT_ORDER, // strategy.close / close_all / market exit / reversal
2095 BRACKET_EXIT, // a strategy.exit bracket leg fill
2097 };
2098
2099
2100
2102 void append_quoted_lot(PyramidEntry lot, double total_qty, double average_price);
2103 // Allocates the new position cycle, lots and observations, then binds
2104 // exits that still remain in request_roster. Settlement that authorized
2105 // pending removals applies those erasures after old-cycle unbind and
2106 // before this opening bind.
2108
2110 void validate_close_trade_counters(const Trade* rows, size_t count) const;
2111 // Quote one resolved execution's current charges. Entry costs on the
2112 // closed rows are historical allocations. Returns close shares in FIFO
2113 // order followed by the opening share (zero when there is no opening).
2114 std::vector<double> quote_execution_commissions(
2115 const std::vector<double>& closed_units, double opening_units,
2116 const execution::Fill& fill) const;
2117 // The arithmetic of emit_close_trade without its bookkeeping: the Trade
2118 // row a close of ``close_qty`` of ``pe`` at ``fill_price`` on the
2119 // current bar would record (pnl, pnl_pct, commission, excursions, bar
2120 // indexes). emit_close_trade builds and commits; the range-end close
2121 // builds only.
2122
2124 double fill_price, bool was_long, double entry_commission,
2125 double exit_commission,
2126 const execution::PhysicalExecutionContext& context) const;
2128 // Reset ALL per-run state (trades, accumulators, position, pending orders,
2129 // equity extremes, risk latches, intraday/day counters, source-series
2130 // history) so a reused handle's run N is bit-identical to a fresh handle's
2131 // run 1. Preserves configuration (initial_capital_, pyramiding_, slippage_,
2132 // commission_*, default_qty_*, syminfo_, inputs_, risk thresholds) — those
2133 // are set before run() and must survive it. Called at the top of every
2134 // run() loop entrypoint. See tests/test_handle_reuse_reset.cpp.
2136 double account_currency_fx_at(int64_t timestamp_ms) const;
2140
2141
2142
2143
2144
2145 // `fill_price` is already resolved. Source sizing, direction and dust
2146 // selection stay here; purge_pending_exits is translated into exact
2147 // pending removals for the settlement coordinator. False does not
2148 // touch request_roster storage.
2149
2150
2151
2152
2153
2154
2155
2156
2157 // run() helpers (defined in engine_run.cpp).
2158
2159
2160 // Native HTF feed routing (engine_aux_security.cpp): per-state label maps
2161 // built after the evaluators' aggregators exist for this run, and the
2162 // substitution a completed bucket applies. Returns whether `bar` was
2163 // replaced by its native sibling.
2164 void prepare_native_security_feeds(const Bar* input_bars, int n_input);
2165 void prepare_chart_day_partition(const Bar* input_bars, int n_input);
2167 bool count_miss = true);
2168
2169
2170#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
2172 virtual void source_aux_security_input_view(const Bar*& bars, int& n) const;
2176#endif
2177 // Runs the standard per-script-bar order/strategy sequence on current_bar_:
2178 // request matching -> update_per_trade_extremes -> on_bar,
2179 // plus a second request matching when close-timing mode is set
2180 // (TV process_orders_on_close: new market orders fill at this bar's close).
2181 // Shared by run(), run_simple_bar_loop, and the no-magnifier aggregation
2182 // path. The magnifier tick loop does NOT use this — it gates the sequence
2183 // on is_last_tick_ and forces is_first_tick_ before on_bar.
2184
2185
2186
2187
2188 // The TF-aware run()'s actual work (dispatch loop selection, the
2189 // try/catch, both cleanup paths). Does NOT touch last_error_,
2190 // last_run_status_, or abort_requested_ -- every public run() overload
2191 // clears those exactly once at its own entry before reaching here, so a
2192 // request_abort() arriving during a delegating overload's own setup
2193 // (e.g. the SymInfo/overrides overload's syminfo/inputs copy) is never
2194 // silently wiped by a second, later clear.
2195 // The caller has already validated the complete chart bar array; do not
2196 // rescan here or clear an abort that arrived during preflight/setup.
2197
2199 virtual void source_stream_entry_comment(const PyramidEntry&, std::string&) const;
2200 void stream_observe_exit(size_t trade_index);
2201 void stream_refresh_action_metadata(size_t first_action, size_t first_trade);
2202
2203 // fill_report helpers (defined in engine_report.cpp).
2207 void fill_trace_section(ReportC* out) const;
2208
2209 void guard_native_mutation(const char* operation);
2210
2212 bool native = false;
2213 mutable std::unique_ptr<IExecutionConsumer> ptr;
2216 : native(other.native) {}
2218 if (this != &other) {
2219 native = other.native;
2220 ptr.reset();
2221 }
2222 return *this;
2223 }
2224 ExecutionConsumerSlot(ExecutionConsumerSlot&& other) noexcept : native(other.native) {
2225 other.ptr.reset();
2226 }
2228 if (this != &other) {
2229 native = other.native;
2230 ptr.reset();
2231 other.ptr.reset();
2232 }
2233 return *this;
2234 }
2235 };
2237
2238public:
2241 bool native_bound() const;
2242 virtual void on_bar(const Bar& bar) = 0;
2243
2244 // All run() overloads preflight the entire chart array before modifying
2245 // configuration, resetting state, preparing scripts or dispatching bars.
2246 // Require n >= 0, a non-null pointer when n > 0, finite enclosed OHLC,
2247 // volume that is finite >= 0 or NaN (unavailable), and strictly increasing
2248 // timestamps with representable positive int64 deltas. Finite signed/zero
2249 // prices, off-grid prices and gaps are structurally admitted, without a
2250 // guarantee about their financial or extreme-calendar arithmetic.
2251 // Rejection sets last_error() and preserves state except entry-time error,
2252 // run-status and abort bookkeeping. n == 0 retains empty-new-run semantics.
2253 // Processing exceptions after preflight do not imply state rollback.
2254 void run(const Bar* bars, int n);
2255
2256 void run(const Bar* input_bars, int n_input,
2257 const std::string& input_tf,
2258 const std::string& script_tf,
2259 bool bar_magnifier = false,
2260 int magnifier_samples = 4,
2262
2263#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
2264 // Copies a finer request.security-only feed for subsequent historical
2265 // runs. n == 0 clears it. Validation that depends on native chart bars is
2266 // intentionally deferred to run(), where failures reach last_error().
2267 virtual bool set_aux_security_feed(const Bar* bars, int n,
2268 const std::string& input_tf);
2269#endif
2270
2271 // Copies the exchange's own bars of one higher timeframe for the
2272 // request.security evaluators that request exactly it (see
2273 // strategy_set_native_security_feed in pineforge.h). n == 0 clears that
2274 // timeframe's feed. Routing is built per run once the evaluators exist.
2275 bool set_native_security_feed(const std::string& timeframe,
2276 const Bar* bars, int n);
2278 return !native_security_feeds_.empty();
2279 }
2280 // Per-run diagnostics: completed buckets that took a native bar, and
2281 // completed buckets of a fed timeframe that found none (kept aggregate).
2285 int64_t native_security_misses() const {
2287 }
2288 // Whether the last run built the chart symbol's native daily partition
2289 // (an intraday chart with a native "D" feed installed).
2291 return !chart_day_partition_.empty();
2292 }
2293
2294 // Execute confirmed historical bars, then keep this exact instance alive
2295 // for realtime trade updates. The warmup feed must contain at least one
2296 // complete input-timeframe bar. Normalized ticks begin at or after the next
2297 // input bar's open; in-session gaps are materialized as zero-volume
2298 // carry-forward bars when a later tick or stream_advance_time() crosses
2299 // their close boundary. Configured out-of-session intervals are skipped.
2300 bool stream_begin(const Bar* warmup_bars, int n_warmup,
2301 const std::string& input_tf,
2302 const std::string& script_tf = "");
2303 // Confirmed input bars use the same OHLC broker kernel as batch. The first
2304 // tick/bar locks the input mode; mixing them is rejected. Missing in-session
2305 // bars are rejected rather than padded. Closed-session gaps may be skipped.
2306 bool stream_push_bar(const Bar& bar);
2307 bool stream_push_tick(const TradeTick& tick);
2308 bool stream_push_ticks(const TradeTick* ticks, int n);
2309 bool stream_advance_time(int64_t timestamp_ms);
2310 bool stream_end(bool finalize_partial_input_bar = false);
2312 int stream_order_actions_len() const { return static_cast<int>(stream_order_actions_.size()); }
2314 return stream_order_actions_.at(static_cast<size_t>(i));
2315 }
2317 // Observable broker + stream cursors/forming bars, not a serialization of
2318 // arbitrary strategy members. Deterministic strategy code is required.
2319 uint64_t stream_state_hash() const;
2320
2321 // Install an effective-time FX curve (account-currency units per one unit
2322 // of symbol quote currency). Points are copied and must have strictly
2323 // increasing epoch-ms timestamps plus positive finite rates. The latest
2324 // point whose timestamp is <= the current broker event is active. Passing
2325 // n=0 clears the curve and restores the scalar metadata fallback.
2326 bool set_account_currency_fx_series(const int64_t* timestamps_ms,
2327 const double* rates, int n);
2328
2329 void run(const Bar* input_bars, int n_input,
2330 const std::string& input_tf,
2331 const std::string& script_tf,
2332 const std::unordered_map<std::string, std::string>& inputs,
2333 const SymInfo& syminfo,
2334 // Opaque host-override handle: the kernel only forwards it to
2335 // prepare_native_begin() as NativeBeginArgs::overrides_opaque
2336 // and never dereferences it. A host layer that defines its own
2337 // override record passes its address and casts it back there.
2338 const void* overrides = nullptr,
2339 bool bar_magnifier = false,
2340 int magnifier_samples = 4,
2342
2343 int trade_count() const { return (int)trades_.size(); }
2344 const Trade& get_trade(int i) const { return trades_[i]; }
2345 // The same closed rows under the `closed_trade_*` name a host reads them
2346 // by (RP6). The per-field `closed_trade_*` family below stays protected
2347 // Pine plumbing: one whole row by reference answers all of it, and a host
2348 // that owns its report needs the row, not twenty wrappers. Range-end
2349 // report rows are NOT here — report_trade_count() / get_report_trade()
2350 // below span both spaces. Unchecked, like get_trade above: index against
2351 // closed_trade_count().
2352 std::size_t closed_trade_count() const noexcept { return trades_.size(); }
2353 const Trade& closed_trade(std::size_t i) const { return trades_[i]; }
2354 // The REPORT's row space: trades_ followed by range_end_trades_, in the
2355 // order fill_trades_section lays pf_report_t::trades out. trade_count()
2356 // / get_trade() stay the Pine-visible closed trades (strategy.closedtrades
2357 // never sees a range-end row); the report-indexed C ABI accessors
2358 // (strategy_closed_trade_entry_incarnation) must index THIS space, or
2359 // every range-end row reads as index-out-of-range (round-4b F3).
2361 return (int)(trades_.size() + range_end_trades_.size());
2362 }
2363 const Trade& get_report_trade(int i) const {
2364 const int n_closed = (int)trades_.size();
2365 return i < n_closed ? trades_[(size_t)i]
2366 : range_end_trades_[(size_t)(i - n_closed)];
2367 }
2368
2369 // ABI v4 live-runtime surface (task 9): classify why a REPORT-row
2370 // closed trade exited -- report-row scope (spans trades_ then
2371 // range_end_trades_, like get_report_trade above), so this cannot reuse
2372 // the existing protected closed_trade_* names below (strategy.
2373 // closedtrades.* scope: trades_ only, std::string returns). 0 UNKNOWN
2374 // (bad index), 1 SCRIPT, 2 BRACKET, 3 MARGIN_CALL, 4 INTRADAY_LOSS_CAP,
2375 // 5 INTRADAY_FILL_CAP, 6 RANGE_END. Defined in engine_trade_accessors.cpp;
2376 // see strategy_closed_trade_close_cause (pineforge.h) for the exact
2377 // derivation order.
2379
2380 // --- Position-size extremes (strategy.max_contracts_held_*) ---
2384
2385 // --- Even-trade count (strategy.eventrades) ---
2386 int eventrades() const { return eventrades_count_; }
2387
2388 void fill_report(ReportC* out) const;
2389 static void free_report(ReportC* report);
2390
2391 // Returns the error message captured by the most recent run() if it
2392 // failed, or an empty string if the run completed normally. Cleared at
2393 // the start of every run(). The C ABI exposes this via
2394 // pf_strategy_get_last_error().
2395 const std::string& last_error() const { return last_error_; }
2396
2397 // Per-input override (title -> serialized value). Must be set before run()
2398 // so get_input_*() lookups pick up the TV-tester value rather than the
2399 // Pine default.
2400 void set_input(const std::string& key, const std::string& value) {
2401 guard_native_mutation("set_input");
2402 inputs_[key] = value;
2403 }
2405 guard_native_mutation("clear_inputs");
2406 inputs_.clear();
2407 }
2408 void set_trade_start_time(int64_t timestamp_ms) {
2409 guard_native_mutation("set_trade_start_time");
2410 trade_start_time_ = timestamp_ms;
2411 }
2412
2413 // Set the chart's display timezone. Stored in a dedicated slot so it
2414 // does NOT clobber ``syminfo_.timezone`` (the symbol/exchange TZ).
2415 //
2416 // Pre-fix this method wrote the chart TZ into ``syminfo_.timezone``,
2417 // which the codegen reads as the default tz argument of the 1-arg
2418 // ``hour(time)`` / ``minute(time)`` / ``dayofweek(time)`` form. That
2419 // conflated two distinct TV concepts and silently shifted the result
2420 // by the chart-vs-exchange offset (e.g. Asia/Taipei vs UTC = +8h)
2421 // for crypto symbols. The shift cascaded into ``hour``-bucketed
2422 // accumulators — see
2423 // ``validation_typed_matrix/typed-matrix-probe-01-bool-regime-mask``,
2424 // whose 24x7 ``matrix<bool>`` regime mask filled in 8 hours earlier
2425 // than TV and produced ~9% trade-count divergence (TV 773, engine 714
2426 // before this fix; ~778 after).
2427 //
2428 // TV semantics (Pine v6 reference docs):
2429 // * Bare variable ``hour`` / ``minute`` / ``dayofweek``: exchange
2430 // timezone (``syminfo.timezone``). Already correct via
2431 // ``_decompose_bar_time()``'s hardcoded ``gmtime_r``, which
2432 // matches the corpus' ETH-USDT (UTC) data.
2433 // * 1-arg function form ``hour(time)``: defaults its tz arg to
2434 // ``syminfo.timezone`` (NOT the chart display TZ). With this
2435 // change, ``syminfo_.timezone`` retains its constructor default
2436 // ("UTC") and the codegen lambda lands on the cheap gmtime_r
2437 // branch — matching TV.
2438 // * 2-arg function form ``hour(time, tz)``: honours the explicit
2439 // argument, unchanged by this fix.
2440 void set_chart_timezone(const std::string& tz) {
2441 guard_native_mutation("set_chart_timezone");
2442 chart_timezone_ = tz;
2443 }
2444 const std::string& chart_timezone() const { return chart_timezone_; }
2445
2446 // --- Symbol metadata injection (data feed → syminfo.*) ---
2447 // The exchange timezone + session feed session.ismarket / time(session)
2448 // predicates. They default to UTC / 24x7 (crypto); a data feed pushes
2449 // the real values via these setters before run().
2450 //
2451 // The source-layer intraday cap consumes a valid explicit session and this
2452 // timezone through its risk-day policy. Other risk-day rules keep
2453 // chart_timezone_, as do continuous/unconfigured order-counter clocks;
2454 // the existing crypto-on-shifted-chart contract therefore remains intact.
2455 void set_syminfo_timezone(const std::string& tz) {
2456 guard_native_mutation("set_syminfo_timezone");
2457 syminfo_.timezone = tz;
2458 }
2459 void set_syminfo_session(const std::string& s) {
2460 guard_native_mutation("set_syminfo_session");
2461 syminfo_.session = s;
2462 }
2463 // ``syminfo.type`` ("crypto" default; "forex" / "stock" / "futures" /
2464 // "index" / "fund" / "cfd" per TradingView). Scripts branch on it for
2465 // instrument conventions — the canonical one being the pip size
2466 // (``syminfo.type == "forex" ? 0.0001 : syminfo.mintick``), which on a
2467 // 5-digit FX symbol under the crypto default computed every pip-scaled
2468 // stop/target 10x too tight (finding 454). Empty is ignored.
2469 void set_syminfo_type(const std::string& t) {
2470 guard_native_mutation("set_syminfo_type");
2471 if (!t.empty()) syminfo_.type = t;
2472 }
2473 /// Whether TradingView's session template for this symbol carries the
2474 /// exchange's early closes and holidays, so a D/W/M request.security
2475 /// bucket completes on a shortened session's actual last chart bar
2476 /// (TimeframeAggregator::set_early_close_completes): exchange-listed
2477 /// kinds (stock, futures, index, fund, dr, ...) yes -- NYSE:F's 12:45 ET
2478 /// half-day bar and CME's 11:45 CT early-close bar are pinned; the OTC
2479 /// quote streams -- forex, cfd, crypto -- no: their period ends at the
2480 /// nominal close and a session ending early completes lazily on the
2481 /// next period's first bar (OANDA:XAUUSD 15m, lab tv oanda pin, ledger
2482 /// log-20260905t034240z-30be11fe). syminfo.type comes from the harness
2483 /// (strategy_set_syminfo_type <- PINEFORGE_VERIFY_SYMTYPE); the
2484 /// constructor default "crypto" keeps an untyped run on the lazy rule.
2486 // Generic string-field injection for the remaining OHLCV-less syminfo
2487 // members (ticker / tickerid / currency / basecurrency / description /
2488 // volumetype / type). Unknown keys and empty values are ignored; returns
2489 // true when a field was set.
2490 bool set_syminfo_string(const std::string& key, const std::string& value) {
2491 guard_native_mutation("set_syminfo_string");
2492 if (value.empty()) return false;
2493 if (key == "type") { syminfo_.type = value; return true; }
2494 if (key == "ticker") { syminfo_.ticker = value; return true; }
2495 if (key == "tickerid") { syminfo_.tickerid = value; return true; }
2496 if (key == "currency") { syminfo_.currency = value; return true; }
2497 if (key == "basecurrency") { syminfo_.basecurrency = value; return true; }
2498 if (key == "description") { syminfo_.description = value; return true; }
2499 if (key == "volumetype") { syminfo_.volumetype = value; return true; }
2500 return false;
2501 }
2502 // Runtime syminfo injection (by design — the engine stores no instrument
2503 // metadata of its own; the harness supplies it per run). mintick drives the
2504 // directional fill snap + slippage*tick economics; pointvalue is the
2505 // futures $-per-point multiplier applied to every money path (realized
2506 // PnL + excursions, open profit / equity, percent/cash sizing, percent
2507 // commission notionals, margin check — see tests/test_pointvalue.cpp).
2508 // Both default to crypto/equity values (0.01 / 1.0) and only matter when the
2509 // harness sets a non-default instrument.
2510 void set_syminfo_mintick(double m) {
2511 guard_native_mutation("set_syminfo_mintick");
2512 if (m > 0.0) { syminfo_.mintick = m; syminfo_mintick_ = m; }
2513 }
2514 void set_syminfo_pointvalue(double pv) {
2515 guard_native_mutation("set_syminfo_pointvalue");
2516 if (pv > 0.0) { syminfo_.pointvalue = pv; }
2517 }
2518
2519 virtual void set_syminfo_metadata(const std::string& key, double value);
2520
2521 // Returns the script's active timeframe string (e.g. "15" for 15-minute,
2522 // "D" for daily). Backs timeframe.main_period in generated Pine v6 code.
2523 const std::string& main_period() const { return script_tf_; }
2524
2525 // Host run-mode overrides behind the frozen C setters
2526 // strategy_set_realtime_tail / strategy_set_probe_suppress_tail_logic
2527 // (pineforge.h). The kernel keeps no forming-tail state of its own: a
2528 // batch is complete input and the forward path is the stream, so
2529 // "the last fed bar is still forming" and "run only the broker steps on
2530 // the last bar" are a host's live-probe protocol, not a kernel mode. A
2531 // host that models one (source::PineStrategyHost, for the live runner's
2532 // probe / settle cycle) overrides both, owns the state and answers true.
2533 // The kernel default keeps the C ingress contract these setters always
2534 // had on a bare host -- accepted before begin, guarded like every other
2535 // setter, and inert (no kernel path ever read the flags on a native
2536 // host) -- so it stores nothing, leaves last_error() untouched and
2537 // answers false, meaning "no such mode here". R5 lane N14: the flags
2538 // were BacktestEngine members until then.
2539 virtual bool set_realtime_tail(bool on, int horizon_bars);
2540 virtual bool set_probe_suppress_tail_logic(bool on);
2541
2542 // Force this run's intrabar path order (ABI v4 live-runtime surface,
2543 // task 4): 0 AUTO (the unchanged |H-O| vs |O-L| rule), 1 HIGH_FIRST
2544 // (O -> H -> L -> C), 2 LOW_FIRST (O -> L -> H -> C). Any other value is
2545 // clamped to AUTO. A live probe runs the SAME forming bar under both
2546 // forced orders and keeps only the fills that agree between the two --
2547 // a fill that depends on which leg TradingView's own still-forming bar
2548 // will resolve to is path-dependent and must be suppressed rather than
2549 // guessed. See internal::bar_path_uses_high_first's thread-local
2550 // override (engine_path_resolve.cpp) and the PathOrderScope guard in
2551 // engine_run.cpp that installs/clears it for exactly the duration of
2552 // this run's own dispatch.
2553 // Persistent configuration, like set_realtime_tail -- stays set until a
2554 // caller passes mode=0. The legacy route installs it through
2555 // PathOrderScope; a native-bound source provider projects the same value
2556 // into NativeRunSpec::path_order.
2557 // Default AUTO (mode=0): every historical run stays byte-identical to
2558 // before this flag existed.
2559 void set_path_order(int mode) {
2560 guard_native_mutation("set_path_order");
2561 path_order_mode_ = (mode == 1 || mode == 2) ? mode : 0;
2562 }
2563
2564 // The dual-entry-stop arbitration decided on the LAST bar this run
2565 // dispatched (a flat position resting one long stop-only ENTRY and one
2566 // short stop-only ENTRY, both touched that bar --
2567 // dual_entry_stop_path_winner, engine_path_resolve.cpp): 0 None (no
2568 // such pair was arbitrated on that bar), 1 LongFirst, 2 ShortFirst --
2569 // internal::DualEntryStopPathWinner's own enumerator order (its Tie
2570 // value never reaches here; dual_entry_stop_path_winner always resolves
2571 // a tie to LongFirst). This reads last_bar_dual_entry_decision_, a
2572 // per-bar snapshot of the arbitration that survives whatever the
2573 // working state (dual_entry_path_) does afterward this same bar -- a
2574 // fill, a declined stop-entry admission, or (under
2575 // process_orders_on_close) the bar's second request matching
2576 // pass, all of which reset dual_entry_path_ to None without undoing the
2577 // fact that an arbitration happened. Only the standard
2578 // (non-calc_on_order_fills) dispatch path updates it; this is a silent
2579 // no-op (stays at its last standard-path value) under the COOF
2580 // scheduler, mirroring set_probe_suppress_tail_logic's
2581 // dispatch-path-scope caveat.
2582
2583 // Live runtime G1 (spec §3.4): a deterministic, order-independent
2584 // FNV-1a 64 hash over every piece of broker state that decides the next
2585 // bar's fills (position, book, pyramid lots, trail scalars, cycle/
2586 // intraday/risk latches, frozen sizing, equity sums). Two engines with
2587 // equal broker state hash equally regardless of unordered-container
2588 // insertion history; any difference in that state changes the hash.
2589 // Implemented in engine_state_hash.cpp; coverage of the marked
2590 // broker-state region(s) below (grep this file for "broker-state") is
2591 // enforced by scripts/check_broker_state_hash_coverage.py.
2592 uint64_t broker_state_hash() const;
2593
2594 // ABI v4 live-runtime surface (task 7, spec 3.6): read-only view of the
2595 // resting-order book after the most recent run() -- the book in force
2596 // for the next bar, in the vector's own (insertion) order; fill
2597 // priority is decided at fill time from created_seq. The C ABI
2598 // (strategy_pending_orders_len / strategy_pending_order_get) copies
2599 // each live request out through PendingIntentView's POD projection
2600 // (pf_pending_order_v1_t, include/pineforge/pending_order_mirror.hpp),
2601 // never by pointer. `i` must be in [0, pending_order_count()).
2602
2603 // ABI v4 live-runtime surface (task 8, spec 3.6): engine-computed
2604 // derived values of a resting order and the position scalars the live
2605 // runtime would otherwise have to re-derive. Pure const reads of the
2606 // engine's own sizing / admission / level-resolution predicates; none
2607 // of them mutates the engine, so a historical run is byte-identical
2608 // whether or not a caller reads them. The source adapter derives the
2609 // projection from native requests, live state, and placement facts.
2610 //
2611 // probe_fill_qty: the quantity the entry kernel would open if the
2612 // order at `index` filled at `fill_price`, and which sizing partition
2613 // produced it -- exactly the "quantity the market / priced-entry kernel
2614 // would actually open with" computation of the zero-lot decline gate in
2615 // apply_filled_order_to_state, tagged:
2616 // 0 EXPLICIT a script-supplied qty: for strategy.entry
2617 // calc_qty_for_type at the slipped fill
2618 // (apply_qty_step of the contracts for FIXED,
2619 // the budget sized at the fill for a per-call
2620 // percent/cash override); a strategy.order
2621 // explicit qty is dispatched VERBATIM
2622 // (apply_raw_order_fill, no lot step).
2623 // 1 FROZEN_PLACEMENT a quantity the engine fixed before the fill,
2624 // never re-derived from the fill price:
2625 // frozen_default_qty (the default
2626 // percent_of_equity / cash MARKET or
2627 // strategy.order size at the signal close), a
2628 // MARKET's frozen broker transaction
2629 // (paired_flat_market_transaction_qty; a
2630 // same-bar-market transaction's frozen total from
2631 // FLAT or as a kept over-cap add), or what one
2632 // of the two MARKET reversal kernels opens:
2633 // a same-bar-market member against an
2634 // opposite live position opens the remainder
2635 // transaction_units - min(transaction_units, live qty)
2636 // (apply_same_bar_market_tx_reversal), and the
2637 // exact SHORT-seed collision's final short
2638 // re-opens the residual pyramid_entries_[0].qty
2639 // - pyramid_entries_[1].qty after closing both
2640 // lots (short_seed_collision_final_short_is_
2641 // live). Both kernels are modelled; each is
2642 // reported with close_only = 1 when it opens
2643 // nothing.
2644 // 2 DEFAULT_STOP_PLACEMENT default_stop_placement_qty, when
2645 // use_default_stop_placement_qty says
2646 // dispatch consumes it (round 7 family K).
2647 // 3 AT_FILL default sizing at the slipped fill,
2648 // calc_qty(fill).
2649 // `fill_price` is slipped the way the kernel slips it
2650 // (native_matching::apply_slippage then the directional grid snap; a
2651 // LIMIT-triggered entry fills limit-or-better and is not slipped, see
2652 // docs/pine-adapter-kernel-notes.md).
2653 // `close_only` is 1 when the kernel's close-only predicate fires -- the
2654 // fill closes against the live opposite position and that predicate
2655 // opens no leg of its own (where the order was created FLAT the branch
2656 // is close_opposite_then_enter: a transaction larger than the live
2657 // position still opens the remainder, so a consumer compares `qty` with
2658 // the live position): the order's
2659 // affordability_close_only (entry leg declined at placement), the
2660 // priced-entry prior_cycle_close_only rule (opposite live position,
2661 // created_position_side != position_side_, and not a KI-65
2662 // reverses_same_bar_market_from_flat), the same-cycle frozen
2663 // explicit-FIXED transaction that the close consumes exactly, or a
2664 // finalized flat MARKET pair, or one of the two reversal kernels above
2665 // opening nothing -- each spelled as apply_entry_order_fill /
2666 // apply_market_order_fill spell it. A replaced default-percent short
2667 // (replaced_percent_short_market_is_live) is dispatched
2668 // close_opposite_then_enter with its frozen_default_qty: `qty` is that
2669 // transaction, close_only 0. Not folded into `qty`: the
2670 // deferred-flip carry (tv_carry_qty, enter_market_from_flat's
2671 // tv_deferred_flip rule adds it on top of this quantity for a priced
2672 // entry firing from FLAT whose placement side is the opposite of the
2673 // requested side) -- the mirror exposes tv_carry_qty and
2674 // created_position_side verbatim. Returns 0 on success; 1 (qty NaN,
2675 // close_only 0, partition -1) when the order is an EXIT, whose fill
2676 // quantity is decided against the live position at the fill, not by a
2677 // sizing partition; -1 on a bad index or a null out-pointer.
2678
2679 // 1 when the order's entry-relative offsets (profit_ticks / loss_ticks /
2680 // trail_points) resolve NOW: entries, plain orders and exits with an
2681 // empty from_entry always; an exit bound to from_entry only once that
2682 // id has filled in the CURRENT position cycle (cycle_filled_entry_ids_,
2683 // the gate materialize_relative_exit_prices_for_live_position and the
2684 // eligibility pass share). 0 otherwise, -1 on a bad index.
2685
2686 // The price levels the order would fire at, as the fill path resolves
2687 // them: a set stop_price / limit_price / trail_price verbatim (they are
2688 // already on the price grid); an unset leg from its tick offset against
2689 // position_entry_price_ when pending_order_level_resolved() == 1 and a
2690 // position is live, with the position side's sign exactly as
2691 // materialize_relative_exit_prices_for_live_position (limit = entry +
2692 // dir * profit_ticks * mintick, stop = entry - dir * loss_ticks *
2693 // mintick, dir = +1 long / -1 short, level_on_price_grid) and
2694 // the trail activation rule (activation = snap_trail_level_to_tick_grid(
2695 // entry +/- ceil(trail_points - 5e-5) * mintick); trail_points wins
2696 // over trail_price when both are set). NaN for a leg that is unset or
2697 // unresolvable. Returns 0, or -1 on a bad index / null out-pointer.
2698
2699 // The live position's volume-weighted average entry price
2700 // (position_entry_price_; 0 when flat -- the engine keeps 0 there, the
2701 // C ABI reports NaN when flat), its cycle id (position_cycle_seq_; 0
2702 // when flat, a fresh nonzero id per open/reversal, kept across
2703 // same-direction adds) and the trail extreme the exit trail legs ride
2704 // (trail_best_price_; NaN until a position fills).
2706 int64_t position_cycle_seq() const { return position_cycle_seq_; }
2707 double trail_best_price() const { return trail_best_price_; }
2708 // ABI v4 live-runtime surface (task 9): public forwarders for the C
2709 // ABI, which -- being extern "C" free functions -- cannot reach the
2710 // protected signed_position_size() / current_equity() above.
2711 // signed_position_size() is strategy.position_size (KI-64 freeze-aware:
2712 // reads the pre-close position while a same-bar POOC close is frozen).
2713 // current_equity() is initial capital plus realized net profit
2714 // (strategy.initial_capital + strategy.netprofit). NOT Pine's
2715 // strategy.equity, which adds open profit on top of this (see the
2716 // sizing_equity formula and the equity-curve remark below, both
2717 // current_equity() + open_profit(...)).
2718 virtual double live_position_size() const {
2721 return 0.0;
2722 }
2724 virtual int observe_pending_count_v1() const;
2725 virtual int observe_pending_copy_v1(int index, pf_pending_order_v1_t* out) const;
2726 virtual int observe_probe_fill_qty(int index, double fill_price, double* qty,
2727 int* close_only, int* partition) const;
2728 virtual int observe_pending_level_resolved(int index) const;
2729 virtual int observe_pending_effective_levels(int index, double* stop,
2730 double* limit,
2731 double* trail_activation) const;
2732 virtual double observe_trail_best_price_v1() const;
2733 double live_current_equity() const { return current_equity(); }
2734 // ABI v4 live-runtime surface (task 9): total SCRIPT bars dispatched by
2735 // the most recent run() (mirrors pf_report_t::script_bars_processed,
2736 // engine_report.cpp), including the stream warmup leg and every
2737 // realtime tick-driven bar after strategy_stream_begin.
2739
2740 // ABI v4 live-runtime surface (task 6): when on, every script bar's
2741 // dispatch (all four script-bar dispatch sites -- the single-TF run()
2742 // loop, run_simple_bar_loop, run_aggregation_bar_loop, and
2743 // stream_dispatch_script_bar, engine_stream.cpp, the realtime-stream
2744 // continuation of a stream_begin warmup) appends broker_state_hash()
2745 // to broker_state_hashes_ immediately after that bar's
2746 // record_equity_point() call, so the recorded array's length matches
2747 // script_bars_processed and pf_report_t::broker_state_hash_len 1:1 --
2748 // including on strategy_stream_fill_report, whose report is the
2749 // cumulative warmup + realtime run. Default off: broker_state_hashes_
2750 // stays empty, fill_report emits a null/zero-length array, and every
2751 // historical run stays byte-identical to before this flag existed.
2752 // reset_run_state() clears the recorded array on every run() (the
2753 // warmup leg of stream_begin included) regardless of this flag's
2754 // value; the flag itself is persistent configuration, like
2755 // set_realtime_tail, so it must be set BEFORE stream_begin to also
2756 // cover the warmup bars.
2757 //
2758 // What a row is, and what it is for. A row is the run's CONTINUATION
2759 // IDENTITY at that bar, not its trade outcome: broker_state_hash() is
2760 // broker_state_hash_from_execution_hash(continuation_hash()), so it folds
2761 // the kernel's broker state AND, ahead of it, the state a resume would
2762 // continue from. NativeRunPhase (Batch/Warmup/Realtime, readable as
2763 // native_state().phase) is part of that continuation on purpose, because a
2764 // consumer mid-warmup and one mid-realtime are not interchangeable
2765 // continuations. So the array is a replay check WITHIN one driving mode
2766 // and deliberately not across modes:
2767 // * same driving -- two runs driven the same way record the same rows,
2768 // and the row after bar k is the last row of a run driven the same way
2769 // that ended at bar k (a batch and a stream at any warmup split);
2770 // * different driving -- run(), stream_begin(warmup=1)+push and
2771 // stream_begin(warmup=all) over the same bars booking the same trades
2772 // record different rows from index 0; only the length identity above
2773 // survives. Two streams share exactly their common Warmup prefix.
2774 // * the broker half alone IS driving-mode invariant: factor the
2775 // continuation out with broker_state_hash_from_execution_hash(fixed)
2776 // and the remaining fold is identical at every bar in every driving.
2777 // The batch<->stream oracle is therefore the OUTCOME, not this array:
2778 // tests/test_native_margin_fx_roll.cpp section 8 and tests/test_streaming.cpp
2779 // for the kernel, scripts/check_corpus_parity.sh for the Pine adapter.
2780 // Both directions are pinned in tests/test_native_report_truth.cpp.
2782 guard_native_mutation("set_broker_state_hash_recording");
2784 }
2785
2786 // Toggle volume-weighted per-sub-bar sampling inside run_magnified_bar.
2787 // Has no effect unless bar magnifier is enabled.
2789 guard_native_mutation("set_magnifier_volume_weighted");
2791 }
2792
2793 // --- Runtime trace API ---
2794 // Default off so existing strategies pay zero cost. The validator
2795 // flips this on per-strategy via ``strategy_set_trace_enabled`` (the
2796 // FFI shim defined in c_abi.cpp) before running a backtest whose
2797 // per-bar values it wants to cross-reference against TradingView.
2798 void set_trace_enabled(bool on) {
2799 guard_native_mutation("set_trace_enabled");
2800 trace_enabled_ = on;
2801 }
2802 bool trace_enabled() const { return trace_enabled_; }
2803
2804 // --- Live-runtime status API (ABI v4) ---
2805 int last_run_status() const { return last_run_status_; }
2806
2807 // Request cooperative abort of the run in progress on this handle (a
2808 // live runtime supersedes an in-flight probe run). Safe to call from any
2809 // thread; consumed by the running loop at its next bar. A request made
2810 // while idle is cleared at the next run() entry and is a no-op.
2811 void request_abort() { abort_requested_.store(true, std::memory_order_relaxed); }
2812
2813 // Push a typed per-bar value into the trace buffer. Cheap when
2814 // disabled — a single bool branch and return. When enabled, name
2815 // interning amortises to a single hash lookup per call after the
2816 // first occurrence; the actual record is a 24-byte POD push_back.
2817 //
2818 // The bool / int overloads internally cast to double so the
2819 // transpiler pragma can emit a single call shape regardless of the
2820 // source variable's Pine type — keeping codegen rewrites trivial.
2821 void trace(const std::string& name, double value);
2822 void trace(const std::string& name, bool value) { trace(name, value ? 1.0 : 0.0); }
2823 void trace(const std::string& name, int value) { trace(name, static_cast<double>(value)); }
2824};
2825
2826} // inline namespace engine_script_run_v18
2827} // namespace pineforge
bool set_native_security_feed(const std::string &timeframe, const Bar *bars, int n)
broker::OpeningObligations opening_obligations_
Definition engine.hpp:394
virtual void hash_source_extension(BrokerStateHashSink &) const
double get_syminfo_metadata(const std::string &key) const
Definition engine.hpp:505
double account_currency_fx_at(int64_t timestamp_ms) const
std::vector< double > quote_execution_commissions(const std::vector< double > &closed_units, double opening_units, const execution::Fill &fill) const
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) const
std::vector< int64_t > account_currency_fx_timestamps_
Definition engine.hpp:442
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::Result settle_native_reversal_at_v1(const execution::ReverseTo &reversal, const execution::Fill &fill, const execution::PhysicalExecutionContext &context)
const StreamOrderAction & stream_order_action_at(int i) const
Definition engine.hpp:2313
void trace(const std::string &name, double value)
void trace(const std::string &name, bool value)
Definition engine.hpp:2822
bool stream_end(bool finalize_partial_input_bar=false)
const Trade & closed_trade(std::size_t i) const
Definition engine.hpp:2353
execution::AccountEffectProjection project_native_settlement_stage(const NativeSettlementStage &stage, const execution::Fill &fill) const
bool substitute_native_security_bar(SecurityEvalState &state, Bar &bar, bool count_miss=true)
std::uint64_t broker_state_hash_from_execution_hash(std::uint64_t) const
execution::Status prepare_native_settlement_commit(const NativeSettlementStage &stage, const execution::Fill &fill, const execution::PhysicalExecutionContext &context, NativeSettlementRows &rows) const
virtual bool set_aux_security_feed(const Bar *bars, int n, const std::string &input_tf)
virtual void set_syminfo_metadata(const std::string &key, double value)
virtual int observe_pending_effective_levels(int index, double *stop, double *limit, double *trail_activation) const
double open_profit(double current_price) const
Definition engine.hpp:1439
execution::Result settle_execution_selected_with_lifecycle(const execution::Action &action, const execution::Fill &fill, const execution::LifecycleEffects &lifecycle, const execution::SelectedOpeningSet &selection)
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::vector< SecurityEvalState > security_eval_states_
Definition engine.hpp:1683
std::string closed_trade_exit_comment(int idx) const
Definition engine.hpp:1907
execution::AccountEffectProjection project_native_settlement_scoped_v1(const execution::Action &action, const execution::Fill &fill, execution::CloseScope scope) const
std::vector< pf_equity_point_t > equity_curve_
Definition engine.hpp:652
std::string open_trade_entry_id(int idx) const
virtual int observe_pending_level_resolved(int index) const
void trace(const std::string &name, int value)
Definition engine.hpp:2823
bool session_template_knows_early_close() const
Whether TradingView's session template for this symbol carries the exchange's early closes and holida...
double calc_commission(double fill_price, double qty) const
Definition engine.hpp:1103
bool get_input_bool(const std::string &key, bool default_val) const
bool stream_push_ticks(const TradeTick *ticks, int n)
void stream_refresh_action_metadata(size_t first_action, size_t first_trade)
virtual std::uint64_t broker_state_hash_projection() const
virtual void source_stream_entry_comment(const PyramidEntry &, std::string &) const
virtual int observe_pending_copy_v1(int index, pf_pending_order_v1_t *out) const
std::vector< StreamOrderAction > stream_order_actions_
Definition engine.hpp:1651
std::string get_input_string(const std::string &key, const std::string &default_val) const
void set_trade_start_time(int64_t timestamp_ms)
Definition engine.hpp:2408
std::vector< PyramidEntry > pyramid_entries_
Definition engine.hpp:414
const IExecutionConsumer & execution_consumer() const
void build_native_settlement_close_rows(const NativeSettlementStage &stage, const execution::Fill &fill, const execution::PhysicalExecutionContext &context, NativeSettlementRows &rows) const
void stream_observe_entry(const PyramidEntry &pe)
void dispatch_security_eval(SecurityEvalState &state, const Bar &bar, bool publish, int64_t bar_index)
double get_input_double(const std::string &key, double default_val) const
virtual void evaluate_security(int sec_id, const Bar &bar, bool is_complete)
Definition engine.hpp:1814
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::AccountEffectProjection project_native_reversal_v1(const execution::ReverseTo &reversal, const execution::Fill &fill) const
execution::Result settle_source_staged_execution(NativeSettlementStage &stage, const execution::Fill &fill, const execution::LifecycleEffects &lifecycle, const execution::PhysicalExecutionContext &context)
void record_equity_point(int64_t script_bar_ts)
Definition engine.hpp:1871
double round_to_mintick(double price) const
Definition engine.hpp:913
execution::AccountEffectProjection project_native_settlement_v1(const execution::Action &action, const execution::Fill &fill) const
execution::Result settle_reversal_with_lifecycle_v1(const execution::ReverseTo &reversal, const execution::Fill &fill, const execution::LifecycleEffects &lifecycle)
execution::SettlementInspection inspect_native_reversal_v1(const execution::ReverseTo &reversal, const execution::Fill &fill) const
bool set_syminfo_string(const std::string &key, const std::string &value)
Definition engine.hpp:2490
broker::PositionCloseObligation position_close_obligation_
Definition engine.hpp:625
double closed_trade_max_drawdown_percent(int idx) const
Definition engine.hpp:1961
void append_quoted_lot(PyramidEntry lot, double total_qty, double average_price)
void set_syminfo_session(const std::string &s)
Definition engine.hpp:2459
execution::Result commit_native_settlement_stage(NativeSettlementStage &stage, const execution::Fill &fill, const execution::LifecycleEffects &lifecycle, const execution::PhysicalExecutionContext &context)
execution::Result settle_native_execution_selected_at(const execution::Action &action, const execution::Fill &fill, const execution::PhysicalExecutionContext &context, const execution::SelectedOpeningSet &selection)
execution::AccountEffectProjection project_with_membership(const execution::Action &action, const execution::Fill &fill, execution::CloseScope book_or_opening, const execution::SelectedOpeningSet *selected) const
execution::Result settle_native_execution_scoped_at(const execution::Action &action, const execution::Fill &fill, const execution::PhysicalExecutionContext &context, execution::CloseScope scope)
std::size_t closed_trade_count() const noexcept
Definition engine.hpp:2352
execution::Result settle_native_execution_at(const execution::Action &action, const execution::Fill &fill, const execution::PhysicalExecutionContext &context)
void snapshot_entry_commission(PyramidEntry &pe) const
Definition engine.hpp:1133
execution::Status preview_native_settlement_commit(const execution::Action &action, const execution::Fill &fill, const execution::PhysicalExecutionContext &context, execution::CloseScope scope, const execution::SelectedOpeningSet *selected, execution::AccountEffectProjection &account, std::vector< double > &row_pnl) const
execution::SettlementInspection inspect_native_settlement_scoped(const execution::Action &action, const execution::Fill &fill, execution::CloseScope scope) const
void set_chart_timezone(const std::string &tz)
Definition engine.hpp:2440
std::string closed_trade_exit_id(int idx) const
Definition engine.hpp:1915
double open_entry_commission(const PyramidEntry &pe) const
Definition engine.hpp:1121
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
std::string closed_trade_entry_comment(int idx) const
Definition engine.hpp:1903
void guard_native_mutation(const char *operation)
std::unordered_map< std::string, double > syminfo_metadata_
Definition engine.hpp:485
virtual void source_aux_security_input_view(const Bar *&bars, int &n) const
std::string open_trade_entry_comment(int idx) const
std::unordered_map< std::string, std::string > inputs_
Definition engine.hpp:478
double bar_fill_price(double raw_bar_price) const
Definition engine.hpp:970
execution::Result settle_with_context(const execution::Action &action, const execution::Fill &fill, const execution::LifecycleEffects &lifecycle, const execution::PhysicalExecutionContext &context)
double level_on_price_grid(double level) const
Definition engine.hpp:1061
int32_t intern_trace_name(const std::string &name)
execution::Status preflight_native_settlement_effects(const NativeSettlementStage &stage, const execution::LifecycleEffects &lifecycle, const NativeSettlementRows &rows)
uint64_t closed_trade_entry_incarnation(int idx) const
Definition engine.hpp:1919
virtual bool set_realtime_tail(bool on, int horizon_bars)
execution::SettlementInspection inspect_native_settlement_selected(const execution::Action &action, const execution::Fill &fill, const execution::SelectedOpeningSet &selection) const
execution::Status allocate_native_settlement_closes(NativeSettlementStage &stage, const execution::CloseScope &book_or_opening, double &remaining) const
void open_quoted_position(PositionSide requested, PyramidEntry lot)
bool stream_push_tick(const TradeTick &tick)
void feed_security_eval_state(SecurityEvalState &state, const Bar &input_bar)
void fill_security_diag_section(ReportC *out) const
bool stream_begin(const Bar *warmup_bars, int n_warmup, const std::string &input_tf, const std::string &script_tf="")
const std::string & chart_timezone() const
Definition engine.hpp:2444
virtual int observe_probe_fill_qty(int index, double fill_price, double *qty, int *close_only, int *partition) const
void run(const Bar *input_bars, int n_input, const std::string &input_tf, const std::string &script_tf, bool bar_magnifier=false, int magnifier_samples=4, MagnifierDistribution magnifier_dist=MagnifierDistribution::ENDPOINTS)
void stage_native_settlement(NativeSettlementStage &stage, const execution::ReverseTo &reversal, const execution::Fill &fill, const execution::LifecycleEffects *lifecycle) const
void prepare_chart_day_partition(const Bar *input_bars, int n_input)
execution::Status validate_native_settlement_book(double &held) const
virtual void hash_host_extension(BrokerStateHashSink &) const
execution::AccountEffectProjection project_native_settlement_selected_v1(const execution::Action &action, const execution::Fill &fill, const execution::SelectedOpeningSet &selection) const
void set_syminfo_timezone(const std::string &tz)
Definition engine.hpp:2455
std::vector< NativeSecurityFeed > native_security_feeds_
Definition engine.hpp:1699
bool stream_advance_time(int64_t timestamp_ms)
execution::Result commit_prepared_native_settlement_stage(NativeSettlementStage &stage, const execution::Fill &fill, const execution::LifecycleEffects &lifecycle, const execution::PhysicalExecutionContext &context, NativeSettlementRows &rows)
execution::SettlementInspection inspect_with_membership(const execution::Action &action, const execution::Fill &fill, execution::CloseScope book_or_opening, const execution::SelectedOpeningSet *selected) const
double apply_percent_exit_qty_step(double requested_qty, double available_qty) const
Definition engine.hpp:1203
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)
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
int get_input_int(const std::string &key, int default_val) const
const Trade & get_report_trade(int i) const
Definition engine.hpp:2363
virtual void prepare_script_run(const Bar *, int, bool)
Definition engine.hpp:1811
void set_input(const std::string &key, const std::string &value)
Definition engine.hpp:2400
execution::SettlementInspection inspect_native_settlement_stage(const NativeSettlementStage &stage, const execution::Fill &fill) const
int64_t get_input_int64(const std::string &key, int64_t default_val) const
std::unordered_map< std::string, int32_t > trace_name_index_
Definition engine.hpp:1764
void finish_native_settlement_stage(NativeSettlementStage &stage, const execution::Fill &fill) const
bool set_account_currency_fx_series(const int64_t *timestamps_ms, const double *rates, int n)
std::string closed_trade_entry_id(int idx) const
Definition engine.hpp:1911
void register_security_eval(int sec_id, const std::string &requested_tf, const std::string &input_tf)
execution::SettlementInspection inspect_native_settlement(const execution::Action &action, const execution::Fill &fill) const
void settle_position_after_partial_exit(PositionReductionCause cause)
std::string closed_trade_direction(int idx) const
Definition engine.hpp:1969
double allocated_entry_commission(const PyramidEntry &pe, double units) const
Definition engine.hpp:1127
void prepare_native_security_feeds(const Bar *input_bars, int n_input)
void validate_close_trade_counters(const Trade *rows, size_t count) const
std::string open_trade_direction(int idx) const
Definition engine.hpp:1973
void set_syminfo_type(const std::string &t)
Definition engine.hpp:2469
virtual std::uint64_t broker_state_hash_projection() const =0
std::variant< Book, OpeningExposure > CloseScope
std::variant< Flatten, order_action::Reduce, order_action::Transact > Action
Definition execution.hpp:17
exit_legs::Lifecycle ExitLegLifecycle
Definition engine.hpp:276
MagnifierDistribution
Definition magnifier.hpp:7
double na< double >()
Definition na.hpp:17
std::function< ClosedLotExcursion(const ClosedLotExcursionFacts &)> LotExcursionHook
Definition engine.hpp:112
std::unordered_map< std::string, std::string > InputsMap
Definition engine.hpp:309
int na< int >()
Definition na.hpp:18
Single per-script-bar equity point.
Definition pineforge.h:325
double open_profit
Mark-to-market open P&L at bar close.
Definition pineforge.h:328
double equity
initial_capital + net_profit + open_profit.
Definition pineforge.h:327
int64_t time_ms
Script-bar OPEN timestamp (Unix ms).
Definition pineforge.h:326
Composite metrics container: trade stats (all / long / short) + equity-curve stats.
Definition pineforge.h:316
double open
Definition bar.hpp:7
double close
Definition bar.hpp:7
double low
Definition bar.hpp:7
double high
Definition bar.hpp:7
std::string entry_comment
Definition engine.hpp:120
double entry_commission_account
Definition engine.hpp:139
int64_t magnifier_sample_ticks_total
Definition engine.hpp:249
pf_metrics_t metrics
Definition engine.hpp:268
SecurityDiagC * security_diag
Definition engine.hpp:255
int64_t equity_curve_len
Definition engine.hpp:270
int64_t security_feeds_total
Definition engine.hpp:245
int64_t magnifier_sub_bars_total
Definition engine.hpp:248
pf_equity_point_t * equity_curve
Definition engine.hpp:269
int64_t broker_state_hash_len
Definition engine.hpp:273
TraceEntryC * trace
Definition engine.hpp:264
int64_t input_bars_processed
Definition engine.hpp:243
int64_t security_eval_complete_total
Definition engine.hpp:246
uint64_t * broker_state_hash
Definition engine.hpp:272
const char ** trace_names
Definition engine.hpp:266
int64_t script_bars_processed
Definition engine.hpp:244
int64_t security_eval_partial_total
Definition engine.hpp:247
std::string timezone
Definition engine.hpp:292
std::string tickerid
Definition engine.hpp:288
std::string ticker
Definition engine.hpp:287
std::string basecurrency
Definition engine.hpp:290
std::string volumetype
Definition engine.hpp:294
std::string type
Definition engine.hpp:291
std::string description
Definition engine.hpp:295
std::string currency
Definition engine.hpp:289
std::string session
Definition engine.hpp:293
int32_t open_at_end
Definition engine.hpp:213
int32_t exit_bar_index
Definition engine.hpp:212
double max_drawdown
Definition engine.hpp:208
int64_t entry_time
Definition engine.hpp:197
int64_t exit_time
Definition engine.hpp:198
int32_t entry_bar_index
Definition engine.hpp:211
bool exit_from_bracket
Definition engine.hpp:177
double commission
Definition engine.hpp:180
std::string exit_comment
Definition engine.hpp:163
execution::CloseCause close_cause
Definition engine.hpp:193
uint64_t entry_incarnation
Definition engine.hpp:183
int64_t exit_time
Definition engine.hpp:152
std::string entry_id
Definition engine.hpp:161
double exit_price
Definition engine.hpp:154
std::string exit_id
Definition engine.hpp:164
std::string entry_comment
Definition engine.hpp:162
int64_t entry_time
Definition engine.hpp:151
double entry_price
Definition engine.hpp:153
double max_drawdown
Definition engine.hpp:179
void store(bool v, std::memory_order order)
Definition engine.hpp:1754
AbortFlag & operator=(const AbortFlag &) noexcept
Definition engine.hpp:1745
ExecutionConsumerSlot & operator=(ExecutionConsumerSlot &&other) noexcept
Definition engine.hpp:2227
ExecutionConsumerSlot & operator=(const ExecutionConsumerSlot &other) noexcept
Definition engine.hpp:2217
ExecutionConsumerSlot(const ExecutionConsumerSlot &other) noexcept
Definition engine.hpp:2215