PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
pine_adapter.hpp
Go to the documentation of this file.
1#pragma once
2
3// Pine-only lowering facts. This header deliberately sits above the generic
4// native-host surface: no native kernel type learns a Pine source identifier,
5// sizing convention, or lifecycle vocabulary from it.
20#include <algorithm>
21#include <cassert>
22#include <cstddef>
23#include <cstdint>
24#include <functional>
25#include <iterator>
26#include <limits>
27#include <map>
28#include <memory>
29#include <optional>
30#include <stdexcept>
31#include <string>
32#include <string_view>
33#include <unordered_map>
34#include <unordered_set>
35#include <utility>
36#include <vector>
37#include <array>
38namespace pineforge::source {
39
40// Pine entry ids and from_entry values are source strings, never a
41// kernel-owned identifier type.
42using SourceId = std::string;
43
45class PineScheduler;
46namespace detail {
47class PositionTailSpill;
48class PositionTailSummary;
49struct CommandScratchRun;
50}
51
52// The v4 digest folds the adapter's live state -- the placement rows it
53// still holds, compact records of the erased ones, the pending queues -- and
54// running digests of its append-only logs, each element folded once, so the
55// extension costs what is live per read, not what the run has done.
56// This retirement representation also covers terminal exit fills, which
57// fold conditionally, and wholly settled bracket families use the explicit
58// pineforge-settled-bracket-families/v1 subdomain (native-feature-parity.md).
59inline constexpr char kSourceAdapterDomain[] = "pineforge-source-adapter/v4";
60
61// The adapter's forced-close vocabulary. These strings are TradingView report
62// shape, so they are written and read entirely inside the source layer: the
63// kernel records execution::CloseCause instead of decoding them; the source
64// layer supplies the corresponding report labels.
65inline constexpr char kMarginCallLabel[] = "__margin_call__";
66inline constexpr char kIntradayLossComment[] = "Close Position (Max intraday Loss)";
67// The published C contract matched this PREFIX, not the whole comment, so the
68// classifier keeps matching the prefix.
69inline constexpr char kFillCapCommentPrefix[] =
70 "Close Position (Max number of filled orders";
71
72// TradingView's calc_on_order_fills cascade guard. It is a Pine literal, not a
73// kernel default: project() hands it to the generic cadence as
74// NativeRunSpec::max_recalculations_per_point, and the adapter's own first-open
75// execution chain keeps refusing at the same count.
76inline constexpr std::uint32_t kCoofLoopGuard = 1U << 20;
77
80 bool calc_on_order_fills = false;
81 double initial_capital = 1000000.0;
82 int default_qty_type = static_cast<int>(QtyType::FIXED);
83 double default_qty_value = 1.0;
84 int pyramiding = 1;
85 double commission_value = 0.0;
87 int slippage = 0;
88 double margin_long = 100.0;
89 double margin_short = 100.0;
91 bool src_series_active = false;
92};
93
95 double initial_capital = std::numeric_limits<double>::quiet_NaN();
96 double commission_value = std::numeric_limits<double>::quiet_NaN();
97 double default_qty_value = std::numeric_limits<double>::quiet_NaN();
98 int pyramiding = -1;
99 int slippage = -1;
105};
106
107// One installed feed of another symbol as the adapter folds it (hash_state):
108// its key and its sizes. Its bars are never staged here: the host hands
109// them to the projected spec for its configure.
111 std::string instrument{};
112 std::string tf{};
113 std::uint64_t bars = 0;
114 std::uint64_t columns = 0;
115};
116
117// Value snapshot supplied by PineStrategyHost at the begin boundary. It owns
118// every source-side value the adapter needs to form a run spec; none of these
119// values are retained by generic native code.
123 std::string chart_timezone{};
124 double account_fx = 1.0;
125 std::vector<std::int64_t> account_fx_effective_from_ms{};
126 std::vector<double> account_fx_per_quote{};
127 std::optional<double> quantity_grid{};
128 // Other symbols' feeds the host installed for its request sites,
129 // by key and size; the host lends their bars to the spec.
130 std::vector<StagedInstrumentFeed> instrument_feeds{};
131};
132
133enum class PineOrderFamily : std::uint8_t {
134 Entry = 0,
135 Close = 1,
140 Order = 6,
142 Risk = 8,
143};
144
146 double limit = std::numeric_limits<double>::quiet_NaN();
147 double stop = std::numeric_limits<double>::quiet_NaN();
148 double trail_points = std::numeric_limits<double>::quiet_NaN();
149 double trail_offset = std::numeric_limits<double>::quiet_NaN();
150 double trail_price = std::numeric_limits<double>::quiet_NaN();
151 double profit_ticks = std::numeric_limits<double>::quiet_NaN();
152 double loss_ticks = std::numeric_limits<double>::quiet_NaN();
153};
154
155enum class PineCancellationCause : std::int32_t {
156 None = 0,
161};
162
165 std::int32_t state = 0;
166 std::int32_t close_claim_release = 0;
167 std::uint64_t source_incarnation = 0;
168 std::int64_t source_sequence = 0;
169 std::uint64_t target_incarnation = 0;
170 std::int64_t target_owner = 0;
171 std::uint64_t target_revision = 0;
172 double close_claim_consumed = std::numeric_limits<double>::quiet_NaN();
173 double close_claim_retired = std::numeric_limits<double>::quiet_NaN();
174};
175
177 double equity = std::numeric_limits<double>::quiet_NaN();
178 double price = std::numeric_limits<double>::quiet_NaN();
179 double fx = std::numeric_limits<double>::quiet_NaN();
180 double mark = std::numeric_limits<double>::quiet_NaN();
181 double frozen_units = std::numeric_limits<double>::quiet_NaN();
182 bool at_fill = false;
183 // strategy.equity at `mark`: every open entry fee charged, the cash ones
184 // included, which `equity` restores for the money gates. Recorded only
185 // where a percent-of-equity default quantity converts it -- a cash
186 // commission (default_sizing_reserves_cash_fee); NaN everywhere else.
187 double strategy_equity = std::numeric_limits<double>::quiet_NaN();
188};
189
190// Source placement evidence keyed by the native request handle, read by the
191// const terms/precommit/projection methods. It is written at the
192// submit/replace/applied boundaries, and a retained row is also rewritten in
193// place after them -- a filled entry's row when its first exit is attached,
194// and the adapter's whole-table scans (suspending, reviving or preserving
195// brackets) -- so a row is not immutable evidence of its placement.
200 std::string comment{};
201 std::string oca_name{};
202 int oca_type = 0;
203 int qty_type = -1;
204 double requested_qty = std::numeric_limits<double>::quiet_NaN();
205 // Live projection of a generic OCA reduction. The original source
206 // operand above remains immutable; this receipt-backed value is only for
207 // the public pending projection.
208 double projection_remaining_qty = std::numeric_limits<double>::quiet_NaN();
209 double qty_percent = std::numeric_limits<double>::quiet_NaN();
210 bool is_long = true;
211 bool immediately = false;
212 bool opening = false;
213 // A terminal Applied fill of an origin-free limit with no protection or
214 // replacement definition. Only K1's narrow filled-limit exception needs
215 // this receipt. A new incarnation clears it even on a copied lifecycle.
216 bool filled_exit = false;
217 bool deferred_cohort = false;
219 // A strategy.exit issued while its from_entry had neither an open trade
220 // nor an entry order waiting to fill, which TradingView voids. Its request
221 // waits in the book as before; the entry's opening withdraws it
222 // (withdraw_void_exits), and a re-issue made once the entry exists takes
223 // a new place in the exit queue rather than this one's.
224 bool void_issue = false;
225 // A strategy.exit without from_entry called while no position was held
226 // and no entry order of any id was working, which TradingView voids too
227 // (MarginOpeningSwitches::void_flat_global_exit). Its request waits in
228 // the book as a void call's does; a market entry's opening check never
229 // gives way to it and its fill withdraws it, a stop or limit entry's
230 // fill clears the flag (settle_void_global_exits).
231 bool void_global_issue = false;
232 // A strategy.exit called while the position was open and its from_entry
233 // held no lot but had an entry order working: TradingView binds it to the
234 // entry id, so it waits for that id's next fill through closes, the flat
235 // and a cancel and re-entry of the id. Cleared when the id fills, which
236 // binds the exit to that fill's lot (ExitBindingRuleSwitches).
237 bool pending_bound_exit = false;
238 // A leg of one entry's bracket of a from_entry "" exit that covers the
239 // position entry by entry (ExitBindingRuleSwitches::
240 // global_exit_per_entry_levels): the incarnation of the entry order whose
241 // fill price set its level, and that entry's quantity, which is all the
242 // leg closes. Zero for every other row.
243 std::uint64_t per_entry_origin = 0;
244 double per_entry_units = std::numeric_limits<double>::quiet_NaN();
245 // Sized when it was placed and executed as a plain market transaction:
246 // a margin call or close whose position shrank before it filled crosses
247 // zero and opens the difference on the other side
248 // (close_point_margin_call).
249 bool crosses_zero = false;
250 // A follow-up unit queued at a close for the next open: the fill of the
251 // call it follows (close_point_follow_up).
252 double follow_up_fill = std::numeric_limits<double>::quiet_NaN();
253 // A call rested at a path point before the bar's adverse extreme
254 // (schedule_margin_call_path).
258 double frozen_market_own_units = std::numeric_limits<double>::quiet_NaN();
259 double frozen_market_transaction_units = std::numeric_limits<double>::quiet_NaN();
262 // Command-boundary facts read immutably by the fill-time terms and
263 // precommit policies.
264 bool direction_gate = false;
269 bool reverse_to = false;
270 bool replaced_opening = false;
273 double frozen_reversal_transaction = std::numeric_limits<double>::quiet_NaN();
274 std::int64_t placement_cycle = 0;
275 std::uint64_t sequential_group = 0;
276 std::uint8_t sequential_rank = 0;
280 std::int32_t preserved_close_all_bar = -1;
281 // Explicit bracket legs retain the source opening provenance that caused
282 // their submission. A pending parent rejected at a later candidate can
283 // then retire only its own deferred legs.
285 std::uint64_t source_sequence = 0;
286 std::uint64_t command_ordinal = 0;
287 std::uint64_t placement_open_epoch = 0;
288 // Command-boundary order is retained separately from native submission
289 // order: deferred source commands may materialize after a later command.
290 std::uint64_t command_sequence = 0;
291 // Source close-callsite lowering facts. They describe one admitted
292 // script-evaluation command; the native request remains the sole
293 // executable order. `close_first_id` is the id the fill books against in
294 // the close ledger (book_close_ledger): the first call of the call site
295 // on the bar, whatever id a later call of the site sized the order for.
296 std::uint64_t close_callsite_token = 0;
297 std::uint32_t close_batch_calls = 0;
299 // Retired with the reservation model: nothing sets
300 // these six any more, and each keeps its default so the v4 placement
301 // fold keeps its layout.
302 double close_first_target = 0.0;
308 std::int64_t placement_script_open_ms = 0;
309 std::int64_t placement_sub_open_ms = 0;
310 // Immutable C-row projection facts. These are source placement facts,
311 // not a second executable order: the native request remains the sole
312 // owner of matching, trigger state, and terminal receipts.
313 std::int32_t projection_created_bar = -1;
314 // Set when a source command pinned projection_created_bar itself, so a
315 // later submit of the same instance (a bracket leg materialized when its
316 // parent entry applies) must not re-stamp it to the submitting bar.
318 std::int32_t projection_position_side = static_cast<std::int32_t>(PositionSide::FLAT);
320 // A close-first entry (MarginRuleSwitches::close_first_admission): the
321 // script called strategy.close(<held id>) or strategy.close_all() of the
322 // position this entry opposes earlier on the entry's own script bar.
323 // Recorded once, at the entry() call; both admission halves read it.
324 bool close_first_entry = false;
325 // A reversal the placement half refused at its signal (rule 5);
326 // it stays placed so the script's later close of the held id is an
327 // ordinary close, and drops at its fill.
331 std::uint64_t projection_predecessor = 0;
332 // The source_sequence of the oldest row reached by following
333 // projection_predecessor from this one (this row's own when it has
334 // none), fixed when the row is remembered. The chain's
335 // older rows may since have been erased, and this is what the terminal
336 // explicit-market policy read by walking them.
337 std::uint64_t chain_origin_sequence = 0;
341 // A flat child declared before a fresh priced parent was already visited
342 // (and skipped) by the legacy whole-bar broker pass. Keep that source
343 // chronology outside the native path matcher until the post-calculation
344 // point of the parent's fill bar.
351 // An adapter-owned immediate source policy may resolve a generic
352 // host-sized close at a previously established broker path price. The
353 // request remains owned and settled by the native core; only its
354 // immutable terms fact is source-specific.
355 double forced_execution_price = std::numeric_limits<double>::quiet_NaN();
356 double forced_waypoint_print = std::numeric_limits<double>::quiet_NaN();
359 double projection_default_stop_equity = std::numeric_limits<double>::quiet_NaN();
360 double projection_default_stop_signal_close = std::numeric_limits<double>::quiet_NaN();
361 double projection_explicit_equity = std::numeric_limits<double>::quiet_NaN();
362 double projection_explicit_signal_close = std::numeric_limits<double>::quiet_NaN();
363 double projection_affordability_equity = std::numeric_limits<double>::quiet_NaN();
364 double projection_affordability_signal_price = std::numeric_limits<double>::quiet_NaN();
365 double projection_affordability_held_qty = std::numeric_limits<double>::quiet_NaN();
368 // Resolved absolute trail activation used by the source fill policy when
369 // trail_points is lowered after its parent opening becomes live.
370 double trail_activation_level = std::numeric_limits<double>::quiet_NaN();
371 // A source reissue that changes only trail_offset keeps the already-seen
372 // raw best while the generic request itself remains live.
373 double retained_trail_best = std::numeric_limits<double>::quiet_NaN();
374 // Immutable source command observation used by the public admission
375 // journal/mirror. It never owns or drives matching.
377 // Source order policy receipts. They are immutable placement/live facts owned by
378 // the adapter, never a second executable pending-order representation.
390 std::int32_t coof_cascade_seg_i = -1;
393 double paired_flat_market_own_qty = std::numeric_limits<double>::quiet_NaN();
394 double paired_flat_market_signal_close = std::numeric_limits<double>::quiet_NaN();
395 double paired_flat_market_signal_equity = std::numeric_limits<double>::quiet_NaN();
396 double paired_flat_market_signal_margin_pct = std::numeric_limits<double>::quiet_NaN();
397 double paired_flat_market_signal_pointvalue = std::numeric_limits<double>::quiet_NaN();
398 double paired_flat_market_signal_fx = std::numeric_limits<double>::quiet_NaN();
400 double paired_flat_market_transaction_qty = std::numeric_limits<double>::quiet_NaN();
401 std::int32_t signal_close_mc_bar = -1;
403 std::uint64_t signal_close_mc_fill_seq = 0;
404 double signal_close_mc_remaining_qty = std::numeric_limits<double>::quiet_NaN();
409};
410
411class PlacementTable;
412
413namespace detail {
414// The tombstone audit of the placement table. A build whose
415// every TU is compiled with -DPINEFORGE_PLACEMENT_AUDIT=1 (CMAKE_CXX_FLAGS)
416// keeps every erased row aside instead of freeing it, and records every read
417// that would have reached one: a lookup of an erased incarnation, and a
418// whole-table scan whose predicate an erased row satisfies. Readers see
419// exactly what an ordinary build sees; the audit only counts, and prints its
420// counts at exit (to $PINEFORGE_PLACEMENT_AUDIT_LOG, else stderr). One
421// process-wide record: audit single-threaded runs. Not installed API.
422void placement_audit_erase(const PlacementTable* table, std::uint64_t incarnation,
423 PlacementSnapshot&& row);
424void placement_audit_forget(const PlacementTable* table) noexcept;
425void placement_audit_miss(const PlacementTable* table, std::uint64_t incarnation,
426 unsigned line) noexcept;
427void placement_audit_scan_hit(const PlacementTable* table, std::uint64_t incarnation,
428 unsigned line) noexcept;
429// The erased rows the audit keeps for `table`, ascending, or null.
430const std::vector<std::pair<std::uint64_t, const PlacementSnapshot*>>*
432
433// The TradingView margin rules pinned on the tapes under
434// tests/fixtures/margin_call_rules, tests/fixtures/margin_ledger_rules and
435// tests/fixtures/admission_rules, one switch each so a regression bisects per
436// rule; all on, only tests change one. Process-wide, read by every adapter;
437// not installed API and no strategy input reaches it.
438//
439// The admission at the signal and at the fill is two switches:
440// unified_placement is its placement half (rules 2 and 5 at the signal close
441// for default orders and reversals; the block in entry() and the signal half
442// inside resolve_terms) and fill_price_recheck its fill half (sig10(sig10(E) /
443// (Q m)) against the slipped tick-built fill quote, a whole drop).
444// close_first_admission exempts a close-first entry -- one the script placed
445// behind its own strategy.close(<held id>) or strategy.close_all() of the
446// position it opposes, on the same bar -- from rule 5 and from the fill half;
447// rule 2 still judges it.
449 bool decimal_sizing = true; // shortest decimal of the quotient, floored
450 bool slipped_signal_admission = true; // placement: money at c', price at c' +- slip
451 bool unified_placement = true; // placement half: rules 2 and 5 at the signal
452 bool fill_price_recheck = true; // fill half: the price-scale re-check
453 bool close_first_admission = true; // a close-first entry: rule 2 only
454 bool gain_loss_money = true; // realized money as the G + L sums
455 bool dust_unit_call = true; // a dust restore calls one unit
456 bool long_open_close_checks = true; // a long's one-unit call at open, low and close
457 bool negative_free_cash_call = true; // negative free cash calls the unit too
458 bool coof_next_point_close = true; // COOF close after a call at the next point
459 bool close_point_reversal = true; // a close_all sized before a close-point call
460 bool pooc_fee_sizing = true; // POOC default sizing on the slipped, fee-grossed unit
461 // At the open, the orders the opening print executes fill before the
462 // margin check runs there.
464 // The point order counts a stop or limit leg of a strategy.exit that also
465 // trails (trail_points, trail_offset or trail_price) like any other
466 // protective exit: the trail leaves the reached leg's order unchanged.
468 // The pyramiding cap counts open close-ledger records, one per entry
469 // fill not yet booked, not physical lots.
471 // A partial strategy.exit child that filled is never revived for the
472 // entry incarnation it filled under.
474 // Rule 2's exact tie judges a reversal beside resting exit stop/trail legs.
476};
478
479// One switch per TradingView rule pinned on the tapes under
480// tests/fixtures/explicit_qty_floor and tests/fixtures/pooc_close_bar_fills, so
481// a regression bisects per rule. All on; only tests change one. Process-wide,
482// read by every adapter and source host; not installed API, and no strategy
483// input reaches it. The session clock rule keeps its own switch (session_time.hpp).
485 // An explicit strategy.entry quantity the grid floor would snap UP to a
486 // grid point (within 1e-6 of a lot below it) is floored on its shortest
487 // decimal instead: 3124.9999999999973 trades 3124.
489 // Script-visible strategy.equity / strategy.openprofit mark an open
490 // position at the tick-built close floor(close / mintick + 0.5) * mintick
491 // when the close is off the tick grid (NYSE:F 15m half-cent closes).
492 bool equity_tick_mark = true;
493 // Under process_orders_on_close, a stop entry and its re-issued bracket
494 // that both fill inside one bar settle before the close-time script even
495 // when the book still holds an exit whose entry is neither open nor
496 // working (flush_pending_bracket_legs).
499};
501
502// One switch per part of TradingView's strategy.exit binding rule, pinned on
503// the tapes under tests/fixtures/exit_binding, the pending-* tapes of
504// tests/fixtures/global_exit_children, tests/fixtures/cross_side_exit and
505// tests/fixtures/same_side_exit and tests/fixtures/per_entry_exit, so a
506// regression bisects per part. A strategy.exit binds when it is called: to
507// its entry's open lots, else to the entry id while an order of it is
508// working, else the call is ignored. Every part but
509// global_exit_binds_held_position and global_exit_per_entry_levels acts only
510// with margin requirements off (margin_long = margin_short = 0), the
511// setting of their tapes. All on; only
512// tests change one. Process-wide, read by every adapter; not installed API,
513// and no strategy input reaches it.
515 // An exit called in position for an entry id with no lot but a limit or
516 // stop order working is bound to the id: a close or the flat does not
517 // remove it, a cancel of the id's order keeps it for the id's next order,
518 // and the id's next fill binds it (PlacementSnapshot::pending_bound_exit).
520 // A global exit called while the book is flat binds to the limit and
521 // stop entry orders working then: it waits for their fill instead of
522 // ending on the flat book.
524 // Under process_orders_on_close an entry order of the side a close
525 // flattens, resting from an earlier bar, survives that close when it is
526 // a stop, as a limit does.
528 // Under process_orders_on_close a priced add of an id holding no lot,
529 // still at the pyramiding cap once its bar's closes are done, leaves the
530 // book at the next opening, judged at that opening only.
532 // A global exit called while a position is held binds to that position,
533 // whatever the margin setting: an entry order of either side working
534 // beside it (resting, placed earlier in the same calculation, or queued
535 // by a fill recalculation) lends it neither its side nor its price
536 // basis, nor a wait for its fill. Its limit and stop rest on the held
537 // side and a profit, loss or trail leg resolves against the held
538 // position (PineExecutionAdapter::exit()).
540 // Such a held global exit with relative legs (profit, loss) covers the
541 // position entry by entry once it holds two entries: each entry has its
542 // own bracket at levels from its own fill price, an entry filling after
543 // the call gets its bracket at its fill (live for the rest of that bar),
544 // each bracket fills once and closes the oldest open trade, and a book
545 // that goes flat ends the brackets left (tests/fixtures/per_entry_exit;
546 // PineExecutionAdapter::per_entry_exit_call()). Pinned without
547 // calc_on_order_fills or process_orders_on_close, on the chart's own bars,
548 // under the FIFO close rule.
550};
552
553// One switch per part of TradingView's same-pass position view under
554// process_orders_on_close, pinned on the tapes under
555// tests/fixtures/pooc_close_fill_view: an order that fills at the bar's close
556// stays invisible to the script pass that placed it. Both parts act only at
557// the re-issued exit that exit() fills at its call (its current-close path,
558// stop or limit leg); an ordinary close_all keeps its own size freeze
559// Freeze-aware. All on; only tests change one. Process-wide, read by every adapter;
560// not installed API, and no strategy input reaches it.
562 // The rest of the pass reads the pre-fill strategy.position_size (and so
563 // strategy.position_size[1] < 0 and strategy.position_size == 0 stays
564 // false until the next bar).
566 // ... and the pre-fill strategy.position_avg_price, while the frozen
567 // view holds. Needs current_exit_keeps_position_view.
569};
571} // namespace detail
572
573#ifndef PINEFORGE_PLACEMENT_AUDIT
574#define PINEFORGE_PLACEMENT_AUDIT 0
575#endif
576
577// Source placement evidence, keyed by the native request handle's
578// incarnation. Handles are monotonically allocated by the native core, so the
579// rows are held in incarnation order: a dense window of the recent
580// incarnations, indexed directly, and below it the few older rows that
581// outlived it. A row lives in a heap node of its own, so a reference to it
582// survives every insert and every erase of another row.
583//
584// A row is erased once the request it describes is no longer
585// working and nothing can read it again (PineExecutionAdapter::
586// erase_retired_rows), so the table holds what is live, not everything the
587// run ever placed. What later logic still consults of an erased row is kept
588// here in compact records -- the per-id command-sequence minima and the
589// non-margin source ids -- and by the adapter itself (bracket families,
590// cohorts). high_water() still names the largest incarnation ever inserted.
592public:
593 template<bool IsConst>
594 class Iterator {
595 using Owner = std::conditional_t<IsConst, const PlacementTable, PlacementTable>;
596 using Snapshot = std::conditional_t<IsConst, const PlacementSnapshot, PlacementSnapshot>;
597 struct Reference {
598 Reference(std::uint64_t key, Snapshot& value) : first(key), second(value) {}
599
600 std::uint64_t first = 0;
601 Snapshot& second;
602 };
603
604 public:
605 // libstdc++ dispatches std::find_if/any_of on iterator_traits; a
606 // proxy iterator must still publish the five Cpp17 typedefs (libc++
607 // tolerated their absence, GCC on the Cloud Run runner did not).
608 using iterator_category = std::forward_iterator_tag;
609 using value_type = Reference;
610 using difference_type = std::ptrdiff_t;
611 using pointer = Reference*;
612 using reference = Reference;
613
614 Iterator() = default;
615
616 Reference operator*() const { return {key_, borrow()}; }
617 Reference* operator->() const {
618 reference_.emplace(key_, borrow());
619 return &*reference_;
620 }
622 const auto next = owner_->next_after(key_);
623 key_ = next.first;
624 row_ = next.second;
625 return *this;
626 }
628 Iterator before = *this;
629 ++*this;
630 return before;
631 }
632 bool operator==(const Iterator& other) const noexcept {
633 return owner_ == other.owner_ && key_ == other.key_;
634 }
635 bool operator!=(const Iterator& other) const noexcept { return !(*this == other); }
636
637 private:
638 friend class PlacementTable;
639 Iterator(Owner* owner, std::uint64_t key, Snapshot* row)
640 : owner_(owner), key_(key), row_(row) {}
641 Snapshot& borrow() const {
642 if constexpr (!IsConst) ++owner_->pending_observer_version_;
643 return *row_;
644 }
645 Owner* owner_ = nullptr; // Nodes stay stable; key 0 is end().
646 std::uint64_t key_ = 0;
647 Snapshot* row_ = nullptr;
648 mutable std::optional<Reference> reference_;
649 };
650
653
654 PlacementTable() = default;
655 explicit PlacementTable(std::size_t cache_bytes) : erased_minima_(cache_bytes) {}
656 PlacementTable(std::size_t cache_bytes, std::size_t sequence_bytes)
657 : sequence_cache_capacity_(CacheCapacity::from_payload(512, sequence_bytes)),
658 erased_minima_(cache_bytes) {}
660 : sequence_cache_capacity_(sequence), erased_minima_(minima) {}
661 // Run setup only, after clear() and before directory/slab binding.
663 erased_minima_.set_cache_capacity(minima);
664 sequence_cache_capacity_ = sequence;
665 sequence_index_.reset();
666 }
669 PlacementTable(PlacementTable&&) noexcept = default;
670 PlacementTable& operator=(PlacementTable&&) noexcept = default;
671
672 void set_auxiliary_tables_directory(const std::string& directory);
673 void bind_auxiliary_scratch_tables(const std::shared_ptr<::pineforge::detail::ScratchSlab>&);
675 std::uint64_t auxiliary_disk_bytes() const noexcept;
676 std::size_t size() const noexcept { return size_; }
677 std::size_t max_size() const noexcept { return window_.max_size(); }
678 // Largest incarnation ever inserted, erased rows included. Incarnations
679 // are monotone, so a handle above this mark at an observation time was
680 // unknown to every adapter collection populated before that observation.
681 std::uint64_t high_water() const noexcept { return high_water_; }
682 std::array<std::uint64_t, 5> sequence_work() const noexcept;
683 std::uint64_t erased_rows() const noexcept { return erased_rows_; }
684 // The window is reserved for the rows a run keeps live at once, not for
685 // every incarnation it will allocate: the table stays O(live).
686 void reserve(std::size_t count) {
687 window_.reserve(std::min<std::size_t>(count, kWindowReserve));
688 }
689 void clear() noexcept { ++pending_observer_version_;
690 window_.clear();
691 head_ = 0;
692 base_ = 1;
693 window_rows_ = 0;
694 old_.clear();
695 size_ = 0;
696 high_water_ = 0;
697 erased_rows_ = 0; visited_slots_ = 0;
698 erased_digest_ = kFoldBasis;
699 erased_minima_.clear(); sequence_index_.reset(); sequence_queries_ = sequence_rows_ = 0;
700 erased_foreign_ids_.clear();
701#if PINEFORGE_PLACEMENT_AUDIT
703#endif
704 }
705 std::uint64_t visited_slots() const noexcept { return visited_slots_; } // Observer only; never folded.
706 iterator begin() noexcept {
707 const auto first = next_after(0);
708 return iterator(this, first.first, first.second);
709 }
710 iterator end() noexcept { return iterator(this, 0, nullptr); }
711 const_iterator begin() const noexcept {
712 const auto first = next_after(0);
713 return const_iterator(this, first.first, first.second);
714 }
715 const_iterator end() const noexcept { return const_iterator(this, 0, nullptr); }
716 // The first row above `incarnation`.
717 const_iterator upper_bound(std::uint64_t incarnation) const noexcept {
718 const auto next = next_after(incarnation);
719 return const_iterator(this, next.first, next.second);
720 }
721 // Every row in incarnation order -- the order begin() walks: the older
722 // rows, then the window's -- read straight off the storage rather than
723 // re-found at every step. `visit(incarnation, row)` returns true to stop.
724 // For a whole-table read: no row is inserted or erased while it runs.
725 template<class Visit>
726 [[gnu::always_inline]] void visit_rows(Visit&& visit) const {
727 for (const auto& entry : old_) { ++visited_slots_;
728 if (visit(entry.first, static_cast<const PlacementSnapshot&>(*entry.second))) return;
729 }
730 for (std::size_t index = head_; index < window_.size(); ++index) {
731 const auto& slot = window_[index]; ++visited_slots_;
732 if (slot
733 && visit(base_ + static_cast<std::uint64_t>(index - head_),
734 static_cast<const PlacementSnapshot&>(*slot))) {
735 return;
736 }
737 }
738 }
739
740#if PINEFORGE_PLACEMENT_AUDIT
741 iterator find(std::uint64_t incarnation, unsigned line = __builtin_LINE()) noexcept {
742 PlacementSnapshot* row = locate(incarnation);
743 if (!row) {
744 detail::placement_audit_miss(this, incarnation, line);
745 return end();
746 }
747 return iterator(this, incarnation, row);
748 }
749 const_iterator find(std::uint64_t incarnation,
750 unsigned line = __builtin_LINE()) const noexcept {
751 const PlacementSnapshot* row = locate(incarnation);
752 if (!row) {
753 detail::placement_audit_miss(this, incarnation, line);
754 return end();
755 }
756 return const_iterator(this, incarnation, row);
757 }
758#else
759 iterator find(std::uint64_t incarnation) noexcept {
760 PlacementSnapshot* row = locate(incarnation);
761 return row ? iterator(this, incarnation, row) : end();
762 }
763 const_iterator find(std::uint64_t incarnation) const noexcept {
764 const PlacementSnapshot* row = locate(incarnation);
765 return row ? const_iterator(this, incarnation, row) : end();
766 }
767#endif
768
769 // Whether a row is held, as a fact about the table rather than a read of
770 // the row (the tombstone audit does not count it).
771 bool contains(std::uint64_t incarnation) const noexcept {
772 return locate(incarnation) != nullptr;
773 }
774
775 PlacementSnapshot& at(std::uint64_t incarnation) { ++pending_observer_version_;
776 PlacementSnapshot* row = locate(incarnation);
777 if (!row) throw std::out_of_range("source placement handle is absent");
778 return *row;
779 }
780 const PlacementSnapshot& at(std::uint64_t incarnation) const {
781 const PlacementSnapshot* row = locate(incarnation);
782 if (!row) throw std::out_of_range("source placement handle is absent");
783 return *row;
784 }
785
786 template<class... Args>
787 std::pair<iterator, bool> try_emplace(std::uint64_t incarnation, Args&&... args) { ++pending_observer_version_;
788 if (incarnation == 0 || incarnation == std::numeric_limits<std::uint64_t>::max()) {
789 throw std::length_error("source placement incarnation is out of range");
790 }
791 if (PlacementSnapshot* existing = locate(incarnation))
792 return {iterator(this, incarnation, existing), false};
793 std::unique_ptr<PlacementSnapshot> node;
794 if (spare_.empty()) {
795 node = std::make_unique<PlacementSnapshot>(std::forward<Args>(args)...);
796 } else {
797 // An erased row's node, reused: no allocation per placement in a
798 // run that retires as fast as it places. Every field is assigned.
799 node = std::move(spare_.back());
800 spare_.pop_back();
801 overwrite(*node, std::forward<Args>(args)...);
802 }
803 PlacementSnapshot* row = node.get(); prepare_place(incarnation);
804 change_sequences(incarnation, nullptr, row); place(incarnation, std::move(node));
805 ++size_;
806 high_water_ = std::max(high_water_, incarnation);
807 return {iterator(this, incarnation, row), true};
808 }
809
810 // Publish the derived identity update before replacing the retained row.
811 // Rewritten predecessors never become erased history.
812 void replace(std::uint64_t incarnation, PlacementSnapshot&& snapshot) {
813 auto& row = at(incarnation); change_sequences(incarnation, &row, &snapshot); row = std::move(snapshot);
814 }
815
816 // Borrow the complete retirement workset before touching any row, cohort
817 // side, bracket fact or digest. Duplicate single/pair keys are counted once.
818 template<class Enumerate>
819 void preflight_erased_rows(const Enumerate& enumerate) const {
820 erased_minima_.preflight_keys([&](const auto& emit) {
821 enumerate([&](const PlacementSnapshot& row) {
822 emit(Minima::KeyView{row.source_id, std::nullopt});
823 emit(Minima::KeyView{row.source_id, std::string_view(row.from_entry)});
824 });
825 });
826 }
827
828 // Erases a row. The compact records keep what later logic reads of it:
829 // its command sequence under its (source_id, from_entry) and, for a
830 // non-margin row, its source id. Returns false when no row is there.
831 bool erase(std::uint64_t incarnation) { ++pending_observer_version_;
832 const PlacementSnapshot* row = locate(incarnation);
833 if (!row) return false;
834 preflight_erased_rows([&](const auto& emit) { emit(*row); });
835 // A failed derived write publishes no placement or erased-history change.
836 change_sequences(incarnation, row, nullptr);
837 try { fold_sequence(erased_minima_, *row); } catch (...) { sequence_index_.reset(); throw; }
838 std::unique_ptr<PlacementSnapshot> node = take(incarnation);
839 remember_erased(*node);
840 --size_;
841 ++erased_rows_;
842#if PINEFORGE_PLACEMENT_AUDIT
843 detail::placement_audit_erase(this, incarnation, std::move(*node));
844#else
845 if (spare_.size() < kSpareNodes) spare_.push_back(std::move(node));
846#endif
847 return true;
848 }
849
850 // Drops the window's empty prefix and, when the window has grown sparse
851 // (a long-lived row holding its front while the rows behind it were
852 // erased), moves its older rows below it, so the window stays about as
853 // long as what is live. Moves no node: every reference stays valid.
854 void compact();
855
856 // Smallest command_sequence among the rows ever inserted whose source_id
857 // is `source_id`: over the rows bound to `from_entry` when any is, else
858 // over all of them; UINT64_MAX when no row carried the id. The retained
859 // rows have a replacement-safe backed identity index; erased minima stay
860 // separate. An explicitly present UINT64_MAX pair still falls back.
861 std::uint64_t command_sequence_for(const std::string& source_id,
862 const std::string& from_entry) const;
863private: friend class PineExecutionAdapter; bool exit_sequences_certified() const noexcept; void begin_exit_sequence_certificate(); void watch_exit_sequence(const SourceId&, const SourceId&); void certify_exit_sequences() noexcept; public:
864 // Whether an erased row that is not a margin close carried a source id
865 // other than `first` and `second`: what a scan for such a row over every
866 // placement ever made reads of the erased ones.
868 const SourceId& second) const noexcept {
869 for (const auto& id : erased_foreign_ids_)
870 if (id != first && id != second) return true;
871 return false;
872 }
873
874 // The v4 extension fold's view of the erased rows: how many, and a
875 // running digest of each one's identity (family, source id, from_entry,
876 // command sequence), folded once, when it left.
877 std::uint64_t erased_digest() const noexcept { return erased_digest_; }
878
879private:
880 static constexpr std::size_t kWindowReserve = 256;
881 static constexpr std::size_t kSpareNodes = 64;
882 static constexpr std::uint64_t kFoldBasis = 1469598103934665603ULL;
883 struct SequenceIndex; SequenceIndex& sequence_index() const; std::array<std::uint64_t, 2> retained_sequences(const SourceId&, const SourceId&) const;
884 // Single keys hold the minimum over any from_entry; pair keys hold the
885 // exact (source_id, from_entry) minimum. Their separate key domains keep
886 // an empty from_entry distinct from the any-entry summary. Neither the
887 // directory nor old keys allocate per-id RAM in the bounded backend.
889
890 // A reused node takes its new row in one assignment.
891 static void overwrite(PlacementSnapshot& node, PlacementSnapshot&& row) {
892 node = std::move(row);
893 }
894 static void overwrite(PlacementSnapshot& node, PlacementSnapshot& row) { node = row; }
895 static void overwrite(PlacementSnapshot& node, const PlacementSnapshot& row) { node = row; }
896 template<class... Args>
897 static void overwrite(PlacementSnapshot& node, Args&&... args) {
898 node = PlacementSnapshot(std::forward<Args>(args)...);
899 }
900
901 static void fold_sequence(Minima& index, const PlacementSnapshot& row) {
902 // Keep both original minima, including an explicitly present
903 // UINT64_MAX: command_sequence_for uses the same fallback for it.
904 // Recurring pairs update their old disk records in place.
905 index.preflight_keys([&](const auto& emit) {
906 emit(Minima::KeyView{row.source_id, std::nullopt});
907 emit(Minima::KeyView{row.source_id, std::string_view(row.from_entry)});
908 });
909 index.min_assign(row.source_id, row.command_sequence);
910 index.min_assign(row.source_id, row.from_entry, row.command_sequence);
911 }
912
913 PlacementSnapshot* locate(std::uint64_t incarnation) const noexcept {
914 if (incarnation >= base_) {
915 const std::uint64_t offset = incarnation - base_;
916 if (offset < window_.size() - head_)
917 return window_[head_ + static_cast<std::size_t>(offset)].get();
918 return nullptr;
919 }
920 const auto found = std::lower_bound(old_.begin(), old_.end(), incarnation,
921 [](const auto& entry, std::uint64_t key) { return entry.first < key; });
922 return found != old_.end() && found->first == incarnation ? found->second.get()
923 : nullptr;
924 }
925
926 // The first row above `after` (0 = the first row), or {0, null}.
927 std::pair<std::uint64_t, PlacementSnapshot*> next_after(std::uint64_t after) const noexcept {
928 if (after < base_) {
929 const auto found = std::upper_bound(old_.begin(), old_.end(), after,
930 [](std::uint64_t key, const auto& entry) { return key < entry.first; });
931 if (found != old_.end()) { ++visited_slots_; return {found->first, found->second.get()}; }
932 }
933 std::size_t index = after < base_ ? head_
934 : head_ + static_cast<std::size_t>(after - base_) + 1U;
935 for (; index < window_.size(); ++index) { ++visited_slots_;
936 if (window_[index])
937 return {base_ + static_cast<std::uint64_t>(index - head_), window_[index].get()};
938 }
939 return {0, nullptr};
940 }
941
942 void place(std::uint64_t incarnation, std::unique_ptr<PlacementSnapshot> node);
943 std::unique_ptr<PlacementSnapshot> take(std::uint64_t incarnation) noexcept;
944 void remember_erased(const PlacementSnapshot& row); void prepare_place(std::uint64_t incarnation);
945 void change_sequences(std::uint64_t, const PlacementSnapshot*, const PlacementSnapshot*);
946 // window_[head_ + i] holds incarnation base_ + i, or null; old_ holds the
947 // rows below base_, ascending.
948 std::vector<std::unique_ptr<PlacementSnapshot>> window_;
949 std::size_t head_ = 0;
950 std::uint64_t base_ = 1;
951 std::size_t window_rows_ = 0;
952 std::vector<std::pair<std::uint64_t, std::unique_ptr<PlacementSnapshot>>> old_;
953 std::size_t size_ = 0;
954 std::uint64_t high_water_ = 0;
955 std::uint64_t erased_rows_ = 0; mutable std::uint64_t visited_slots_ = 0;
956 std::uint64_t erased_digest_ = kFoldBasis;
957 mutable std::shared_ptr<SequenceIndex> sequence_index_; mutable std::uint64_t sequence_queries_ = 0, sequence_rows_ = 0;
958 // Physical policy only; the derived backing remains lazy and rebuildable.
959 CacheCapacity sequence_cache_capacity_ = CacheCapacity::from_payload(512, 64 * 1024);
960 Minima erased_minima_;
961 // Up to three distinct source ids of erased non-margin rows: as many as
962 // erased_non_margin_id_outside() can ever need.
963 std::vector<SourceId> erased_foreign_ids_;
964 // Spare nodes and the observer epoch are derived, outside broker state.
965 friend class PineExecutionAdapter;
966 std::uint64_t pending_observer_version_ = 0;
967 std::vector<std::unique_ptr<PlacementSnapshot>> spare_;
968};
969
970// Acceptance-ordered opening roster. size()/digest() describe all accepted
971// members; members()/iteration expose the retained window [base(), size()).
972// The adapter retires a released prefix once no live placement names it.
973// Global positions never change. Indexing outside the retained window is
974// an engine_invariant, including historical positions below base().
976public:
977 using const_iterator = std::vector<native_order::RequestHandle>::const_iterator;
979 std::vector<native_order::RequestHandle>::const_reverse_iterator;
980
982 auto& positions = positions_[handle.incarnation];
983 positions.push_back(size());
984 try {
985 items_.push_back(handle);
986 } catch (...) {
987 positions.pop_back();
988 throw;
989 }
990 digest_ = (digest_ ^ handle.incarnation) * 1099511628211ULL;
991 }
992 const std::vector<native_order::RequestHandle>& members() const noexcept { return items_; }
993 // A running digest of the members' incarnations, each
994 // folded once, when it was appended (the v4 extension fold's view).
995 std::uint64_t digest() const noexcept { return digest_; }
996 static OriginRoster from_retired(std::size_t count, std::uint64_t digest);
997 const native_order::RequestHandle& operator[](std::size_t global) const;
998 const native_order::RequestHandle& back() const { return (*this)[size() - 1]; }
999 bool empty() const noexcept { return size() == 0; }
1000 std::size_t size() const noexcept { return base_ + items_.size(); }
1001 const_iterator begin() const noexcept { return items_.begin(); }
1002 const_iterator end() const noexcept { return items_.end(); }
1003 const_reverse_iterator rbegin() const noexcept { return items_.rbegin(); }
1004 const_reverse_iterator rend() const noexcept { return items_.rend(); }
1005 std::size_t base() const noexcept { return base_; }
1006 // Ascending global positions of retained entries equal to `handle`.
1007 void append_positions_of(const native_order::RequestHandle& handle,
1008 std::vector<std::size_t>& out) const;
1009 // Retire [base(), global); the adapter proves release and reachability.
1010 void retire_before(std::size_t global);
1011 // size()/digest() survive retirement; references and iterators may move.
1012 // empty() describes the logical history, members().empty() the window.
1013
1014 // Ascending roster positions of the entries carrying `incarnation`, or
1015 // null when none does. Allocation-free.
1016 const std::vector<std::size_t>* positions_of(std::uint64_t incarnation) const noexcept {
1017 const auto found = positions_.find(incarnation);
1018 return found == positions_.end() ? nullptr : &found->second;
1019 }
1020 std::size_t heap_bytes_estimate() const noexcept {
1021 auto bytes = items_.capacity() * sizeof(native_order::RequestHandle)
1022 + positions_.bucket_count() * sizeof(void*);
1023 for (const auto& row : positions_)
1024 bytes += sizeof(decltype(positions_)::value_type) + 2 * sizeof(void*)
1025 + row.second.capacity() * sizeof(std::size_t);
1026 // The per-run session key inside each handle is a shared run-level
1027 // object rather than per-row storage. This estimate counts only
1028 // the handle slots and the incarnation-to-position index above.
1029 return bytes;
1030 }
1031
1032private:
1033 std::vector<native_order::RequestHandle> items_;
1034 std::size_t base_ = 0; // Storage offset; size() already hashes the total.
1035 std::unordered_map<std::uint64_t, std::vector<std::size_t>> positions_;
1036 std::uint64_t digest_ = 1469598103934665603ULL;
1037};
1038
1039// Ordered member list of one source bracket family. Members are appended per
1040// accepted leg and only removed when cancelled, so the list retains every
1041// historical leg while removals target the few live ones near its tail.
1042//
1043// The list is still every leg the family placed, in order --
1044// strategy.exit's bracket cancel asks the kernel to cancel each of them, and a
1045// leg that no longer works leaves a NotWorking receipt in the command history
1046// -- but its settled prefix, the members whose placement rows were erased,
1047// is kept as the members' incarnations, zigzag-varint deltas of one run
1048// identity, about a byte each, beside a running digest of them. The members
1049// from the first one whose row the table still holds onward stay as handles:
1050// only they can be removed (a removal targets a live leg) or read (a
1051// consumed-leg question reads a leg's row). A member whose row was erased
1052// answered the consumed-leg question for good; the (leg kind, origin) pair
1053// it answered for is kept in consumed_ while that origin can still be asked
1054// about.
1055//
1056// An erased member behind one whose row the table still holds
1057// is parked the same way, in a run between the two handles it sits between
1058// (Parked: its place, count, digest and varint deltas), so the handles left
1059// are the members whose rows are retained. An early member a pin keeps (a
1060// leg of the current position cycle) no longer holds every later member as a
1061// handle, and the v4 fold, which reads the handles in full and each run as
1062// its place, count and digest, costs what is retained, not what was placed.
1064 static constexpr std::uint64_t kDigestBasis = 1469598103934665603ULL;
1065 static std::uint64_t fold(std::uint64_t digest, std::uint64_t value) noexcept {
1066 return (digest ^ value) * 1099511628211ULL;
1067 }
1068 static void put(std::vector<std::uint8_t>& bytes, std::uint64_t& last,
1069 std::uint64_t incarnation) {
1070 const auto delta = static_cast<std::int64_t>(incarnation) - static_cast<std::int64_t>(last);
1071 std::uint64_t zigzag = (static_cast<std::uint64_t>(delta) << 1)
1072 ^ static_cast<std::uint64_t>(delta >> 63);
1073 do {
1074 std::uint8_t byte = static_cast<std::uint8_t>(zigzag & 0x7FU);
1075 zigzag >>= 7;
1076 if (zigzag) byte |= 0x80U;
1077 bytes.push_back(byte);
1078 } while (zigzag);
1079 last = incarnation;
1080 }
1081 static std::uint64_t get(const std::vector<std::uint8_t>& bytes, std::size_t& offset,
1082 std::uint64_t previous) noexcept {
1083 std::uint64_t zigzag = 0;
1084 int shift = 0;
1085 for (;;) {
1086 const std::uint8_t byte = bytes[offset++];
1087 zigzag |= static_cast<std::uint64_t>(byte & 0x7FU) << shift;
1088 if (!(byte & 0x80U)) break;
1089 shift += 7;
1090 }
1091 const auto delta = static_cast<std::int64_t>(zigzag >> 1)
1092 ^ -static_cast<std::int64_t>(zigzag & 1U);
1093 return static_cast<std::uint64_t>(static_cast<std::int64_t>(previous) + delta);
1094 }
1095
1096 // Erased members between two retained ones: `at` handles of the working
1097 // tail come before them.
1098 struct Parked {
1099 std::size_t at = 0;
1100 std::size_t count = 0;
1101 std::uint64_t last = 0;
1102 std::uint64_t digest = kDigestBasis;
1103 std::vector<std::uint8_t> bytes;
1104 };
1105
1106public:
1107 // Every materialized member in roster order after the settled prefix,
1108 // with each parked run in its place. The settled prefix is represented by
1109 // its count and digest rather than by historical handle bytes.
1111 public:
1112 using iterator_category = std::forward_iterator_tag;
1114 using difference_type = std::ptrdiff_t;
1117
1118 const_iterator() = default;
1119 reference operator*() const { return current_; }
1120 pointer operator->() const { return &current_; }
1122 advance();
1123 return *this;
1124 }
1126 const_iterator before = *this;
1127 advance();
1128 return before;
1129 }
1130 bool operator==(const const_iterator& other) const noexcept {
1131 return owner_ == other.owner_ && hot_ == other.hot_ && run_ == other.run_
1132 && in_run_ == other.in_run_ && run_offset_ == other.run_offset_;
1133 }
1134 bool operator!=(const const_iterator& other) const noexcept { return !(*this == other); }
1135
1136 private:
1137 friend class BracketRoster;
1138 const_iterator(const BracketRoster* owner, bool at_end) : owner_(owner) {
1139 if (at_end) {
1140 hot_ = owner->hot_.size();
1141 run_ = owner->parked_.size();
1142 return;
1143 }
1144 load();
1145 }
1146 bool in_parked() const noexcept {
1147 return run_ < owner_->parked_.size() && owner_->parked_[run_].at == hot_;
1148 }
1149 void load() {
1150 if (in_parked()) {
1151 run_previous_ = get(owner_->parked_[run_].bytes, run_offset_, run_previous_);
1152 current_ = native_order::RequestHandle{owner_->run_, run_previous_};
1153 } else if (hot_ < owner_->hot_.size()) {
1154 current_ = owner_->hot_[hot_];
1155 }
1156 }
1157 void advance() {
1158 if (in_parked()) {
1159 if (++in_run_ == owner_->parked_[run_].count) {
1160 ++run_;
1161 in_run_ = 0;
1162 run_offset_ = 0;
1163 run_previous_ = 0;
1164 }
1165 } else {
1166 ++hot_;
1167 }
1168 load();
1169 }
1170
1171 const BracketRoster* owner_ = nullptr;
1172 std::size_t hot_ = 0;
1173 std::size_t run_ = 0;
1174 std::size_t in_run_ = 0;
1175 std::size_t run_offset_ = 0;
1176 std::uint64_t run_previous_ = 0;
1177 native_order::RequestHandle current_{};
1178 };
1179
1181 auto& count = counts_[handle.incarnation];
1182 hot_.push_back(handle);
1183 ++count;
1184 }
1185 // Every materialized member after the settled prefix (the bracket
1186 // cancel's roster). `size()` still includes the settled prefix count.
1187 std::vector<native_order::RequestHandle> members() const {
1188 std::vector<native_order::RequestHandle> out;
1189 out.reserve(size() - settled_count_);
1190 for (const auto& handle : *this) out.push_back(handle);
1191 return out;
1192 }
1193 // The members whose rows the table still holds, in roster order, from the
1194 // first one onward (erased members between them are parked separately).
1195 const std::vector<native_order::RequestHandle>& working_tail() const noexcept { return hot_; }
1196 bool empty() const noexcept { return settled_count_ == 0 && parked_.empty() && hot_.empty(); }
1197 std::size_t size() const noexcept { return settled_count_ + parked_count_ + hot_.size(); }
1198 const_iterator begin() const { return const_iterator(this, false); }
1199 const_iterator end() const { return const_iterator(this, true); }
1200 // How many members of the working tail carry `incarnation`: every member
1201 // whose placement row the table still holds. Allocation-free.
1202 std::size_t count(std::uint64_t incarnation) const noexcept {
1203 const auto found = counts_.find(incarnation);
1204 return found == counts_.end() ? 0 : found->second;
1205 }
1206 // Whether `handle` is a member of the working tail (where every member
1207 // whose row is retained is).
1208 bool holds(const native_order::RequestHandle& handle) const noexcept {
1209 if (count(handle.incarnation) == 0) return false;
1210 return std::find(hot_.begin(), hot_.end(), handle) != hot_.end();
1211 }
1212
1213 // Moves the working tail's leading members whose rows
1214 // were erased into the settled prefix, each folded once into the running
1215 // digest. The parked runs before them join it too, and
1216 // every later member whose row was erased is parked in place.
1217 template<class Erased>
1218 void settle(Erased&& erased) {
1219 std::size_t moved = 0;
1220 std::size_t absorbed = 0;
1221 for (;;) {
1222 for (; absorbed < parked_.size() && parked_[absorbed].at == moved; ++absorbed)
1223 absorb(parked_[absorbed]);
1224 if (moved == hot_.size()) break;
1225 const auto& handle = hot_[moved];
1226 if (!erased(handle.incarnation) || !claims(handle)) break;
1227 settled_last_ = handle.incarnation;
1228 ++settled_count_;
1229 settled_digest_ = fold(settled_digest_, handle.incarnation);
1230 uncount(handle.incarnation);
1231 ++moved;
1232 }
1233 // Anything behind the first member the table still holds to park?
1234 bool interior = false;
1235 for (std::size_t in = moved + 1; in < hot_.size() && !interior; ++in)
1236 interior = erased(hot_[in].incarnation) && claims(hot_[in]);
1237 if (!interior) {
1238 if (moved) hot_.erase(hot_.begin(), hot_.begin() + static_cast<std::ptrdiff_t>(moved));
1239 if (absorbed)
1240 parked_.erase(parked_.begin(), parked_.begin() + static_cast<std::ptrdiff_t>(absorbed));
1241 for (auto& run : parked_) run.at -= moved;
1242 return;
1243 }
1244 // Rebuild the tail: the members kept as handles, and the runs between
1245 // them, a run's old members before the ones it gains, adjacent runs
1246 // merged.
1247 std::vector<Parked> runs;
1248 std::size_t next_run = absorbed;
1249 std::size_t out = 0;
1250 const auto place_runs_before = [&](std::size_t in) {
1251 for (; next_run < parked_.size() && parked_[next_run].at == in; ++next_run) {
1252 Parked& run = parked_[next_run];
1253 if (!runs.empty() && runs.back().at == out) {
1254 join(runs.back(), run);
1255 } else {
1256 run.at = out;
1257 runs.push_back(std::move(run));
1258 }
1259 }
1260 };
1261 for (std::size_t in = moved; in < hot_.size(); ++in) {
1262 place_runs_before(in);
1263 const native_order::RequestHandle handle = hot_[in];
1264 if (in > moved && erased(handle.incarnation) && claims(handle)) {
1265 if (runs.empty() || runs.back().at != out) {
1266 runs.emplace_back();
1267 runs.back().at = out;
1268 }
1269 park(runs.back(), handle.incarnation);
1270 uncount(handle.incarnation);
1271 continue;
1272 }
1273 hot_[out++] = handle;
1274 }
1275 place_runs_before(hot_.size());
1276 hot_.resize(out);
1277 parked_ = std::move(runs);
1278 }
1279 void record_consumed(std::uint8_t leg_kind, std::uint64_t origin) {
1280 const auto fact = std::make_pair(leg_kind, origin);
1281 if (std::find(consumed_.begin(), consumed_.end(), fact) == consumed_.end())
1282 consumed_.push_back(fact);
1283 }
1284 bool consumed(std::uint8_t leg_kind, std::uint64_t origin) const noexcept {
1285 return std::find(consumed_.begin(), consumed_.end(),
1286 std::make_pair(leg_kind, origin)) != consumed_.end();
1287 }
1288 // Drops the facts whose origin can no longer be asked about.
1289 template<class Keep>
1290 void prune_consumed(Keep&& keep) {
1291 consumed_.erase(std::remove_if(consumed_.begin(), consumed_.end(),
1292 [&](const auto& fact) { return !keep(fact.second); }), consumed_.end());
1293 }
1294 const std::vector<std::pair<std::uint8_t, std::uint64_t>>& consumed_facts() const noexcept {
1295 return consumed_;
1296 }
1297 // The settled prefix as the v4 extension fold reads it: its count, and
1298 // its running digest -- with every parked run's place, count and digest
1299 // folded after it while any is parked.
1300 std::size_t settled_count() const noexcept { return settled_count_; }
1301 // The run identity is enough to reproduce a settled member's non-working
1302 // cancel outcome after its incarnation bytes have been dropped.
1303 native_order::RunIdentity run_identity() const noexcept { return run_; }
1304 std::uint64_t settled_digest() const noexcept {
1305 std::uint64_t digest = settled_digest_;
1306 for (const auto& run : parked_) {
1307 digest = fold(digest, run.at);
1308 digest = fold(digest, run.count);
1309 digest = fold(digest, run.digest);
1310 }
1311 return digest;
1312 }
1313 // A wholly settled family has no mutable member or consumed-origin fact.
1314 // Keep precisely the prefix needed by a later append or bracket cancel;
1315 // it need not join the per-bar family walks.
1316 struct Settled {
1318 std::uint64_t last = 0;
1319 std::size_t count = 0;
1320 std::uint64_t digest = kDigestBasis;
1321 };
1322 bool wholly_settled() const noexcept {
1323 return hot_.empty() && parked_.empty() && consumed_.empty();
1324 }
1325 Settled settled_record() const noexcept {
1326 return {run_, settled_last_, settled_count_, settled_digest_};
1327 }
1328 explicit BracketRoster(const Settled& value)
1329 : run_(value.run), settled_last_(value.last),
1330 settled_count_(value.count), settled_digest_(value.digest) {}
1331 BracketRoster() = default;
1332
1333 // Erases every member equal to one of `doomed`, keeping the order of the
1334 // rest: the same result as erase(remove_if(find in doomed)). A doomed
1335 // member is a live leg, so it is in the working tail.
1336 void remove_all(const std::vector<native_order::RequestHandle>& doomed) {
1337 std::vector<std::uint64_t> incarnations;
1338 std::size_t sharing = 0;
1339 for (const auto& handle : doomed) {
1340 if (std::find(incarnations.begin(), incarnations.end(), handle.incarnation)
1341 != incarnations.end()) continue;
1342 incarnations.push_back(handle.incarnation);
1343 const auto found = counts_.find(handle.incarnation);
1344 if (found != counts_.end()) sharing += found->second;
1345 }
1346 if (sharing == 0) return;
1347 // Lowest position holding a member that shares a doomed incarnation.
1348 std::size_t first = hot_.size();
1349 while (sharing > 0 && first > 0) {
1350 --first;
1351 if (std::find(incarnations.begin(), incarnations.end(), hot_[first].incarnation)
1352 != incarnations.end()) --sharing;
1353 }
1354 remove_all_from(first, [&](const native_order::RequestHandle& handle) {
1355 return std::find(doomed.begin(), doomed.end(), handle) != doomed.end();
1356 });
1357 }
1358 // Hoisted once per selection, outside the per-family loop. The common
1359 // one-leg close needs no scratch reads per family; larger selections
1360 // retain their bounded scratch representation rather than a heap vector.
1363 std::optional<std::uint64_t> single;
1366 : rows(doomed) {
1367 if (rows.size() == 1) single = rows.front().incarnation;
1368 }
1369 template<class Predicate> bool any(Predicate predicate) const {
1370 if (single) return predicate(*single);
1371 for (const auto& handle : rows)
1372 if (predicate(handle.incarnation)) return true;
1373 return false;
1374 }
1375 };
1377 if (doomed.empty() || hot_.empty()) return;
1379 for (const auto& handle : doomed) members.put(handle.incarnation, 0);
1380 const RemovalSelection selection(doomed);
1381 remove_selected(members, doomed.context().run, selection);
1382 }
1384 const native_order::RunIdentity& run,
1385 const RemovalSelection& selection,
1386 bool precheck = true) {
1387 // counts_ is exactly the incarnation multiplicity of hot_: push_back
1388 // increments, settle uncounts each removed handle, and remove_all_from
1389 // decrements. Settled/parked members are never in this index.
1390 if (precheck && !selection.any(
1391 [&](std::uint64_t incarnation) { return count(incarnation) != 0; })) {
1392 // Every doomed incarnation has count zero. Checking that EVERY
1393 // hot member has positive count proves this skipped hot_ contains
1394 // no doomed member, in O(hot_) without a scratch-tree lookup.
1395 assert(std::all_of(hot_.begin(), hot_.end(),
1396 [&](const auto& handle) { return count(handle.incarnation) != 0; }));
1397 return;
1398 }
1400 }
1402 const native_order::RunIdentity& run) {
1403 const auto contains = [&](const native_order::RequestHandle& handle) {
1404 return handle.run == run
1405 && members.find(handle.incarnation) != detail::ExitScratchIndex::absent;
1406 };
1407 std::size_t first = 0;
1408 while (first < hot_.size() && !contains(hot_[first])) ++first;
1409 if (first != hot_.size()) remove_all_from(first, contains);
1410 }
1411
1412private:
1413 template<class Contains> void remove_all_from(std::size_t first, Contains contains) {
1414 // A parked run keeps its place among the handles that stay: its
1415 // `at` counts the handles before it, and two runs no handle
1416 // separates any more are one.
1417 std::size_t run = 0;
1418 while (run < parked_.size() && parked_[run].at <= first) ++run;
1419 std::size_t out = first;
1420 for (std::size_t in = first; in < hot_.size(); ++in) {
1421 for (; run < parked_.size() && parked_[run].at == in; ++run) parked_[run].at = out;
1422 if (contains(hot_[in])) {
1423 const auto found = counts_.find(hot_[in].incarnation);
1424 if (--found->second == 0) counts_.erase(found);
1425 continue;
1426 }
1427 if (out != in) hot_[out] = hot_[in];
1428 ++out;
1429 }
1430 for (; run < parked_.size(); ++run) parked_[run].at = out;
1431 hot_.resize(out);
1432 for (std::size_t index = 1; index < parked_.size();) {
1433 if (parked_[index].at == parked_[index - 1].at) {
1434 join(parked_[index - 1], parked_[index]);
1435 parked_.erase(parked_.begin() + static_cast<std::ptrdiff_t>(index));
1436 } else {
1437 ++index;
1438 }
1439 }
1440 }
1441
1442 // Whether a member of `handle`'s run identity can be kept as an
1443 // incarnation: every settled and parked member shares one.
1444 bool claims(const native_order::RequestHandle& handle) {
1445 if (settled_count_ == 0 && parked_count_ == 0) {
1446 run_ = handle.run;
1447 return true;
1448 }
1449 return handle.run == run_;
1450 }
1451 void uncount(std::uint64_t incarnation) {
1452 const auto found = counts_.find(incarnation);
1453 if (found != counts_.end() && --found->second == 0) counts_.erase(found);
1454 }
1455 void park(Parked& run, std::uint64_t incarnation) {
1456 put(run.bytes, run.last, incarnation);
1457 ++run.count;
1458 run.digest = fold(run.digest, incarnation);
1459 ++parked_count_;
1460 }
1461 // Appends `from`'s members to `into`, in order.
1462 void join(Parked& into, const Parked& from) {
1463 std::size_t offset = 0;
1464 std::uint64_t previous = 0;
1465 for (std::size_t index = 0; index < from.count; ++index) {
1466 previous = get(from.bytes, offset, previous);
1467 put(into.bytes, into.last, previous);
1468 into.digest = fold(into.digest, previous);
1469 }
1470 into.count += from.count;
1471 }
1472 // Moves a parked run's members to the end of the settled prefix.
1473 void absorb(const Parked& run) {
1474 std::size_t offset = 0;
1475 std::uint64_t previous = 0;
1476 for (std::size_t index = 0; index < run.count; ++index) {
1477 previous = get(run.bytes, offset, previous);
1478 settled_last_ = previous;
1479 settled_digest_ = fold(settled_digest_, previous);
1480 }
1481 settled_count_ += run.count;
1482 parked_count_ -= run.count;
1483 }
1484
1485 // The settled prefix is represented by its last incarnation, count and
1486 // running digest. Its historical varint bytes are intentionally dropped;
1487 // walks begin at the parked runs and working tail below it.
1488 native_order::RunIdentity run_{};
1489 std::uint64_t settled_last_ = 0;
1490 std::size_t settled_count_ = 0;
1491 std::uint64_t settled_digest_ = kDigestBasis;
1492 // The working tail: handles, and the parked runs between them (run_'s
1493 // incarnations too), in place order.
1494 std::vector<native_order::RequestHandle> hot_;
1495 std::vector<Parked> parked_;
1496 std::size_t parked_count_ = 0;
1497 // Derived member multiplicity per incarnation over the working tail's
1498 // handles (never next-decision state).
1499 std::unordered_map<std::uint64_t, std::size_t> counts_;
1500 // (leg kind, origin) of the erased consumed members.
1501 std::vector<std::pair<std::uint8_t, std::uint64_t>> consumed_;
1502};
1503
1512 double seed_qty = std::numeric_limits<double>::quiet_NaN();
1513 std::int64_t seed_cycle = 0;
1514 bool active = false;
1515 // Generic matching must execute the artifact before the final source
1516 // short. For variable-size source books their acceptance handles are
1517 // consequently opposite to their source report incarnations; retain the
1518 // pending source-report projection until the remnant is closed.
1520};
1521
1522// The legacy three-object qualification is evaluated at the next broker open
1523// (`created_bar + 1 == bar_index`), so the complete source candidate waits
1524// here until that live fact is available. It is not an executable order.
1527 std::uint64_t expected_open_epoch = 0;
1528 bool ready = false;
1529};
1530
1533 std::string comment{};
1534 double qty = std::numeric_limits<double>::quiet_NaN();
1535 double qty_percent = std::numeric_limits<double>::quiet_NaN();
1536 bool immediately = false;
1537 std::uint64_t callsite_token = 0;
1538 std::uint64_t command_ordinal = 0;
1539};
1540
1547
1549 std::int64_t current_day = std::numeric_limits<std::int64_t>::min();
1550 std::int64_t last_loss_day = std::numeric_limits<std::int64_t>::min();
1552 std::int64_t intraday_loss_day = std::numeric_limits<std::int64_t>::min();
1553 double intraday_start_equity = std::numeric_limits<double>::quiet_NaN();
1554 // The script bar whose open captured intraday_start_equity: fills at that
1555 // open point precede the day.
1556 std::int64_t intraday_start_script_bar = std::numeric_limits<std::int64_t>::min();
1557 double intraday_realized = 0.0;
1558 std::uint64_t observed_applied_ordinal = 0;
1559};
1560
1562 int direction = 0; // 0 both, >0 long, <0 short
1564 double max_drawdown = 0.0;
1566 double max_intraday_loss = 0.0;
1568 double max_position_size = 0.0;
1569 bool halted = false;
1570 // Runtime facts belong to the source policy, not to the generic
1571 // settlement kernel. They are updated from public native projections at
1572 // the same broker coordinates at which the retired source route updated
1573 // its latches.
1574 double observed_peak_equity = std::numeric_limits<double>::quiet_NaN();
1576 std::int64_t intraday_block_day = std::numeric_limits<std::int64_t>::min();
1578};
1579
1580// The source-risk CONFIGURATION alone, for the selected-window replay bracket
1581// (PineExecutionAdapter::capture_replay_risk_config and
1582// restore_replay_risk_config). It holds no runtime fact of PineRiskState
1583// (halted, observed peak or drawdown, intraday block, cancel latch) and none
1584// of the cap's consumed state. The percent flags are kept exactly because the
1585// setters make them sticky.
1598
1600
1601} // namespace pineforge::source
1602
1603namespace pineforge::detail {
1604void bind_tree_scratch_tables(source::PineExecutionAdapter&,
1605 const std::shared_ptr<ScratchSlab>&);
1606void bind_id_pair_scratch_tables(source::PineExecutionAdapter&,
1607 const std::shared_ptr<ScratchSlab>&);
1608} // namespace pineforge::detail
1609
1610namespace pineforge::source {
1611
1612// Read-only view facade for the C pending-order projection. This
1613// exposes named source values to native fixture tests while keeping
1614// observer reads side-effect-free; C ABI wiring is separate.
1616public:
1617 int size() const noexcept;
1618 int probe_fill_qty(int index, double fill_price, double* qty,
1619 int* close_only, int* partition) const noexcept;
1620 int level_resolved(int index) const noexcept;
1621 int effective_levels(int index, double* stop, double* limit,
1622 double* trail_activation) const noexcept;
1623 int copy_v1(int index, pf_pending_order_v1_t* out) const noexcept;
1624 int short_seed_collision_role(int index) const noexcept;
1625 int last_bar_dual_entry_path() const noexcept;
1626 double trail_best_price() const noexcept;
1627
1628private:
1630 const PineExecutionAdapter* owner_ = nullptr;
1631};
1632
1634public:
1636 std::uint64_t incarnation = 0;
1638 bool staged = false;
1639 };
1641 std::uint64_t token = 0;
1642 bool active = false;
1643 double target = 0.0;
1644 int calls = 0;
1646 std::string comment{};
1647 std::uint64_t queue_sequence = 0;
1648 };
1649 // Keep the legacy host's construction surface valid. A null
1650 // host means this compatibility carrier has no lowering authority.
1651 explicit PineExecutionAdapter(
1655
1656 void bind(NativeStrategyHost& host) noexcept;
1657 // Override the run-owned store after bind/reset_for_run for an explicit
1658 // host-provisioned context. Borrowed through the last nested reader.
1660 // Pine source state counts chart/source bars. The kernel coordinate is
1661 // script-bar space, so projection rows use this explicit source index and
1662 // never rewrite a kernel lot or trade row.
1663 int projection_bar_index(const NativeDecisionContext&) const noexcept;
1664 int projection_bar_index(const NativeCoordinate&) const noexcept;
1665 void reset_for_run();
1666 void set_cache_policy_for_run(const CacheDefaults& policy);
1667 void set_configuration(const PineStrategyConfig& config) noexcept;
1669 void set_begin_mode(bool is_stream, bool bar_magnifier = false) noexcept;
1670 void set_id_tables_directory(const std::string& directory); // Source-id tables, after reset.
1671 void set_auxiliary_tables_directory(const std::string& directory); // Same binding.
1673 void fixture_cohort_implicit_spill_interval(std::size_t calls) noexcept {
1674 cohorts_by_id_.fixture_implicit_spill_interval(calls);
1675 }
1676 // Test seam: forwards the spill predicate from the adapter to its cohort table.
1677 void fixture_spill_predicate(bool (*predicate)(const std::string&)) noexcept {
1678 cohorts_by_id_.fixture_spill_predicate(predicate);
1679 }
1680 void fixture_verify_source_entry_slots(bool enabled) noexcept {
1681 fixture_verify_source_entry_slots_ = enabled;
1682 }
1683 void fixture_bracket_remove_precheck(bool enabled) noexcept {
1684 fixture_bracket_remove_precheck_ = enabled;
1685 }
1686 std::uint64_t fixture_auxiliary_disk_bytes() const noexcept;
1687 std::uint64_t fixture_erased_digest() const noexcept { return placement_.erased_digest(); }
1688 std::uint64_t fixture_erased_rows() const noexcept { return placement_.erased_rows(); }
1689 const ::pineforge::detail::IdPairStore<std::int64_t, true>&
1690 fixture_consumed_partial_exit_cycles() const noexcept { return consumed_partial_exit_cycles_; }
1691
1692 // Diagnostic work counters, excluded from broker continuation state.
1694 std::uint64_t calls = 0;
1696 std::uint64_t open_lot_iterations = 0;
1697 std::uint64_t max_live_handles = 0, max_placement = 0;
1698 std::uint64_t max_pending_bracket_legs = 0;
1700 };
1704 const PendingScanWork& pending_scan_work() const noexcept { return pending_scan_work_; }
1705 const auto& cohort_table_work() const noexcept { return cohorts_by_id_.work_counters(); }
1707 std::size_t resident_rows = 0, total_bytes = 0, bytes_per_row = 0;
1708 std::size_t row_bytes = 0, key_bytes = 0, origin_bytes = 0;
1710 };
1711 // Snapshot only: O(resident rows + retained origins), no allocation/I/O.
1712 // Capacities include inline string storage; node/control sizes are
1713 // estimates. bytes_per_row counts PHYSICAL rows (deferred empty rows
1714 // inflate it). Physical resident rows <= 2x the rows kept at the last
1715 // spill pass + 64 (+ pinned). This is not a bound on pending-order memory.
1717
1726 std::uint64_t queries = 0, certified_queries = 0, fallback_queries = 0;
1727 std::uint64_t update_rows = 0, replay_rows = 0, origin_visits = 0;
1728 // Populated only by the observation seam. Counts are cumulative
1729 // within a run; observers take differences at each query.
1730 std::uint64_t host_rows = 0, spill_rows = 0, replay_host_rows = 0;
1731 std::uint64_t begin_updates = 0, begin_rows = 0, closed_updates = 0;
1732 std::uint64_t open_updates = 0, reissue_updates = 0, reissue_rows = 0;
1733 std::uint64_t sync_updates = 0, invalidations = 0;
1734 std::uint64_t guard_reasons[static_cast<std::size_t>(PositionTailGuard::Count)]{};
1736 };
1737 PositionTailWork position_tail_work() const noexcept;
1738
1739 // `instrument_feeds`, when given, is moved into the spec's instrument
1740 // feeds (and back into it if the projection throws).
1742 const NativeBeginArgs&,
1743 NativePathOrder path_order = NativePathOrder::Auto,
1744 std::vector<NativeInstrumentFeed>* instrument_feeds = nullptr) const;
1745
1746 void entry(const SourceId& id, bool is_long,
1747 double limit_price = std::numeric_limits<double>::quiet_NaN(),
1748 double stop_price = std::numeric_limits<double>::quiet_NaN(),
1749 double qty = std::numeric_limits<double>::quiet_NaN(),
1750 const std::string& comment = {}, const std::string& oca_name = {},
1751 int oca_type = 0, int qty_type = -1);
1752 void close(const SourceId& id, const std::string& comment = {},
1753 double qty = std::numeric_limits<double>::quiet_NaN(),
1754 double qty_percent = std::numeric_limits<double>::quiet_NaN(),
1755 bool immediately = false, std::uint64_t callsite_token = 0);
1756 void close_all();
1757 void exit(const SourceId& exit_id, const SourceId& from_entry,
1758 double limit_price, double stop_price,
1759 double trail_points = std::numeric_limits<double>::quiet_NaN(),
1760 double trail_offset = std::numeric_limits<double>::quiet_NaN(),
1761 double trail_price = std::numeric_limits<double>::quiet_NaN(),
1762 double qty_percent = 100.0, const std::string& comment = {},
1763 double qty = std::numeric_limits<double>::quiet_NaN(),
1764 const std::string& oca_name = {},
1765 double profit_ticks = std::numeric_limits<double>::quiet_NaN(),
1766 double loss_ticks = std::numeric_limits<double>::quiet_NaN());
1767 void exit_cancel_bracket(const SourceId& exit_id, const SourceId& from_entry,
1768 const std::string& comment = {});
1769 void cancel(const SourceId& id);
1770 void cancel_all();
1771 void order(const SourceId& id, bool is_long, double qty,
1772 double limit_price = std::numeric_limits<double>::quiet_NaN(),
1773 double stop_price = std::numeric_limits<double>::quiet_NaN(),
1774 const std::string& oca_name = {}, int oca_type = 0);
1775
1776 native_order::ExecutionTerms resolve_terms(const NativeExecutionTermsFacts&) const;
1777 // anchor_relative_exits runs at the end of a source
1778 // evaluation: every queued relative strategy.exit whose parent entry is a
1779 // live kernel request is submitted as that parent's anchored bracket
1780 // child. resolve_anchored_level is the kernel's once-per-arm level hook:
1781 // the kernel computes fill + ticks on the tick ladder, and TradingView's
1782 // spelling of that ladder point and its trigger projection (the half-tick
1783 // threshold exit() also applies) are restated there.
1784 void anchor_relative_exits();
1785 std::optional<double> resolve_anchored_level(const NativeAnchoredLevelView&) const;
1786 // How many relative legs this run placed as anchored kernel children, how
1787 // many of those the fill-point re-run adopted as its own request, and how
1788 // many were withdrawn instead (parent ended, definition changed, or the
1789 // fill point asked for another request).
1791 std::uint64_t anchored = 0;
1792 std::uint64_t adopted = 0;
1793 std::uint64_t withdrawn = 0;
1794 };
1796 return anchored_relative_stats_;
1797 }
1798 // Sizing classification, answered for the command a default-sized MARKET
1799 // entry on this side would write at the CURRENT execution point: true when
1800 // the core's native_order::Sized names the whole conversion, so the units
1801 // that settle are the core's and only the source lot floor and the source
1802 // admission gates run on top. Pure in the run's declaration, the staged
1803 // instrument and that point; nothing is remembered.
1804 bool core_sizes_default_opening(bool is_long) const;
1805 // Read-only source scratch ticket, called after exit admission and before
1806 // financial commit. Host held-units publication must follow validation.
1809 // The three policy answers the kernel's margin model asks of its
1810 // host. `margin_check_allowed` is TradingView's scheduling -- the kernel
1811 // offers its own check points and the adapter admits only the ones
1812 // TradingView would also check at; `resolve_margin_requirement` is
1813 // TradingView's money (its fee-adjusted equity basis and its
1814 // ten-significant-digit requirement); `resolve_margin_call_units` is its
1815 // sizing (the lot-floored 4x restore and the whole-drop band).
1816 // TradingView's margin money at one mark: the requirement (on its
1817 // ten-significant-digit ladder where a lot is worth under one unit of
1818 // account) against its fee-adjusted live equity. One implementation, read
1819 // by the adapter's own checkpoints and by the kernel's requirement hook.
1821 bool valid = false;
1822 double mark = 0.0;
1823 double held = 0.0;
1824 double unit_margin = 0.0;
1825 double exact_required = 0.0;
1826 double required = 0.0;
1827 double equity = 0.0;
1828 // One unit's margin per unit of price (point value x FX x margin
1829 // fraction), and whether the book is short.
1830 double scale = 0.0;
1831 bool short_book = false;
1832 };
1833 SourceMarginMoney source_margin_money(double mark_price,
1834 std::int64_t sub_bar_open_ms) const;
1835 // `print`, when finite, is the price the call executes at: its one-unit
1836 // band and the short gate are priced there instead of at the mark moved
1837 // by the exit side's slippage.
1838 double source_margin_units(const SourceMarginMoney&, bool opening_checkpoint, bool gated = true,
1839 double print = std::numeric_limits<double>::quiet_NaN()) const;
1840 double lagged_margin_follow_up_units(const std::vector<NativeOpenLot>& lots,
1841 const SourceMarginMoney& money, double called,
1842 double fill, std::int64_t sub_bar_open_ms) const;
1843 double source_margin_fill_price(double fire, bool close_is_buy) const;
1844
1845 bool source_margin_rounded_tie_veto() const;
1848 std::optional<NativeMarginDecision> resolve_margin_requirement(
1849 const NativeMarginRequirementView&) const;
1850 std::optional<double> resolve_margin_call_units(const NativeMarginCallView&) const;
1851 void on_bar_open(const Bar&, const NativeDecisionContext&);
1852 void on_tick(const Bar&, const NativeTickContext&);
1853 // Called from the generic calculation callback before the source script
1854 // runs, at the executable decision point on_bar_close then sees.
1856 // Called from the generic calculation callback after the source script
1857 // has returned while the current decision point remains executable.
1858 void on_bar_close(const Bar&, const NativeDecisionContext&);
1860 void source_batch_end();
1861
1862 // Fixture/public projection of the chart-timezone day decomposition used
1863 // by the risk and intraday-cap ledgers. It carries no generic-kernel
1864 // policy and lets native-route tests avoid reaching into retired host
1865 // internals.
1866 std::int64_t chart_day_key(std::int64_t timestamp_ms) const noexcept;
1867
1869 const PendingIntentView& pending_intent_view() const noexcept { return pending_view_; }
1870 // Read-only fixture facade for command-boundary rows that have not yet
1871 // become live native requests. It never participates in matching.
1872 std::vector<FixturePendingSnapshot> fixture_pending_snapshots() const;
1873#ifdef PINEFORGE_VALUE_INVALID_FIXTURES
1874 // Ownership boundary control: bypass source admission for a stored limit.
1875 // Inline, no new state; absent from ordinary builds.
1876 void fixture_value_invalid_drawdown_limit(double value, bool percent) noexcept {
1877 risk_.max_drawdown = value;
1878 risk_.max_drawdown_percent = percent;
1879 }
1880#endif
1881 // Fixture-only read of the source cohort's currently live quantity. It
1882 // projects the adapter's truthful opening facts; it does not recreate the
1883 // deleted executable id ledger.
1884 double source_unclosed_qty_for(const SourceId& id) const noexcept {
1885 return cohort_exposure_for(id);
1886 }
1887 int source_entry_slot_count() const noexcept;
1888 double fixture_close_logical_units(const SourceId&) const noexcept;
1889 std::size_t fixture_close_logical_count() const noexcept {
1890 return close_logical_units_.size();
1891 }
1892 double fixture_close_pending_debt() const noexcept {
1893 return close_batch_pending_debt_;
1894 }
1895 double fixture_close_admitted_total() const noexcept {
1896 return close_batch_admitted_total_;
1897 }
1898 std::vector<FixtureCloseCallsite> fixture_close_callsites() const;
1899
1900 void set_risk_direction(int direction) noexcept;
1901 void set_risk_max_cons_loss_days(int value) noexcept;
1902 void set_risk_max_drawdown(double value, bool percent) noexcept;
1903 void set_risk_max_intraday_loss(double value, bool percent) noexcept;
1904 void set_risk_max_position_size(double value) noexcept;
1905 // Exact capture/restore of the source-risk configuration (replay seam).
1906 // Restore writes the seven risk fields directly, because the setters above
1907 // keep percent flags sticky, and restores the cap's three configuration
1908 // fields through IntradayCap::restore_replay_configuration. No risk runtime
1909 // fact and no cap accounting is read or written.
1912 bool allows_risk_direction(bool is_long) const noexcept {
1913 return risk_.direction == 0 || (is_long ? risk_.direction > 0 : risk_.direction < 0);
1914 }
1915 bool max_drawdown_is_percent() const noexcept { return risk_.max_drawdown_percent; }
1916 bool max_intraday_loss_is_percent() const noexcept {
1917 return risk_.max_intraday_loss_percent;
1918 }
1919 bool take_intraday_loss_relabel(std::uint64_t ordinal) noexcept;
1920 void set_margin_call_enabled(bool enabled) noexcept;
1921 // Moved out of PyramidEntry: the physical
1922 // lots opened by request incarnation `incarnation` came from a
1923 // same-direction MARKET pyramid add — not the base open, not a priced
1924 // entry. When a from_entry priced bracket exit fills on such an add's OWN
1925 // entry bar, and after it in TV's open-tick fill sequence, the exit covers
1926 // (scratches) the add dur-0. The reader also gates on
1927 // `entry_bar_index == bar`, so prior-bar slices are never covered. This
1928 // is TradingView provenance, so it is adapter state keyed by the lot's
1929 // entry incarnation rather than a field on the generic lot.
1930 void mark_market_pyramid_add(std::uint64_t incarnation);
1931 bool market_pyramid_add(std::uint64_t incarnation) const noexcept {
1932 return market_pyramid_adds_.count(incarnation) != 0;
1933 }
1934 void enable_intraday_cap() noexcept;
1935 void attach_execution_adapter() noexcept;
1936 bool calc_on_order_fills() const noexcept { return config_.calc_on_order_fills; }
1937 bool process_orders_on_close() const noexcept { return config_.process_orders_on_close; }
1938 // Read-only fixture observations. They expose callback coordinates
1939 // already owned by the adapter; no test path can mutate the native book.
1940 bool fixture_coof_recalc_active() const noexcept { return coof_recalc_active_; }
1941 bool fixture_coof_cursor_is_bar_close() const noexcept {
1942 return coof_recalc_active_ && coof_context_.coordinate.path_phase == NativePathPhase::Close;
1943 }
1946 void begin_source_evaluation() noexcept {
1947 named_entry_cancel_tokens_.clear();
1948 pending_same_bar_close_qty_ = 0.0;
1949 source_batch_mutated_ = false;
1950 }
1951 bool fixture_named_entry_cancel_active(const SourceId& id) const noexcept {
1952 return named_entry_cancel_tokens_.find(id) != named_entry_cancel_tokens_.end();
1953 }
1954 std::vector<native_order::RequestHandle> take_first_open_newborns();
1955 // Pull terminal generic receipts before a source callback observes the
1956 // next command boundary. This retires group-cancelled bracket siblings
1957 // and pending origins rejected by native admission.
1959 // Called by the fixture scheduler after one source script evaluation so
1960 // re-priced carried bracket legs retain their original roster order before
1961 // newly pending-entry legs are appended.
1963 native_order::RequestHandle just_applied = {},
1964 bool post_calculation = true,
1965 bool pre_script_drain = false);
1966 // Ordinary POOC same-direction adds are held until the source evaluation
1967 // closes, so a later close_all in that same evaluation settles first and
1968 // the add opens the next source position at the same close point.
1969 void flush_pending_entries();
1970 void flush_pending_closes();
1972 bool explicit_brackets_only = false,
1973 double current_open = std::numeric_limits<double>::quiet_NaN());
1975 const NativeDecisionContext&, bool first_open,
1976 std::uint64_t source_fill_sequence);
1977 void end_coof_recalc() noexcept;
1979 const native_order::ExecutionAppliedEvent&,
1980 const NativeDecisionContext&) const noexcept;
1981 // The event fills a strategy.close / close_all sent with immediately =
1982 // true, executed by the bar's own calculation.
1984 const native_order::ExecutionAppliedEvent&,
1985 const NativeDecisionContext&) const noexcept;
1986 struct ExitGroupWork;
1987 std::uint64_t command_sequence_for_exit(const SourceId& exit_id,
1988 const SourceId& from_entry = {}) const;
1989 bool is_open_phase_exit(std::size_t trade_index) const;
1990 std::uint8_t exit_phase_value(std::size_t trade_index) const;
1991 std::pair<std::size_t, std::uint64_t> exit_phase_prefix() const noexcept {
1992 return {exit_phase_final_, exit_phase_digest_};
1993 }
1994 std::size_t resident_exit_phases() const noexcept {
1995 return std::min(trade_exit_phase_.size(), trade_exit_phase_.resident_capacity());
1996 }
1997 // Reorders trade_exit_phase_[start..) to follow a same-bar exit-group sort:
1998 // slot start + i takes the phase of the trade previously at indices[i].
1999 void permute_exit_phases(std::size_t start, const std::vector<std::size_t>& indices);
2000 void permute_exit_phase_rows(std::size_t start, std::size_t count,
2001 const std::function<std::size_t(std::size_t)>& source_at); ExitGroupWork exit_group_work() const noexcept;
2002 // The place of each of `ids` among the children one global strategy.exit
2003 // fill closes, in TradingView's entry-id table (exit_rank_keys_); nullopt
2004 // where the pinned table does not reach (PineStrategyHost::
2005 // order_global_exit_children).
2006 std::optional<std::vector<std::uint32_t>> global_exit_child_ranks(
2007 const std::vector<SourceId>& ids) const;
2008 // Whether a live leg of any exit other than `exit_id` (a strategy.exit
2009 // that names no entry, of which `filled` is the leg that filled) would
2010 // trigger at `price` too, closing a position of the given side.
2011 bool other_exit_reaches(double price, bool closing_long, const SourceId& exit_id,
2012 const native_order::RequestHandle& filled) const;
2013 void hash_state(BrokerStateHashSink&) const;
2014 // The v4 folds of one dropped-close receipt and one exit phase, onto a
2015 // running digest (pine_state_hash.cpp).
2016 static std::uint64_t fold_dropped_close(std::uint64_t digest,
2017 const DroppedCloseReceipt& receipt) noexcept;
2018 static std::uint64_t fold_exit_phase(std::uint64_t digest, std::uint8_t phase) noexcept;
2019
2020 // Retained for the untouched legacy source host. New fixture state is
2021 // represented by the private maps below rather than this carrier alone.
2022 compat::pine::IntradayCap cap;
2023 compat::pine::OrderPriority priority;
2025
2026private:
2027 friend class PendingIntentView;
2028 friend class PineStrategyHost;
2029 friend class PineScheduler;
2030 friend void ::pineforge::detail::bind_id_pair_scratch_tables(
2031 PineExecutionAdapter&, const std::shared_ptr<::pineforge::detail::ScratchSlab>&);
2032 friend void ::pineforge::detail::bind_tree_scratch_tables(
2033 PineExecutionAdapter&, const std::shared_ptr<::pineforge::detail::ScratchSlab>&);
2034 bool partial_exit_sibling_live(const PlacementSnapshot&,
2035 native_order::RequestHandle) const;
2036 // Called once for [trades_.base(), global_end), in forward
2037 // order, before retiring that prefix. scratch_directory is the
2038 // already-resolved directory. This is the only dependency binding.
2039 void preserve_position_tail_before_seal(
2040 std::size_t global_end, const std::string& scratch_directory);
2041
2042 // Physical storage of immutable, already-hashed closed rows, not a
2043 // second logical state. The closed-row digest and position-open mark
2044 // reconstruct its reachable sequence. File offsets/cache are excluded
2045 // so the sealed/resident partition cannot change continuation hashes.
2046 std::shared_ptr<detail::PositionTailSpill> position_tail_spill_;
2047 // Derived acceleration of the same closed-row source and current live
2048 // maps. Every successful certificate proves equality to ordered replay;
2049 // a cache miss, invalidation or complete discard changes only work.
2050 // No independent broker state and no new H leaf.
2051 std::shared_ptr<detail::PositionTailSummary> position_tail_summary_;
2052 void observe_position_tail_rows();
2053 void begin_position_tail_summary();
2054 friend detail::ExitScratchContext& detail::exit_scratch(const PineExecutionAdapter&);
2055 struct CohortFacts {
2057 OriginRoster origins;
2058 std::vector<native_order::RequestHandle> opened;
2059 struct Record {
2060 std::uint64_t handle; std::int64_t cycle;
2062 };
2063 // Live source exposure by opening provenance. This is source-layer
2064 // bookkeeping only: the generic core still resolves cohort authority
2065 // at every candidate. It lets a command snapshot an existing id's
2066 // percentage basis before later requests in the same script pass
2067 // reduce that cohort.
2068 std::unordered_map<std::uint64_t, double> live_units_by_origin;
2069 std::int64_t cycle = 0;
2070 Record retired_record() const noexcept;
2071 static CohortFacts restore_record(const Record&, bool arena_backed = false);
2072 bool spillable() const noexcept;
2073 bool held() const noexcept;
2074 // The sides (bit 0 long, bit 1 short) of the openings
2075 // in `origins` whose placement rows were erased, so a question about
2076 // every origin the cohort ever accepted still covers them.
2077 std::uint8_t erased_opening_sides = 0;
2078 };
2079 using CohortTable = ::pineforge::detail::DiskResidentIdTable<CohortFacts>;
2080 struct PendingBracketLeg {
2081 native_order::Request request;
2082 PlacementSnapshot snapshot;
2083 SourceId replacement_key;
2084 std::uint64_t family_key = 0;
2085 };
2086
2087 struct PendingEntry {
2088 native_order::Request request;
2089 PlacementSnapshot snapshot;
2090 SourceId replacement_key;
2091 };
2092
2093 struct DelayedMarketOrder {
2094 native_order::Request request;
2095 PlacementSnapshot snapshot;
2096 SourceId replacement_key;
2097 std::uint64_t release_open_epoch = 0;
2098 bool execute_at_open = false;
2099 };
2100
2101 // A sell-side stop that is already marketable at the open, while
2102 // a buy-side open-marketable stop of the same flat pair is still live,
2103 // is held out of kernel matching (legacy opposing-stop pass-0 deferral)
2104 // and admitted after the bar path so later-path same-direction legs can
2105 // pyramid first. forced_execution_price keeps the open fill.
2106 struct DeferredOpenMarketableSell {
2107 PlacementSnapshot snapshot;
2108 SourceId replacement_key;
2109 double fill_price = std::numeric_limits<double>::quiet_NaN();
2110 double path_position = 0.0;
2111 bool open_marketable = false;
2112 };
2113
2114 // The legacy same-bar MARKET transaction is a source-side command batch:
2115 // all BUY members are admitted before SELL members at the next broker
2116 // open, while each member retains its placement-time physical quantity.
2117 // Keep the batch outside the generic core; it contains source ids and the
2118 // targeted-close artifact that the generic request model must not learn.
2119 struct EntrySubmissionContext {
2120 double current = 0.0;
2121 double limit_price = std::numeric_limits<double>::quiet_NaN();
2122 double stop_price = std::numeric_limits<double>::quiet_NaN();
2123 bool default_sized = false;
2124 bool priced = false;
2125 bool reverses = false;
2126 bool opposite_opening_pending = false;
2127 bool pure_stop_entry = false;
2128 bool coof_market_next_open = false;
2129 bool coof_priced_next_open = false;
2130 bool coof_market_at_second_extreme = false;
2131 bool paired_all_in_reentry = false;
2132 };
2133
2134 struct PendingSameBarCommand {
2135 native_order::Request request;
2136 PlacementSnapshot snapshot;
2137 SourceId replacement_key;
2138 bool opening = false;
2139 // A same-id MARKET reissue booked as a transaction of its own units
2140 // and the held opposite units it left out, which the
2141 // short-seed book puts back.
2142 bool staged_reversal = false;
2143 double staged_reversal_held_units = 0.0;
2144 std::optional<native_order::Request> unbatched_request;
2145 std::optional<PlacementSnapshot> unbatched_snapshot;
2146 EntrySubmissionContext unbatched_context;
2147 };
2148
2149 // A source command can remain observable through the enclosing source
2150 // evaluation after generic admission has already produced its terminal
2151 // receipt. This is a source projection row, never a second executable
2152 // request; it expires at the following broker open.
2153 struct SourceShadowPending {
2154 PlacementSnapshot snapshot;
2155 std::string label;
2156 };
2157
2158 struct PendingRelativeExit {
2159 SourceId exit_id;
2160 SourceId from_entry;
2161 double trail_points = std::numeric_limits<double>::quiet_NaN();
2162 double trail_offset = std::numeric_limits<double>::quiet_NaN();
2163 double trail_price = std::numeric_limits<double>::quiet_NaN();
2164 double qty_percent = 100.0;
2165 std::string comment;
2166 double qty = std::numeric_limits<double>::quiet_NaN();
2167 std::string oca_name;
2168 double profit_ticks = std::numeric_limits<double>::quiet_NaN();
2169 double loss_ticks = std::numeric_limits<double>::quiet_NaN();
2170 };
2171
2172 // One relative leg of a queued strategy.exit, spelled as the
2173 // kernel's anchored bracket child: the host-sized book close exit()
2174 // submits at the parent's fill, placed ahead of it (FromOwnerFill under
2175 // WaitForApplied{parent, PendingUntilArmed, AfterArmPrint, Book}). Until
2176 // its parent fills it is a kernel request only: no placement row, no live
2177 // handle, so every source book (reservations, admission, the pending
2178 // projection) reads exactly what it read before. `installed_level` is the
2179 // trigger level the arm hook restated; the fill-point exit() re-run
2180 // adopts the armed request when it is the request it would submit (same
2181 // trigger bits, same group, a host-sized book close), and withdraws it
2182 // otherwise.
2183 struct AnchoredRelativeLeg {
2184 SourceId exit_id;
2185 SourceId from_entry;
2187 native_order::RequestHandle handle{};
2188 native_order::RequestHandle parent{};
2189 bool parent_long = true;
2190 // The source tick operand this leg was anchored from (profit, loss, or
2191 // the ceiled trail_points) and, for a trailing leg, the source offset.
2192 double operand_ticks = 0.0;
2193 double trail_offset = std::numeric_limits<double>::quiet_NaN();
2194 native_order::Request request;
2195 bool armed = false;
2196 double installed_level = std::numeric_limits<double>::quiet_NaN();
2197 };
2198
2199 // A from_entry "" exit with relative legs that covers the position entry
2200 // by entry (ExitBindingRuleSwitches::global_exit_per_entry_levels): kept
2201 // from the moment the position it stands on holds a second entry until the
2202 // book goes flat. `children` are the entries in fill order, each with the
2203 // fill price its bracket's levels come from and the quantity it closes;
2204 // `consumed` once a leg of its bracket filled (TradingView never gives that
2205 // entry another one).
2206 struct PerEntryExitChild {
2207 std::uint64_t origin = 0;
2208 double fill_price = std::numeric_limits<double>::quiet_NaN();
2209 double units = 0.0;
2210 bool consumed = false;
2211 };
2212 struct PerEntryExit {
2213 SourceId exit_id;
2214 std::int64_t cycle = 0;
2215 bool long_side = true;
2216 double profit_ticks = std::numeric_limits<double>::quiet_NaN();
2217 double loss_ticks = std::numeric_limits<double>::quiet_NaN();
2218 std::string comment;
2219 std::vector<PerEntryExitChild> children;
2220 };
2221
2222 struct PendingCoofRequest {
2223 native_order::Request request;
2224 PlacementSnapshot snapshot;
2225 SourceId replacement_key;
2226 bool opening = false;
2227 std::uint64_t family_key = 0;
2228 bool next_open = false;
2229 };
2230
2231 struct PendingMarginRevival {
2232 PlacementSnapshot snapshot;
2233 std::int32_t decline_bar = -1;
2234 };
2235
2236 struct NamedEntryCancelToken {
2237 std::uint64_t entry_incarnation = 0;
2238 std::uint64_t surviving_exit_incarnation = 0;
2239 };
2240
2241 // One strategy.close call site on one bar: TradingView keeps one order
2242 // per site. Its quantity and comment are the last effective call's, the
2243 // id its fill books against the first effective call's.
2244 struct CloseCallsiteState {
2245 bool active = false;
2246 std::uint64_t token = 0;
2247 int calls = 0;
2248 SourceId first_id{};
2249 SourceId id{};
2250 std::string comment{};
2251 double target = 0.0;
2252 std::uint64_t queue_sequence = 0;
2253 };
2254
2255 // One opening fill's share of the close ledger: the units entered under
2256 // `id` that no close has booked yet. Records keep fill order; adjacent
2257 // records of one id merge.
2258 struct CloseLedgerRecord {
2259 SourceId id{};
2260 double units = 0.0;
2261 };
2262
2263 NativeStrategyHost& require_host() const;
2264 bool modeled_input() const;
2265 void require_cohort_ticket(const SourceId* prospective = nullptr) const;
2266 native_order::CohortHandle cohort_for(const SourceId& id);
2267 std::optional<native_order::RequestHandle> submit_or_replace(
2268 native_order::Request request, PlacementSnapshot snapshot, bool opening,
2269 const SourceId& replacement_key = {});
2270 void remember(const native_order::RequestHandle&, PlacementSnapshot&&);
2271 void retire(native_order::RequestHandle) noexcept;
2272 // Erases the placement rows no reader can reach again
2273 // (pine_adapter.cpp explains the rule); run at every bar open.
2274 void erase_retired_rows(const NativeDecisionContext&);
2275 // Takes out of the kernel's cohort rosters every origin
2276 // that can no longer be bound, before retiring the adapter's prefix.
2277 void retire_origin_prefixes();
2278 void release_closed_cohort_origins();
2279 bool lifecycle_readable(const PlacementSnapshot&) const noexcept;
2280 // Writes one trade's exit phase, refolding when the write lands below the
2281 // final mark.
2282 void set_exit_phase(std::size_t trade_index, std::uint8_t phase);
2283 std::vector<native_order::RequestHandle> openings_for(const SourceId&) const;
2284 double cohort_exposure_for(const SourceId&) const noexcept;
2285 double cohort_exposure_for(const CohortFacts&) const noexcept;
2286 // The units an exit's from_entry holds; none when it names no entry.
2287 std::optional<double> from_entry_units(const SourceId&) const noexcept;
2288 bool from_entry_filled_this_cycle(const SourceId&) const;
2289 double percent_commission_live_equity(double) const noexcept;
2290 // The same equity at an explicit account-currency rate: the rate an
2291 // execution converts at, which its terms facts carry
2292 // (NativeExecutionTermsFacts::active_fx); none, the presented clock's.
2293 double percent_commission_live_equity(double, std::optional<double> fx) const noexcept;
2294 // The kernel's marked equity at `mark` (every open entry fee charged),
2295 // at `fx` when given: strategy.equity under a cash commission.
2296 double strategy_equity_at(double mark, std::optional<double> fx) const noexcept;
2297 // Records both equities a sizing snapshot carries at `mark`: `equity`
2298 // (percent_commission_live_equity) and, for a cash-fee percent-of-equity
2299 // default quantity, `strategy_equity`.
2300 void mark_sizing_equity(PineSizingSnapshot&, double mark,
2301 std::optional<double> fx = std::nullopt) const noexcept;
2302 double quantize_close_units(double basis, double percent) const noexcept;
2303 double quantize_percent_exit_units(double requested,
2304 double available) const noexcept;
2305 bool compute_exit_reservation(const SourceId& exit_id,
2306 const SourceId& from_entry,
2307 double requested_qty,
2308 double& qty_percent,
2309 double live_basis,
2310 double percentage_basis,
2311 double& reserved_qty) const;
2312 void reconcile_deferred_exit_reservations(const SourceId& from_entry,
2313 double live_basis);
2314 void reconcile_exit_reservations_after_margin();
2315 double active_staged_fx(std::int64_t) const noexcept;
2316 void apply_fx_open_margin_slice(const Bar&, const NativeDecisionContext&);
2317 void apply_fx_opening_margin_slice(const native_order::ExecutionAppliedEvent&,
2318 const NativeDecisionContext&);
2319 void schedule_preopen_margin_slice(const Bar&, const NativeDecisionContext&);
2320 bool leveraged_entry_bar_checked() const noexcept;
2321 bool preopen_slice_class(const PlacementSnapshot&,
2322 const NativeDecisionContext&) const noexcept;
2323 // For both helpers, resolved_execution_price == nullopt prices at mark_price
2324 // with exit-side slippage; a value pins that already-resolved price unchanged
2325 // when the execution price is forced.
2326 bool submit_margin_call_slice(double mark_price, const NativeDecisionContext&,
2327 bool opening_checkpoint = false,
2328 std::optional<double> resolved_execution_price = std::nullopt);
2329 bool submit_margin_call_units(double mark_price, const NativeDecisionContext&,
2330 double units,
2331 bool force_execution_price = true,
2332 std::optional<double> resolved_execution_price = std::nullopt);
2333 bool submit_tv_money_long_margin_call(const Bar&, const NativeDecisionContext&,
2334 int* fired_waypoint = nullptr);
2335 void fold_source_money();
2336 void fold_source_money_through(const PineStrategyHost& pine, std::uint64_t count);
2337 bool gain_loss_regime() const;
2338 bool gain_loss_run_regime() const;
2339 double gain_loss_signal_equity(double mark) const;
2340 double gain_loss_mirror_equity(double mark) const;
2341 std::optional<double> pooc_fee_units(const PineSizingSnapshot&) const;
2342 double tv_pooc_fee_grossed_unit(double price) const;
2343 bool unified_admission_scope(const PlacementSnapshot&, bool market_or_stop,
2344 double signed_units, double units,
2345 bool beside_exits = false) const;
2346 bool unified_admission_terms(const PlacementSnapshot&, const NativeExecutionTermsFacts&,
2347 bool market_or_stop, bool opposite,
2348 native_order::ExecutionTerms& result, bool& admitted) const;
2349 bool unified_fill_admits(const PlacementSnapshot&, double units, double quote) const;
2350 void record_close_first(bool whole_position);
2351 void credit_pyramiding_record(const SourceId& id, std::uint64_t incarnation, double units);
2352 void book_pyramiding_records(const SourceId& id, std::uint64_t armed_for, double units);
2353 std::optional<std::size_t> open_ledger_records() const;
2354 std::optional<std::size_t> held_ledger_records() const;
2355 std::uint64_t entry_incarnation_of(const SourceId& from_entry) const;
2356 bool exit_tombstoned(const SourceId& exit_id, const SourceId& from_entry) const;
2357 bool unified_placement_book_clear() const;
2358 bool pinned_lot_grid() const;
2359 bool slipped_long_margin_scope() const;
2360 bool slipped_long_book_scope() const;
2361 double slipped_long_margin_units(double quote) const;
2362 bool slipped_long_unit_shortfall(double quote) const;
2363 bool close_all_at_next_point(
2364 const Bar&, int fired,
2365 const std::vector<std::pair<native_order::RequestHandle, PlacementSnapshot>>& closes,
2366 const NativeDecisionContext&);
2367 bool slipped_pooc_opening_money_scope(
2368 const Bar&, const NativeDecisionContext&) const;
2369 bool submit_slipped_pooc_opening_money_call(
2370 const Bar&, const NativeDecisionContext&);
2371 bool schedule_tv_money_long_margin_before_trail(
2372 const Bar&, const NativeDecisionContext&);
2373 std::optional<std::int64_t> flat_sibling_placement(std::size_t min_lots) const;
2374 bool flat_sibling_book(const PlacementSnapshot&, std::int32_t interval_index,
2375 NativePathPhase phase) const;
2376 bool flat_sibling_fill_follows(const PlacementSnapshot&,
2377 const native_order::RequestHandle&) const;
2378 bool add_sibling_fill_follows(const PlacementSnapshot&,
2379 const native_order::RequestHandle&) const;
2380 std::size_t add_sibling_lots(std::int64_t placed) const;
2381 bool whole_unit_follow_up_due(double called_units, double mark) const;
2382 bool margin_follow_up_scope() const noexcept;
2383 double margin_follow_up_units(double called_units, double fill,
2384 std::int64_t sub_bar_open_ms) const;
2385 void follow_margin_call(double called_units, double fill, double current,
2386 const NativeDecisionContext&);
2387 bool short_call_vetoed(const SourceMarginMoney&, double units, bool opening_checkpoint,
2388 double print = std::numeric_limits<double>::quiet_NaN()) const;
2389 // The opening-call schedule after a process_orders_on_close close fill
2390 // (MarginOpeningSwitches): its scope, the call sized at the close and the
2391 // calls the open takes after it.
2392 bool close_sized_open_call_scope(bool short_book, bool several_lots = false) const;
2393 double close_sized_open_units(double close_mark, double print,
2394 std::int64_t sub_bar_open_ms) const;
2395 bool run_close_sized_open_calls(double units, double opening_mark,
2396 const NativeDecisionContext&);
2397 // The opening checkpoint at the fill that opened or grew a full-margin
2398 // book (on_applied): the exit legs that fill ahead of it, and its call.
2399 bool opening_released_leg(const PlacementSnapshot& row, std::uint64_t leg,
2400 std::uint64_t opening, const SourceId& id, bool short_book,
2401 const NativeDecisionContext&) const;
2402 bool priced_exit_precedes_opening(bool short_book, std::uint64_t opening,
2403 const SourceId& opening_id, const NativeDecisionContext&,
2404 const PlacementSnapshot* filled, bool* at_open) const;
2405 void run_opening_checkpoint(double fill, bool market, bool sibling_book,
2406 bool adds_book_last, const NativeDecisionContext&);
2407 double open_print_follow_up_units(double held_before, double equity_before, double entry,
2408 double called, double fill, bool short_book,
2409 std::int64_t sub_bar_open_ms) const;
2410 double open_print_lot_follow_up_units(const std::vector<std::pair<double, double>>& taken,
2411 double equity_before, double held_before, double fill,
2412 bool short_book, std::int64_t sub_bar_open_ms) const;
2413 bool lagged_short_scope() const noexcept;
2414 double lagged_short_follow_up_units(const std::vector<std::pair<double, double>>& taken,
2415 double equity_before, double held_before, double fill,
2416 std::int64_t sub_bar_open_ms) const;
2417 double call_short_with_lagged_follow_up(double mark, double units,
2418 const NativeDecisionContext&, bool owed = false);
2419 bool whole_share_lag_scope() const noexcept;
2420 double whole_share_lag_units(const std::vector<std::pair<double, double>>& taken,
2421 double equity_before, double held_before, double fill,
2422 std::int64_t sub_bar_open_ms) const;
2423 bool open_call_owing_follow_up(double opening_mark, const NativeDecisionContext&);
2424 bool short_path_points_scope(const NativeDecisionContext&) const;
2425 void execute_owed_short_follow_up(int point, double price, const NativeDecisionContext&);
2426 void hold_owed_close_follow_up(double close, const NativeDecisionContext&);
2427 void book_owed_close_follow_up(const Bar&, const NativeDecisionContext&);
2428 void walk_short_path_points(const Bar&, const NativeDecisionContext&);
2429 void schedule_lagged_short_follow_up(const native_order::ExecutionAppliedEvent&,
2430 const NativeDecisionContext&);
2431 bool pending_veto_point_first(const Bar&, const NativeDecisionContext&, double opening_mark);
2432 void record_close_call_after_script(double book_before, double called,
2433 const NativeDecisionContext&);
2434 void apply_frozen_reversal_close(const PlacementSnapshot&, const NativeExecutionTermsFacts&,
2435 native_order::ExecutionTerms&) const;
2436 native_order::ExecutionTerms resolve_source_terms(const NativeExecutionTermsFacts&) const;
2437 double gain_loss_closed_equity() const;
2438 bool declined_reversal_at_open(const Bar&) const;
2439 bool schedule_margin_call_path(const Bar&, const NativeDecisionContext&);
2440 bool schedule_priced_opening_margin(const Bar&, const NativeDecisionContext&, double fill);
2441 bool close_point_margin_scope() const noexcept;
2442 void withdraw_waypoint_margin_calls();
2443 bool rest_waypoint_margin_call(const Bar&, int from, int to, const NativeDecisionContext&);
2444 bool close_point_margin_call(const Bar&, const NativeDecisionContext&, bool cancelled);
2445 bool book_close_point_call(double mark, double units, const NativeDecisionContext&,
2446 bool queued, double reference_mark = 0.0);
2447 void close_point_follow_up(double mark, double called_units, const NativeDecisionContext&);
2448 std::vector<std::pair<native_order::RequestHandle, PlacementSnapshot>>
2449 same_bar_close_alls(const NativeDecisionContext&) const;
2450 void size_close_alls_at_placement(
2451 std::vector<std::pair<native_order::RequestHandle, PlacementSnapshot>>&, double placed);
2452 bool close_point_margin_call_at_open(const Bar&, const Bar& prior,
2453 const NativeDecisionContext&);
2454 double source_margin_one_unit(const SourceMarginMoney&) const;
2455 bool source_margin_unit_restores(const SourceMarginMoney&, double fill) const;
2456 void defer_declined_reversal_exits_at_adverse(const Bar&,
2457 const NativeDecisionContext&,
2458 bool margin_scheduled);
2459 bool intraday_loss_breached(double mark_price) const;
2460 bool submit_intraday_loss_close(double mark_price, const NativeDecisionContext&,
2461 bool execute_current);
2462 void schedule_intraday_loss_path(const Bar&, const NativeDecisionContext&);
2463 void update_risk_state(double mark_price);
2464 bool intraday_loss_orders_blocked() const noexcept;
2465 compat::pine::CapClock cap_clock(const NativeDecisionContext&) const;
2466 compat::pine::Calculation cap_calculation(const NativeDecisionContext&) const;
2467 compat::pine::MatchedAttempt cap_attempt(
2468 const PlacementSnapshot&, std::uint64_t incarnation,
2469 const native_order::ExecutionAppliedEvent* applied = nullptr) const;
2470 bool cap_placement_denied(const NativeDecisionContext&);
2471 void observe_intraday_cap(const native_order::ExecutionAppliedEvent&,
2472 const PlacementSnapshot&, const NativeDecisionContext&);
2473 void observe_intraday_cap_noop(bool is_long, const NativeDecisionContext&);
2474 void execute_cap_close_now(const compat::pine::CloseNow&);
2475 void execute_due_cap_close(const NativeDecisionContext&);
2476 void maybe_activate_short_seed_plan();
2477 void activate_short_seed_plan_at_open(const NativeDecisionContext&);
2478 bool qualify_short_seed_plan(const ShortSeedPlan&) const;
2479 bool short_seed_context_is_live() const noexcept;
2480 void record_dropped_close(const SourceId&, const std::string&, double, double,
2481 bool, std::uint64_t);
2482 void record_opening_fee(const PlacementSnapshot&,
2483 const native_order::ExecutionAppliedEvent&);
2484 void consume_opening_fees(const native_order::ExecutionAppliedEvent&,
2485 const SourceId*);
2486 void consume_cohort_units(const SourceId&, const native_order::ExecutionAppliedEvent&);
2487 void consume_closed_trade_rows(const native_order::ExecutionAppliedEvent&,
2488 const PlacementSnapshot*);
2489 bool origin_is_pending(const native_order::RequestHandle&) const noexcept;
2490 // Roster positions, latest first, of the cohort's origins that are live
2491 // handles. origin_is_pending() accepts only live handles, so walking these
2492 // positions meets exactly the pending origins a reverse walk of the whole
2493 // roster meets, in the same order.
2494 std::vector<std::size_t> live_origin_positions(const CohortFacts&) const;
2495 // Lookups over the bookkeeping the adapter retains for
2496 // the whole run -- every origin a cohort ever accepted, every leg an exit
2497 // family ever placed -- answered from an index the native consumer keeps
2498 // for this adapter (its host cache; pine_adapter.cpp), since the adapter
2499 // is a by-value member of PineStrategyHost and so holds none. Each
2500 // answers exactly what the walk it replaces answers, and each walks when
2501 // the consumer keeps no cache. bound_consumer() is the bound host's
2502 // consumer, or null for a host that is not a PineStrategyHost.
2503 IExecutionConsumer* bound_consumer() const noexcept;
2504 // Whether any origin the cohort ever accepted is an opening on that side.
2505 bool cohort_opened_on_side(const CohortFacts&, bool is_long) const noexcept;
2506 // Whether a leg of that kind the (exit id, from_entry) family placed for
2507 // that origin was consumed: a member of the family neither working nor
2508 // suspended.
2509 bool origin_leg_consumed(std::uint64_t family_key, PineOrderFamily,
2510 const native_order::RequestHandle& origin) const noexcept;
2511 // Whether an immediate strategy.close was placed on that bar.
2512 bool immediate_close_placed_on(std::int32_t bar) const noexcept;
2513 void cancel_bracket_origin(native_order::RequestHandle);
2514 // executed_at: the ordinal a terminal receipt executed at, when the
2515 // observer reads it later; a later strategy.exit call's leg accepted
2516 // after it is not a sibling.
2517 void cancel_bracket_siblings(native_order::RequestHandle, std::uint64_t executed_at = 0);
2518 void cancel_exit_orders_for_full_close(const SourceId& from_entry);
2519 void cancel_exit_orders_for_full_close(const detail::ScratchByteSource& from_entry);
2520 void retire_in_position_exits_at_flat(bool preserve_pending_parents,
2521 bool dormant_rows_only,
2522 const PlacementSnapshot* paired_close);
2523 void materialize_relative_exits(PlacementSnapshot,
2524 const native_order::ExecutionAppliedEvent&);
2525 void withdraw_anchored_relative_legs(const SourceId* exit_id,
2526 const SourceId* from_entry);
2527 void withdraw_anchored_relative_readers(const detail::ScratchByteSource* exit_id,
2528 const detail::ScratchByteSource* from_entry);
2529 bool anchorable_relative_exit(const PendingRelativeExit&,
2530 native_order::RequestHandle& parent,
2531 bool& parent_long) const;
2532 struct RelativeLegShape {
2534 native_order::Trigger trigger = native_order::Limit{};
2535 double signed_ticks = 0.0;
2536 double operand_ticks = 0.0;
2537 };
2538 std::vector<RelativeLegShape> relative_leg_shapes(const PendingRelativeExit&,
2539 bool parent_long) const;
2540 bool armed_relative_legs_adoptable(const PlacementSnapshot& opening,
2541 const native_order::ExecutionAppliedEvent&,
2542 const std::vector<PendingRelativeExit>&) const;
2543 // TradingView's trigger projection of an exit level, one spelling for
2544 // exit() and for the kernel's arm hook: the limit leg's tick-quantized
2545 // threshold (raw on-grid under calc_on_order_fills) and the one-shot
2546 // trail activation touch.
2547 double exit_limit_trigger(double limit_price, double tick, bool exit_is_buy) const noexcept;
2548 double one_shot_trail_trigger(double activation, double source_trail_offset, double tick,
2549 bool exit_is_buy, bool quantized_activation) const noexcept;
2550 void materialize_pending_bracket_legs(
2551 const native_order::ExecutionAppliedEvent&);
2552 void stage_flat_children_before_parent(const SourceId&, std::int32_t,
2553 std::int64_t);
2554 bool defer_coof_tail() const noexcept;
2555 bool coof_fill_at_second_extreme() const noexcept;
2556 bool source_path_uses_high_first(const Bar&) const noexcept;
2557 bool coof_fill_on_path_point() const noexcept;
2558 bool coof_fill_at_path_point(double waypoint) const noexcept;
2559 bool coof_waits_for_next_sub_bar() const noexcept;
2560 bool coof_current_fill_was_forced_waypoint() const noexcept;
2561 double coof_next_waypoint(int* path_index = nullptr) const noexcept;
2562 bool coof_remaining_recrosses(double level, bool long_position) const noexcept;
2563 bool coof_leg_reaches_exit(const PlacementSnapshot&) const noexcept;
2564 void flush_coof_tail(bool openings_only = false,
2565 bool include_next_open = false);
2566 native_order::Owner owner_for_close(const SourceId&, bool dynamic) const;
2567 bool same_bar_market_tx_scope() const;
2568 bool fixed_unmargined_market_batch_scope() const;
2569 void reroute_fixed_entries_before_request(bool invalidate_batch = true);
2570 std::size_t accepted_entries_in_cycle(bool is_long, const SourceId& id) const;
2571 bool opposite_entry_opening_pending(bool is_long) const;
2572 void submit_entry_with_policy(native_order::Request request, PlacementSnapshot snapshot,
2573 const SourceId& id, const EntrySubmissionContext& context);
2574 void reroute_incomplete_fixed_batch(bool invalidate_batch);
2575 void flush_pending_same_bar_commands(bool flat_pair_follows = false,
2576 bool invalidate_batch = true);
2577 // The placement-time default quantity: the core's own conversion, read as
2578 // a query (NativeStrategyHost::native_sized_units) and floored by the
2579 // source. The source's money band and affordability gates consume the
2580 // number before any request exists, which is why it is computed here at
2581 // all; the arithmetic itself exists once, in the core.
2582 double default_sizing_units(const PineSizingSnapshot&) const;
2583 // The three pieces of the default quantity the source keeps as its own
2584 // policy: the money the percentage is taken of (with its equity mark and
2585 // its ten-significant-digit rounding), whether a percentage fee is
2586 // reserved out of it, and the lot floor applied to the core's quotient.
2587 double default_sizing_cash(const PineSizingSnapshot&) const;
2588 bool default_sizing_reserves_percent_fee() const noexcept;
2589 // A percent-of-equity default quantity under a cash commission (per
2590 // order or per contract) takes its percentage of strategy.equity and
2591 // leaves out the fee its own order pays.
2592 bool default_sizing_reserves_cash_fee() const noexcept;
2593 double default_sizing_lot_floor(double units) const noexcept;
2594 // A typed quantity (qty_type cash / percent_of_equity) names money, not
2595 // units: `money` converted at `price` by the core (native_sized_units:
2596 // CashValue, the percentage fee reserve) and floored onto the quantity
2597 // grid. typed_entry_units picks the money a typed entry's fill books --
2598 // the cash itself, or a percentage of the source's sizing equity at
2599 // `price` (of the hypothetical Flatten's realized balance for a typed
2600 // reversal); std::nullopt where that projection has no Pine host.
2601 double typed_quantity_units(double money, bool percent, double price, double fx) const;
2602 std::optional<double> typed_entry_units(const PlacementSnapshot&, double price, double fx,
2603 native_order::RequestHandle target) const;
2604 // The core intent a default-quantity declaration spells -- the source
2605 // money as a CashValue basis, the core's fee reserve, the raw quotient --
2606 // and the sided intent a re-lowerable opening carries. std::nullopt keeps
2607 // the host-resolved shape for every path the core cannot name.
2608 std::optional<native_order::Sized> default_sizing_shape(
2609 const PineSizingSnapshot&) const;
2610 std::optional<native_order::Sized> default_sizing_intent(
2611 const PineSizingSnapshot&, bool is_long) const;
2612 // True when the price the core will freeze for a Sized opening accepted
2613 // NOW is bit-for-bit the price this snapshot already froze. A command
2614 // deferred to a later execution point, or one whose sizing price the
2615 // source took from somewhere other than the signal rule, fails it and
2616 // keeps its host-resolved intent.
2617 bool core_sizing_price_matches(const PineSizingSnapshot&, bool is_long) const;
2618 double default_market_sizing_price(double mark, bool is_long) const noexcept;
2619 native_order::Trigger trigger_for(double limit_price, double stop_price) const;
2620 native_order::Group group_for(const std::string&, int, std::int64_t = 0) const;
2621 PineSizingSnapshot sizing_snapshot() const;
2622 std::uint64_t key_for(const SourceId&, const SourceId& = {}) const noexcept;
2623 void refresh_pending_view() noexcept;
2624 OrderBirth capture_order_birth() const;
2625 bool opening_market_short_scope(PineOrderFamily) const;
2626 void initialize_l4c_policy(PlacementSnapshot&, native_order::RequestHandle);
2627 void update_l4c_priority();
2628 void update_l4c_lifecycle(const native_order::ExecutionAppliedEvent&,
2629 const NativeDecisionContext&);
2630 using ReceiptHighWaterReader = std::uint64_t (*)(const NativeStrategyHost&) noexcept;
2631
2632 // The consumer's terminal-receipt high water as observe_terminal_receipts
2633 // last read it, advanced only on a run without an intrabar path (it stays
2634 // 0 under the magnifier). It no longer gates any read: it only feeds the
2635 // broker-state hash (pine_state_hash.cpp), so removing it would move
2636 // hash values.
2637 std::uint64_t terminal_receipt_cursor_ = 0;
2638 // Retired receipt-reader bindings that PineStrategyHost installed
2639 // at every begin. They only identified the host as a
2640 // PineStrategyHost. The readers ask the host's
2641 // PineStrategyHost view instead, so nothing reads or writes these two
2642 // slots; they keep this class's layout, which is part of
2643 // PineStrategyHost's and so of the generated script's ABI. Removing
2644 // both slots and ReceiptHighWaterReader requires an engine_script_run
2645 // epoch change because it alters that ABI's layout.
2646 ReceiptHighWaterReader event_high_water_reader_ = nullptr;
2647 ReceiptHighWaterReader terminal_receipt_high_water_reader_ = nullptr;
2648 bool is_declined_market_reversal(
2649 const native_order::MatchRejectedEvent&) const noexcept;
2650 bool follows_same_bar_declined_reversal(
2651 const PlacementSnapshot&, const NativePrecommitView&) const;
2652 bool bracket_belongs_to_reversal(
2653 const PlacementSnapshot&, const PlacementSnapshot&) const;
2654 void suspend_brackets_for_reversal(
2655 const PlacementSnapshot&, const exit_legs::Frame&, double open_price);
2656 void suspend_declined_reversal_brackets(
2657 const native_order::MatchRejectedEvent&);
2658 bool reissue_revives_declined_exit(const PlacementSnapshot&) const noexcept;
2659 void suspend_coof_declined_reversal_at_open(
2660 const Bar&, const NativeDecisionContext&);
2661 void hold_reversal_pair_brackets(const SourceId&);
2662 // A script cancel (strategy.cancel of an id, or cancel_all when `id` is
2663 // null) retires every leg of the lifecycle of each exit it withdrew --
2664 // the live requests in `cancelled` (incarnation, Cancelled event ordinal)
2665 // and every other exit row it names -- so no later margin call revives
2666 // one, including dormant rows and the other rows of the withdrawn
2667 // lifecycle.
2668 void retire_cancelled_exits(
2669 const SourceId* id,
2670 const std::vector<std::pair<std::uint64_t, std::uint64_t>>& cancelled);
2671 void purge_brackets_after_applied_reversal(const PlacementSnapshot&);
2672 void revive_brackets_after_margin(
2673 const native_order::ExecutionAppliedEvent&, const NativeDecisionContext&);
2674 int projected_pending_size() const noexcept;
2675 bool projected_pending_at(int index, const PlacementSnapshot*& snapshot,
2676 native_order::RequestHandle& handle) const noexcept;
2677 int projected_raw_pending_size() const noexcept;
2678 bool projected_raw_pending_at(int index, const PlacementSnapshot*& snapshot,
2679 native_order::RequestHandle& handle) const noexcept;
2680 static bool same_projected_order(const PlacementSnapshot& left,
2681 const PlacementSnapshot& right) noexcept;
2682 void apply_open_market_admission(const NativeDecisionContext&);
2683 void defer_open_marketable_sells(const Bar& bar);
2684 void admit_deferred_open_marketable_sells();
2685 void rearm_throttled_reopens();
2686 // A carried trailing stop reached on the bar's tick-built path: the
2687 // trail's distance re-priced for one bar, then restored (pine_adapter.cpp).
2688 void retune_carried_trails_for_tick_reach(const Bar&, const NativeDecisionContext&);
2689 void restore_carried_trail_retunes();
2690 void flush_pooc_marketable_limit_entry_fills(const Bar&, const NativeDecisionContext&);
2691 // The close pass's fills at a close tick: its own pass, or (after_close)
2692 // the reversing stops a same-bar close's fill releases.
2693 void fill_pooc_close_entries(double raw_close, const NativeDecisionContext&,
2694 bool after_close);
2695 void flush_pooc_marketable_exit_fills(const Bar&, const NativeDecisionContext&);
2696 // The close pass's exit fills at a close tick, and whether the calculation
2697 // still has an order the close fills at any price.
2698 void fill_pooc_close_exits(double raw_close, const NativeDecisionContext&);
2699 bool pooc_close_market_pending(const NativeDecisionContext&) const;
2700 bool pooc_close_entry_immediate_pending(const NativeDecisionContext&,
2701 const native_order::RequestHandle* except) const;
2702 // A from_entry "" exit without a quantity that is the book's only exit:
2703 // it closes its percentage of the position it fills against.
2704 bool fill_time_global_exit(const PlacementSnapshot&) const;
2705 // A held global exit with relative legs, entry by entry
2706 // (ExitBindingRuleSwitches::global_exit_per_entry_levels).
2707 bool per_entry_exit_scope() const;
2708 detail::ExitScratchRows<PerEntryExitChild> per_entry_open_entries(bool long_side) const;
2709 detail::ExitScratchRows<native_order::RequestHandle> per_entry_child_legs(
2710 const SourceId& exit_id, std::uint64_t origin) const;
2711 bool per_entry_exit_call(const SourceId& exit_id, const SourceId& from_entry, double limit,
2712 double stop, double trail_points, double trail_offset,
2713 double trail_price, double qty_percent, const std::string& comment,
2714 double qty, const std::string& oca_name, double profit_ticks,
2715 double loss_ticks, std::uint64_t command_sequence);
2716 void submit_per_entry_child(const PerEntryExit& exit, const PerEntryExitChild& child,
2717 std::uint64_t command_sequence);
2718 void per_entry_exits_at_entry_fill(const PlacementSnapshot& opening,
2719 const native_order::ExecutionAppliedEvent& event);
2720 void per_entry_exits_after_fill(const std::optional<PlacementSnapshot>& placement,
2721 const native_order::ExecutionAppliedEvent& event);
2722 void end_per_entry_exit(std::size_t index);
2723 void cancel_working_legs(
2724 const detail::ExitScratchRows<native_order::RequestHandle>& legs);
2725 void order_per_entry_point_ties();
2726 void record_market_review(admission::Checkpoint, int,
2727 const std::vector<native_order::RequestHandle>&);
2728 void refresh_pending_sizing_after_margin(
2729 const native_order::ExecutionAppliedEvent&, const NativeDecisionContext&);
2730 void apply_reversal_gap_bracket_policy(
2731 const Bar&, const NativeDecisionContext&, bool defer_trails = false);
2732 void reaccept_gapped_bracket_behind_same_id_add(
2733 const Bar&, const NativeDecisionContext&);
2734 void order_open_marketable_limit_entries(const Bar&, const NativeDecisionContext&);
2735 void apply_terminal_explicit_market_policy(const NativeDecisionContext&);
2736 void apply_terminal_explicit_market_policy(const NativeDecisionContext&, bool execute_now);
2737 bool enqueue_pooc_fifo_close(const SourceId&, const std::string&,
2738 std::uint64_t, std::uint64_t);
2739 void observe_close_ledger(const native_order::ExecutionAppliedEvent&,
2740 const PlacementSnapshot*);
2741 bool entry_order_pending(const SourceId&) const;
2742 bool entry_order_working(const SourceId* id) const;
2743 bool priced_entry_order_working(const SourceId* id) const;
2744 bool margins_disabled() const noexcept;
2745 bool whole_level_exit(double limit_price, double stop_price, double trail_points,
2746 double trail_price, double profit_ticks, double loss_ticks, double qty,
2747 double qty_percent) const noexcept;
2748 bool open_lot_of(const SourceId& id) const;
2749 bool standing_exit(const SourceId& exit_id, const SourceId& from_entry) const;
2750 void unvoid_exit(PlacementSnapshot& row);
2751 void execute_or_withdraw_close(native_order::RequestHandle close, bool void_issue);
2752 void withdraw_void_exits(const SourceId& entry_id);
2753 void settle_void_global_exits(bool market_entry);
2754 void bind_pending_exits_to_fill(const SourceId& entry_id);
2755 void withdraw_unplaced_cap_adds(const NativeDecisionContext& context);
2756 void stash_id_bound_exits(const SourceId& entry_id);
2757 void rearm_id_bound_exits(const SourceId& entry_id);
2758 void place_entry(const SourceId& id, bool is_long, double limit_price, double stop_price,
2759 double qty, const std::string& comment, const std::string& oca_name,
2760 int oca_type, int qty_type);
2761 void credit_close_ledger(const SourceId&, double units);
2762 void book_close_ledger(const SourceId&, double units);
2763
2764 // @source-state begin
2765 NativeStrategyHost* host_ = nullptr;
2766 mutable std::shared_ptr<detail::ExitScratchContext> exit_scratch_;
2767 // Retired ownership slots, kept empty to preserve generated host layout.
2768 // Trade/dependency scratch now belongs to the admitted native run.
2769 mutable std::shared_ptr<detail::CommandScratchRun> trade_scratch_run_;
2770 mutable std::shared_ptr<detail::CommandScratchRun> dependency_scratch_run_;
2771 const PineScheduler* input_scheduler_ = nullptr;
2772 PineStrategyConfig config_{};
2773 StagedConfiguration staged_{};
2774 mutable std::uint64_t run_counter_ = 0;
2775 std::uint64_t source_sequence_ = 0;
2776 std::uint64_t command_ordinal_ = 0;
2777 std::uint64_t broker_open_epoch_ = 0;
2778 std::int64_t last_broker_open_ms_ = std::numeric_limits<std::int64_t>::min();
2779 std::uint64_t source_command_sequence_ = 0;
2780 std::int32_t entry_attempt_bar_ = -1;
2781 std::uint32_t entry_attempts_on_bar_ = 0;
2782 // 192 KiB per adapter, not per engine. One adapter owns each cache;
2783 // these private disk read caches are never shared across threads.
2784 CohortTable cohorts_by_id_{pineforge::detail::IdTableReadCache::kCacheBytes};
2785 void observe_pending_scan(PendingScanCounter&) const noexcept;
2786 CohortTable::iterator require_cohort(const SourceId&);
2788 std::unordered_map<std::uint64_t, detail::LiveSourceHandle> live_by_source_key_;
2789 std::unordered_map<std::uint64_t, BracketRoster> bracket_families_;
2790 // Immutable, wholly settled families. All appends restore through
2791 // bracket_family(); exit_cancel_bracket also consults this archive. The keyed
2792 // digest is updated on every insertion/removal, never cached over a
2793 // mutable roster. Run identity has the same normalization as v4 handles.
2794 std::unordered_map<std::uint64_t, BracketRoster::Settled> settled_bracket_families_;
2795 std::uint64_t settled_bracket_digest_ = 0;
2796 BracketRoster& bracket_family(std::uint64_t key);
2797 void archive_settled_bracket_families();
2798 void clear_bracket_families() noexcept;
2799 std::vector<PendingBracketLeg> pending_bracket_legs_;
2800 std::vector<PendingEntry> pending_entries_;
2801 std::vector<DelayedMarketOrder> delayed_market_orders_;
2802 std::vector<DeferredOpenMarketableSell> deferred_open_marketable_sells_;
2803 // Flat stop entries the per-bar priced-entry throttle refused; re-armed
2804 // as their original stop at the bar close (validate_precommit is const).
2805 mutable std::vector<PlacementSnapshot> throttled_reopen_rearm_;
2806 int entry_openings_interval_index_ = -1;
2807 int entry_openings_this_interval_ = 0;
2808 std::vector<PendingSameBarCommand> pending_same_bar_commands_;
2809 std::vector<SourceShadowPending> source_shadow_pending_;
2810 double pending_same_bar_close_qty_ = 0.0;
2811 std::vector<PendingRelativeExit> pending_relative_exits_;
2812 // Mutable: the kernel's arm hook is const and records the level it
2813 // installed on the leg it restated.
2814 mutable std::vector<AnchoredRelativeLeg> anchored_relative_legs_;
2815 AnchoredRelativeStats anchored_relative_stats_{};
2816 std::int64_t anchored_cohort_sequence_ = 0;
2817 std::vector<PerEntryExit> per_entry_exits_;
2818 std::vector<PendingCoofRequest> pending_coof_requests_;
2819 std::vector<PendingMarginRevival> pending_margin_revivals_;
2820 std::vector<native_order::RequestHandle> live_handles_;
2821 std::vector<native_order::RequestHandle> first_open_newborns_;
2822 // The dropped-close receipts were a log no decision reads;
2823 // v4 keeps their count and a running digest, each folded once
2824 // (fold_dropped_close, pine_state_hash.cpp).
2825 std::uint64_t dropped_close_count_ = 0;
2826 std::uint64_t dropped_close_digest_ = 1469598103934665603ULL;
2827 std::vector<OpenEntryFeeFact> open_entry_fees_;
2828 // Current executions settle synchronously, while their generic Applied
2829 // notification is delivered after the enclosing callback. Record the
2830 // source-cohort debit so a second immediate command sees the new basis,
2831 // then suppress just that duplicate debit at notification delivery.
2832 std::unordered_set<std::uint64_t> current_debited_applied_ordinals_;
2833 std::unordered_set<std::uint64_t> intraday_loss_relabel_ordinals_;
2834 pineforge::detail::IdPairStore<std::int64_t, true> consumed_partial_exit_cycles_{
2836 std::unordered_set<std::uint64_t> bracket_shadowed_openings_;
2837 std::unordered_map<SourceId, NamedEntryCancelToken> named_entry_cancel_tokens_;
2838 // An exit bound to its entry id whose working order a strategy.cancel of
2839 // the id withdrew (PlacementSnapshot::pending_bound_exit): TradingView
2840 // keeps the binding, so the id's next entry order re-arms the exit as
2841 // called. The native request ends with its parent order, so the call is
2842 // kept here, one per (exit id, entry id) pair.
2843 struct IdBoundExit {
2844 SourceId exit_id;
2845 SourceId from_entry;
2846 PineExitLevels levels;
2847 double qty_percent = std::numeric_limits<double>::quiet_NaN();
2848 double qty = std::numeric_limits<double>::quiet_NaN();
2849 std::string comment;
2850 std::string oca_name;
2851 };
2852 std::vector<IdBoundExit> id_bound_exits_;
2853 // The close ledger: per id, the units entered under it that no close has
2854 // booked yet (close_logical_units_), and the same units by opening fill
2855 // in fill order (close_ledger_records_), which a booking beyond its id's
2856 // own units spills over oldest first.
2857 std::map<SourceId, double> close_logical_units_;
2858 std::vector<CloseLedgerRecord> close_ledger_records_;
2859 // The pyramiding records: one per entry fill, unmerged, with the incarnation
2860 // of the opening that filled it, booked as the close ledger books
2861 // (book_pyramiding_records); kept only under pyramiding above one.
2862 struct PyramidingRecord {
2863 SourceId id{};
2864 std::uint64_t incarnation = 0;
2865 double units = 0.0;
2866 };
2867 std::vector<PyramidingRecord> pyramiding_records_;
2868 // The partial strategy.exit children that filled, keyed
2869 // exit_id "\x1f" from_entry, to the entry incarnation they filled under.
2870 std::unordered_map<SourceId, std::uint64_t> exit_tombstones_;
2871 std::map<std::uint64_t, CloseCallsiteState> close_batch_callsites_;
2872 std::int32_t close_batch_bar_ = -1;
2873 std::uint64_t close_batch_queue_sequence_ = 0;
2874 double close_batch_pending_debt_ = 0.0;
2875 double close_batch_admitted_total_ = 0.0;
2876 std::uint64_t receipt_cursor_ = 0;
2877 std::uint64_t last_applied_ordinal_ = 0;
2878 bool materializing_relative_ = false;
2879 // The parent whose fill the current materialize_relative_exits re-run
2880 // serves; only that parent's armed anchored legs are adoptable.
2881 native_order::RequestHandle materializing_parent_{};
2882 std::int64_t current_position_cycle_ = 0;
2883 int current_position_sign_ = 0;
2884 std::uint64_t next_sequential_group_ = 0;
2885 bool source_batch_mutated_ = false;
2886 bool coof_recalc_active_ = false;
2887 bool coof_first_open_ = false;
2888 bool coof_open_stop_next_waypoint_ = false;
2889 // Whether the recalculating fill booked the forced_execution_price the
2890 // adapter set on its request (a fill AT a path point), not the matcher's
2891 // own price. It sits in the padding before the
2892 // next 8-byte member, so the class layout does not move.
2893 bool coof_fill_forced_ = false;
2894 std::uint64_t coof_market_entry_recalc_incarnation_ = 0;
2895 std::uint64_t coof_market_entry_recalc_fill_seq_ = 0;
2896 std::uint64_t coof_current_fill_seq_ = 0;
2897 double coof_fill_cursor_t_ = std::numeric_limits<double>::quiet_NaN();
2898 double coof_fill_target_print_ = std::numeric_limits<double>::quiet_NaN();
2899 NativeDecisionContext coof_context_{};
2900 Bar coof_script_bar_{};
2901 bool coof_script_bar_valid_ = false;
2902 // The per-script-bar POOC close basis was a log no
2903 // decision reads (the first basis each bar); v4 keeps the last bar key,
2904 // the count and a running digest of the first basis of each new bar.
2905 std::int64_t pooc_close_basis_last_bar_ = std::numeric_limits<std::int64_t>::min();
2906 std::uint64_t pooc_close_basis_count_ = 0;
2907 std::uint64_t pooc_close_basis_digest_ = 1469598103934665603ULL;
2908 double pooc_open_basis_ = 0.0;
2909 std::int64_t pooc_open_script_bar_ = std::numeric_limits<std::int64_t>::min();
2910 std::int64_t close_all_pending_script_bar_ = std::numeric_limits<std::int64_t>::min();
2911 // The script bar on which the script last called strategy.close(<held
2912 // id>) or strategy.close_all() while a position was open, and that
2913 // position's side (+1 long, -1 short): the close-first fact entry() reads.
2914 std::int64_t close_first_script_bar_ = std::numeric_limits<std::int64_t>::min();
2915 std::int32_t close_first_side_ = 0;
2916 double last_fx_rate_ = std::numeric_limits<double>::quiet_NaN();
2917 std::int64_t position_open_script_bar_ = std::numeric_limits<std::int64_t>::min();
2918 std::uint64_t position_open_epoch_ = 0;
2919 std::int32_t position_open_bar_index_ = -1;
2920 NativePathPhase position_open_phase_ = NativePathPhase::None;
2921 bool position_open_priced_ = false;
2922 std::int64_t last_margin_call_script_bar_ = std::numeric_limits<std::int64_t>::min();
2923 // The driver point ordinal whose kernel margin check the adapter's
2924 // own scheduling admitted. The kernel offers its check points at every
2925 // bar; exactly the one TradingView also checks at is let through, and
2926 // only for the driver point it was armed on.
2927 //
2928 // Deliberately NOT folded into the broker state hash. Driver point
2929 // ordinals are strictly monotone within a run and this value is only ever
2930 // compared for equality with the current point's, so from the first point
2931 // after the one it names it can never match again: it carries no
2932 // information across any observation boundary the hash identifies. Folding
2933 // it would move every adapter state hash (and the pinned hash-neutrality
2934 // evidence in tests/test_adapter_report_relower.cpp) for a value that is
2935 // dead by the time the next one is taken.
2936 std::uint64_t kernel_margin_path_point_ = std::numeric_limits<std::uint64_t>::max();
2937 // The driver point ordinal at which the adapter re-sizes a RESTING kernel
2938 // liquidation from the post-exit book (matching
2939 // the legacy cancel-and-reschedule after a priced bracket leg of this
2940 // script bar fills). A point named here is admitted only while a slice
2941 // actually rests, because the legacy broker did nothing at all when none
2942 // did. Same hash argument as kernel_margin_path_point_ above: a strictly
2943 // monotone ordinal compared only for equality with the current point's.
2944 std::uint64_t kernel_margin_resize_point_ = std::numeric_limits<std::uint64_t>::max();
2945 // The kernel check point whose adverse mark is the bar's close alone: its
2946 // call is the close's, taken after the script (close_point_margin_call).
2947 // And the script bar whose script called strategy.cancel_all(), which
2948 // withdraws that call. Same hash argument as kernel_margin_path_point_:
2949 // each is compared only for equality with the current point or bar, and
2950 // is dead once that bar's close has been checked.
2951 std::uint64_t close_margin_point_ = std::numeric_limits<std::uint64_t>::max();
2952 std::int64_t close_margin_cancelled_bar_ = std::numeric_limits<std::int64_t>::min();
2953 // The script bar whose close owes a follow-up (follow_margin_call), booked
2954 // there after the script, and the units it owes. Same hash argument: set
2955 // on the bar's path, dead once its close is checked.
2956 std::int64_t close_margin_follow_up_bar_ = std::numeric_limits<std::int64_t>::min();
2957 double close_margin_follow_up_units_ = 0.0;
2958 // The script bar whose path checks the adapter rests itself, point by
2959 // point, after a call at a point before the bar's adverse extreme
2960 // (rest_waypoint_margin_call). Same hash argument: set on the bar's path,
2961 // cleared at its close.
2962 std::int64_t waypoint_chain_bar_ = std::numeric_limits<std::int64_t>::min();
2963 // The script bar whose close queued a call for the next open
2964 // (close_point_margin_call): one the script's cancel_all() withdrew,
2965 // placed again behind its orders, or a one-unit call the close's fill
2966 // would not restore, tried again there.
2967 std::int64_t close_margin_open_bar_ = std::numeric_limits<std::int64_t>::min();
2968 // Retired checkpoint deferral: a carried
2969 // process_orders_on_close short used to wait behind its bar's close market
2970 // fills; TradingView checks its checkpoint over
2971 // the bar's path before the script instead. Never set now, and kept as
2972 // the constant it folds into the broker-state hash.
2973 std::int64_t pooc_close_checkpoint_deferred_ms_ = std::numeric_limits<std::int64_t>::min();
2974 std::int32_t signal_close_mc_event_bar_ = -1;
2975 std::int64_t signal_close_mc_position_cycle_ = 0;
2976 std::uint64_t signal_close_mc_entry_incarnation_ = 0;
2977 std::uint64_t signal_close_mc_fill_seq_ = 0;
2978 double signal_close_mc_before_qty_ = std::numeric_limits<double>::quiet_NaN();
2979 double signal_close_mc_remaining_qty_ = std::numeric_limits<double>::quiet_NaN();
2980 std::uint64_t last_margin_call_event_ordinal_ = 0;
2981 std::uint64_t last_margin_call_entry_incarnation_ = 0;
2982 std::int64_t last_margin_call_position_cycle_ = 0;
2983 bool last_margin_call_at_script_close_ = false;
2984 double last_margin_call_closed_units_ = 0.0;
2985 double last_margin_call_remaining_units_ = 0.0;
2986 // TradingView's realized money (gain_loss_money): every closed trade's
2987 // profit, in booking order, summed into gains and into losses apart.
2988 double source_gains_ = 0.0;
2989 double source_losses_ = 0.0;
2990 std::uint64_t source_money_folded_ = 0;
2991 std::int64_t risk_coof_direct_script_bar_ = std::numeric_limits<std::int64_t>::min();
2992 std::uint64_t cap_latest_fill_ = 0;
2993 bool source_margin_call_enabled_ = true;
2994 Bar policy_script_bar_{};
2995 bool policy_script_bar_valid_ = false;
2996 std::unordered_set<std::uint64_t> market_pyramid_adds_;
2997 std::unordered_map<std::uint64_t, NativeTrailState> trail_state_at_open_;
2998 // The trails retune_carried_trails_for_tick_reach re-priced for the bar
2999 // in flight, each with its own trailing distance; empty between bars.
3000 std::vector<std::pair<native_order::RequestHandle, double>> carried_trail_retunes_;
3001 bool stream_mode_ = false;
3002 SourceDayLedger day_ledger_{};
3003 PineRiskState risk_{};
3004 ShortSeedPlan short_seed_{};
3005 PendingShortSeedPlan pending_short_seed_{};
3006 native_order::RequestHandle short_seed_long_candidate_{};
3007 int last_bar_dual_entry_path_ = 0;
3008 std::int64_t last_bar_dual_entry_script_open_ms_ =
3009 std::numeric_limits<std::int64_t>::min();
3010 PendingIntentView pending_view_{};
3011 ::pineforge::detail::ScratchSequence<std::uint8_t> trade_exit_phase_;
3012public:
3014 std::uint64_t publications = 0, tail_rows = 0, comparisons = 0;
3017 std::uint64_t scratch_operations = 0, replacements = 0;
3018 std::size_t retained_placements = 0;
3019 };
3020private:
3021 // Derived ordering evidence and diagnostics are absent from the state fold.
3022 struct ExitGroupCertificate {
3023 const TradeLedger* ledger = nullptr;
3024 std::size_t base = 0, start = 0, end = 0;
3025 bool rows_valid = false;
3026 } exit_group_certificate_;
3027 ExitGroupWork exit_group_work_{};
3028 bool exit_group_certified(const TradeLedger&) const noexcept;
3029 void invalidate_exit_group(std::size_t first, std::size_t count = 1) noexcept;
3030 void certify_exit_group(const TradeLedger&, std::size_t start, bool ordered = true);
3031 void finish_exit_group_sort(::pineforge::detail::CommandScratchStore&,
3032 const ::pineforge::detail::ScratchStoreStats&) noexcept;
3033 // TradingView's entry-id table (global_exit_child_ranks): every entry id
3034 // in the order of its first fill, never removed, with a running digest,
3035 // and the most keys it ever held counting one transient key per live
3036 // strategy.close / close_all order -- and, for the alternative the tapes
3037 // leave open, per live strategy.order market order too.
3038 ::pineforge::detail::DiskIdTable<std::uint32_t> exit_rank_keys_{
3040 std::uint64_t exit_rank_keys_digest_ = 1469598103934665603ULL;
3041 std::uint32_t exit_rank_peak_ = 0;
3042 std::uint32_t exit_rank_alt_peak_ = 0;
3043 void require_tree_precommit(const NativePrecommitView&) const;
3044 void require_exit_rank_key(const SourceId&) const;
3045 void note_exit_rank_key(const SourceId&);
3046 void note_exit_rank_transients();
3047 // The exit phases of the trades before exit_phase_final_
3048 // are final (a same-bar exit sort reorders only its own bar's rows) and
3049 // folded once into exit_phase_digest_; a write below the mark refolds.
3050 std::size_t exit_phase_final_ = 0;
3051 std::uint64_t exit_phase_digest_ = 1469598103934665603ULL;
3052 // The v4 admission fold uses global event counts. Its retired prefix is
3053 // the restart point; the window may still change within a command/bar.
3054 // Folding at each script-bar end keeps recording-off runs bounded too.
3055 // admission_events_last_ validates the boundary without indexing a row
3056 // below the journal's retired prefix.
3057 mutable std::size_t admission_events_folded_ = 0;
3058 mutable std::uint64_t admission_events_last_ = 0;
3059 mutable std::uint64_t admission_events_digest_ = 1469598103934665603ULL;
3060 // A short's lagged follow-up (MarginScheduleSwitches::
3061 // lagged_short_follow_up) owed at a point of a script bar's path, booked
3062 // there ahead of that point's own check: the script bar, the point (0
3063 // its open, 1 and 2 its extremes in path order, 3 its close) and the
3064 // units. A process_orders_on_close close owes the next bar's open.
3065 std::int64_t owed_follow_up_bar_ = std::numeric_limits<std::int64_t>::min();
3066 std::int32_t owed_follow_up_point_ = -1;
3067 double owed_follow_up_units_ = 0.0;
3068 // A margin call booked at a close after the script (the close's own
3069 // check or a follow-up owed there): the script bar, the book the script
3070 // saw and the units called. A reversal the script placed there keeps the
3071 // close quantity it saw (MarginScheduleSwitches::frozen_reversal_close).
3072 std::int64_t close_call_after_script_bar_ = std::numeric_limits<std::int64_t>::min();
3073 double close_call_after_script_book_ = 0.0;
3074 double close_call_after_script_units_ = 0.0;
3075 // The opening checkpoint a market entry opening the book at an open owes
3076 // once the exit legs it released, marketable at that open, have filled
3077 // there, on the book they leave (MarginOpeningSwitches::
3078 // open_marketable_exit_first): the script bar, the entry's request and id
3079 // and its fill.
3080 std::int64_t open_exit_first_bar_ = std::numeric_limits<std::int64_t>::min();
3081 std::uint64_t open_exit_first_origin_ = 0;
3082 SourceId open_exit_first_id_{};
3083 double open_exit_first_fill_ = 0.0;
3084 // @source-state end
3085 // Diagnostic work counter: written from const methods, never read by a
3086 // decision, deliberately outside source state and the broker hash.
3087 mutable PendingScanWork pending_scan_work_{};
3088 // Derived decision cache, maintained from CohortFacts::opened pushes,
3089 // erases and whole-vector clears, asserted against those hashed vectors
3090 // in non-Release builds and checked by the fixture oracle in Release.
3091 std::size_t cohort_opened_slot_count_ = 0;
3092 bool fixture_verify_source_entry_slots_ = false;
3093 bool fixture_bracket_remove_precheck_ = true;
3094 // Set while an open's calls that TradingView does not repeat at one
3095 // price book (run_close_sized_open_calls, open_call_owing_follow_up); the
3096 // requests submitted meanwhile are listed in unrepeated_margin_calls_
3097 // until their on_applied, dispatched after the open's hook returns, reads
3098 // them there (recheck_at_fill). Not state: the flag is set and cleared
3099 // within one call, and the list lives within one driver point -- every
3100 // listed request is applied at the open that placed it, and each
3101 // on_bar_open empties it.
3102 bool open_call_sequence_ = false;
3103 std::vector<std::uint64_t> unrepeated_margin_calls_;
3104 // Install-time magnifier fact from NativeBeginArgs. Scheduler already
3105 // folds retained_.bar_magnifier; this copy is the host-kind-free query
3106 // for qualify_short_seed_plan.
3107 bool bar_magnifier_ = false;
3108 // The bound host's PineStrategyHost view (pine_view_of),
3109 // kept with the host it was taken from. It was a thread_local memo, and a
3110 // strategy module is dlopen'd, where every thread_local read is a dynamic
3111 // TLS lookup. Not state: a function of host_.
3112 mutable const NativeStrategyHost* pine_view_host_ = nullptr;
3113 mutable PineStrategyHost* pine_view_ = nullptr;
3114 PineStrategyHost* pine_view_of(NativeStrategyHost* host) const noexcept;
3115 // erase_retired_rows()'s working sets, kept so that a bar
3116 // that erases nothing allocates nothing once they have grown (a quiet bar
3117 // must not allocate: tests/test_adapter_quiet_bar.cpp). Not state: every
3118 // sweep empties the working sets first; only the derived memos described
3119 // below survive a sweep.
3120 struct RetiredRowScratch {
3121 std::vector<std::uint64_t> roots;
3122 std::vector<std::uint64_t> askable_origins;
3123 std::vector<std::uint64_t> live_groups;
3124 std::vector<std::int32_t> source_bars;
3125 std::vector<std::pair<std::uint64_t, const PlacementSnapshot*>> candidates;
3126 std::vector<std::uint64_t> candidate_names;
3127 std::vector<std::uint64_t> first_reissues;
3128 std::vector<const PlacementSnapshot*> origin_pins; // Rebuilt each sweep; not state.
3129 std::vector<std::uint64_t> doomed;
3130 // K1 legs that supersession can release (incarnation,
3131 // row, read-through high water, released flag); released incarnations sorted.
3132 struct HeldLeg {
3133 std::uint64_t incarnation = 0;
3134 const PlacementSnapshot* row = nullptr;
3135 std::uint64_t read_through = 0;
3136 bool released = false;
3137 };
3138 std::vector<HeldLeg> held;
3139 std::vector<std::uint64_t> released;
3140 // A memo that survives sweeps (clear() keeps it; a run begin empties
3141 // it): per leg held at the last test, the table's high water it was
3142 // found unsuperseded through. It answers what reading every row would:
3143 // the fields the test reads never change once a row is placed, and a
3144 // row that could supersede the leg is newer than every row it covers.
3145 std::vector<std::pair<std::uint64_t, std::uint64_t>> unsuperseded;
3146 // K3's exact first-reissue lookup over immutable identity/age/stop
3147 // fields. New candidates read all rows; unresolved old candidates
3148 // read only rows above reissue_high_water. A pinned first witness
3149 // needs no more reads. A run begin clears this derived memo.
3150 std::uint64_t reissue_high_water = 0;
3151 std::int64_t reissue_cycle = -1;
3152 std::vector<std::pair<std::uint64_t, std::uint64_t>> reissues;
3153 std::vector<std::pair<std::uint64_t, std::uint64_t>> next_reissues;
3154 std::vector<std::size_t> new_candidates;
3155 std::vector<std::size_t> known_candidates;
3156 std::vector<std::size_t> unresolved_candidates;
3157 // Anchored legs may be remembered after newer incarnations. They
3158 // are new peers too, even below the high water. Kept until a K3
3159 // pass consumes them (quiet-bar returns must not discard them).
3160 std::vector<std::uint64_t> adopted_reissues;
3161 // release_closed_cohort_origins()'s working sets --
3162 // the chain roots of the working requests, the openings of the open
3163 // lots and those no roster holds as a root, the origins leaving a
3164 // roster (cohort, incarnation), and the handle a lot is looked up by
3165 // (its run identity copied only when it changes) -- and whether a
3166 // receipt read has seen a command other than Accepted since its last
3167 // pass (a memo: fresh working roots release nothing; begin sets it).
3168 std::vector<std::uint64_t> working_roots;
3169 std::vector<std::uint64_t> lot_openings;
3170 std::vector<std::uint64_t> successor_lots;
3171 std::vector<std::pair<native_order::CohortHandle, std::uint64_t>> released_origins;
3172 native_order::RequestHandle lot_probe{};
3173 bool roster_pass_due = true;
3174 // Derived live-family counts stay in step with placements and live handles
3175 // at their shared writer; they add no lifecycle or hash state. Contiguous
3176 // Entry..Risk slots count families; extra slots count unpriced Order rows
3177 // and rows eligible for dynamic-exit or capture policy.
3178 static constexpr std::size_t family_count =
3179 static_cast<std::size_t>(PineOrderFamily::Risk) + 1;
3180 static constexpr std::size_t market_order_slot = family_count;
3181 static constexpr std::size_t exit_policy_slot = family_count + 1;
3182 std::size_t live_families[family_count + 2]{};
3183 // True certifies the existing retire_origin_prefixes early return:
3184 // each nonempty front has a placement. Only placement erasure can
3185 // falsify it; a new origin is appended after remember().
3186 bool origin_fronts_pinned = true;
3187 void clear() noexcept {
3188 roots.clear();
3189 askable_origins.clear();
3190 live_groups.clear();
3191 source_bars.clear();
3192 candidates.clear();
3193 candidate_names.clear();
3194 first_reissues.clear();
3195 origin_pins.clear();
3196 doomed.clear();
3197 held.clear();
3198 released.clear();
3199 next_reissues.clear();
3200 new_candidates.clear();
3201 known_candidates.clear();
3202 unresolved_candidates.clear();
3203 }
3204 };
3205 RetiredRowScratch retired_row_scratch_;
3206 void remember_live(const native_order::RequestHandle&, PlacementSnapshot&&);
3207 void forget_live(const native_order::RequestHandle&) noexcept;
3208 void requeue_live(const native_order::RequestHandle&);
3209 void clear_live() noexcept;
3210 void erase_placement(std::uint64_t);
3211 void count_live(const PlacementSnapshot&, bool adding) noexcept;
3212 void assert_live_counts() const noexcept;
3213 bool has_live_family(PineOrderFamily) const noexcept;
3214 bool has_live_exit_policy() const noexcept;
3215 bool has_live_trail_policy() const noexcept;
3216 // The configuration's part of the margin and admission scopes, which the
3217 // per-bar path asks several times a bar: each a conjunction of reads of
3218 // config_ and staged_ alone, formed again whenever either is set
3219 // (set_configuration, set_staged_configuration), the only way either
3220 // changes. Not state: a function of config_ and staged_. A run outside a
3221 // scope answers it here without the call; inside, the scope's own tests
3222 // run as before.
3223 struct StagedScopeFacts {
3224 bool positive_grid = false; // a lot step above 0
3225 bool unit_grid = false; // a lot step in (0, 1]
3226 bool unit_point_money = false; // point value 1, no FX series
3227 bool unit_money = false; // and an account FX rate of 1
3228 bool mintick = false; // a finite positive mintick
3229 };
3230 StagedScopeFacts staged_scopes_{};
3231 bool lot_grid_pinned_ = false; // pinned_lot_grid()
3232 bool gain_loss_configured_ = false; // gain_loss_regime()'s configuration
3233 bool slipped_long_configured_ = false; // slipped_long_margin_scope()'s
3234 bool unified_configured_ = false; // unified_admission_scope()'s
3235 bool ledger_records_configured_ = false; // the pyramiding records'
3236 bool pooc_fee_configured_ = false; // pooc_fee_units()'s
3237 void refresh_configuration_scopes(const PineStrategyConfig& config) noexcept;
3238 // After a source evaluation, queue this bar's marketable
3239 // flat entries in TradingView's order at their shared fill point.
3240 void order_same_point_entries();
3241 double pending_opposite_market_units(bool is_long, std::int64_t script_open_ms, const SourceId& id) const;
3242 // The configurations those flat-pair rules cover,
3243 // and whether a pair's opposite member fills next at the same price.
3244 bool same_point_pair_scope() const;
3245 bool same_point_two_leg_book(const SourceId& id, std::int64_t script_open_ms) const;
3246 bool same_point_pair_fill_follows(const PlacementSnapshot& filled,
3247 const native_order::RequestHandle& filled_handle,
3248 double price) const;
3249 // A flat market-and-priced pair at a process_orders_on_close close.
3250 bool fill_pooc_close_pair(double raw_close, const NativeDecisionContext& context);
3251 void finish_admission_bar();
3252 void on_bar_close_policy(const Bar&, const NativeDecisionContext&);
3253 void fold_admission_events() const;
3254 std::uint64_t checked_live_key(const SourceId&) const;
3255 detail::LiveSourceHandle live_source_value(native_order::RequestHandle, const SourceId&) const;
3256 void cancel_reversal_exit_owners(const std::vector<SourceId>& preserved,
3257 detail::ExitScratchRows<SourceId>& selection);
3258 // Diagnostic only; never folded into continuation state.
3259 mutable std::uint64_t projected_raw_pending_calls_ = 0;
3260 // A disposable projection of current raw positions, never row pointers.
3261 // Mutable placement borrows and every queue writer invalidate its inputs.
3262 std::uint64_t pending_projection_version_ = 0;
3263 mutable std::uint64_t projected_pending_version_ = 0;
3264 mutable std::uint64_t projected_placement_version_ = 0;
3265 mutable bool projected_pending_valid_ = false;
3266 mutable std::vector<int> projected_pending_indexes_;
3267 mutable std::vector<int> projected_pending_next_;
3268 bool ensure_projected_pending_index() const;
3269 int projected_pending_next(int index) const noexcept;
3270 int projected_raw_pending_next(int raw) const noexcept;
3271 int projected_level_resolved(const PlacementSnapshot&) const noexcept;
3272 // Bump the version before mutation. Do not keep the returned reference
3273 // across another pending_write of the same container.
3274 template<class Row> Row& pending_write(Row& row) noexcept {
3275 refresh_pending_view();
3276 return row;
3277 }
3278 static int scan_projected_pending_size(const PineExecutionAdapter&) noexcept;
3279 static bool scan_projected_pending_at(
3280 const PineExecutionAdapter&, int index, const PlacementSnapshot*&,
3281 native_order::RequestHandle&, int* raw_index = nullptr) noexcept;
3282
3283public:
3284 // Cumulative physical work, reset at run begin. No decision or state fold
3285 // reads these integers. Population snapshots are explicit diagnostics.
3287 std::uint64_t placement_sweeps = 0, prefix_sweeps = 0;
3288 std::uint64_t row_visits = 0, pin_rows = 0, gate_rows = 0;
3289 std::uint64_t candidate_rows = 0, final_rows = 0;
3290 std::uint64_t pin_comparisons = 0, peer_visits = 0, held_comparisons = 0;
3291 std::uint64_t k3_pairs = 0, root_visits = 0, family_visits = 0;
3292 std::uint64_t cohort_rows = 0, certificate_rows = 0, fronts_tested = 0;
3294 std::uint64_t rows_erased = 0, origins_retired = 0;
3295 // Slots include empty dense-window slots, as well as returned old rows.
3296 // Backing counts are restored/spilled cohort records, not page I/O.
3299 // Debug repeats the sweep independently of the decision-path counts.
3301 };
3303 std::uint64_t m = 0, R = 0, C = 0;
3304 std::uint64_t live = 0, pending = 0, owners = 0, opened = 0, retired_exit_rows = 0;
3305 };
3306 const RetirementWork& retirement_work() const noexcept { return retirement_work_; }
3307 // Explicit observation only: visits placements to classify nonlive exits.
3308 RetirementPopulation retirement_population() const;
3309
3310private:
3311 RetirementWork retirement_work_{};
3312 struct RetirementObservation {
3313 PineExecutionAdapter& adapter;
3314 RetirementWork& work;
3315 std::uint64_t slots, restored, spilled, spill_visits;
3316 explicit RetirementObservation(PineExecutionAdapter& owner) noexcept
3317 : RetirementObservation(owner, owner.retirement_work_) {}
3318 RetirementObservation(PineExecutionAdapter& owner, RetirementWork& destination) noexcept
3319 : adapter(owner), work(destination), slots(owner.placement_.visited_slots()),
3320 restored(owner.cohorts_by_id_.work_counters().rows_restored),
3321 spilled(owner.cohorts_by_id_.work_counters().rows_spilled),
3322 spill_visits(owner.cohorts_by_id_.work_counters().rows_visited_by_spill) {}
3323 ~RetirementObservation() {
3324 const auto& backing = adapter.cohorts_by_id_.work_counters();
3325 work.placement_slots += adapter.placement_.visited_slots() - slots;
3326 work.backing_rows_restored += backing.rows_restored - restored;
3327 work.backing_rows_spilled += backing.rows_spilled - spilled;
3328 work.spill_rows_visited += backing.rows_visited_by_spill - spill_visits;
3329 }
3330 };
3331 // Exact current references, never a digest or a historical placement copy.
3332 // These sets and the certificate are derived scratch, outside source state.
3333 struct RetirementInputs {
3334 std::vector<std::uint64_t> roots, askable, groups, marks;
3335 std::vector<std::int32_t> bar_numbers;
3336 bool operator==(const RetirementInputs& other) const noexcept {
3337 return roots == other.roots && askable == other.askable
3338 && groups == other.groups && marks == other.marks && bar_numbers == other.bar_numbers;
3339 }
3340 };
3341 struct RetirementGate {
3342 RetirementInputs saved, current;
3343 bool certified = false;
3344 std::int64_t cycle = -1, interval = -1;
3345 std::int64_t broker_open = std::numeric_limits<std::int64_t>::min();
3346 std::int64_t interval_expiry = std::numeric_limits<std::int64_t>::max();
3347 std::int64_t broker_expiry = std::numeric_limits<std::int64_t>::max();
3348 };
3349 RetirementGate retirement_gate_;
3350 void invalidate_retirement() noexcept { retirement_gate_.certified = false; }
3351 bool capture_retirement_inputs(RetirementInputs&) const;
3352 void observe_retirement_expiry(const PlacementSnapshot&, std::int64_t interval) noexcept;
3353 void sweep_retired_rows(const NativeDecisionContext&, RetirementWork&);
3354 void finish_retirement_phases();
3355#ifndef NDEBUG
3356 std::array<std::uint64_t, 3> retirement_validation_state() const;
3357#endif
3358};
3359
3360namespace detail {
3361// The TradingView margin-call schedule rules pinned on the tapes under
3362// tests/fixtures/margin_call_rules (short-cutoff-gate) and
3363// tests/fixtures/margin_schedule_rules, one switch each so a regression
3364// bisects per rule; all on, only tests change one. Process-wide, read by every
3365// adapter; not installed API and no strategy input reaches it.
3367 // A short's call of X units found at mark p is taken only where it
3368 // restores the book at its own slipped print p': X p' m > Q p' m - E(p).
3369 bool short_call_gate = true;
3370 // A carried process_orders_on_close short is checked at every point of
3371 // its bar's path before the script: the open, both extremes, the close.
3372 // Ablate it together with close_call_follow_up_at_open: each corrects its
3373 // own bars, and with one off the other's equity error can flip a later
3374 // one-lot boundary (NYSE:F 15 axealgo: 144 exact rows with this on and
3375 // the follow-up off, 169 with both off, 177 with both on).
3377 // After a short's call TradingView checks the book again at the call's
3378 // fill with the called units out of it but their P&L unbooked, lot by
3379 // lot, first in first out, and calls the last still-short check's
3380 // lot-floored restore four times at the bar's next path point.
3382 // A deficit the open's check found and vetoed stays pending: the first
3383 // path point whose segment touches a resting exit is checked before that
3384 // exit, and after a call there the exit fills at that point.
3386 // A reversal or close the script placed at a close where a margin call
3387 // is booked after it keeps the close quantity the script saw.
3389 // A same-side add at full margin is judged as the whole book at the
3390 // tick-built signal close against the fee-charged equity there.
3391 bool add_signal_close = true;
3392 // A long's lagged follow-up on whole-share lot grids too.
3394 // The money of a commission-free long's one-unit call after a trade
3395 // history: initial capital plus the G + L sums.
3397 // A process_orders_on_close short's call sized at the previous close and
3398 // executed at the open is followed at that open, at the call's own fill.
3400 // A process_orders_on_close short's lagged follow-up owed to its bar's
3401 // close is booked after the script, ahead of the fills of the orders the
3402 // script placed there, which keep the sizes they were placed with: a
3403 // strategy.close(id) or close_all() opens the excess on the other side as
3404 // its own lot, a reversal entry's excess joins the entry's lot.
3406};
3408
3409// The opening-call rules pinned on the tapes under
3410// tests/fixtures/margin_open_rules: the call a full-margin
3411// process_orders_on_close position takes at the open after the close fill
3412// that opened it, the open's check after an add filled there, the fill-time
3413// admission of an explicit quantity at the open and the refined lagged
3414// follow-up. One switch each so a regression bisects per rule; all on, only
3415// tests change one. Process-wide; not installed API and no strategy input
3416// reaches it.
3418 // The call is sized at the source close c': money and unit margin at c',
3419 // a long's as a short's.
3421 // Its one-unit band and the short gate are priced at the call's own
3422 // print, the open's tick moved by the exit side's slippage.
3424 // After it the open checks the booked book at its own mark, gated at the
3425 // same print; short there with nothing booked, the sequence ends.
3427 // Then the lagged follow-up of the last call, at that print; a short's
3428 // booked re-check owes it to the first extreme instead.
3430 // A leveraged book's open check runs after the market adds filling at
3431 // that open, on the book holding them and their entry fees.
3433 // An explicit-quantity market entry from flat filling at the open is
3434 // dropped where sig10(sig10(E) / Q m) is below the slipped open tick.
3436 // The lagged follow-up, with its one-unit fallback, on whole-share books.
3438 // A follow-up that books leaves its own follow-up for the next point.
3440 // An owed follow-up is dropped at a point whose own check is short.
3441 // In the opening-call scope the open's own call, and a short's booked
3442 // re-check, owe their follow-up to the bar's first extreme.
3444 // The schedule on a book of several lots the close's fills opened: the
3445 // call sized at the close on the whole book, taken first in first out,
3446 // and its follow-up checked lot by lot over the lots it took.
3448 // A market entry opening the book from flat at a bar's open (one entry,
3449 // neither process_orders_on_close nor calc_on_order_fills) is checked
3450 // there after the exit legs it released -- its own, and a strategy.exit
3451 // without from_entry placed while flat after the entry -- already
3452 // marketable at that open, have filled, on the book they leave, a long's
3453 // as a short's; a leg the bar reaches only after the open fills after the
3454 // check's call. Off, and for every other fill or leg: a short's check
3455 // gives way to any leg the bar touches.
3457 // A strategy.exit without from_entry called while flat with no entry
3458 // order of any id working (not calc_on_order_fills) is void: the opening
3459 // check of the market entry placed after it is taken as with no exit,
3460 // and that entry's fill withdraws its legs, which never fill. Where a
3461 // stop or limit entry fills first the legs keep their former course.
3463};
3465} // namespace detail
3466
3467} // namespace pineforge::source
static constexpr std::size_t kCacheBytes
static constexpr std::size_t kPairCacheBytes
The public native host: an abstract subclass of BacktestEngine with no PineScript on it.
const native_order::RequestHandle * pointer
const native_order::RequestHandle & reference
bool operator!=(const const_iterator &other) const noexcept
bool operator==(const const_iterator &other) const noexcept
bool consumed(std::uint8_t leg_kind, std::uint64_t origin) const noexcept
bool holds(const native_order::RequestHandle &handle) const noexcept
bool wholly_settled() const noexcept
Settled settled_record() const noexcept
std::size_t settled_count() const noexcept
void remove_selected(const detail::ExitScratchIndex &members, const native_order::RunIdentity &run)
void remove_selected(const detail::ExitScratchIndex &members, const native_order::RunIdentity &run, const RemovalSelection &selection, bool precheck=true)
void remove_selected(const detail::ExitScratchRows< native_order::RequestHandle > &doomed)
BracketRoster(const Settled &value)
void push_back(const native_order::RequestHandle &handle)
native_order::RunIdentity run_identity() const noexcept
std::size_t count(std::uint64_t incarnation) const noexcept
void record_consumed(std::uint8_t leg_kind, std::uint64_t origin)
const std::vector< std::pair< std::uint8_t, std::uint64_t > > & consumed_facts() const noexcept
std::vector< native_order::RequestHandle > members() const
std::size_t size() const noexcept
const std::vector< native_order::RequestHandle > & working_tail() const noexcept
std::uint64_t settled_digest() const noexcept
void remove_all(const std::vector< native_order::RequestHandle > &doomed)
void push_back(const native_order::RequestHandle &handle)
const native_order::RequestHandle & back() const
const_reverse_iterator rbegin() const noexcept
std::size_t heap_bytes_estimate() const noexcept
const_iterator begin() const noexcept
std::size_t size() const noexcept
std::vector< native_order::RequestHandle >::const_reverse_iterator const_reverse_iterator
const_reverse_iterator rend() const noexcept
const_iterator end() const noexcept
std::vector< native_order::RequestHandle >::const_iterator const_iterator
const std::vector< std::size_t > * positions_of(std::uint64_t incarnation) const noexcept
std::size_t base() const noexcept
std::uint64_t digest() const noexcept
const std::vector< native_order::RequestHandle > & members() const noexcept
double trail_best_price() const noexcept
int level_resolved(int index) const noexcept
int copy_v1(int index, pf_pending_order_v1_t *out) const noexcept
int effective_levels(int index, double *stop, double *limit, double *trail_activation) const noexcept
int probe_fill_qty(int index, double fill_price, double *qty, int *close_only, int *partition) const noexcept
int short_seed_collision_role(int index) const noexcept
int last_bar_dual_entry_path() const noexcept
void restore_replay_risk_config(const PineReplayRiskConfig &) noexcept
const PendingScanWork & pending_scan_work() const noexcept
std::optional< double > resolve_anchored_level(const NativeAnchoredLevelView &) const
void set_risk_max_drawdown(double value, bool percent) noexcept
const RetirementWork & retirement_work() const noexcept
friend detail::ExitScratchContext & detail::exit_scratch(const PineExecutionAdapter &)
void hash_state(BrokerStateHashSink &) const
std::optional< std::vector< std::uint32_t > > global_exit_child_ranks(const std::vector< SourceId > &ids) const
void set_configuration(const PineStrategyConfig &config) noexcept
void fixture_remove_entry_without_named_cancel(const SourceId &)
void set_margin_call_enabled(bool enabled) noexcept
bool allows_risk_direction(bool is_long) const noexcept
void begin_coof_recalc(const native_order::ExecutionAppliedEvent &, const NativeDecisionContext &, bool first_open, std::uint64_t source_fill_sequence)
PineExecutionAdapter(compat::pine::CapAttachment attachment=compat::pine::CapAttachment::None)
void on_bar_open(const Bar &, const NativeDecisionContext &)
void set_risk_direction(int direction) noexcept
void permute_exit_phases(std::size_t start, const std::vector< std::size_t > &indices)
void bind_exit_scratch(detail::ExitScratchContext &context)
bool fixture_coof_recalc_active() const noexcept
std::optional< NativeMarginDecision > resolve_margin_requirement(const NativeMarginRequirementView &) const
double lagged_margin_follow_up_units(const std::vector< NativeOpenLot > &lots, const SourceMarginMoney &money, double called, double fill, std::int64_t sub_bar_open_ms) const
void set_begin_mode(bool is_stream, bool bar_magnifier=false) noexcept
double fixture_close_pending_debt() const noexcept
void fixture_cohort_implicit_spill_interval(std::size_t calls) noexcept
bool fixture_named_entry_cancel_active(const SourceId &id) const noexcept
native_order::ExecutionTerms resolve_terms(const NativeExecutionTermsFacts &) const
NativePrecommitVerdict validate_precommit(const NativePrecommitView &) const
CohortHeapEstimate cohort_heap_estimate() const noexcept
void set_risk_max_position_size(double value) noexcept
std::uint64_t fixture_erased_rows() const noexcept
bool fixture_coof_cursor_is_bar_close() const noexcept
void on_tick(const Bar &, const NativeTickContext &)
void exit_cancel_bracket(const SourceId &exit_id, const SourceId &from_entry, const std::string &comment={})
std::vector< native_order::RequestHandle > take_first_open_newborns()
std::size_t fixture_close_logical_count() const noexcept
ExitGroupWork exit_group_work() const noexcept
double source_unclosed_qty_for(const SourceId &id) const noexcept
std::uint64_t command_sequence_for_exit(const SourceId &exit_id, const SourceId &from_entry={}) const
static std::uint64_t fold_exit_phase(std::uint64_t digest, std::uint8_t phase) noexcept
void on_bar_close_before_script(const Bar &, const NativeDecisionContext &)
PineReplayRiskConfig capture_replay_risk_config() const noexcept
void exit(const SourceId &exit_id, const SourceId &from_entry, double limit_price, double stop_price, double trail_points=std::numeric_limits< double >::quiet_NaN(), double trail_offset=std::numeric_limits< double >::quiet_NaN(), double trail_price=std::numeric_limits< double >::quiet_NaN(), double qty_percent=100.0, const std::string &comment={}, double qty=std::numeric_limits< double >::quiet_NaN(), const std::string &oca_name={}, double profit_ticks=std::numeric_limits< double >::quiet_NaN(), double loss_ticks=std::numeric_limits< double >::quiet_NaN())
void set_risk_max_cons_loss_days(int value) noexcept
void set_staged_configuration(const StagedConfiguration &staged)
void set_auxiliary_tables_directory(const std::string &directory)
std::uint64_t fixture_erased_digest() const noexcept
NativeRunSpec project(const PineStrategyConfig &, const StagedConfiguration &, const NativeBeginArgs &, NativePathOrder path_order=NativePathOrder::Auto, std::vector< NativeInstrumentFeed > *instrument_feeds=nullptr) const
double source_margin_fill_price(double fire, bool close_is_buy) const
std::uint8_t exit_phase_value(std::size_t trade_index) const
void fixture_spill_predicate(bool(*predicate)(const std::string &)) noexcept
PositionTailWork position_tail_work() const noexcept
void set_id_tables_directory(const std::string &directory)
void fixture_bracket_remove_precheck(bool enabled) noexcept
const auto & cohort_table_work() const noexcept
void preflight_id_pair_precommit(const NativePrecommitView &) const
std::int64_t chart_day_key(std::int64_t timestamp_ms) const noexcept
void set_risk_max_intraday_loss(double value, bool percent) noexcept
double source_margin_units(const SourceMarginMoney &, bool opening_checkpoint, bool gated=true, double print=std::numeric_limits< double >::quiet_NaN()) const
void bind(NativeStrategyHost &host) noexcept
std::vector< FixtureCloseCallsite > fixture_close_callsites() const
void release_delayed_orders(bool explicit_brackets_only=false, double current_open=std::numeric_limits< double >::quiet_NaN())
SourceMarginMoney source_margin_money(double mark_price, std::int64_t sub_bar_open_ms) const
void close(const SourceId &id, const std::string &comment={}, double qty=std::numeric_limits< double >::quiet_NaN(), double qty_percent=std::numeric_limits< double >::quiet_NaN(), bool immediately=false, std::uint64_t callsite_token=0)
RetirementPopulation retirement_population() const
std::vector< FixturePendingSnapshot > fixture_pending_snapshots() const
void permute_exit_phase_rows(std::size_t start, std::size_t count, const std::function< std::size_t(std::size_t)> &source_at)
const PendingIntentView & pending_intent_view() const noexcept
bool max_intraday_loss_is_percent() const noexcept
compat::pine::OrderPriority priority
bool take_intraday_loss_relabel(std::uint64_t ordinal) noexcept
bool core_sizes_default_opening(bool is_long) const
void on_applied(const native_order::ExecutionAppliedEvent &, const NativeDecisionContext &)
bool is_open_phase_exit(std::size_t trade_index) const
void flush_pending_bracket_legs(native_order::RequestHandle just_applied={}, bool post_calculation=true, bool pre_script_drain=false)
double fixture_close_logical_units(const SourceId &) const noexcept
void entry(const SourceId &id, bool is_long, double limit_price=std::numeric_limits< double >::quiet_NaN(), double stop_price=std::numeric_limits< double >::quiet_NaN(), double qty=std::numeric_limits< double >::quiet_NaN(), const std::string &comment={}, const std::string &oca_name={}, int oca_type=0, int qty_type=-1)
const ::pineforge::detail::IdPairStore< std::int64_t, true > & fixture_consumed_partial_exit_cycles() const noexcept
std::optional< double > resolve_margin_call_units(const NativeMarginCallView &) const
bool margin_check_allowed(const NativeMarginCheckPoint &) const
void order(const SourceId &id, bool is_long, double qty, double limit_price=std::numeric_limits< double >::quiet_NaN(), double stop_price=std::numeric_limits< double >::quiet_NaN(), const std::string &oca_name={}, int oca_type=0)
const AnchoredRelativeStats & anchored_relative_stats() const noexcept
bool intrabar_sample_checked(const NativeMarginCheckPoint &) const
std::size_t resident_exit_phases() const noexcept
PineCancellationReceipt * fixture_mutable_cancellation(int index) noexcept
std::uint64_t fixture_auxiliary_disk_bytes() const noexcept
std::pair< std::size_t, std::uint64_t > exit_phase_prefix() const noexcept
int projection_bar_index(const NativeDecisionContext &) const noexcept
int short_seed_collision_role_v1(native_order::RequestHandle) const noexcept
void on_bar_close(const Bar &, const NativeDecisionContext &)
bool other_exit_reaches(double price, bool closing_long, const SourceId &exit_id, const native_order::RequestHandle &filled) const
void fixture_verify_source_entry_slots(bool enabled) noexcept
bool market_pyramid_add(std::uint64_t incarnation) const noexcept
double fixture_close_admitted_total() const noexcept
bool suppress_grouped_stop_recalc(const native_order::ExecutionAppliedEvent &, const NativeDecisionContext &) const noexcept
void mark_market_pyramid_add(std::uint64_t incarnation)
static std::uint64_t fold_dropped_close(std::uint64_t digest, const DroppedCloseReceipt &receipt) noexcept
bool immediate_calculation_close(const native_order::ExecutionAppliedEvent &, const NativeDecisionContext &) const noexcept
void set_cache_policy_for_run(const CacheDefaults &policy)
bool operator==(const Iterator &other) const noexcept
bool operator!=(const Iterator &other) const noexcept
std::uint64_t high_water() const noexcept
PlacementTable(PlacementTable &&) noexcept=default
void set_auxiliary_tables_directory(const std::string &directory)
const_iterator find(std::uint64_t incarnation) const noexcept
PlacementSnapshot & at(std::uint64_t incarnation)
bool erase(std::uint64_t incarnation)
void set_cache_capacity(CacheCapacity minima, CacheCapacity sequence)
bool erased_non_margin_id_outside(const SourceId &first, const SourceId &second) const noexcept
void reserve(std::size_t count)
std::uint64_t erased_rows() const noexcept
void visit_rows(Visit &&visit) const
void preflight_erased_rows(const Enumerate &enumerate) const
PlacementTable(std::size_t cache_bytes)
PlacementTable & operator=(const PlacementTable &)=delete
const_iterator begin() const noexcept
const_iterator end() const noexcept
const_iterator upper_bound(std::uint64_t incarnation) const noexcept
std::uint64_t erased_digest() const noexcept
bool contains(std::uint64_t incarnation) const noexcept
void replace(std::uint64_t incarnation, PlacementSnapshot &&snapshot)
void bind_auxiliary_scratch_tables(const std::shared_ptr<::pineforge::detail::ScratchSlab > &)
std::uint64_t auxiliary_disk_bytes() const noexcept
const PlacementSnapshot & at(std::uint64_t incarnation) const
PlacementTable(CacheCapacity minima, CacheCapacity sequence)
std::size_t size() const noexcept
std::uint64_t visited_slots() const noexcept
PlacementTable(std::size_t cache_bytes, std::size_t sequence_bytes)
PlacementTable(const PlacementTable &)=delete
std::pair< iterator, bool > try_emplace(std::uint64_t incarnation, Args &&... args)
std::size_t max_size() const noexcept
iterator find(std::uint64_t incarnation) noexcept
static constexpr std::uint64_t absent
ExitScratchContext & context() const noexcept
ScratchStatus put(CommandScratchStore &store, std::uint64_t key, const ScratchByteSource &value) noexcept
NativePrecommitVerdict
The host is consulted before generic opening-margin admission.
NativePathPhase
Which leg of a modeled OHLC walk a point sits on.
std::variant< Market, Limit, Stop, StopLimit, Trail > Trigger
NativePathOrder
Generic ordering for a modeled OHLC path.
MarginRuleSwitches & margin_rule_switches() noexcept
void placement_audit_erase(const PlacementTable *table, std::uint64_t incarnation, PlacementSnapshot &&row)
MarginOpeningSwitches & margin_opening_switches() noexcept
ScriptRuleSwitches & script_rule_switches() noexcept
MarginScheduleSwitches & margin_schedule_switches() noexcept
ExitBindingRuleSwitches & exit_binding_rule_switches() noexcept
void placement_audit_forget(const PlacementTable *table) noexcept
PoocCloseFillViewSwitches & pooc_close_fill_view_switches() noexcept
const std::vector< std::pair< std::uint64_t, const PlacementSnapshot * > > * placement_audit_tombstones(const PlacementTable *table)
void placement_audit_scan_hit(const PlacementTable *table, std::uint64_t incarnation, unsigned line) noexcept
void placement_audit_miss(const PlacementTable *table, std::uint64_t incarnation, unsigned line) noexcept
constexpr std::uint32_t kCoofLoopGuard
constexpr char kMarginCallLabel[]
constexpr char kSourceAdapterDomain[]
constexpr char kIntradayLossComment[]
std::string SourceId
constexpr char kFillCapCommentPrefix[]
admission::Draft MarketAdmissionDraft
std::unordered_map< std::string, std::string > InputsMap
Definition engine.hpp:502
admission::Journal MarketAdmissionJournal
Borrowed begin-call facts.
Ephemeral factual view of one kernel-issued liquidation before its units are fixed.
Ephemeral factual view of one kernel check point, offered to the host before the check runs.
Ephemeral factual view of the numbers the kernel is about to compare, at one check point,...
Ephemeral factual view of one prepared execution before any physical effect.
One accepted realtime print before native matching at its current decision point.
Everything the kernel knows about WHERE a point is, as one owning value: its event ordinal,...
A host-maintained, run-scoped roster identity.
One committed execution: the definition, the cursor, the resolved price and units,...
Aggregate field order keeps market construction: Request{Transact{1.0}, "buy", "comment"}...
Another instrument's own bars of one timeframe, installed as data.
One complete setup value, staged/copied by NativeStrategyHost before it is applied at begin.
RemovalSelection(const detail::ExitScratchRows< native_order::RequestHandle > &doomed)
const detail::ExitScratchRows< native_order::RequestHandle > & rows
native_order::RequestHandle opening
std::uint64_t guard_reasons[static_cast< std::size_t >(PositionTailGuard::Count)]
compat::pine::CapConfiguration cap_configuration
compat::pine::CapAttachment cap_attachment
std::uint64_t recreated_after_named_cancelled_entry_incarnation
native_order::RequestHandle paired_reversal_parent
ReservationGrowthSource reservation_growth_source
native_order::RequestHandle bracket_origin
compat::pine::HistoricalBirthReach birth_reach
PineCancellationReceipt cancellation
native_order::RequestHandle preserved_by_close_all
ReservationExpansion reservation_expansion
compat::pine::ExitActivationPolicy exit_activation
native_order::RequestHandle final_short
native_order::RequestHandle long_entry
native_order::RequestHandle materialize_long
std::vector< std::int64_t > account_fx_effective_from_ms
std::vector< StagedInstrumentFeed > instrument_feeds