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(); reset_storage_work();
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 (!old_only_ && 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 [this](const auto& entry, std::uint64_t key) { ++lookup_work_; 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); void place_old(std::uint64_t, std::unique_ptr<PlacementSnapshot>);
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); void grow_window(std::size_t); bool compact_old() noexcept; void reset_storage_work() noexcept;
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_; bool old_only_ = false;
953 std::size_t size_ = 0; std::uint64_t placement_work_ = 0, removal_work_ = 0;
954 std::uint64_t high_water_ = 0;
955 std::uint64_t erased_rows_ = 0; mutable std::uint64_t visited_slots_ = 0, lookup_work_ = 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 // An exit without from_entry called while no position was held and no
2171 // entry order was working, which TradingView voids
2172 // (PlacementSnapshot::void_global_issue): no entry's fill arms it.
2173 bool void_global = false;
2174 };
2175
2176 // One relative leg of a queued strategy.exit, spelled as the
2177 // kernel's anchored bracket child: the host-sized book close exit()
2178 // submits at the parent's fill, placed ahead of it (FromOwnerFill under
2179 // WaitForApplied{parent, PendingUntilArmed, AfterArmPrint, Book}). Until
2180 // its parent fills it is a kernel request only: no placement row, no live
2181 // handle, so every source book (reservations, admission, the pending
2182 // projection) reads exactly what it read before. `installed_level` is the
2183 // trigger level the arm hook restated; the fill-point exit() re-run
2184 // adopts the armed request when it is the request it would submit (same
2185 // trigger bits, same group, a host-sized book close), and withdraws it
2186 // otherwise.
2187 struct AnchoredRelativeLeg {
2188 SourceId exit_id;
2189 SourceId from_entry;
2191 native_order::RequestHandle handle{};
2192 native_order::RequestHandle parent{};
2193 bool parent_long = true;
2194 // The source tick operand this leg was anchored from (profit, loss, or
2195 // the ceiled trail_points) and, for a trailing leg, the source offset.
2196 double operand_ticks = 0.0;
2197 double trail_offset = std::numeric_limits<double>::quiet_NaN();
2198 native_order::Request request;
2199 bool armed = false;
2200 double installed_level = std::numeric_limits<double>::quiet_NaN();
2201 };
2202
2203 // A from_entry "" exit with relative legs that covers the position entry
2204 // by entry (ExitBindingRuleSwitches::global_exit_per_entry_levels): kept
2205 // from the moment the position it stands on holds a second entry until the
2206 // book goes flat. `children` are the entries in fill order, each with the
2207 // fill price its bracket's levels come from and the quantity it closes;
2208 // `consumed` once a leg of its bracket filled (TradingView never gives that
2209 // entry another one).
2210 struct PerEntryExitChild {
2211 std::uint64_t origin = 0;
2212 double fill_price = std::numeric_limits<double>::quiet_NaN();
2213 double units = 0.0;
2214 bool consumed = false;
2215 };
2216 struct PerEntryExit {
2217 SourceId exit_id;
2218 std::int64_t cycle = 0;
2219 bool long_side = true;
2220 double profit_ticks = std::numeric_limits<double>::quiet_NaN();
2221 double loss_ticks = std::numeric_limits<double>::quiet_NaN();
2222 std::string comment;
2223 std::vector<PerEntryExitChild> children;
2224 };
2225
2226 struct PendingCoofRequest {
2227 native_order::Request request;
2228 PlacementSnapshot snapshot;
2229 SourceId replacement_key;
2230 bool opening = false;
2231 std::uint64_t family_key = 0;
2232 bool next_open = false;
2233 };
2234
2235 struct PendingMarginRevival {
2236 PlacementSnapshot snapshot;
2237 std::int32_t decline_bar = -1;
2238 };
2239
2240 struct NamedEntryCancelToken {
2241 std::uint64_t entry_incarnation = 0;
2242 std::uint64_t surviving_exit_incarnation = 0;
2243 };
2244
2245 // One strategy.close call site on one bar: TradingView keeps one order
2246 // per site. Its quantity and comment are the last effective call's, the
2247 // id its fill books against the first effective call's.
2248 struct CloseCallsiteState {
2249 bool active = false;
2250 std::uint64_t token = 0;
2251 int calls = 0;
2252 SourceId first_id{};
2253 SourceId id{};
2254 std::string comment{};
2255 double target = 0.0;
2256 std::uint64_t queue_sequence = 0;
2257 };
2258
2259 // One opening fill's share of the close ledger: the units entered under
2260 // `id` that no close has booked yet. Records keep fill order; adjacent
2261 // records of one id merge.
2262 struct CloseLedgerRecord {
2263 SourceId id{};
2264 double units = 0.0;
2265 };
2266
2267 NativeStrategyHost& require_host() const;
2268 bool modeled_input() const;
2269 void require_cohort_ticket(const SourceId* prospective = nullptr) const;
2270 native_order::CohortHandle cohort_for(const SourceId& id);
2271 std::optional<native_order::RequestHandle> submit_or_replace(
2272 native_order::Request request, PlacementSnapshot snapshot, bool opening,
2273 const SourceId& replacement_key = {});
2274 void remember(const native_order::RequestHandle&, PlacementSnapshot&&);
2275 void retire(native_order::RequestHandle) noexcept;
2276 // Erases the placement rows no reader can reach again
2277 // (pine_adapter.cpp explains the rule); run at every bar open.
2278 void erase_retired_rows(const NativeDecisionContext&);
2279 // Takes out of the kernel's cohort rosters every origin
2280 // that can no longer be bound, before retiring the adapter's prefix.
2281 void retire_origin_prefixes();
2282 void release_closed_cohort_origins();
2283 bool lifecycle_readable(const PlacementSnapshot&) const noexcept;
2284 // Writes one trade's exit phase, refolding when the write lands below the
2285 // final mark.
2286 void set_exit_phase(std::size_t trade_index, std::uint8_t phase);
2287 std::vector<native_order::RequestHandle> openings_for(const SourceId&) const;
2288 double cohort_exposure_for(const SourceId&) const noexcept;
2289 double cohort_exposure_for(const CohortFacts&) const noexcept;
2290 // The units an exit's from_entry holds; none when it names no entry.
2291 std::optional<double> from_entry_units(const SourceId&) const noexcept;
2292 bool from_entry_filled_this_cycle(const SourceId&) const;
2293 double percent_commission_live_equity(double) const noexcept;
2294 // The same equity at an explicit account-currency rate: the rate an
2295 // execution converts at, which its terms facts carry
2296 // (NativeExecutionTermsFacts::active_fx); none, the presented clock's.
2297 double percent_commission_live_equity(double, std::optional<double> fx) const noexcept;
2298 // The kernel's marked equity at `mark` (every open entry fee charged),
2299 // at `fx` when given: strategy.equity under a cash commission.
2300 double strategy_equity_at(double mark, std::optional<double> fx) const noexcept;
2301 // Records both equities a sizing snapshot carries at `mark`: `equity`
2302 // (percent_commission_live_equity) and, for a cash-fee percent-of-equity
2303 // default quantity, `strategy_equity`.
2304 void mark_sizing_equity(PineSizingSnapshot&, double mark,
2305 std::optional<double> fx = std::nullopt) const noexcept;
2306 double quantize_close_units(double basis, double percent) const noexcept;
2307 double quantize_percent_exit_units(double requested,
2308 double available) const noexcept;
2309 bool compute_exit_reservation(const SourceId& exit_id,
2310 const SourceId& from_entry,
2311 double requested_qty,
2312 double& qty_percent,
2313 double live_basis,
2314 double percentage_basis,
2315 double& reserved_qty) const;
2316 void reconcile_deferred_exit_reservations(const SourceId& from_entry,
2317 double live_basis);
2318 void reconcile_exit_reservations_after_margin();
2319 double active_staged_fx(std::int64_t) const noexcept;
2320 void apply_fx_open_margin_slice(const Bar&, const NativeDecisionContext&);
2321 void apply_fx_opening_margin_slice(const native_order::ExecutionAppliedEvent&,
2322 const NativeDecisionContext&);
2323 void schedule_preopen_margin_slice(const Bar&, const NativeDecisionContext&);
2324 bool leveraged_entry_bar_checked() const noexcept;
2325 bool preopen_slice_class(const PlacementSnapshot&,
2326 const NativeDecisionContext&) const noexcept;
2327 // For both helpers, resolved_execution_price == nullopt prices at mark_price
2328 // with exit-side slippage; a value pins that already-resolved price unchanged
2329 // when the execution price is forced.
2330 bool submit_margin_call_slice(double mark_price, const NativeDecisionContext&,
2331 bool opening_checkpoint = false,
2332 std::optional<double> resolved_execution_price = std::nullopt);
2333 bool submit_margin_call_units(double mark_price, const NativeDecisionContext&,
2334 double units,
2335 bool force_execution_price = true,
2336 std::optional<double> resolved_execution_price = std::nullopt);
2337 bool submit_tv_money_long_margin_call(const Bar&, const NativeDecisionContext&,
2338 int* fired_waypoint = nullptr);
2339 void fold_source_money();
2340 void fold_source_money_through(const PineStrategyHost& pine, std::uint64_t count);
2341 bool gain_loss_regime() const;
2342 bool gain_loss_run_regime() const;
2343 double gain_loss_signal_equity(double mark) const;
2344 double gain_loss_mirror_equity(double mark) const;
2345 std::optional<double> pooc_fee_units(const PineSizingSnapshot&) const;
2346 double tv_pooc_fee_grossed_unit(double price) const;
2347 bool unified_admission_scope(const PlacementSnapshot&, bool market_or_stop,
2348 double signed_units, double units,
2349 bool beside_exits = false) const;
2350 bool unified_admission_terms(const PlacementSnapshot&, const NativeExecutionTermsFacts&,
2351 bool market_or_stop, bool opposite,
2352 native_order::ExecutionTerms& result, bool& admitted) const;
2353 bool unified_fill_admits(const PlacementSnapshot&, double units, double quote) const;
2354 void record_close_first(bool whole_position);
2355 void credit_pyramiding_record(const SourceId& id, std::uint64_t incarnation, double units);
2356 void book_pyramiding_records(const SourceId& id, std::uint64_t armed_for, double units);
2357 std::optional<std::size_t> open_ledger_records() const;
2358 std::optional<std::size_t> held_ledger_records() const;
2359 std::uint64_t entry_incarnation_of(const SourceId& from_entry) const;
2360 bool exit_tombstoned(const SourceId& exit_id, const SourceId& from_entry) const;
2361 bool unified_placement_book_clear() const;
2362 bool pinned_lot_grid() const;
2363 bool slipped_long_margin_scope() const;
2364 bool slipped_long_book_scope() const;
2365 double slipped_long_margin_units(double quote) const;
2366 bool slipped_long_unit_shortfall(double quote) const;
2367 bool close_all_at_next_point(
2368 const Bar&, int fired,
2369 const std::vector<std::pair<native_order::RequestHandle, PlacementSnapshot>>& closes,
2370 const NativeDecisionContext&);
2371 bool slipped_pooc_opening_money_scope(
2372 const Bar&, const NativeDecisionContext&) const;
2373 bool submit_slipped_pooc_opening_money_call(
2374 const Bar&, const NativeDecisionContext&);
2375 bool schedule_tv_money_long_margin_before_trail(
2376 const Bar&, const NativeDecisionContext&);
2377 std::optional<std::int64_t> flat_sibling_placement(std::size_t min_lots) const;
2378 bool flat_sibling_book(const PlacementSnapshot&, std::int32_t interval_index,
2379 NativePathPhase phase) const;
2380 bool flat_sibling_fill_follows(const PlacementSnapshot&,
2381 const native_order::RequestHandle&) const;
2382 bool add_sibling_fill_follows(const PlacementSnapshot&,
2383 const native_order::RequestHandle&) const;
2384 std::size_t add_sibling_lots(std::int64_t placed) const;
2385 bool whole_unit_follow_up_due(double called_units, double mark) const;
2386 bool margin_follow_up_scope() const noexcept;
2387 double margin_follow_up_units(double called_units, double fill,
2388 std::int64_t sub_bar_open_ms) const;
2389 void follow_margin_call(double called_units, double fill, double current,
2390 const NativeDecisionContext&);
2391 bool short_call_vetoed(const SourceMarginMoney&, double units, bool opening_checkpoint,
2392 double print = std::numeric_limits<double>::quiet_NaN()) const;
2393 // The opening-call schedule after a process_orders_on_close close fill
2394 // (MarginOpeningSwitches): its scope, the call sized at the close and the
2395 // calls the open takes after it.
2396 bool close_sized_open_call_scope(bool short_book, bool several_lots = false) const;
2397 double close_sized_open_units(double close_mark, double print,
2398 std::int64_t sub_bar_open_ms) const;
2399 bool run_close_sized_open_calls(double units, double opening_mark,
2400 const NativeDecisionContext&);
2401 // The opening checkpoint at the fill that opened or grew a full-margin
2402 // book (on_applied): the exit legs that fill ahead of it, and its call.
2403 bool opening_released_leg(const PlacementSnapshot& row, std::uint64_t leg,
2404 std::uint64_t opening, const SourceId& id, bool short_book,
2405 const NativeDecisionContext&) const;
2406 bool priced_exit_precedes_opening(bool short_book, std::uint64_t opening,
2407 const SourceId& opening_id, const NativeDecisionContext&,
2408 const PlacementSnapshot* filled, bool* at_open) const;
2409 void run_opening_checkpoint(double fill, bool market, bool sibling_book,
2410 bool adds_book_last, const NativeDecisionContext&);
2411 double open_print_follow_up_units(double held_before, double equity_before, double entry,
2412 double called, double fill, bool short_book,
2413 std::int64_t sub_bar_open_ms) const;
2414 double open_print_lot_follow_up_units(const std::vector<std::pair<double, double>>& taken,
2415 double equity_before, double held_before, double fill,
2416 bool short_book, std::int64_t sub_bar_open_ms) const;
2417 bool lagged_short_scope() const noexcept;
2418 double lagged_short_follow_up_units(const std::vector<std::pair<double, double>>& taken,
2419 double equity_before, double held_before, double fill,
2420 std::int64_t sub_bar_open_ms) const;
2421 double call_short_with_lagged_follow_up(double mark, double units,
2422 const NativeDecisionContext&, bool owed = false);
2423 bool whole_share_lag_scope() const noexcept;
2424 double whole_share_lag_units(const std::vector<std::pair<double, double>>& taken,
2425 double equity_before, double held_before, double fill,
2426 std::int64_t sub_bar_open_ms) const;
2427 bool open_call_owing_follow_up(double opening_mark, const NativeDecisionContext&);
2428 bool short_path_points_scope(const NativeDecisionContext&) const;
2429 void execute_owed_short_follow_up(int point, double price, const NativeDecisionContext&);
2430 void hold_owed_close_follow_up(double close, const NativeDecisionContext&);
2431 void book_owed_close_follow_up(const Bar&, const NativeDecisionContext&);
2432 void walk_short_path_points(const Bar&, const NativeDecisionContext&);
2433 void schedule_lagged_short_follow_up(const native_order::ExecutionAppliedEvent&,
2434 const NativeDecisionContext&);
2435 bool pending_veto_point_first(const Bar&, const NativeDecisionContext&, double opening_mark);
2436 void record_close_call_after_script(double book_before, double called,
2437 const NativeDecisionContext&);
2438 void apply_frozen_reversal_close(const PlacementSnapshot&, const NativeExecutionTermsFacts&,
2439 native_order::ExecutionTerms&) const;
2440 native_order::ExecutionTerms resolve_source_terms(const NativeExecutionTermsFacts&) const;
2441 double gain_loss_closed_equity() const;
2442 bool declined_reversal_at_open(const Bar&) const;
2443 bool schedule_margin_call_path(const Bar&, const NativeDecisionContext&);
2444 bool schedule_priced_opening_margin(const Bar&, const NativeDecisionContext&, double fill);
2445 bool close_point_margin_scope() const noexcept;
2446 void withdraw_waypoint_margin_calls();
2447 bool rest_waypoint_margin_call(const Bar&, int from, int to, const NativeDecisionContext&);
2448 bool close_point_margin_call(const Bar&, const NativeDecisionContext&, bool cancelled);
2449 bool book_close_point_call(double mark, double units, const NativeDecisionContext&,
2450 bool queued, double reference_mark = 0.0);
2451 void close_point_follow_up(double mark, double called_units, const NativeDecisionContext&);
2452 std::vector<std::pair<native_order::RequestHandle, PlacementSnapshot>>
2453 same_bar_close_alls(const NativeDecisionContext&) const;
2454 void size_close_alls_at_placement(
2455 std::vector<std::pair<native_order::RequestHandle, PlacementSnapshot>>&, double placed);
2456 bool close_point_margin_call_at_open(const Bar&, const Bar& prior,
2457 const NativeDecisionContext&);
2458 double source_margin_one_unit(const SourceMarginMoney&) const;
2459 bool source_margin_unit_restores(const SourceMarginMoney&, double fill) const;
2460 void defer_declined_reversal_exits_at_adverse(const Bar&,
2461 const NativeDecisionContext&,
2462 bool margin_scheduled);
2463 bool intraday_loss_breached(double mark_price) const;
2464 bool submit_intraday_loss_close(double mark_price, const NativeDecisionContext&,
2465 bool execute_current);
2466 void schedule_intraday_loss_path(const Bar&, const NativeDecisionContext&);
2467 void update_risk_state(double mark_price);
2468 bool intraday_loss_orders_blocked() const noexcept;
2469 compat::pine::CapClock cap_clock(const NativeDecisionContext&) const;
2470 compat::pine::Calculation cap_calculation(const NativeDecisionContext&) const;
2471 compat::pine::MatchedAttempt cap_attempt(
2472 const PlacementSnapshot&, std::uint64_t incarnation,
2473 const native_order::ExecutionAppliedEvent* applied = nullptr) const;
2474 bool cap_placement_denied(const NativeDecisionContext&);
2475 void observe_intraday_cap(const native_order::ExecutionAppliedEvent&,
2476 const PlacementSnapshot&, const NativeDecisionContext&);
2477 void observe_intraday_cap_noop(bool is_long, const NativeDecisionContext&);
2478 void execute_cap_close_now(const compat::pine::CloseNow&);
2479 void execute_due_cap_close(const NativeDecisionContext&);
2480 void maybe_activate_short_seed_plan();
2481 void activate_short_seed_plan_at_open(const NativeDecisionContext&);
2482 bool qualify_short_seed_plan(const ShortSeedPlan&) const;
2483 bool short_seed_context_is_live() const noexcept;
2484 void record_dropped_close(const SourceId&, const std::string&, double, double,
2485 bool, std::uint64_t);
2486 void record_opening_fee(const PlacementSnapshot&,
2487 const native_order::ExecutionAppliedEvent&);
2488 void consume_opening_fees(const native_order::ExecutionAppliedEvent&,
2489 const SourceId*);
2490 void consume_cohort_units(const SourceId&, const native_order::ExecutionAppliedEvent&);
2491 void consume_closed_trade_rows(const native_order::ExecutionAppliedEvent&,
2492 const PlacementSnapshot*);
2493 bool origin_is_pending(const native_order::RequestHandle&) const noexcept;
2494 // Roster positions, latest first, of the cohort's origins that are live
2495 // handles. origin_is_pending() accepts only live handles, so walking these
2496 // positions meets exactly the pending origins a reverse walk of the whole
2497 // roster meets, in the same order.
2498 std::vector<std::size_t> live_origin_positions(const CohortFacts&) const;
2499 // Lookups over the bookkeeping the adapter retains for
2500 // the whole run -- every origin a cohort ever accepted, every leg an exit
2501 // family ever placed -- answered from an index the native consumer keeps
2502 // for this adapter (its host cache; pine_adapter.cpp), since the adapter
2503 // is a by-value member of PineStrategyHost and so holds none. Each
2504 // answers exactly what the walk it replaces answers, and each walks when
2505 // the consumer keeps no cache. bound_consumer() is the bound host's
2506 // consumer, or null for a host that is not a PineStrategyHost.
2507 IExecutionConsumer* bound_consumer() const noexcept;
2508 // Whether any origin the cohort ever accepted is an opening on that side.
2509 bool cohort_opened_on_side(const CohortFacts&, bool is_long) const noexcept;
2510 // Whether a leg of that kind the (exit id, from_entry) family placed for
2511 // that origin was consumed: a member of the family neither working nor
2512 // suspended.
2513 bool origin_leg_consumed(std::uint64_t family_key, PineOrderFamily,
2514 const native_order::RequestHandle& origin) const noexcept;
2515 // Whether an immediate strategy.close was placed on that bar.
2516 bool immediate_close_placed_on(std::int32_t bar) const noexcept;
2517 void cancel_bracket_origin(native_order::RequestHandle);
2518 // executed_at: the ordinal a terminal receipt executed at, when the
2519 // observer reads it later; a later strategy.exit call's leg accepted
2520 // after it is not a sibling.
2521 void cancel_bracket_siblings(native_order::RequestHandle, std::uint64_t executed_at = 0);
2522 void cancel_exit_orders_for_full_close(const SourceId& from_entry);
2523 void cancel_exit_orders_for_full_close(const detail::ScratchByteSource& from_entry);
2524 void retire_in_position_exits_at_flat(bool preserve_pending_parents,
2525 bool dormant_rows_only,
2526 const PlacementSnapshot* paired_close);
2527 void materialize_relative_exits(PlacementSnapshot,
2528 const native_order::ExecutionAppliedEvent&);
2529 void withdraw_anchored_relative_legs(const SourceId* exit_id,
2530 const SourceId* from_entry);
2531 void withdraw_anchored_relative_readers(const detail::ScratchByteSource* exit_id,
2532 const detail::ScratchByteSource* from_entry);
2533 bool anchorable_relative_exit(const PendingRelativeExit&,
2534 native_order::RequestHandle& parent,
2535 bool& parent_long) const;
2536 struct RelativeLegShape {
2538 native_order::Trigger trigger = native_order::Limit{};
2539 double signed_ticks = 0.0;
2540 double operand_ticks = 0.0;
2541 };
2542 std::vector<RelativeLegShape> relative_leg_shapes(const PendingRelativeExit&,
2543 bool parent_long) const;
2544 bool armed_relative_legs_adoptable(const PlacementSnapshot& opening,
2545 const native_order::ExecutionAppliedEvent&,
2546 const std::vector<PendingRelativeExit>&) const;
2547 // TradingView's trigger projection of an exit level, one spelling for
2548 // exit() and for the kernel's arm hook: the limit leg's tick-quantized
2549 // threshold (raw on-grid under calc_on_order_fills) and the one-shot
2550 // trail activation touch.
2551 double exit_limit_trigger(double limit_price, double tick, bool exit_is_buy) const noexcept;
2552 double one_shot_trail_trigger(double activation, double source_trail_offset, double tick,
2553 bool exit_is_buy, bool quantized_activation) const noexcept;
2554 void materialize_pending_bracket_legs(
2555 const native_order::ExecutionAppliedEvent&);
2556 void stage_flat_children_before_parent(const SourceId&, std::int32_t,
2557 std::int64_t);
2558 bool defer_coof_tail() const noexcept;
2559 bool coof_fill_at_second_extreme() const noexcept;
2560 bool source_path_uses_high_first(const Bar&) const noexcept;
2561 bool coof_fill_on_path_point() const noexcept;
2562 bool coof_fill_at_path_point(double waypoint) const noexcept;
2563 bool coof_waits_for_next_sub_bar() const noexcept;
2564 bool coof_current_fill_was_forced_waypoint() const noexcept;
2565 double coof_next_waypoint(int* path_index = nullptr) const noexcept;
2566 bool coof_remaining_recrosses(double level, bool long_position) const noexcept;
2567 bool coof_leg_reaches_exit(const PlacementSnapshot&) const noexcept;
2568 void flush_coof_tail(bool openings_only = false,
2569 bool include_next_open = false);
2570 native_order::Owner owner_for_close(const SourceId&, bool dynamic) const;
2571 bool same_bar_market_tx_scope() const;
2572 bool fixed_unmargined_market_batch_scope() const;
2573 void reroute_fixed_entries_before_request(bool invalidate_batch = true);
2574 std::size_t accepted_entries_in_cycle(bool is_long, const SourceId& id) const;
2575 bool opposite_entry_opening_pending(bool is_long) const;
2576 void submit_entry_with_policy(native_order::Request request, PlacementSnapshot snapshot,
2577 const SourceId& id, const EntrySubmissionContext& context);
2578 void reroute_incomplete_fixed_batch(bool invalidate_batch);
2579 void flush_pending_same_bar_commands(bool flat_pair_follows = false,
2580 bool invalidate_batch = true);
2581 // The placement-time default quantity: the core's own conversion, read as
2582 // a query (NativeStrategyHost::native_sized_units) and floored by the
2583 // source. The source's money band and affordability gates consume the
2584 // number before any request exists, which is why it is computed here at
2585 // all; the arithmetic itself exists once, in the core.
2586 double default_sizing_units(const PineSizingSnapshot&) const;
2587 // The three pieces of the default quantity the source keeps as its own
2588 // policy: the money the percentage is taken of (with its equity mark and
2589 // its ten-significant-digit rounding), whether a percentage fee is
2590 // reserved out of it, and the lot floor applied to the core's quotient.
2591 double default_sizing_cash(const PineSizingSnapshot&) const;
2592 bool default_sizing_reserves_percent_fee() const noexcept;
2593 // A percent-of-equity default quantity under a cash commission (per
2594 // order or per contract) takes its percentage of strategy.equity and
2595 // leaves out the fee its own order pays.
2596 bool default_sizing_reserves_cash_fee() const noexcept;
2597 double default_sizing_lot_floor(double units) const noexcept;
2598 // A typed quantity (qty_type cash / percent_of_equity) names money, not
2599 // units: `money` converted at `price` by the core (native_sized_units:
2600 // CashValue, the percentage fee reserve) and floored onto the quantity
2601 // grid. typed_entry_units picks the money a typed entry's fill books --
2602 // the cash itself, or a percentage of the source's sizing equity at
2603 // `price` (of the hypothetical Flatten's realized balance for a typed
2604 // reversal); std::nullopt where that projection has no Pine host.
2605 double typed_quantity_units(double money, bool percent, double price, double fx) const;
2606 std::optional<double> typed_entry_units(const PlacementSnapshot&, double price, double fx,
2607 native_order::RequestHandle target) const;
2608 // The core intent a default-quantity declaration spells -- the source
2609 // money as a CashValue basis, the core's fee reserve, the raw quotient --
2610 // and the sided intent a re-lowerable opening carries. std::nullopt keeps
2611 // the host-resolved shape for every path the core cannot name.
2612 std::optional<native_order::Sized> default_sizing_shape(
2613 const PineSizingSnapshot&) const;
2614 std::optional<native_order::Sized> default_sizing_intent(
2615 const PineSizingSnapshot&, bool is_long) const;
2616 // True when the price the core will freeze for a Sized opening accepted
2617 // NOW is bit-for-bit the price this snapshot already froze. A command
2618 // deferred to a later execution point, or one whose sizing price the
2619 // source took from somewhere other than the signal rule, fails it and
2620 // keeps its host-resolved intent.
2621 bool core_sizing_price_matches(const PineSizingSnapshot&, bool is_long) const;
2622 double default_market_sizing_price(double mark, bool is_long) const noexcept;
2623 native_order::Trigger trigger_for(double limit_price, double stop_price) const;
2624 native_order::Group group_for(const std::string&, int, std::int64_t = 0) const;
2625 PineSizingSnapshot sizing_snapshot() const;
2626 std::uint64_t key_for(const SourceId&, const SourceId& = {}) const noexcept;
2627 void refresh_pending_view() noexcept;
2628 OrderBirth capture_order_birth() const;
2629 bool opening_market_short_scope(PineOrderFamily) const;
2630 void initialize_l4c_policy(PlacementSnapshot&, native_order::RequestHandle);
2631 void update_l4c_priority();
2632 void update_l4c_lifecycle(const native_order::ExecutionAppliedEvent&,
2633 const NativeDecisionContext&);
2634 using ReceiptHighWaterReader = std::uint64_t (*)(const NativeStrategyHost&) noexcept;
2635
2636 // The consumer's terminal-receipt high water as observe_terminal_receipts
2637 // last read it, advanced only on a run without an intrabar path (it stays
2638 // 0 under the magnifier). It no longer gates any read: it only feeds the
2639 // broker-state hash (pine_state_hash.cpp), so removing it would move
2640 // hash values.
2641 std::uint64_t terminal_receipt_cursor_ = 0;
2642 // Retired receipt-reader bindings that PineStrategyHost installed
2643 // at every begin. They only identified the host as a
2644 // PineStrategyHost. The readers ask the host's
2645 // PineStrategyHost view instead, so nothing reads or writes these two
2646 // slots; they keep this class's layout, which is part of
2647 // PineStrategyHost's and so of the generated script's ABI. Removing
2648 // both slots and ReceiptHighWaterReader requires an engine_script_run
2649 // epoch change because it alters that ABI's layout.
2650 ReceiptHighWaterReader event_high_water_reader_ = nullptr;
2651 ReceiptHighWaterReader terminal_receipt_high_water_reader_ = nullptr;
2652 bool is_declined_market_reversal(
2653 const native_order::MatchRejectedEvent&) const noexcept;
2654 bool follows_same_bar_declined_reversal(
2655 const PlacementSnapshot&, const NativePrecommitView&) const;
2656 bool bracket_belongs_to_reversal(
2657 const PlacementSnapshot&, const PlacementSnapshot&) const;
2658 void suspend_brackets_for_reversal(
2659 const PlacementSnapshot&, const exit_legs::Frame&, double open_price);
2660 void suspend_declined_reversal_brackets(
2661 const native_order::MatchRejectedEvent&);
2662 bool reissue_revives_declined_exit(const PlacementSnapshot&) const noexcept;
2663 void suspend_coof_declined_reversal_at_open(
2664 const Bar&, const NativeDecisionContext&);
2665 void hold_reversal_pair_brackets(const SourceId&);
2666 // A script cancel (strategy.cancel of an id, or cancel_all when `id` is
2667 // null) retires every leg of the lifecycle of each exit it withdrew --
2668 // the live requests in `cancelled` (incarnation, Cancelled event ordinal)
2669 // and every other exit row it names -- so no later margin call revives
2670 // one, including dormant rows and the other rows of the withdrawn
2671 // lifecycle.
2672 void retire_cancelled_exits(
2673 const SourceId* id,
2674 const std::vector<std::pair<std::uint64_t, std::uint64_t>>& cancelled);
2675 void purge_brackets_after_applied_reversal(const PlacementSnapshot&);
2676 void revive_brackets_after_margin(
2677 const native_order::ExecutionAppliedEvent&, const NativeDecisionContext&);
2678 int projected_pending_size() const noexcept;
2679 bool projected_pending_at(int index, const PlacementSnapshot*& snapshot,
2680 native_order::RequestHandle& handle) const noexcept;
2681 int projected_raw_pending_size() const noexcept;
2682 bool projected_raw_pending_at(int index, const PlacementSnapshot*& snapshot,
2683 native_order::RequestHandle& handle) const noexcept;
2684 static bool same_projected_order(const PlacementSnapshot& left,
2685 const PlacementSnapshot& right) noexcept;
2686 void apply_open_market_admission(const NativeDecisionContext&);
2687 void defer_open_marketable_sells(const Bar& bar);
2688 void admit_deferred_open_marketable_sells();
2689 void rearm_throttled_reopens();
2690 // A carried trailing stop reached on the bar's tick-built path: the
2691 // trail's distance re-priced for one bar, then restored (pine_adapter.cpp).
2692 void retune_carried_trails_for_tick_reach(const Bar&, const NativeDecisionContext&);
2693 void restore_carried_trail_retunes();
2694 void flush_pooc_marketable_limit_entry_fills(const Bar&, const NativeDecisionContext&);
2695 // The close pass's fills at a close tick: its own pass, or (after_close)
2696 // the reversing stops a same-bar close's fill releases.
2697 void fill_pooc_close_entries(double raw_close, const NativeDecisionContext&,
2698 bool after_close);
2699 void flush_pooc_marketable_exit_fills(const Bar&, const NativeDecisionContext&);
2700 // The close pass's exit fills at a close tick, and whether the calculation
2701 // still has an order the close fills at any price.
2702 void fill_pooc_close_exits(double raw_close, const NativeDecisionContext&);
2703 bool pooc_close_market_pending(const NativeDecisionContext&) const;
2704 bool pooc_close_entry_immediate_pending(const NativeDecisionContext&,
2705 const native_order::RequestHandle* except) const;
2706 // A from_entry "" exit without a quantity that is the book's only exit:
2707 // it closes its percentage of the position it fills against.
2708 bool fill_time_global_exit(const PlacementSnapshot&) const;
2709 // A held global exit with relative legs, entry by entry
2710 // (ExitBindingRuleSwitches::global_exit_per_entry_levels).
2711 bool per_entry_exit_scope() const;
2712 detail::ExitScratchRows<PerEntryExitChild> per_entry_open_entries(bool long_side) const;
2713 detail::ExitScratchRows<native_order::RequestHandle> per_entry_child_legs(
2714 const SourceId& exit_id, std::uint64_t origin) const;
2715 bool per_entry_exit_call(const SourceId& exit_id, const SourceId& from_entry, double limit,
2716 double stop, double trail_points, double trail_offset,
2717 double trail_price, double qty_percent, const std::string& comment,
2718 double qty, const std::string& oca_name, double profit_ticks,
2719 double loss_ticks, std::uint64_t command_sequence);
2720 void submit_per_entry_child(const PerEntryExit& exit, const PerEntryExitChild& child,
2721 std::uint64_t command_sequence);
2722 void per_entry_exits_at_entry_fill(const PlacementSnapshot& opening,
2723 const native_order::ExecutionAppliedEvent& event);
2724 void per_entry_exits_after_fill(const std::optional<PlacementSnapshot>& placement,
2725 const native_order::ExecutionAppliedEvent& event);
2726 void end_per_entry_exit(std::size_t index);
2727 void cancel_working_legs(
2728 const detail::ExitScratchRows<native_order::RequestHandle>& legs);
2729 void order_per_entry_point_ties();
2730 void record_market_review(admission::Checkpoint, int,
2731 const std::vector<native_order::RequestHandle>&);
2732 void refresh_pending_sizing_after_margin(
2733 const native_order::ExecutionAppliedEvent&, const NativeDecisionContext&);
2734 void apply_reversal_gap_bracket_policy(
2735 const Bar&, const NativeDecisionContext&, bool defer_trails = false);
2736 void reaccept_gapped_bracket_behind_same_id_add(
2737 const Bar&, const NativeDecisionContext&);
2738 void order_open_marketable_limit_entries(const Bar&, const NativeDecisionContext&);
2739 void apply_terminal_explicit_market_policy(const NativeDecisionContext&);
2740 void apply_terminal_explicit_market_policy(const NativeDecisionContext&, bool execute_now);
2741 bool enqueue_pooc_fifo_close(const SourceId&, const std::string&,
2742 std::uint64_t, std::uint64_t);
2743 void observe_close_ledger(const native_order::ExecutionAppliedEvent&,
2744 const PlacementSnapshot*);
2745 bool entry_order_pending(const SourceId&) const;
2746 bool entry_order_working(const SourceId* id) const;
2747 bool priced_entry_order_working(const SourceId* id) const;
2748 bool margins_disabled() const noexcept;
2749 bool whole_level_exit(double limit_price, double stop_price, double trail_points,
2750 double trail_price, double profit_ticks, double loss_ticks, double qty,
2751 double qty_percent) const noexcept;
2752 bool open_lot_of(const SourceId& id) const;
2753 bool standing_exit(const SourceId& exit_id, const SourceId& from_entry) const;
2754 void unvoid_exit(PlacementSnapshot& row);
2755 void execute_or_withdraw_close(native_order::RequestHandle close, bool void_issue);
2756 void withdraw_void_exits(const SourceId& entry_id);
2757 void settle_void_global_exits(bool market_entry);
2758 void bind_pending_exits_to_fill(const SourceId& entry_id);
2759 void withdraw_unplaced_cap_adds(const NativeDecisionContext& context);
2760 void stash_id_bound_exits(const SourceId& entry_id);
2761 void rearm_id_bound_exits(const SourceId& entry_id);
2762 void place_entry(const SourceId& id, bool is_long, double limit_price, double stop_price,
2763 double qty, const std::string& comment, const std::string& oca_name,
2764 int oca_type, int qty_type);
2765 void credit_close_ledger(const SourceId&, double units);
2766 void book_close_ledger(const SourceId&, double units);
2767
2768 // @source-state begin
2769 NativeStrategyHost* host_ = nullptr;
2770 mutable std::shared_ptr<detail::ExitScratchContext> exit_scratch_;
2771 // Retired ownership slots, kept empty to preserve generated host layout.
2772 // Trade/dependency scratch now belongs to the admitted native run.
2773 mutable std::shared_ptr<detail::CommandScratchRun> trade_scratch_run_;
2774 mutable std::shared_ptr<detail::CommandScratchRun> dependency_scratch_run_;
2775 const PineScheduler* input_scheduler_ = nullptr;
2776 PineStrategyConfig config_{};
2777 StagedConfiguration staged_{};
2778 mutable std::uint64_t run_counter_ = 0;
2779 std::uint64_t source_sequence_ = 0;
2780 std::uint64_t command_ordinal_ = 0;
2781 std::uint64_t broker_open_epoch_ = 0;
2782 std::int64_t last_broker_open_ms_ = std::numeric_limits<std::int64_t>::min();
2783 std::uint64_t source_command_sequence_ = 0;
2784 std::int32_t entry_attempt_bar_ = -1;
2785 std::uint32_t entry_attempts_on_bar_ = 0;
2786 // 192 KiB per adapter, not per engine. One adapter owns each cache;
2787 // these private disk read caches are never shared across threads.
2788 CohortTable cohorts_by_id_{pineforge::detail::IdTableReadCache::kCacheBytes};
2789 void observe_pending_scan(PendingScanCounter&) const noexcept;
2790 CohortTable::iterator require_cohort(const SourceId&);
2792 std::unordered_map<std::uint64_t, detail::LiveSourceHandle> live_by_source_key_;
2793 std::unordered_map<std::uint64_t, BracketRoster> bracket_families_;
2794 // Immutable, wholly settled families. All appends restore through
2795 // bracket_family(); exit_cancel_bracket also consults this archive. The keyed
2796 // digest is updated on every insertion/removal, never cached over a
2797 // mutable roster. Run identity has the same normalization as v4 handles.
2798 std::unordered_map<std::uint64_t, BracketRoster::Settled> settled_bracket_families_;
2799 std::uint64_t settled_bracket_digest_ = 0;
2800 BracketRoster& bracket_family(std::uint64_t key);
2801 void archive_settled_bracket_families();
2802 void clear_bracket_families() noexcept;
2803 std::vector<PendingBracketLeg> pending_bracket_legs_;
2804 std::vector<PendingEntry> pending_entries_;
2805 std::vector<DelayedMarketOrder> delayed_market_orders_;
2806 std::vector<DeferredOpenMarketableSell> deferred_open_marketable_sells_;
2807 // Flat stop entries the per-bar priced-entry throttle refused; re-armed
2808 // as their original stop at the bar close (validate_precommit is const).
2809 mutable std::vector<PlacementSnapshot> throttled_reopen_rearm_;
2810 int entry_openings_interval_index_ = -1;
2811 int entry_openings_this_interval_ = 0;
2812 std::vector<PendingSameBarCommand> pending_same_bar_commands_;
2813 std::vector<SourceShadowPending> source_shadow_pending_;
2814 double pending_same_bar_close_qty_ = 0.0;
2815 std::vector<PendingRelativeExit> pending_relative_exits_;
2816 // Mutable: the kernel's arm hook is const and records the level it
2817 // installed on the leg it restated.
2818 mutable std::vector<AnchoredRelativeLeg> anchored_relative_legs_;
2819 AnchoredRelativeStats anchored_relative_stats_{};
2820 std::int64_t anchored_cohort_sequence_ = 0;
2821 std::vector<PerEntryExit> per_entry_exits_;
2822 std::vector<PendingCoofRequest> pending_coof_requests_;
2823 std::vector<PendingMarginRevival> pending_margin_revivals_;
2824 std::vector<native_order::RequestHandle> live_handles_;
2825 std::vector<native_order::RequestHandle> first_open_newborns_;
2826 // The dropped-close receipts were a log no decision reads;
2827 // v4 keeps their count and a running digest, each folded once
2828 // (fold_dropped_close, pine_state_hash.cpp).
2829 std::uint64_t dropped_close_count_ = 0;
2830 std::uint64_t dropped_close_digest_ = 1469598103934665603ULL;
2831 std::vector<OpenEntryFeeFact> open_entry_fees_;
2832 // Current executions settle synchronously, while their generic Applied
2833 // notification is delivered after the enclosing callback. Record the
2834 // source-cohort debit so a second immediate command sees the new basis,
2835 // then suppress just that duplicate debit at notification delivery.
2836 std::unordered_set<std::uint64_t> current_debited_applied_ordinals_;
2837 std::unordered_set<std::uint64_t> intraday_loss_relabel_ordinals_;
2838 pineforge::detail::IdPairStore<std::int64_t, true> consumed_partial_exit_cycles_{
2840 std::unordered_set<std::uint64_t> bracket_shadowed_openings_;
2841 std::unordered_map<SourceId, NamedEntryCancelToken> named_entry_cancel_tokens_;
2842 // An exit bound to its entry id whose working order a strategy.cancel of
2843 // the id withdrew (PlacementSnapshot::pending_bound_exit): TradingView
2844 // keeps the binding, so the id's next entry order re-arms the exit as
2845 // called. The native request ends with its parent order, so the call is
2846 // kept here, one per (exit id, entry id) pair.
2847 struct IdBoundExit {
2848 SourceId exit_id;
2849 SourceId from_entry;
2850 PineExitLevels levels;
2851 double qty_percent = std::numeric_limits<double>::quiet_NaN();
2852 double qty = std::numeric_limits<double>::quiet_NaN();
2853 std::string comment;
2854 std::string oca_name;
2855 };
2856 std::vector<IdBoundExit> id_bound_exits_;
2857 // The close ledger: per id, the units entered under it that no close has
2858 // booked yet (close_logical_units_), and the same units by opening fill
2859 // in fill order (close_ledger_records_), which a booking beyond its id's
2860 // own units spills over oldest first.
2861 std::map<SourceId, double> close_logical_units_;
2862 std::vector<CloseLedgerRecord> close_ledger_records_;
2863 // The pyramiding records: one per entry fill, unmerged, with the incarnation
2864 // of the opening that filled it, booked as the close ledger books
2865 // (book_pyramiding_records); kept only under pyramiding above one.
2866 struct PyramidingRecord {
2867 SourceId id{};
2868 std::uint64_t incarnation = 0;
2869 double units = 0.0;
2870 };
2871 std::vector<PyramidingRecord> pyramiding_records_;
2872 // The partial strategy.exit children that filled, keyed
2873 // exit_id "\x1f" from_entry, to the entry incarnation they filled under.
2874 std::unordered_map<SourceId, std::uint64_t> exit_tombstones_;
2875 std::map<std::uint64_t, CloseCallsiteState> close_batch_callsites_;
2876 std::int32_t close_batch_bar_ = -1;
2877 std::uint64_t close_batch_queue_sequence_ = 0;
2878 double close_batch_pending_debt_ = 0.0;
2879 double close_batch_admitted_total_ = 0.0;
2880 std::uint64_t receipt_cursor_ = 0;
2881 std::uint64_t last_applied_ordinal_ = 0;
2882 bool materializing_relative_ = false;
2883 // The parent whose fill the current materialize_relative_exits re-run
2884 // serves; only that parent's armed anchored legs are adoptable.
2885 native_order::RequestHandle materializing_parent_{};
2886 std::int64_t current_position_cycle_ = 0;
2887 int current_position_sign_ = 0;
2888 std::uint64_t next_sequential_group_ = 0;
2889 bool source_batch_mutated_ = false;
2890 bool coof_recalc_active_ = false;
2891 bool coof_first_open_ = false;
2892 bool coof_open_stop_next_waypoint_ = false;
2893 // Whether the recalculating fill booked the forced_execution_price the
2894 // adapter set on its request (a fill AT a path point), not the matcher's
2895 // own price. It sits in the padding before the
2896 // next 8-byte member, so the class layout does not move.
2897 bool coof_fill_forced_ = false;
2898 std::uint64_t coof_market_entry_recalc_incarnation_ = 0;
2899 std::uint64_t coof_market_entry_recalc_fill_seq_ = 0;
2900 std::uint64_t coof_current_fill_seq_ = 0;
2901 double coof_fill_cursor_t_ = std::numeric_limits<double>::quiet_NaN();
2902 double coof_fill_target_print_ = std::numeric_limits<double>::quiet_NaN();
2903 NativeDecisionContext coof_context_{};
2904 Bar coof_script_bar_{};
2905 bool coof_script_bar_valid_ = false;
2906 // The per-script-bar POOC close basis was a log no
2907 // decision reads (the first basis each bar); v4 keeps the last bar key,
2908 // the count and a running digest of the first basis of each new bar.
2909 std::int64_t pooc_close_basis_last_bar_ = std::numeric_limits<std::int64_t>::min();
2910 std::uint64_t pooc_close_basis_count_ = 0;
2911 std::uint64_t pooc_close_basis_digest_ = 1469598103934665603ULL;
2912 double pooc_open_basis_ = 0.0;
2913 std::int64_t pooc_open_script_bar_ = std::numeric_limits<std::int64_t>::min();
2914 std::int64_t close_all_pending_script_bar_ = std::numeric_limits<std::int64_t>::min();
2915 // The script bar on which the script last called strategy.close(<held
2916 // id>) or strategy.close_all() while a position was open, and that
2917 // position's side (+1 long, -1 short): the close-first fact entry() reads.
2918 std::int64_t close_first_script_bar_ = std::numeric_limits<std::int64_t>::min();
2919 std::int32_t close_first_side_ = 0;
2920 double last_fx_rate_ = std::numeric_limits<double>::quiet_NaN();
2921 std::int64_t position_open_script_bar_ = std::numeric_limits<std::int64_t>::min();
2922 std::uint64_t position_open_epoch_ = 0;
2923 std::int32_t position_open_bar_index_ = -1;
2924 NativePathPhase position_open_phase_ = NativePathPhase::None;
2925 bool position_open_priced_ = false;
2926 std::int64_t last_margin_call_script_bar_ = std::numeric_limits<std::int64_t>::min();
2927 // The driver point ordinal whose kernel margin check the adapter's
2928 // own scheduling admitted. The kernel offers its check points at every
2929 // bar; exactly the one TradingView also checks at is let through, and
2930 // only for the driver point it was armed on.
2931 //
2932 // Deliberately NOT folded into the broker state hash. Driver point
2933 // ordinals are strictly monotone within a run and this value is only ever
2934 // compared for equality with the current point's, so from the first point
2935 // after the one it names it can never match again: it carries no
2936 // information across any observation boundary the hash identifies. Folding
2937 // it would move every adapter state hash (and the pinned hash-neutrality
2938 // evidence in tests/test_adapter_report_relower.cpp) for a value that is
2939 // dead by the time the next one is taken.
2940 std::uint64_t kernel_margin_path_point_ = std::numeric_limits<std::uint64_t>::max();
2941 // The driver point ordinal at which the adapter re-sizes a RESTING kernel
2942 // liquidation from the post-exit book (matching
2943 // the legacy cancel-and-reschedule after a priced bracket leg of this
2944 // script bar fills). A point named here is admitted only while a slice
2945 // actually rests, because the legacy broker did nothing at all when none
2946 // did. Same hash argument as kernel_margin_path_point_ above: a strictly
2947 // monotone ordinal compared only for equality with the current point's.
2948 std::uint64_t kernel_margin_resize_point_ = std::numeric_limits<std::uint64_t>::max();
2949 // The kernel check point whose adverse mark is the bar's close alone: its
2950 // call is the close's, taken after the script (close_point_margin_call).
2951 // And the script bar whose script called strategy.cancel_all(), which
2952 // withdraws that call. Same hash argument as kernel_margin_path_point_:
2953 // each is compared only for equality with the current point or bar, and
2954 // is dead once that bar's close has been checked.
2955 std::uint64_t close_margin_point_ = std::numeric_limits<std::uint64_t>::max();
2956 std::int64_t close_margin_cancelled_bar_ = std::numeric_limits<std::int64_t>::min();
2957 // The script bar whose close owes a follow-up (follow_margin_call), booked
2958 // there after the script, and the units it owes. Same hash argument: set
2959 // on the bar's path, dead once its close is checked.
2960 std::int64_t close_margin_follow_up_bar_ = std::numeric_limits<std::int64_t>::min();
2961 double close_margin_follow_up_units_ = 0.0;
2962 // The script bar whose path checks the adapter rests itself, point by
2963 // point, after a call at a point before the bar's adverse extreme
2964 // (rest_waypoint_margin_call). Same hash argument: set on the bar's path,
2965 // cleared at its close.
2966 std::int64_t waypoint_chain_bar_ = std::numeric_limits<std::int64_t>::min();
2967 // The script bar whose close queued a call for the next open
2968 // (close_point_margin_call): one the script's cancel_all() withdrew,
2969 // placed again behind its orders, or a one-unit call the close's fill
2970 // would not restore, tried again there.
2971 std::int64_t close_margin_open_bar_ = std::numeric_limits<std::int64_t>::min();
2972 // Retired checkpoint deferral: a carried
2973 // process_orders_on_close short used to wait behind its bar's close market
2974 // fills; TradingView checks its checkpoint over
2975 // the bar's path before the script instead. Never set now, and kept as
2976 // the constant it folds into the broker-state hash.
2977 std::int64_t pooc_close_checkpoint_deferred_ms_ = std::numeric_limits<std::int64_t>::min();
2978 std::int32_t signal_close_mc_event_bar_ = -1;
2979 std::int64_t signal_close_mc_position_cycle_ = 0;
2980 std::uint64_t signal_close_mc_entry_incarnation_ = 0;
2981 std::uint64_t signal_close_mc_fill_seq_ = 0;
2982 double signal_close_mc_before_qty_ = std::numeric_limits<double>::quiet_NaN();
2983 double signal_close_mc_remaining_qty_ = std::numeric_limits<double>::quiet_NaN();
2984 std::uint64_t last_margin_call_event_ordinal_ = 0;
2985 std::uint64_t last_margin_call_entry_incarnation_ = 0;
2986 std::int64_t last_margin_call_position_cycle_ = 0;
2987 bool last_margin_call_at_script_close_ = false;
2988 double last_margin_call_closed_units_ = 0.0;
2989 double last_margin_call_remaining_units_ = 0.0;
2990 // TradingView's realized money (gain_loss_money): every closed trade's
2991 // profit, in booking order, summed into gains and into losses apart.
2992 double source_gains_ = 0.0;
2993 double source_losses_ = 0.0;
2994 std::uint64_t source_money_folded_ = 0;
2995 std::int64_t risk_coof_direct_script_bar_ = std::numeric_limits<std::int64_t>::min();
2996 std::uint64_t cap_latest_fill_ = 0;
2997 bool source_margin_call_enabled_ = true;
2998 Bar policy_script_bar_{};
2999 bool policy_script_bar_valid_ = false;
3000 std::unordered_set<std::uint64_t> market_pyramid_adds_;
3001 std::unordered_map<std::uint64_t, NativeTrailState> trail_state_at_open_;
3002 // The trails retune_carried_trails_for_tick_reach re-priced for the bar
3003 // in flight, each with its own trailing distance; empty between bars.
3004 std::vector<std::pair<native_order::RequestHandle, double>> carried_trail_retunes_;
3005 bool stream_mode_ = false;
3006 SourceDayLedger day_ledger_{};
3007 PineRiskState risk_{};
3008 ShortSeedPlan short_seed_{};
3009 PendingShortSeedPlan pending_short_seed_{};
3010 native_order::RequestHandle short_seed_long_candidate_{};
3011 int last_bar_dual_entry_path_ = 0;
3012 std::int64_t last_bar_dual_entry_script_open_ms_ =
3013 std::numeric_limits<std::int64_t>::min();
3014 PendingIntentView pending_view_{};
3015 ::pineforge::detail::ScratchSequence<std::uint8_t> trade_exit_phase_;
3016public:
3018 std::uint64_t publications = 0, tail_rows = 0, comparisons = 0;
3021 std::uint64_t scratch_operations = 0, replacements = 0;
3022 std::size_t retained_placements = 0;
3023 };
3024private:
3025 // Derived ordering evidence and diagnostics are absent from the state fold.
3026 struct ExitGroupCertificate {
3027 const TradeLedger* ledger = nullptr;
3028 std::size_t base = 0, start = 0, end = 0;
3029 bool rows_valid = false;
3030 } exit_group_certificate_;
3031 ExitGroupWork exit_group_work_{};
3032 bool exit_group_certified(const TradeLedger&) const noexcept;
3033 void invalidate_exit_group(std::size_t first, std::size_t count = 1) noexcept;
3034 void certify_exit_group(const TradeLedger&, std::size_t start, bool ordered = true);
3035 void finish_exit_group_sort(::pineforge::detail::CommandScratchStore&,
3036 const ::pineforge::detail::ScratchStoreStats&) noexcept;
3037 // TradingView's entry-id table (global_exit_child_ranks): every entry id
3038 // in the order of its first fill, never removed, with a running digest,
3039 // and the most keys it ever held counting one transient key per live
3040 // strategy.close / close_all order -- and, for the alternative the tapes
3041 // leave open, per live strategy.order market order too.
3042 ::pineforge::detail::DiskIdTable<std::uint32_t> exit_rank_keys_{
3044 std::uint64_t exit_rank_keys_digest_ = 1469598103934665603ULL;
3045 std::uint32_t exit_rank_peak_ = 0;
3046 std::uint32_t exit_rank_alt_peak_ = 0;
3047 void require_tree_precommit(const NativePrecommitView&) const;
3048 void require_exit_rank_key(const SourceId&) const;
3049 void note_exit_rank_key(const SourceId&);
3050 void note_exit_rank_transients();
3051 // The exit phases of the trades before exit_phase_final_
3052 // are final (a same-bar exit sort reorders only its own bar's rows) and
3053 // folded once into exit_phase_digest_; a write below the mark refolds.
3054 std::size_t exit_phase_final_ = 0;
3055 std::uint64_t exit_phase_digest_ = 1469598103934665603ULL;
3056 // The v4 admission fold uses global event counts. Its retired prefix is
3057 // the restart point; the window may still change within a command/bar.
3058 // Folding at each script-bar end keeps recording-off runs bounded too.
3059 // admission_events_last_ validates the boundary without indexing a row
3060 // below the journal's retired prefix.
3061 mutable std::size_t admission_events_folded_ = 0;
3062 mutable std::uint64_t admission_events_last_ = 0;
3063 mutable std::uint64_t admission_events_digest_ = 1469598103934665603ULL;
3064 // A short's lagged follow-up (MarginScheduleSwitches::
3065 // lagged_short_follow_up) owed at a point of a script bar's path, booked
3066 // there ahead of that point's own check: the script bar, the point (0
3067 // its open, 1 and 2 its extremes in path order, 3 its close) and the
3068 // units. A process_orders_on_close close owes the next bar's open.
3069 std::int64_t owed_follow_up_bar_ = std::numeric_limits<std::int64_t>::min();
3070 std::int32_t owed_follow_up_point_ = -1;
3071 double owed_follow_up_units_ = 0.0;
3072 // A margin call booked at a close after the script (the close's own
3073 // check or a follow-up owed there): the script bar, the book the script
3074 // saw and the units called. A reversal the script placed there keeps the
3075 // close quantity it saw (MarginScheduleSwitches::frozen_reversal_close).
3076 std::int64_t close_call_after_script_bar_ = std::numeric_limits<std::int64_t>::min();
3077 double close_call_after_script_book_ = 0.0;
3078 double close_call_after_script_units_ = 0.0;
3079 // The opening checkpoint a market entry opening the book at an open owes
3080 // once the exit legs it released, marketable at that open, have filled
3081 // there, on the book they leave (MarginOpeningSwitches::
3082 // open_marketable_exit_first): the script bar, the entry's request and id
3083 // and its fill.
3084 std::int64_t open_exit_first_bar_ = std::numeric_limits<std::int64_t>::min();
3085 std::uint64_t open_exit_first_origin_ = 0;
3086 SourceId open_exit_first_id_{};
3087 double open_exit_first_fill_ = 0.0;
3088 // @source-state end
3089 // Diagnostic work counter: written from const methods, never read by a
3090 // decision, deliberately outside source state and the broker hash.
3091 mutable PendingScanWork pending_scan_work_{};
3092 // Derived decision cache, maintained from CohortFacts::opened pushes,
3093 // erases and whole-vector clears, asserted against those hashed vectors
3094 // in non-Release builds and checked by the fixture oracle in Release.
3095 std::size_t cohort_opened_slot_count_ = 0;
3096 bool fixture_verify_source_entry_slots_ = false;
3097 bool fixture_bracket_remove_precheck_ = true;
3098 // Set while an open's calls that TradingView does not repeat at one
3099 // price book (run_close_sized_open_calls, open_call_owing_follow_up); the
3100 // requests submitted meanwhile are listed in unrepeated_margin_calls_
3101 // until their on_applied, dispatched after the open's hook returns, reads
3102 // them there (recheck_at_fill). Not state: the flag is set and cleared
3103 // within one call, and the list lives within one driver point -- every
3104 // listed request is applied at the open that placed it, and each
3105 // on_bar_open empties it.
3106 bool open_call_sequence_ = false;
3107 std::vector<std::uint64_t> unrepeated_margin_calls_;
3108 // Install-time magnifier fact from NativeBeginArgs. Scheduler already
3109 // folds retained_.bar_magnifier; this copy is the host-kind-free query
3110 // for qualify_short_seed_plan.
3111 bool bar_magnifier_ = false;
3112 // The bound host's PineStrategyHost view (pine_view_of),
3113 // kept with the host it was taken from. It was a thread_local memo, and a
3114 // strategy module is dlopen'd, where every thread_local read is a dynamic
3115 // TLS lookup. Not state: a function of host_.
3116 mutable const NativeStrategyHost* pine_view_host_ = nullptr;
3117 mutable PineStrategyHost* pine_view_ = nullptr;
3118 PineStrategyHost* pine_view_of(NativeStrategyHost* host) const noexcept;
3119 // erase_retired_rows()'s working sets, kept so that a bar
3120 // that erases nothing allocates nothing once they have grown (a quiet bar
3121 // must not allocate: tests/test_adapter_quiet_bar.cpp). Not state: every
3122 // sweep empties the working sets first; only the derived memos described
3123 // below survive a sweep.
3124 struct RetiredRowScratch {
3125 std::vector<std::uint64_t> roots;
3126 std::vector<std::uint64_t> askable_origins;
3127 std::vector<std::uint64_t> live_groups;
3128 std::vector<std::int32_t> source_bars;
3129 std::vector<std::pair<std::uint64_t, const PlacementSnapshot*>> candidates;
3130 std::vector<std::uint64_t> candidate_names;
3131 std::vector<std::uint64_t> first_reissues;
3132 std::vector<const PlacementSnapshot*> origin_pins; // Rebuilt each sweep; not state.
3133 std::vector<std::uint64_t> doomed;
3134 // K1 legs that supersession can release (incarnation,
3135 // row, read-through high water, released flag); released incarnations sorted.
3136 struct HeldLeg {
3137 std::uint64_t incarnation = 0;
3138 const PlacementSnapshot* row = nullptr;
3139 std::uint64_t read_through = 0;
3140 bool released = false;
3141 };
3142 std::vector<HeldLeg> held;
3143 std::vector<std::uint64_t> released;
3144 // A memo that survives sweeps (clear() keeps it; a run begin empties
3145 // it): per leg held at the last test, the table's high water it was
3146 // found unsuperseded through. It answers what reading every row would:
3147 // the fields the test reads never change once a row is placed, and a
3148 // row that could supersede the leg is newer than every row it covers.
3149 std::vector<std::pair<std::uint64_t, std::uint64_t>> unsuperseded;
3150 // K3's exact first-reissue lookup over immutable identity/age/stop
3151 // fields. New candidates read all rows; unresolved old candidates
3152 // read only rows above reissue_high_water. A pinned first witness
3153 // needs no more reads. A run begin clears this derived memo.
3154 std::uint64_t reissue_high_water = 0;
3155 std::int64_t reissue_cycle = -1;
3156 std::vector<std::pair<std::uint64_t, std::uint64_t>> reissues;
3157 std::vector<std::pair<std::uint64_t, std::uint64_t>> next_reissues;
3158 std::vector<std::size_t> new_candidates;
3159 std::vector<std::size_t> known_candidates;
3160 std::vector<std::size_t> unresolved_candidates;
3161 // Anchored legs may be remembered after newer incarnations. They
3162 // are new peers too, even below the high water. Kept until a K3
3163 // pass consumes them (quiet-bar returns must not discard them).
3164 std::vector<std::uint64_t> adopted_reissues;
3165 // release_closed_cohort_origins()'s working sets --
3166 // the chain roots of the working requests, the openings of the open
3167 // lots and those no roster holds as a root, the origins leaving a
3168 // roster (cohort, incarnation), and the handle a lot is looked up by
3169 // (its run identity copied only when it changes) -- and whether a
3170 // receipt read has seen a command other than Accepted since its last
3171 // pass (a memo: fresh working roots release nothing; begin sets it).
3172 std::vector<std::uint64_t> working_roots;
3173 std::vector<std::uint64_t> lot_openings;
3174 std::vector<std::uint64_t> successor_lots;
3175 std::vector<std::pair<native_order::CohortHandle, std::uint64_t>> released_origins;
3176 native_order::RequestHandle lot_probe{};
3177 bool roster_pass_due = true;
3178 // Derived live-family counts stay in step with placements and live handles
3179 // at their shared writer; they add no lifecycle or hash state. Contiguous
3180 // Entry..Risk slots count families; extra slots count unpriced Order rows
3181 // and rows eligible for dynamic-exit or capture policy.
3182 static constexpr std::size_t family_count =
3183 static_cast<std::size_t>(PineOrderFamily::Risk) + 1;
3184 static constexpr std::size_t market_order_slot = family_count;
3185 static constexpr std::size_t exit_policy_slot = family_count + 1;
3186 std::size_t live_families[family_count + 2]{};
3187 // True certifies the existing retire_origin_prefixes early return:
3188 // each nonempty front has a placement. Only placement erasure can
3189 // falsify it; a new origin is appended after remember().
3190 bool origin_fronts_pinned = true;
3191 void clear() noexcept {
3192 roots.clear();
3193 askable_origins.clear();
3194 live_groups.clear();
3195 source_bars.clear();
3196 candidates.clear();
3197 candidate_names.clear();
3198 first_reissues.clear();
3199 origin_pins.clear();
3200 doomed.clear();
3201 held.clear();
3202 released.clear();
3203 next_reissues.clear();
3204 new_candidates.clear();
3205 known_candidates.clear();
3206 unresolved_candidates.clear();
3207 }
3208 };
3209 RetiredRowScratch retired_row_scratch_;
3210 void remember_live(const native_order::RequestHandle&, PlacementSnapshot&&);
3211 void forget_live(const native_order::RequestHandle&) noexcept;
3212 void requeue_live(const native_order::RequestHandle&);
3213 void clear_live() noexcept;
3214 void erase_placement(std::uint64_t);
3215 void count_live(const PlacementSnapshot&, bool adding) noexcept;
3216 void assert_live_counts() const noexcept;
3217 bool has_live_family(PineOrderFamily) const noexcept;
3218 bool has_live_exit_policy() const noexcept;
3219 bool has_live_trail_policy() const noexcept;
3220 // The configuration's part of the margin and admission scopes, which the
3221 // per-bar path asks several times a bar: each a conjunction of reads of
3222 // config_ and staged_ alone, formed again whenever either is set
3223 // (set_configuration, set_staged_configuration), the only way either
3224 // changes. Not state: a function of config_ and staged_. A run outside a
3225 // scope answers it here without the call; inside, the scope's own tests
3226 // run as before.
3227 struct StagedScopeFacts {
3228 bool positive_grid = false; // a lot step above 0
3229 bool unit_grid = false; // a lot step in (0, 1]
3230 bool unit_point_money = false; // point value 1, no FX series
3231 bool unit_money = false; // and an account FX rate of 1
3232 bool mintick = false; // a finite positive mintick
3233 };
3234 StagedScopeFacts staged_scopes_{};
3235 bool lot_grid_pinned_ = false; // pinned_lot_grid()
3236 bool gain_loss_configured_ = false; // gain_loss_regime()'s configuration
3237 bool slipped_long_configured_ = false; // slipped_long_margin_scope()'s
3238 bool unified_configured_ = false; // unified_admission_scope()'s
3239 bool ledger_records_configured_ = false; // the pyramiding records'
3240 bool pooc_fee_configured_ = false; // pooc_fee_units()'s
3241 void refresh_configuration_scopes(const PineStrategyConfig& config) noexcept;
3242 // After a source evaluation, queue this bar's marketable
3243 // flat entries in TradingView's order at their shared fill point.
3244 void order_same_point_entries();
3245 double pending_opposite_market_units(bool is_long, std::int64_t script_open_ms, const SourceId& id) const;
3246 // The configurations those flat-pair rules cover,
3247 // and whether a pair's opposite member fills next at the same price.
3248 bool same_point_pair_scope() const;
3249 bool same_point_two_leg_book(const SourceId& id, std::int64_t script_open_ms) const;
3250 bool same_point_pair_fill_follows(const PlacementSnapshot& filled,
3251 const native_order::RequestHandle& filled_handle,
3252 double price) const;
3253 // A flat market-and-priced pair at a process_orders_on_close close.
3254 bool fill_pooc_close_pair(double raw_close, const NativeDecisionContext& context);
3255 void finish_admission_bar();
3256 void on_bar_close_policy(const Bar&, const NativeDecisionContext&);
3257 void fold_admission_events() const;
3258 std::uint64_t checked_live_key(const SourceId&) const;
3259 detail::LiveSourceHandle live_source_value(native_order::RequestHandle, const SourceId&) const;
3260 void cancel_reversal_exit_owners(const std::vector<SourceId>& preserved,
3261 detail::ExitScratchRows<SourceId>& selection);
3262 // Diagnostic only; never folded into continuation state.
3263 mutable std::uint64_t projected_raw_pending_calls_ = 0;
3264 // A disposable projection of current raw positions, never row pointers.
3265 // Mutable placement borrows and every queue writer invalidate its inputs.
3266 std::uint64_t pending_projection_version_ = 0;
3267 mutable std::uint64_t projected_pending_version_ = 0;
3268 mutable std::uint64_t projected_placement_version_ = 0;
3269 mutable bool projected_pending_valid_ = false;
3270 mutable std::vector<int> projected_pending_indexes_;
3271 mutable std::vector<int> projected_pending_next_;
3272 bool ensure_projected_pending_index() const;
3273 int projected_pending_next(int index) const noexcept;
3274 int projected_raw_pending_next(int raw) const noexcept;
3275 int projected_level_resolved(const PlacementSnapshot&) const noexcept;
3276 // Bump the version before mutation. Do not keep the returned reference
3277 // across another pending_write of the same container.
3278 template<class Row> Row& pending_write(Row& row) noexcept {
3279 refresh_pending_view();
3280 return row;
3281 }
3282 static int scan_projected_pending_size(const PineExecutionAdapter&) noexcept;
3283 static bool scan_projected_pending_at(
3284 const PineExecutionAdapter&, int index, const PlacementSnapshot*&,
3285 native_order::RequestHandle&, int* raw_index = nullptr) noexcept;
3286
3287public:
3288 // Cumulative physical work, reset at run begin. No decision or state fold
3289 // reads these integers. Population snapshots are explicit diagnostics.
3291 std::uint64_t placement_sweeps = 0, prefix_sweeps = 0;
3292 std::uint64_t row_visits = 0, pin_rows = 0, gate_rows = 0;
3293 std::uint64_t candidate_rows = 0, final_rows = 0;
3294 std::uint64_t pin_comparisons = 0, peer_visits = 0, held_comparisons = 0;
3295 std::uint64_t k3_pairs = 0, root_visits = 0, family_visits = 0;
3296 std::uint64_t cohort_rows = 0, certificate_rows = 0, fronts_tested = 0;
3298 std::uint64_t rows_erased = 0, origins_retired = 0;
3299 // Slots include empty dense-window slots, as well as returned old rows.
3300 // Backing counts are restored/spilled cohort records, not page I/O.
3303 // Debug repeats the sweep independently of the decision-path counts.
3305 };
3307 std::uint64_t m = 0, R = 0, C = 0;
3308 std::uint64_t live = 0, pending = 0, owners = 0, opened = 0, retired_exit_rows = 0;
3309 };
3310 const RetirementWork& retirement_work() const noexcept { return retirement_work_; }
3311 // Explicit observation only: visits placements to classify nonlive exits.
3312 RetirementPopulation retirement_population() const;
3313
3314private:
3315 RetirementWork retirement_work_{};
3316 struct RetirementObservation {
3317 PineExecutionAdapter& adapter;
3318 RetirementWork& work;
3319 std::uint64_t slots, restored, spilled, spill_visits;
3320 explicit RetirementObservation(PineExecutionAdapter& owner) noexcept
3321 : RetirementObservation(owner, owner.retirement_work_) {}
3322 RetirementObservation(PineExecutionAdapter& owner, RetirementWork& destination) noexcept
3323 : adapter(owner), work(destination), slots(owner.placement_.visited_slots()),
3324 restored(owner.cohorts_by_id_.work_counters().rows_restored),
3325 spilled(owner.cohorts_by_id_.work_counters().rows_spilled),
3326 spill_visits(owner.cohorts_by_id_.work_counters().rows_visited_by_spill) {}
3327 ~RetirementObservation() {
3328 const auto& backing = adapter.cohorts_by_id_.work_counters();
3329 work.placement_slots += adapter.placement_.visited_slots() - slots;
3330 work.backing_rows_restored += backing.rows_restored - restored;
3331 work.backing_rows_spilled += backing.rows_spilled - spilled;
3332 work.spill_rows_visited += backing.rows_visited_by_spill - spill_visits;
3333 }
3334 };
3335 // Exact current references, never a digest or a historical placement copy.
3336 // These sets and the certificate are derived scratch, outside source state.
3337 struct RetirementInputs {
3338 std::vector<std::uint64_t> roots, askable, groups, marks;
3339 std::vector<std::int32_t> bar_numbers;
3340 bool operator==(const RetirementInputs& other) const noexcept {
3341 return roots == other.roots && askable == other.askable
3342 && groups == other.groups && marks == other.marks && bar_numbers == other.bar_numbers;
3343 }
3344 };
3345 struct RetirementGate {
3346 RetirementInputs saved, current;
3347 bool certified = false;
3348 std::int64_t cycle = -1, interval = -1;
3349 std::int64_t broker_open = std::numeric_limits<std::int64_t>::min();
3350 std::int64_t interval_expiry = std::numeric_limits<std::int64_t>::max();
3351 std::int64_t broker_expiry = std::numeric_limits<std::int64_t>::max();
3352 };
3353 RetirementGate retirement_gate_;
3354 void invalidate_retirement() noexcept { retirement_gate_.certified = false; }
3355 bool capture_retirement_inputs(RetirementInputs&) const;
3356 void observe_retirement_expiry(const PlacementSnapshot&, std::int64_t interval) noexcept;
3357 void sweep_retired_rows(const NativeDecisionContext&, RetirementWork&);
3358 void finish_retirement_phases();
3359#ifndef NDEBUG
3360 std::array<std::uint64_t, 3> retirement_validation_state() const;
3361#endif
3362};
3363
3364namespace detail {
3365// The TradingView margin-call schedule rules pinned on the tapes under
3366// tests/fixtures/margin_call_rules (short-cutoff-gate) and
3367// tests/fixtures/margin_schedule_rules, one switch each so a regression
3368// bisects per rule; all on, only tests change one. Process-wide, read by every
3369// adapter; not installed API and no strategy input reaches it.
3371 // A short's call of X units found at mark p is taken only where it
3372 // restores the book at its own slipped print p': X p' m > Q p' m - E(p).
3373 bool short_call_gate = true;
3374 // A carried process_orders_on_close short is checked at every point of
3375 // its bar's path before the script: the open, both extremes, the close.
3376 // Ablate it together with close_call_follow_up_at_open: each corrects its
3377 // own bars, and with one off the other's equity error can flip a later
3378 // one-lot boundary (a third-party NYSE:F 15 strategy: 144 exact rows with this on and
3379 // the follow-up off, 169 with both off, 177 with both on).
3381 // After a short's call TradingView checks the book again at the call's
3382 // fill with the called units out of it but their P&L unbooked, lot by
3383 // lot, first in first out, and calls the last still-short check's
3384 // lot-floored restore four times at the bar's next path point.
3386 // A deficit the open's check found and vetoed stays pending: the first
3387 // path point whose segment touches a resting exit is checked before that
3388 // exit, and after a call there the exit fills at that point.
3390 // A reversal or close the script placed at a close where a margin call
3391 // is booked after it keeps the close quantity the script saw.
3393 // A same-side add at full margin is judged as the whole book at the
3394 // tick-built signal close against the fee-charged equity there.
3395 bool add_signal_close = true;
3396 // A long's lagged follow-up on whole-share lot grids too.
3398 // The money of a commission-free long's one-unit call after a trade
3399 // history: initial capital plus the G + L sums.
3401 // A process_orders_on_close short's call sized at the previous close and
3402 // executed at the open is followed at that open, at the call's own fill.
3404 // A process_orders_on_close short's lagged follow-up owed to its bar's
3405 // close is booked after the script, ahead of the fills of the orders the
3406 // script placed there, which keep the sizes they were placed with: a
3407 // strategy.close(id) or close_all() opens the excess on the other side as
3408 // its own lot, a reversal entry's excess joins the entry's lot.
3410};
3412
3413// The opening-call rules pinned on the tapes under
3414// tests/fixtures/margin_open_rules: the call a full-margin
3415// process_orders_on_close position takes at the open after the close fill
3416// that opened it, the open's check after an add filled there, the fill-time
3417// admission of an explicit quantity at the open and the refined lagged
3418// follow-up. One switch each so a regression bisects per rule; all on, only
3419// tests change one. Process-wide; not installed API and no strategy input
3420// reaches it.
3422 // The call is sized at the source close c': money and unit margin at c',
3423 // a long's as a short's.
3425 // Its one-unit band and the short gate are priced at the call's own
3426 // print, the open's tick moved by the exit side's slippage.
3428 // After it the open checks the booked book at its own mark, gated at the
3429 // same print; short there with nothing booked, the sequence ends.
3431 // Then the lagged follow-up of the last call, at that print; a short's
3432 // booked re-check owes it to the first extreme instead.
3434 // A leveraged book's open check runs after the market adds filling at
3435 // that open, on the book holding them and their entry fees.
3437 // An explicit-quantity market entry from flat filling at the open is
3438 // dropped where sig10(sig10(E) / Q m) is below the slipped open tick.
3440 // The lagged follow-up, with its one-unit fallback, on whole-share books.
3442 // A follow-up that books leaves its own follow-up for the next point.
3444 // An owed follow-up is dropped at a point whose own check is short.
3445 // In the opening-call scope the open's own call, and a short's booked
3446 // re-check, owe their follow-up to the bar's first extreme.
3448 // The schedule on a book of several lots the close's fills opened: the
3449 // call sized at the close on the whole book, taken first in first out,
3450 // and its follow-up checked lot by lot over the lots it took.
3452 // A market entry opening the book from flat at a bar's open (one entry,
3453 // neither process_orders_on_close nor calc_on_order_fills) is checked
3454 // there after the exit legs it released -- its own, and a strategy.exit
3455 // without from_entry placed while flat after the entry -- already
3456 // marketable at that open, have filled, on the book they leave, a long's
3457 // as a short's; a leg the bar reaches only after the open fills after the
3458 // check's call. Off, and for every other fill or leg: a short's check
3459 // gives way to any leg the bar touches.
3461 // A strategy.exit without from_entry called while flat with no entry
3462 // order of any id working (not calc_on_order_fills) is void: the opening
3463 // check of the market entry placed after it is taken as with no exit,
3464 // and that entry's fill withdraws its legs, which never fill. Where a
3465 // stop or limit entry fills first the legs keep their former course.
3467};
3469} // namespace detail
3470
3471} // 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:503
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