PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
pine_adapter.cpp
Go to the documentation of this file.
10
11#include "../engine_internal.hpp"
12#include "../engine_value_invalid.hpp"
13#include "../native_execution_consumer.hpp"
15#include "../timezone.hpp"
16#include "pine_host_reads.hpp"
17#include "pine_quiet_bar.hpp"
20#include "pine_run_failure.hpp"
23#include "../command_scratch_cache.hpp"
31#include "roster_pass_work.hpp"
32// Only an adapter TU using the existing observation seam selects the
33// counting instantiations. Normal builds select the empty diagnostic tag.
34#ifndef PINEFORGE_POSITION_TAIL_DIAGNOSTICS
35#ifdef PINEFORGE_POSITION_TAIL_OBSERVE
36#define PINEFORGE_POSITION_TAIL_DIAGNOSTICS \
37 ::pineforge::source::detail::PositionTailDiagnostics
38#else
39#define PINEFORGE_POSITION_TAIL_DIAGNOSTICS \
40 ::pineforge::source::detail::PositionTailNoDiagnostics
41#endif
42#endif
43
44// Compile-time observation only, as for the position-tail seam above. No
45// environment variable, strategy input or test population selects a path.
46#ifndef PINEFORGE_TAIL_WORK
47#define PINEFORGE_TAIL_WORK(site, count) ((void)0)
48#endif
49// Separate from the retirement-loop observer's fixed site vocabulary.
50// Overrides observe lookup work only; the default has no storage or I/O.
51#ifndef PINEFORGE_LOOKUP_INDEX_WORK
52#define PINEFORGE_LOOKUP_INDEX_WORK(site, owner, run, count) ((void)0)
53#endif
54#ifndef PINEFORGE_RETIREMENT_OBSERVE
55#define PINEFORGE_RETIREMENT_OBSERVE(rows, cycle, askable, roots, candidates, first) ((void)0)
56#endif
57#ifndef PINEFORGE_MARGIN_REVIVAL_OBSERVE
58#define PINEFORGE_MARGIN_REVIVAL_OBSERVE(before, rows, event, context, units, cycle, selected) ((void)0)
59#endif
60
61#include <algorithm>
62#include <atomic>
63#include <array>
64#include <cassert>
65#include <cerrno>
66#include <cmath>
67#include <cstdint>
68#include <cstdio>
69#include <cstdlib>
70#include <ctime>
71#include <cstring>
72#include <exception>
73#include <limits>
74#include <map>
75#include <memory>
76#include <new>
77#include <stdexcept>
78#include <string_view>
79#include <type_traits>
80#include <typeinfo>
81#include <utility>
82
83// A condition the per-bar path almost never meets: a narrow margin or
84// admission scope. The hint lays its body out of the fall-through path; the
85// condition is evaluated exactly as written (CMakeLists.txt,
86// -falign-functions: the per-bar Pine path is front-end bound).
87#define PF_RARE(condition) __builtin_expect(static_cast<bool>(condition), 0)
88
89namespace pineforge::source {
90#ifdef PINEFORGE_CR277_DIAGNOSTIC
91namespace {
92// Process totals: 0 = close(""), 1 = close_all(). Count successful transfers
93// of a live, unpriced Entry placed on this script bar, including same-side
94// entries. This does not count batch transfers or prove a later reissue.
95std::atomic<std::uint64_t> cr277_pending_close_pushes[2]{};
96}
97extern "C" std::uint64_t pineforge_cr277_diagnostic_hits(int site) noexcept {
98 return site >= 0 && site < 2
99 ? cr277_pending_close_pushes[site].load(std::memory_order_relaxed) : 0;
100}
101#endif
102#ifdef PINEFORGE_VALUE_INVALID_FIXTURES
103namespace {
104thread_local double (*adapter_execution_value_fixture)(const char*, double) = nullptr;
105}
106void fixture_adapter_execution_values(double (*fixture)(const char*, double)) noexcept {
107 adapter_execution_value_fixture = fixture;
108}
109#endif
110bool PineExecutionAdapter::modeled_input() const {
111 if (input_scheduler_ && input_scheduler_->input_is_observed_ticks()) {
112 return false;
113 }
114 const auto* point = host_ ? detail::callback_point(*host_) : nullptr;
115 return modeled_pine_input(point
116 ? point->decision.coordinate.provenance : NativePriceProvenance::Calculation);
117}
118
119namespace {
120
121constexpr double kNaN = std::numeric_limits<double>::quiet_NaN();
122
123void check_slippage_fill_price(double value, bool has_basis) {
124 if (has_basis && !std::isfinite(value)) {
125 engine_command_value_invalid_or_count("slippage.fill_price", EngineValueStage::slippage,
126 EngineValueReason::numeric_overflow);
127 }
128}
129
130#ifdef PINEFORGE_VALUE_INVALID_FIXTURES
131double fixture_adapter_execution_value(const char* site, double value) {
132 return adapter_execution_value_fixture ? adapter_execution_value_fixture(site, value) : value;
133}
134#endif
135
136bool same_double_bits(double left, double right) noexcept {
137 std::uint64_t left_bits = 0;
138 std::uint64_t right_bits = 0;
139 std::memcpy(&left_bits, &left, sizeof(left_bits));
140 std::memcpy(&right_bits, &right, sizeof(right_bits));
141 return left_bits == right_bits;
142}
143
144bool same_exit_levels(const PineExitLevels& left, const PineExitLevels& right) noexcept {
145 return same_double_bits(left.limit, right.limit)
146 && same_double_bits(left.stop, right.stop)
147 && same_double_bits(left.trail_points, right.trail_points)
148 && same_double_bits(left.trail_offset, right.trail_offset)
149 && same_double_bits(left.trail_price, right.trail_price)
150 && same_double_bits(left.profit_ticks, right.profit_ticks)
151 && same_double_bits(left.loss_ticks, right.loss_ticks);
152}
153
154// Every leg of an exit's lifecycle retired: the script withdrew the exit
155// (strategy.cancel / strategy.cancel_all, retire_cancelled_exits), a state no
156// suspension produces (Suspend retires at most the trail leg). No margin call
157// revives such an exit.
158bool withdrawn(const exit_legs::Lifecycle& legs) noexcept {
159 return legs.retired(exit_legs::Leg::Stop) && legs.retired(exit_legs::Leg::Limit)
160 && legs.retired(exit_legs::Leg::Trail);
161}
162
163// A filled named origin-free limit has no consumed-origin reader. Even if a
164// later reversal suspends it, margin revival cannot execute it: both the placed
165// and inherited lifecycle definitions lack a stop (and every loss/trail input).
166// Global rows must stay: the revival's supersession walk reads them, while K3
167// only supplies witnesses for named exits. Do not infer safety from !live alone:
168// cancelled stops and dormant replacements can still revive. K2/K3 may
169// nevertheless need a named filled limit as another row's supersessor.
170bool unbound_limit_without_revival(const PlacementSnapshot& row) noexcept {
171 const auto& levels = row.exit_levels;
172 const auto no_protection = [](const exit_legs::Prices& prices) {
173 return std::isnan(prices.stop_price) && std::isnan(prices.loss_ticks)
174 && std::isnan(prices.trail_points) && std::isnan(prices.trail_price)
175 && std::isnan(prices.trail_offset);
176 };
177 const auto& suspension = row.legs.suspension();
178 return row.family == PineOrderFamily::ExitLimit && !row.from_entry.empty()
179 && row.bracket_origin.incarnation == 0 && row.per_entry_origin == 0
180 && !row.pending_bound_exit && !row.legs.pending_replacement()
181 && std::isnan(levels.stop) && std::isnan(levels.loss_ticks)
182 && std::isnan(levels.trail_points) && std::isnan(levels.trail_price)
183 && std::isnan(levels.trail_offset)
184 && no_protection(row.legs.prices())
185 && (!suspension || !suspension->revival_definition
186 || no_protection(suspension->revival_definition->prices()));
187}
188
189bool filled_limit_without_revival(const PlacementSnapshot& row) noexcept {
190 return row.filled_exit && unbound_limit_without_revival(row);
191}
192
193// Order-independent keyed fold: only immutable archive records enter it.
194// XOR permits exact removal on restore/cancel without replaying history.
195std::uint64_t settled_bracket_hash(std::uint64_t key, const BracketRoster::Settled& row) {
196 BrokerStateHashSink f;
197 f.u(key); f.u(row.last); f.u(row.count); f.u(row.digest);
198 return f.h;
199}
200
201bool finite_positive(double value) noexcept {
202 return std::isfinite(value) && value > 0.0;
203}
204
205bool finite_non_negative(double value) noexcept {
206 return std::isfinite(value) && value >= 0.0;
207}
208
209// ab9714be pine_strategy_commands.cpp:533-537: a non-NaN limit/stop is a
210// present price level, including 0.0.
211bool price_present(double value) noexcept { return !std::isnan(value); }
212
213bool pure_stop_entry_marketable_at(const PlacementSnapshot& snapshot, double open) noexcept {
214 if (snapshot.family != PineOrderFamily::Entry) return false;
215 if (!finite_positive(snapshot.exit_levels.stop)) return false;
216 if (finite_positive(snapshot.exit_levels.limit)
217 || finite_positive(snapshot.exit_levels.trail_points)
218 || finite_positive(snapshot.exit_levels.trail_price)
219 || finite_positive(snapshot.exit_levels.trail_offset)) {
220 return false;
221 }
222 return snapshot.is_long ? open >= snapshot.exit_levels.stop
223 : open <= snapshot.exit_levels.stop;
224}
225
226// An order resting at a LIMIT level alone (no stop, no trail).
227bool pure_limit_entry(const PlacementSnapshot& snapshot) noexcept {
228 return finite_positive(snapshot.exit_levels.limit)
229 && !finite_positive(snapshot.exit_levels.stop)
230 && !finite_positive(snapshot.exit_levels.trail_points)
231 && !finite_positive(snapshot.exit_levels.trail_price)
232 && !finite_positive(snapshot.exit_levels.trail_offset);
233}
234
235double next_source_path_waypoint(const Bar& bar, NativePathPhase phase,
236 double current, bool high_first,
237 double tick = 0.0, int slippage = 0) noexcept {
238 const double tol = (slippage > 0 && tick > 0.0) ? (slippage + 0.5) * tick : 1e-6;
239 const auto at = [tol](double left, double right) {
240 return std::abs(left - right) <= tol;
241 };
242 switch (phase) {
243 case NativePathPhase::Open:
244 return high_first ? bar.high : bar.low;
245 case NativePathPhase::High:
246 if (!at(current, bar.high) && current < bar.high - tol) return bar.high;
247 return high_first ? bar.low : kNaN;
248 case NativePathPhase::Low:
249 if (!at(current, bar.low) && current > bar.low + tol) return bar.low;
250 return high_first ? kNaN : bar.high;
251 case NativePathPhase::Close:
252 return kNaN;
253 case NativePathPhase::None:
254 return kNaN;
255 }
256 return kNaN;
257}
258
259std::uint64_t fnv_append(std::uint64_t value, const void* bytes, std::size_t size) noexcept {
260 const auto* p = static_cast<const unsigned char*>(bytes);
261 for (std::size_t i = 0; i < size; ++i) {
262 value ^= p[i];
263 value *= 1099511628211ULL;
264 }
265 return value;
266}
267
268std::uint64_t fnv_string(std::string_view value) noexcept {
269 return fnv_append(1469598103934665603ULL, value.data(), value.size());
270}
271
272// java.lang.String.hashCode of a UTF-8 id: h = 31 * h + u over its UTF-16 code
273// units, wrapping at 32 bits. False for a malformed UTF-8 sequence: a bad lead
274// or continuation byte, an overlong form, an encoded surrogate or a code point
275// past U+10FFFF (none of which a Pine string can hold).
276bool java_string_hash(std::string_view text, std::uint32_t& hash) noexcept {
277 std::uint32_t h = 0;
278 for (std::size_t i = 0; i < text.size();) {
279 const auto lead = static_cast<unsigned char>(text[i]);
280 std::uint32_t point = 0;
281 std::size_t width = 0;
282 if (lead < 0x80) { point = lead; width = 1; }
283 else if ((lead & 0xE0) == 0xC0) { point = lead & 0x1FU; width = 2; }
284 else if ((lead & 0xF0) == 0xE0) { point = lead & 0x0FU; width = 3; }
285 else if ((lead & 0xF8) == 0xF0) { point = lead & 0x07U; width = 4; }
286 else return false;
287 if (i + width > text.size()) return false;
288 for (std::size_t k = 1; k < width; ++k) {
289 const auto next = static_cast<unsigned char>(text[i + k]);
290 if ((next & 0xC0) != 0x80) return false;
291 point = (point << 6) | (next & 0x3FU);
292 }
293 if ((width == 2 && point < 0x80U) || (width == 3 && point < 0x800U)
294 || (width == 4 && (point < 0x10000U || point > 0x10FFFFU))
295 || (point >= 0xD800U && point <= 0xDFFFU)) {
296 return false;
297 }
298 i += width;
299 if (point >= 0x10000U) {
300 point -= 0x10000U;
301 h = 31U * h + (0xD800U + (point >> 10));
302 h = 31U * h + (0xDC00U + (point & 0x3FFU));
303 } else {
304 h = 31U * h + point;
305 }
306 }
307 hash = h;
308 return true;
309}
310
311void copy_pending_string(std::string_view value, char* out, std::uint8_t* truncated,
312 std::uint64_t* hash) noexcept {
313 *hash = fnv_string(value);
314 const std::size_t size = std::min<std::size_t>(value.size(), 63U);
315 if (size != 0) std::memcpy(out, value.data(), size);
316 out[size] = '\0';
317 *truncated = value.size() > size ? 1U : 0U;
318}
319
320void copy_pending_prefixed_string(std::string_view prefix, std::string_view value,
321 char* out, std::uint8_t* truncated,
322 std::uint64_t* hash) noexcept {
323 // The public mirror is callable with allocation disabled. Hash and copy
324 // the synthetic close prefix in-place instead of materializing a string.
325 std::uint64_t digest = fnv_append(
326 1469598103934665603ULL, prefix.data(), prefix.size());
327 *hash = fnv_append(digest, value.data(), value.size());
328 const std::size_t total = prefix.size() + value.size();
329 const std::size_t prefix_size = std::min<std::size_t>(prefix.size(), 63U);
330 if (prefix_size != 0) std::memcpy(out, prefix.data(), prefix_size);
331 const std::size_t value_size = std::min<std::size_t>(
332 value.size(), 63U - prefix_size);
333 if (value_size != 0)
334 std::memcpy(out + prefix_size, value.data(), value_size);
335 out[prefix_size + value_size] = '\0';
336 *truncated = total > prefix_size + value_size ? 1U : 0U;
337}
338
339int mirror_order_type(PineOrderFamily family) noexcept {
340 switch (family) {
341 case PineOrderFamily::Entry: return 1;
342 case PineOrderFamily::Order: return 3;
347 case PineOrderFamily::ExitTrail: return 2;
349 case PineOrderFamily::Risk: return 0;
350 }
351 return 0;
352}
353
354std::uint64_t source_key(const SourceId& left, const SourceId& right) noexcept {
355 std::uint64_t value = fnv_string(left);
356 const char separator = '\0';
357 value = fnv_append(value, &separator, sizeof(separator));
358 return detail::source_key_test_mask(fnv_append(value, right.data(), right.size()));
359}
360
361double nearest_tick(double value, double tick) noexcept {
362 if (!std::isfinite(value) || !finite_positive(tick)) return value;
363 return std::floor(value / tick + 0.5) * tick;
364}
365
366// The grid index k = floor(p / mintick + 0.5) re-derived as the decimal
367// quotient k / (1 / mintick), never the n * mintick product: a function of k
368// alone, so two prints on one grid point compare equal and a limit level on
369// the grid is never crossed by a ULP.
370double source_decimal_tick(double value, double tick) noexcept {
371 if (!std::isfinite(value) || !finite_positive(tick)) return value;
372 const double k = std::floor(value / tick + 0.5);
373 const double inverse = 1.0 / tick;
374 const double integral_inverse = std::floor(inverse + 0.5);
375 if (integral_inverse > 0.0
376 && std::abs(inverse - integral_inverse) <= 1e-6 * integral_inverse) {
377 return k / integral_inverse;
378 }
379 return k * tick;
380}
381
382double source_bar_fill_tick(double value, double tick) noexcept {
383 if (!std::isfinite(value) || !finite_positive(tick)) return value;
384 const double k = std::floor(value / tick + 0.5);
385 // ab9714be include/pineforge/engine.hpp:1207-1210 and 1258-1266: the
386 // legacy bar fill price is ``round_to_mintick`` = floor(p / mintick + 0.5)
387 // * mintick, and it is IDEMPOTENT — a price that is already on the tick
388 // grid comes back bit-identical (the comment there pins that the
389 // downstream directional snap is an identity on such a result, because
390 // n * mintick sits one binary64 ULP above the plain decimal quotient).
391 // Re-deriving an already rounded price as k / (1 / tick) dropped that ULP,
392 // so a same-bar reversal entry no longer booked on its own close's print.
393 if (k * tick == value) return value;
394 return source_decimal_tick(value, tick);
395}
396
397// ab9714be engine.hpp:1264-1266: a fill AT an on-grid raw print books
398// bar_fill_price(print) = floor(p / mintick + 0.5) * mintick, which can sit
399// one binary64 ULP off the print itself; both name the same path point.
400bool source_same_point(double booked, double raw, double tick) noexcept {
401 return booked == raw
402 || (finite_positive(tick) && source_bar_fill_tick(raw, tick) == raw
403 && nearest_tick(raw, tick) == booked);
404}
405
406// Only raw door callers supply a literal field. Derived prices retain the
407// original arithmetic; no source ownership is inferred from their bits.
408double source_arith_value(const std::optional<double>& value, const char* field) {
409 if (!value)
411 return *value;
412}
413
414double directional_tick(double value, double tick, bool upward,
415 const char* field = nullptr) {
416 if (!std::isfinite(value) || !finite_positive(tick)) return value;
417 if (field) {
418 const double scaled = source_arith_value(checked_div(value, tick), field);
419 return source_arith_value(checked_mul(
420 upward ? std::ceil(scaled - 1e-9) : std::floor(scaled + 1e-9), tick), field);
421 }
422 const double scaled = value / tick;
423 return (upward ? std::ceil(scaled - 1e-9) : std::floor(scaled + 1e-9)) * tick;
424}
425
426// A same-side default MARKET add as TradingView judges it when it is placed:
427// the held units and its own, at the signal close's tick, against the
428// placement equity (lane W5-ENG-MARGIN-V6; entry() and
429// apply_open_market_admission), with no lot of slack: an add whose shortfall
430// is under one lot's notional is dropped like any other (lab tv tapes
431// tests/fixtures/margin_residual w5b-pa-f-market-p50 and -pooc-p50 on NYSE:F's
432// whole shares; lane W5B-ENG-MARGIN-RESIDUAL).
433bool add_margin_exceeds_equity(double held_units, const PineSizingSnapshot& sizing,
434 double margin_pct, double mintick, double pointvalue) noexcept {
435 const double signal = nearest_tick(sizing.mark, mintick);
436 const double unit_margin = signal * pointvalue * sizing.fx * margin_pct / 100.0;
437 const double required = (held_units + sizing.frozen_units) * unit_margin;
438 const double slack = std::max(1e-9, std::abs(sizing.equity) * 1e-12);
439 return margin_pct > 0.0 && std::isfinite(required) && std::isfinite(sizing.equity)
440 && required > sizing.equity + slack;
441}
442
443int price_grid_decimals(double tick) noexcept {
444 if (!finite_positive(tick)) return -1;
445 double scaled = tick;
446 for (int digits = 0; digits <= 10; ++digits) {
447 const double k = std::floor(scaled + 0.5);
448 if (k >= 1.0 && std::abs(scaled - k) <= 1e-6 * k) return digits;
449 scaled *= 10.0;
450 }
451 return -1;
452}
453
454double source_level_on_price_grid(double level, double tick,
455 const char* field = nullptr) {
456 if (!std::isfinite(level)) return level;
457 const int digits = price_grid_decimals(tick);
458 if (digits < 0) return level;
459 double price_scale = 1.0;
460 for (int i = 0; i < digits; ++i) price_scale *= 10.0;
461 const double point_size = 1.0 / price_scale;
462 const double points = field
463 ? source_arith_value(checked_div(level, point_size), field)
464 : level / point_size;
465 const double grid = std::floor(points + 0.5);
466 if (std::abs(points - grid) <= 0.01 / price_scale)
467 return grid / price_scale;
468 return level;
469}
470
471double source_trigger_threshold(double level, double tick,
472 bool is_buy, bool is_limit,
473 const char* field = nullptr) {
474 if (!std::isfinite(level) || !finite_positive(tick)) return level;
475 // ab9714be pine_strategy_commands.cpp:533-537: 0.0 is a valid stop level.
476 // The native matcher's finite_non_negative trigger validator refuses negative
477 // levels, so a non-positive trigger cannot be shifted half a tick lower.
478 if (level <= 0.0) return 0.0;
479 const bool upward = is_limit ? !is_buy : is_buy;
480 double scaled = field ? source_arith_value(checked_div(level, tick), field) : level / tick;
481 const double inverse = 1.0 / tick;
482 const double integral_inverse = std::floor(inverse + 0.5);
483 if (integral_inverse > 0.0
484 && std::abs(inverse - integral_inverse) <= 1e-6 * integral_inverse) {
485 scaled = field ? source_arith_value(checked_mul(level, integral_inverse), field)
486 : level * integral_inverse;
487 const double nearest_index = std::floor(scaled + 0.5);
488 if (source_bar_fill_tick(level, tick) == level)
489 scaled = nearest_index;
490 }
491 const double target_index = upward
492 ? std::ceil(scaled - 1e-12)
493 : std::floor(scaled + 1e-12);
494 const double grid = field ? source_arith_value(checked_mul(target_index, tick), field)
495 : target_index * tick;
496 double threshold = field
497 ? source_arith_value(checked_add(grid, (upward ? -0.5 : 0.5) * tick), field)
498 : grid + (upward ? -0.5 : 0.5) * tick;
499 // Materialize the exact binary64 boundary using the same half-up broker
500 // projection as the legacy trigger bar. Decimal half ticks can land one
501 // or two ULPs to either side depending on the literal and multiplication
502 // order (11.805 and 13.775 are the two pinned opposite cases).
503 const auto reaches_target = [&](double price) {
504 const double rounded_index = field
505 ? std::floor(source_arith_value(checked_div(price, tick), field) + 0.5)
506 : std::floor(price / tick + 0.5);
507 return upward ? rounded_index >= target_index
508 : rounded_index <= target_index;
509 };
510 for (int i = 0; i < 16 && !reaches_target(threshold); ++i) {
511 threshold = std::nextafter(threshold, upward
512 ? std::numeric_limits<double>::infinity()
513 : -std::numeric_limits<double>::infinity());
514 }
515 // Walk to the outermost representable value which still projects to the
516 // target grid. This handles literal half ticks whose binary value and the
517 // multiply-built boundary lie one ULP apart.
518 for (int i = 0; i < 16; ++i) {
519 const double candidate = std::nextafter(threshold, upward
520 ? -std::numeric_limits<double>::infinity()
521 : std::numeric_limits<double>::infinity());
522 if (!reaches_target(candidate)) break;
523 threshold = candidate;
524 }
525 // An infinite outside candidate can be rejected by the walk. Only an
526 // installed boundary derived from this finite raw level is an overflow.
527 if (field && !std::isfinite(threshold))
529 return threshold;
530}
531
532// A trailing exit's activation is reached on the tick-quantized path whatever
533// its trailing offset: the one-shot's (test_trail_activation_tick_bar) and a
534// trailing leg's arm alike. A measured TradingView run on NYSE:F 15m with
535// trail_offset 1 (tests/fixtures/offset_trail_arm): synthetic lows 25.041 /
536// 26.041 and highs 25.049 / 26.049 arm the activations their ticks
537// equal, 6 of 6 exits on that bar. The generic Trail compares arm_price with
538// the raw path, so the arm is the half-tick boundary where the quantized path
539// first reaches the activation, the threshold a one-shot leg rests at; a
540// sub-tick activation keeps its own level (the ETH tapes: never rounded onto
541// the fill).
542double source_trail_arm_level(double activation, double tick, bool exit_is_buy) noexcept {
543 return source_trigger_threshold(activation, tick, exit_is_buy, true);
544}
545
546int legacy_volume_weighted_max_samples(int samples) noexcept {
547 constexpr int kMaxSamples = 1 << 20;
548 const int nonnegative = std::max(samples, 0);
549 const int scaled = nonnegative > kMaxSamples / 4
550 ? kMaxSamples : nonnegative * 4;
551 return std::max(scaled, 8);
552}
553
554// The price a one-shot trail books when its activation is first reached
555// inside a bar's path: the tick the quantized path reaches there. The one-shot
556// rests as a generic Limit at that tick's half-tick boundary
557// (one_shot_trail_trigger, source_trigger_threshold), so the tick is the
558// resting level rounded AWAY from the position (a sell up, a buy down), a
559// price the limit admits by construction. A measured TradingView run on
560// BINANCE:ETHUSDT.P 15m (tests/fixtures/trail_activation_tick_reach): a long
561// filled at synthetic 25.00 with trail_price 25.004 rests at 25.005 and books 25.01
562// (5 of 5 exits); a synthetic short filled at 35.00 with 34.996 rests at 34.995 and
563// books 34.99 (4 of 4); the stop-style snap toward the position (25.00,
564// 35.00) is refused as InvalidTerms. On the grid it is the activation.
565double source_one_shot_reach_tick(double resting_level, double tick, bool exit_is_buy) noexcept {
566 return directional_tick(resting_level, tick, !exit_is_buy);
567}
568
569// Whether a print has reached a trail's activation: on the tick-quantized
570// path, like every print the arm is tested on (source_trail_arm_level), the
571// placement print included. A measured TradingView run on NYSE:F 15m
572// (tests/fixtures/trail_activation_tick_reach): a placement close half a tick
573// short of the activation whose tick IS the activation (synthetic 25.005 under 25.01,
574// 35.035 over a short's 35.03) has reached it; 8 of 8 trades exit on the next
575// bar, trail_offset 1 and 0 alike. On an on-grid feed no print lies in the
576// half cell, so it admits what the raw compare did, save a print on the
577// activation's own grid point that the raw compare missed by a binary64 ULP.
578bool source_trail_reached_at(double price, double activation, double tick,
579 bool exit_is_buy) noexcept {
580 const double arm = source_trail_arm_level(activation, tick, exit_is_buy);
581 return exit_is_buy ? price <= arm : price >= arm;
582}
583
584double floor_quantity_grid(double units, const std::optional<double>& grid) noexcept {
585 if (!std::isfinite(units) || units <= 0.0) return 0.0;
586 if (!grid || !std::isfinite(*grid) || *grid <= 0.0) return units;
587 const double floored = std::floor(units / *grid + 1e-6) * *grid;
588 // Preserve the caller's exact representation when quantization is a
589 // no-op. Reconstructing an already-on-grid value can move it by one ULP
590 // (ab9714be:engine.hpp:1547-1570).
591 return floored < units ? floored : units;
592}
593
594double source_fifo_close_units(double units, const std::vector<NativeOpenLot>& lots) noexcept {
595 if (!std::isfinite(units) || units <= 0.0) return units;
596 const double lower = units - internal::kQtyEpsilon;
597 const double upper = units + internal::kQtyEpsilon;
598 double prefix = 0.0;
599 for (const auto& lot : lots) {
600 prefix += std::abs(lot.signed_units);
601 if (prefix >= lower && prefix <= upper) return prefix;
602 if (prefix > upper) break;
603 }
604 return units;
605}
606
607bool source_fifo_close_needs_resolution(double units,
608 const std::vector<PyramidEntry>& lots,
609 const std::vector<double>& preceding_closes) {
610 if (!std::isfinite(units) || units <= 0.0) return false;
611 std::vector<double> remaining;
612 remaining.reserve(lots.size());
613 for (const auto& lot : lots) remaining.push_back(lot.qty);
614 const auto ambiguous_prefix = [&] {
615 double prefix = 0.0;
616 for (const double quantity : remaining) {
617 prefix += quantity;
618 if (prefix == units) return false;
619 if (std::abs(prefix - units) <= internal::kQtyEpsilon)
620 return true;
621 if (prefix > units + internal::kQtyEpsilon) break;
622 }
623 return false;
624 };
625 if (ambiguous_prefix()) return true;
626 for (const double quantity : preceding_closes) {
627 if (!std::isfinite(quantity) || quantity <= 0.0) continue;
628 double closed = 0.0;
629 double left = quantity;
630 for (auto& lot : remaining) {
631 if (!(left > internal::kQtyEpsilon)) break;
632 const double amount = std::min(lot, left);
633 lot -= amount;
634 closed += amount;
635 left = quantity - closed;
636 }
637 remaining.erase(std::remove_if(remaining.begin(), remaining.end(), [](double lot) {
638 return lot <= internal::kQtyEpsilon;
639 }), remaining.end());
640 if (ambiguous_prefix()) return true;
641 }
642 return false;
643}
644
645double source_money_round(double value) noexcept {
646 if (!std::isfinite(value) || value == 0.0) return value;
647 const double magnitude = std::floor(std::log10(std::abs(value)));
648 const double scale = std::pow(10.0, 9.0 - magnitude);
649 const double rounded = std::floor(std::abs(value) * scale + 0.5) / scale;
650 return value < 0.0 ? -rounded : rounded;
651}
652
653// TV's market band is inclusive against its tick-index product, not a strict
654// decimal-price test: band-{reversal,flat}-equal admits 3865.5, whereas
655// fractional-fill-band and band-flat-residue refuse the 3386.68 tick product
656// (tests/fixtures/source_buying_power). Rebuild that operand from the grid
657// index rather than allowing an incidental resolved-price ULP to decide it.
658bool source_price_band_affordable(double equity, double units,
659 double fill_price, double tick) noexcept {
660 if (!finite_positive(units) || !finite_positive(tick)) return false;
661 const double affordable = source_money_round(source_money_round(equity) / units);
662 return affordable >= nearest_tick(fill_price, tick);
663}
664
665// The power of ten `grid` is, as its exponent, or nullopt. The nineteen
666// powers are formed once, by the same expressions, rather than at every
667// call: the lot-grid scopes ask several times a bar.
668std::optional<int> decimal_grid_exponent(double grid) noexcept {
669 static const std::array<double, 19> powers = [] {
670 std::array<double, 19> out{};
671 for (int exponent = -12; exponent <= 6; ++exponent) {
672 out[static_cast<std::size_t>(exponent + 12)] = exponent >= 0
673 ? std::pow(10.0, exponent) : 1.0 / std::pow(10.0, -exponent);
674 }
675 return out;
676 }();
677 for (int exponent = -12; exponent <= 6; ++exponent) {
678 if (powers[static_cast<std::size_t>(exponent + 12)] == grid) return exponent;
679 }
680 return std::nullopt;
681}
682
683// Margin rule 1: TradingView floors the SHORTEST round-trip decimal of the
684// binary64 quotient to the lot, not the binary quotient, and keeps the
685// nearest double of that decimal. Synthetic ETH quotient 0.0003 keeps 0.0003
686// on a 0.0001 grid where a binary floor can take 0.0002; a quotient of
687// 0.0002999999999999999 stays below 0.0003 and floors to 0.0002 (tests/fixtures/
688// margin_call_rules sizing and boundary tapes). Nullopt off a power-of-ten
689// lot grid, where the decimal floor is not pinned.
690std::optional<double> decimal_floor_lot(double units, double grid) noexcept {
691 const auto exponent = decimal_grid_exponent(grid);
692 if (!exponent || !std::isfinite(units) || units <= 0.0) return std::nullopt;
693 char text[40];
694 int digits = 1;
695 for (; digits <= 17; ++digits) {
696 std::snprintf(text, sizeof text, "%.*e", digits - 1, units);
697 if (std::strtod(text, nullptr) == units) break;
698 }
699 if (digits > 17) return std::nullopt;
700 // text = d.ddd...e+XX: the integer of its digits and the power it scales.
701 std::uint64_t mantissa = 0;
702 const char* cursor = text;
703 for (; *cursor != 'e' && *cursor != '\0'; ++cursor) {
704 if (*cursor >= '0' && *cursor <= '9')
705 mantissa = mantissa * 10 + static_cast<std::uint64_t>(*cursor - '0');
706 }
707 if (*cursor != 'e') return std::nullopt;
708 const int shift = std::atoi(cursor + 1) - (digits - 1) - *exponent;
709 std::uint64_t lots = mantissa;
710 if (shift < 0) {
711 for (int i = 0; i < -shift && lots > 0; ++i) lots /= 10;
712 } else {
713 for (int i = 0; i < shift; ++i) {
714 if (lots > std::numeric_limits<std::uint64_t>::max() / 10) return std::nullopt;
715 lots *= 10;
716 }
717 }
718 std::snprintf(text, sizeof text, "%llue%d", static_cast<unsigned long long>(lots), *exponent);
719 return std::strtod(text, nullptr);
720}
721
722// An explicit strategy.entry quantity on the lot grid. floor_quantity_grid's
723// 1e-6 tolerance lifts a quotient just under a grid point to that point, finds
724// it above the request and hands the raw, off-grid request back, which then
725// never fills. TradingView floors the shortest decimal instead and never
726// rounds up (a risk-sized (E/100)/0.3200000000000003 = 3124.9999999999973
727// trades 3124, 1562 + 1562 after a 50% leg; literal 3124.9999995 trades 3124,
728// 3125.0000000000005 trades 3125; tests/fixtures/explicit_qty_floor). Only that
729// snap changes: an on-grid request, a floor below the request and a non-decimal
730// grid keep the grid floor's result. No tape covers strategy.order's explicit
731// quantity (order()) or a lot that is not a power of ten (0.5, 0.25): both keep
732// the grid floor, snapped request included, as before.
733double explicit_entry_quantity_floor(double units, const std::optional<double>& grid) noexcept {
734 const double floored = floor_quantity_grid(units, grid);
735 if (!detail::script_rule_switches().explicit_qty_decimal_floor || floored != units
736 || !grid || !std::isfinite(*grid) || *grid <= 0.0 || !std::isfinite(units)
737 || units <= 0.0) {
738 return floored;
739 }
740 if (!(std::floor(units / *grid + 1e-6) > std::floor(units / *grid))) return floored;
741 const auto decimal = decimal_floor_lot(units, *grid);
742 return decimal && *decimal < units ? *decimal : floored;
743}
744
745double source_money_floor_lot(double units, const std::optional<double>& grid) noexcept {
746 if (!grid || !std::isfinite(*grid) || *grid <= 0.0) return units;
747 if (!std::isfinite(units) || units <= 0.0) return units;
748 double floored = std::floor(units / *grid) * *grid;
749 if (*grid == 0.01) {
750 const double cent_candidate = std::floor(units * 100.0) * *grid;
751 if (cent_candidate > floored && cent_candidate <= units) floored = cent_candidate;
752 }
753 return floored < units ? floored : units;
754}
755
756NativeFeeKind fee_kind_for(int commission_type) noexcept {
757 switch (static_cast<CommissionType>(commission_type)) {
758 case CommissionType::CASH_PER_CONTRACT: return NativeFeeKind::CashPerUnit;
759 case CommissionType::CASH_PER_ORDER: return NativeFeeKind::CashPerExecution;
761 default: return NativeFeeKind::Percent;
762 }
763}
764
765NativeOpenDirections directions_for(int direction) noexcept {
766 if (direction > 0) return NativeOpenDirections::Long;
767 if (direction < 0) return NativeOpenDirections::Short;
768 return NativeOpenDirections::Both;
769}
770
771// R4-D L10z review fix 1: one throttled re-arm per refused source identity.
772// A throttled opening can be refused at two driver points within the same bar;
773// the bar-close re-arm must resubmit it exactly once, so the queue is deduped
774// on the placement identity (source id + source sequence) of the refused row.
775bool throttled_rearm_already_queued(
776 const std::vector<PlacementSnapshot>& queue,
777 const PlacementSnapshot& source) noexcept {
778 for (const auto& queued : queue) {
779 if (queued.source_id == source.source_id
780 && queued.source_sequence == source.source_sequence) {
781 return true;
782 }
783 }
784 return false;
785}
786
787// An exit's place in TradingView's exit queue (lane W3-ENG-EXIT-ALLOC, F05).
788// TradingView reserves an entry's quantity for its exits in the order they
789// were created: re-issuing a strategy.exit modifies that order in place, it
790// does not queue it again, and a later-created exit gets only what the
791// earlier ones leave. Here a re-issue is a replacement chain, whose first
792// row's source_sequence remember() keeps as chain_origin_sequence. A leg
793// staged before remember() names its predecessor (still in the table while
794// its retirement is pending) or carries the source_sequence it retained; one
795// with neither is new, at the back of the queue. A call TradingView voided (its
796// entry neither existed nor rested) holds no place: a re-issue made once the
797// entry exists queues from that re-issue (lab tv w3f05-s12).
798std::uint64_t exit_queue_rank(const PlacementTable& table,
799 const PlacementSnapshot& row) noexcept {
800 if (row.chain_origin_sequence != 0) return row.chain_origin_sequence;
801 if (row.projection_predecessor != 0 && table.contains(row.projection_predecessor)) {
802 const auto& predecessor = table.at(row.projection_predecessor);
803 const std::uint64_t origin = predecessor.chain_origin_sequence;
804 if (origin != 0 && (!predecessor.void_issue || row.void_issue)) return origin;
805 }
806 if (row.source_sequence != 0) return row.source_sequence;
807 return std::numeric_limits<std::uint64_t>::max();
808}
809
810
811// True when the placement row of some live handle satisfies `row`: the
812// quiet-bar gates' scan (pine_quiet_bar.hpp). It reads and allocates nothing
813// beyond the rows it visits.
814template <class Row>
815bool any_live_row(const std::vector<native_order::RequestHandle>& live,
816 const PlacementTable& placement, Row&& row) {
817 for (const auto& handle : live) {
818 const auto found = placement.find(handle.incarnation);
819 if (found != placement.end() && row(found->second)) return true;
820 }
821 return false;
822}
823
824} // namespace
825
826namespace detail {
827namespace {
828// The callback-lifecycle switches: process-wide like carry_bindings, and for the same reason.
829std::atomic<bool> callback_lifecycle_rules[kPineCallbackLifecycleRuleCount] = {true, true, true, true, true};
830} // namespace
832 const int index = static_cast<int>(rule);
833 if (index >= 0 && index < kPineCallbackLifecycleRuleCount)
834 callback_lifecycle_rules[index].store(on, std::memory_order_relaxed);
835}
837 const int index = static_cast<int>(rule);
838 return index < 0 || index >= kPineCallbackLifecycleRuleCount
839 || callback_lifecycle_rules[index].load(std::memory_order_relaxed);
840}
842 static MarginRuleSwitches switches;
843 return switches;
844}
845
847 static MarginScheduleSwitches switches;
848 return switches;
849}
850
852 static MarginOpeningSwitches switches;
853 return switches;
854}
855} // namespace detail
856
857namespace {
858// trail_points as whole ticks (compat::pine::trail_points_to_ticks): with
859// the mintick-scaled tolerance TradingView applies, or the constant one when
860// the trail switch is off.
861double source_trail_points_ticks(double trail_points, double mintick,
862 const char* field = nullptr) {
863 if (field && finite_positive(mintick)
865 const double tolerance = source_arith_value(checked_mul(
867 return std::ceil(source_arith_value(checked_add(trail_points, -tolerance), field));
868 }
870 ? compat::pine::trail_points_to_ticks(trail_points, mintick)
871 : compat::pine::trail_points_to_ticks(trail_points);
872}
873} // namespace
874
875#if PINEFORGE_PINE_QUIET_BAR_PROBE
876namespace detail {
877QuietBarCounts& quiet_bar_counts() noexcept {
878 static QuietBarCounts counts;
879 return counts;
880}
881} // namespace detail
882#endif
883
884namespace detail {
886 static ScriptRuleSwitches switches;
887 return switches;
888}
890 static ExitBindingRuleSwitches switches;
891 return switches;
892}
894 static PoocCloseFillViewSwitches switches;
895 return switches;
896}
897} // namespace detail
898
899// The retired-row compaction: the placement table's storage.
900void PlacementTable::place(std::uint64_t incarnation, std::unique_ptr<PlacementSnapshot> node) { if (old_only_) return place_old(incarnation, std::move(node));
901 if (window_rows_ == 0 && (old_.empty() || incarnation > old_.back().first)) {
902 // An empty window restarts at the new row, above every older row.
903 window_.clear();
904 head_ = 0;
905 base_ = incarnation;
906 }
907 if (incarnation < base_) {
908 // Below the window (a request the core accepted before the rows the
909 // window starts at, remembered only now): among the older rows.
910 const auto at = std::lower_bound(old_.begin(), old_.end(), incarnation,
911 [](const auto& entry, std::uint64_t key) { return entry.first < key; });
912 placement_work_ += static_cast<std::uint64_t>(old_.end() - at) + 1U; old_.emplace(at, incarnation, std::move(node));
913 return;
914 }
915 const auto index = head_ + static_cast<std::size_t>(incarnation - base_);
916 if (index >= window_.size()) grow_window(index);
917 ++placement_work_; window_[index] = std::move(node);
918 ++window_rows_;
919}
920
921std::unique_ptr<PlacementSnapshot> PlacementTable::take(std::uint64_t incarnation) noexcept {
922 if (!old_only_ && incarnation >= base_) {
923 const std::uint64_t offset = incarnation - base_;
924 if (offset >= window_.size() - head_) return nullptr;
925 auto& slot = window_[head_ + static_cast<std::size_t>(offset)];
926 if (slot) { --window_rows_; ++removal_work_; }
927 return std::move(slot);
928 }
929 const auto found = std::lower_bound(old_.begin(), old_.end(), incarnation,
930 [this](const auto& entry, std::uint64_t key) { ++lookup_work_; return entry.first < key; });
931 if (found == old_.end() || found->first != incarnation) return nullptr;
932 auto node = std::move(found->second);
933 removal_work_ += static_cast<std::uint64_t>(old_.end() - found); old_.erase(found);
934 return node;
935}
936
937void PlacementTable::remember_erased(const PlacementSnapshot& row) {
938 if (row.family != PineOrderFamily::Margin && erased_foreign_ids_.size() < 3
939 && std::find(erased_foreign_ids_.begin(), erased_foreign_ids_.end(), row.source_id)
940 == erased_foreign_ids_.end()) {
941 erased_foreign_ids_.push_back(row.source_id);
942 }
943 const auto family = static_cast<std::uint64_t>(row.family);
944 erased_digest_ = fnv_append(erased_digest_, &family, sizeof family);
945 const std::uint64_t id_size = row.source_id.size();
946 erased_digest_ = fnv_append(erased_digest_, &id_size, sizeof id_size);
947 erased_digest_ = fnv_append(erased_digest_, row.source_id.data(), row.source_id.size());
948 const std::uint64_t from_size = row.from_entry.size();
949 erased_digest_ = fnv_append(erased_digest_, &from_size, sizeof from_size);
950 erased_digest_ = fnv_append(erased_digest_, row.from_entry.data(), row.from_entry.size());
951 erased_digest_ = fnv_append(erased_digest_, &row.command_sequence,
952 sizeof row.command_sequence);
953}
954
955void PlacementTable::compact() { if (compact_old()) return;
956 constexpr std::size_t kDenseFloor = 64;
957 for (;;) {
958 while (head_ < window_.size() && !window_[head_]) {
959 ++head_;
960 ++base_;
961 }
962 if (head_ == window_.size()) {
963 window_.clear();
964 head_ = 0;
965 base_ = high_water_ + 1U;
966 return;
967 }
968 if (head_ >= kDenseFloor && head_ >= window_.size() / 2) {
969 window_.erase(window_.begin(), window_.begin() + static_cast<std::ptrdiff_t>(head_));
970 head_ = 0;
971 }
972 const std::size_t span = window_.size() - head_;
973 if (span <= kDenseFloor || span <= 4U * window_rows_) return;
974 // Sparse: the older half's rows go below the window. They are above
975 // every older row already there, so old_ stays ascending.
976 const std::size_t half = head_ + span / 2U;
977 for (std::size_t index = head_; index < half; ++index) {
978 if (!window_[index]) continue;
979 old_.emplace_back(base_ + static_cast<std::uint64_t>(index - head_),
980 std::move(window_[index]));
981 --window_rows_;
982 }
983 }
984}
985
986std::uint64_t PlacementTable::command_sequence_for(const std::string& source_id,
987 const std::string& from_entry) const {
988 constexpr auto absent = std::numeric_limits<std::uint64_t>::max();
989 bool known = false;
990 std::uint64_t any = absent;
991 bool paired_known = false;
992 std::uint64_t paired = absent;
993 const auto retained = retained_sequences(source_id, from_entry);
994 ++sequence_queries_;
995 any = retained[0];
996 paired = retained[1];
997 known = any != absent;
998 paired_known = paired != absent;
999 // The backed index contains only current retained identities. Replacement
1000 // removes its predecessor rather than folding it into erased history.
1001 // Single and exact-pair domains remain distinct, including empty pairs.
1002 // Both minima still combine with the original erased evidence below.
1003 if (const auto minima = erased_minima_.lookup(source_id)) {
1004 known = true;
1005 any = std::min(any, *minima);
1006 if (const auto pair = erased_minima_.lookup(source_id, from_entry)) {
1007 paired_known = true;
1008 paired = std::min(paired, *pair);
1009 }
1010 }
1011 if (!known) return absent;
1012 if (paired_known && paired != absent) return paired;
1013 return any;
1014}
1015
1016namespace detail {
1017namespace {
1018// The tombstone audit's state: every erased row, per table, and every read
1019// that reached one, per source line. One process-wide instance; the Pine
1020// adapter is single-threaded per host.
1021struct PlacementAudit {
1022 std::map<const PlacementTable*,
1023 std::map<std::uint64_t, std::unique_ptr<PlacementSnapshot>>> tombstones;
1024 std::map<std::pair<int, unsigned>, std::uint64_t> hits;
1025 std::vector<std::pair<std::uint64_t, const PlacementSnapshot*>> view;
1026 ~PlacementAudit() {
1027 const char* path = std::getenv("PINEFORGE_PLACEMENT_AUDIT_LOG");
1028 std::FILE* out = path ? std::fopen(path, "a") : nullptr;
1029 std::uint64_t rows = 0;
1030 for (const auto& table : tombstones) rows += table.second.size();
1031 std::fprintf(out ? out : stderr,
1032 "placement-audit: %zu sites hit, %llu tombstones retained at exit\n",
1033 hits.size(), static_cast<unsigned long long>(rows));
1034 for (const auto& hit : hits) {
1035 std::fprintf(out ? out : stderr, "placement-audit: %s pine_adapter line %u: %llu\n",
1036 hit.first.first == 0 ? "lookup" : "scan", hit.first.second,
1037 static_cast<unsigned long long>(hit.second));
1038 }
1039 if (out) std::fclose(out);
1040 }
1041};
1042PlacementAudit& placement_audit() {
1043 static PlacementAudit audit;
1044 return audit;
1045}
1046} // namespace
1047
1048void placement_audit_erase(const PlacementTable* table, std::uint64_t incarnation,
1049 PlacementSnapshot&& row) {
1050 placement_audit().tombstones[table][incarnation] =
1051 std::make_unique<PlacementSnapshot>(std::move(row));
1052}
1053
1054void placement_audit_forget(const PlacementTable* table) noexcept {
1055 placement_audit().tombstones.erase(table);
1056}
1057
1058void placement_audit_miss(const PlacementTable* table, std::uint64_t incarnation,
1059 unsigned line) noexcept {
1060 auto& audit = placement_audit();
1061 const auto stones = audit.tombstones.find(table);
1062 if (stones == audit.tombstones.end() || stones->second.count(incarnation) == 0) return;
1063 ++audit.hits[{0, line}];
1064}
1065
1066void placement_audit_scan_hit(const PlacementTable*, std::uint64_t, unsigned line) noexcept {
1067 ++placement_audit().hits[{1, line}];
1068}
1069
1070const std::vector<std::pair<std::uint64_t, const PlacementSnapshot*>>*
1072 auto& audit = placement_audit();
1073 const auto stones = audit.tombstones.find(table);
1074 if (stones == audit.tombstones.end()) return nullptr;
1075 audit.view.clear();
1076 for (const auto& stone : stones->second) audit.view.emplace_back(stone.first, stone.second.get());
1077 return &audit.view;
1078}
1079} // namespace detail
1080
1081namespace {
1082// A whole-table scan's audit: the scan reads only the retained rows, and in a
1083// PINEFORGE_PLACEMENT_AUDIT build every erased row its predicate would also
1084// have accepted is recorded against the scan's line.
1085#if PINEFORGE_PLACEMENT_AUDIT
1086template <class Predicate>
1087void audit_placement_scan(const PlacementTable& table, unsigned line, Predicate&& predicate) {
1088 const auto* stones = detail::placement_audit_tombstones(&table);
1089 if (!stones) return;
1090 const auto copy = *stones;
1091 for (const auto& stone : copy) {
1092 if (predicate(stone.first, *stone.second))
1093 detail::placement_audit_scan_hit(&table, stone.first, line);
1094 }
1095}
1096#define PINEFORGE_AUDIT_PLACEMENT_SCAN(table, ...) \
1097 audit_placement_scan((table), __LINE__, __VA_ARGS__)
1098#else
1099#define PINEFORGE_AUDIT_PLACEMENT_SCAN(table, ...) ((void)0)
1100#endif
1101} // namespace
1102
1104 : cap(attachment) {
1105 pending_view_.owner_ = this;
1106 cohorts_by_id_.set_store(::pineforge::detail::ScratchStore::cohort);
1107 exit_rank_keys_.set_store(::pineforge::detail::ScratchStore::exit_rank);
1108}
1109
1115
1116namespace detail {
1117// Share the production attached-arena/disk selection and per-run I/O policy.
1118struct CommandScratchRun {
1119 // Fixed geometry, independent of bars, handles and text widths.
1120 static constexpr std::size_t page_size = 512;
1121 std::unique_ptr<pineforge::detail::ScratchBacking> backing;
1122 CommandScratchStore store;
1123
1124 CommandScratchRun(ScratchKeyOrder order, CacheCapacity capacity)
1125 : backing(make_command_scratch_backing(page_size, capacity)),
1126 store{{2, page_size, 1, 1, order,
1128 exit_scratch_check(store.status(), store);
1129 }
1130};
1131
1132namespace {
1133struct ExitScratchRun {
1134 CommandScratchRun scratch;
1135 ExitScratchContext context;
1136 ExitScratchVersionsProduction versions{scratch.store};
1137
1138 ExitScratchRun(const native_order::RunIdentity& run, CacheCapacity capacity)
1139 : scratch(exit_scratch_key_order(), capacity), context{scratch.store, run} {
1140 context.versions = &versions;
1141 }
1142};
1143
1144std::shared_ptr<ExitScratchContext> make_exit_scratch(
1145 const native_order::RunIdentity& identity, CacheCapacity capacity) {
1146 auto run = std::make_shared<ExitScratchRun>(identity, capacity);
1147 return {run, &run->context};
1148}
1149} // namespace
1150
1152 if (!adapter.exit_scratch_) exit_scratch_failure(ScratchStatus::programming_error);
1153 return *adapter.exit_scratch_;
1154}
1156 : coded<std::logic_error>(
1159 {},
1160 "exit scratch operation failed"),
1161 status_(status) {}
1162
1163[[noreturn]] void exit_scratch_failure(ScratchStatus status) {
1164 throw ExitScratchError(status);
1165}
1166} // namespace detail
1167
1169 detail::exit_scratch_check(context.store.status(), context.store);
1170 if (context.store.transaction_active())
1172 if (exit_scratch_.get() == &context) return;
1173 exit_scratch_ = std::shared_ptr<detail::ExitScratchContext>(
1174 &context, [](detail::ExitScratchContext*) {});
1175}
1176
1178 if (position_tail_summary_)
1179 position_tail_summary_->invalidate(PositionTailGuard::Rebind,
1181 host_ = &host;
1182 exit_scratch_ = nullptr;
1183 pine_view_host_ = nullptr;
1184 const auto* pine_host = pine_view_of(host_);
1185 input_scheduler_ = pine_host ? &pine_host->scheduler_ : nullptr;
1186}
1187
1188// The bound host's PineStrategyHost view: dynamic_cast's answer, taken once
1189// per host instead of at every call site (several a bar), and again at every
1190// bind and run begin. The handle-preserving reissue change: an adapter member; it was a thread_local
1191// memo (keyed by the host, its object and its dynamic type), and a strategy
1192// module is dlopen'd, where every thread_local read is a dynamic TLS lookup.
1193// The adapter is its host's own member, bound to it at construction, and a
1194// host's dynamic type is fixed once the adapter can be asked.
1195PineStrategyHost* PineExecutionAdapter::pine_view_of(NativeStrategyHost* host) const noexcept {
1196 if (host != pine_view_host_) {
1197 pine_view_host_ = host;
1198 pine_view_ = host ? dynamic_cast<PineStrategyHost*>(host) : nullptr;
1199 }
1200 return pine_view_;
1201}
1202
1204 const NativeDecisionContext& context) const noexcept {
1205 // Pine's pending projection historically keyed this source-side field to
1206 // the input slot the kernel presented (including magnifier sub-bars).
1207 // Keep that policy on the named input coordinate while the kernel's public
1208 // interval_index remains script-bar space.
1209 return context.coordinate.input_interval_index;
1210}
1211
1213 const NativeCoordinate& coordinate) const noexcept {
1214 NativeDecisionContext context;
1215 context.coordinate = coordinate;
1216 context.script_bar_open_ms = last_broker_open_ms_;
1217 return projection_bar_index(context);
1218}
1219
1220namespace {
1221[[noreturn]] void refuse_unbound_adapter() {
1223 "Pine execution adapter is not bound to a native host");
1224}
1225} // namespace
1226
1227// The refusal is out of line so this check is a test and a load wherever it
1228// inlines (the per-pump state cache: the adapter reads its host through it at every read).
1229NativeStrategyHost& PineExecutionAdapter::require_host() const {
1230 if (!host_) refuse_unbound_adapter();
1231 return *host_;
1232}
1233
1234OrderBirth PineExecutionAdapter::capture_order_birth() const {
1235 const auto point = detail::callback_point(require_host());
1236 if (!point) return OrderBirth::direct_command(-1, require_host().native_decision_floor());
1237 const int bar = point->decision.coordinate.interval_index;
1238 const std::int64_t timestamp = point->decision.sub_bar_open_ms;
1239 if (!coof_recalc_active_) return OrderBirth::chart_evaluation(bar, timestamp);
1240
1241 const bool magnified = point->decision.sub_count > 1;
1242 const auto domain = magnified ? BirthCursorDomain::MagnifierTicks
1244 const int count = magnified ? std::max(1, point->decision.sub_count) : 4;
1245 int index = magnified ? point->decision.sub_index : 0;
1246 if (!magnified) {
1247 const bool high_first = source_path_uses_high_first(coof_script_bar_);
1248 switch (point->decision.coordinate.path_phase) {
1249 case NativePathPhase::Open: index = 0; break;
1250 case NativePathPhase::High: index = high_first ? 1 : 2; break;
1251 case NativePathPhase::Low: index = high_first ? 2 : 1; break;
1252 case NativePathPhase::Close: index = 3; break;
1253 case NativePathPhase::None: index = 0; break;
1254 }
1255 }
1256 index = std::max(0, std::min(index, count - 1));
1257 const auto cursor = BirthCursor::point(domain, index, count);
1258 const std::uint64_t ordinal = std::max<std::uint64_t>(1, last_applied_ordinal_);
1259 return OrderBirth::fill_evaluation(bar, timestamp, cursor, point->price,
1260 ordinal, ordinal, ordinal);
1261}
1262
1263bool PineExecutionAdapter::opening_market_short_scope(PineOrderFamily family) const {
1264 const auto point = detail::callback_point(require_host());
1265 const auto opening_parent = placement_.find(coof_market_entry_recalc_incarnation_);
1266 const bool carried_opening_parent = point && opening_parent != placement_.end()
1267 && opening_parent->second.placement_script_open_ms < point->decision.script_bar_open_ms;
1268 const bool opening_high = coof_script_bar_valid_
1269 && source_decimal_tick(coof_script_bar_.open, staged_.syminfo.mintick)
1270 == source_decimal_tick(coof_script_bar_.high, staged_.syminfo.mintick);
1271 const auto native = detail::run_state(require_host());
1272 return family == PineOrderFamily::ExitStop && coof_recalc_active_
1273 && coof_market_entry_recalc_incarnation_ != 0 && config_.slippage == 0
1274 && (opening_high || (config_.margin_short == 0.0
1275 && (coof_first_open_ || carried_opening_parent)))
1276 && detail::run_position(require_host()).signed_units < 0.0
1277 && (!native.spec || native.spec->intrabar.is_none())
1278 && point && point->decision.coordinate.path_phase == NativePathPhase::Open;
1279}
1280
1281void PineExecutionAdapter::initialize_l4c_policy(PlacementSnapshot& snapshot,
1282 native_order::RequestHandle handle) {
1283 if (snapshot.birth.cause() == OrderBirthCause::Unattributed)
1284 snapshot.birth = capture_order_birth();
1285 const bool trailing = finite_positive(snapshot.exit_levels.trail_points)
1286 || finite_positive(snapshot.exit_levels.trail_price)
1287 || finite_positive(snapshot.exit_levels.trail_offset);
1288 snapshot.birth_reach = compat::pine::select_historical_birth_reach(snapshot.birth, trailing);
1289 if (coof_recalc_active_) {
1290 snapshot.coof_cascade_seg_i = coof_context_.coordinate.interval_index;
1291 snapshot.coof_cascade_inflight_fires = true;
1292 }
1293 if (handle.incarnation && placement_.contains(handle.incarnation)) invalidate_retirement();
1294 const bool exit = snapshot.family == PineOrderFamily::ExitLimit
1297 if (!exit) return;
1298
1299 const exit_legs::Prices prices{snapshot.exit_levels.limit, snapshot.exit_levels.stop,
1300 snapshot.exit_levels.trail_points, snapshot.exit_levels.trail_price,
1301 snapshot.exit_levels.trail_offset, snapshot.exit_levels.profit_ticks,
1302 snapshot.exit_levels.loss_ticks};
1303 if (!snapshot.legs.target().incarnation && handle.incarnation != 0) {
1304 snapshot.legs.set_prices(prices);
1305 snapshot.legs.attach(handle.incarnation, std::max<std::int64_t>(0, snapshot.placement_cycle));
1306 } else if (handle.incarnation != 0
1307 && snapshot.legs.target().incarnation != handle.incarnation) {
1308 snapshot.legs.fork(handle.incarnation, std::max<std::int64_t>(0, snapshot.placement_cycle));
1309 snapshot.legs.set_prices(prices);
1310 }
1311
1312 const auto physical = detail::run_position(require_host());
1313 if (physical.signed_units == 0.0 || snapshot.projection_created_bar < 0) return;
1314 const int direction = physical.signed_units > 0.0 ? 1 : -1;
1315 const auto point = detail::callback_point(require_host());
1316 const Bar activation_bar = coof_script_bar_valid_ ? coof_script_bar_
1317 : policy_script_bar_;
1318 const bool path_high_first = source_path_uses_high_first(activation_bar);
1319 int historical_point = 0;
1320 double waypoint = activation_bar.open;
1321 if (point) {
1322 switch (point->decision.coordinate.path_phase) {
1323 case NativePathPhase::Open:
1324 historical_point = 0; waypoint = activation_bar.open; break;
1325 case NativePathPhase::High:
1326 historical_point = path_high_first ? 1 : 2; waypoint = activation_bar.high; break;
1327 case NativePathPhase::Low:
1328 historical_point = path_high_first ? 2 : 1; waypoint = activation_bar.low; break;
1329 case NativePathPhase::Close:
1330 historical_point = 3; waypoint = activation_bar.close; break;
1331 case NativePathPhase::None:
1332 historical_point = 0; waypoint = point->price; break;
1333 }
1334 }
1335 const bool at_waypoint = point
1336 && source_same_point(point->price, waypoint, staged_.syminfo.mintick);
1337 const bool historical_segment = point && historical_point > 0 && !at_waypoint;
1338 const int recalc_leg = historical_segment
1339 ? std::max(0, historical_point - 1) : historical_point;
1340
1341 std::string_view first_lot_id;
1342 std::uint64_t first_lot_incarnation = 0;
1343 for (const auto& cohort : cohorts_by_id_.held_rows()) {
1344 for (const auto& opening : cohort.second.opened) {
1345 const auto units = cohort.second.live_units_by_origin.find(opening.incarnation);
1346 if (units == cohort.second.live_units_by_origin.end()
1347 || !(units->second > 0.0)) {
1348 continue;
1349 }
1350 first_lot_id = cohort.first;
1351 first_lot_incarnation = opening.incarnation;
1352 break;
1353 }
1354 if (first_lot_incarnation != 0) break;
1355 }
1356
1357 const double activation_quantity = std::isfinite(snapshot.projection_remaining_qty)
1358 ? snapshot.projection_remaining_qty : snapshot.requested_qty;
1359 const bool full_quantity = (!std::isfinite(snapshot.qty_percent)
1360 || snapshot.qty_percent >= 100.0 - 1e-9)
1361 && (!std::isfinite(activation_quantity)
1362 || activation_quantity >= std::abs(physical.signed_units) - 1e-9);
1363 compat::pine::ExitActivationContext context;
1364 context.cycle = current_position_cycle_;
1365 context.bar_index = snapshot.projection_created_bar;
1366 context.position_open_bar = position_open_bar_index_;
1367 context.direction = direction;
1368 context.cursor_price = point ? point->price : snapshot.sizing.price;
1369 context.fill_recalc = coof_recalc_active_;
1370 context.scheduler = config_.calc_on_order_fills;
1371 context.magnifier = point && point->decision.sub_count > 1;
1372 context.process_on_close = config_.process_orders_on_close;
1373 context.warmup = !modeled_input() && detail::run_state(require_host()).phase == NativeRunPhase::Warmup;
1374 context.stream_idle = modeled_input();
1375 context.first_open_fill = opening_market_short_scope(snapshot.family);
1376 context.after_first_open_fill = coof_recalc_active_ && !coof_first_open_
1377 && point && point->decision.coordinate.path_phase == NativePathPhase::Open;
1378 context.recalc_leg = recalc_leg;
1379 context.historical_segment = historical_segment;
1380 context.at_extreme = at_waypoint
1381 && (historical_point == 1 || historical_point == 2);
1382 context.historical_point = historical_point;
1383 context.current_fill = coof_current_fill_seq_;
1384 context.bar = activation_bar;
1385 context.position_entry_count = static_cast<int>(physical.lot_count);
1386 context.position_quantity = std::abs(physical.signed_units);
1387 context.pyramiding = config_.pyramiding;
1388 context.lot_count = physical.lot_count;
1389 context.first_lot_id = first_lot_id;
1390 context.first_lot_incarnation = first_lot_incarnation;
1391 context.market_recalc_incarnation = coof_market_entry_recalc_incarnation_;
1392 context.market_recalc_fill = coof_market_entry_recalc_fill_seq_;
1393 context.pending_empty = live_handles_.empty() && pending_entries_.empty()
1394 && pending_bracket_legs_.empty() && pending_same_bar_commands_.empty()
1395 && pending_coof_requests_.empty();
1396 context.slippage = config_.slippage;
1397 context.pointvalue = staged_.syminfo.pointvalue;
1398 context.account_fx = point ? active_staged_fx(point->decision.sub_bar_open_ms)
1399 : staged_.account_fx;
1400 context.fx_series_empty = staged_.account_fx_effective_from_ms.empty();
1401 context.bar_path_high_first = path_high_first;
1402 context.tick_high = source_bar_fill_tick(activation_bar.high, staged_.syminfo.mintick);
1403 compat::pine::ExitActivationRequest request;
1404 request.requested_trailing = trailing;
1405 request.full_quantity = full_quantity;
1406 request.from_fill = snapshot.birth.from_fill();
1407 request.has_from_entry = !snapshot.from_entry.empty();
1408 request.birth_reach = snapshot.birth_reach;
1409 request.from_entry = snapshot.from_entry;
1410 request.oca_name = snapshot.oca_name;
1411 request.quantity = activation_quantity;
1413 request, snapshot.exit_levels.stop, snapshot.exit_levels.limit, context);
1414 if (snapshot.exit_activation.evidence()) {
1415 snapshot.leg_activation.bind(snapshot.exit_activation.resolve(
1416 current_position_cycle_, position_open_bar_index_));
1417 }
1418
1419 // ab9714be reservation_expansion.cpp:7-27 and
1420 // pine_strategy_commands.cpp:1891-1924: only an ordinary same-bar MARKET
1421 // population can grow a global full POOC exit. The immutable source birth
1422 // and placement facts select that population; the native cohort remains
1423 // the execution authority.
1424 const bool full_global = config_.process_orders_on_close && snapshot.from_entry.empty()
1425 && !std::isfinite(snapshot.requested_qty)
1426 && (!std::isfinite(snapshot.qty_percent) || snapshot.qty_percent >= 100.0)
1427 && physical.signed_units != 0.0;
1428 const bool side = physical.signed_units > 0.0;
1429 const auto is_unpriced_market_add = [](const PlacementSnapshot& candidate) {
1430 return !finite_positive(candidate.exit_levels.limit)
1431 && !finite_positive(candidate.exit_levels.stop)
1432 && !finite_positive(candidate.exit_levels.trail_offset)
1433 && !finite_positive(candidate.exit_levels.trail_price);
1434 };
1435 const auto is_entry_like = [](const PlacementSnapshot& candidate) {
1436 return candidate.opening && (candidate.family == PineOrderFamily::Entry
1437 || candidate.family == PineOrderFamily::Order);
1438 };
1439 struct CandidateRef {
1440 PlacementSnapshot* snapshot = nullptr;
1441 std::uint64_t incarnation = 0;
1442 };
1443 std::vector<compat::pine::ReservationGrowthCandidate> candidates;
1444 std::vector<CandidateRef> candidate_refs;
1445 const auto collect = [&](PlacementSnapshot& candidate,
1446 std::uint64_t incarnation) {
1447 if (!is_entry_like(candidate)) return;
1448 compat::pine::ReservationGrowthCandidate fact;
1449 fact.incarnation = incarnation;
1450 fact.source_id = candidate.source_id;
1451 fact.market_entry = candidate.family == PineOrderFamily::Entry
1452 && is_unpriced_market_add(candidate);
1453 fact.from_fill = candidate.birth.from_fill();
1454 fact.at_entry_capacity = candidate.projection_over_pyramiding;
1455 fact.is_long = candidate.is_long;
1456 fact.created_position_side =
1457 static_cast<PositionSide>(candidate.projection_position_side);
1458 fact.created_bar = candidate.projection_created_bar;
1459 candidates.push_back(std::move(fact));
1460 candidate_refs.push_back({&candidate, incarnation});
1461 };
1462 // The candidates feed the reservation-growth selection alone, which only
1463 // a full global exit makes; otherwise nothing is selected, nothing is
1464 // admitted, and the references are never visited (the trigger-band and scratch reuse).
1465 if (full_global) {
1466 for (auto& pending : pending_entries_) collect(pending.snapshot, 0);
1467 for (auto& pending : pending_same_bar_commands_) collect(pending.snapshot, 0);
1468 for (const auto& live : live_handles_) {
1469 const auto existing = placement_.find(live.incarnation);
1470 if (existing != placement_.end()) collect(existing->second, live.incarnation);
1471 }
1472 }
1473 const double percent = std::isfinite(snapshot.qty_percent)
1474 ? snapshot.qty_percent : 100.0;
1475 const auto selected = full_global
1477 candidates, snapshot.from_entry, config_.process_orders_on_close,
1478 physical.signed_units == 0.0, percent,
1479 snapshot.projection_created_bar,
1481 : std::vector<std::uint64_t>{};
1482 const bool partial = percent < 100.0 - 1e-9
1483 || (std::isfinite(snapshot.projection_remaining_qty)
1484 && snapshot.projection_remaining_qty
1485 < std::abs(physical.signed_units) - 1e-9);
1486 const bool qualified_adds = compat::pine::admits_reservation_expansion(
1487 selected, partial, snapshot.projection_remaining_qty,
1488 std::abs(physical.signed_units));
1489 snapshot.pooc_global_full_exit_dynamic_qty = qualified_adds;
1490 snapshot.pooc_global_full_exit_tracks_bound_adds = qualified_adds;
1491 if (qualified_adds && handle.incarnation != 0) {
1492 try {
1493 snapshot.reservation_expansion.capture(handle.incarnation, current_position_cycle_,
1494 direction > 0 ? PositionSide::LONG
1496 std::abs(physical.signed_units));
1497 } catch (const std::invalid_argument&) {
1498 // A replacement carries its existing immutable capture.
1499 }
1500 for (const auto& selected_source : candidate_refs) {
1501 auto* candidate = selected_source.snapshot;
1502 if (!candidate) continue;
1503 candidate->reservation_growth_owner_incarnation = handle.incarnation; invalidate_retirement();
1504 if (selected_source.incarnation != 0
1505 && selected_source.incarnation != handle.incarnation) {
1506 try {
1507 candidate->reservation_growth_source.assign_capture(
1508 selected_source.incarnation, handle.incarnation);
1509 candidate->pooc_global_full_exit_bound_add = true;
1510 } catch (const std::invalid_argument&) {
1511 // This exact source already owns the capture receipt.
1512 }
1513 }
1514 }
1515 }
1516}
1517
1518void PineExecutionAdapter::update_l4c_priority() {
1519 auto& host = require_host();
1520 // The one ordering the priority can select, the retained-child /
1521 // fresh-parent exception, is refused by OrderPriority::select before it
1522 // reads a candidate unless the policy is attached and on, the run
1523 // processes orders on close without calc_on_order_fills or a fill
1524 // recalculation, and exactly two live handles carry a placement row
1525 // (src/compat/pine/order_priority.cpp). Those facts are the adapter's
1526 // own, so a call they rule out builds no candidates and reads nothing
1527 // (the trigger-band and scratch reuse); its only effect, the reorder of live_handles_, needs
1528 // a selection.
1529 if (!priority.attached() || !priority.retained_parent_first()
1530 || !config_.process_orders_on_close || config_.calc_on_order_fills
1531 || coof_recalc_active_) {
1532 return;
1533 }
1534 std::size_t placed = 0;
1535 for (const auto& handle : live_handles_) {
1536 if (placement_.find(handle.incarnation) != placement_.end()) ++placed;
1537 }
1538 if (placed != 2) return;
1539 std::vector<compat::pine::OrderPriorityCandidate> candidates;
1540 candidates.reserve(live_handles_.size());
1541 for (const auto& handle : live_handles_) {
1542 const auto found = placement_.find(handle.incarnation);
1543 if (found == placement_.end()) continue;
1544 const auto& snapshot = found->second;
1545 compat::pine::OrderPriorityCandidate candidate;
1546 candidate.handle = handle;
1547 candidate.kind = snapshot.family == PineOrderFamily::Entry
1554 candidate.id = snapshot.source_id;
1555 candidate.from_entry = snapshot.from_entry;
1556 candidate.created_bar = snapshot.projection_created_bar;
1557 candidate.source_sequence = snapshot.source_sequence;
1558 candidate.predecessor = snapshot.projection_predecessor;
1559 candidate.recreated_after_named_cancelled =
1560 snapshot.recreated_after_named_cancelled_entry_incarnation;
1561 candidate.named_cancel_surviving_exit = snapshot.named_cancel_surviving_exit_incarnation;
1562 candidate.created_flat = snapshot.projection_position_side
1563 == static_cast<std::int32_t>(PositionSide::FLAT);
1564 candidate.birth_from_fill = snapshot.birth.from_fill();
1565 candidate.prior_close = snapshot.projection_after_close;
1566 candidate.at_entry_capacity = snapshot.projection_over_pyramiding;
1567 candidate.stop_limit_activated = snapshot.stop_limit_activated;
1568 candidate.default_quantity = !std::isfinite(snapshot.requested_qty);
1569 candidate.requested_qty = snapshot.requested_qty;
1570 candidate.qty_percent = snapshot.qty_percent;
1571 candidate.stop = snapshot.exit_levels.stop;
1572 candidate.limit = snapshot.exit_levels.limit;
1573 candidate.trail_points = snapshot.exit_levels.trail_points;
1574 candidate.trail_price = snapshot.exit_levels.trail_price;
1575 candidate.trail_offset = snapshot.exit_levels.trail_offset;
1576 candidate.profit_ticks = snapshot.exit_levels.profit_ticks;
1577 candidate.loss_ticks = snapshot.exit_levels.loss_ticks;
1578 candidate.oca_name = snapshot.oca_name;
1579 candidate.oca_type = snapshot.oca_type;
1580 candidates.push_back(std::move(candidate));
1581 }
1582 const auto point = detail::callback_point(host);
1583 const compat::pine::OrderPriorityContext context{
1585 config_.process_orders_on_close, config_.calc_on_order_fills,
1586 coof_recalc_active_, point && point->decision.sub_count > 1, false, true,
1587 point ? point->decision.coordinate.interval_index : -1};
1588 const auto decision = priority.select(context, candidates);
1589 if (!decision) return;
1590 const auto rank = [&](const native_order::RequestHandle& handle) {
1591 if (handle == decision->parent) return 0;
1592 if (handle == decision->child) return 1;
1593 return 2;
1594 };
1595 std::stable_sort(pending_write(live_handles_).begin(), live_handles_.end(),
1596 [&](const auto& left, const auto& right) { return rank(left) < rank(right); });
1597}
1598
1599void PineExecutionAdapter::update_l4c_lifecycle(
1600 const native_order::ExecutionAppliedEvent& event,
1601 const NativeDecisionContext& context) {
1602 const auto found = placement_.find(event.handle().incarnation);
1603 if (found == placement_.end()) return;
1604 auto& snapshot = found->second;
1605 const bool exit = snapshot.family == PineOrderFamily::ExitLimit
1608 if (!exit || !snapshot.legs.target().incarnation) return;
1609 const auto domain = context.sub_count > 1
1610 ? (config_.calc_on_order_fills ? exit_legs::Domain::MagnifierFillRecalc
1611 : exit_legs::Domain::Magnifier)
1612 : (config_.calc_on_order_fills ? exit_legs::Domain::FillRecalc
1613 : exit_legs::Domain::Ordinary);
1614 const exit_legs::Frame cause{event.ordinal, context.coordinate.interval_index,
1615 domain, exit_legs::Phase::AfterMargin};
1616 if (const auto completion = compat::pine::select_exit_completion(snapshot.legs, cause)) {
1617 const exit_legs::Action action{snapshot.legs.target(), snapshot.legs.revision(),
1618 cause, *completion};
1619 (void)snapshot.legs.apply(snapshot.legs.target(), action); invalidate_retirement();
1620 }
1621 if (detail::run_position(require_host()).signed_units == 0.0) snapshot.leg_activation.unbind();
1622}
1623
1624bool PineExecutionAdapter::is_declined_market_reversal(
1625 const native_order::MatchRejectedEvent& event) const noexcept {
1626 if (event.reason != native_order::MatchRejectReason::HostPrecommit
1627 && event.reason != native_order::MatchRejectReason::InitialMargin) {
1628 return false;
1629 }
1630 const auto found = placement_.find(event.handle().incarnation);
1631 if (found == placement_.end()) return false;
1632 const auto& source = found->second;
1633 const auto prior = static_cast<PositionSide>(source.projection_position_side);
1634 return source.opening && source.family == PineOrderFamily::Entry
1635 && source.reverse_to && std::holds_alternative<native_order::Market>(event.request().trigger)
1636 && prior != PositionSide::FLAT
1637 && ((prior == PositionSide::LONG) != source.is_long);
1638}
1639
1640bool PineExecutionAdapter::follows_same_bar_declined_reversal(
1641 const PlacementSnapshot& exit, const NativePrecommitView& view) const {
1642 if (!exit.legs.target().incarnation) return false;
1643 const auto follows = [&](const native_order::CommandEvent& command) {
1644 const auto* rejected = std::get_if<native_order::MatchRejectedEvent>(&command);
1645 if (!rejected || rejected->cursor.point.interval_index != view.cursor.point.interval_index
1646 || !is_declined_market_reversal(*rejected)) {
1647 return false;
1648 }
1649 const auto reversal = placement_.find(rejected->handle().incarnation);
1650 // A from_entry="" exit names every entry, so it is always the held
1651 // position's: the declined reversal holds it as it holds a named
1652 // bracket of that side (lab tv tapes tests/fixtures/margin_v6 w5-mr-*,
1653 // lane W5-ENG-MARGIN-V6) -- unless it was placed under the side the
1654 // rejected entry would have opened, which another entry of the bar
1655 // already reversed into (lab tv w3bf05-r1c, lane W3B-ENG-GRID).
1656 return reversal != placement_.end()
1657 && (exit.from_entry.empty()
1658 ? exit.projection_position_side
1659 != static_cast<std::int32_t>(reversal->second.is_long
1661 : bracket_belongs_to_reversal(exit, reversal->second));
1662 };
1663 auto& host = require_host();
1664 // In place on a PineStrategyHost, as in observe_terminal_receipts.
1665 if (auto* pine = pine_view_of(&host)) {
1666 return as_native_consumer(pine->execution_consumer())
1667 .visit_commands_after(receipt_cursor_, follows);
1668 }
1669 for (const auto& row : host.native_events(receipt_cursor_)) {
1670 if (row.command && follows(*row.command)) return true;
1671 }
1672 return false;
1673}
1674
1675// The host bookkeeping index cache: the indexes the adapter reads its retained bookkeeping
1676// through. The native consumer keeps them for the adapter (a by-value member
1677// of PineStrategyHost, whose layout is the generated script's ABI, holds no
1678// index itself), drops them at every run begin, and never reads them. They
1679// are derived from state the adapter retains and hashes, folded into nothing,
1680// and each lookup answers what the walk it replaces answers.
1681// The kind of the cache below (pine_host_reads.hpp, the per-pump state cache).
1683
1684namespace {
1685
1686class AdapterLookupIndex final : public detail::PineRunCache {
1687public:
1688
1689 // Per cohort, over its origin roster: how many roster entries are folded,
1690 // and whether a folded origin is an opening on each side. The roster only
1691 // grows (every accepted opening is appended once, after its placement row
1692 // exists), and an opening row's `opening` and `is_long` are never written
1693 // after it is remembered, so the two flags only ever turn on and a fold
1694 // over the entries appended since the last lookup keeps them exact. Keyed
1695 // by the durable cohort handle, not a resident row address: a row can
1696 // become empty outside retire_origin_prefixes and its storage can be
1697 // reused before the derived entry is explicitly erased. A restored empty
1698 // roster answers from its erased sides, and reset_for_run clears the index.
1699 struct CohortSides {
1700 std::size_t folded = 0;
1701 bool opened_long = false;
1702 bool opened_short = false;
1703 };
1704
1705 std::uint64_t answers = 0;
1706 // Derived index, one entry (~24 payload bytes, plus hash-node/bucket
1707 // overhead) per id that exceeded 16 origins and needed this query.
1708 // Bound: ids that ever exceeded 16 origins, not live/held cohorts.
1709 // Prefix retirement can erase entries; the unjailed profile is unbounded.
1710 std::unordered_map<std::uint64_t, CohortSides> cohort_sides;
1711
1712 // Over the placement table's rows: exit legs by the origin they are
1713 // bound to and their leg kind, and immediate closes by the bar they were
1714 // placed on. A row's family, `immediately` and projection_created_bar are
1715 // never written after it is remembered; its bracket_origin is, exactly
1716 // once, from zero to the parent whose fill binds it (on_applied), and only
1717 // while the leg is working. So a row is filed where it will stay when it
1718 // is folded, except a working leg still bound to origin zero, which waits
1719 // in `unbound` until it is bound (then it is filed) or stops working (a
1720 // handle that stops working never works again, so it is never bound).
1721 // Rows mostly arrive above every row folded so far; one that arrives
1722 // below (an anchored leg adopted at its parent's fill), and any erasure
1723 // (the retired-row compaction: the table keeps only what can still be read), refolds
1724 // the index from the table's rows, which are the live few. Each lookup
1725 // verifies every row it reads.
1726 std::uint64_t erased_seen = 0;
1727 std::uint64_t folded_high = 0;
1728 std::size_t rows_folded = 0;
1729 std::vector<std::uint64_t> unbound;
1730 std::array<std::unordered_map<std::uint64_t, std::vector<std::uint64_t>>, 3> legs_by_origin;
1731 std::unordered_map<std::int32_t, std::vector<std::uint64_t>> immediate_closes_by_bar;
1732
1733 // The UTC chart-day memo: chart_day_key's last day on a UTC chart -- the whole
1734 // seconds [day_lo, day_hi) of one floor day inside the civil arithmetic's
1735 // span, and the key they answer. That key is a function of the floor day
1736 // alone, so every second of the range answers it. Empty until the first
1737 // arithmetic read; never counted in `answers`.
1738 std::int64_t day_lo = 1;
1739 std::int64_t day_hi = 0;
1740 std::int64_t day_key = 0;
1741
1742 std::uint64_t answered() const noexcept override { return answers; }
1743
1744 void clear() noexcept {
1745 cohort_sides.clear();
1746 clear_rows();
1747 day_lo = 1;
1748 day_hi = 0;
1749 day_key = 0;
1750 }
1751
1752 void clear_rows() noexcept {
1753 erased_seen = 0;
1754 folded_high = 0;
1755 rows_folded = 0;
1756 unbound.clear();
1757 for (auto& legs : legs_by_origin) legs.clear();
1758 immediate_closes_by_bar.clear();
1759 }
1760
1761 static bool exit_leg(PineOrderFamily family) noexcept {
1762 return family == PineOrderFamily::ExitLimit || family == PineOrderFamily::ExitStop
1763 || family == PineOrderFamily::ExitTrail;
1764 }
1765
1766 static bool working(const std::vector<native_order::RequestHandle>& live,
1767 std::uint64_t incarnation) noexcept {
1768 return std::any_of(live.begin(), live.end(),
1769 [&](const native_order::RequestHandle& handle) {
1770 return handle.incarnation == incarnation;
1771 });
1772 }
1773
1774 // The rows bound to `origin` of that leg kind, or null.
1775 const std::vector<std::uint64_t>* legs_of(PineOrderFamily family,
1776 std::uint64_t origin) const noexcept {
1777 const auto& legs = legs_by_origin[static_cast<std::size_t>(family)
1778 - static_cast<std::size_t>(PineOrderFamily::ExitLimit)];
1779 const auto found = legs.find(origin);
1780 return found == legs.end() ? nullptr : &found->second;
1781 }
1782
1783 void fold(std::uint64_t incarnation, const PlacementSnapshot& row,
1784 const std::vector<native_order::RequestHandle>& live) {
1785 if (exit_leg(row.family)) {
1786 if (row.bracket_origin.incarnation != 0) {
1787 legs_by_origin[static_cast<std::size_t>(row.family)
1788 - static_cast<std::size_t>(PineOrderFamily::ExitLimit)]
1789 [row.bracket_origin.incarnation].push_back(incarnation);
1790 } else if (working(live, incarnation)) {
1791 unbound.push_back(incarnation);
1792 }
1793 } else if (row.family == PineOrderFamily::Close && row.immediately) {
1794 immediate_closes_by_bar[row.projection_created_bar].push_back(incarnation);
1795 }
1796 }
1797
1798 // Brings the row index up to the table. May throw std::bad_alloc; every
1799 // step leaves the index consistent, so a later sync resumes it.
1800 void sync_rows(const PlacementTable& table,
1801 const std::vector<native_order::RequestHandle>& live) {
1802 if (table.erased_rows() != erased_seen || table.size() < rows_folded
1803 || table.high_water() < folded_high) {
1804 clear_rows();
1805 erased_seen = table.erased_rows();
1806 }
1807 for (std::size_t i = 0; i < unbound.size();) {
1808 const auto found = table.find(unbound[i]);
1809 if (found != table.end() && found->second.bracket_origin.incarnation != 0) {
1810 fold(unbound[i], found->second, live);
1811 } else if (found != table.end() && working(live, unbound[i])) {
1812 ++i;
1813 continue;
1814 }
1815 unbound.erase(unbound.begin() + static_cast<std::ptrdiff_t>(i));
1816 }
1817 for (auto row = table.upper_bound(folded_high); row != table.end(); ++row) {
1818 fold(row->first, row->second, live);
1819 ++rows_folded;
1820 folded_high = row->first;
1821 }
1822 if (rows_folded < table.size()) {
1823 // A row arrived below the folded mark: fold the table afresh.
1824 clear_rows();
1825 erased_seen = table.erased_rows();
1826 for (const auto& row : table) {
1827 fold(row.first, row.second, live);
1828 ++rows_folded;
1829 folded_high = row.first;
1830 }
1831 }
1832 }
1833};
1834
1835// A walk over at most this many retained entries costs less than reaching
1836// the consumer's cache (the host downcast, the consumer, the cache's type):
1837// below it the lookups walk, above it they read the index. Either answer is
1838// the walk's, so the threshold only moves cost.
1839constexpr std::size_t kIndexedFrom = 16;
1840
1841// The index `consumer` keeps for the adapter at `owner` in run `run`: the
1842// one it adopted in this run, else (when `create`) a fresh one, else null.
1843// Any other cache it holds (another adapter bound to the same host, an
1844// earlier run) is replaced, never read.
1845AdapterLookupIndex* adapter_lookup_index(IExecutionConsumer* consumer, const void* owner,
1846 std::uint64_t run, bool create = true) noexcept {
1847 if (!consumer) return nullptr;
1848 auto& native = as_native_consumer(*consumer);
1849 NativeHostCache* const cache = native.host_cache();
1850 auto* index = cache && cache->kind() == &detail::kPineRunCacheKind
1851 ? static_cast<AdapterLookupIndex*>(cache) : nullptr;
1852 if (index && index->owner == owner && index->run == run) return index;
1853 if (!create) return nullptr;
1854 try {
1855 auto fresh = std::make_unique<AdapterLookupIndex>();
1856 fresh->owner = owner;
1857 fresh->run = run;
1858 return static_cast<AdapterLookupIndex*>(native.adopt_host_cache(std::move(fresh)));
1859 } catch (const std::bad_alloc&) {
1860 return nullptr;
1861 }
1862}
1863
1864// A fresh index for the adapter at `owner` in run `run`, adopted by `native`,
1865// or null when it holds none. Out of line: the per-bar read below reaches it
1866// once a run.
1867[[gnu::noinline]] AdapterLookupIndex* adopt_lookup_index(NativeExecutionConsumer& native,
1868 const void* owner,
1869 std::uint64_t run) noexcept {
1870 try {
1871 auto fresh = std::make_unique<AdapterLookupIndex>();
1872 fresh->owner = owner;
1873 fresh->run = run;
1874 return static_cast<AdapterLookupIndex*>(native.adopt_host_cache(std::move(fresh)));
1875 } catch (...) {
1876 return nullptr;
1877 }
1878}
1879
1880// The same index, reached for a read made on every bar (the UTC chart-day memo): the
1881// consumer straight off the host's engine slot (NativeExecutionConsumer::
1882// bound, which is what the host's own reads use) and the cache's exact type,
1883// instead of the host's Pine view and a dynamic_cast, which cost about what
1884// that read saves. AdapterLookupIndex is final, so the type test answers
1885// exactly what adapter_lookup_index's dynamic_cast does, and the index it
1886// finds or adopts is the one adapter_lookup_index answers for the same run.
1887AdapterLookupIndex* parked_lookup_index(NativeStrategyHost* host, const void* owner,
1888 std::uint64_t run) noexcept {
1889 if (host == nullptr) return nullptr;
1890 NativeExecutionConsumer* native = nullptr;
1891 try {
1892 native = &NativeExecutionConsumer::bound(*host);
1893 } catch (...) {
1894 return nullptr;
1895 }
1896 NativeHostCache* cache = native->host_cache();
1897 if (cache != nullptr && typeid(*cache) == typeid(AdapterLookupIndex)) {
1898 auto* index = static_cast<AdapterLookupIndex*>(cache);
1899 if (index->owner == owner && index->run == run) return index;
1900 }
1901 return adopt_lookup_index(*native, owner, run);
1902}
1903
1904} // namespace
1905
1906// The per-pump state cache (pine_host_reads.hpp): the running spec's facts the host reads
1907// every bar, taken once per run into the adapter's cache -- the P7 index, which
1908// the run's first lookup would otherwise create. Only a running spec is fixed
1909// for the run the cache belongs to.
1910void detail::take_run_facts(NativeExecutionConsumer& consumer,
1911 const PineExecutionAdapter& adapter, std::uint64_t run) {
1912 if (!consumer.running()) return;
1913 if (auto* index = adapter_lookup_index(&consumer, &adapter, run))
1914 index->aggregates_input_bars = consumer.aggregates_input();
1915}
1916
1917IExecutionConsumer* PineExecutionAdapter::bound_consumer() const noexcept {
1918 auto* pine = pine_view_of(host_);
1919 if (!pine) return nullptr;
1920 try {
1921 return &detail::run_consumer(*pine);
1922 } catch (...) {
1923 return nullptr;
1924 }
1925}
1926
1927bool PineExecutionAdapter::cohort_opened_on_side(const CohortFacts& cohort,
1928 bool is_long) const noexcept {
1929 // The openings whose rows were erased left their side behind (the
1930 // retired-row compaction); the walk and the index read the retained rows beside it.
1931 const bool erased = (cohort.erased_opening_sides & (is_long ? 1U : 2U)) != 0;
1932 const auto walk = [&] {
1933 if (erased) return true;
1934 for (const auto& origin : cohort.origins) {
1935 if (!placement_.contains(origin.incarnation)) continue; // in `erased`
1936 const auto opening = placement_.find(origin.incarnation);
1937 if (opening != placement_.end() && opening->second.opening
1938 && opening->second.is_long == is_long) {
1939 return true;
1940 }
1941 }
1942 return false;
1943 };
1944 if (cohort.origins.size() <= kIndexedFrom || cohort.origins.members().empty()) return walk();
1945 AdapterLookupIndex* index = adapter_lookup_index(bound_consumer(), this, run_counter_);
1946 if (!index) return walk();
1947 try {
1948 auto& sides = index->cohort_sides[cohort.handle.value];
1949 const auto& roster = cohort.origins;
1950 if (roster.size() < sides.folded) sides = {};
1951 for (sides.folded = std::max(sides.folded, roster.base());
1952 sides.folded < roster.size(); ++sides.folded) {
1953 if (!placement_.contains(roster[sides.folded].incarnation)) continue;
1954 const auto opening = placement_.find(roster[sides.folded].incarnation);
1955 if (opening == placement_.end() || !opening->second.opening) continue;
1956 (opening->second.is_long ? sides.opened_long : sides.opened_short) = true;
1957 }
1958 ++index->answers;
1959 PINEFORGE_LOOKUP_INDEX_WORK("cohort_side_answer", this, run_counter_, 1);
1960 const bool opened = erased || (is_long ? sides.opened_long : sides.opened_short);
1961 assert(opened == walk());
1962 return opened;
1963 } catch (const std::bad_alloc&) {
1964 return walk();
1965 }
1966}
1967
1968bool PineExecutionAdapter::origin_leg_consumed(
1969 std::uint64_t family_key, PineOrderFamily family,
1970 const native_order::RequestHandle& origin) const noexcept {
1971 PINEFORGE_LOOKUP_INDEX_WORK("consumed_leg_probe", this, run_counter_, 1);
1972 const auto members = bracket_families_.find(family_key);
1973 if (members == bracket_families_.end()) return false;
1974 // A consumed member whose row was erased left the (leg kind, origin) it
1975 // answers for in the family (the retired-row compaction).
1976 const bool erased = members->second.consumed(static_cast<std::uint8_t>(family),
1977 origin.incarnation);
1978 const auto consumed = [&](std::uint64_t incarnation) {
1979 // A leg whose row was erased answers through its fact (`erased`).
1980 if (!placement_.contains(incarnation)) return false;
1981 const auto found = placement_.find(incarnation);
1982 if (found == placement_.end()) return false;
1983 const auto& prior = found->second;
1984 return prior.family == family && prior.bracket_origin == origin
1985 && !prior.legs.dormant()
1986 && std::none_of(live_handles_.begin(), live_handles_.end(),
1987 [&](const native_order::RequestHandle& live) {
1988 return live.incarnation == incarnation;
1989 });
1990 };
1991 // The walk: every leg the family placed. A leg whose row was erased is
1992 // in the settled prefix or the working tail and answered through its
1993 // fact; the rest are in the working tail.
1994 const auto walk = [&] {
1995 return erased || std::any_of(members->second.working_tail().begin(),
1996 members->second.working_tail().end(),
1997 [&](const native_order::RequestHandle& handle) {
1998 return consumed(handle.incarnation);
1999 });
2000 };
2001 if (members->second.size() <= kIndexedFrom) return walk();
2002 AdapterLookupIndex* index = AdapterLookupIndex::exit_leg(family)
2003 ? adapter_lookup_index(bound_consumer(), this, run_counter_) : nullptr;
2004 if (!index) return walk();
2005 try {
2006 index->sync_rows(placement_, live_handles_);
2007 // Only a row bound to this origin, of this leg kind, can satisfy
2008 // `consumed`; the family is the walk's set, so a candidate counts
2009 // only while the family holds it.
2010 bool answer = erased;
2011 if (const auto* legs = answer ? nullptr : index->legs_of(family, origin.incarnation)) {
2012 for (const auto incarnation : *legs) {
2013 if (members->second.count(incarnation) != 0 && consumed(incarnation)) {
2014 answer = true;
2015 break;
2016 }
2017 }
2018 }
2019 ++index->answers;
2020 PINEFORGE_LOOKUP_INDEX_WORK("consumed_leg_answer", this, run_counter_, 1);
2021 assert(answer == walk());
2022 return answer;
2023 } catch (const std::bad_alloc&) {
2024 return walk();
2025 }
2026}
2027
2028bool PineExecutionAdapter::immediate_close_placed_on(std::int32_t bar) const noexcept {
2029 const auto placed = [&](const PlacementSnapshot& row) {
2030 return row.family == PineOrderFamily::Close && row.immediately
2031 && row.projection_created_bar == bar;
2032 };
2033 // The walk: every row the table holds (an immediate close is kept while
2034 // its placement bar is the current one or later: erase_retired_rows, K4).
2035 const auto walk = [&] {
2036 return std::any_of(placement_.begin(), placement_.end(),
2037 [&](const auto& row) { return placed(row.second); });
2038 };
2040 [&](std::uint64_t, const PlacementSnapshot& row) { return placed(row); });
2041 if (placement_.size() <= kIndexedFrom) return walk();
2042 AdapterLookupIndex* index = adapter_lookup_index(bound_consumer(), this, run_counter_);
2043 if (!index) return walk();
2044 try {
2045 index->sync_rows(placement_, live_handles_);
2046 bool answer = false;
2047 if (const auto closes = index->immediate_closes_by_bar.find(bar);
2048 closes != index->immediate_closes_by_bar.end()) {
2049 for (const auto incarnation : closes->second) {
2050 const auto found = placement_.find(incarnation);
2051 if (found != placement_.end() && placed(found->second)) {
2052 answer = true;
2053 break;
2054 }
2055 }
2056 }
2057 ++index->answers;
2058 PINEFORGE_LOOKUP_INDEX_WORK("same_bar_answer", this, run_counter_, 1);
2059 assert(answer == walk());
2060 return answer;
2061 } catch (const std::bad_alloc&) {
2062 return walk();
2063 }
2064}
2065
2066bool PineExecutionAdapter::bracket_belongs_to_reversal(
2067 const PlacementSnapshot& bracket,
2068 const PlacementSnapshot& reversal) const {
2069 if (bracket.projection_position_side == reversal.projection_position_side)
2070 return true;
2071 const auto cohort = cohorts_by_id_.find(bracket.from_entry);
2072 if (cohort == cohorts_by_id_.end()) return false;
2073 const bool prior_long = reversal.projection_position_side
2074 == static_cast<std::int32_t>(PositionSide::LONG);
2075 return cohort_opened_on_side(cohort->second, prior_long);
2076}
2077
2078void PineExecutionAdapter::suspend_declined_reversal_brackets(
2079 const native_order::MatchRejectedEvent& event) {
2080 if (!is_declined_market_reversal(event)) return;
2081 const auto reversal = placement_.find(event.handle().incarnation);
2082 if (reversal == placement_.end()) return;
2083 const auto domain = config_.calc_on_order_fills ? exit_legs::Domain::FillRecalc
2084 : exit_legs::Domain::Ordinary;
2085 const exit_legs::Frame cause{event.ordinal, event.cursor.point.interval_index,
2086 domain, exit_legs::Phase::Observation};
2087 suspend_brackets_for_reversal(reversal->second, cause,
2088 policy_script_bar_valid_ ? policy_script_bar_.open : reversal->second.sizing.mark);
2089}
2090
2091bool PineExecutionAdapter::reissue_revives_declined_exit(
2092 const PlacementSnapshot& previous) const noexcept {
2093 // A declined reversal kills the held position's stop and limit exits
2094 // (round 9 family X: an exit issued once never fills after the decline,
2095 // test_famx_declined_reversal_trail_leg_l4c). Re-issued by a later
2096 // calculation the exit is a new order and lives, as the replacement path
2097 // already treats a changed re-issue (fresh_after_dormant): an unchanged
2098 // re-issue must not keep the killed row either, whatever the side, the
2099 // sizing or the exit's kind. `lab tv` synthetics in
2100 // tests/fixtures/callback_lifecycle, BINANCE:ETHUSDT.P 15m 2025-05-28,
2101 // a 100%-of-equity reversal declined at the 19:45 open and the exit
2102 // re-issued on every bar, 4 identical exports each: the long's limit
2103 // (range-breakout-may-rev, -rev-sl), the short mirror's limit
2104 // (declined-reissue-short) and a partial stop (declined-reissue-stop)
2105 // each fill on their first reaching bar after the decline. Fixed and
2106 // cash sizing never reach this row: their unaffordable reversal keeps
2107 // its close leg and flattens the long (declined-reissue-fixed, -cash).
2108 //
2109 // Only the kill revives: the entry/strategy.close pair hold
2110 // (hold_reversal_pair_brackets) also makes a row dormant, behind a
2111 // barrier that keeps the pending reversal ahead of the position's own
2112 // gapped bracket at the next open; a re-issue in the pair's calculation
2113 // keeps that barrier (pair-hold-reissue: the admitted pair closes the
2114 // long at the gap open, its gapped stop never fills). The
2115 // fill-recalculation pass declines through its own path and keeps its
2116 // dormant rows.
2118 || config_.calc_on_order_fills
2119 || (previous.family != PineOrderFamily::ExitLimit
2120 && previous.family != PineOrderFamily::ExitStop)) {
2121 return false;
2122 }
2123 const auto& suspension = previous.legs.suspension();
2124 return suspension && !suspension->hold && suspension->window
2125 && !suspension->replacement;
2126}
2127
2128void PineExecutionAdapter::suspend_brackets_for_reversal(
2129 const PlacementSnapshot& reversal, const exit_legs::Frame& cause,
2130 double open_price) {
2131 const int direction = reversal.projection_position_side
2132 == static_cast<std::int32_t>(PositionSide::LONG) ? 1 : -1;
2133 // Off the reversal's side, bracket_belongs_to_reversal() answers from a
2134 // walk of the bracket's from_entry origins, which this loop never changes
2135 // (it only rewrites leg lifecycles): memoise that answer per from_entry
2136 // instead of repeating the walk for every retained exit row.
2137 std::unordered_map<SourceId, bool> cross_side_by_entry;
2138 const auto belongs = [&](const PlacementSnapshot& bracket) {
2139 // A from_entry="" exit is the held position's, not one placed under
2140 // the side the reversal would open (follows_same_bar_declined_reversal).
2141 if (bracket.from_entry.empty()) {
2142 return bracket.projection_position_side
2143 != static_cast<std::int32_t>(reversal.is_long ? PositionSide::LONG
2145 }
2146 if (bracket.projection_position_side == reversal.projection_position_side)
2147 return true;
2148 const auto cached = cross_side_by_entry.find(bracket.from_entry);
2149 if (cached != cross_side_by_entry.end()) return cached->second;
2150 const bool value = bracket_belongs_to_reversal(bracket, reversal);
2151 cross_side_by_entry.emplace(bracket.from_entry, value);
2152 return value;
2153 };
2154 // An erased row this suspension would rewrite is a leg of an ended cycle
2155 // bound to an origin that can no longer be asked about (erase_retired_
2156 // rows, K1): nothing reads its lifecycle again. The audit holds that.
2158 [&](std::uint64_t, const PlacementSnapshot& candidate) {
2159 return lifecycle_readable(candidate);
2160 });
2161 for (auto row : placement_) {
2162 auto& candidate = row.second;
2163 const bool exit = candidate.family == PineOrderFamily::ExitLimit
2164 || candidate.family == PineOrderFamily::ExitStop
2165 || candidate.family == PineOrderFamily::ExitTrail;
2166 if (!exit || candidate.legs.dormant()
2167 || !candidate.legs.target().incarnation
2168 || !belongs(candidate)) {
2169 continue;
2170 }
2171 const compat::pine::ExitSuspensionContext context{
2172 cause, direction, require_host().position_avg_price(), staged_.syminfo.mintick,
2173 open_price,
2174 candidate.legs.trail_best(), false, true};
2175 const auto operation = compat::pine::select_exit_suspension(candidate.legs, context);
2176 if (!operation) continue;
2177 const exit_legs::Action action{candidate.legs.target(), candidate.legs.revision(),
2178 cause, *operation};
2179 (void)candidate.legs.apply(candidate.legs.target(), action); invalidate_retirement();
2180 }
2181}
2182
2183void PineExecutionAdapter::suspend_coof_declined_reversal_at_open(
2184 const Bar& bar, const NativeDecisionContext& context) {
2185 const auto physical = detail::run_position(require_host());
2186 if (physical.signed_units == 0.0 || !finite_positive(bar.open)) return;
2187 const auto handles = live_handles_;
2188 for (const auto& handle : handles) {
2189 const auto found = placement_.find(handle.incarnation);
2190 if (found == placement_.end()) continue;
2191 const auto& reversal = found->second;
2192 const bool opposite = (physical.signed_units > 0.0) != reversal.is_long;
2193 const bool market_entry = !finite_positive(reversal.exit_levels.limit)
2194 && !finite_positive(reversal.exit_levels.stop)
2195 && !finite_positive(reversal.exit_levels.trail_offset)
2196 && !finite_positive(reversal.exit_levels.trail_price);
2197 if (!reversal.opening || reversal.family != PineOrderFamily::Entry
2198 || !reversal.reverse_to || !opposite || !market_entry) {
2199 continue;
2200 }
2201 const double units = finite_positive(reversal.sizing.frozen_units)
2202 ? reversal.sizing.frozen_units : reversal.requested_qty;
2203 const double margin = reversal.is_long ? config_.margin_long : config_.margin_short;
2204 const double fx = active_staged_fx(context.sub_bar_open_ms);
2205 const double required = units * bar.open * staged_.syminfo.pointvalue
2206 * fx * margin / 100.0;
2207 const double equity = std::isfinite(reversal.sizing.equity)
2208 ? reversal.sizing.equity : require_host().native_marked_equity(bar.open);
2209 const double guard = std::max(1e-9, std::abs(equity) * 1e-12);
2210 if (finite_positive(units) && finite_positive(margin) && std::isfinite(required)
2211 && std::isfinite(equity) && required > equity + guard) {
2212 const auto domain = config_.calc_on_order_fills ? exit_legs::Domain::FillRecalc
2213 : exit_legs::Domain::Ordinary;
2214 const exit_legs::Frame cause{context.coordinate.ordinal,
2215 context.coordinate.interval_index, domain,
2216 exit_legs::Phase::Observation};
2217 suspend_brackets_for_reversal(reversal, cause, bar.open);
2218 }
2219 }
2220}
2221
2222void PineExecutionAdapter::hold_reversal_pair_brackets(const SourceId& from_entry) {
2223 const auto point = detail::callback_point(require_host());
2224 // The cause's ordinal is the kernel's own event number, the live frame's even
2225 // where `point` is a retained one being replayed.
2226 const auto live = detail::live_callback_point(require_host());
2227 if (!point || !live) return;
2228 const auto domain = config_.calc_on_order_fills ? exit_legs::Domain::FillRecalc
2229 : exit_legs::Domain::Ordinary;
2230 const exit_legs::Frame cause{live->decision.coordinate.ordinal,
2231 point->decision.coordinate.interval_index, domain, exit_legs::Phase::Observation};
2232 // As in suspend_brackets_for_reversal: no erased lifecycle is read again.
2234 [&](std::uint64_t, const PlacementSnapshot& candidate) {
2235 return lifecycle_readable(candidate);
2236 });
2237 for (auto row : placement_) {
2238 auto& candidate = row.second;
2239 const bool exit = candidate.family == PineOrderFamily::ExitLimit
2240 || candidate.family == PineOrderFamily::ExitStop
2241 || candidate.family == PineOrderFamily::ExitTrail;
2242 if (!exit || candidate.from_entry != from_entry || candidate.legs.dormant()
2243 || !candidate.legs.target().incarnation) {
2244 continue;
2245 }
2246 const auto operation = compat::pine::select_pair_hold(candidate.legs, cause);
2247 const exit_legs::Action action{candidate.legs.target(), candidate.legs.revision(),
2248 cause, operation};
2249 (void)candidate.legs.apply(candidate.legs.target(), action); invalidate_retirement();
2250 }
2251}
2252
2253// strategy.cancel / strategy.cancel_all withdraw an exit whatever its state:
2254// a live exit the cancel takes off the book, and a dormant one -- an exit a
2255// declined reversal held (finding-311) -- alike. TradingView never brings a
2256// cancelled exit back. The paired control and cancel tapes
2257// (NYSE:F 15m) show one script with and without
2258// strategy.cancel("X") while X is dormant: the margin call revives
2259// the standing dormant X and closes the position through it; after the cancel
2260// it does not, and a second margin call follows. The tapes
2261// that cancel before the pair and after the entry fill (the cancelled-origin retirement) cancel
2262// X while it is still live. Synthetic accounting: 1000 initial shares,
2263// calls of 20 and 80, then 900 left open (1000 - 20 - 80): the
2264// declined pair's suspension still makes the cancelled leg dormant later,
2265// and the revival must not read that as a standing exit. The legacy engine
2266// skipped any cancelled order in the same pass (ab9714be
2267// pine_fills.cpp:2065-2076). The lifecycle records the withdrawal where the
2268// revival reads it: every leg retired (`withdrawn`), a state no suspension
2269// produces. `cancelled` names the live requests this cancel withdrew, each
2270// with the ordinal of its Cancelled event: that event is the cause, so the
2271// withdrawal lands after whatever the same callback did to the lifecycle
2272// before (a pair hold at the callback's own ordinal would otherwise make it a
2273// conflicting replay, and the lifecycle would refuse it). Every other exit
2274// row the cancel names -- a dormant one, or one the adapter itself took off
2275// the book on the script's behalf -- is found by id, caused at the callback,
2276// or just past the lifecycle's last step when that step is no older.
2277//
2278// The adapter takes an exit off the book on the script's behalf where
2279// TradingView keeps it pending: a gapped exit stop a declined reversal
2280// retires waits in pending_margin_revivals_ for the next margin-call slice
2281// (apply_reversal_gap_bracket_policy), and the declined pair's suspension
2282// can still make its row dormant. A cancel drops the waiting revival and
2283// withdraws the row -- the paired gapped-stop control and cancel tapes
2284// (synthetic stop 25.15) close the remaining 980 shares through X at the
2285// margin call without the cancel and book the cancel tape's trades with it.
2286void PineExecutionAdapter::retire_cancelled_exits(
2287 const SourceId* id,
2288 const std::vector<std::pair<std::uint64_t, std::uint64_t>>& cancelled) {
2289 const auto point = detail::callback_point(require_host());
2290 // The sweep's event ordinal below is the kernel's own, the live frame's even
2291 // where `point` is a retained one being replayed (the bar stays `point`'s).
2292 const auto live = detail::live_callback_point(require_host());
2293 if (!point || !live) return;
2294 const auto domain = config_.calc_on_order_fills ? exit_legs::Domain::FillRecalc
2295 : exit_legs::Domain::Ordinary;
2296 const auto retire_legs = [&](PlacementSnapshot& candidate, std::uint64_t event) {
2297 const bool exit = candidate.family == PineOrderFamily::ExitLimit
2298 || candidate.family == PineOrderFamily::ExitStop
2299 || candidate.family == PineOrderFamily::ExitTrail;
2300 if (!exit || !candidate.legs.target().incarnation || withdrawn(candidate.legs)) return;
2301 // Never at or before the lifecycle's last step: a dormant row a pair
2302 // hold suspended earlier in this callback took the callback's own
2303 // ordinal, and the lifecycle refuses a second, different action there
2304 // (a conflicting replay) as it refuses an older one.
2305 if (const auto& last = candidate.legs.last_action(); last && last->cause.event >= event)
2306 event = last->cause.event + 1;
2307 const exit_legs::Frame cause{event, point->decision.coordinate.interval_index, domain,
2308 exit_legs::Phase::Observation};
2309 const exit_legs::Action action{candidate.legs.target(), candidate.legs.revision(),
2310 cause, exit_legs::Cancel{{exit_legs::Leg::Stop, exit_legs::Leg::Limit,
2311 exit_legs::Leg::Trail}}};
2312 (void)candidate.legs.apply(candidate.legs.target(), action); invalidate_retirement();
2313 };
2314 for (const auto& [incarnation, event] : cancelled) {
2315 const auto found = placement_.find(incarnation);
2316 if (found != placement_.end()) retire_legs(found->second, event);
2317 }
2318 // As in hold_reversal_pair_brackets: no erased lifecycle is read again.
2320 [&](std::uint64_t, const PlacementSnapshot& candidate) {
2321 return lifecycle_readable(candidate);
2322 });
2323 for (auto row : placement_) {
2324 auto& candidate = row.second;
2325 if (id && candidate.source_id != *id) continue;
2326 retire_legs(candidate, live->decision.coordinate.ordinal);
2327 }
2328}
2329
2330void PineExecutionAdapter::purge_brackets_after_applied_reversal(
2331 const PlacementSnapshot& reversal) {
2332 const bool prior_long = reversal.projection_position_side
2333 == static_cast<std::int32_t>(PositionSide::LONG);
2334 std::vector<native_order::RequestHandle> stale;
2335 for (const auto& handle : live_handles_) {
2336 const auto found = placement_.find(handle.incarnation);
2337 if (found == placement_.end()) continue;
2338 const auto& candidate = found->second;
2339 const bool exit = candidate.family == PineOrderFamily::ExitLimit
2340 || candidate.family == PineOrderFamily::ExitStop
2341 || candidate.family == PineOrderFamily::ExitTrail;
2342 const bool stale_margin = candidate.family == PineOrderFamily::Margin
2343 && candidate.projection_position_side == reversal.projection_position_side;
2344 bool targets_prior = false;
2345 if (const auto cohort = cohorts_by_id_.find(candidate.from_entry);
2346 cohort != cohorts_by_id_.end()) {
2347 targets_prior = cohort_opened_on_side(cohort->second, prior_long);
2348 }
2349 if ((exit && targets_prior) || stale_margin) stale.push_back(handle);
2350 }
2351 for (const auto& handle : stale) {
2352 (void)require_host().cancel(handle);
2353 retire(handle);
2354 }
2355 const auto held = cohorts_by_id_.held_rows();
2356 for (auto at = held.begin(); at != held.end();) {
2357 const auto current = at++;
2358 auto& cohort = *current;
2359 // The flip cleared every held cohort before this opening was credited,
2360 // so the reversal's cohort can only hold the new credit.
2361 if (cohort.first == reversal.source_id) continue;
2362 if (cohort_opened_on_side(cohort.second, prior_long)) {
2363 if (position_tail_summary_) {
2364 for (const auto& origin : cohort.second.live_units_by_origin)
2365 position_tail_summary_->retire_origin(origin.first);
2366 position_tail_summary_->live_cohort(&cohort, false);
2367 }
2368 assert(cohort_opened_slot_count_ >= cohort.second.opened.size());
2369 cohort_opened_slot_count_ -= cohort.second.opened.size();
2371 ::pineforge::detail::clear_exposure_storage(cohort.second.live_units_by_origin);
2372 cohorts_by_id_.refresh_held(current);
2373 }
2374 }
2375}
2376
2377void PineExecutionAdapter::revive_brackets_after_margin(
2378 const native_order::ExecutionAppliedEvent& event,
2379 const NativeDecisionContext& context) {
2380 const auto physical = detail::run_position(require_host());
2381 if (physical.signed_units == 0.0) return;
2382 // A same-path declined reversal and its dormant bracket can both precede
2383 // this margin event before the next bar-open receipt sweep. Apply the
2384 // already-recorded rejection at this Applied boundary so the revival sees
2385 // the same lifecycle state as the legacy margin callback.
2386 const auto suspend = [&](const native_order::CommandEvent& command) {
2387 const auto* rejected = std::get_if<native_order::MatchRejectedEvent>(&command);
2388 if (rejected
2389 && rejected->cursor.point.interval_index == context.coordinate.interval_index
2390 && is_declined_market_reversal(*rejected)) {
2391 suspend_declined_reversal_brackets(*rejected);
2392 }
2393 return false;
2394 };
2395 // In place on a PineStrategyHost, as in observe_terminal_receipts.
2396 if (auto* pine = pine_view_of(&require_host())) {
2397 as_native_consumer(pine->execution_consumer())
2398 .visit_commands_after(receipt_cursor_, suspend);
2399 } else {
2400 for (const auto& row : require_host().native_events(receipt_cursor_)) {
2401 if (row.command) suspend(*row.command);
2402 }
2403 }
2404 const auto domain = context.sub_count > 1
2405 ? (config_.calc_on_order_fills ? exit_legs::Domain::MagnifierFillRecalc
2406 : exit_legs::Domain::Magnifier)
2407 : (config_.calc_on_order_fills ? exit_legs::Domain::FillRecalc
2408 : exit_legs::Domain::Ordinary);
2409 const exit_legs::Frame cause{event.ordinal, context.coordinate.interval_index,
2410 domain, exit_legs::Phase::AfterMargin};
2411 std::optional<PlacementSnapshot> marketable;
2412 native_order::RequestHandle marketable_handle{};
2413 // K1 keeps unwithdrawn lifecycle rows for the whole current cycle,
2414 // including global legs and named ids with temporarily zero exposure.
2415 // Superseded legs can leave: K2 keeps a row naming their predecessor;
2416 // K3 keeps the first different-stop re-issue. One retained supersessor
2417 // answers this test; withdrawn and past-cycle legs are never revived.
2419 [&](std::uint64_t, const PlacementSnapshot& candidate) {
2420 return candidate.placement_cycle == current_position_cycle_
2421 && candidate.legs.dormant() && candidate.legs.target().incarnation != 0
2422 && !withdrawn(candidate.legs) && !filled_limit_without_revival(candidate);
2423 });
2424#if PINEFORGE_PLACEMENT_AUDIT
2425 for (const auto& retained : placement_) {
2426 const auto& candidate = retained.second;
2427 if (retained.second.placement_cycle != current_position_cycle_
2428 || !candidate.legs.dormant() || !candidate.legs.target().incarnation
2429 || withdrawn(candidate.legs) || filled_limit_without_revival(candidate)) {
2430 continue;
2431 }
2432 const auto inc = retained.first;
2433 const auto target_inc = candidate.legs.target().incarnation;
2434 const auto supersedes = [&](std::uint64_t incarnation, const PlacementSnapshot& peer) {
2435 return peer.placement_cycle == current_position_cycle_
2436 && ((peer.projection_predecessor != 0
2437 && (peer.projection_predecessor == inc
2438 || peer.projection_predecessor == target_inc))
2439 || (incarnation > inc && peer.family == candidate.family
2440 && peer.source_id == candidate.source_id
2441 && peer.from_entry == candidate.from_entry
2442 && peer.placement_script_open_ms > candidate.placement_script_open_ms
2443 && !same_double_bits(peer.exit_levels.stop,
2444 candidate.exit_levels.stop)));
2445 };
2446 // The test below stops at the first supersessor it finds: an erased
2447 // one only reaches it when no retained row answers first.
2448 const bool answered = std::any_of(placement_.begin(), placement_.end(),
2449 [&](const auto& peer) { return supersedes(peer.first, peer.second); });
2450 if (!answered) PINEFORGE_AUDIT_PLACEMENT_SCAN(placement_, supersedes);
2451 }
2452#endif
2453 PINEFORGE_MARGIN_REVIVAL_OBSERVE(true, placement_, event, context,
2454 physical.signed_units, current_position_cycle_, 0);
2455 for (auto row : placement_) {
2456 // ab9714be pine_orders.cpp:597-608: leg ownership is position-cycle
2457 // scoped, so only a bracket bound to the cycle this margin call is
2458 // settling may be revived; one parked by an earlier cycle stays
2459 // dormant until that cycle re-arms it.
2460 if (row.second.placement_cycle != current_position_cycle_) continue;
2461 auto& candidate = row.second;
2462 const bool exit = candidate.family == PineOrderFamily::ExitLimit
2463 || candidate.family == PineOrderFamily::ExitStop
2464 || candidate.family == PineOrderFamily::ExitTrail;
2465 // A from_entry="" exit the declined reversal held is revived like a
2466 // named bracket: it names every entry, so the whole held position is
2467 // its exposure (lab tv tape tests/fixtures/margin_v6
2468 // w5-rv-global-held, lane W5-ENG-MARGIN-V6).
2469 const bool global = candidate.from_entry.empty();
2470 if (!exit || !candidate.legs.dormant()
2471 || (!global && !(cohort_exposure_for(candidate.from_entry) > 0.0))
2472 || !candidate.legs.target().incarnation) {
2473 continue;
2474 }
2475 // The script cancelled it, live or dormant (retire_cancelled_exits):
2476 // every leg is retired, and a cancelled exit is never revived
2477 // (V19D-P1, the cancelled-origin retirement).
2478 if (withdrawn(candidate.legs)) continue;
2479 // A same-id exit re-issued after the slice REPLACED this bracket: the
2480 // successor carries the cycle's legs, and reviving the superseded parent
2481 // here would fire the finished cycle's already-touched level against the
2482 // new lot (ab9714be pine_fills.cpp:7669-7673).
2483 const auto inc = row.first;
2484 const auto target_inc = candidate.legs.target().incarnation;
2485 bool superseded = false;
2486 for (const auto other : placement_) {
2487 if (other.second.placement_cycle == current_position_cycle_
2488 && other.second.projection_predecessor != 0
2489 && (other.second.projection_predecessor == inc
2490 || other.second.projection_predecessor == target_inc)) {
2491 superseded = true;
2492 break;
2493 }
2494 // The owner keeps ONE pending exit per (id, from_entry): a later
2495 // bar's same-id strategy.exit re-priced it in place, so the older
2496 // stop is gone and only that re-issue is a revival candidate
2497 // (ab9714be pine_fills.cpp:2069-2094 scans the live pending_orders_
2498 // only). A re-issue made over a dormant predecessor carries no
2499 // projection link, so match it by identity: same leg family, a
2500 // later script bar and a different stop. A same-bar re-issue
2501 // still revives the originally armed stop (ab9714be
2502 // pine_fills.cpp:2088-2094); a same-level re-materialization
2503 // changes nothing and keeps the predecessor.
2504 const auto& peer = other.second;
2505 if (other.first > inc && peer.placement_cycle == current_position_cycle_
2506 && peer.family == candidate.family
2507 && peer.source_id == candidate.source_id
2508 && peer.from_entry == candidate.from_entry
2509 && peer.placement_script_open_ms > candidate.placement_script_open_ms
2510 && !same_double_bits(peer.exit_levels.stop, candidate.exit_levels.stop)) {
2511 superseded = true;
2512 break;
2513 }
2514 }
2515 if (superseded) continue;
2516 const double revive_stop = compat::pine::select_margin_revival_stop(candidate.legs);
2517 const exit_legs::Action restore{candidate.legs.target(), candidate.legs.revision(),
2518 cause, exit_legs::Restore{{exit_legs::Leg::Stop, exit_legs::Leg::Limit,
2519 exit_legs::Leg::Trail}}};
2520 const auto restored = candidate.legs.apply(candidate.legs.target(), restore); invalidate_retirement();
2521 if (restored != exit_legs::Result::Applied
2522 && restored != exit_legs::Result::Replay) {
2523 continue;
2524 }
2525 candidate.restored_after_margin = true;
2526 const double held = std::abs(physical.signed_units);
2527 // Full coverage is compared with a tolerance: the slice has already been
2528 // split off the physical lot, so the surviving quantity sits below the
2529 // pre-slice request (ab9714be spells full-position coverage as
2530 // `qty - kQtyEpsilon`, pine_fills.cpp:1618).
2531 const bool full = !std::isfinite(candidate.requested_qty)
2532 ? (!std::isfinite(candidate.qty_percent) || candidate.qty_percent >= 100.0 - 1e-5)
2533 : candidate.requested_qty >= held - 1e-9;
2534 const bool ready = !candidate.leg_activation.bounds()
2535 || candidate.leg_activation.stop_ready(
2536 current_position_cycle_, context.coordinate.interval_index);
2537 const double executable_stop = finite_positive(candidate.exit_levels.stop)
2538 ? candidate.exit_levels.stop : revive_stop;
2539 const bool reaches = std::isfinite(executable_stop)
2540 && (physical.signed_units < 0.0
2541 ? executable_stop <= event.resolved_price
2542 : executable_stop >= event.resolved_price);
2543 if (!marketable && full && ready && reaches) {
2544 marketable = candidate;
2545 marketable_handle = native_order::RequestHandle{
2546 event.handle().run, candidate.legs.target().incarnation};
2547 }
2548 }
2549 PINEFORGE_MARGIN_REVIVAL_OBSERVE(false, placement_, event, context,
2550 physical.signed_units, current_position_cycle_, marketable_handle.incarnation);
2551 if (!marketable) return;
2552 if (marketable_handle.incarnation) {
2553 (void)require_host().cancel(marketable_handle);
2554 retire(marketable_handle);
2555 }
2556 native_order::Request request;
2557 request.intent = native_order::Flatten{};
2558 request.label = marketable->source_id;
2559 request.comment = marketable->comment;
2560 PlacementSnapshot snapshot = *marketable;
2561 snapshot.forced_execution_price = event.resolved_price;
2562 snapshot.requested_qty = std::numeric_limits<double>::quiet_NaN();
2563 snapshot.qty_percent = 100.0;
2564 snapshot.immediately = true;
2565 snapshot.legs = {};
2566 snapshot.leg_activation = {};
2567 snapshot.exit_activation = {};
2568 const auto accepted = submit_or_replace(
2569 std::move(request), std::move(snapshot), false,
2570 "__margin_revival__" + std::to_string(marketable->legs.target().incarnation));
2571 if (accepted) {
2572 (void)require_host().execute_current(
2573 {*accepted, NativeCurrentPriceRule::NearestTick});
2574 }
2575}
2576
2578 position_tail_spill_.reset();
2579 position_tail_summary_ = std::make_shared<detail::AdapterPositionTailSummary>();
2580 exit_scratch_ = nullptr;
2581 pine_view_host_ = nullptr;
2582 // Incarnations start over with the run.
2583 retired_row_scratch_.unsuperseded.clear();
2584 retired_row_scratch_.reissues.clear();
2585 retired_row_scratch_.adopted_reissues.clear();
2586 retired_row_scratch_.reissue_high_water = 0;
2587 retired_row_scratch_.reissue_cycle = -1;
2588 retired_row_scratch_.roster_pass_due = true; retirement_gate_ = {};
2589 if (auto* index = adapter_lookup_index(bound_consumer(), this, run_counter_, false))
2590 index->clear();
2591 admission_journal.reset();
2592 cohort_opened_slot_count_ = 0;
2593 cohorts_by_id_.clear();
2594 pending_scan_work_ = {}; retirement_work_ = {};
2595 placement_.clear();
2596 live_by_source_key_.clear();
2597 clear_bracket_families();
2598 pending_write(pending_bracket_legs_).clear();
2599 pending_write(pending_entries_).clear();
2600 delayed_market_orders_.clear();
2601 deferred_open_marketable_sells_.clear();
2602 throttled_reopen_rearm_.clear();
2603 entry_openings_interval_index_ = -1;
2604 entry_openings_this_interval_ = 0;
2605 pending_write(pending_same_bar_commands_).clear();
2606 pending_write(source_shadow_pending_).clear();
2607 pending_same_bar_close_qty_ = 0.0;
2608 pending_relative_exits_.clear();
2609 anchored_relative_legs_.clear();
2610 anchored_relative_stats_ = {};
2611 anchored_cohort_sequence_ = 0;
2612 per_entry_exits_.clear();
2613 pending_write(pending_coof_requests_).clear();
2614 pending_margin_revivals_.clear();
2615 clear_live();
2616 first_open_newborns_.clear();
2617 dropped_close_count_ = 0;
2618 dropped_close_digest_ = 1469598103934665603ULL;
2619 open_entry_fees_.clear();
2620 trade_exit_phase_.clear(); exit_group_certificate_ = {}; exit_group_work_ = {};
2621 exit_phase_final_ = 0;
2622 exit_phase_digest_ = 1469598103934665603ULL;
2623 exit_rank_keys_.clear();
2624 exit_rank_keys_digest_ = 1469598103934665603ULL;
2625 exit_rank_peak_ = 0;
2626 exit_rank_alt_peak_ = 0;
2627 admission_events_folded_ = 0;
2628 admission_events_last_ = 0;
2629 admission_events_digest_ = 1469598103934665603ULL;
2630 current_debited_applied_ordinals_.clear();
2631 intraday_loss_relabel_ordinals_.clear();
2632 consumed_partial_exit_cycles_.clear();
2633 bracket_shadowed_openings_.clear();
2634 named_entry_cancel_tokens_.clear();
2635 id_bound_exits_.clear();
2636 close_logical_units_.clear();
2637 close_ledger_records_.clear();
2638 pyramiding_records_.clear();
2639 exit_tombstones_.clear();
2640 close_batch_callsites_.clear();
2641 close_batch_bar_ = -1;
2642 close_batch_queue_sequence_ = 0;
2643 close_batch_pending_debt_ = 0.0;
2644 close_batch_admitted_total_ = 0.0;
2645 receipt_cursor_ = 0;
2646 last_applied_ordinal_ = 0;
2647 terminal_receipt_cursor_ = 0;
2648 materializing_relative_ = false;
2649 current_position_cycle_ = 0;
2650 current_position_sign_ = 0;
2651 next_sequential_group_ = 0;
2652 source_batch_mutated_ = false;
2653 coof_recalc_active_ = false;
2654 coof_first_open_ = false;
2655 coof_open_stop_next_waypoint_ = false;
2656 coof_fill_forced_ = false;
2657 coof_market_entry_recalc_incarnation_ = 0;
2658 coof_market_entry_recalc_fill_seq_ = 0;
2659 coof_current_fill_seq_ = 0;
2660 coof_fill_cursor_t_ = kNaN;
2661 coof_fill_target_print_ = kNaN;
2662 coof_context_ = {};
2663 coof_script_bar_ = {};
2664 coof_script_bar_valid_ = false;
2665 pooc_close_basis_last_bar_ = std::numeric_limits<std::int64_t>::min();
2666 pooc_close_basis_count_ = 0;
2667 pooc_close_basis_digest_ = 1469598103934665603ULL;
2668 pooc_open_basis_ = 0.0;
2669 pooc_open_script_bar_ = std::numeric_limits<std::int64_t>::min();
2670 close_all_pending_script_bar_ = std::numeric_limits<std::int64_t>::min();
2671 close_first_script_bar_ = std::numeric_limits<std::int64_t>::min();
2672 close_first_side_ = 0;
2673 last_fx_rate_ = kNaN;
2674 position_open_script_bar_ = std::numeric_limits<std::int64_t>::min();
2675 position_open_epoch_ = 0;
2676 position_open_bar_index_ = -1;
2677 position_open_phase_ = NativePathPhase::None;
2678 position_open_priced_ = false;
2679 last_margin_call_script_bar_ = std::numeric_limits<std::int64_t>::min();
2680 kernel_margin_path_point_ = std::numeric_limits<std::uint64_t>::max();
2681 kernel_margin_resize_point_ = std::numeric_limits<std::uint64_t>::max();
2682 close_margin_point_ = std::numeric_limits<std::uint64_t>::max();
2683 close_margin_cancelled_bar_ = std::numeric_limits<std::int64_t>::min();
2684 close_margin_follow_up_bar_ = std::numeric_limits<std::int64_t>::min();
2685 close_margin_follow_up_units_ = 0.0;
2686 close_margin_open_bar_ = std::numeric_limits<std::int64_t>::min();
2687 owed_follow_up_bar_ = std::numeric_limits<std::int64_t>::min();
2688 owed_follow_up_point_ = -1;
2689 owed_follow_up_units_ = 0.0;
2690 open_exit_first_bar_ = std::numeric_limits<std::int64_t>::min();
2691 open_exit_first_origin_ = 0;
2692 open_exit_first_id_.clear();
2693 open_exit_first_fill_ = 0.0;
2694 open_call_sequence_ = false;
2695 unrepeated_margin_calls_.clear();
2696 close_call_after_script_bar_ = std::numeric_limits<std::int64_t>::min();
2697 close_call_after_script_book_ = 0.0;
2698 close_call_after_script_units_ = 0.0;
2699 waypoint_chain_bar_ = std::numeric_limits<std::int64_t>::min();
2700 pooc_close_checkpoint_deferred_ms_ = std::numeric_limits<std::int64_t>::min();
2701 signal_close_mc_event_bar_ = -1;
2702 signal_close_mc_position_cycle_ = 0;
2703 signal_close_mc_entry_incarnation_ = 0;
2704 signal_close_mc_fill_seq_ = 0;
2705 signal_close_mc_before_qty_ = kNaN;
2706 signal_close_mc_remaining_qty_ = kNaN;
2707 last_margin_call_event_ordinal_ = 0;
2708 last_margin_call_entry_incarnation_ = 0;
2709 last_margin_call_position_cycle_ = 0;
2710 last_margin_call_at_script_close_ = false;
2711 last_margin_call_closed_units_ = 0.0;
2712 last_margin_call_remaining_units_ = 0.0;
2713 source_gains_ = 0.0;
2714 source_losses_ = 0.0;
2715 source_money_folded_ = 0;
2716 risk_coof_direct_script_bar_ = std::numeric_limits<std::int64_t>::min();
2717 cap_latest_fill_ = 0;
2718 day_ledger_ = {};
2719 risk_.observed_peak_equity = kNaN;
2720 risk_.observed_max_drawdown = 0.0;
2721 risk_.intraday_block_day = std::numeric_limits<std::int64_t>::min();
2722 risk_.intraday_cancel_pending = false;
2723 policy_script_bar_ = {};
2724 policy_script_bar_valid_ = false;
2725 market_pyramid_adds_.clear();
2726 trail_state_at_open_.clear();
2727 carried_trail_retunes_.clear();
2728 stream_mode_ = false;
2729 bar_magnifier_ = false;
2730 short_seed_ = {};
2731 pending_short_seed_ = {};
2732 short_seed_long_candidate_ = {};
2733 last_bar_dual_entry_path_ = 0;
2734 last_bar_dual_entry_script_open_ms_ =
2735 std::numeric_limits<std::int64_t>::min();
2736 source_sequence_ = 0;
2737 command_ordinal_ = 0;
2738 broker_open_epoch_ = 0;
2739 last_broker_open_ms_ = std::numeric_limits<std::int64_t>::min();
2740 source_command_sequence_ = 0;
2741 entry_attempt_bar_ = -1;
2742 entry_attempts_on_bar_ = 0;
2743 cap.reset_run();
2744 refresh_pending_view();
2745}
2746
2747// The configuration's conjuncts of the margin and admission scopes, formed
2748// once per configuration rather than at every ask: each scope function
2749// answers false whenever its flag is false, and reads the rest of its scope
2750// as before. The process-wide switches stay read at every ask. The host sets
2751// the configuration again after every script calculation, so this part is
2752// plain reads of config_ against the staged facts set_staged_configuration
2753// formed.
2754// `config` is the configuration being set (read before it is copied in).
2755[[gnu::always_inline]] inline void
2756PineExecutionAdapter::refresh_configuration_scopes(const PineStrategyConfig& config) noexcept {
2757 const bool commission_free = config.commission_value == 0.0;
2758 const bool one_lot_cap = config.pyramiding >= 0 && config.pyramiding <= 1;
2759 gain_loss_configured_ = commission_free && staged_scopes_.unit_money && lot_grid_pinned_;
2760 slipped_long_configured_ = config.margin_long == 100.0 && commission_free
2761 && config.slippage > 0 && staged_scopes_.unit_grid && one_lot_cap
2762 && staged_scopes_.unit_money;
2763 unified_configured_ = lot_grid_pinned_ && !config.calc_on_order_fills && commission_free
2764 && one_lot_cap && staged_scopes_.unit_money && staged_scopes_.mintick;
2765 ledger_records_configured_ = config.pyramiding > 1 && !config.close_entries_rule_any
2766 && lot_grid_pinned_;
2767 pooc_fee_configured_ = config.process_orders_on_close
2768 && config.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
2769 && config.commission_value > 0.0
2770 && config.commission_type == static_cast<int>(CommissionType::PERCENT)
2771 && staged_scopes_.positive_grid && staged_scopes_.unit_point_money;
2772}
2773
2775 refresh_configuration_scopes(config);
2776 config_ = config;
2777}
2779 staged_ = staged;
2780 const auto& grid = staged_.quantity_grid;
2781 lot_grid_pinned_ = grid && *grid > 0.0 && decimal_grid_exponent(*grid).has_value();
2782 staged_scopes_.positive_grid = grid && *grid > 0.0;
2783 staged_scopes_.unit_grid = grid && *grid > 0.0 && *grid <= 1.0;
2784 staged_scopes_.unit_point_money = staged_.syminfo.pointvalue == 1.0
2785 && staged_.account_fx_effective_from_ms.empty();
2786 staged_scopes_.unit_money = staged_scopes_.unit_point_money && staged_.account_fx == 1.0;
2787 staged_scopes_.mintick = finite_positive(staged_.syminfo.mintick);
2788 refresh_configuration_scopes(config_);
2789}
2790void PineExecutionAdapter::set_begin_mode(bool is_stream, bool bar_magnifier) noexcept {
2791 stream_mode_ = is_stream;
2792 bar_magnifier_ = bar_magnifier;
2793}
2794
2796 switch (field) {
2797 case NativeRunSpecField::InstrumentFeedInstrument: return "instrument_feed_instrument";
2798 case NativeRunSpecField::InstrumentFeedTimeframe: return "instrument_feed_timeframe";
2799 case NativeRunSpecField::InstrumentFeedBars: return "instrument_feed_bars";
2800 case NativeRunSpecField::InstrumentFeedClose: return "instrument_feed_close";
2801 case NativeRunSpecField::InstrumentFeedColumns: return "instrument_feed_columns";
2802 case NativeRunSpecField::SubscriptionInstrument: return "subscription_instrument";
2803 default: return nullptr;
2804 }
2805}
2806
2808 using F = NativeRunSpecField;
2809 const char* symbol = nullptr;
2810 const char* setting = nullptr;
2811 const char* option = nullptr;
2812 const char* feed = instrument_feed_field_name(field);
2813 switch (field) {
2814 case F::Ticker: symbol = "ticker"; break;
2815 case F::TickerId: symbol = "ticker_id"; break;
2816 case F::Type: symbol = "type"; break;
2817 case F::Currency: symbol = "currency"; break;
2818 case F::BaseCurrency: symbol = "base_currency"; break;
2819 case F::Description: symbol = "description"; break;
2820 case F::VolumeType: symbol = "volume_type"; break;
2821 case F::Timezone: symbol = "timezone"; break;
2822 case F::Session: symbol = "session"; break;
2823 case F::ChartTimezone: symbol = "chart_timezone"; break;
2824 case F::PointValue: symbol = "point_value"; break;
2825 case F::AccountFx: symbol = "account_fx"; break;
2826 case F::PriceTick: symbol = "price_tick"; break;
2827 case F::QuantityGrid:
2829 case F::InitialCapital: setting = "initial_capital"; break;
2830 case F::SlippageTicks: setting = "slippage_ticks"; break;
2831 case F::FeeKind: setting = "fee_kind"; break;
2832 case F::FeeValue: setting = "fee_value"; break;
2833 case F::CloseExecution: setting = "close_execution"; break;
2834 case F::MaxAbsUnits: setting = "max_abs_units"; break;
2835 case F::MaxOpenLots: setting = "max_open_lots"; break;
2836 case F::AllowedOpenDirections: setting = "allowed_open_directions"; break;
2837 case F::InitialMarginFraction: setting = "initial_margin_fraction"; break;
2838 case F::PriceGrid: setting = "price_grid"; break;
2839 case F::GridRounding: setting = "grid_rounding"; break;
2840 case F::MarginModel: setting = "margin_model"; break;
2841 case F::MarginInitial: setting = "margin_initial"; break;
2842 case F::MarginMaintenance: setting = "margin_maintenance"; break;
2843 case F::MarginSizing: setting = "margin_sizing"; break;
2844 case F::MarginShortfallMultiple: setting = "margin_shortfall_multiple"; break;
2845 case F::MarginMinUnits: setting = "margin_min_units"; break;
2846 case F::MarginCheck: setting = "margin_check"; break;
2847 case F::MarginEquityBasis: setting = "margin_equity_basis"; break;
2848 case F::MarginLevelBase: setting = "margin_level_base"; break;
2849 case F::Calculation: setting = "calculation"; break;
2850 case F::OpenBarView: setting = "open_bar_view"; break;
2851 case F::RiskLimits: setting = "risk_limits"; break;
2852 case F::RiskDrawdown: setting = "risk_drawdown"; break;
2853 case F::RiskIntradayLoss: setting = "risk_intraday_loss"; break;
2854 case F::RiskLossDays: setting = "risk_loss_days"; break;
2855 case F::RiskFillsPerDay: setting = "risk_fills_per_day"; break;
2856 case F::RiskDayBasis: setting = "risk_day_basis"; break;
2857 case F::RiskAction: setting = "risk_action"; break;
2858 case F::QuantityTolerance: setting = "quantity_tolerance"; break;
2859 case F::InputTimeframe: option = "input_timeframe"; break;
2860 case F::ScriptTimeframe: option = "script_timeframe"; break;
2861 case F::IntrabarTimeframe: option = "intrabar_timeframe"; break;
2862 case F::IntrabarSamples: option = "intrabar_samples"; break;
2863 case F::IntrabarDistribution: option = "intrabar_distribution"; break;
2864 case F::IntrabarVolumeSamples: option = "intrabar_volume_samples"; break;
2865 case F::IntrabarSampleEligibility: option = "intrabar_sample_eligibility"; break;
2866 case F::PathOrder: option = "path_order"; break;
2867 // The other symbols' feeds the harness installed (strategy_set_symbol_feed):
2868 // symbol_feeds_refused, named by instrument_feed_field_name above.
2869 case F::InstrumentFeedInstrument:
2870 case F::InstrumentFeedTimeframe:
2871 case F::InstrumentFeedBars:
2872 case F::InstrumentFeedClose:
2873 case F::InstrumentFeedColumns:
2874 case F::SubscriptionInstrument:
2875 break;
2876 // The fields the adapter builds itself: a refusal there is our bug.
2877 case F::None:
2878 case F::SessionKey:
2879 case F::RunNumber:
2880 case F::AbortReporting:
2881 case F::TimeframeUndetected:
2882 case F::SlotLabelPolicy:
2883 case F::LegacyTolerance:
2884 case F::ReportPolicy:
2885 case F::SubscriptionTimeframe:
2886 case F::SubscriptionBars:
2887 case F::AuxiliaryFeedTimeframe:
2888 case F::AuxiliaryFeedBars:
2889 case F::SubscriptionSource:
2890 case F::EventRetention:
2891 break;
2892 }
2893 try {
2894 if (symbol) {
2896 {{"field", symbol}});
2897 }
2898 if (setting) {
2900 {{"field", setting}});
2901 }
2902 if (option) {
2904 {{"option", option}});
2905 }
2906 if (feed) {
2908 {{"reason", "kernel_refused_series"}, {"field", feed}});
2909 }
2910 } catch (const std::bad_alloc&) {
2912 }
2914}
2915
2917 const StagedConfiguration& staged,
2918 const NativeBeginArgs& args,
2919 NativePathOrder path_order,
2920 std::vector<NativeInstrumentFeed>* instrument_feeds) const {
2921 NativeRunSpec spec;
2922 if (run_counter_ == std::numeric_limits<std::uint64_t>::max()) {
2924 "Pine native run counter exhausted");
2925 }
2926 const std::string timezone = staged.syminfo.timezone.empty() ? "UTC" : staged.syminfo.timezone;
2927 const std::string session = staged.syminfo.session.empty() ? "24x7" : staged.syminfo.session;
2928 // Bind a stable identity only to staged session/timezone facts; labels and
2929 // source ids never cross this generic identity boundary.
2930 spec.identity = {session + "@" + timezone, ++run_counter_};
2931
2932 // A11: sub-two-bar public starts retain the explicit undetected state and
2933 // intentionally leave both string fields empty.
2934 // A historical one-bar run has no detectable timeframe, but a stream
2935 // begin carries an explicit provider timeframe even when its warmup has
2936 // only one bar. Preserve that public stream contract rather than
2937 // erasing the caller's labels into the undetected batch shape.
2938 // The simple begin has no timeframe argument to preserve when fewer than
2939 // two bars cannot establish one. A TF-aware public begin is explicit even
2940 // for one historical bar, and follows the legacy run_tf_impl path.
2941 spec.timeframe_undetected = args.n < 2 && !args.is_stream
2942 && args.input_tf.empty() && args.script_tf.empty();
2943 if (!spec.timeframe_undetected) {
2944 std::string effective_input = args.input_tf;
2945 if (effective_input.empty() && args.n >= 2 && args.bars != nullptr) {
2946 effective_input = detect_timeframe(args.bars, args.n);
2947 }
2948 spec.input_tf = std::move(effective_input);
2949 spec.script_tf = args.script_tf.empty() ? spec.input_tf : args.script_tf;
2950 // Legacy run_tf_impl used tf_ratio(), for which spelling aliases such
2951 // as D and 1D are passthrough. When the input spelling was detected
2952 // (and therefore was not an explicit public argument), retain the
2953 // caller's equal-duration script spelling so the generic calendar
2954 // also takes its raw-label passthrough path.
2955 if (args.input_tf.empty() && !spec.input_tf.empty() && !spec.script_tf.empty()
2956 && tf_ratio(spec.input_tf, spec.script_tf) == 1) {
2957 spec.input_tf = spec.script_tf;
2958 }
2959 }
2960 spec.ticker = staged.syminfo.ticker;
2961 spec.tickerid = staged.syminfo.tickerid;
2962 spec.type = staged.syminfo.type;
2963 spec.currency = staged.syminfo.currency;
2964 spec.basecurrency = staged.syminfo.basecurrency;
2965 spec.description = staged.syminfo.description;
2966 spec.volumetype = staged.syminfo.volumetype;
2967 spec.timezone = timezone;
2968 spec.session = session;
2969 spec.chart_timezone = staged.chart_timezone;
2970 spec.initial_capital = config.initial_capital;
2971 spec.point_value = staged.syminfo.pointvalue;
2972 spec.account_fx = staged.account_fx;
2973 spec.price_tick = staged.syminfo.mintick;
2974 spec.slippage_ticks = config.slippage < 0 ? 0U : static_cast<std::uint32_t>(config.slippage);
2975 spec.fee_kind = fee_kind_for(config.commission_type);
2976 spec.fee_value = config.commission_value;
2977 spec.quantity_grid = staged.quantity_grid;
2978 // Lane XSYM-D: another symbol's bars a request site reads are the
2979 // kernel's instrument feeds, merged by interval there. A run that
2980 // installs none leaves the field empty, which folds nothing. They are the
2981 // host's own, moved in rather than copied (AR-R4); a throw below, the
2982 // validation at the end included, moves them back.
2983 struct FeedsBack {
2984 std::vector<NativeInstrumentFeed>* home;
2985 NativeRunSpec& spec;
2986 int pending = std::uncaught_exceptions();
2987 ~FeedsBack() {
2988 if (home != nullptr && std::uncaught_exceptions() > pending)
2989 *home = std::exchange(spec.instrument_feeds, {});
2990 }
2991 } feeds_back{instrument_feeds, spec};
2992 if (instrument_feeds != nullptr) spec.instrument_feeds = std::exchange(*instrument_feeds, {});
2993 // A13: source hosts opt into the generic tolerant batch ingress.
2994 // Native-only hosts retain the strict Canonical/None defaults.
2995 spec.slot_label_policy = NativeSlotLabelPolicy::FeedTolerant;
2996 spec.legacy_tolerance = NativeFeedTolerance::BatchStructuralBars;
2997 if (args.is_stream) {
2998 // A36: legacy stream warmups permit zero-valued interim OHLC bars;
2999 // the final close is checked by the stream preflight boundary.
3000 spec.legacy_tolerance = static_cast<NativeFeedTolerance>(
3001 static_cast<std::uint32_t>(spec.legacy_tolerance)
3002 | static_cast<std::uint32_t>(NativeFeedTolerance::WarmupNonNegativeOHLC));
3003 }
3004 spec.path_order = path_order;
3006 ? NativeCloseExecution::AfterCalculation : NativeCloseExecution::NextEligiblePoint;
3007 // R6: calc_on_order_fills IS the kernel's fill-triggered cadence. The
3008 // consumer drives one recalculation at each applied execution's cursor
3009 // from its own notification drain (A.4 chronology), delivered to
3010 // PineStrategyHost::on_native_recalculate with reason OrderFill; Pine
3011 // keeps only its language-state rollback, its waypoint deferral and this
3012 // guard literal. The open-bar view stays Complete: TradingView's COOF
3013 // callback reads the whole script bar (CT4).
3014 if (config.calc_on_order_fills) {
3015 spec.calculation = NativeCalculationTrigger::BarCloseAndFills;
3017 }
3018 // Pine's request_abort surface reports a cooperative cancellation through
3019 // status, not through last_error(). Native-only hosts retain Error.
3020 spec.abort_reporting = NativeAbortReporting::Quiet;
3021 // RP3/RP9 (L2): the kernel records the equity curve and its extremes; the
3022 // source host keeps only the Pine cadence that marks where the points
3023 // fall (pine_strategy_host.cpp scheduler_mark_report_point). TradingView's
3024 // range-end report — which re-marks the curve's last point and re-folds
3025 // every extreme from it (pine_strategy_host.cpp scheduler_record_range_end)
3026 // — is report shape, not a mark-to-market row, so the kernel's own
3027 // range-end producer stays off and report_open_position_at_end with it.
3028 //
3029 // R5 audit lane Q6 measured what that costs. The ROWS are no longer
3030 // duplicated: scheduler_record_range_end calls the kernel's generic
3031 // producer (NativeExecutionConsumer::append_open_position_report_rows),
3032 // and tests/test_adapter_range_end_relower.cpp asserts both sides get
3033 // the identical rows out of it (RE1 pnl=500/300 with no fee; RE2
3034 // pnl=479.5 comm=20.5 and 279.30000000000001/20.700000000000003 at
3035 // 0.1 %). What this policy value keeps out is the SHAPE, and each piece
3036 // is a named divergence in docs/design/native-feature-parity.md §3.7:
3037 // the equity re-mark off the NET row P&L (RE2: the curve's last point
3038 // reads eq=100758.8 op=0 here against the kernel's eq=100800 op=800 —
3039 // 41.2 apart, the round-trip commission of both lots), the extreme
3040 // re-fold that follows it (RE3: maxru=1758.8000000000029 against 1800),
3041 // the same-bar bracket re-sort, and three marks on the terminal bar
3042 // against the kernel's one at run end. RE4, the flat control, agrees on
3043 // both sides, so every divergence above belongs to the range end.
3044 spec.report_policy = NativeReportPolicy::KernelRecordedAtHostMarks;
3045 // Contract P6: Pine pyramiding is adapter command policy. A resting source
3046 // entry must not consume a generic physical-lot cap before it fills, so
3047 // the projected native spec deliberately leaves max_open_lots unbounded.
3048 spec.allowed_open_directions = directions_for(risk_.direction);
3049 // Pine's frozen default sizing admits against its signal-time tuple. The
3050 // generic initial-margin gate only sees the later fill-time FX rate, so
3051 // source admission is reproduced in validate_precommit instead: every
3052 // opening verdict below is AdmitWithHostMargin, which is what keeps the
3053 // kernel's own initial-margin requirement out of the CANDIDATE decision.
3054 // The declared model is maintenance-only (zero per-side initial, below),
3055 // which is what keeps it out of L3b's PLACEMENT decision, where a
3056 // candidate verdict does not exist yet. The two together are the whole
3057 // statement "the source owns opening admission".
3058 // R5: the broker model itself is the kernel's. TradingView's two margin
3059 // percents ARE the maintenance fractions (strategy(margin_long=,
3060 // margin_short=) is the fraction of the position's value the account must
3061 // keep, and the emulator liquidates the moment it cannot), the check is
3062 // made at the remaining path's adverse mark and rests there, and the
3063 // whole model is present exactly when the call is enabled -- which is how
3064 // set_margin_call_enabled() keeps its C-ABI semantics without a second
3065 // switch. The money rules TradingView layers on top (its equity basis,
3066 // its ten-significant-digit requirement, its lot-floored 4x slice with
3067 // the whole-drop band, its scheduling) are host policy and live in the
3068 // three hooks below, never in this spec.
3069 const double margin_long_fraction = config.margin_long / 100.0;
3070 const double margin_short_fraction = config.margin_short / 100.0;
3071 if (source_margin_call_enabled_
3072 && std::isfinite(margin_long_fraction) && margin_long_fraction > 0.0
3073 && std::isfinite(margin_short_fraction) && margin_short_fraction > 0.0) {
3074 NativeMarginModel margin;
3075 // Wave-4 ruling: opening admission and liquidation are two different
3076 // broker functions, and this adapter takes only the second one from
3077 // the kernel. Its own pre-trade check is TradingView's
3078 // ten-significant-digit money admission against the signal-time
3079 // tuple, answered as AdmitWithHostMargin at every opening candidate
3080 // (validate_precommit below), so the model is declared
3081 // MAINTENANCE-ONLY: zero per-side initial means the kernel enforces
3082 // no opening requirement -- at the candidate gate and at L3b's
3083 // placement gate alike -- while the maintenance fractions keep the
3084 // kernel's liquidation. A positive initial here would re-run the
3085 // kernel's raw-double requirement over a quantity the source's money
3086 // rule has already admitted, and decline openings TradingView takes.
3087 margin.initial_long = 0.0;
3088 margin.initial_short = 0.0;
3089 margin.maintenance_long = margin_long_fraction;
3090 margin.maintenance_short = margin_short_fraction;
3091 // No sizing knob (R5 N11): resolve_margin_call_units answers every
3092 // call, so sizing / shortfall_multiple / liquidation_min_units would
3093 // be set only to be shadowed, and TradingView's slice (restore
3094 // lot-floored BEFORE the 4x, floored again, one-contract whole-drop
3095 // band) is no generic policy; check_adapter_spec_shadowing.py guards.
3096 // The slice belongs to the adverse waypoint it was measured at, not to
3097 // a solved level -- and a 1x long, whose slope solves no level at all,
3098 // still has to be checked.
3099 margin.check = NativeLiquidationCheck::PathAdverseExtremeMark;
3100 // TradingView's margin equity charges only a PERCENT entry commission
3101 // against the account; a cash or per-contract entry fee is added back
3102 // (percent_commission_live_equity).
3103 margin.basis =
3104 (config.commission_type == static_cast<int>(CommissionType::PERCENT)
3105 && config.commission_value > 0.0)
3106 ? NativeMarginEquityBasis::MarkedEquity
3107 : NativeMarginEquityBasis::MarkedEquityBeforeOpenCommission;
3108 // margin_liquidation_price() reads this level: closed money alone.
3109 margin.level_base = NativeLiquidationLevelBase::RealizedOnly;
3110 // TradingView's own liquidation ticket. The adapter's host classifies
3111 // a row as MARGIN_CALL from exactly this exit id (the kernel/source boundary extraction, 2.ii l:
3112 // PineStrategyHost::on_native_applied states execution::CloseCause,
3113 // which pineforge.h pins as `3`), so the ticket the kernel books has
3114 // to be this one.
3115 margin.liquidation_label = "__margin_call__";
3116 margin.liquidation_comment = "Margin call";
3117 spec.margin = margin;
3118 }
3119 if (host_ != nullptr)
3120 NativeExecutionConsumer::bound(*host_).stage_lower_path_source(nullptr);
3121 if (args.bar_magnifier) {
3122 // pine_scheduler.cpp:894-899/:1035-1041 supplied the legacy
3123 // volume-weighted bound. The native API's generic default remains
3124 // 64; the source provider owns this policy projection.
3125 const int volume_weighted_cap =
3126 legacy_volume_weighted_max_samples(args.magnifier_samples);
3127 const bool synthesized = spec.timeframe_undetected || spec.input_tf == spec.script_tf;
3128 if (synthesized) {
3129 IntrabarPath::synthesized path;
3130 path.samples = args.magnifier_samples;
3131 path.distribution = args.magnifier_distribution;
3132 path.volume_weighted = args.magnifier_volume_weighted;
3133 path.volume_weighted_min_samples = args.magnifier_volume_weighted_min_samples;
3134 path.volume_weighted_max_samples = volume_weighted_cap;
3135 spec.intrabar.value = std::move(path);
3136 } else {
3137 // A18: a genuinely finer supplied feed uses a lower-timeframe
3138 // path. The generic validator owns duration and
3139 // divisibility rejection for any non-finer malformed pairing.
3140 // TradingView's magnifier walks its own intrabars at its table's
3141 // timeframe, not the feed: on a 15-minute chart 2-minute bars,
3142 // each owned by the chart bar holding its last minute, between
3143 // the chart bar's own open and close (the TradingView intrabar import,
3144 // tests/fixtures/magnifier_intrabars). The path is built from the
3145 // feed and stays on its grid, so its literal is the input's.
3146 IntrabarPath::lower_tf path;
3147 if (host_ != nullptr) {
3148 NativeExecutionConsumer::bound(*host_).stage_lower_path_source(
3149 make_magnifier_path_source(args.bars, args.n, spec.input_tf,
3150 spec.script_tf, spec.session, spec.timezone));
3151 } else if (args.bars && args.n > 0) {
3152 // Standalone projection remains a value-only reference path.
3153 path.bars = tradingview_magnifier_bars(
3154 args.bars, args.n, spec.input_tf, spec.script_tf, spec.session,
3155 spec.timezone);
3156 }
3157 path.tf = spec.input_tf;
3158 path.samples = args.magnifier_samples;
3159 path.distribution = args.magnifier_distribution;
3160 path.volume_weighted = args.magnifier_volume_weighted;
3161 path.volume_weighted_min_samples = args.magnifier_volume_weighted_min_samples;
3162 path.volume_weighted_max_samples = volume_weighted_cap;
3163 // Real lower-timeframe bars already carry the legacy four
3164 // turning points. Preserve the source broker's continuous
3165 // segment crossing over those points: a stop reached between the
3166 // open and an endpoint fills at its level, while synthesized
3167 // paths below retain the sampled one-price/gap semantics.
3169 spec.intrabar.value = std::move(path);
3170 }
3171 } else if (!spec.timeframe_undetected
3172 && (args.magnifier_samples != 4
3174 // Legacy TF-aware callers accept inactive sampler arguments. Preserve
3175 // that source-surface shape without relaxing the strict native public
3176 // API: an empty lower path is already defined to fall back to the
3177 // caller's confirmed script-bar path at delivery.
3178 IntrabarPath::lower_tf inert;
3179 inert.tf = spec.input_tf;
3180 inert.samples = 4;
3181 inert.distribution = MagnifierDistribution::ENDPOINTS;
3182 inert.volume_weighted = false;
3183 inert.volume_weighted_min_samples = 2;
3184 inert.volume_weighted_max_samples = 64;
3185 inert.sample_eligibility = IntrabarPath::SampleEligibility::ContinuousSegments;
3186 spec.intrabar.value = std::move(inert);
3187 }
3188 // The adapter reads the command journal above its own receipt cursor and
3189 // nowhere else, and acknowledges the cursor as it moves
3190 // (observe_terminal_receipts), so it keeps only the unread window: the
3191 // kernel default, which folds nothing into the spec digest.
3192 spec.event_retention = NativeEventRetention::Window;
3193 const auto validation = host_ != nullptr
3194 ? NativeExecutionConsumer::bound(*host_).normalize_projected_spec(spec)
3196 if (!validation) {
3197 throw coded<std::logic_error>(run_spec_field_failure(validation.field),
3198 "Pine adapter produced invalid native run spec field "
3199 + std::to_string(static_cast<unsigned>(validation.field)));
3200 }
3201 // Projection owns only the standalone exit service. Trade and dependency
3202 // workspaces are installed once by the admitted run's consumer/host setup.
3203 const CacheDefaults defaults;
3204 const auto& cache_policy = host_ ? host_->cache_policy_for_run() : defaults;
3205 auto exit_scratch = detail::make_exit_scratch(spec.identity, cache_policy.projection_exit_pages);
3206 exit_scratch_ = std::move(exit_scratch);
3207 return spec;
3208}
3209
3210std::uint64_t PineExecutionAdapter::key_for(const SourceId& left, const SourceId& right) const noexcept {
3211 return source_key(left, right);
3212}
3213
3214native_order::CohortHandle PineExecutionAdapter::cohort_for(const SourceId& id) {
3215 // Read index bytes before find can spill and before the host issues a handle.
3216 cohorts_by_id_.require_insert(id);
3217 auto found = cohorts_by_id_.find(id);
3218 if (found != cohorts_by_id_.end()) return found->second.handle;
3219 CohortFacts facts;
3220 cohorts_by_id_.admit(id, facts.spillable());
3221 facts.handle = require_host().cohort_open();
3222 if (facts.handle.value == 0) {
3224 "native cohort allocation refused");
3225 }
3226 const auto result = facts.handle;
3227 cohorts_by_id_.place(id, std::move(facts));
3228 return result;
3229}
3230PineSizingSnapshot PineExecutionAdapter::sizing_snapshot() const {
3231 PineSizingSnapshot snapshot;
3232 const auto& host = require_host();
3233 if (const auto point = detail::callback_point(host)) {
3234 // The source broker captures its signal tuple at the tick-built close,
3235 // never at the raw sub-tick callback print. Preserve that one basis
3236 // for frozen sizing, money-band admission and the paired FX fact.
3237 snapshot.mark = nearest_tick(point->price, staged_.syminfo.mintick);
3238 snapshot.price = snapshot.mark;
3239 mark_sizing_equity(snapshot, snapshot.mark);
3240 }
3241 snapshot.fx = staged_.account_fx;
3242 if (const auto point = detail::callback_point(host))
3243 snapshot.fx = active_staged_fx(point->decision.sub_bar_open_ms);
3244 return snapshot;
3245}
3246
3247// The money a default-quantity declaration converts. CASH is the declared
3248// value itself; a percentage is taken of the equity the snapshot marked, and
3249// that money -- not the equity it is a percentage of -- goes on the
3250// ten-significant-digit money grid whenever the instrument has a lot grid
3251// (lab tv tapes tests/fixtures/margin_residual w5b-sz-eur-p50-10m and -p33-10m:
3252// on ten million, whose equity carries more decimals than ten digits keep,
3253// rounding the equity first floors 44 of the 504 quantities at 50 % and 37 at
3254// 33 % one lot off TradingView's, and no rounding at all 8 and 18; at 100 %
3255// the two roundings are one number, w5b-sz-eur-p100-10m-0410, where no
3256// rounding misses 98 of 130; lane W5B-ENG-MARGIN-RESIDUAL). Which equity that
3257// is (ab9714be pine_fills.cpp:1411-1426, restated in
3258// percent_commission_live_equity) and this rounding are source policy the
3259// generic EquityFraction basis deliberately does not model, so the source
3260// hands the core the money and the core converts it. Any other declaration
3261// has no money of its own and returns NaN.
3262//
3263// Under a cash commission the percentage is taken of strategy.equity -- every
3264// open entry fee charged, the cash fees percent_commission_live_equity
3265// restores included -- and the fee the order itself pays is left out of the
3266// money: the value per order, or the value per unit, so that the units' notional
3267// and their fee together spend the money (units = pct * E / (price * pointvalue
3268// + fee)). The cash-sizing fee correction, `lab tv` tapes tests/fixtures/cash_fee_sizing:
3269// every sized entry of the thirteen tapes there (percentages 50, 100 and 200,
3270// long and short, flat, pyramided, after a partial close, reversing, priced
3271// entries and strategy.order, point values 1 and 50) and all 366 trades of
3272// corpus order-percent-equity-cash-commission-01.
3273double PineExecutionAdapter::default_sizing_cash(
3274 const PineSizingSnapshot& sizing) const {
3275 if (config_.default_qty_type == static_cast<int>(QtyType::CASH)) {
3276 if (!std::isfinite(config_.default_qty_value)) {
3277 engine_command_value_invalid_or_count("sizing.quantity", EngineValueStage::sizing,
3278 EngineValueReason::nonfinite);
3279 }
3280 return config_.default_qty_value;
3281 }
3282 if (config_.default_qty_type != static_cast<int>(QtyType::PERCENT_OF_EQUITY)) return kNaN;
3283 if (default_sizing_reserves_cash_fee()) {
3284 if (!finite_positive(sizing.strategy_equity)) {
3285 if (!std::isfinite(sizing.strategy_equity)) {
3286 engine_command_value_invalid_or_count("sizing.basis", EngineValueStage::sizing,
3287 EngineValueReason::nonfinite);
3288 }
3289 return kNaN;
3290 }
3291 const double equity = staged_.quantity_grid ? source_money_round(sizing.strategy_equity)
3292 : sizing.strategy_equity;
3293 const double money = config_.default_qty_value / 100.0 * equity;
3294 if (!std::isfinite(money)) {
3295 engine_command_value_invalid_or_count("sizing.quantity", EngineValueStage::sizing,
3296 EngineValueReason::nonfinite);
3297 }
3298 if (config_.commission_type == static_cast<int>(CommissionType::CASH_PER_ORDER)) {
3299 const double cash = money - config_.commission_value;
3300 if (!std::isfinite(cash)) {
3301 engine_command_value_invalid_or_count("sizing.quantity", EngineValueStage::sizing,
3302 EngineValueReason::nonfinite);
3303 }
3304 return cash;
3305 }
3306 // The core converts cash / unit_cost; this cash leaves
3307 // units * (unit_cost + fee) == money.
3308 const double unit_cost = sizing.price * staged_.syminfo.pointvalue * sizing.fx;
3309 if (!finite_positive(unit_cost)) {
3310 if (!std::isfinite(unit_cost)) {
3311 engine_command_value_invalid_or_count("sizing.quantity", EngineValueStage::sizing,
3312 EngineValueReason::nonfinite);
3313 }
3314 return kNaN;
3315 }
3316 const double divisor = unit_cost + config_.commission_value;
3317 if (!std::isfinite(divisor)) {
3318 engine_command_value_invalid_or_count("sizing.quantity", EngineValueStage::sizing,
3319 EngineValueReason::nonfinite);
3320 }
3321 const double cash = money * unit_cost / divisor;
3322 if (!std::isfinite(cash)) {
3323 engine_command_value_invalid_or_count("sizing.quantity", EngineValueStage::sizing,
3324 EngineValueReason::nonfinite);
3325 }
3326 return cash;
3327 }
3328 if (!finite_positive(sizing.equity)) {
3329 if (!std::isfinite(sizing.equity)) {
3330 engine_command_value_invalid_or_count("sizing.basis", EngineValueStage::sizing,
3331 EngineValueReason::nonfinite);
3332 }
3333 return kNaN;
3334 }
3335 const double money = config_.default_qty_value / 100.0 * sizing.equity;
3336 const double cash = staged_.quantity_grid ? source_money_round(money) : money;
3337 if (!std::isfinite(money) || !std::isfinite(cash)) {
3338 engine_command_value_invalid_or_count("sizing.quantity", EngineValueStage::sizing,
3339 EngineValueReason::nonfinite);
3340 }
3341 return cash;
3342}
3343
3344// A percentage declaration reserves a percentage commission out of its own
3345// money before converting; a cash declaration does not. The divisor is the
3346// exact inverse of the charge, which is what native_order::Sized spells as
3347// reserve_percent_fee.
3348bool PineExecutionAdapter::default_sizing_reserves_percent_fee() const noexcept {
3349 return config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
3350 && config_.commission_type == static_cast<int>(CommissionType::PERCENT)
3351 && config_.commission_value > 0.0;
3352}
3353
3354bool PineExecutionAdapter::default_sizing_reserves_cash_fee() const noexcept {
3355 return config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
3356 && (config_.commission_type == static_cast<int>(CommissionType::CASH_PER_ORDER)
3357 || config_.commission_type == static_cast<int>(CommissionType::CASH_PER_CONTRACT))
3358 && config_.commission_value > 0.0;
3359}
3360
3361// The source lot floor. The two declarations do not share one: a percentage
3362// quantity uses the money floor with its cent-grid case, a cash quantity the
3363// ordinary grid helper with its proportional epsilon. Neither is the core's
3364// generic "largest grid multiple at or below the quotient", so the core hands
3365// back the raw quotient (ExecutionGridPolicy::ExplicitUnits) and this runs on
3366// top of it in resolve_terms.
3367double PineExecutionAdapter::default_sizing_lot_floor(double units) const noexcept {
3368 if (config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
3369 && staged_.quantity_grid) {
3370 // Margin rule 1, pinned fee-free at point value 1 without an FX
3371 // series: the quotient is the one division sig10(E) / close.
3372 if (detail::margin_rule_switches().decimal_sizing
3373 && !default_sizing_reserves_percent_fee() && !default_sizing_reserves_cash_fee()
3374 && staged_.syminfo.pointvalue == 1.0 && staged_.account_fx == 1.0
3375 && staged_.account_fx_effective_from_ms.empty()) {
3376 if (const auto floored = decimal_floor_lot(units, *staged_.quantity_grid))
3377 return *floored;
3378 }
3379 return source_money_floor_lot(units, staged_.quantity_grid);
3380 }
3381 return floor_quantity_grid(units, staged_.quantity_grid);
3382}
3383
3384// The core intent a default-quantity declaration spells: the source money as
3385// the CashValue basis, the core's fee reserve, the raw quotient for the source
3386// lot floor, frozen at acceptance against the signal rule. No money, no shape.
3387std::optional<native_order::Sized> PineExecutionAdapter::default_sizing_shape(
3388 const PineSizingSnapshot& sizing) const {
3389 const double cash = default_sizing_cash(sizing);
3390 if (!finite_positive(cash)) return std::nullopt;
3391 native_order::Sized sized;
3392 sized.basis = native_order::CashValue{cash};
3393 sized.time = native_order::SizeTime::AtAcceptance;
3394 sized.price = native_order::SizePrice::SignalOnTick;
3395 sized.grid_policy = native_order::ExecutionGridPolicy::ExplicitUnits;
3396 sized.reserve_percent_fee = default_sizing_reserves_percent_fee();
3397 return sized;
3398}
3399
3400// The placement-time default quantity: the source's money band and
3401// affordability gates consume it before any request exists (entry(): "The TV
3402// money band is a source policy"), so the source asks the core for its own
3403// conversion and floors it -- the arithmetic exists once (R5 N11).
3404double PineExecutionAdapter::default_sizing_units(const PineSizingSnapshot& sizing) const {
3405 if (!finite_positive(sizing.price) || !finite_positive(sizing.fx)) {
3406 if (!std::isfinite(sizing.price) || !std::isfinite(sizing.fx)) {
3407 engine_command_value_invalid_or_count("sizing.quantity", EngineValueStage::sizing,
3408 EngineValueReason::nonfinite);
3409 }
3410 return 0.0;
3411 }
3412 const auto sized = default_sizing_shape(sizing);
3413 if (!sized) return 0.0;
3414 const auto quotient = require_host().native_sized_units(
3415 *sized, sizing.price, sizing.equity, sizing.fx);
3416 return quotient ? default_sizing_lot_floor(*quotient) : 0.0;
3417}
3418
3419// A typed quantity's units at `price`: the core converts the money the source
3420// chose, and reserves a percentage fee out of a percentage's money, exactly as
3421// for a default quantity; the source floors the quotient onto its grid, as
3422// ab9714be calc_qty_for_type (pine_orders.cpp:130-170) did for both types.
3423double PineExecutionAdapter::typed_quantity_units(double money, bool percent, double price,
3424 double fx) const {
3425 native_order::Sized sized;
3426 sized.basis = native_order::CashValue{money};
3427 sized.grid_policy = native_order::ExecutionGridPolicy::ExplicitUnits;
3428 sized.reserve_percent_fee = percent
3429 && config_.commission_type == static_cast<int>(CommissionType::PERCENT)
3430 && config_.commission_value > 0.0;
3431 return floor_quantity_grid(
3432 require_host().native_sized_units(sized, price, 0.0, fx).value_or(0.0),
3433 staged_.quantity_grid);
3434}
3435
3436// What a typed entry opens when it fills at `price`: its cash, or its
3437// percentage of the source's sizing equity -- of the hypothetical Flatten's
3438// realized balance when it was placed as a reversal (ab9714be
3439// pine_orders.cpp:96-191: the old opening fee and this close's fee are
3440// realized once, before the new opening's percentage fee is reserved).
3441std::optional<double> PineExecutionAdapter::typed_entry_units(
3442 const PlacementSnapshot& source, double price, double fx,
3443 native_order::RequestHandle target) const {
3444 const double qty = source.requested_qty;
3445 if (source.qty_type == static_cast<int>(QtyType::CASH))
3446 return typed_quantity_units(qty, false, price, fx);
3447 if (source.qty_type != static_cast<int>(QtyType::PERCENT_OF_EQUITY)) return qty;
3448 if (source.terms_priced_reverse) {
3449 const auto* pine_host = pine_view_of(&require_host());
3450 if (!pine_host) return std::nullopt;
3451 const auto projection = pine_host->project_native_settlement_at(
3452 execution::Flatten{},
3453 execution::Fill{price, source.source_id, source.comment, target.incarnation}, fx);
3454 return typed_quantity_units(projection.realized_balance * qty / 100.0, true, price, fx);
3455 }
3456 return typed_quantity_units(percent_commission_live_equity(price, fx) * qty / 100.0, true,
3457 price, fx);
3458}
3459
3460std::optional<native_order::Sized> PineExecutionAdapter::default_sizing_intent(
3461 const PineSizingSnapshot& sizing, bool is_long) const {
3462 // The core converts at the run's own point value; whether it also converts
3463 // at the source's FX rate is decided where it accepts the command
3464 // (core_sizing_price_matches), so a staged FX series keeps this shape.
3465 if (!finite_positive(staged_.account_fx)
3466 || !finite_positive(sizing.price) || !finite_positive(staged_.syminfo.pointvalue)
3467 || !finite_positive(staged_.syminfo.mintick)) {
3468 return std::nullopt;
3469 }
3470 auto sized = default_sizing_shape(sizing);
3471 if (sized) sized->side = is_long ? native_order::Side::Long : native_order::Side::Short;
3472 return sized;
3473}
3474
3475// The sizing price of a default-sized MARKET entry: the signal mark carried
3476// to the expected fill by the side's slippage ticks and re-snapped onto the
3477// chart tick (ab9714be pine_strategy_commands.cpp:325-328). This is the rule
3478// native_order::SizePrice::SignalOnTick names generically.
3479double PineExecutionAdapter::default_market_sizing_price(
3480 double mark, bool is_long) const noexcept {
3481 const double tick = staged_.syminfo.mintick;
3482 return nearest_tick(mark + (is_long ? 1.0 : -1.0) * config_.slippage * tick, tick);
3483}
3484
3485// POOC_FEE_SIZING (switch pooc_fee_sizing): under process_orders_on_close a
3486// default percent-of-equity MARKET order is sized on its slipped tick-built
3487// execution price, and with a percentage commission on that price grossed up
3488// by the fee on the ten-digit money grid: unit = sig10(exec x (1 + fee)),
3489// Q = the decimal lot floor of sig10(E) / unit. A BTC reversal at a close of
3490// 85253.54 with a 0.05 % fee buys 1.13834 on sig10(85296.15676499999) =
3491// 85296.15676, where the raw product buys 1.13833 (tests/fixtures/
3492// margin_call_rules fee-sizing tapes); at fee 0 the unit is exec itself. The
3493// order keeps this quantity to its fill (host-sized, not the core's quotient).
3494// Out of line: inlined into its one caller, it widened that per-bar
3495// function's fetch footprint (CMakeLists.txt, -falign-functions).
3496[[gnu::noinline]]
3497std::optional<double> PineExecutionAdapter::pooc_fee_units(
3498 const PineSizingSnapshot& sizing) const {
3499 const auto& rules = detail::margin_rule_switches();
3500 if (!rules.pooc_fee_sizing || !config_.process_orders_on_close
3501 || config_.default_qty_type != static_cast<int>(QtyType::PERCENT_OF_EQUITY)
3502 || !(config_.commission_value > 0.0)
3503 || config_.commission_type != static_cast<int>(CommissionType::PERCENT)
3504 || !staged_.quantity_grid || !(*staged_.quantity_grid > 0.0)
3505 || staged_.syminfo.pointvalue != 1.0 || sizing.fx != 1.0
3506 || !staged_.account_fx_effective_from_ms.empty() || !finite_positive(sizing.price)
3507 || !std::isfinite(sizing.equity)) {
3508 return std::nullopt;
3509 }
3510 const double money = config_.default_qty_value / 100.0 * sizing.equity;
3511 const double cash = source_money_round(money);
3512 const double unit = tv_pooc_fee_grossed_unit(sizing.price);
3513 if (!finite_positive(cash) || !finite_positive(unit)) return std::nullopt;
3514 const double quotient = cash / unit;
3515 if (rules.decimal_sizing) {
3516 if (const auto floored = decimal_floor_lot(quotient, *staged_.quantity_grid))
3517 return *floored;
3518 }
3519 return source_money_floor_lot(quotient, staged_.quantity_grid);
3520}
3521
3522// TradingView's process_orders_on_close fee-grossed sizing unit: the slipped
3523// execution price grossed up by the percentage commission on the ten-digit
3524// money grid, sig10(exec x (1 + c / 100)). Pinned by the fee-sizing tapes
3525// under tests/fixtures/margin_call_rules (fee-sizing/, 13 tapes; the
3526// factorial/c1r3-fee0-* controls show no rounding at commission 0): a BTC
3527// reversal at a close of 85253.54 with a 0.05 % fee buys 1.13834 on
3528// sig10(85296.15676499999) = 85296.15676, where the raw product buys 1.13833.
3529// It is not the core's reserve divisor -- the quotient divides the ten-digit
3530// cash by this rounded unit -- and it is the one place the adapter spells the
3531// fee factor: guard (e) of tests/test_adapter_sizing_relower.cpp allows the
3532// spelling inside this function alone, by an explicit exception.
3533double PineExecutionAdapter::tv_pooc_fee_grossed_unit(double price) const {
3534 double unit = source_money_round(price * (1.0 + config_.commission_value / 100.0));
3535#ifdef PINEFORGE_VALUE_INVALID_FIXTURES
3536 unit = fixture_adapter_execution_value("fee.adapter.grossed_unit", unit);
3537#endif
3538 if (std::isfinite(price) && !std::isfinite(unit)) {
3539 engine_command_value_invalid_or_count("fee.commission", EngineValueStage::fee,
3540 EngineValueReason::numeric_overflow);
3541 }
3542 return unit;
3543}
3544
3546 PineSizingSnapshot sizing = sizing_snapshot();
3547 if (!finite_positive(sizing.mark)) return false;
3548 sizing.price = default_market_sizing_price(sizing.mark, is_long);
3549#ifdef PINEFORGE_VALUE_INVALID_FIXTURES
3550 sizing.price = fixture_adapter_execution_value("slippage.default.core", sizing.price);
3551#endif
3552 check_slippage_fill_price(sizing.price, std::isfinite(sizing.mark));
3553 mark_sizing_equity(sizing, sizing.mark);
3554 return finite_positive(default_sizing_units(sizing))
3555 && default_sizing_intent(sizing, is_long).has_value()
3556 && core_sizing_price_matches(sizing, is_long);
3557}
3558
3559bool PineExecutionAdapter::core_sizing_price_matches(
3560 const PineSizingSnapshot& sizing, bool is_long) const {
3561 const double tick = staged_.syminfo.mintick;
3562 if (!finite_positive(tick) || !finite_positive(sizing.price)) return false;
3563 // The core freezes at its own live frame, never at a retained one a replay
3564 // is reading, so this prediction reads the live point.
3565 const auto point = detail::live_callback_point(require_host());
3566 if (!point || !finite_positive(point->price)) return false;
3567 // The core freezes the quotient at the FX of the acceptance coordinate (a
3568 // script calculation's coordinate is the next bar's open); the source
3569 // sized at its sub-bar open's. They differ only across a curve step.
3570 if (active_staged_fx(point->decision.coordinate.effective_time_ms) != sizing.fx) return false;
3571 // SizePrice::SignalOnTick in the core's own arithmetic -- its nearest tick
3572 // is std::round with ties away from zero, not this file's
3573 // floor(v / tick + 0.5) -- so this is a comparison against the core and
3574 // not a restatement of the source rule. The two agree on every price a
3575 // tick ladder can present; where they would not, the command keeps its
3576 // host-resolved intent and its own frozen quantity.
3577 const auto on_tick = [tick](double value) { return std::round(value / tick) * tick; };
3578 const double ticks = config_.slippage < 0 ? 0.0 : static_cast<double>(config_.slippage);
3579 const double slipped = is_long ? on_tick(point->price) + ticks * tick
3580 : on_tick(point->price) - ticks * tick;
3581 return on_tick(slipped) == sizing.price;
3582}
3583
3584bool PineExecutionAdapter::same_bar_market_tx_scope() const {
3585 return config_.commission_value == 0.0 && config_.pyramiding <= 1
3586 && !config_.calc_on_order_fills && same_point_pair_scope();
3587}
3588
3589bool PineExecutionAdapter::fixed_unmargined_market_batch_scope() const {
3590 return config_.pyramiding >= 2
3591 && config_.default_qty_type == static_cast<int>(QtyType::FIXED)
3592 && config_.margin_long == 0.0 && config_.margin_short == 0.0
3593 && config_.commission_value == 0.0 && !config_.calc_on_order_fills
3594 && same_point_pair_scope();
3595}
3596
3597// The flat-entry pair handling: where the flat-pair rules of the entry/reversal fill ordering hold
3598// -- the order at one fill point, the later call's transaction of its own
3599// quantity plus the pending market's, and that transaction's admission. The
3600// zero-cost batch above is its subset; TradingView books a commissioned pair
3601// the same way, the admission leaving the commission out (lab tv w6-f10i,
3602// w6-f10j, w6b-p1b/p1c/p1d), a pair under any pyramiding, same-side lots by
3603// rank too (w6-f10c, w6b-p2a/p2b/p2c), and a pair under calc_on_order_fills
3604// placed outside a fill recalculation (w6-f10f, w6b-p4a/p4b;
3605// tests/fixtures/same_point_entries).
3606bool PineExecutionAdapter::same_point_pair_scope() const {
3607 const bool all_in_percent = config_.default_qty_type
3608 == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
3609 && config_.default_qty_value >= 100.0;
3610 const bool fixed_default = config_.default_qty_type == static_cast<int>(QtyType::FIXED);
3611 const bool variable_default = config_.default_qty_type == static_cast<int>(QtyType::CASH)
3612 || (config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
3613 && config_.default_qty_value < 100.0);
3614 const bool variable_short_seed = variable_default && short_seed_context_is_live();
3615 if (!host_ || config_.process_orders_on_close
3616 || coof_recalc_active_ || config_.close_entries_rule_any || all_in_percent
3617 || (!fixed_default && !variable_short_seed)
3618 || config_.slippage != 0
3619 || risk_.direction != 0 || risk_.max_cons_loss_days != 0
3620 || risk_.max_drawdown > 0.0 || risk_.max_intraday_loss > 0.0
3621 || risk_.max_position_size > 0.0 || risk_.halted || cap.active()) {
3622 return false;
3623 }
3624 const auto state = detail::run_state(require_host());
3625 const auto* inert = state.spec ? state.spec->intrabar.lower() : nullptr;
3626 const auto views = detail::run_consumer(require_host()).bar_views();
3627 const bool inactive_sampler_path = inert != nullptr && inert->bars.empty()
3628 && (views.lower_path == nullptr || views.lower_path->count() == 0)
3629 && inert->tf == state.spec->input_tf && inert->samples == 4
3630 && inert->distribution == MagnifierDistribution::ENDPOINTS
3631 && !inert->volume_weighted && inert->volume_weighted_min_samples == 2
3632 && inert->volume_weighted_max_samples == 64
3633 && inert->sample_eligibility == IntrabarPath::SampleEligibility::ContinuousSegments;
3634 return state.spec != nullptr
3635 && (state.spec->intrabar.is_none() || inactive_sampler_path);
3636}
3637
3638// Entry-family triggers only: every trailing shape the source language can
3639// ask for is built in exit(), on the kernel's own Trail / TrailTicks
3640// spelling, so this helper never carried a live trail case.
3641native_order::Trigger PineExecutionAdapter::trigger_for(double limit_price,
3642 double stop_price) const {
3643 if (price_present(limit_price) && price_present(stop_price))
3644 return native_order::StopLimit{stop_price, limit_price};
3645 if (price_present(limit_price)) return native_order::Limit{limit_price};
3646 if (price_present(stop_price)) return native_order::Stop{stop_price};
3647 return native_order::Market{};
3648}
3649
3650native_order::Group PineExecutionAdapter::group_for(const std::string& name, int type,
3651 std::int64_t cohort) const {
3652 if (name.empty() || type == 0) return native_order::NoGroup{};
3653 const auto group = fnv_string(name);
3654 return native_order::Member{group == 0 ? 1 : group, cohort,
3655 type == 1 ? native_order::GroupEffect::Cancel : native_order::GroupEffect::Reduce};
3656}
3657
3658void PineExecutionAdapter::remember(const native_order::RequestHandle& handle,
3659 PlacementSnapshot&& snapshot) {
3660 snapshot.filled_exit = false;
3661 // Most rows arrive above the high water. An anchored leg can be adopted
3662 // later: K3 must compare that new peer against old (even resolved)
3663 // candidates too. A wholesale rewrite invalidates the immutable-field
3664 // memo rather than trusting its former identity/stop answer.
3665 if (placement_.contains(handle.incarnation)) {
3666 retired_row_scratch_.reissues.clear();
3667 retired_row_scratch_.reissue_high_water = 0;
3668 } else if (handle.incarnation <= retired_row_scratch_.reissue_high_water) {
3669 retired_row_scratch_.adopted_reissues.push_back(handle.incarnation);
3670 }
3671 if (snapshot.reservation_growth_owner_incarnation != 0
3672 && snapshot.reservation_growth_owner_incarnation != handle.incarnation) {
3673 snapshot.reservation_growth_source.assign_capture(
3674 handle.incarnation, snapshot.reservation_growth_owner_incarnation);
3675 snapshot.pooc_global_full_exit_bound_add = true;
3676 }
3677 initialize_l4c_policy(snapshot, handle);
3678 // The chain's oldest source sequence: the predecessor's, while it is
3679 // retained (a queued command's predecessor is a root of
3680 // erase_retired_rows).
3681 // A re-issue made once its entry exists does not continue a void call's
3682 // chain: its place in the exit queue is its own (exit_queue_rank).
3683 if (snapshot.projection_predecessor == 0) {
3684 snapshot.chain_origin_sequence = snapshot.source_sequence;
3685 } else if (placement_.contains(snapshot.projection_predecessor)) {
3686 const auto& predecessor = placement_.at(snapshot.projection_predecessor);
3687 snapshot.chain_origin_sequence = predecessor.void_issue && !snapshot.void_issue
3688 ? snapshot.source_sequence : predecessor.chain_origin_sequence;
3689 } else if (snapshot.chain_origin_sequence == 0) {
3690 snapshot.chain_origin_sequence = snapshot.source_sequence;
3691 }
3692 // Otherwise the snapshot is a copy of a row that named the same
3693 // predecessor, now erased, and carries that row's chain origin: the one
3694 // a walk through the predecessor reaches.
3695 // Every accepted request receives a fresh incarnation. Construct its
3696 // immutable placement evidence directly in the hash table rather than
3697 // default-constructing a string-bearing snapshot and move-assigning it.
3698 // The insertion and live-family accounting share one mutation boundary.
3699 // A new placement cannot already have a live handle: erasure keeps every
3700 // live placement. Re-adoption / replacement still checks the old handle.
3701 remember_live(handle, std::move(snapshot));
3702 // A close order is a transient key of the entry-id table while it waits
3703 // (global_exit_child_ranks).
3704 const auto family = placement_.at(handle.incarnation).family;
3705 if (family == PineOrderFamily::Close || family == PineOrderFamily::CloseAll
3706 || family == PineOrderFamily::Order) {
3707 note_exit_rank_transients();
3708 }
3709 update_l4c_priority();
3710 refresh_pending_view();
3711}
3712
3713void PineExecutionAdapter::retire(native_order::RequestHandle handle) noexcept {
3714 retired_row_scratch_.roster_pass_due = true;
3715 forget_live(handle);
3716 first_open_newborns_.erase(std::remove(first_open_newborns_.begin(),
3717 first_open_newborns_.end(), handle), first_open_newborns_.end());
3718 for (auto it = live_by_source_key_.begin(); it != live_by_source_key_.end();) {
3719 if (it->second == handle) it = live_by_source_key_.erase(it); else ++it;
3720 }
3721 if (pending_short_seed_.ready && (handle == pending_short_seed_.plan.long_entry
3722 || handle == pending_short_seed_.plan.materialize_long
3723 || handle == pending_short_seed_.plan.final_short)) {
3724 pending_short_seed_ = {};
3725 }
3726 if (handle == short_seed_long_candidate_) short_seed_long_candidate_ = {};
3727 if (short_seed_.active && (handle == short_seed_.long_entry
3728 || handle == short_seed_.materialize_long || handle == short_seed_.final_short)) {
3729 short_seed_.active = false;
3730 }
3731 refresh_pending_view();
3732}
3733
3734namespace detail {
3735namespace {
3736// Process-wide, not thread_local: a strategy module is dlopen'd, and every
3737// thread_local it reads per bar is a dynamic TLS lookup.
3738std::atomic<bool> retain_rows{false};
3739std::atomic<std::uint64_t> rows_erased{0};
3740} // namespace
3741void set_retain_retired_rows(bool retain) noexcept {
3742 retain_rows.store(retain, std::memory_order_relaxed);
3743}
3744bool retain_retired_rows() noexcept { return retain_rows.load(std::memory_order_relaxed); }
3745namespace {
3746// The handle-preserving reissue change: process-wide like retain_rows, and for the same reason.
3747std::atomic<bool> carry_bindings{true};
3748} // namespace
3749void set_carry_reissue_bindings(bool carry) noexcept {
3750 carry_bindings.store(carry, std::memory_order_relaxed);
3751}
3752bool carry_reissue_bindings() noexcept {
3753 return carry_bindings.load(std::memory_order_relaxed);
3754}
3755std::uint64_t retired_rows_erased() noexcept { return rows_erased.load(std::memory_order_relaxed); }
3756} // namespace detail
3757
3758// Audit mirror of K1's cycle and oldest dormant-representative pins.
3759// A named id's momentary exposure does not shorten the cycle pin.
3760bool PineExecutionAdapter::lifecycle_readable(const PlacementSnapshot& row) const noexcept {
3761 if (row.legs.target().incarnation == 0 || filled_limit_without_revival(row)) return false;
3762 const std::uint64_t origin = row.bracket_origin.incarnation;
3763 bool askable = false;
3764 for (const auto& handle : live_handles_) if (handle.incarnation == origin) askable = true;
3765 for (const auto& cohort : cohorts_by_id_.held_rows())
3766 for (const auto& opening : cohort.second.opened) if (opening.incarnation == origin) askable = true;
3767 if ((row.placement_cycle >= current_position_cycle_ && !withdrawn(row.legs))
3768 || (askable && !row.legs.dormant())) return true;
3769 if (!askable || !row.legs.dormant() || (row.family != PineOrderFamily::ExitLimit
3770 && row.family != PineOrderFamily::ExitStop && row.family != PineOrderFamily::ExitTrail)) return false;
3771 for (auto peer : placement_) {
3772 const auto& pin = peer.second;
3773 if (!pin.legs.dormant() || pin.legs.target().incarnation == 0 || pin.family != row.family
3774 || pin.bracket_origin != row.bracket_origin || pin.source_id != row.source_id
3775 || pin.from_entry != row.from_entry) continue;
3776 return &pin == &row;
3777 }
3778 return false;
3779}
3780
3781// The retired-row compaction: which placement rows can go.
3782//
3783// retire() takes a request out of the live collections when it stops
3784// working; its row used to stay for the rest of the run. Here, at every bar
3785// open -- the start of an outermost native callback, after
3786// observe_terminal_receipts() has consumed every command event up to the
3787// high water (so the terminal receipt of every retired request has been
3788// observed, and no applied notification is still queued: the kernel drains
3789// them when the callback that caused them returns) -- each row whose request
3790// is not live is erased unless some reader can still reach it. The readers,
3791// by how they reach a row:
3792// - by a live handle (live_handles_, live_by_source_key_,
3793// first_open_newborns_, the precommit/terms target, a list the same call
3794// built from those): a live row is never erased;
3795// - by a kernel event (on_applied, observe_terminal_receipts, the commands
3796// visited after the receipt cursor): consumed before the sweep;
3797// - by a handle another collection holds: the roots below keep the row;
3798// - by a whole-table scan: a row is kept while some scan could still accept
3799// it (the pins below), or the scan reads a compact record instead
3800// (PlacementTable: the per-id sequence minima, the non-margin source
3801// ids; BracketRoster: the consumed legs; CohortFacts: the sides of the
3802// erased openings; PlacementSnapshot::chain_origin_sequence).
3803// Roots: the cohorts' opened lists and the host's open lots (an opening
3804// while it holds units); every handle a queued command's snapshot or request
3805// names (bracket origin, paired reversal parent, close-all preservation,
3806// predecessor, lifecycle target, reservation owner, wait/bind owner); the
3807// anchored relative legs and the parent being materialized; the short-seed
3808// plans.
3809// Pins, one per scan that reads rows of requests that are no longer live:
3810// K1 an unwithdrawn lifecycle for the whole current cycle, including global
3811// legs and named ids whose exposure may return; askable origins also keep
3812// non-dormant rows and one dormant representative per family. Margin revival,
3813// reversal suspension and pair hold read lifecycles; exit() asks whether
3814// an opened origin's leg was consumed;
3815// K2 a current-cycle row naming a revival candidate as its predecessor,
3816// K3 the first later-bar current-cycle exit of the same family, source id
3817// and from-entry id with different stop bits: the revival's superseded
3818// test asks whether ANY such re-issue exists, and a row's family, ids, placement bar and stop
3819// never change, so the lowest-incarnation one answers for every later
3820// one (pinning them all kept two rows per exit per bar through a flat
3821// cycle that re-issues its exits every bar, each walked at every bar
3822// open);
3823// K4 an immediate strategy.close placed on this bar or later
3824// (immediate_close_placed_on asks about the current bar);
3825// K5 an entry or raw order placed on the previous bar or later (the open
3826// admission counts the previous bar's entry commands);
3827// K7 an entry of a sequential group that still has a live or queued member
3828// (resolve_terms pairs the group's ranks);
3829// K8 an opening flat stop entry whose placement bar a live or queued entry
3830// shares, or placed on this bar or later (the flat dual-stop test of
3831// resolve_terms and on_applied).
3832// K1 leaves a leg of an ended cycle bound to an origin that can never open
3833// again: the revival never picks an ended cycle, and origin_leg_consumed only
3834// asks about opened origins, so its lifecycle is read by nobody.
3835//
3836// Nor does K1 keep a dormant leg bound to an askable origin,
3837// or an origin-free current-cycle leg, once the superseded test answers -- some
3838// row of the cycle names it (or its target) as its predecessor, or a later-bar
3839// exit of the same family and ids has a different stop. K2 and K3 keep one such row for as long
3840// as the leg is a candidate, so a leg superseded now stays superseded for the
3841// rest of its cycle: the revival never picks it again, and the suspension and
3842// the pair hold only rewrite its own lifecycle, which nothing else reads. A
3843// cycle that cancels and re-places its exits every bar at a moving level no
3844// longer holds a row per bar.
3845//
3846// The cancelled-origin retirement: nor a leg the script withdrew (every leg retired, by
3847// strategy.cancel or strategy.cancel_all whether it was live or dormant:
3848// retire_cancelled_exits), whatever its cycle, unless it is bound to an
3849// askable origin as its family's one representative. A withdrawn leg is no candidate
3850// and K2/K3 keep nothing for it; the suspension and the pair hold only
3851// rewrite its own lifecycle, which only origin_leg_consumed reads, and only
3852// for an origin that is live or opened. A cycle that cancels and re-places an
3853// exit every bar at the same level -- no re-issue at another stop supersedes
3854// it -- no longer holds a row per bar either.
3855//
3856// O1W: a terminally filled named origin-free limit without any stop/loss/trail
3857// or inherited replacement definition is not a revival candidate. K2, K3 and
3858// the ordinary roots still keep it when another reader needs its identity.
3859// Global filled limits retain K1: they can supersede global dormant legs that
3860// are outside K3's named candidate population.
3861void PineExecutionAdapter::sweep_retired_rows(const NativeDecisionContext& context, RetirementWork& work) {
3862 // The previous bars' trades are final: their exit phases fold once.
3863 const std::size_t phase_base = detail::run_consumer(require_host()).closed_trade_prefix().base;
3864 if (exit_phase_final_ < phase_base)
3865 throw coded<std::logic_error>(RunFailureCode::engine_invariant, {},
3866 "exit-phase fold precedes the sealed prefix");
3867 const auto finish_phases = [&] {
3868 for (; exit_phase_final_ < phase_base + trade_exit_phase_.size(); ++exit_phase_final_) {
3869 exit_phase_digest_ = fold_exit_phase(exit_phase_digest_,
3870 trade_exit_phase_.at(exit_phase_final_ - phase_base));
3871 }
3872 detail::run_consumer(require_host()).finish_exit_phase_restart();
3873 };
3874 const bool retain_rows = detail::retain_retired_rows(); RetirementObservation observation(*this, work);
3875 // Every live handle has a row; nothing beyond them means nothing retired.
3876 if (placement_.size() <= live_handles_.size()) { finish_phases(); return; }
3877 const int interval = context.coordinate.interval_index;
3878 const std::int64_t cycle = current_position_cycle_;
3879 ++work.placement_sweeps;
3880 auto& sweep = retired_row_scratch_;
3881 sweep.clear();
3882 const auto contains = [](const std::vector<std::uint64_t>& sorted, std::uint64_t value) {
3883 return std::binary_search(sorted.begin(), sorted.end(), value);
3884 };
3885 const auto exit_family = [](PineOrderFamily family) {
3886 return family == PineOrderFamily::ExitLimit || family == PineOrderFamily::ExitStop
3887 || family == PineOrderFamily::ExitTrail;
3888 };
3889 // The origins a live leg or an opened entry answers for (K1, and the
3890 // consumed-leg facts pruned below).
3891 auto& askable_origins = sweep.askable_origins;
3892 for (const auto& handle : live_handles_) askable_origins.push_back(handle.incarnation);
3893 for (const auto& cohort : cohorts_by_id_.held_rows()) {
3894 for (const auto& opening : cohort.second.opened)
3895 askable_origins.push_back(opening.incarnation);
3896 }
3897 std::sort(askable_origins.begin(), askable_origins.end());
3898 askable_origins.erase(std::unique(askable_origins.begin(), askable_origins.end()),
3899 askable_origins.end());
3900 // One dormant origin pin per (origin, exit id, from-entry, family), in oldest
3901 // incarnation order. Keep its lifecycle so suspension/cancellation still
3902 // changes the consumed-leg answer; do not replace it with a sticky bit.
3903 auto& origin_pins = sweep.origin_pins;
3904 placement_.visit_rows([&](std::uint64_t, const PlacementSnapshot& value) { ++work.row_visits; ++work.pin_rows; observe_retirement_expiry(value, interval);
3905 if (!exit_family(value.family) || !value.legs.dormant()
3906 || value.legs.target().incarnation == 0
3907 || !contains(askable_origins, value.bracket_origin.incarnation)) return false;
3908 const bool represented = std::any_of(origin_pins.begin(), origin_pins.end(),
3909 [&](const PlacementSnapshot* pin) {
3910 ++work.pin_comparisons; return pin->bracket_origin == value.bracket_origin && pin->family == value.family
3911 && pin->source_id == value.source_id && pin->from_entry == value.from_entry;
3912 });
3913 if (!represented) origin_pins.push_back(&value);
3914 return false;
3915 });
3916 const auto pinned_by_itself = [&](const PlacementSnapshot& value) {
3917 // K1: preserve unwithdrawn lifecycle rows for their entire cycle.
3918 // Global legs need no named exposure; an empty named cohort may reopen.
3919 // Exposure at this sweep cannot decide what a later margin call reads.
3920 // Supersession below still releases legs the revival can no longer use.
3921 const bool askable = contains(askable_origins, value.bracket_origin.incarnation);
3922 if (value.legs.target().incarnation != 0 && !filled_limit_without_revival(value)
3923 && ((value.placement_cycle >= cycle && !withdrawn(value.legs))
3924 || (askable && !value.legs.dormant())
3925 || std::find_if(origin_pins.begin(), origin_pins.end(), [&](const auto* pin) { ++work.pin_comparisons; return pin == &value; }) != origin_pins.end())) return true;
3926 // K4
3927 if (value.family == PineOrderFamily::Close && value.immediately
3928 && value.projection_created_bar >= interval) {
3929 return true;
3930 }
3931 // K5: last_broker_open_ms_ is still the prior script bar's open (on_bar_open updates it after this sweep).
3932 return (value.family == PineOrderFamily::Entry || value.family == PineOrderFamily::Order)
3933 && value.placement_script_open_ms >= last_broker_open_ms_;
3934 };
3935 // An askable-origin leg, dormant or not, stays pinned for its cycle as
3936 // v1.6.3's K1 pinned it: releasing a dormant one let the sweep erase its
3937 // supersessor while K3 kept the released leg as a witness, and the margin
3938 // revival then restored the superseded leg.
3939 const auto held_by_cycle = [&](const PlacementSnapshot& value) {
3940 return pinned_by_itself(value) && value.placement_cycle == cycle
3941 && exit_family(value.family) && !value.from_entry.empty()
3942 && !withdrawn(value.legs)
3943 && !contains(askable_origins, value.bracket_origin.incarnation);
3944 };
3945 auto& held = sweep.held;
3946 bool unpinned = false;
3947 // K5 ages in script-bar space even when a chart gathers finer input;
3948 // only the current and previous script bars keep their retired commands.
3949 auto askable_cursor = askable_origins.begin();
3950 placement_.visit_rows([&](std::uint64_t incarnation, const PlacementSnapshot& value) { ++work.row_visits; ++work.gate_rows;
3951 const bool cycle_held = held_by_cycle(value);
3952 if (!cycle_held && pinned_by_itself(value)) return false;
3953 while (askable_cursor != askable_origins.end() && *askable_cursor < incarnation)
3954 ++askable_cursor;
3955 if (askable_cursor != askable_origins.end() && *askable_cursor == incarnation)
3956 return false;
3957 if (cycle_held) {
3958 held.push_back({incarnation, &value});
3959 return false;
3960 }
3961 unpinned = true;
3962 return true;
3963 });
3964 // The revival's exact superseded test, over retained rows. Supersessors
3965 // are newer than their named leg/target, so start above the oldest name
3966 // or the memo's previously read high water.
3967 auto& released = sweep.released;
3968 auto& memo = sweep.unsuperseded;
3969 const auto release_superseded = [&] {
3970 if (held.empty()) return;
3971 const std::uint64_t top = placement_.high_water();
3972 std::uint64_t scan_from = std::numeric_limits<std::uint64_t>::max();
3973 std::size_t cursor = 0;
3974 for (auto& leg : held) {
3975 while (cursor < memo.size() && memo[cursor].first < leg.incarnation) ++cursor;
3976 if (cursor < memo.size() && memo[cursor].first == leg.incarnation) {
3977 leg.read_through = memo[cursor].second;
3978 } else {
3979 const std::uint64_t target = leg.row->legs.target().incarnation;
3980 leg.read_through = std::min(leg.incarnation, target) - 1;
3981 }
3982 scan_from = std::min(scan_from, leg.read_through);
3983 }
3984 // Every leg was read through the high water, and no row has been
3985 // placed since: nothing new to read, and the memo stands.
3986 if (scan_from >= top) return;
3987 const auto stop_bits = [](double value) {
3988 std::uint64_t out = 0;
3989 std::memcpy(&out, &value, sizeof(out));
3990 return out;
3991 };
3992 const PlacementTable& rows = placement_;
3993 for (auto it = rows.upper_bound(scan_from); it != rows.end(); ++it) {
3994 const PlacementSnapshot& peer = it->second; ++work.row_visits; ++work.peer_visits;
3995 if (peer.placement_cycle != cycle) continue;
3996 const bool reissue = exit_family(peer.family);
3997 const std::uint64_t named = peer.projection_predecessor;
3998 if (named == 0 && !reissue) continue;
3999 for (auto& leg : held) { ++work.held_comparisons;
4000 if (leg.released || it->first <= leg.read_through) continue;
4001 const PlacementSnapshot& own = *leg.row;
4002 if (named != 0
4003 && (named == leg.incarnation || named == own.legs.target().incarnation)) {
4004 leg.released = true;
4005 } else if (reissue && it->first > leg.incarnation && peer.family == own.family
4006 && peer.placement_script_open_ms > own.placement_script_open_ms
4007 && stop_bits(peer.exit_levels.stop) != stop_bits(own.exit_levels.stop)
4008 && peer.source_id == own.source_id
4009 && peer.from_entry == own.from_entry) {
4010 leg.released = true;
4011 }
4012 }
4013 }
4014 std::size_t kept = 0;
4015 for (const auto& leg : held) kept += leg.released ? 0 : 1;
4016 memo.resize(kept);
4017 kept = 0;
4018 for (const auto& leg : held) {
4019 if (leg.released) released.push_back(leg.incarnation);
4020 else memo[kept++] = {leg.incarnation, top};
4021 }
4022 };
4023 // With every row walked, the held legs are all known.
4024 if (!unpinned) release_superseded();
4025 // Quiet bars stop before gathering roots. Market-add marks still need
4026 // the full pass to prune them against the roots.
4027 if (market_pyramid_adds_.empty() && !unpinned && released.empty()) { finish_phases(); return; }
4028 // Only a host whose kernel can say what it still works erases a row.
4029 IExecutionConsumer* consumer = bound_consumer();
4030 if (!consumer) { finish_phases(); return; }
4031 const auto& core = as_native_consumer(*consumer).request_core();
4032
4033 auto& roots = sweep.roots;
4034 const auto root = [&](std::uint64_t incarnation) {
4035 ++work.root_visits; if (incarnation != 0) roots.push_back(incarnation);
4036 };
4037 for (const auto& handle : live_handles_) root(handle.incarnation);
4038 // A request the kernel still works may still be matched, filled or
4039 // refused, and every such event reads its row: the kernel liquidation the
4040 // adapter adopted on its first fill (on_applied) is one that is never a
4041 // live handle of the adapter's own.
4042 for (const auto& working : core.live()) root(working.definition->handle.incarnation);
4043 // Commands above receipt_cursor_ are unobserved; even cancels issued
4044 // during receipt observation name rows still needed at the next read.
4045 // V19-B retires only acknowledged events. The adapter acknowledges no
4046 // further than its monotone cursor, so the window includes every root.
4047 assert(as_native_consumer(*consumer).event_window_start() <= receipt_cursor_ + 1);
4048 as_native_consumer(*consumer).visit_commands_after(receipt_cursor_,
4049 [&](const native_order::CommandEvent& command) {
4050 std::visit([&](const auto& event) {
4051 using Event = std::decay_t<decltype(event)>;
4052 if constexpr (std::is_same_v<Event, native_order::RejectedEvent>) {
4053 // No request exists.
4054 } else if constexpr (std::is_same_v<Event, native_order::ReplacedEvent>) {
4055 root(event.predecessor().incarnation);
4056 root(event.successor().incarnation);
4057 } else if constexpr (std::is_same_v<Event, native_order::ReplaceRejectedEvent>) {
4058 root(event.target().incarnation);
4059 } else if constexpr (std::is_same_v<Event, native_order::NotWorkingEvent>
4060 || std::is_same_v<Event, native_order::InvalidHandleEvent>) {
4061 root(event.target.incarnation);
4062 } else if constexpr (std::is_same_v<Event, native_order::NativeRiskEvent>) {
4063 // An account fact; it names no request.
4064 } else {
4065 if (event.definition) root(event.definition->handle.incarnation);
4066 if constexpr (std::is_same_v<Event, native_order::ReservationReducedEvent>
4067 || std::is_same_v<Event,
4068 native_order::DeferredGroupAdjustmentEvent>) {
4069 root(event.recipient.incarnation);
4070 }
4071 }
4072 }, command);
4073 return false;
4074 });
4075 for (const auto& cohort : cohorts_by_id_.held_rows()) {
4076 for (const auto& opening : cohort.second.opened) root(opening.incarnation);
4077 }
4078 if (auto* pine = pine_view_of(host_)) {
4079 for (const auto& lot : pine->pyramid_entries_) root(lot.entry_incarnation);
4080 }
4081 for (const auto& handle : first_open_newborns_) root(handle.incarnation);
4082 for (const auto& row : live_by_source_key_) root(row.second.incarnation);
4083 auto& live_groups = sweep.live_groups;
4084 auto& source_bars = sweep.source_bars;
4085 const auto queued_snapshot = [&](const PlacementSnapshot& snapshot) {
4086 root(snapshot.bracket_origin.incarnation);
4087 root(snapshot.paired_reversal_parent.incarnation);
4088 root(snapshot.preserved_by_close_all.incarnation);
4089 root(snapshot.projection_predecessor);
4090 root(snapshot.legs.target().incarnation);
4091 root(snapshot.reservation_growth_owner_incarnation);
4092 if (snapshot.sequential_group != 0) live_groups.push_back(snapshot.sequential_group);
4093 if (snapshot.projection_created_bar_pinned)
4094 source_bars.push_back(snapshot.projection_created_bar);
4095 };
4096 const auto queued_request = [&](const native_order::Request& request) {
4097 if (const auto* wait = std::get_if<native_order::WaitForApplied>(&request.owner)) {
4098 root(wait->parent.incarnation);
4099 } else if (const auto* opening = std::get_if<native_order::BindOpening>(&request.owner)) {
4100 root(opening->opening.incarnation);
4101 } else if (const auto* openings
4102 = std::get_if<native_order::BindOpenings>(&request.owner)) {
4103 for (const auto& handle : openings->openings) root(handle.incarnation);
4104 }
4105 };
4106 for (const auto& leg : pending_bracket_legs_) {
4107 queued_snapshot(leg.snapshot);
4108 queued_request(leg.request);
4109 }
4110 for (const auto& entry : pending_entries_) {
4111 queued_snapshot(entry.snapshot);
4112 queued_request(entry.request);
4113 }
4114 for (const auto& order : delayed_market_orders_) {
4115 queued_snapshot(order.snapshot);
4116 queued_request(order.request);
4117 }
4118 for (const auto& sell : deferred_open_marketable_sells_) queued_snapshot(sell.snapshot);
4119 for (const auto& snapshot : throttled_reopen_rearm_) queued_snapshot(snapshot);
4120 for (const auto& command : pending_same_bar_commands_) {
4121 queued_snapshot(command.snapshot);
4122 queued_request(command.request);
4123 }
4124 for (const auto& shadow : source_shadow_pending_) queued_snapshot(shadow.snapshot);
4125 for (const auto& pending : pending_coof_requests_) {
4126 queued_snapshot(pending.snapshot);
4127 queued_request(pending.request);
4128 }
4129 for (const auto& revival : pending_margin_revivals_) queued_snapshot(revival.snapshot);
4130 for (const auto& leg : anchored_relative_legs_) {
4131 root(leg.handle.incarnation);
4132 root(leg.parent.incarnation);
4133 queued_request(leg.request);
4134 }
4135 root(materializing_parent_.incarnation);
4136 for (const ShortSeedPlan* plan : {&short_seed_, &pending_short_seed_.plan}) {
4137 root(plan->long_entry.incarnation);
4138 root(plan->materialize_long.incarnation);
4139 root(plan->final_short.incarnation);
4140 }
4141 root(short_seed_long_candidate_.incarnation);
4142 std::sort(roots.begin(), roots.end());
4143 roots.erase(std::unique(roots.begin(), roots.end()), roots.end());
4144 // Live rows supply K7/K8; K1's unwithdrawn current-cycle lifecycles
4145 // supply the K2/K3 revival candidates.
4146 for (const auto& handle : live_handles_) {
4147 const auto found = placement_.find(handle.incarnation);
4148 if (found == placement_.end()) continue;
4149 if (found->second.sequential_group != 0)
4150 live_groups.push_back(found->second.sequential_group);
4151 if (found->second.family == PineOrderFamily::Entry)
4152 source_bars.push_back(found->second.projection_created_bar);
4153 }
4154 std::sort(live_groups.begin(), live_groups.end());
4155 std::sort(source_bars.begin(), source_bars.end());
4156 auto& candidates = sweep.candidates;
4157 auto& candidate_names = sweep.candidate_names;
4158 // A partial pre-pass needs its held legs gathered alongside candidates.
4159 if (unpinned) held.clear();
4160 auto candidate_origin_cursor = askable_origins.begin();
4161 placement_.visit_rows([&](std::uint64_t incarnation, const PlacementSnapshot& value) { ++work.row_visits; ++work.candidate_rows;
4162 if (value.legs.target().incarnation == 0 || value.placement_cycle != cycle
4163 || !exit_family(value.family) || value.from_entry.empty()
4164 || withdrawn(value.legs) || filled_limit_without_revival(value)) {
4165 return false;
4166 }
4167 if (unpinned) {
4168 while (candidate_origin_cursor != askable_origins.end()
4169 && *candidate_origin_cursor < incarnation) ++candidate_origin_cursor;
4170 const bool askable = candidate_origin_cursor != askable_origins.end()
4171 && *candidate_origin_cursor == incarnation;
4172 if (!askable && !contains(askable_origins, value.bracket_origin.incarnation))
4173 held.push_back({incarnation, &value});
4174 }
4175 candidates.emplace_back(incarnation, &value);
4176 return false;
4177 });
4178 if (unpinned) release_superseded();
4179 // Only newly released askable-origin legs lose K2/K3; keep legacy certificates.
4180 candidates.erase(std::remove_if(candidates.begin(), candidates.end(), [&](const auto& c) {
4181 return !contains(roots, c.first) && contains(released, c.first)
4182 && contains(askable_origins, c.second->bracket_origin.incarnation);
4183 }), candidates.end());
4184 for (const auto& c : candidates) {
4185 candidate_names.push_back(c.first);
4186 candidate_names.push_back(c.second->legs.target().incarnation);
4187 }
4188 std::sort(candidate_names.begin(), candidate_names.end());
4189 // K3: preserve the first witness, but never repeat old-candidate x old-row
4190 // comparisons. Identity, stop bits and placement time do not change.
4191 // Rows appended since the last K3 pass can only add later witnesses.
4192 auto& first_reissues = sweep.first_reissues;
4193 auto& next_reissues = sweep.next_reissues;
4194 if (sweep.reissue_cycle != cycle) {
4195 sweep.reissues.clear();
4196 sweep.reissue_high_water = 0;
4197 sweep.reissue_cycle = cycle;
4198 }
4199 std::size_t prior = 0;
4200 for (std::size_t index = 0; index < candidates.size(); ++index) {
4201 const auto incarnation = candidates[index].first;
4202 while (prior < sweep.reissues.size() && sweep.reissues[prior].first < incarnation) ++prior;
4203 const bool known = prior < sweep.reissues.size()
4204 && sweep.reissues[prior].first == incarnation
4205 && (sweep.reissues[prior].second == 0
4206 || placement_.contains(sweep.reissues[prior].second));
4207 const auto witness = known ? sweep.reissues[prior].second : 0;
4208 next_reissues.emplace_back(incarnation, witness);
4209 if (!known) sweep.new_candidates.push_back(index);
4210 else {
4211 sweep.known_candidates.push_back(index);
4212 if (witness == 0) sweep.unresolved_candidates.push_back(index);
4213 }
4214 }
4215 const auto compare_reissue = [&](std::uint64_t incarnation, const PlacementSnapshot& value,
4216 std::size_t index) {
4217 ++work.k3_pairs; PINEFORGE_TAIL_WORK("k3_pairs", 1);
4218 auto& witness = next_reissues[index].second;
4219 const auto& c = candidates[index];
4220 if (incarnation <= c.first || (witness != 0 && incarnation >= witness)) return;
4221 const auto& q = *c.second;
4222 if (q.family == value.family
4223 && q.placement_script_open_ms < value.placement_script_open_ms
4224 && !same_double_bits(q.exit_levels.stop, value.exit_levels.stop)
4225 && q.source_id == value.source_id && q.from_entry == value.from_entry)
4226 witness = incarnation;
4227 };
4228 if (!sweep.new_candidates.empty()) {
4229 placement_.visit_rows([&](std::uint64_t incarnation, const PlacementSnapshot& value) { ++work.row_visits; ++work.peer_visits;
4230 if (value.placement_cycle == cycle && exit_family(value.family))
4231 for (const auto index : sweep.new_candidates) compare_reissue(incarnation, value, index);
4232 return false;
4233 });
4234 }
4235 if (!sweep.unresolved_candidates.empty()) {
4236 const PlacementTable& rows = placement_;
4237 for (auto it = rows.upper_bound(sweep.reissue_high_water); it != rows.end(); ++it) {
4238 const auto& value = it->second; ++work.row_visits; ++work.peer_visits;
4239 if (value.placement_cycle == cycle && exit_family(value.family))
4240 for (const auto index : sweep.unresolved_candidates)
4241 compare_reissue(it->first, value, index);
4242 }
4243 }
4244 for (const auto incarnation : sweep.adopted_reissues) {
4245 const auto row = placement_.find(incarnation);
4246 if (row == placement_.end() || row->second.placement_cycle != cycle
4247 || !exit_family(row->second.family)) continue;
4248 ++work.row_visits; ++work.peer_visits; for (const auto index : sweep.known_candidates)
4249 compare_reissue(incarnation, row->second, index);
4250 }
4251 sweep.adopted_reissues.clear();
4252 for (const auto& answer : next_reissues)
4253 if (answer.second != 0) first_reissues.push_back(answer.second);
4254 std::sort(first_reissues.begin(), first_reissues.end());
4255 sweep.reissues.swap(next_reissues);
4256 sweep.reissue_high_water = placement_.high_water();
4257 PINEFORGE_RETIREMENT_OBSERVE(placement_, cycle, askable_origins, roots,
4258 candidates, first_reissues);
4259
4260 auto& doomed = sweep.doomed;
4261 auto root_cursor = roots.begin();
4262 placement_.visit_rows([&](std::uint64_t incarnation, const PlacementSnapshot& value) { ++work.row_visits; ++work.final_rows;
4263 // K1 (but for the legs it releases), K4, K5; then the roots. A row
4264 // either keeps is kept, so the cheaper test comes first.
4265 if (pinned_by_itself(value) && (released.empty() || !contains(released, incarnation)))
4266 return false;
4267 while (root_cursor != roots.end() && *root_cursor < incarnation) ++root_cursor;
4268 if (root_cursor != roots.end() && *root_cursor == incarnation) return false;
4269 if (value.placement_cycle == cycle) {
4270 // K2
4271 if (value.projection_predecessor != 0
4272 && contains(candidate_names, value.projection_predecessor)) {
4273 return false;
4274 }
4275 // K3
4276 if (exit_family(value.family) && contains(first_reissues, incarnation)) return false;
4277 }
4278 // K7
4279 if (value.family == PineOrderFamily::Entry && value.sequential_group != 0
4280 && value.sequential_rank != 0
4281 && std::binary_search(live_groups.begin(), live_groups.end(),
4282 value.sequential_group)) {
4283 return false;
4284 }
4285 // K8
4286 if (value.opening && value.family == PineOrderFamily::Entry
4287 && value.projection_position_side == static_cast<std::int32_t>(PositionSide::FLAT)
4288 && finite_positive(value.exit_levels.stop) && !finite_positive(value.exit_levels.limit)
4289 && (value.projection_created_bar >= interval
4290 || std::binary_search(source_bars.begin(), source_bars.end(),
4291 value.projection_created_bar))) {
4292 return false;
4293 }
4294 doomed.push_back(incarnation);
4295 return false;
4296 });
4297 // The entire retirement workset, before phase/digest publication, mark
4298 // pruning, bracket consumed facts, cohort sides or the first row removal.
4299 // The workset contains handles only; the ticket borrows the retained rows.
4300 if (!retain_rows) {
4301 placement_.preflight_erased_rows([&](const auto& emit) {
4302 for (const auto incarnation : doomed) emit(placement_.at(incarnation));
4303 });
4304 }
4305 finish_phases();
4306 // Only an open lot or working request can still ask for a market-add mark.
4307 for (auto mark = market_pyramid_adds_.begin(); mark != market_pyramid_adds_.end();) {
4308 if (std::binary_search(roots.begin(), roots.end(), *mark)) ++mark;
4309 else mark = market_pyramid_adds_.erase(mark);
4310 }
4311 if (doomed.empty()) return;
4312
4313 for (const auto incarnation : doomed) {
4314 const auto& value = placement_.at(incarnation);
4315 // A leg of a bracket family stays its member (the bracket cancel still
4316 // names it); the consumed-leg question it answered is kept as a fact.
4317 // Only exit legs are ever members.
4318 if (exit_family(value.family) && !value.legs.dormant()) {
4319 const auto record = [&](BracketRoster& family) {
4320 if (family.count(incarnation) == 0) return false;
4321 family.record_consumed(static_cast<std::uint8_t>(value.family),
4322 value.bracket_origin.incarnation);
4323 return true;
4324 };
4325 const auto keyed = bracket_families_.find(key_for(value.source_id, value.from_entry));
4326 if (keyed == bracket_families_.end() || !record(keyed->second)) {
4327 for (auto& family : bracket_families_) record(family.second);
4328 }
4329 }
4330 // An opening of a cohort's roster leaves its side behind.
4331 if (value.opening) {
4332 const auto cohort = cohorts_by_id_.find(value.source_id);
4333 if (cohort != cohorts_by_id_.end()
4334 && cohort->second.origins.positions_of(incarnation) != nullptr) {
4335 cohort->second.erased_opening_sides |= value.is_long ? 1U : 2U;
4336 }
4337 }
4338 if (!retain_rows) erase_placement(incarnation);
4339 if (!retain_rows) detail::rows_erased.fetch_add(1, std::memory_order_relaxed);
4340 }
4341 // Every family settles the members whose rows would be gone (or are gone),
4342 // and keeps a consumed-leg fact only while its origin is live or opened.
4343 for (auto& family : bracket_families_) {
4344 ++work.family_visits; PINEFORGE_TAIL_WORK("settle_families", 1);
4345 family.second.settle([&](std::uint64_t incarnation) {
4346 return retain_rows ? contains(doomed, incarnation)
4347 : !placement_.contains(incarnation);
4348 });
4349 family.second.prune_consumed([&](std::uint64_t origin) {
4350 return contains(askable_origins, origin);
4351 });
4352 }
4353 // The existing every-row-retained reference also keeps family records
4354 // in their old table; normal runs archive only immutable prefixes.
4355 if (!retain_rows) archive_settled_bracket_families();
4356 if (!retain_rows) placement_.compact();
4357}
4358
4359BracketRoster& PineExecutionAdapter::bracket_family(std::uint64_t key) {
4360 const auto archived = settled_bracket_families_.find(key);
4361 if (archived != settled_bracket_families_.end()) {
4362 // Publish before removing the immutable record, so a failed allocation
4363 // leaves the old family and its digest intact.
4364 const auto placed = bracket_families_.try_emplace(key, archived->second);
4365 assert(placed.second);
4366 settled_bracket_digest_ ^= settled_bracket_hash(key, archived->second);
4367 settled_bracket_families_.erase(archived);
4368 return placed.first->second;
4369 }
4370 return bracket_families_[key];
4371}
4372
4373void PineExecutionAdapter::archive_settled_bracket_families() {
4374 for (auto at = bracket_families_.begin(); at != bracket_families_.end();) {
4375 if (!at->second.wholly_settled()) {
4376 ++at;
4377 continue;
4378 }
4379 const auto value = at->second.settled_record();
4380 [[maybe_unused]] const auto inserted = settled_bracket_families_.emplace(at->first, value);
4381 assert(inserted.second);
4382 settled_bracket_digest_ ^= settled_bracket_hash(at->first, value);
4383 at = bracket_families_.erase(at);
4384 }
4385}
4386
4387void PineExecutionAdapter::clear_bracket_families() noexcept {
4388 bracket_families_.clear();
4389 settled_bracket_families_.clear();
4390 settled_bracket_digest_ = 0;
4391}
4392
4393// The cancelled-origin retirement: the kernel's cohort rosters hold what can still be bound.
4394//
4395// Every opening this adapter accepts joins its id's kernel roster
4396// (cohort_add, submit_or_replace), and the kernel folds every member of every
4397// roster into each continuation read (hash_cohorts), so a run that recorded a
4398// hash per bar paid for every opening it ever made at every bar. A roster is
4399// matching authority for the lots it names and nothing else: a CohortClose or
4400// BindCohort request reads it only through the open lots
4401// (NativeExecutionConsumer::cohort_contains over the book's lots). An origin
4402// whose chain has no working request and no open lot can never be bound
4403// again -- nothing opens a lot under a request that stopped working -- so at
4404// every bar open, after observe_terminal_receipts, each such origin leaves
4405// its kernel roster (cohort_remove). The adapter's own roster
4406// (CohortFacts::origins) can then retire its unreachable front prefix while
4407// keeping the count/digest and erased sides. The kernel roster costs what is live.
4408//
4409// A roster holds chain roots; a working request names its root in its
4410// definition, and a lot names the request that filled it, which is the root
4411// unless it is a replace successor. The root of a successor's lot is not a
4412// public fact, but it is older than the successor (incarnations only grow):
4413// while such a lot is open, every member of its roster older than it stays.
4414//
4415// Commands ending requests/closing lots mark the pass due at receipt
4416// observation; a bar open after only quiet reads has no member to remove.
4417void PineExecutionAdapter::release_closed_cohort_origins() {
4418 auto& scratch = retired_row_scratch_;
4419 if (!scratch.roster_pass_due) return;
4420 scratch.roster_pass_due = false; PF_ROSTER_PASS_WORK(due_passes);
4421 IExecutionConsumer* consumer = bound_consumer();
4422 if (!consumer) return;
4423 auto* pine = pine_view_of(host_);
4424 if (!pine) return;
4425 const auto& core = as_native_consumer(*consumer).request_core();
4426 std::size_t members = 0;
4427 for (const auto& [key, roster] : core.nonempty_cohorts()) { PF_ROSTER_PASS_WORK(roster_cohort_visits); members += roster.origins.size(); }
4428 if (members == 0) return;
4429 // The common case, in a direct scan with nothing allocated or sorted:
4430 // every member still opened a lot that is open, or roots a working
4431 // request, so none can go.
4432 const auto root_of = [](const native_order::LiveRequest& row) {
4433 return row.definition->root ? row.definition->root->incarnation
4434 : row.handle().incarnation;
4435 };
4436 constexpr std::size_t kDirectScan = 64;
4437 if (members * (pine->pyramid_entries_.size() + core.live().size()) <= kDirectScan) {
4438 bool every_member_bound = true;
4439 for (const auto& [key, roster] : core.nonempty_cohorts()) { PF_ROSTER_PASS_WORK(roster_cohort_visits);
4440 for (const auto& origin : roster.origins) {
4441 const std::uint64_t incarnation = origin.incarnation;
4442 const bool bound = std::any_of(pine->pyramid_entries_.begin(),
4443 pine->pyramid_entries_.end(), [&](const PyramidEntry& lot) {
4444 return lot.entry_incarnation == incarnation;
4445 })
4446 || std::any_of(core.live().begin(), core.live().end(),
4447 [&](const native_order::LiveRequest& row) {
4448 return root_of(row) == incarnation;
4449 });
4450 if (!bound) {
4451 every_member_bound = false;
4452 break;
4453 }
4454 }
4455 if (!every_member_bound) break;
4456 }
4457 if (every_member_bound) return;
4458 }
4459 PINEFORGE_LOOKUP_INDEX_WORK("cohort_origin_pass", this, run_counter_, 1);
4460 auto& working = scratch.working_roots;
4461 auto& lots = scratch.lot_openings;
4462 if (!position_tail_summary_) position_tail_summary_ = std::make_shared<detail::AdapterPositionTailSummary>();
4463 auto& roster_roots = static_cast<detail::AdapterPositionTailSummary&>(
4464 *position_tail_summary_).roster_roots;
4465 auto& successor_lots = scratch.successor_lots;
4466 auto& released = scratch.released_origins;
4467 working.clear();
4468 lots.clear();
4469 roster_roots.clear();
4470 successor_lots.clear();
4471 released.clear();
4472 for (const auto& live : core.live()) working.push_back(root_of(live));
4473 std::sort(working.begin(), working.end());
4474 for (const auto& lot : pine->pyramid_entries_) lots.push_back(lot.entry_incarnation);
4475 std::sort(lots.begin(), lots.end());
4476 lots.erase(std::unique(lots.begin(), lots.end()), lots.end());
4477 for (const auto& [key, roster] : core.nonempty_cohorts()) { PF_ROSTER_PASS_WORK(roster_cohort_visits);
4478 PINEFORGE_LOOKUP_INDEX_WORK("cohort_origin_roster", this, run_counter_, 1);
4479 for (const auto& origin : roster.origins)
4480 roster_roots.push_back(origin.incarnation);
4481 }
4482 std::sort(roster_roots.begin(), roster_roots.end());
4483 roster_roots.erase(std::unique(roster_roots.begin(), roster_roots.end()),
4484 roster_roots.end());
4485 // The lots no roster names as a root: a replace successor's (or an
4486 // opening no roster holds).
4487 for (const auto opening : lots) {
4488 if (opening == 0) continue;
4489 if (!std::binary_search(roster_roots.begin(), roster_roots.end(), opening)) {
4490 successor_lots.push_back(opening);
4491 }
4492 }
4493 auto& probe = scratch.lot_probe;
4494 for (const auto& [key, roster] : core.nonempty_cohorts()) { PF_ROSTER_PASS_WORK(roster_cohort_visits);
4495 const auto& origins = roster.origins;
4496 if (origins.empty()) continue;
4497 if (probe.run != origins.front().run) probe.run = origins.front().run;
4498 // The newest open successor lot this roster binds: every member older
4499 // than it may be its root.
4500 std::uint64_t kept_below = 0;
4501 for (const auto opening : successor_lots) { PF_ROSTER_PASS_WORK(roster_cohort_visits);
4502 probe.incarnation = opening;
4503 if (core.cohort_contains(roster.handle, probe)) kept_below = opening;
4504 }
4505 for (const auto& origin : origins) {
4506 const std::uint64_t incarnation = origin.incarnation;
4507 if (incarnation < kept_below
4508 || std::binary_search(working.begin(), working.end(), incarnation)
4509 || std::binary_search(lots.begin(), lots.end(), incarnation)) {
4510 continue;
4511 }
4512 released.emplace_back(roster.handle, incarnation);
4513 }
4514 }
4515 if (released.empty()) return;
4516 auto& host = require_host();
4517 for (const auto& [cohort, incarnation] : released) {
4518 native_order::RequestHandle origin = probe;
4519 origin.incarnation = incarnation;
4520 host.cohort_remove(cohort, std::move(origin));
4521 }
4522}
4523
4524void PineExecutionAdapter::maybe_activate_short_seed_plan() {
4525 // The legacy sort qualifies at the broker boundary, never midway through
4526 // the source callback that created the three objects. on_bar_open owns
4527 // the final activation when the live next-bar predicate is available.
4528}
4529
4530bool PineExecutionAdapter::short_seed_context_is_live() const noexcept {
4531 if (!host_) return false;
4532 const auto physical = detail::run_position(*host_);
4533 if (!(physical.signed_units < 0.0) || physical.lot_count != 1U) return false;
4534 const double held = std::abs(physical.signed_units);
4535 for (const auto& cohort : cohorts_by_id_.held_rows()) {
4536 if (cohort_exposure_for(cohort.second) == held) return true;
4537 }
4538 return false;
4539}
4540
4541bool PineExecutionAdapter::qualify_short_seed_plan(const ShortSeedPlan& plan) const {
4542 if (!host_ || plan.long_entry.incarnation == 0 || plan.materialize_long.incarnation == 0
4543 || plan.final_short.incarnation == 0 || plan.seed_id.empty()) {
4544 return false;
4545 }
4546 const auto long_it = placement_.find(plan.long_entry.incarnation);
4547 const auto materialize_it = placement_.find(plan.materialize_long.incarnation);
4548 const auto final_it = placement_.find(plan.final_short.incarnation);
4549 if (long_it == placement_.end() || materialize_it == placement_.end()
4550 || final_it == placement_.end()) {
4551 return false;
4552 }
4553 const PlacementSnapshot& long_entry = long_it->second;
4554 const PlacementSnapshot& materialize = materialize_it->second;
4555 const PlacementSnapshot& final_short = final_it->second;
4556 const bool bar_magnifier = bar_magnifier_;
4557 const auto is_live = [&](const native_order::RequestHandle& handle) {
4558 return std::find(live_handles_.begin(), live_handles_.end(), handle) != live_handles_.end();
4559 };
4560 const auto no_level = [](const PineExitLevels& levels) {
4561 return std::isnan(levels.limit) && std::isnan(levels.stop)
4562 && std::isnan(levels.trail_points) && std::isnan(levels.trail_offset)
4563 && std::isnan(levels.trail_price) && std::isnan(levels.profit_ticks)
4564 && std::isnan(levels.loss_ticks);
4565 };
4566 const auto fresh_plain = [&](const PlacementSnapshot& row) {
4567 return row.placement_open_epoch + 1U == broker_open_epoch_
4568 && row.projection_position_side == static_cast<std::int32_t>(PositionSide::SHORT)
4569 && !row.replaced_opening && row.projection_predecessor == 0
4570 && row.recreated_after_named_cancelled_entry_incarnation == 0
4571 && row.named_cancel_surviving_exit_incarnation == 0
4572 && !row.birth.from_fill() && !row.birth.at_terminal_fill()
4573 && !compat::pine::historical_cascade_reach(row.birth_reach)
4574 && !row.projection_created_during_coof
4575 && !row.reservation_expansion.capture()
4576 && row.oca_name.empty() && row.oca_type == 0;
4577 };
4578 const bool fixed_default = config_.default_qty_type == static_cast<int>(QtyType::FIXED);
4579 const auto pure_default_market_entry = [&](const PlacementSnapshot& row) {
4580 const bool sizing_shape = fixed_default ? std::isnan(row.sizing.frozen_units)
4581 : finite_positive(row.sizing.frozen_units) && finite_positive(row.sizing.equity)
4582 && finite_positive(row.sizing.price) && finite_positive(row.sizing.mark)
4583 && finite_positive(row.sizing.fx);
4584 return row.family == PineOrderFamily::Entry && row.deferred_cohort
4585 && std::isnan(row.requested_qty) && row.qty_type == -1 && sizing_shape
4586 && no_level(row.exit_levels) && !row.projection_after_close;
4587 };
4588 const auto exact_full_fifo_close_short = [&]() {
4589 return materialize.family == PineOrderFamily::Close
4590 && materialize.source_id == plan.seed_id && materialize.from_entry == plan.seed_id
4591 && !materialize.is_long && finite_positive(materialize.requested_qty)
4592 && materialize.qty_percent == 100.0 && no_level(materialize.exit_levels)
4593 && materialize.frozen_market_instruction && materialize.frozen_market_targeted_close
4594 && finite_positive(materialize.frozen_market_transaction_units)
4595 && !materialize.reservation_expansion.capture()
4596 && std::abs(materialize.frozen_market_transaction_units - plan.seed_qty) <= 1e-12
4597 && std::abs(materialize.requested_qty - plan.seed_qty) <= 1e-12;
4598 };
4599 std::array<std::uint64_t, 3> incarnations{
4600 plan.long_entry.incarnation,
4601 plan.materialize_long.incarnation,
4602 plan.final_short.incarnation,
4603 };
4604 std::sort(incarnations.begin(), incarnations.end());
4605 const bool consecutive_incarnations = incarnations[0] != 0
4606 && incarnations[0] != std::numeric_limits<std::uint64_t>::max()
4607 && incarnations[0] + 1U == incarnations[1]
4608 && incarnations[1] != std::numeric_limits<std::uint64_t>::max()
4609 && incarnations[1] + 1U == incarnations[2];
4610 const auto physical = detail::run_position(*host_);
4611 const auto point = detail::callback_point(*host_);
4612 const double open = point ? point->price : kNaN;
4613 const double tick = staged_.syminfo.mintick;
4614 const double admit = finite_positive(tick) ? nearest_tick(open, tick) : open;
4615 const double entry_qty = fixed_default ? config_.default_qty_value : long_entry.sizing.frozen_units;
4616 const double final_qty = fixed_default ? config_.default_qty_value : final_short.sizing.frozen_units;
4617 const double fx = point ? active_staged_fx(point->decision.sub_bar_open_ms) : staged_.account_fx;
4618 const double notional = staged_.syminfo.pointvalue * fx;
4619 const double marked = percent_commission_live_equity(open);
4620 const double projected_long = entry_qty + std::min(plan.seed_qty, entry_qty);
4621 const double projected_free = marked - projected_long * open * notional;
4622 const double projected_required = (projected_long + final_qty) * admit * notional;
4623 const double epsilon = std::max(1e-9, std::abs(marked) * 1e-12);
4624 const bool projected_final_short_admission_is_safe = finite_positive(admit)
4625 && finite_positive(entry_qty) && finite_positive(final_qty) && finite_positive(notional)
4626 && std::isfinite(projected_free) && std::isfinite(projected_required)
4627 && projected_required <= projected_free + epsilon;
4628 bool percent_rechecks_safe = true;
4629 if (config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
4630 && config_.default_qty_value <= 100.0) {
4631 for (const PlacementSnapshot* row : {&long_entry, &final_short}) {
4632 const double required = row->sizing.frozen_units * admit * staged_.syminfo.pointvalue
4633 * row->sizing.fx;
4634 if (!(finite_positive(required) && required <= row->sizing.equity)) {
4635 percent_rechecks_safe = false;
4636 break;
4637 }
4638 }
4639 }
4640 const int source_bar = long_entry.projection_created_bar;
4641 return !config_.close_entries_rule_any && !config_.process_orders_on_close
4642 && !config_.calc_on_order_fills && !coof_recalc_active_
4643 && !bar_magnifier && !risk_.halted
4644 && risk_.direction == 0 && risk_.max_cons_loss_days == 0 && risk_.max_drawdown <= 0.0
4645 && risk_.max_intraday_loss <= 0.0 && risk_.max_position_size <= 0.0 && !cap.active()
4646 && config_.pyramiding == 1 && config_.slippage == 0 && config_.commission_value == 0.0
4647 && std::abs(config_.margin_long - 100.0) < 1e-12
4648 && std::abs(config_.margin_short - 100.0) < 1e-12
4649 && physical.signed_units < 0.0 && physical.lot_count == 1U && current_position_cycle_ > 0
4650 && is_live(plan.long_entry) && is_live(plan.materialize_long) && is_live(plan.final_short)
4651 && source_bar >= 0
4653 && long_entry.command_ordinal + 1U == final_short.command_ordinal
4654 && final_short.command_ordinal + 1U == materialize.command_ordinal
4655 && consecutive_incarnations
4656 && fresh_plain(long_entry) && fresh_plain(final_short) && fresh_plain(materialize)
4657 && pure_default_market_entry(long_entry) && pure_default_market_entry(final_short)
4658 && !long_entry.source_id.empty() && long_entry.is_long
4659 && !long_entry.projection_over_pyramiding && !final_short.source_id.empty()
4660 && long_entry.source_id != final_short.source_id
4661 && long_entry.source_id != materialize.source_id && !final_short.is_long
4662 && final_short.projection_over_pyramiding
4663 && long_entry.placement_cycle == plan.seed_cycle
4664 && final_short.placement_cycle == plan.seed_cycle
4665 && std::abs(long_entry.projection_tv_carry_qty - plan.seed_qty) <= 1e-12
4666 && std::abs(final_short.projection_tv_carry_qty - plan.seed_qty) <= 1e-12
4667 && exact_full_fifo_close_short() && plan.seed_id == final_short.source_id
4668 && position_open_epoch_ < broker_open_epoch_
4669 && finite_positive(plan.seed_qty) && std::abs(std::abs(physical.signed_units) - plan.seed_qty) <= 1e-12
4670 && (fixed_default ? std::abs(config_.default_qty_value - plan.seed_qty) <= 1e-12
4671 : std::abs(long_entry.sizing.frozen_units - final_short.sizing.frozen_units) <= 1e-12)
4672 && std::abs(materialize.projection_tv_carry_qty - plan.seed_qty) <= 1e-12
4673 && projected_final_short_admission_is_safe && percent_rechecks_safe;
4674}
4675
4676void PineExecutionAdapter::activate_short_seed_plan_at_open(const NativeDecisionContext&) {
4677 if (!pending_short_seed_.ready) return;
4678 if (broker_open_epoch_ < pending_short_seed_.expected_open_epoch) return;
4679 if (broker_open_epoch_ == pending_short_seed_.expected_open_epoch
4680 && qualify_short_seed_plan(pending_short_seed_.plan)) {
4681 short_seed_ = pending_short_seed_.plan;
4682 short_seed_.active = true;
4683 } else if (short_seed_.long_entry == pending_short_seed_.plan.long_entry
4684 && short_seed_.materialize_long == pending_short_seed_.plan.materialize_long
4685 && short_seed_.final_short == pending_short_seed_.plan.final_short) {
4686 short_seed_ = {};
4687 }
4688 pending_short_seed_ = {};
4689}
4690
4691std::optional<native_order::RequestHandle> PineExecutionAdapter::submit_or_replace(
4692 native_order::Request request, PlacementSnapshot snapshot, bool opening,
4693 const SourceId& replacement_key) {
4694 // The accepted opening appends its cohort below. Hold the complete node
4695 // ticket before any host submit/replace or source placement/origin write.
4696 if (opening) cohorts_by_id_.require_insert(snapshot.source_id);
4697 auto& host = require_host();
4698 // A core-sized opening freezes its basis at the execution point the CORE
4699 // sees when it accepts the command; the source froze the same rule when
4700 // the command was written. A command queued for a later point, or one
4701 // the source repriced after writing it, has moved the two apart: it keeps
4702 // its host-resolved intent and its own frozen quantity rather than
4703 // silently resizing at the submission point.
4704 if (std::holds_alternative<native_order::Sized>(request.intent)
4705 && !core_sizing_price_matches(snapshot.sizing, snapshot.is_long)) {
4706 request.intent = native_order::HostSized{native_order::HostSizedKind::Open,
4707 snapshot.is_long ? native_order::Side::Long : native_order::Side::Short};
4708 }
4709 const NativePhysicalPosition physical = detail::run_position(host);
4710 snapshot.projection_position_side = physical.signed_units > 0.0
4711 ? static_cast<std::int32_t>(PositionSide::LONG)
4712 : (physical.signed_units < 0.0 ? static_cast<std::int32_t>(PositionSide::SHORT)
4713 : static_cast<std::int32_t>(PositionSide::FLAT));
4714 snapshot.projection_after_close = snapshot.projection_after_close
4715 || pending_same_bar_close_qty_ > 0.0;
4716 const auto ledger_records = opening ? open_ledger_records() : std::nullopt;
4717 snapshot.projection_over_pyramiding = opening
4718 && !(config_.process_orders_on_close && snapshot.projection_after_close)
4719 && config_.pyramiding > 0
4720 && ((physical.signed_units > 0.0) == snapshot.is_long)
4721 && physical.signed_units != 0.0
4722 && (ledger_records ? *ledger_records : physical.lot_count)
4723 >= static_cast<std::size_t>(config_.pyramiding);
4724 snapshot.projection_created_during_coof =
4725 snapshot.projection_created_during_coof || coof_recalc_active_;
4726 snapshot.projection_coof_at_terminal = snapshot.projection_coof_at_terminal
4727 || (coof_recalc_active_ && coof_context_.is_terminal_sub_bar);
4728 snapshot.projection_coof_mid_bar = snapshot.projection_coof_mid_bar
4729 || (coof_recalc_active_ && !coof_context_.is_terminal_sub_bar);
4730 snapshot.projection_tv_carry_qty = opening
4731 ? std::max(0.0, std::abs(physical.signed_units)
4732 - pending_same_bar_close_qty_)
4733 : std::abs(physical.signed_units);
4734 snapshot.projection_default_stop_equity = snapshot.sizing.equity;
4735 snapshot.projection_default_stop_signal_close = snapshot.sizing.mark;
4736 snapshot.projection_explicit_equity = std::isfinite(snapshot.requested_qty)
4737 ? snapshot.sizing.equity : kNaN;
4738 snapshot.projection_explicit_signal_close = std::isfinite(snapshot.requested_qty)
4739 ? snapshot.sizing.price : kNaN;
4740 if (snapshot.affordability_policy_active) {
4741 snapshot.projection_affordability_equity = snapshot.sizing.equity;
4742 snapshot.projection_affordability_signal_price = snapshot.sizing.price;
4743 snapshot.projection_affordability_held_qty = std::abs(physical.signed_units);
4744 } else {
4745 snapshot.projection_affordability_equity = kNaN;
4746 snapshot.projection_affordability_signal_price = kNaN;
4747 snapshot.projection_affordability_held_qty = kNaN;
4748 }
4749 const bool fill_time_any_percentage = snapshot.family == PineOrderFamily::Close
4750 && config_.close_entries_rule_any && !std::isfinite(snapshot.requested_qty)
4751 && std::isfinite(snapshot.qty_percent);
4752 if (!opening && !snapshot.reservation_deferred_to_pending_entry
4753 && !fill_time_any_percentage
4754 && !std::isfinite(snapshot.projection_remaining_qty)
4755 && snapshot.deferred_cohort && !std::isfinite(snapshot.requested_qty)
4756 && physical.signed_units != 0.0) {
4757 const double percent = std::isfinite(snapshot.qty_percent)
4758 ? snapshot.qty_percent : 100.0;
4759 snapshot.projection_remaining_qty = quantize_close_units(
4760 std::abs(physical.signed_units), percent);
4761 }
4762 if (opening && !snapshot.source_id.empty()) {
4763 if (const auto token = named_entry_cancel_tokens_.find(snapshot.source_id);
4764 token != named_entry_cancel_tokens_.end()) {
4765 snapshot.recreated_after_named_cancelled_entry_incarnation =
4766 token->second.entry_incarnation;
4767 snapshot.named_cancel_surviving_exit_incarnation =
4768 token->second.surviving_exit_incarnation;
4769 named_entry_cancel_tokens_.erase(token);
4770 }
4771 }
4772 // ab9714be pine_strategy_commands.cpp:497: order records created_position_cycle_seq from current cycle
4773 if (snapshot.placement_cycle == 0)
4774 snapshot.placement_cycle = current_position_cycle_;
4775 if (snapshot.birth.cause() == OrderBirthCause::Unattributed)
4776 snapshot.birth = capture_order_birth();
4777 snapshot.coof_cascade_seg_i = coof_recalc_active_
4778 ? coof_context_.coordinate.interval_index : -1;
4779 snapshot.coof_cascade_inflight_fires = coof_recalc_active_;
4780 if (!snapshot.paired_flat_market_candidate) {
4781 snapshot.paired_flat_market_candidate = snapshot.frozen_market_instruction
4782 && physical.signed_units == 0.0;
4783 snapshot.paired_flat_market_own_qty = snapshot.frozen_market_own_units;
4784 snapshot.paired_flat_market_transaction_qty =
4785 snapshot.frozen_market_transaction_units;
4786 }
4787 snapshot.paired_flat_market_signal_close = snapshot.sizing.price;
4788 snapshot.paired_flat_market_signal_equity = snapshot.sizing.equity;
4789 snapshot.paired_flat_market_signal_margin_pct = snapshot.is_long
4790 ? config_.margin_long : config_.margin_short;
4791 snapshot.paired_flat_market_signal_pointvalue = staged_.syminfo.pointvalue;
4792 snapshot.paired_flat_market_signal_fx = snapshot.sizing.fx;
4793 snapshot.pooc_global_full_exit_dynamic_qty = config_.process_orders_on_close
4794 && !opening && !std::isfinite(snapshot.requested_qty)
4795 && (!std::isfinite(snapshot.qty_percent) || snapshot.qty_percent >= 100.0);
4796 snapshot.pooc_global_full_exit_tracks_bound_adds = snapshot.pooc_global_full_exit_dynamic_qty;
4797 if (const auto point = detail::callback_point(host)) {
4798 // ab9714be src/source/pine_strategy_commands.cpp:481 and
4799 // src/source/pine_strategy_commands.cpp:1959 stamp an order's
4800 // created_bar at the bar of the source command, and a bracket leg the
4801 // pass skipped while flat stays in that same book slot
4802 // (src/source/pine_fills.cpp:7461-7468), so only the
4803 // process_orders_on_close gate of src/source/pine_fills.cpp:7620-7648
4804 // compares created_bar with the current bar. Submitting the staged leg
4805 // when its parent entry applies must not re-stamp the bar.
4806 if (!snapshot.projection_created_bar_pinned) {
4807 snapshot.projection_created_bar = projection_bar_index(point->decision);
4808 }
4809 snapshot.placement_script_open_ms = point->decision.script_bar_open_ms;
4810 snapshot.placement_sub_open_ms = point->decision.sub_bar_open_ms;
4811 }
4812 if (opening && snapshot.family == PineOrderFamily::Entry
4813 && !snapshot.is_long && snapshot.deferred_cohort
4814 && snapshot.affordability_close_only
4815 && !std::isfinite(snapshot.requested_qty)
4816 && !finite_positive(snapshot.exit_levels.limit)
4817 && !finite_positive(snapshot.exit_levels.stop)
4818 && !snapshot.projection_after_close
4819 && !config_.process_orders_on_close && !config_.calc_on_order_fills
4820 && !coof_recalc_active_
4821 && physical.signed_units > 0.0 && physical.lot_count == 1U
4822 && last_margin_call_script_bar_ == snapshot.placement_script_open_ms
4823 && last_margin_call_event_ordinal_ != 0
4824 && last_margin_call_event_ordinal_ == last_applied_ordinal_
4825 && last_margin_call_entry_incarnation_ != 0
4826 && last_margin_call_position_cycle_ == current_position_cycle_
4827 && last_margin_call_at_script_close_
4828 && last_margin_call_closed_units_ == 1.0
4829 && last_margin_call_remaining_units_ == std::abs(physical.signed_units)) {
4830 const auto* pine_host = pine_view_of(&host);
4831 const auto state = detail::run_state(host);
4832 const bool magnifier = pine_host
4833 && pine_host->scheduler_.bar_magnifier_enabled();
4834 const double carried = last_margin_call_remaining_units_
4835 + last_margin_call_closed_units_;
4836 if (pine_host && !magnifier
4837 && std::isfinite(carried)
4838 && std::abs(carried - std::abs(physical.signed_units) - 1.0) < 1e-6) {
4839 // The native pre-open/path callback has already applied the
4840 // signal-close margin event before this source command is
4841 // published. Preserve the exact event receipt the legacy pending
4842 // owner wrote after command publication (pine_fills.cpp:2049-52).
4843 snapshot.projection_tv_carry_qty = carried;
4844 snapshot.signal_close_mc_bar = snapshot.projection_created_bar;
4845 snapshot.signal_close_mc_entry_incarnation =
4846 last_margin_call_entry_incarnation_;
4847 snapshot.signal_close_mc_fill_seq =
4848 pine_host->adapter_broker_fill_event_sequence();
4849 snapshot.signal_close_mc_remaining_qty =
4850 last_margin_call_remaining_units_;
4851 snapshot.affordability_keep_mc_close_surplus = true;
4852 }
4853 }
4854 if (snapshot.opening && snapshot.family == PineOrderFamily::Entry
4855 && !snapshot.is_long && snapshot.rounded_signal_cost_close_only
4856 && snapshot.affordability_close_only
4857 && signal_close_mc_event_bar_ == snapshot.projection_created_bar
4858 && signal_close_mc_position_cycle_ == snapshot.placement_cycle
4859 && signal_close_mc_entry_incarnation_ != 0
4860 && signal_close_mc_fill_seq_ != 0) {
4861 snapshot.signal_close_mc_bar = signal_close_mc_event_bar_;
4862 snapshot.signal_close_mc_entry_incarnation =
4863 signal_close_mc_entry_incarnation_;
4864 snapshot.signal_close_mc_fill_seq = signal_close_mc_fill_seq_;
4865 snapshot.signal_close_mc_remaining_qty = signal_close_mc_remaining_qty_;
4866 snapshot.projection_tv_carry_qty = signal_close_mc_before_qty_;
4867 }
4868 snapshot.placement_open_epoch = broker_open_epoch_;
4869 if (snapshot.command_ordinal == 0) snapshot.command_ordinal = ++command_ordinal_;
4870 if (snapshot.command_sequence == 0) {
4871 if (source_command_sequence_ == std::numeric_limits<std::uint64_t>::max()) {
4872 throw coded<std::overflow_error>(RunFailureCode::engine_invariant, {},
4873 "Pine source command sequence exhausted");
4874 }
4875 snapshot.command_sequence = ++source_command_sequence_;
4876 }
4877 std::optional<admission::Allocation> admission_allocation;
4878 std::shared_ptr<const admission::CommandObservation> admission_observation;
4879 if (snapshot.family == PineOrderFamily::Entry
4880 || snapshot.family == PineOrderFamily::Order) {
4881 admission_allocation.emplace(admission_journal.reserve());
4882 auto observed = std::make_shared<admission::CommandObservation>();
4883 observed->command = admission_allocation->sequence();
4884 observed->kind = snapshot.family == PineOrderFamily::Entry
4885 ? admission::CommandKind::Entry : admission::CommandKind::Raw;
4886 observed->birth = snapshot.birth;
4887 observed->id = snapshot.source_id;
4888 observed->requested_quantity = snapshot.requested_qty;
4889 observed->quantity_type = snapshot.qty_type;
4890 observed->buy = snapshot.is_long;
4891 observed->prices = {snapshot.exit_levels.limit, snapshot.exit_levels.stop};
4892 observed->oca_name = snapshot.oca_name;
4893 observed->oca_type = snapshot.oca_type;
4894 const auto native = detail::run_state(host);
4895 auto& configuration = observed->configuration;
4896 configuration.process_on_close = config_.process_orders_on_close;
4897 configuration.calc_on_fills = config_.calc_on_order_fills;
4898 configuration.magnifier = native.spec && !native.spec->intrabar.is_none();
4899 configuration.fill_recalculation = coof_recalc_active_;
4900 configuration.scheduler = config_.calc_on_order_fills;
4901 configuration.slippage = config_.slippage;
4902 configuration.pyramiding = config_.pyramiding;
4903 configuration.default_quantity_type = config_.default_qty_type;
4904 configuration.default_quantity_value = config_.default_qty_value;
4905 configuration.long_margin = config_.margin_long;
4906 configuration.short_margin = config_.margin_short;
4907 configuration.commission_value = config_.commission_value;
4908 configuration.commission_type = config_.commission_type;
4909 configuration.pointvalue = staged_.syminfo.pointvalue;
4910 configuration.fx = snapshot.sizing.fx;
4911 configuration.quantity_step = staged_.quantity_grid.value_or(0.0);
4912 configuration.mintick = staged_.syminfo.mintick;
4913 configuration.risk_direction = risk_.direction;
4914 configuration.loss_days_limit = risk_.max_cons_loss_days;
4915 configuration.drawdown_limit = risk_.max_drawdown;
4916 configuration.intraday_loss_limit = risk_.max_intraday_loss;
4917 configuration.position_limit = risk_.max_position_size;
4918 configuration.fill_cap_active = cap.active();
4919 configuration.risk_halted = risk_.halted;
4920 observed->bar = snapshot.projection_created_bar;
4921 observed->placement_side = snapshot.projection_position_side;
4922 observed->placement_cycle = snapshot.placement_cycle;
4923 observed->prior_close_quantity = snapshot.projection_after_close
4924 ? snapshot.projection_tv_carry_qty : 0.0;
4925 observed->held_quantity = snapshot.projection_tv_carry_qty;
4926 observed->held_entries = static_cast<int>(physical.lot_count);
4927 observed->realized_equity = snapshot.sizing.equity;
4928 observed->placement_equity = snapshot.sizing.equity;
4929 observed->signal_close = snapshot.sizing.mark;
4930 observed->quantized_fixed_quantity = std::isfinite(snapshot.requested_qty)
4931 ? snapshot.requested_qty : kNaN;
4932 if (!std::isfinite(snapshot.requested_qty)
4933 && std::holds_alternative<native_order::Market>(request.trigger)
4934 && (config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
4935 || config_.default_qty_type == static_cast<int>(QtyType::CASH))) {
4936 observed->original_sizing = admission::SizingObservation{
4937 snapshot.sizing.frozen_units, snapshot.sizing.equity,
4938 snapshot.sizing.price, snapshot.sizing.mark, snapshot.sizing.fx};
4939 }
4940 observed->explicit_equity = snapshot.projection_explicit_equity;
4941 observed->explicit_price = snapshot.projection_explicit_signal_close;
4942 admission_observation = std::move(observed);
4943 snapshot.market_admission.bind(admission_observation);
4944 }
4945 if (auto* member = std::get_if<native_order::Member>(&request.group)) {
4946 if (source_sequence_ >= static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max())) {
4947 throw coded<std::overflow_error>(RunFailureCode::engine_invariant, {},
4948 "Pine OCA member sequence exhausted");
4949 }
4950 // The generic group member's cohort distinguishes siblings. A source
4951 // OCA name identifies the group; every accepted source instruction is
4952 // a distinct member of it, including replacement incarnations.
4953 member->cohort = static_cast<std::int64_t>(source_sequence_ + 1U);
4954 }
4955 const auto key = replacement_key.empty() ? 0 : checked_live_key(replacement_key);
4956 std::optional<native_order::RequestHandle> accepted;
4957 // The predecessor's placement row, read in place (the trigger-band and scratch reuse; this
4958 // was a copy of the whole row). Nothing writes the row between here and
4959 // its last read below -- the replace runs no adapter code and the table
4960 // gains no row before remember() -- except the reset of its legs after
4961 // the replace, which moves their value out into predecessor_legs.
4962 const PlacementSnapshot* predecessor_snapshot = nullptr;
4963 exit_legs::Lifecycle predecessor_legs;
4964 // The node retire() would erase for `key`, kept for the successor's
4965 // entry instead of allocating one.
4966 decltype(live_by_source_key_)::node_type reused_key_node;
4967 bool predecessor_exit = false;
4968 bool predecessor_market = false;
4969 const auto unchanged_dynamic_exit = [&](const PlacementSnapshot& prior) {
4970 const bool preserve_coof_birth = prior.projection_created_during_coof
4971 && !coof_recalc_active_;
4972 if (opening || materializing_relative_
4973 || ((coof_recalc_active_ || config_.calc_on_order_fills)
4974 && !preserve_coof_birth)
4975 || !snapshot.deferred_cohort
4976 || !prior.deferred_cohort || snapshot.family != prior.family
4980 || !std::isnan(snapshot.requested_qty) || !std::isnan(prior.requested_qty)
4981 || snapshot.source_id != prior.source_id || snapshot.from_entry != prior.from_entry
4982 || snapshot.comment != prior.comment || snapshot.oca_name != prior.oca_name
4983 || snapshot.oca_type != prior.oca_type
4984 || !same_double_bits(snapshot.qty_percent, prior.qty_percent)
4985 || snapshot.bracket_origin != prior.bracket_origin
4986 || !same_exit_levels(snapshot.exit_levels, prior.exit_levels)) {
4987 return false;
4988 }
4989 const auto* sized = std::get_if<native_order::HostSized>(&request.intent);
4990 const auto* owner = std::get_if<native_order::BindCohort>(&request.owner);
4991 const auto cohort = cohorts_by_id_.find(snapshot.from_entry);
4992 const bool fifo_live_owner = !config_.close_entries_rule_any
4993 && std::holds_alternative<native_order::Independent>(request.owner)
4994 && cohort != cohorts_by_id_.end()
4995 && cohort_exposure_for(snapshot.from_entry) > 0.0;
4996 if (!sized || sized->kind != native_order::HostSizedKind::Close || sized->side
4997 || ((!owner || cohort == cohorts_by_id_.end()
4998 || owner->cohort != cohort->second.handle)
4999 && !fifo_live_owner)) {
5000 return false;
5001 }
5002 if (const auto* limit = std::get_if<native_order::Limit>(&request.trigger)) {
5003 if (preserve_coof_birth)
5004 return snapshot.family == PineOrderFamily::ExitLimit;
5005 return snapshot.family == PineOrderFamily::ExitLimit
5006 && same_double_bits(limit->price, prior.exit_levels.limit);
5007 }
5008 if (const auto* stop = std::get_if<native_order::Stop>(&request.trigger)) {
5009 if (preserve_coof_birth)
5010 return snapshot.family == PineOrderFamily::ExitStop;
5011 return snapshot.family == PineOrderFamily::ExitStop
5012 && same_double_bits(stop->price, prior.exit_levels.stop);
5013 }
5014 if (const auto* trail = std::get_if<native_order::Trail>(&request.trigger)) {
5015 // An attempted request still carries the kernel's tick spelling;
5016 // acceptance resolves it against the same mintick, so read the
5017 // price distance this leg will be stored with.
5018 const double trail_offset = trail->ticks
5019 ? trail->ticks->ticks * staged_.syminfo.mintick : trail->offset;
5020 // exit() arms the leg at source_trail_arm_level of the source
5021 // trail_price, on the close side of the cohort pinned above.
5022 const auto arm_of_prior = [&](bool exit_is_buy) {
5023 return source_trail_arm_level(prior.exit_levels.trail_price,
5024 staged_.syminfo.mintick, exit_is_buy);
5025 };
5026 return snapshot.family == PineOrderFamily::ExitTrail
5027 && same_double_bits(trail_offset, prior.exit_levels.trail_offset)
5028 && trail->arm_price.has_value() == std::isfinite(prior.exit_levels.trail_price)
5029 && (!trail->arm_price
5030 || same_double_bits(*trail->arm_price, arm_of_prior(true))
5031 || same_double_bits(*trail->arm_price, arm_of_prior(false)));
5032 }
5033 return false;
5034 };
5035 std::optional<std::uint64_t> retained_source_sequence;
5036 if (key != 0) {
5037 std::optional<native_order::RequestHandle> existing_handle;
5038 if (const auto existing = live_by_source_key_.find(key);
5039 existing != live_by_source_key_.end()) {
5040 // `retire` removes this key from live_by_source_key_. Keep the
5041 // handle independent of the map node before either operation.
5042 existing_handle = existing->second;
5043 if (const auto previous = placement_.find(existing_handle->incarnation);
5044 previous != placement_.end()) {
5045 predecessor_snapshot = &previous->second;
5046 // The legacy pending book leaves a same-definition bracket
5047 // untouched. Its source cohort remains live and its dynamic
5048 // close quantity is resolved at fill time, so a normal-bar
5049 // reissue has no new executable fact to record.
5050 if (unchanged_dynamic_exit(previous->second)
5051 && !reissue_revives_declined_exit(previous->second)) {
5052 if (previous->second.void_issue && !snapshot.void_issue)
5053 unvoid_exit(previous->second);
5054 return existing_handle;
5055 }
5056 const auto family = previous->second.family;
5057 predecessor_exit = family == PineOrderFamily::ExitLimit
5058 || family == PineOrderFamily::ExitStop || family == PineOrderFamily::ExitTrail;
5059 predecessor_market = family == PineOrderFamily::Entry
5060 && !std::isfinite(previous->second.exit_levels.limit)
5061 && !std::isfinite(previous->second.exit_levels.stop)
5062 && !std::isfinite(previous->second.exit_levels.trail_offset);
5063 }
5064 }
5065 if (existing_handle) {
5066 // A close-time re-issue whose activation is unchanged but whose
5067 // offset alone moves keeps the running trail extreme. The
5068 // native Trail owns that evolving generic state, so replacing it
5069 // would incorrectly reset the track at every script close. The
5070 // source projection still records the latest offset operand.
5071 if (predecessor_snapshot
5072 && predecessor_snapshot->family == PineOrderFamily::ExitTrail
5073 && snapshot.family == PineOrderFamily::ExitTrail) {
5074 const auto same = [](double left, double right) {
5075 return (std::isnan(left) && std::isnan(right)) || left == right;
5076 };
5077 const bool same_activation = same(predecessor_snapshot->exit_levels.trail_points,
5078 snapshot.exit_levels.trail_points)
5079 && same(predecessor_snapshot->exit_levels.trail_price,
5080 snapshot.exit_levels.trail_price);
5081 const bool offset_changed = !same(predecessor_snapshot->exit_levels.trail_offset,
5082 snapshot.exit_levels.trail_offset);
5083 if (same_activation) {
5084 const auto live = placement_.find(existing_handle->incarnation);
5085 if (live != placement_.end()) {
5086 if (offset_changed) {
5087 if (const auto trail = host.trail_state(*existing_handle);
5088 trail && trail->activated
5089 && std::isfinite(trail->best_price)) {
5090 live->second.retained_trail_best = trail->best_price;
5091 }
5092 }
5093 if (live->second.void_issue && !snapshot.void_issue)
5094 unvoid_exit(live->second);
5095 live->second.exit_levels.trail_offset = snapshot.exit_levels.trail_offset; invalidate_retirement();
5096 live->second.trail_activation_level = snapshot.trail_activation_level;
5097 live->second.requested_qty = snapshot.requested_qty;
5098 live->second.projection_remaining_qty = snapshot.projection_remaining_qty;
5099 live->second.qty_percent = snapshot.qty_percent;
5100 live->second.comment = snapshot.comment;
5101 live->second.sizing = snapshot.sizing;
5102 }
5103 refresh_pending_view();
5104 return existing_handle;
5105 }
5106 if (std::isfinite(snapshot.sizing.price))
5107 snapshot.retained_trail_best = snapshot.sizing.price;
5108 }
5109 // The handle-preserving reissue change: the re-issue carries a close's book binding
5110 // (ReplaceOptions::keep_binding). A leg the kernel bound to the
5111 // position binds to it again at its next point with no
5112 // CloseBoundEvent when the position has not moved, and exactly
5113 // as before when it has; every other successor carries nothing.
5114 // The handle is not kept: this table, the families and the
5115 // pending-order rows name each placement by the incarnation its
5116 // own command was issued.
5117 native_order::ReplaceOptions reissue;
5118 reissue.keep_binding = detail::carry_reissue_bindings();
5119 const auto result = host.replace(*existing_handle, request, reissue);
5120 if (result.status == native_order::ReplaceStatus::Replaced && result.successor) {
5121 // ab9714be pine_strategy_host.cpp:239-241 + 270-275: the flatten
5122 // unbinds every exit's leg activation and the fresh open rebinds
5123 // only RETAINED activations, so a same-id exit that replaces a
5124 // bracket across a stop-out starts its successor with clean legs
5125 // instead of inheriting the finished cycle's touched stop / trail
5126 // state and firing it against the new lot.
5127 if (const auto found_p = placement_.find(existing_handle->incarnation);
5128 found_p != placement_.end()) {
5129 predecessor_legs = std::move(found_p->second.legs);
5130 found_p->second.legs = {}; invalidate_retirement();
5131 }
5132 snapshot.projection_predecessor = existing_handle->incarnation;
5133 if (predecessor_snapshot) {
5134 const auto family = predecessor_snapshot->family;
5135 snapshot.projection_predecessor_exit = family == PineOrderFamily::ExitLimit
5136 || family == PineOrderFamily::ExitStop || family == PineOrderFamily::ExitTrail;
5137 snapshot.projection_predecessor_market = family == PineOrderFamily::Entry
5138 && !std::isfinite(predecessor_snapshot->exit_levels.limit)
5139 && !std::isfinite(predecessor_snapshot->exit_levels.stop)
5140 && !std::isfinite(predecessor_snapshot->exit_levels.trail_offset);
5141 const bool fresh_after_dormant = predecessor_legs.dormant()
5145 // A void call's request was never TradingView's order: a
5146 // re-issue made once the entry exists starts afresh.
5147 const bool fresh_after_void =
5148 predecessor_snapshot->void_issue && !snapshot.void_issue;
5149 const auto predecessor_revision = predecessor_legs.revision();
5150 // pine_execution_lifecycle.cpp's same-(id, from_entry)
5151 // reissue replaces a dormant bracket wholesale. Carrying
5152 // its suspension into the successor would leave the fresh
5153 // source prices permanently unmatchable.
5154 if (!fresh_after_dormant && !fresh_after_void) {
5155 snapshot.legs = std::move(predecessor_legs);
5156 snapshot.leg_activation = predecessor_snapshot->leg_activation;
5157 snapshot.exit_activation = predecessor_snapshot->exit_activation;
5158 snapshot.restored_after_margin =
5159 predecessor_snapshot->restored_after_margin;
5160 }
5161 if (!fresh_after_void) {
5162 snapshot.reservation_expansion =
5163 predecessor_snapshot->reservation_expansion;
5164 snapshot.reservation_growth_source =
5165 predecessor_snapshot->reservation_growth_source;
5166 }
5167 snapshot.cancellation = {PineCancellationCause::Replacement, 1, 0,
5168 existing_handle->incarnation,
5169 static_cast<std::int64_t>(predecessor_snapshot->source_sequence),
5170 existing_handle->incarnation, predecessor_snapshot->placement_cycle,
5171 predecessor_revision,
5172 predecessor_snapshot->requested_qty, kNaN};
5173 if ((family == PineOrderFamily::ExitLimit
5174 || family == PineOrderFamily::ExitStop
5175 || family == PineOrderFamily::ExitTrail)
5176 && !snapshot.from_entry.empty()) {
5177 const auto token = named_entry_cancel_tokens_.find(snapshot.from_entry);
5178 bool retained_child = token != named_entry_cancel_tokens_.end()
5179 && token->second.surviving_exit_incarnation
5180 == existing_handle->incarnation;
5181 for (const auto& live : live_handles_) {
5182 const auto parent = placement_.find(live.incarnation);
5183 if (parent != placement_.end() && parent->second.opening
5184 && parent->second.family == PineOrderFamily::Entry
5185 && parent->second.source_id == snapshot.from_entry
5186 && (parent->second.named_cancel_surviving_exit_incarnation
5187 == existing_handle->incarnation
5188 || predecessor_snapshot->source_sequence
5189 < parent->second.source_sequence)) {
5190 retained_child = true;
5191 break;
5192 }
5193 }
5194 if (retained_child
5195 && (!predecessor_snapshot->void_issue || snapshot.void_issue)) {
5196 retained_source_sequence = predecessor_snapshot->source_sequence;
5197 }
5198 }
5199 // ab9714be pine_strategy_commands.cpp:482: same-id entry replacement preserves created_seq (preserved_seq > 0)
5200 if ((family == PineOrderFamily::Entry && snapshot.family == PineOrderFamily::Entry)
5201 || (family == PineOrderFamily::Order && snapshot.family == PineOrderFamily::Order)) {
5202 retained_source_sequence = predecessor_snapshot->source_sequence;
5203 }
5204 }
5205 snapshot.projection_predecessor_exit = predecessor_exit;
5206 snapshot.projection_predecessor_market = predecessor_market;
5207 reused_key_node = live_by_source_key_.extract(key);
5208 retire(*existing_handle);
5209 accepted = *result.successor;
5210 }
5211 }
5212 }
5213 if (auto* member = std::get_if<native_order::Member>(&request.group)) {
5214 if (source_sequence_ >= static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max())) {
5215 throw coded<std::overflow_error>(RunFailureCode::engine_invariant, {},
5216 "Pine OCA member sequence exhausted");
5217 }
5218 // The generic group member's cohort distinguishes siblings. A source
5219 // OCA name identifies the group; every accepted source instruction is
5220 // a distinct member of it, including replacement incarnations.
5221 member->cohort = static_cast<std::int64_t>(source_sequence_ + 1U);
5222 }
5223 if (!accepted && materializing_relative_ && !opening) {
5224 // The anchored-leg integration: the kernel already armed this leg at the parent's
5225 // fill. When the request this fill point builds is that very trigger
5226 // on that very group, the armed child IS the accepted request and
5227 // nothing is submitted; every source fact of the snapshot stays the
5228 // fill point's own.
5229 const auto armed = std::find_if(
5230 anchored_relative_legs_.begin(), anchored_relative_legs_.end(),
5231 [&](const AnchoredRelativeLeg& leg) {
5232 return leg.armed && leg.parent == materializing_parent_
5233 && leg.family == snapshot.family
5234 && leg.exit_id == snapshot.source_id
5235 && leg.from_entry == snapshot.from_entry;
5236 });
5237 if (armed != anchored_relative_legs_.end()) {
5238 const auto same_trigger = [&] {
5239 const auto& mine = armed->request.trigger;
5240 if (mine.index() != request.trigger.index()) return false;
5241 if (const auto* limit = std::get_if<native_order::Limit>(&request.trigger)) {
5242 return same_double_bits(limit->price, armed->installed_level)
5243 && limit->fill_through
5244 == std::get<native_order::Limit>(mine).fill_through;
5245 }
5246 if (const auto* stop = std::get_if<native_order::Stop>(&request.trigger))
5247 return same_double_bits(stop->price, armed->installed_level);
5248 if (const auto* trail = std::get_if<native_order::Trail>(&request.trigger)) {
5249 const auto& anchored = std::get<native_order::Trail>(mine);
5250 return trail->arm_price && trail->ticks && anchored.ticks
5251 && same_double_bits(*trail->arm_price, armed->installed_level)
5252 && same_double_bits(trail->ticks->ticks, anchored.ticks->ticks)
5253 && trail->offset == 0.0;
5254 }
5255 return false;
5256 }();
5257 const auto* mine_group = std::get_if<native_order::Member>(&armed->request.group);
5258 const auto* group = std::get_if<native_order::Member>(&request.group);
5259 const bool same_group = mine_group && group && mine_group->group == group->group
5260 && mine_group->effect == group->effect;
5261 // The armed child is a host-sized close of the book (its arm
5262 // scope); so must the request be, or it is another request.
5263 const auto* sized = std::get_if<native_order::HostSized>(&request.intent);
5264 const bool same_close = sized && sized->kind == native_order::HostSizedKind::Close
5265 && !sized->side
5266 && std::holds_alternative<native_order::Independent>(request.owner)
5267 && std::holds_alternative<native_order::ImmediateRemaining>(request.capacity);
5268 if (same_trigger && same_group && same_close) {
5269 accepted = armed->handle;
5270 anchored_relative_legs_.erase(armed);
5271 ++anchored_relative_stats_.adopted;
5272 }
5273 }
5274 }
5275 if (!accepted) {
5276 const auto result = host.submit(request);
5277 if (result.status != native_order::SubmitStatus::Accepted || !result.handle) return std::nullopt;
5278 accepted = *result.handle;
5279 }
5280 snapshot.opening = opening;
5281 if (!std::isfinite(snapshot.projection_remaining_qty)
5282 && std::isfinite(snapshot.requested_qty)) {
5283 snapshot.projection_remaining_qty = snapshot.requested_qty;
5284 }
5285 const auto next_source_sequence = ++source_sequence_;
5286 const bool staged_replacement_sequence = snapshot.projection_predecessor != 0
5287 && snapshot.source_sequence != 0;
5288 snapshot.source_sequence = retained_source_sequence
5289 ? *retained_source_sequence
5290 : (staged_replacement_sequence ? snapshot.source_sequence : next_source_sequence);
5291 if (admission_observation) {
5292 admission::CommandEvent event;
5293 event.observation = admission_observation;
5294 event.outcome = admission::Outcome::Admitted;
5295 event.admitted_incarnation = accepted->incarnation;
5296 admission_journal.append(std::move(event));
5297 }
5298 // remember() consumes the snapshot; the same-id cleanup below reads the
5299 // accepted request's family and its place in the source command order.
5300 const PineOrderFamily accepted_family = snapshot.family;
5301 const std::uint64_t accepted_command_sequence = snapshot.command_sequence;
5302 remember(*accepted, std::move(snapshot));
5303 // ab9714be pine_strategy_commands.cpp:445-446 (entry/order) and
5304 // 1694-1703 (exit): an accepted same-id instruction removes the prior
5305 // pending order, including one still queued here for a later submission.
5306 if (!replacement_key.empty()) {
5307 pending_write(pending_coof_requests_).erase(
5308 std::remove_if(pending_coof_requests_.begin(), pending_coof_requests_.end(),
5309 [&](const PendingCoofRequest& row) { return row.replacement_key == replacement_key; }),
5310 pending_coof_requests_.end());
5311 pending_write(pending_bracket_legs_).erase(
5312 std::remove_if(pending_bracket_legs_.begin(), pending_bracket_legs_.end(),
5313 [&](const PendingBracketLeg& row) { return row.replacement_key == replacement_key; }),
5314 pending_bracket_legs_.end());
5315 delayed_market_orders_.erase(
5316 std::remove_if(delayed_market_orders_.begin(), delayed_market_orders_.end(),
5317 [&](const DelayedMarketOrder& row) { return row.replacement_key == replacement_key; }),
5318 delayed_market_orders_.end());
5319 // A flat calc_on_order_fills evaluation holds its priced entries
5320 // back until the evaluation ends (submit_entry_with_policy). An
5321 // accepted entry replaces a held earlier entry of the same id, so
5322 // the held row is dropped here: flushed when the evaluation ends, it
5323 // would replace the later entry instead. The rule is entry to entry:
5324 // an accepted strategy.order call leaves the held row alone. A row
5325 // issued after the accepted entry stays, and so does one waiting for
5326 // a close (paired_reversal_parent) or a recreated parent
5327 // (retained_parent_topology): those keep their own submission rules.
5328 if (opening && accepted_family == PineOrderFamily::Entry
5329 && config_.calc_on_order_fills && !coof_recalc_active_) {
5330 pending_write(pending_entries_).erase(
5331 std::remove_if(pending_entries_.begin(), pending_entries_.end(),
5332 [&](const PendingEntry& row) {
5333 return row.replacement_key == replacement_key
5334 && row.snapshot.family == PineOrderFamily::Entry
5335 && row.snapshot.opening
5336 && row.snapshot.paired_reversal_parent.incarnation == 0
5337 && !row.snapshot.retained_parent_topology
5338 && row.snapshot.command_sequence < accepted_command_sequence;
5339 }),
5340 pending_entries_.end());
5341 }
5342 }
5343 if (key != 0) {
5344 if (reused_key_node) {
5345 reused_key_node.mapped() = live_source_value(*accepted, replacement_key);
5346 auto placed = live_by_source_key_.insert(std::move(reused_key_node));
5347 if (!placed.inserted) { (void)checked_live_key(replacement_key); placed.position->second = live_source_value(*accepted, replacement_key); }
5348 } else {
5349 live_by_source_key_[checked_live_key(replacement_key)] = live_source_value(*accepted, replacement_key);
5350 }
5351 }
5352 if (opening) {
5353 const auto source = placement_.at(accepted->incarnation).source_id;
5354 const auto cohort = cohort_for(source);
5355 host.cohort_add(cohort, *accepted);
5356 require_cohort(source)->second.origins.push_back(*accepted);
5357 }
5358 const auto& accepted_snapshot = placement_.at(accepted->incarnation);
5359 if (coof_recalc_active_
5360 && ((std::holds_alternative<native_order::Market>(request.trigger)
5361 && (coof_first_open_
5362 || finite_positive(accepted_snapshot.forced_execution_price)))
5363 || (coof_first_open_
5364 && finite_positive(accepted_snapshot.forced_execution_price)))
5365 && std::holds_alternative<native_order::ImmediateRemaining>(request.capacity)
5366 && detail::callback_point(host)) {
5367 first_open_newborns_.push_back(*accepted);
5368 }
5369 return accepted;
5370}
5371
5372std::vector<native_order::RequestHandle> PineExecutionAdapter::openings_for(const SourceId& id) const {
5373 const auto found = cohorts_by_id_.find(id);
5374 return found == cohorts_by_id_.end() ? std::vector<native_order::RequestHandle>{}
5375 : found->second.opened;
5376}
5377
5378bool PineExecutionAdapter::from_entry_filled_this_cycle(const SourceId& id) const {
5379 // The source analogue of ab9714be cycle_filled_entry_ids_: the id opened
5380 // in the live position cycle (reset when the book goes flat).
5381 const auto found = cohorts_by_id_.find(id);
5382 return found != cohorts_by_id_.end() && current_position_cycle_ > 0
5383 && found->second.cycle == current_position_cycle_;
5384}
5385
5386double PineExecutionAdapter::cohort_exposure_for(const SourceId& id) const noexcept {
5387 const auto found = cohorts_by_id_.find_resident(id);
5388 return found == cohorts_by_id_.end() ? 0.0 : cohort_exposure_for(found->second);
5389}
5390
5391double PineExecutionAdapter::cohort_exposure_for(const CohortFacts& facts) const noexcept {
5392 PINEFORGE_LOOKUP_INDEX_WORK("cohort_exposure_call", this, run_counter_, 1);
5393 // Quantities follow source insertion order, never the lookup table's
5394 // hash buckets. Each retained position contributes separately.
5395 // Repeated incarnations contribute once per occurrence, in that order.
5396 // Allocation-free: pending-order readback calls this const noexcept member.
5397 // Finite/positive filtering and the final clamp preserve overflow to zero.
5398 // Reordering, regrouping or multiplying repeated units changes bits.
5399 // Diagnostic work belongs to the returned value, never adapter state.
5400 // The seam receives the original roster and map by const reference.
5401 // Global positions keep their order after prefix retirement.
5402 // A unit row without a retained position contributes no term.
5403 const auto folded = detail::fold_cohort_exposure(facts.origins,
5404 facts.live_units_by_origin);
5405 PINEFORGE_LOOKUP_INDEX_WORK("cohort_exposure_step", this, run_counter_, folded.steps);
5406 PINEFORGE_LOOKUP_INDEX_WORK("cohort_exposure_window", this, run_counter_,
5407 facts.origins.members().size());
5408 PINEFORGE_LOOKUP_INDEX_WORK("cohort_exposure_rows", this, run_counter_,
5409 facts.live_units_by_origin.size());
5410 if (folded.path == 2)
5411 PINEFORGE_LOOKUP_INDEX_WORK("cohort_exposure_path_b", this, run_counter_, 1);
5412 if (folded.path == 3)
5413 PINEFORGE_LOOKUP_INDEX_WORK("cohort_exposure_b_abandoned", this, run_counter_, 1);
5414#ifdef PINEFORGE_COHORT_EXPOSURE_FIXTURES
5415 detail::verify_cohort_exposure(facts.origins, facts.live_units_by_origin,
5416 folded.total);
5417#endif
5418 // The fixture oracle is independent of the selected production path.
5419 // It checks every call, including both empty-state early returns.
5420 // No aggregate or reservation state is updated by this read.
5421 // Callers receive only the clamped exposure value.
5422 return folded.total;
5423}
5424
5425// The units of the position an exit's entry (from_entry) holds; none for an
5426// exit that names no entry, which acts on the whole position. An exit whose
5427// entry holds none is inert: it fills nothing until that entry fills. Each
5428// caller keeps its own test: the point order at the open counts an exit only
5429// above kQtyEpsilon, and the bracket ranking under process_orders_on_close
5430// (flush_pending_bracket_legs) drops one only while its entry also has no
5431// working order.
5432std::optional<double> PineExecutionAdapter::from_entry_units(
5433 const SourceId& from_entry) const noexcept {
5434 if (from_entry.empty()) return std::nullopt;
5435 return cohort_exposure_for(from_entry);
5436}
5437
5439 assert(cohorts_by_id_.resident_opened_slot_count() == cohort_opened_slot_count_);
5440 if (fixture_verify_source_entry_slots_) {
5441 std::size_t direct = 0;
5442 for (const auto& row : cohorts_by_id_.resident_rows()) direct += row.second.opened.size();
5443 if (direct != cohort_opened_slot_count_) std::abort();
5444 }
5445 return cohort_opened_slot_count_ > static_cast<std::size_t>(std::numeric_limits<int>::max())
5446 ? std::numeric_limits<int>::max()
5447 : static_cast<int>(cohort_opened_slot_count_);
5448}
5449
5452 // bytes_per_row counts PHYSICAL resident rows; deferred empty rows inflate
5453 // the estimate even though they carry no live cohort members.
5454 CohortHeapEstimate result;
5455 result.resident_rows = cohorts_by_id_.resident_size();
5457 result.directory_bytes = cohorts_by_id_.resident_directory_heap_estimate();
5458 for (const auto& row : cohorts_by_id_.resident_rows()) {
5459 result.key_bytes += row.first.capacity() + 1;
5460 const auto& facts = row.second;
5461 result.origin_bytes += facts.origins.heap_bytes_estimate();
5462 result.opened_bytes += facts.opened.capacity() * sizeof(native_order::RequestHandle);
5463 // Session keys inside handles are shared run-level objects: not counted.
5464 result.live_unit_bytes += facts.live_units_by_origin.bucket_count() * sizeof(void*)
5465 + facts.live_units_by_origin.size()
5466 * (sizeof(decltype(facts.live_units_by_origin)::value_type) + 2 * sizeof(void*));
5467 }
5468 result.total_bytes = result.row_bytes + result.key_bytes + result.origin_bytes
5469 + result.opened_bytes + result.live_unit_bytes + result.directory_bytes;
5470 result.bytes_per_row = result.resident_rows == 0 ? 0 : result.total_bytes / result.resident_rows;
5471 return result;
5472}
5473
5474double PineExecutionAdapter::percent_commission_live_equity(double mark) const noexcept {
5475 return percent_commission_live_equity(mark, std::nullopt);
5476}
5477
5478double PineExecutionAdapter::percent_commission_live_equity(
5479 double mark, std::optional<double> fx) const noexcept {
5480 if (!host_) return std::numeric_limits<double>::quiet_NaN();
5481 // ab9714be pine_fills.cpp:1411-1426: entry-bar affordability marks open positions
5482 // from closed equity (initial capital + realized net profit) minus the open
5483 // entries' percentage commission, plus the lot's open PnL.
5484 if (auto* pine = pine_view_of(host_)) {
5485 if (config_.commission_type == static_cast<int>(CommissionType::PERCENT)
5486 && config_.commission_value > 0.0
5487 && pine->position_side_ != PositionSide::FLAT) {
5488 double paid_open_commission = 0.0;
5489 for (const auto& pe : pine->pyramid_entries_) {
5490 if (pe.qty <= internal::kQtyEpsilon) continue;
5491 const double fee = pine->open_entry_commission(pe);
5492 if (!std::isfinite(fee)) return std::numeric_limits<double>::quiet_NaN();
5493 paid_open_commission += fee;
5494 }
5495 const double open = fx ? pine->open_profit_at(mark, *fx)
5496 : pine->BacktestEngine::open_profit(mark);
5497 return (pine->closed_trade_equity() + open) - paid_open_commission;
5498 }
5499 }
5500 const double marked = strategy_equity_at(mark, fx);
5501 if (!std::isfinite(marked)) return marked;
5502 // `marked_equity()` accounts for every open entry fee. The legacy money
5503 // gates read an equity that subtracts only surviving PERCENT entry
5504 // commissions, so restore the adapter-recorded
5505 // cash-per-order/contract fees without changing generic accounting or
5506 // marked equity itself. A percent-of-equity default QUANTITY does not
5507 // read this: TradingView sizes it from strategy.equity, cash fees charged
5508 // (default_sizing_cash, the cash-sizing fee correction). source_margin_money also
5509 // charges cash-per-contract fees for its tape-pinned margin policy.
5510 double restored = 0.0;
5511 for (const auto& fact : open_entry_fees_) {
5512 if (!std::isfinite(fact.nonpercent_fee))
5513 return std::numeric_limits<double>::quiet_NaN();
5514 restored += fact.nonpercent_fee;
5515 }
5516 return std::isfinite(restored) ? marked + restored
5517 : std::numeric_limits<double>::quiet_NaN();
5518}
5519
5520double PineExecutionAdapter::strategy_equity_at(
5521 double mark, std::optional<double> fx) const noexcept {
5522 if (!host_) return std::numeric_limits<double>::quiet_NaN();
5523 const auto* pine = pine_view_of(host_);
5524 return fx && pine ? pine->marked_equity_at(mark, *fx) : host_->native_marked_equity(mark);
5525}
5526
5527void PineExecutionAdapter::mark_sizing_equity(
5528 PineSizingSnapshot& sizing, double mark, std::optional<double> fx) const noexcept {
5529 sizing.equity = percent_commission_live_equity(mark, fx);
5530 if (!fx) {
5531 const double mirrored = gain_loss_signal_equity(mark);
5532 if (std::isfinite(mirrored)) sizing.equity = mirrored;
5533 }
5534 sizing.strategy_equity = default_sizing_reserves_cash_fee()
5535 ? strategy_equity_at(mark, fx) : kNaN;
5536}
5537
5538void PineExecutionAdapter::record_opening_fee(
5539 const PlacementSnapshot& source, const native_order::ExecutionAppliedEvent& event) {
5540 const double opened = std::abs(event.opened_units);
5541 if (!(opened > 0.0) || !std::isfinite(opened)
5542 || config_.commission_type == static_cast<int>(CommissionType::PERCENT)) {
5543 return;
5544 }
5545 double fee = 0.0;
5546 if (config_.commission_type == static_cast<int>(CommissionType::CASH_PER_CONTRACT)) {
5547 fee = config_.commission_value * opened;
5548 } else if (config_.commission_type == static_cast<int>(CommissionType::CASH_PER_ORDER)) {
5549 const double total = opened + std::abs(event.closed_units);
5550 fee = total > 0.0 ? config_.commission_value * opened / total : 0.0;
5551 }
5552#ifdef PINEFORGE_VALUE_INVALID_FIXTURES
5553 fee = fixture_adapter_execution_value("fee.adapter.opening", fee);
5554#endif
5555 if (!std::isfinite(fee)) {
5556 engine_command_value_invalid_or_count("fee.commission", EngineValueStage::fee,
5557 EngineValueReason::numeric_overflow);
5558 return;
5559 }
5560 open_entry_fees_.push_back({event.handle(), source.source_id, opened, fee});
5561}
5562
5563void PineExecutionAdapter::consume_opening_fees(
5564 const native_order::ExecutionAppliedEvent& event, const SourceId* source_id) {
5565 if (!(event.closed_units > 0.0) || !std::isfinite(event.closed_units)) return;
5566 std::vector<std::uint64_t> selected;
5567 if (const auto* opening = std::get_if<execution::OpeningExposure>(&event.scope)) {
5568 selected.push_back(opening->incarnation);
5569 } else if (const auto* openings = std::get_if<native_order::SelectedExposure>(&event.scope)) {
5570 selected = openings->incarnations;
5571 }
5572 double remaining = event.closed_units;
5573 for (auto it = open_entry_fees_.begin(); it != open_entry_fees_.end() && remaining > 0.0;) {
5574 const bool selected_origin = selected.empty()
5575 || std::find(selected.begin(), selected.end(), it->opening.incarnation) != selected.end();
5576 if (!selected_origin || (source_id && it->source_id != *source_id)
5577 || !(it->units > 0.0) || !std::isfinite(it->units)) {
5578 ++it;
5579 continue;
5580 }
5581 const double consumed = std::min(it->units, remaining);
5582 const double fraction = consumed / it->units;
5583 it->units -= consumed;
5584 it->nonpercent_fee -= it->nonpercent_fee * fraction;
5585 remaining -= consumed;
5586 if (!(it->units > 0.0)) it = open_entry_fees_.erase(it); else ++it;
5587 }
5588}
5589
5590void PineExecutionAdapter::record_dropped_close(
5591 const SourceId& id, const std::string& comment, double qty, double qty_percent,
5592 bool immediately, std::uint64_t callsite_token) {
5593 dropped_close_digest_ = fold_dropped_close(dropped_close_digest_,
5594 {id, comment, qty, qty_percent, immediately, callsite_token, command_ordinal_});
5595 ++dropped_close_count_;
5596}
5597
5598double PineExecutionAdapter::quantize_close_units(double basis, double percent) const noexcept {
5599 if (!std::isfinite(basis) || basis <= 0.0 || !std::isfinite(percent) || percent <= 0.0)
5600 return 0.0;
5601 // ab9714be pine_fills.cpp:6893-6896: `is_partial` is decided by
5602 // `qp < 100.0 - kFullPercentEps`, so a reservation percentage that only
5603 // misses 100 by the binary64 rounding of `reserved / live_basis * 100`
5604 // (pine_fills.cpp:7124) is a FULL exit and books the whole live basis.
5605 // Sizing it through the multiply/grid floor instead leaves a sub-lot dust
5606 // remainder that never flattens (ab9714be pine_fills.cpp:1618 spells the
5607 // same whole-position coverage as `qty - kQtyEpsilon`).
5608 if (percent >= 100.0 - internal::kFullPercentEps) {
5609 if (staged_.quantity_grid && *staged_.quantity_grid > 0.0) {
5610 const double nearest = std::round(basis / *staged_.quantity_grid) * *staged_.quantity_grid;
5611 if (std::abs(basis - nearest) <= internal::kQtyEpsilon) return nearest;
5612 }
5613 return basis;
5614 }
5615 double units = basis * percent / 100.0;
5616 if (!std::isfinite(units) || units <= 0.0) return 0.0;
5617 return quantize_percent_exit_units(units, basis);
5618}
5619
5620double PineExecutionAdapter::quantize_percent_exit_units(
5621 double requested, double available) const noexcept {
5622 if (!std::isfinite(requested) || requested <= 0.0) return 0.0;
5623 if (!staged_.quantity_grid || !std::isfinite(*staged_.quantity_grid)
5624 || *staged_.quantity_grid <= 0.0) {
5625 return requested;
5626 }
5627 const double step = *staged_.quantity_grid;
5628 const double floored = std::floor(requested / step + 1e-6) * step;
5629 double result = floored < requested ? floored : requested;
5630 // ab9714be:engine.hpp:1587-1598. A percentage exit keeps one minimum
5631 // unit while one full step of reservation capacity remains: 50 % of one
5632 // contract, and of one 0.01 lot of OANDA:XAUUSD, reserves the whole of it
5633 // on TradingView (lane TAIL-H rule PS, tape tailh-d1a).
5634 if (requested < step && available + internal::kQtyEpsilon >= step) result = step;
5635 return result;
5636}
5637
5638bool PineExecutionAdapter::compute_exit_reservation(
5639 const SourceId& exit_id, const SourceId& from_entry,
5640 double requested_qty, double& qty_percent, double live_basis,
5641 double percentage_basis, double& reserved_qty) const {
5642 qty_percent = std::isfinite(qty_percent)
5643 ? std::clamp(qty_percent, 0.0, 100.0) : 100.0;
5644 reserved_qty = kNaN;
5645 if (!(live_basis > internal::kQtyEpsilon)) {
5646 if (std::isfinite(requested_qty)) {
5647 reserved_qty = std::abs(requested_qty);
5648 return reserved_qty > internal::kQtyEpsilon;
5649 }
5650 return true;
5651 }
5652
5653 struct Reservation {
5654 std::uint64_t family = 0;
5655 double units = kNaN;
5656 double percent = 100.0;
5657 bool explicit_units = false;
5658 std::vector<std::uint64_t> origins;
5659 // The family's place in the exit queue (exit_queue_rank); a
5660 // strategy.close reservation has none and always holds its units.
5661 std::uint64_t rank = std::numeric_limits<std::uint64_t>::max();
5662 bool queued_exit = false;
5663 };
5664 std::vector<Reservation> reservations;
5665 auto observe = [&](const PlacementSnapshot& snapshot) {
5666 const bool queued_exit = snapshot.family == PineOrderFamily::ExitLimit
5669 const bool exit = queued_exit || snapshot.family == PineOrderFamily::Close;
5670 // A call TradingView voided reserves nothing (void_issue).
5671 if (!exit || snapshot.from_entry != from_entry || snapshot.void_issue) return;
5672 // ab9714be pine_orders.cpp:599-602: close reservations only apply to positions matching active position_cycle_seq_
5673 if (snapshot.family == PineOrderFamily::Close
5674 && snapshot.placement_cycle != 0
5675 && snapshot.placement_cycle < current_position_cycle_) {
5676 return;
5677 }
5678 const auto family = key_for(snapshot.source_id, snapshot.from_entry);
5679 auto row = std::find_if(reservations.begin(), reservations.end(),
5680 [&](const Reservation& value) { return value.family == family; });
5681 const double percent = std::isfinite(snapshot.qty_percent)
5682 ? std::clamp(snapshot.qty_percent, 0.0, 100.0) : 100.0;
5683 const double units = std::isfinite(snapshot.projection_remaining_qty)
5684 ? std::max(0.0, snapshot.projection_remaining_qty)
5685 : (std::isfinite(snapshot.requested_qty)
5686 ? std::max(0.0, std::abs(snapshot.requested_qty))
5687 // ab9714be src/source/pine_strategy_commands.cpp:2761 and
5688 // src/source/pine_fills.cpp:7094 both reserve a sibling leg's
5689 // share as ``live_pos * (oqp / 100.0)``, percent divided first.
5690 : live_basis * (percent / 100.0));
5691 const bool explicit_units = std::isfinite(snapshot.requested_qty);
5692 const auto origin = snapshot.bracket_origin.incarnation;
5693 const auto rank = queued_exit ? exit_queue_rank(placement_, snapshot)
5694 : std::numeric_limits<std::uint64_t>::max();
5695 if (row == reservations.end()) {
5696 Reservation next;
5697 next.family = family;
5698 next.units = units;
5699 next.percent = percent;
5700 next.explicit_units = explicit_units;
5701 if (explicit_units) next.origins.push_back(origin);
5702 next.rank = rank;
5703 next.queued_exit = queued_exit;
5704 reservations.push_back(std::move(next));
5705 } else {
5706 if (explicit_units && row->explicit_units
5707 && std::find(row->origins.begin(), row->origins.end(), origin)
5708 == row->origins.end()) {
5709 row->units += units;
5710 row->origins.push_back(origin);
5711 } else {
5712 row->units = std::max(row->units, units);
5713 }
5714 row->explicit_units = row->explicit_units || explicit_units;
5715 row->percent = std::max(row->percent, percent);
5716 row->rank = std::min(row->rank, rank);
5717 row->queued_exit = row->queued_exit && queued_exit;
5718 }
5719 };
5720 for (const auto& handle : live_handles_) {
5721 const auto found = placement_.find(handle.incarnation);
5722 if (found != placement_.end()) observe(found->second);
5723 }
5724 for (const auto& pending : pending_bracket_legs_) observe(pending.snapshot);
5725 for (const auto& pending : pending_coof_requests_) observe(pending.snapshot);
5726 for (const auto& delayed : delayed_market_orders_) observe(delayed.snapshot);
5727
5728 const auto this_family = key_for(exit_id, from_entry);
5729 // A re-issued exit keeps its place in the queue (exit_queue_rank): an exit
5730 // created after it holds nothing ahead of it, however it was re-issued
5731 // since. A new exit is at the back and yields to every one already there.
5732 std::uint64_t this_rank = std::numeric_limits<std::uint64_t>::max();
5733 for (const auto& reservation : reservations) {
5734 if (reservation.family == this_family) this_rank = reservation.rank;
5735 }
5736 double already_reserved = 0.0;
5737 double preserved_reserved = kNaN;
5738 bool other_full_exit = false;
5739 for (const auto& reservation : reservations) {
5740 if (reservation.family == this_family) {
5741 if (std::isfinite(reservation.units)) {
5742 preserved_reserved = std::isfinite(preserved_reserved)
5743 ? std::max(preserved_reserved, reservation.units)
5744 : reservation.units;
5745 }
5746 continue;
5747 }
5748 if (reservation.queued_exit && reservation.rank > this_rank) continue;
5749 if (std::isfinite(reservation.units)) already_reserved += reservation.units;
5750 if (reservation.percent >= 100.0 - internal::kFullPercentEps) other_full_exit = true;
5751 }
5752 const double available = std::max(0.0, live_basis - already_reserved);
5753 if (std::isfinite(requested_qty)) {
5754 reserved_qty = std::min(std::abs(requested_qty), available);
5755 } else if (qty_percent < 100.0 - internal::kFullPercentEps
5756 && std::isfinite(preserved_reserved)) {
5757 reserved_qty = std::min(preserved_reserved, live_basis);
5758 } else {
5759 // A full source bracket owns the exact live exposure. Multiplying
5760 // that exposure by 100/100 can round down by one binary64 step and
5761 // turn the legacy execute_market_exit branch into a dust reduction
5762 // (ab9714be:src/source/pine_fills.cpp:6893-6932; A33).
5763 double requested = qty_percent >= 100.0 - internal::kFullPercentEps
5764 ? live_basis : percentage_basis * qty_percent / 100.0;
5765 if (qty_percent < 100.0 - internal::kFullPercentEps) {
5766 requested = quantize_percent_exit_units(requested, available);
5767 }
5768 reserved_qty = std::min(requested, available);
5769 }
5770 if (!(reserved_qty > internal::kQtyEpsilon)) return false;
5771 qty_percent = reserved_qty / live_basis * 100.0;
5772 const bool partial = reserved_qty < live_basis - 1e-9;
5773 if (partial && other_full_exit) return false;
5774 return true;
5775}
5776
5777void PineExecutionAdapter::reconcile_deferred_exit_reservations(
5778 const SourceId& from_entry, double live_basis) {
5779 if (!(live_basis > internal::kQtyEpsilon)) return;
5780
5781 struct Family {
5782 std::uint64_t key = 0;
5783 std::uint64_t command_sequence = 0;
5784 std::uint64_t rank = std::numeric_limits<std::uint64_t>::max();
5785 double percent = 100.0;
5786 double existing = kNaN;
5787 double explicit_requested = kNaN;
5788 std::vector<native_order::RequestHandle> handles;
5789 std::vector<std::size_t> queued;
5790 std::vector<std::size_t> delayed;
5791 std::vector<std::uint64_t> origins;
5792 };
5793 std::vector<Family> families;
5794 for (const auto& handle : live_handles_) {
5795 const auto found = placement_.find(handle.incarnation);
5796 if (found == placement_.end()) continue;
5797 const auto& snapshot = found->second;
5798 const bool exit = snapshot.family == PineOrderFamily::ExitLimit
5801 if (!exit || snapshot.from_entry != from_entry || snapshot.void_issue) continue;
5802 const auto key = key_for(snapshot.source_id, snapshot.from_entry);
5803 auto family = std::find_if(families.begin(), families.end(),
5804 [&](const Family& value) { return value.key == key; });
5805 if (family == families.end()) {
5806 Family next;
5807 next.key = key;
5808 next.command_sequence = snapshot.command_sequence;
5809 next.rank = exit_queue_rank(placement_, snapshot);
5810 next.percent = std::isfinite(snapshot.qty_percent)
5811 ? std::clamp(snapshot.qty_percent, 0.0, 100.0) : 100.0;
5812 next.existing = snapshot.projection_remaining_qty;
5813 next.explicit_requested = snapshot.requested_qty;
5814 next.handles.push_back(handle);
5815 next.origins.push_back(snapshot.bracket_origin.incarnation);
5816 families.push_back(std::move(next));
5817 } else {
5818 family->command_sequence = std::min(family->command_sequence,
5819 snapshot.command_sequence);
5820 family->rank = std::min(family->rank, exit_queue_rank(placement_, snapshot));
5821 family->handles.push_back(handle);
5822 if (std::isfinite(snapshot.projection_remaining_qty)) {
5823 family->existing = std::isfinite(family->existing)
5824 ? std::max(family->existing, snapshot.projection_remaining_qty)
5825 : snapshot.projection_remaining_qty;
5826 }
5827 if (std::isfinite(snapshot.requested_qty))
5828 family->explicit_requested = snapshot.requested_qty;
5829 if (std::find(family->origins.begin(), family->origins.end(),
5830 snapshot.bracket_origin.incarnation) == family->origins.end()) {
5831 family->origins.push_back(snapshot.bracket_origin.incarnation);
5832 }
5833 }
5834 }
5835 for (std::size_t index = 0; index < pending_bracket_legs_.size(); ++index) {
5836 const auto& snapshot = pending_bracket_legs_[index].snapshot;
5837 const bool exit = snapshot.family == PineOrderFamily::ExitLimit
5840 if (!exit || snapshot.from_entry != from_entry || snapshot.void_issue) continue;
5841 const auto key = key_for(snapshot.source_id, snapshot.from_entry);
5842 auto family = std::find_if(families.begin(), families.end(),
5843 [&](const Family& value) { return value.key == key; });
5844 if (family == families.end()) {
5845 Family next;
5846 next.key = key;
5847 next.command_sequence = snapshot.command_sequence;
5848 next.rank = exit_queue_rank(placement_, snapshot);
5849 next.percent = std::isfinite(snapshot.qty_percent)
5850 ? std::clamp(snapshot.qty_percent, 0.0, 100.0) : 100.0;
5851 next.existing = snapshot.projection_remaining_qty;
5852 next.explicit_requested = snapshot.requested_qty;
5853 next.queued.push_back(index);
5854 next.origins.push_back(snapshot.bracket_origin.incarnation);
5855 families.push_back(std::move(next));
5856 } else {
5857 family->command_sequence = std::min(family->command_sequence,
5858 snapshot.command_sequence);
5859 family->rank = std::min(family->rank, exit_queue_rank(placement_, snapshot));
5860 family->queued.push_back(index);
5861 if (std::isfinite(snapshot.projection_remaining_qty)) {
5862 family->existing = std::isfinite(family->existing)
5863 ? std::max(family->existing, snapshot.projection_remaining_qty)
5864 : snapshot.projection_remaining_qty;
5865 }
5866 if (std::isfinite(snapshot.requested_qty))
5867 family->explicit_requested = snapshot.requested_qty;
5868 if (std::find(family->origins.begin(), family->origins.end(),
5869 snapshot.bracket_origin.incarnation) == family->origins.end()) {
5870 family->origins.push_back(snapshot.bracket_origin.incarnation);
5871 }
5872 }
5873 }
5874 for (std::size_t index = 0; index < delayed_market_orders_.size(); ++index) {
5875 const auto& snapshot = delayed_market_orders_[index].snapshot;
5876 const bool exit = snapshot.family == PineOrderFamily::ExitLimit
5879 if (!exit || snapshot.from_entry != from_entry || snapshot.void_issue) continue;
5880 const auto key = key_for(snapshot.source_id, snapshot.from_entry);
5881 auto family = std::find_if(families.begin(), families.end(),
5882 [&](const Family& value) { return value.key == key; });
5883 if (family == families.end()) {
5884 Family next;
5885 next.key = key;
5886 next.command_sequence = snapshot.command_sequence;
5887 next.rank = exit_queue_rank(placement_, snapshot);
5888 next.percent = std::isfinite(snapshot.qty_percent)
5889 ? std::clamp(snapshot.qty_percent, 0.0, 100.0) : 100.0;
5890 next.existing = snapshot.projection_remaining_qty;
5891 next.explicit_requested = snapshot.requested_qty;
5892 next.delayed.push_back(index);
5893 next.origins.push_back(snapshot.bracket_origin.incarnation);
5894 families.push_back(std::move(next));
5895 } else {
5896 family->command_sequence = std::min(family->command_sequence,
5897 snapshot.command_sequence);
5898 family->rank = std::min(family->rank, exit_queue_rank(placement_, snapshot));
5899 family->delayed.push_back(index);
5900 if (std::isfinite(snapshot.projection_remaining_qty)) {
5901 family->existing = std::isfinite(family->existing)
5902 ? std::max(family->existing, snapshot.projection_remaining_qty)
5903 : snapshot.projection_remaining_qty;
5904 }
5905 if (std::isfinite(snapshot.requested_qty))
5906 family->explicit_requested = snapshot.requested_qty;
5907 if (std::find(family->origins.begin(), family->origins.end(),
5908 snapshot.bracket_origin.incarnation) == family->origins.end()) {
5909 family->origins.push_back(snapshot.bracket_origin.incarnation);
5910 }
5911 }
5912 }
5913 // The live position goes to the exits in their queue order
5914 // (exit_queue_rank), each taking what the ones before it leave.
5915 std::stable_sort(families.begin(), families.end(),
5916 [](const Family& left, const Family& right) {
5917 if (left.rank != right.rank) return left.rank < right.rank;
5918 return left.command_sequence < right.command_sequence;
5919 });
5920
5921 double reserved = 0.0;
5922 std::vector<native_order::RequestHandle> cancel;
5923 for (const auto& family : families) {
5924 const double available = std::max(0.0, live_basis - reserved);
5925 double units = 0.0;
5926 if (std::isfinite(family.explicit_requested)) {
5927 // An explicit strategy.exit quantity belongs to each bound entry
5928 // instance. Opening a later same-id parent adds another leg; it
5929 // must not turn every existing one-unit leg into a percentage of
5930 // the enlarged net position (ab9714be:pine_strategy_commands.cpp
5931 // :2739-2811 and test_exit_bracket_pending_entry_leg).
5932 units = std::min(std::abs(family.explicit_requested), available);
5933 } else if (family.percent < 100.0 - internal::kFullPercentEps
5934 && std::isfinite(family.existing)) {
5935 units = std::min(family.existing, available);
5936 } else {
5937 // ab9714be pine_fills.cpp:6893-6896: a reservation percentage
5938 // inside kFullPercentEps of 100 is a FULL exit, so it owns the
5939 // exact live exposure. Recomputing `live_basis * 100 / 100` can
5940 // land one binary64 step low and strand a sub-lot dust remainder
5941 // that never flattens (ab9714be pine_fills.cpp:1618 spells the
5942 // same whole-position coverage as `qty - kQtyEpsilon`).
5943 double requested = family.percent >= 100.0 - internal::kFullPercentEps
5944 ? live_basis : live_basis * family.percent / 100.0;
5945 if (family.percent < 100.0 - internal::kFullPercentEps) {
5946 requested = quantize_percent_exit_units(requested, available);
5947 }
5948 units = std::min(requested, available);
5949 }
5950 if (!(units > internal::kQtyEpsilon)) {
5951 cancel.insert(cancel.end(), family.handles.begin(), family.handles.end());
5952 for (const auto index : family.queued) {
5953 if (index < pending_bracket_legs_.size())
5954 pending_bracket_legs_[index].snapshot.qty_percent = 0.0;
5955 }
5956 for (const auto index : family.delayed) {
5957 if (index < delayed_market_orders_.size())
5958 delayed_market_orders_[index].snapshot.qty_percent = 0.0;
5959 }
5960 continue;
5961 }
5962 const double normalized_percent = units / live_basis * 100.0;
5963 for (const auto& handle : family.handles) {
5964 const auto found = placement_.find(handle.incarnation);
5965 if (found == placement_.end()) continue;
5966 found->second.projection_remaining_qty = units;
5967 found->second.qty_percent = normalized_percent;
5968 found->second.fixed_exit_reservation =
5969 std::isfinite(family.explicit_requested)
5970 || family.percent < 100.0 - internal::kFullPercentEps
5971 || std::isfinite(family.existing);
5972 found->second.reservation_deferred_to_pending_entry = false;
5973 }
5974 for (const auto index : family.queued) {
5975 if (index >= pending_bracket_legs_.size()) continue;
5976 auto& snapshot = pending_bracket_legs_[index].snapshot;
5977 snapshot.projection_remaining_qty = units;
5978 snapshot.qty_percent = normalized_percent;
5979 snapshot.fixed_exit_reservation =
5980 std::isfinite(family.explicit_requested)
5981 || family.percent < 100.0 - internal::kFullPercentEps
5982 || std::isfinite(family.existing);
5983 snapshot.reservation_deferred_to_pending_entry = false;
5984 }
5985 for (const auto index : family.delayed) {
5986 if (index >= delayed_market_orders_.size()) continue;
5987 auto& snapshot = delayed_market_orders_[index].snapshot;
5988 snapshot.projection_remaining_qty = units;
5989 snapshot.qty_percent = normalized_percent;
5990 snapshot.fixed_exit_reservation =
5991 std::isfinite(family.explicit_requested)
5992 || family.percent < 100.0 - internal::kFullPercentEps
5993 || std::isfinite(family.existing);
5994 snapshot.reservation_deferred_to_pending_entry = false;
5995 }
5996 // A limit/stop/trail OCA set for one opening consumes one reservation,
5997 // even though it has multiple native request handles. Count distinct
5998 // bound entry origins, not executable sibling legs.
5999 const std::size_t multiplicity = std::max<std::size_t>(1, family.origins.size());
6000 reserved += std::isfinite(family.explicit_requested)
6001 ? std::min(available, units * static_cast<double>(multiplicity))
6002 : units;
6003 }
6004 for (const auto& handle : cancel) {
6005 const auto result = require_host().cancel(handle);
6006 if (result.status == native_order::CancelStatus::Cancelled) retire(handle);
6007 }
6008 pending_write(pending_bracket_legs_).erase(
6009 std::remove_if(pending_bracket_legs_.begin(), pending_bracket_legs_.end(),
6010 [](const PendingBracketLeg& leg) {
6011 return leg.snapshot.qty_percent == 0.0;
6012 }),
6013 pending_bracket_legs_.end());
6014}
6015
6016// A margin call shrinks the position under its exits without filling any of
6017// them. TradingView keeps each percentage exit's reserved share and hands the
6018// position to the exits in their queue order again, each taking what the ones
6019// before it leave (lane TAIL-H rule PK, tapes tailh-d2a / tailh-d2b): with 8
6020// of 25 contracts left, a 50 % exit created first holds all 8 of its 12 and
6021// the stop exit created after it, left nothing, never fires.
6022void PineExecutionAdapter::reconcile_exit_reservations_after_margin() {
6023 std::vector<SourceId> entries;
6024 const auto note = [&](const PlacementSnapshot& snapshot) {
6025 const bool exit = snapshot.family == PineOrderFamily::ExitLimit
6028 if (!exit || snapshot.void_issue) return;
6029 if (std::find(entries.begin(), entries.end(), snapshot.from_entry) == entries.end())
6030 entries.push_back(snapshot.from_entry);
6031 };
6032 for (const auto& handle : live_handles_) {
6033 const auto found = placement_.find(handle.incarnation);
6034 if (found != placement_.end()) note(found->second);
6035 }
6036 for (const auto& pending : pending_bracket_legs_) note(pending.snapshot);
6037 for (const auto& delayed : delayed_market_orders_) note(delayed.snapshot);
6038 if (entries.empty()) return;
6039 const double physical = std::abs(detail::run_position(require_host()).signed_units);
6040 for (const auto& entry : entries) {
6041 const double exposure = entry.empty() ? physical : cohort_exposure_for(entry);
6042 if (exposure > internal::kQtyEpsilon)
6043 reconcile_deferred_exit_reservations(entry, exposure);
6044 }
6045}
6046
6047// The account FX in force at an instant is the kernel's lookup: the staged
6048// scalar and series are the engine's own (staged_configuration copies them at
6049// begin), validated positive and finite by their setters.
6050double PineExecutionAdapter::active_staged_fx(std::int64_t timestamp_ms) const noexcept {
6051 const auto* pine = pine_view_of(host_);
6052 return pine ? pine->account_currency_fx_at(timestamp_ms) : staged_.account_fx;
6053}
6054
6055// TradingView revalues a carried position at the first broker open under a new
6056// account FX rate (ab9714be pine_fills.cpp:830-1000, the TV-pinned 1x long
6057// row; test_affordability_fx G, G2, NEW-S1): its money at the OPEN price, its
6058// lot-floored 4x restore, executed at the open before any resting order sees
6059// the bar -- exactly the open checkpoint submit_margin_call_slice takes, at the
6060// position side's own margin (the margin and cancellation comparison: that formula was always per-side,
6061// ab9714be's leveraged cells were only switched off). A survivor that is not a
6062// 1x long is then checked over the rest of the bar by the kernel's liquidation
6063// (on_applied re-schedules the path). The kernel's own FxRoll point is not
6064// this checkpoint -- see margin_check_allowed.
6065void PineExecutionAdapter::apply_fx_open_margin_slice(
6066 const Bar& bar, const NativeDecisionContext& context) {
6067 const double rate = active_staged_fx(context.sub_bar_open_ms);
6068 const double prior = last_fx_rate_;
6069 last_fx_rate_ = rate;
6070 if (!std::isfinite(prior) || prior == rate) return;
6071 const auto position = detail::run_position(require_host());
6072 if (position.signed_units == 0.0
6073 || position_open_script_bar_ >= context.script_bar_open_ms) return;
6074 (void)submit_margin_call_slice(bar.open, context);
6075}
6076
6077void PineExecutionAdapter::apply_fx_opening_margin_slice(
6078 const native_order::ExecutionAppliedEvent& event,
6079 const NativeDecisionContext& context) {
6080 // A percentage-commission percent-of-equity configuration only: any other
6081 // returns before the row is copied (every fill asks).
6082 if (config_.default_qty_type != static_cast<int>(QtyType::PERCENT_OF_EQUITY)
6083 || config_.commission_type != static_cast<int>(CommissionType::PERCENT)
6084 || !(config_.commission_value > 0.0)) {
6085 return;
6086 }
6087 std::optional<PlacementSnapshot> opening_snapshot;
6088 if (const auto found = placement_.find(event.handle().incarnation);
6089 found != placement_.end()) {
6090 opening_snapshot = found->second;
6091 }
6092 if (!opening_snapshot || !opening_snapshot->opening
6093 || opening_snapshot->family != PineOrderFamily::Entry
6094 || !finite_positive(opening_snapshot->sizing.frozen_units)
6095 || config_.default_qty_type != static_cast<int>(QtyType::PERCENT_OF_EQUITY)
6096 || config_.commission_type != static_cast<int>(CommissionType::PERCENT)
6097 || !(config_.commission_value > 0.0)) {
6098 return;
6099 }
6100 const double rate = active_staged_fx(context.sub_bar_open_ms);
6101 if (!std::isfinite(opening_snapshot->sizing.fx) || opening_snapshot->sizing.fx == rate) return;
6102 // The FX twin of on_applied's opening checkpoint (a full-margin opening
6103 // whose rate moved between its signal and its fill): the same TradingView
6104 // money and slice at the fill, settled at the same current coordinate
6105 // before the next candidate (A31(b)).
6106 const auto position = detail::run_position(require_host());
6107 if ((position.signed_units > 0.0 ? config_.margin_long : config_.margin_short) != 100.0) {
6108 return;
6109 }
6110 (void)submit_margin_call_slice(event.resolved_price, context, true);
6111}
6112
6113// Whether on_applied admits the kernel's post-fill point on a leveraged
6114// opening's entry bar: the run shapes ab9714be took the entry-bar suffix on
6115// (pine_fills.cpp:1014-1023), and process_orders_on_close and
6116// calc_on_order_fills, where TradingView books the same call on the same bar
6117// (the intrabar margin correction: tests/fixtures/margin_entry_bar/pm2-m7-coof-* and
6118// pm2-m7-poocl-*). Not a timestamped-FX run, whose path checks on_bar_open
6119// does not schedule either, nor a magnified one, whose later samples the
6120// kernel's IntrabarSample point checks (intrabar_sample_checked).
6121bool PineExecutionAdapter::leveraged_entry_bar_checked() const noexcept {
6122 return !bar_magnifier_ && staged_.account_fx_effective_from_ms.empty();
6123}
6124
6125// The opening schedule_preopen_margin_slice answers for: a default-percent
6126// stop entry taken at its bar's open. That slice sizes its entry-bar call on
6127// the entry's frozen units, and its verdict -- a slice, or none -- stands for
6128// the bar; the kernel's post-fill point is not admitted beside it.
6129bool PineExecutionAdapter::preopen_slice_class(
6130 const PlacementSnapshot& opening, const NativeDecisionContext& context) const noexcept {
6131 return config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
6132 && opening.family == PineOrderFamily::Entry
6133 && finite_positive(opening.sizing.frozen_units)
6134 && finite_positive(opening.exit_levels.stop)
6135 && context.coordinate.path_phase == NativePathPhase::Open;
6136}
6137
6138void PineExecutionAdapter::schedule_preopen_margin_slice(
6139 const Bar& bar, const NativeDecisionContext& context) {
6140 // The legacy broker checks the adverse excursion of a default-percent
6141 // stop entry's fill bar after the opening is admitted. Submit its
6142 // source-owned close leg at the preceding open: BindCohort resolves only
6143 // after that opening applies, then the ordinary native path matches the
6144 // adverse waypoint in the same bar. This keeps the liquidation policy
6145 // entirely above the generic driver.
6146 if (!source_margin_call_enabled_
6147 || detail::run_position(require_host()).signed_units != 0.0
6148 || config_.default_qty_type != static_cast<int>(QtyType::PERCENT_OF_EQUITY)
6149 || !finite_positive(staged_.syminfo.pointvalue)
6150 || !finite_positive(staged_.syminfo.mintick)
6151 || !finite_positive(bar.open)) {
6152 return;
6153 }
6154
6155 // A pre-open margin submission can append to live_handles_ and insert
6156 // into placement_; scan value copies rather than retaining either
6157 // container's elements across submit_or_replace.
6158 const auto live_handles = live_handles_;
6159 for (const auto& handle : live_handles) {
6160 std::optional<PlacementSnapshot> opening_copy;
6161 if (const auto found = placement_.find(handle.incarnation);
6162 found != placement_.end()) {
6163 opening_copy = found->second;
6164 }
6165 if (!opening_copy) continue;
6166 const PlacementSnapshot& opening = *opening_copy;
6167 if (!opening.opening || opening.family != PineOrderFamily::Entry
6168 || !finite_positive(opening.sizing.frozen_units)
6169 || !finite_positive(opening.exit_levels.stop)) {
6170 continue;
6171 }
6172 const bool marketable = opening.is_long
6173 ? opening.exit_levels.stop <= bar.open
6174 : opening.exit_levels.stop >= bar.open;
6175 if (!marketable) continue;
6176
6177 const double entry = nearest_tick(bar.open, staged_.syminfo.mintick);
6178 const double adverse_raw = opening.is_long ? bar.low : bar.high;
6179 const double adverse = nearest_tick(adverse_raw, staged_.syminfo.mintick);
6180 if (!finite_positive(entry) || !finite_positive(adverse)
6181 || (opening.is_long ? !(adverse < entry) : !(adverse > entry))) {
6182 continue;
6183 }
6184 const double fx = active_staged_fx(context.sub_bar_open_ms);
6185 const double margin = opening.is_long ? config_.margin_long : config_.margin_short;
6186 if (!finite_positive(fx) || !finite_positive(margin)) continue;
6187 const double units = opening.sizing.frozen_units;
6188 // Owner sequencing: the source broker evaluates the adverse excursion
6189 // only AFTER the opening is admitted, and an all-in marketable stop
6190 // entry that cannot be funded at its own fill price is declined there
6191 // (ab9714be src/source/pine_fills.cpp:4618
6192 // stop_entry_margin_admission_declines consumes the fill at :4741:
6193 // required = qty * round_to_mintick(fill) * pointvalue * fx * margin,
6194 // refused when it exceeds realized equity plus the same
6195 // max(1e-9, |equity|*1e-12) guard; a gap-through fill price is the
6196 // rounded open). Arming the deferred close for such a candidate
6197 // leaves a stale Reduce that a LATER bar's different opening matches:
6198 // A synthetic OANDA:XAUUSD example sizes 3.20 units against
6199 // equity 8000.00, costs 3.20 * 2501.00 = 8003.20 at
6200 // the open, so the owner never opens there -- its all-in long fills on
6201 // the next open (3.19 * 2502.00 = 7981.38) as ONE lot. A synthetic
6202 // ETH feed's admitted all-in short (4.00 * 2000.00 against
6203 // equity 8000.00, cost 8000.00) keeps its legitimate
6204 // same-bar slice.
6205 const double admission_cost = units * entry * staged_.syminfo.pointvalue
6206 * fx * margin / 100.0;
6207 const double admission_guard = std::max(
6208 1e-9, std::abs(opening.sizing.equity) * 1e-12);
6209 if (!std::isfinite(admission_cost)
6210 || admission_cost > opening.sizing.equity + admission_guard) {
6211 continue;
6212 }
6213 const double unrealized = (opening.is_long ? adverse - entry : entry - adverse)
6214 * units * staged_.syminfo.pointvalue * fx;
6215 const double marked_equity = opening.sizing.equity + unrealized;
6216 const double required = units * adverse * staged_.syminfo.pointvalue * fx
6217 * margin / 100.0;
6218 const double unit_margin = adverse * staged_.syminfo.pointvalue * fx
6219 * margin / 100.0;
6220 if (!std::isfinite(marked_equity) || !std::isfinite(required)
6221 || !finite_positive(unit_margin) || !(required > marked_equity)) {
6222 continue;
6223 }
6224 // TradingView's slice from this money is the shared rule
6225 // (source_margin_units: the lot-floored restore taken four times and
6226 // floored again, the one-contract band, the dust gates), sized here
6227 // on the frozen signal-time units the opening will book.
6228 SourceMarginMoney money;
6229 money.valid = true;
6230 money.mark = adverse;
6231 money.held = units;
6232 money.unit_margin = unit_margin;
6233 money.exact_required = required;
6234 money.required = required;
6235 money.equity = marked_equity;
6236 const double slice = source_margin_units(money, /*opening_checkpoint=*/false);
6237 if (!finite_positive(slice)) continue;
6238
6239 native_order::Request request;
6240 request.intent = native_order::HostSized{native_order::HostSizedKind::Close, std::nullopt};
6241 request.label = "__margin_preopen__" + opening.source_id;
6242 request.comment = "Margin call";
6243 // Preserve the exact source adverse waypoint as the generic trigger;
6244 // terms rounds its resulting fill by the source tick rule.
6245 request.trigger = native_order::Stop{adverse_raw};
6246 request.owner = native_order::BindCohort{cohort_for(opening.source_id)};
6247 PlacementSnapshot snapshot;
6249 snapshot.source_id = request.label;
6250 snapshot.from_entry = opening.source_id;
6251 snapshot.comment = request.comment;
6252 snapshot.requested_qty = slice;
6253 snapshot.sizing = opening.sizing;
6254 (void)submit_or_replace(std::move(request), std::move(snapshot), false,
6255 "__margin_preopen__" + opening.source_id);
6256 // The source stop-entry row is unique under this pre-open condition;
6257 // a second candidate belongs to a later source evaluation.
6258 return;
6259 }
6260}
6261
6262void PineExecutionAdapter::consume_cohort_units(
6263 const SourceId& id, const native_order::ExecutionAppliedEvent& event) {
6264 if (!(event.closed_units > 0.0) || !std::isfinite(event.closed_units)) return;
6265 observe_position_tail_rows(); // execute_current can expose rows before Applied
6266 const auto found = cohorts_by_id_.find(id);
6267 if (found == cohorts_by_id_.end()) return;
6268 auto& facts = found->second;
6269 std::vector<std::uint64_t> selected;
6270 if (const auto* opening = std::get_if<execution::OpeningExposure>(&event.scope)) {
6271 selected.push_back(opening->incarnation);
6272 } else if (const auto* openings = std::get_if<native_order::SelectedExposure>(&event.scope)) {
6273 selected = openings->incarnations;
6274 } else {
6275 for (const auto& handle : facts.opened) selected.push_back(handle.incarnation);
6276 }
6277 double remaining = event.closed_units;
6278 for (const auto incarnation : selected) {
6279 auto unit = facts.live_units_by_origin.find(incarnation);
6280 if (unit == facts.live_units_by_origin.end() || !(unit->second > 0.0)) continue;
6281 const double deduction = std::min(unit->second, remaining);
6282 unit->second -= deduction;
6283 remaining -= deduction;
6284 if (unit->second == 0.0) {
6285 facts.live_units_by_origin.erase(unit);
6286 if (position_tail_summary_) position_tail_summary_->retire_origin(incarnation);
6287 }
6288 if (!(remaining > 0.0)) break;
6289 }
6290 if (position_tail_summary_)
6291 position_tail_summary_->live_cohort(&*found, !facts.live_units_by_origin.empty());
6292 cohorts_by_id_.refresh_held(found);
6294}
6295
6296void PineExecutionAdapter::consume_closed_trade_rows(
6297 const native_order::ExecutionAppliedEvent& event,
6298 const PlacementSnapshot* cause) {
6299 observe_position_tail_rows();
6300 auto& host = require_host();
6301 const auto settle_slot = [&](CohortFacts& cohort, const Trade& trade,
6302 bool drained) {
6304 if (!cause) return;
6305 const bool bracket = cause->family == PineOrderFamily::ExitLimit
6306 || cause->family == PineOrderFamily::ExitStop
6307 || cause->family == PineOrderFamily::ExitTrail;
6308 const bool close_path = cause->family == PineOrderFamily::Close
6309 || cause->family == PineOrderFamily::CloseAll
6310 || cause->family == PineOrderFamily::Risk
6311 || cause->family == PineOrderFamily::Margin
6312 || cause->family == PineOrderFamily::Entry
6313 || cause->family == PineOrderFamily::Order;
6314 const bool exact_bracket_owner = cause->bracket_origin.incarnation != 0
6315 ? cause->bracket_origin.incarnation == trade.entry_incarnation
6316 : cause->from_entry == trade.entry_id;
6317 if (bracket && !exact_bracket_owner)
6318 bracket_shadowed_openings_.insert(trade.entry_incarnation);
6319 if (!drained) return;
6320 const bool owned_bracket = bracket && exact_bracket_owner
6321 && bracket_shadowed_openings_.find(trade.entry_incarnation)
6322 == bracket_shadowed_openings_.end();
6323 if (!close_path && !owned_bracket) return;
6324 const auto removed_at = std::remove_if(cohort.opened.begin(), cohort.opened.end(),
6325 [&](const auto& opening) {
6326 return opening.incarnation == trade.entry_incarnation;
6327 });
6328 const auto removed = static_cast<std::size_t>(
6329 std::distance(removed_at, cohort.opened.end()));
6330 cohort.opened.erase(removed_at, cohort.opened.end());
6331 assert(cohort_opened_slot_count_ >= removed);
6332 cohort_opened_slot_count_ -= removed;
6334 bracket_shadowed_openings_.erase(trade.entry_incarnation);
6335 };
6336 for (std::size_t row = 0; row < event.closed_trade_count; ++row) {
6337 const std::size_t index = event.first_trade_index + row;
6338 if (index >= static_cast<std::size_t>(host.trade_count())) continue;
6339 const Trade& trade = host.get_trade(static_cast<int>(index));
6340 double remaining = trade.qty;
6341 bool matched = false;
6342 const auto held = cohorts_by_id_.held_rows();
6343 for (auto at = held.begin(); at != held.end();) {
6344 const auto current = at++; // Removal below may erase the held node.
6345 auto& cohort = *current;
6346 auto units = cohort.second.live_units_by_origin.find(
6347 trade.entry_incarnation);
6348 if (units == cohort.second.live_units_by_origin.end()) continue;
6349 const double consumed = std::min(units->second, remaining);
6350 units->second -= consumed;
6351 remaining -= consumed;
6352 const bool drained = !(units->second > 1e-10);
6353 if (drained) {
6354 cohort.second.live_units_by_origin.erase(units);
6355 if (position_tail_summary_)
6356 position_tail_summary_->retire_origin(trade.entry_incarnation);
6357 }
6358 if (position_tail_summary_)
6359 position_tail_summary_->live_cohort(
6360 &cohort, !cohort.second.live_units_by_origin.empty());
6361 settle_slot(cohort.second, trade, drained);
6362 cohorts_by_id_.refresh_held(current);
6363 matched = true;
6364 break;
6365 }
6366 if (matched || !(remaining > 0.0)) continue;
6367 const auto cohort = cohorts_by_id_.find(trade.entry_id);
6368 if (cohort == cohorts_by_id_.end()) continue;
6369 // False once nothing remains to consume.
6370 const auto consume_origin = [&](std::uint64_t incarnation) {
6371 auto units = cohort->second.live_units_by_origin.find(incarnation);
6372 if (units == cohort->second.live_units_by_origin.end()) return true;
6373 const double consumed = std::min(units->second, remaining);
6374 units->second -= consumed;
6375 remaining -= consumed;
6376 const bool drained = !(units->second > 1e-10);
6377 if (drained) {
6378 cohort->second.live_units_by_origin.erase(units);
6379 if (position_tail_summary_) position_tail_summary_->retire_origin(incarnation);
6380 }
6381 if (position_tail_summary_)
6382 position_tail_summary_->live_cohort(
6383 &*cohort, !cohort->second.live_units_by_origin.empty());
6384 settle_slot(cohort->second, trade, drained);
6385 cohorts_by_id_.refresh_held(cohort);
6386 return remaining > 0.0;
6387 };
6388 auto* lookup = cohort->second.origins.size() <= kIndexedFrom ? nullptr
6389 : adapter_lookup_index(bound_consumer(), this, run_counter_);
6390 if (!lookup) {
6391 for (const auto& origin : cohort->second.origins)
6392 if (!consume_origin(origin.incarnation)) break;
6393 continue;
6394 }
6395 // The walk above consumes only at a roster position whose origin
6396 // still has a unit row, and each such origin only at its first
6397 // position: after consuming there, its row is gone (drained) or
6398 // nothing remains. So visit exactly those first positions, ascending,
6399 // which the roster's position index names without walking the id's
6400 // whole history.
6401 std::vector<std::pair<std::size_t, std::uint64_t>> carrying;
6402 carrying.reserve(cohort->second.live_units_by_origin.size());
6403 for (const auto& unit : cohort->second.live_units_by_origin) {
6404 // Empty only after a push_back that failed to allocate: no roster
6405 // entry carries the incarnation, so the walk never visits it.
6406 const auto* positions = cohort->second.origins.positions_of(unit.first);
6407 if (positions && !positions->empty())
6408 carrying.emplace_back(positions->front(), unit.first);
6409 }
6410 std::sort(carrying.begin(), carrying.end());
6411 ++lookup->answers;
6412 PINEFORGE_LOOKUP_INDEX_WORK("closed_trade_origin_answer", this, run_counter_, 1);
6413 for (const auto& origin : carrying)
6414 if (!consume_origin(origin.second)) break;
6415 }
6416}
6417
6418bool PineExecutionAdapter::origin_is_pending(
6419 const native_order::RequestHandle& origin) const noexcept {
6420 if (origin.incarnation == 0) return false;
6421 // Live first: a live handle's row is always held.
6422 if (std::find(live_handles_.begin(), live_handles_.end(), origin) == live_handles_.end())
6423 return false;
6424 const auto placement = placement_.find(origin.incarnation);
6425 return placement != placement_.end() && placement->second.opening;
6426}
6427
6428std::vector<std::size_t> PineExecutionAdapter::live_origin_positions(
6429 const CohortFacts& cohort) const {
6430 std::vector<std::size_t> positions;
6431 for (const auto& handle : live_handles_) cohort.origins.append_positions_of(handle, positions);
6432 std::sort(positions.rbegin(), positions.rend());
6433 positions.erase(std::unique(positions.begin(), positions.end()), positions.end());
6434 return positions;
6435}
6436
6437void PineExecutionAdapter::cancel_bracket_origin(native_order::RequestHandle origin) {
6438 pending_write(pending_bracket_legs_).erase(std::remove_if(pending_bracket_legs_.begin(), pending_bracket_legs_.end(),
6439 [&](const PendingBracketLeg& leg) { return leg.snapshot.bracket_origin == origin; }),
6440 pending_bracket_legs_.end());
6441 std::vector<native_order::RequestHandle> matches;
6442 for (const auto& handle : live_handles_) {
6443 const auto placement = placement_.find(handle.incarnation);
6444 if (placement != placement_.end() && placement->second.bracket_origin == origin)
6445 matches.push_back(handle);
6446 }
6447 for (const auto& handle : matches) {
6448 const auto result = require_host().cancel(handle);
6449 if (result.status == native_order::CancelStatus::Cancelled) {
6450 if (const auto placement = placement_.find(handle.incarnation);
6451 placement != placement_.end()) {
6452 placement->second.cancellation = {PineCancellationCause::Explicit, 1, 0,
6453 handle.incarnation, static_cast<std::int64_t>(placement->second.source_sequence),
6454 handle.incarnation, placement->second.placement_cycle,
6455 placement->second.legs.revision(), placement->second.requested_qty, kNaN};
6456 }
6457 retire(handle);
6458 }
6459 }
6460}
6461
6462void PineExecutionAdapter::cancel_bracket_siblings(native_order::RequestHandle handle,
6463 std::uint64_t executed_at) {
6464 // Only an exit leg has siblings: any other row is answered before it is
6465 // copied (every fill asks).
6466 std::optional<PlacementSnapshot> snapshot_copy;
6467 if (const auto source = placement_.find(handle.incarnation);
6468 source != placement_.end()) {
6469 const auto family = source->second.family;
6470 if (family != PineOrderFamily::ExitLimit && family != PineOrderFamily::ExitStop
6471 && family != PineOrderFamily::ExitTrail) return;
6472 snapshot_copy = source->second;
6473 }
6474 if (!snapshot_copy) return;
6475 const PlacementSnapshot& snapshot = *snapshot_copy;
6476 std::vector<native_order::RequestHandle> matches;
6477 for (const auto& candidate : live_handles_) {
6478 if (candidate == handle) continue;
6479 const auto placement = placement_.find(candidate.incarnation);
6480 if (placement == placement_.end()) continue;
6481 const auto& sibling = placement->second;
6482 if (sibling.source_id == snapshot.source_id && sibling.from_entry == snapshot.from_entry
6483 && sibling.bracket_origin == snapshot.bracket_origin
6484 && (sibling.family == PineOrderFamily::ExitLimit || sibling.family == PineOrderFamily::ExitStop
6485 || sibling.family == PineOrderFamily::ExitTrail)) {
6486 matches.push_back(candidate);
6487 }
6488 }
6489 // The terminal-receipt integration: a terminal receipt is observed at the adapter's next
6490 // read, not when its leg executed. A bracket leg the pre-script drain
6491 // settles on its parent's touch bar is read at the next bar open, after
6492 // that bar's source body and its process_orders_on_close pass; by then a
6493 // same-bar re-entry the close already reached has opened (PAR-ORDERS,
6494 // H-MEASURE 6b) and its own strategy.exit legs of the same id are
6495 // working. A leg of a later strategy.exit call (another command_sequence)
6496 // that the kernel accepted after the execution is that later order, not a
6497 // sibling of the leg that filled, and it stays. A leg of the filled leg's
6498 // own call is its sibling however late it was accepted -- the drain
6499 // submits a bracket's legs one at a time, executing each -- and goes.
6500 const auto later_call = [&](const native_order::RequestHandle& sibling) {
6501 const auto placement = placement_.find(sibling.incarnation);
6502 return placement != placement_.end()
6503 && placement->second.command_sequence != snapshot.command_sequence;
6504 };
6505 if (executed_at != 0 && std::any_of(matches.begin(), matches.end(), later_call)) {
6506 const auto working = require_host().native_working_requests();
6507 matches.erase(std::remove_if(matches.begin(), matches.end(),
6508 [&](const native_order::RequestHandle& sibling) {
6509 if (!later_call(sibling)) return false;
6510 const auto row = std::find_if(working.begin(), working.end(),
6511 [&](const NativeWorkingRequest& request) {
6512 return request.definition->handle == sibling;
6513 });
6514 return row != working.end()
6515 && row->definition->birth.acceptance_ordinal > executed_at;
6516 }), matches.end());
6517 }
6518 for (const auto& sibling : matches) {
6519 const auto result = require_host().cancel(sibling);
6520 if (result.status == native_order::CancelStatus::Cancelled) retire(sibling);
6521 }
6522}
6523
6524void PineExecutionAdapter::cancel_exit_orders_for_full_close(
6525 const SourceId& from_entry) {
6526 const auto text = detail::exit_scratch_text(from_entry);
6527 cancel_exit_orders_for_full_close(text);
6528}
6529
6530void PineExecutionAdapter::cancel_exit_orders_for_full_close(
6531 const detail::ScratchByteSource& from_entry) {
6532 // An exit bound to the id while it held no lot waits for the id's next
6533 // fill: a close of the id, or of the position, has nothing of it to close
6534 // (tests/fixtures/exit_binding close-pending-id, cancel-flat-replace).
6535 const bool keep_pending_bound =
6537 const auto matches = [&](const PlacementSnapshot& snapshot) {
6538 const bool exit = snapshot.family == PineOrderFamily::ExitLimit
6541 return exit && !(keep_pending_bound && snapshot.pending_bound_exit)
6543 from_entry);
6544 };
6545 pending_write(pending_bracket_legs_).erase(
6546 std::remove_if(pending_bracket_legs_.begin(), pending_bracket_legs_.end(),
6547 [&](const PendingBracketLeg& row) { return matches(row.snapshot); }),
6548 pending_bracket_legs_.end());
6549 pending_write(pending_coof_requests_).erase(
6550 std::remove_if(pending_coof_requests_.begin(), pending_coof_requests_.end(),
6551 [&](const PendingCoofRequest& row) { return matches(row.snapshot); }),
6552 pending_coof_requests_.end());
6553 pending_write(source_shadow_pending_).erase(
6554 std::remove_if(source_shadow_pending_.begin(), source_shadow_pending_.end(),
6555 [&](const SourceShadowPending& row) { return matches(row.snapshot); }),
6556 source_shadow_pending_.end());
6557 pending_relative_exits_.erase(
6558 std::remove_if(pending_relative_exits_.begin(), pending_relative_exits_.end(),
6559 [&](const PendingRelativeExit& row) {
6560 return detail::exit_scratch_equal(detail::exit_scratch_text(row.from_entry),
6561 from_entry);
6562 }),
6563 pending_relative_exits_.end());
6564 if (from_entry.size() != 0) withdraw_anchored_relative_readers(nullptr, &from_entry);
6565
6566 detail::ExitScratchRows<native_order::RequestHandle> handles(*this);
6567 for (const auto& handle : live_handles_) {
6568 const auto found = placement_.find(handle.incarnation);
6569 if (found != placement_.end() && matches(found->second)
6570 && !coof_leg_reaches_exit(found->second)) {
6571 handles.push_back(handle);
6572 }
6573 }
6574 for (const auto& handle : handles) {
6575 const auto result = require_host().cancel(handle);
6576 if (result.status == native_order::CancelStatus::Cancelled) retire(handle);
6577 }
6578 detail::ExitScratchIndex removed(handles.context());
6579 for (const auto& handle : handles) removed.put(handle.incarnation, 0);
6580 const BracketRoster::RemovalSelection selection(handles);
6581 for (auto family = bracket_families_.begin(); family != bracket_families_.end();) {
6582 if (!handles.empty()) family->second.remove_selected(
6583 removed, handles.context().run, selection,
6584 fixture_bracket_remove_precheck_);
6585 if (family->second.empty()) family = bracket_families_.erase(family);
6586 else ++family;
6587 }
6588 refresh_pending_view();
6589}
6590
6591// ab9714be pine_fills.cpp:7461-7468: while the book is flat, an EXIT whose
6592// ``created_position_side`` is not FLAT is Removed outright and never Skipped
6593// — a bracket armed against a position that has since closed does not survive
6594// the flat. pine_fills.cpp:7667-7675 gives the reason and the second half of
6595// the rule: an EXIT whose ``from_entry`` has not filled in the CURRENT
6596// position cycle is Removed as well, and ``cycle_filled_entry_ids_`` is
6597// cleared the moment the book goes flat, so a stale bracket must not fire
6598// later against a FUTURE position reusing the same entry id. A retired
6599// candidate row keeps its lifecycle, so ``revive_brackets_after_margin`` can
6600// resurrect the dormant leg the moment the reused id regains exposure, and a
6601// still-live leg can fill against the reversal's fresh lot on the same bar.
6602// Removing both the book entry and the lifecycle restores the legacy rule.
6603// The lifecycle half is the position-cycle filter at the top of
6604// ``revive_brackets_after_margin`` (pine_orders.cpp:597-608): a row parked by
6605// a finished cycle is never revived, so no walk over the whole placement
6606// history is needed here -- one per flat made a long run quadratic.
6607//
6608// Both legacy Removes are LAZY per-broker-point eligibility checks that walk
6609// the pending queue once in ``created_seq`` order, not an eager sweep. An
6610// exit placed on the SAME script bar as the flattening close and BEHIND it in
6611// command order — the close's paired reversal entry sits between the two
6612// (pine_fills.cpp:8013-8018) — is therefore only reached once that entry has
6613// filled and repopulated ``cycle_filled_entry_ids_`` for the new cycle, so
6614// neither Remove applies to it and it stays armed for the fresh lot. A
6615// bracket placed on any EARLIER bar was reached while the retiring cycle was
6616// still the live one, with its ``from_entry`` absent from that cycle's filled
6617// set, and was Removed there. ``paired_close`` carries the same-bar close
6618// that defines the boundary; a null pointer keeps the unrestricted
6619// preservation the transient-flat cleanup at the end of a close relies on.
6620void PineExecutionAdapter::retire_in_position_exits_at_flat(
6621 bool preserve_pending_parents, bool dormant_rows_only,
6622 const PlacementSnapshot* paired_close) {
6623 // A transient flat between two same-point reversal transactions does not
6624 // end the pending parent's lifecycle, so the parent's own brackets are
6625 // kept (the same exclusion the flat cleanup below applies per owner).
6626 // A batched strategy.close takes its command sequence when the batch
6627 // flushes, after the commands its calculation placed behind it, so the
6628 // bracket that follows the close's paired entry -- the one that opens the
6629 // other side -- in that calculation is also behind the close: TradingView
6630 // fills it for the entry (lab tv w4-f14-pooc-pct50 cell F, w4-f08-plain
6631 // cell H; lane W4-ENG-POOC-SAMEPASS).
6632 const auto pending_parent = [&](const PlacementSnapshot& row) {
6633 if (!preserve_pending_parents || row.from_entry.empty()) return false;
6634 if (paired_close != nullptr
6635 && row.projection_created_bar != paired_close->projection_created_bar) {
6636 return false;
6637 }
6638 const SourceId& owner = row.from_entry;
6639 const auto admits = [&](const PlacementSnapshot& parent) {
6640 return paired_close == nullptr
6641 || row.command_sequence > paired_close->command_sequence
6642 || (parent.projection_created_bar == row.projection_created_bar
6643 && row.command_sequence > parent.command_sequence
6644 && paired_close->projection_position_side
6645 == static_cast<std::int32_t>(parent.is_long ? PositionSide::SHORT
6647 };
6648 for (const auto& handle : live_handles_) {
6649 const auto found = placement_.find(handle.incarnation);
6650 if (found != placement_.end() && found->second.opening
6651 && found->second.family == PineOrderFamily::Entry
6652 && found->second.source_id == owner && admits(found->second)) {
6653 return true;
6654 }
6655 }
6656 return std::any_of(pending_entries_.begin(), pending_entries_.end(),
6657 [&](const PendingEntry& entry) {
6658 return entry.snapshot.opening && entry.snapshot.source_id == owner
6659 && admits(entry.snapshot);
6660 });
6661 };
6662 // An exit bound to its entry id while that id's order worked is not an
6663 // exit of the position the flat ends: TradingView keeps it for the id's
6664 // next fill, whatever bar it was called on and whether an order of the id
6665 // still works now (tests/fixtures/exit_binding).
6666 const bool keep_pending_bound =
6668 const auto matches = [&](const PlacementSnapshot& snapshot) {
6669 const bool exit = snapshot.family == PineOrderFamily::ExitLimit
6672 return exit
6673 && static_cast<PositionSide>(snapshot.projection_position_side)
6675 && !(keep_pending_bound && snapshot.pending_bound_exit)
6676 && !pending_parent(snapshot);
6677 };
6678 if (!dormant_rows_only) {
6679 pending_write(pending_bracket_legs_).erase(
6680 std::remove_if(pending_bracket_legs_.begin(), pending_bracket_legs_.end(),
6681 [&](const PendingBracketLeg& row) { return matches(row.snapshot); }),
6682 pending_bracket_legs_.end());
6683 pending_write(pending_coof_requests_).erase(
6684 std::remove_if(pending_coof_requests_.begin(), pending_coof_requests_.end(),
6685 [&](const PendingCoofRequest& row) { return matches(row.snapshot); }),
6686 pending_coof_requests_.end());
6687 pending_write(source_shadow_pending_).erase(
6688 std::remove_if(source_shadow_pending_.begin(), source_shadow_pending_.end(),
6689 [&](const SourceShadowPending& row) { return matches(row.snapshot); }),
6690 source_shadow_pending_.end());
6691 }
6692
6693 std::vector<native_order::RequestHandle> handles;
6694 if (!dormant_rows_only) {
6695 for (const auto& handle : live_handles_) {
6696 const auto found = placement_.find(handle.incarnation);
6697 if (found != placement_.end() && matches(found->second))
6698 handles.push_back(handle);
6699 }
6700 for (const auto& handle : handles) {
6701 const auto result = require_host().cancel(handle);
6702 if (result.status == native_order::CancelStatus::Cancelled) retire(handle);
6703 }
6704 for (auto family = bracket_families_.begin(); family != bracket_families_.end();) {
6705 family->second.remove_all(handles);
6706 if (family->second.empty()) family = bracket_families_.erase(family);
6707 else ++family;
6708 }
6709 }
6710 refresh_pending_view();
6711}
6712
6713// R4-D L10z review fix 6: the exit's placement row is selected by the
6714// (exit id, from_entry) pair, so two exits sharing a source id over different
6715// entries no longer tie-break on the wrong command sequence. The pair-less
6716// minimum is kept as a documented fallback for a re-issued exit whose row was
6717// bound to no entry (it preserves the historical ordering of those rows);
6718// UINT64_MAX is returned only when the exit id has no placement row at all.
6719// ab9714be pine_fills.cpp:664-670: candidate exit fills tie-break by order creation sequence created_seq
6721 const SourceId& exit_id, const SourceId& from_entry) const {
6722 // The same-bar exit sort asks this on every script bar while its tail
6723 // group persists; the table scans retained placements and consults the
6724 // historical minima for erased rows.
6725 return placement_.command_sequence_for(exit_id, from_entry);
6726}
6727
6728std::uint8_t PineExecutionAdapter::exit_phase_value(std::size_t trade_index) const {
6729 const std::size_t base = host_ ? detail::run_consumer(*host_).closed_trade_prefix().base : 0;
6730 if (trade_index < base)
6732 "exit-phase read precedes the sealed prefix");
6733 return trade_index - base < trade_exit_phase_.size()
6734 ? trade_exit_phase_[trade_index - base] : static_cast<std::uint8_t>(NativePathPhase::None);
6735}
6736
6737bool PineExecutionAdapter::is_open_phase_exit(std::size_t trade_index) const {
6738 return exit_phase_value(trade_index) == static_cast<std::uint8_t>(NativePathPhase::Open);
6739}
6740
6742 const std::vector<std::size_t>& indices) {
6743 permute_exit_phase_rows(start, indices.size(),
6744 [&](std::size_t i) { return indices[i]; });
6745}
6746
6748 std::size_t start, std::size_t count,
6749 const std::function<std::size_t(std::size_t)>& source_at) {
6750 // Tests use this permutation seam, including amendments which leave the
6751 // closed-row count unchanged. Production same-bar sorts and global-child
6752 // re-cuts read exit_phase_value and write set_exit_phase directly.
6753 if (position_tail_summary_)
6754 position_tail_summary_->invalidate(PositionTailGuard::Amendment,
6756 const auto none = static_cast<std::uint8_t>(NativePathPhase::None);
6757 const std::size_t base = host_ ? detail::run_consumer(*host_).closed_trade_prefix().base : 0;
6758 if (start < base)
6760 "exit-phase permutation precedes the sealed prefix");
6762 if (host_)
6763 permuted.set_directory(detail::run_consumer(*host_).report_scratch_directory(*host_));
6764 for (std::size_t i = 0; i < count; ++i) {
6765 const std::size_t index = source_at(i);
6766 if (index < base)
6768 "exit-phase read precedes the sealed prefix");
6769 permuted.push_back(index - base < trade_exit_phase_.size()
6770 ? trade_exit_phase_[index - base] : none);
6771 }
6772 for (std::size_t i = 0; i < permuted.size(); ++i) {
6773 const std::size_t target = start + i;
6774 if (target - base >= trade_exit_phase_.size() && permuted[i] == none) continue;
6775 set_exit_phase(target, permuted[i]);
6776 }
6777}
6778
6779// TradingView orders the children of a strategy.exit that names no entry,
6780// when one path point triggers several, by the entry ids' place in a table
6781// that iterates as a java.util.HashMap<String, ?> keyed by String.hashCode
6782// spread as h ^ (h >>> 16) over power-of-two buckets: buckets ascending, the
6783// newest key first inside one. Its keys are every entry id from its first
6784// fill on, never removed, and one transient key per strategy.close /
6785// close_all order from the call to its fill (an entry order still waiting is
6786// no key); it starts at 16 buckets and doubles once the keys exceed 13/16 of
6787// them, never shrinking (127 TradingView tapes of synthetic scripts,
6788// tests/fixtures/global_exit_children). The pin reaches 16 and 32 buckets
6789// and, in a grown table, no two of the ids in one bucket; whether a
6790// strategy.order market order is a transient key too it leaves open, so a
6791// table the two readings size differently is out of its reach.
6792void PineExecutionAdapter::note_exit_rank_key(const SourceId& id) {
6793 // Past 26 keys the table needs 64 buckets, which the pin does not reach;
6794 // the peak keeps it there, so the ids beyond need no record.
6795 if (exit_rank_keys_.size() > 26
6796 || exit_rank_keys_.find(id).has_value()) {
6797 // The recorded population remains capped at 27 keys, as before.
6798 return;
6799 }
6800 exit_rank_keys_.require_insert(id);
6801 exit_rank_keys_.insert(id, static_cast<std::uint32_t>(exit_rank_keys_.size()));
6802 const std::uint64_t size = id.size();
6803 exit_rank_keys_digest_ = fnv_append(exit_rank_keys_digest_, &size, sizeof(size));
6804 exit_rank_keys_digest_ = fnv_append(exit_rank_keys_digest_, id.data(), id.size());
6805 note_exit_rank_transients();
6806}
6807
6808void PineExecutionAdapter::note_exit_rank_transients() {
6809 assert_live_counts();
6810 const auto& counts = retired_row_scratch_.live_families;
6811 const auto closes = static_cast<std::uint32_t>(
6812 counts[static_cast<std::size_t>(PineOrderFamily::Close)]
6813 + counts[static_cast<std::size_t>(PineOrderFamily::CloseAll)]);
6814 const auto markets = static_cast<std::uint32_t>(
6815 counts[RetiredRowScratch::family_count]);
6816 // This is the same census as the old live-handle walk: family and the
6817 // Order row's source limit/stop are fixed by remember_live. Replacements
6818 // subtract the old row before adding the new one; retire subtracts once.
6819 // Keep the original uint32 arithmetic (including wrap) for the peaks.
6820 // Debug recounts at every reader also check the unpriced-Order slot.
6821 // No mutable activation, allowance or reservation quantity is summarized.
6822
6823 const auto keys = static_cast<std::uint32_t>(exit_rank_keys_.size());
6824 exit_rank_peak_ = std::max(exit_rank_peak_, keys + closes);
6825 exit_rank_alt_peak_ = std::max(exit_rank_alt_peak_, keys + closes + markets);
6826}
6827
6828std::optional<std::vector<std::uint32_t>> PineExecutionAdapter::global_exit_child_ranks(
6829 const std::vector<SourceId>& ids) const {
6830 const auto buckets_for = [](std::uint32_t peak) {
6831 std::uint32_t capacity = 16;
6832 while (static_cast<std::uint64_t>(peak) * 16U > static_cast<std::uint64_t>(capacity) * 13U)
6833 capacity *= 2;
6834 return capacity;
6835 };
6836 const std::uint32_t capacity = buckets_for(exit_rank_peak_);
6837 if (capacity > 32 || buckets_for(exit_rank_alt_peak_) != capacity) return std::nullopt;
6838 std::vector<std::uint32_t> bucket(exit_rank_keys_.size());
6839 std::vector<std::uint32_t> load(capacity, 0);
6840 bool supported = true;
6841 exit_rank_keys_.for_each_inserted([&](const auto& key, auto age) {
6842 std::uint32_t hash = 0;
6843 if (!java_string_hash(key, hash)) { supported = false; return; }
6844 bucket[age] = (hash ^ (hash >> 16)) & (capacity - 1);
6845 // A bin of eight would make java.util.HashMap treeify or resize. The
6846 // count is of entry ids only: a transient close key landing in a bin
6847 // of seven would do the same, which no tape reaches.
6848 if (++load[bucket[age]] >= 8) supported = false;
6849 });
6850 if (!supported) return std::nullopt;
6851 std::vector<std::size_t> ages;
6852 ages.reserve(ids.size());
6853 for (const auto& id : ids) {
6854 const auto found = exit_rank_keys_.find(id);
6855 if (!found) return std::nullopt;
6856 const auto age = static_cast<std::size_t>(exit_rank_keys_.get(*found));
6857 if (capacity > 16) {
6858 for (const auto other : ages)
6859 if (bucket[other] == bucket[age]) return std::nullopt;
6860 }
6861 ages.push_back(age);
6862 }
6863 std::vector<std::uint32_t> ranks;
6864 ranks.reserve(ids.size());
6865 for (const auto age : ages) {
6866 std::uint32_t rank = 0;
6867 for (std::size_t other = 0; other < bucket.size(); ++other) {
6868 if (bucket[other] < bucket[age] || (bucket[other] == bucket[age] && other > age))
6869 ++rank;
6870 }
6871 ranks.push_back(rank);
6872 }
6873 return ranks;
6874}
6875
6877 const SourceId& exit_id,
6878 const native_order::RequestHandle& filled) const {
6879 for (const auto& handle : live_handles_) {
6880 if (handle == filled) continue;
6881 const auto row = placement_.find(handle.incarnation);
6882 if (row == placement_.end()) continue;
6883 const auto& leg = row->second;
6884 // The filled exit's own other leg is one of the same children.
6885 if (leg.from_entry.empty() && leg.source_id == exit_id) continue;
6886 if (leg.family == PineOrderFamily::ExitTrail) return true;
6887 if (leg.family == PineOrderFamily::ExitLimit && finite_positive(leg.exit_levels.limit)
6888 && (closing_long ? price >= leg.exit_levels.limit : price <= leg.exit_levels.limit)) {
6889 return true;
6890 }
6891 if (leg.family == PineOrderFamily::ExitStop && finite_positive(leg.exit_levels.stop)
6892 && (closing_long ? price <= leg.exit_levels.stop : price >= leg.exit_levels.stop)) {
6893 return true;
6894 }
6895 }
6896 return false;
6897}
6898
6899void PineExecutionAdapter::set_exit_phase(std::size_t trade_index, std::uint8_t phase) {
6900 const auto prefix = host_ ? detail::run_consumer(*host_).closed_trade_prefix()
6901 : NativeExecutionConsumer::ClosedTradePrefix{};
6902 if (trade_index < prefix.base)
6904 "exit-phase write precedes the sealed prefix");
6905 const std::size_t offset = trade_index - prefix.base;
6906 if (trade_exit_phase_.empty() && host_)
6907 trade_exit_phase_.set_directory(detail::run_consumer(*host_).report_scratch_directory(*host_));
6908 if (offset >= trade_exit_phase_.size())
6909 trade_exit_phase_.resize(offset + 1, static_cast<std::uint8_t>(NativePathPhase::None));
6910 if (trade_index < exit_phase_final_ && trade_exit_phase_[offset] != phase) {
6911 // A final phase rewritten: fold the prefix afresh at the next mark.
6912 exit_phase_final_ = prefix.base;
6913 exit_phase_digest_ = prefix.exit_phase_digest;
6914 if (host_) detail::run_consumer(*host_).restart_exit_phases();
6915 }
6916 if (trade_exit_phase_[offset] != phase) { invalidate_exit_group(trade_index); } trade_exit_phase_.set(offset, phase);
6917}
6918
6920 auto& host = require_host();
6921 // A PineStrategyHost's command history is read in place, through its
6922 // consumer; any other host is read through native_events().
6923 auto* pine = pine_view_of(&host);
6924 const NativeExecutionConsumer* consumer = pine ? &detail::run_consumer(*pine) : nullptr;
6925 // Quiet: no command event above the cursor -- nothing at all above it,
6926 // or only driver points and account rows, which are never observed. This
6927 // is every read of a bar that issued no command. The read below would
6928 // then visit nothing and end with the cursor on the high water, which is
6929 // all this does. It also leaves the terminal watermark alone, as the read
6930 // would: a terminal receipt is a command, and every command's ordinal
6931 // exceeds the high water of the read before it, so the first read after
6932 // one always takes the full path, which raises the watermark past it.
6933 if (consumer) {
6934 const std::uint64_t high_water = consumer->event_high_water();
6935 const bool quiet = high_water <= receipt_cursor_
6936 || !consumer->has_command_after(receipt_cursor_);
6938 // No acknowledgement (V19-B's, at the end of the full read) is
6939 // needed: no command lies between the one the last full read
6940 // acknowledged and the cursor, so acknowledging the new cursor
6941 // would let the journal window retire nothing more.
6942 receipt_cursor_ = std::max(receipt_cursor_, high_water);
6943 return;
6944 }
6945 }
6946 // Accepted only adds a working root, so the roster release pass can wait.
6947 // Other commands, including replacements and arms, mark the pass due below.
6948 // Applied callbacks do the same before any nested adapter work.
6949 const auto state = detail::run_state(host);
6950 const bool in_place = consumer != nullptr;
6951 // The terminal watermark is folded into the broker-state hash
6952 // (pine_state_hash.cpp) and has only ever advanced on a run without an
6953 // intrabar path; it still does. The read's early return below no longer
6954 // depends on the path.
6955 std::optional<std::uint64_t> terminal_high_water;
6956 if (in_place && (!state.spec || state.spec->intrabar.is_none())) {
6957 terminal_high_water = consumer->terminal_receipt_high_water();
6958 }
6959 const auto observe = [&](const native_order::CommandEvent& command) {
6960 std::visit([&](const auto& event) {
6961 using Event = std::decay_t<decltype(event)>;
6962 if constexpr (!std::is_same_v<Event, native_order::AcceptedEvent>)
6963 retired_row_scratch_.roster_pass_due = true;
6964 if constexpr (std::is_same_v<Event, native_order::MatchRejectedEvent>
6965 || std::is_same_v<Event, native_order::CancelledEvent>
6966 || std::is_same_v<Event, native_order::NoEffectEvent>) {
6967 // An anchored relative leg whose parent ended without a fill
6968 // left with it (OwnerGone); its queued definition waits for
6969 // the next parent of that id exactly as before.
6970 const auto ended = std::remove_if(
6971 anchored_relative_legs_.begin(), anchored_relative_legs_.end(),
6972 [&](const AnchoredRelativeLeg& leg) {
6973 return leg.handle == event.handle();
6974 });
6975 anchored_relative_stats_.withdrawn += static_cast<std::uint64_t>(
6976 std::distance(ended, anchored_relative_legs_.end()));
6977 anchored_relative_legs_.erase(ended, anchored_relative_legs_.end());
6978 const auto placement = placement_.find(event.handle().incarnation);
6979 if (placement != placement_.end()) {
6980 if constexpr (std::is_same_v<Event, native_order::MatchRejectedEvent>) {
6981 suspend_declined_reversal_brackets(event);
6982 }
6983 const auto handle = event.handle();
6984 const bool opening = placement->second.opening;
6985 placement->second.cancellation = {
6986 std::is_same_v<Event, native_order::CancelledEvent>
6988 1, 0, handle.incarnation,
6989 static_cast<std::int64_t>(placement->second.source_sequence),
6990 handle.incarnation, placement->second.placement_cycle,
6991 placement->second.legs.revision(),
6992 placement->second.requested_qty, kNaN};
6993 retire(handle);
6994 if (opening) cancel_bracket_origin(handle);
6995 }
6996 } else if constexpr (std::is_same_v<Event, native_order::ActivatedEvent>) {
6997 if (std::holds_alternative<native_order::StopLimitLive>(event.after)) {
6998 const auto placement = placement_.find(event.definition->handle.incarnation);
6999 if (placement != placement_.end()) placement->second.stop_limit_activated = true;
7000 }
7001 } else if constexpr (std::is_same_v<Event, native_order::ReservationReducedEvent>) {
7002 const auto placement = placement_.find(event.recipient.incarnation);
7003 if (placement != placement_.end()) {
7004 if (const auto* remaining = std::get_if<native_order::RemainingProjectionUnits>(
7005 &event.after)) {
7006 placement->second.projection_remaining_qty = remaining->q;
7007 }
7008 }
7009 } else if constexpr (std::is_same_v<Event, native_order::ExecutionAppliedEvent>) {
7010 if (event.terminal) cancel_bracket_siblings(event.handle(), event.ordinal);
7011 }
7012 }, command);
7013 };
7014 if (in_place) {
7015 // The in-place command journal reads: visit the command events above the cursor in place
7016 // instead of copying out every row above it. The materialised rows
7017 // were in ordinal order, commands first at a tied ordinal, so the
7018 // cursor held each command's ordinal while that command was observed
7019 // and ended on the high water the rows reached, driver points and
7020 // account rows included; it moves the same way here. A cancel issued
7021 // while observing appends above that high water -- as it appended
7022 // past the end of the snapshot -- and the next read observes it.
7023 const std::uint64_t high_water = consumer->event_high_water();
7024 if (high_water > receipt_cursor_) {
7025 consumer->visit_commands_after(receipt_cursor_,
7026 [&](const native_order::CommandEvent& command) {
7027 receipt_cursor_ = std::max(receipt_cursor_, std::visit(
7028 [](const auto& payload) { return payload.ordinal; }, command));
7029 observe(command);
7030 return false;
7031 });
7032 receipt_cursor_ = std::max(receipt_cursor_, high_water);
7033 }
7034 } else {
7035 for (const auto& row : host.native_events(receipt_cursor_)) {
7036 receipt_cursor_ = std::max(receipt_cursor_, row.ordinal);
7037 if (row.command) observe(*row.command);
7038 }
7039 }
7040 if (terminal_high_water) {
7041 terminal_receipt_cursor_ = std::max(
7042 terminal_receipt_cursor_, *terminal_high_water);
7043 }
7044 // Every command through the cursor is observed, and every later read the
7045 // adapter makes starts above it (follows_same_bar_declined_reversal, the
7046 // margin revival), so the kernel's journal window may retire it.
7047 host.native_acknowledge_events(receipt_cursor_);
7048}
7049
7050native_order::Owner PineExecutionAdapter::owner_for_close(const SourceId& id, bool dynamic) const {
7051 // A global strategy.exit has no source-id cohort. Its source policy is
7052 // HostSized, but the generic book authority remains the whole physical
7053 // position rather than a synthetic empty cohort.
7054 if (id.empty()) return native_order::Independent{};
7055 cohorts_by_id_.require_insert(id);
7056 // pine_fills.cpp:7669-7675 uses from_entry as a position-cycle existence
7057 // gate under FIFO; the actual reduction still consumes the global physical
7058 // roster. ANY alone selects the named cohort as the settlement scope.
7059 const auto found = cohorts_by_id_.find(id);
7060 if (!config_.close_entries_rule_any) {
7061 // Before the named parent has opened, retain the generic cohort's
7062 // NoTarget deferral. Once that parent is live, FIFO settles against
7063 // the global book rather than the named cohort.
7064 if (found == cohorts_by_id_.end() || !(cohort_exposure_for(id) > 0.0)) {
7066 found == cohorts_by_id_.end()
7067 ? const_cast<PineExecutionAdapter*>(this)->cohort_for(id)
7068 : found->second.handle};
7069 }
7070 return native_order::Independent{};
7071 }
7072 // A bracket born by the first-open COOF callback already has one durable
7073 // opening receipt. Bind that exact roster at the callback boundary so its
7074 // next real magnifier tick can consume it; later/deferred source commands
7075 // retain the growing cohort owner.
7076 if (dynamic && coof_recalc_active_ && coof_first_open_
7077 && found != cohorts_by_id_.end() && !found->second.opened.empty()) {
7078 return native_order::BindOpenings{found->second.opened, found->second.cycle};
7079 }
7080 if (dynamic || found == cohorts_by_id_.end()) {
7081 if (found == cohorts_by_id_.end())
7082 return native_order::BindCohort{const_cast<PineExecutionAdapter*>(this)->cohort_for(id)};
7083 return native_order::BindCohort{found->second.handle};
7084 }
7085 return native_order::BindOpenings{found->second.opened, found->second.cycle};
7086}
7087
7088void PineExecutionAdapter::stage_flat_children_before_parent(
7089 const SourceId& parent_id, std::int32_t created_bar,
7090 std::int64_t script_open_ms) {
7091 std::vector<native_order::RequestHandle> children;
7092 for (const auto& handle : live_handles_) {
7093 const auto found = placement_.find(handle.incarnation);
7094 if (found == placement_.end()) continue;
7095 const auto& child = found->second;
7096 const bool bracket = child.family == PineOrderFamily::ExitLimit
7097 || child.family == PineOrderFamily::ExitStop;
7098 if (bracket && child.from_entry == parent_id
7099 && child.projection_position_side
7100 == static_cast<std::int32_t>(PositionSide::FLAT)
7101 && child.projection_created_bar == created_bar
7102 && child.placement_script_open_ms == script_open_ms) {
7103 children.push_back(handle);
7104 }
7105 }
7106 for (const auto& handle : children) {
7107 const auto found = placement_.find(handle.incarnation);
7108 if (found == placement_.end()) continue;
7109 PlacementSnapshot snapshot = found->second;
7110 native_order::Request request;
7111 request.intent = native_order::HostSized{
7112 native_order::HostSizedKind::Close, std::nullopt};
7113 request.label = snapshot.source_id;
7114 request.comment = snapshot.comment;
7115 if (snapshot.family == PineOrderFamily::ExitLimit) {
7116 request.trigger = native_order::Limit{snapshot.exit_levels.limit};
7117 } else {
7118 request.trigger = native_order::Stop{snapshot.exit_levels.stop};
7119 }
7120 request.owner = owner_for_close(snapshot.from_entry, true);
7121 const std::string group_name = snapshot.oca_name.empty()
7122 ? snapshot.source_id + "\x1f" + snapshot.from_entry
7123 : snapshot.oca_name;
7124 request.group = group_for(group_name, 1);
7125 snapshot.defer_until_post_parent_calculation = true;
7126 const SourceId replacement_key = snapshot.source_id + "\x1f"
7127 + snapshot.from_entry + std::to_string(static_cast<int>(snapshot.family));
7128 const auto cancelled = require_host().cancel(handle);
7129 if (cancelled.status != native_order::CancelStatus::Cancelled) continue;
7130 retire(handle);
7131 auto queued = std::find_if(pending_bracket_legs_.begin(), pending_bracket_legs_.end(),
7132 [&](const PendingBracketLeg& row) {
7133 return row.replacement_key == replacement_key;
7134 });
7135 PendingBracketLeg staged{std::move(request), std::move(snapshot),
7136 replacement_key,
7137 key_for(found->second.source_id,
7138 found->second.from_entry)};
7139 if (queued == pending_bracket_legs_.end()) {
7140 pending_write(pending_bracket_legs_).push_back(std::move(staged));
7141 } else {
7142 pending_write(*queued) = std::move(staged);
7143 }
7144 }
7145 refresh_pending_view();
7146}
7147
7150 const NativeDecisionContext& context, bool first_open,
7151 std::uint64_t source_fill_sequence) {
7152 coof_recalc_active_ = true;
7153 coof_first_open_ = first_open;
7154 coof_current_fill_seq_ = source_fill_sequence;
7155 coof_fill_cursor_t_ = event.cursor.t;
7156 coof_market_entry_recalc_fill_seq_ = source_fill_sequence;
7157 coof_market_entry_recalc_incarnation_ = 0;
7158 const auto placement = placement_.find(event.handle().incarnation);
7159 coof_fill_target_print_ = kNaN;
7160 if (!bar_magnifier_ && placement != placement_.end()
7161 && finite_positive(placement->second.forced_execution_price)
7162 && finite_positive(placement->second.forced_waypoint_print)
7163 && placement->second.placement_script_open_ms == context.script_bar_open_ms) {
7164 const double tick = staged_.syminfo.mintick;
7165 const double target = placement->second.forced_waypoint_print;
7166 const double slippage = config_.slippage * tick;
7167 const double forced_price = placement->second.forced_execution_price;
7168 const bool paired = same_double_bits(forced_price, nearest_tick(target, tick) + slippage)
7169 || same_double_bits(forced_price, nearest_tick(target, tick) - slippage)
7170 || same_double_bits(forced_price, source_bar_fill_tick(target, tick) + slippage)
7171 || same_double_bits(forced_price, source_bar_fill_tick(target, tick) - slippage);
7172 if (paired) coof_fill_target_print_ = target;
7173 }
7174 coof_open_stop_next_waypoint_ = placement != placement_.end()
7175 && placement->second.coof_open_stop_next_waypoint;
7176 if (event.opened_units != 0.0
7177 && std::holds_alternative<native_order::Market>(event.request().trigger)) {
7178 if (placement != placement_.end()
7179 && placement->second.family == PineOrderFamily::Entry) {
7180 coof_market_entry_recalc_incarnation_ = event.handle().incarnation;
7181 }
7182 }
7183 // An exit limit already through where a recalculation of the open's
7184 // refill placed it is armed at the first extreme, which satisfies its
7185 // level (exit(): StopLimit{W1, level}), and gap-fills at that point: a
7186 // fill forced onto the extreme, not the matcher's at the limit's own
7187 // level, even with that level exactly on it -- the orders its
7188 // recalculation places start the next leg, as TradingView books them
7189 // (lab tv int27-open-x*-w1-reentry-*).
7190 const auto* armed_limit = placement != placement_.end()
7191 && placement->second.family == PineOrderFamily::ExitLimit
7192 ? std::get_if<native_order::StopLimit>(&event.request().trigger) : nullptr;
7193 const bool armed_limit_gap_fill = armed_limit
7194 && (placement->second.projection_position_side
7195 == static_cast<std::int32_t>(PositionSide::LONG)
7196 ? armed_limit->stop >= armed_limit->limit
7197 : armed_limit->stop <= armed_limit->limit)
7198 && coof_fill_at_path_point(armed_limit->stop);
7199 coof_fill_forced_ = placement != placement_.end()
7200 && (finite_positive(placement->second.forced_execution_price)
7201 || armed_limit_gap_fill);
7202 coof_context_ = context;
7203}
7204
7206 coof_recalc_active_ = false;
7207 coof_first_open_ = false;
7208 coof_open_stop_next_waypoint_ = false;
7209 coof_fill_forced_ = false;
7210 coof_market_entry_recalc_incarnation_ = 0;
7211 coof_market_entry_recalc_fill_seq_ = 0;
7212 coof_current_fill_seq_ = 0;
7213 coof_fill_cursor_t_ = kNaN;
7214 coof_fill_target_print_ = kNaN;
7215 coof_context_ = {};
7216}
7217
7220 const NativeDecisionContext& context) const noexcept {
7221 // Every exit of one entry that the fill's point also triggers fills there
7222 // before the point recalculates: the recalculation after the first cannot
7223 // cancel or outrun the others (lab tv tapes
7224 // tests/fixtures/coof_same_point_exits td-m1e, -f and -g: two exits of one
7225 // entry share a stop, and the recalculation of a stop fill calls
7226 // cancel_all() and close_all(); TradingView books both at the stop; lane
7227 // TAIL-D). The recalculation of the point's last such fill runs.
7228 if (host_ && config_.calc_on_order_fills && event.closed_units > 0.0) {
7229 const double held = detail::run_position(*host_).signed_units;
7230 const auto filled = placement_.find(event.handle().incarnation);
7231 if (held != 0.0 && filled != placement_.end() && !filled->second.from_entry.empty()
7232 && (filled->second.family == PineOrderFamily::ExitStop
7233 || filled->second.family == PineOrderFamily::ExitLimit)) {
7234 const double price = event.raw_price;
7235 const bool closing_long = held > 0.0;
7236 for (const auto& handle : live_handles_) {
7237 if (handle == event.handle()) continue;
7238 const auto sibling = placement_.find(handle.incarnation);
7239 if (sibling == placement_.end()) continue;
7240 const auto& row = sibling->second;
7241 if (row.from_entry != filled->second.from_entry
7242 || row.source_id == filled->second.source_id) {
7243 continue;
7244 }
7245 const double stop = row.exit_levels.stop;
7246 const double limit = row.exit_levels.limit;
7247 // The point's print and a level the script computed meet on
7248 // the tick grid, not in their last bits.
7249 const auto at = [tick = staged_.syminfo.mintick, price](double level) {
7250 return price == level || (finite_positive(tick)
7251 && nearest_tick(price, tick) == nearest_tick(level, tick));
7252 };
7253 const bool triggered =
7254 (row.family == PineOrderFamily::ExitStop && finite_positive(stop)
7255 && ((closing_long ? price < stop : price > stop) || at(stop)))
7256 || (row.family == PineOrderFamily::ExitLimit && finite_positive(limit)
7257 && ((closing_long ? price > limit : price < limit) || at(limit)));
7258 if (triggered) return true;
7259 }
7260 }
7261 }
7262 if (!host_ || !config_.calc_on_order_fills || config_.process_orders_on_close
7263 || !modeled_input() || config_.pyramiding != 0 || config_.close_entries_rule_any
7264 || config_.slippage != 0 || config_.commission_value != 0.0
7265 || staged_.account_fx != 1.0 || !staged_.account_fx_effective_from_ms.empty()
7266 || risk_.max_intraday_loss != 0.0 || risk_.max_drawdown != 0.0
7267 || risk_.max_cons_loss_days != 0 || cap.active()
7268 || context.coordinate.path_phase != NativePathPhase::Low
7269 || !policy_script_bar_valid_ || event.closed_units <= 0.0
7270 || detail::run_position(*host_).signed_units <= 0.0) {
7271 return false;
7272 }
7273 const auto state = detail::run_state(*host_);
7274 if (state.spec && !state.spec->intrabar.is_none()) return false;
7275 const auto filled = placement_.find(event.handle().incarnation);
7276 if (filled == placement_.end()) return false;
7277 const auto eligible = [&](const PlacementSnapshot& row) {
7278 return row.family == PineOrderFamily::ExitStop
7279 && row.projection_created_bar < projection_bar_index(context)
7280 && row.from_entry == filled->second.from_entry && !row.from_entry.empty()
7281 && std::isfinite(row.requested_qty) && row.requested_qty > 0.0
7282 && row.oca_name.empty() && std::isnan(row.exit_levels.trail_points)
7283 && std::isnan(row.exit_levels.trail_price)
7284 && std::isfinite(row.exit_levels.stop)
7285 && row.exit_levels.stop <= event.resolved_price
7286 && row.exit_levels.stop >= policy_script_bar_.low;
7287 };
7288 if (!eligible(filled->second)) return false;
7289 return std::any_of(live_handles_.begin(), live_handles_.end(),
7290 [&](const native_order::RequestHandle& handle) {
7291 if (handle == event.handle()) return false;
7292 const auto sibling = placement_.find(handle.incarnation);
7293 return sibling != placement_.end() && eligible(sibling->second);
7294 });
7295}
7296
7299 const NativeDecisionContext& context) const noexcept {
7300 if (context.coordinate.provenance != NativePriceProvenance::Calculation) return false;
7301 const auto filled = placement_.find(event.handle().incarnation);
7302 return filled != placement_.end() && filled->second.immediately
7303 && (filled->second.family == PineOrderFamily::Close
7304 || filled->second.family == PineOrderFamily::CloseAll);
7305}
7306
7307bool PineExecutionAdapter::defer_coof_tail() const noexcept {
7308 if (!coof_recalc_active_ || coof_first_open_) return false;
7309 const auto state = detail::run_state(require_host());
7310 if (state.spec && state.spec->intrabar.lower()) return false;
7311 const auto phase = coof_context_.coordinate.path_phase;
7312 if (phase == NativePathPhase::Close || phase == NativePathPhase::None)
7313 return true;
7314 // A fill the adapter forced onto the second extreme leaves the
7315 // recalculation the close point alone; the matcher's own fill there
7316 // does not (coof_fill_at_second_extreme, H-MEASURE Finding 6d).
7317 return coof_fill_forced_ && coof_fill_at_second_extreme();
7318}
7319
7320// The recalculating fill sits AT the bar's second extreme, the end of its
7321// approach leg (the high of a low-first bar, the low of a high-first one).
7322// Two fills book that point, and TradingView answers the recalculation's
7323// market orders differently for them. The matcher's fill of a resting
7324// request at its own level there ends the approach leg: those orders fill at
7325// the extreme (hm-chart-diff-v2 / -v3 / -v6 / -v7 bar 19, lab tv
7326// pa2-i2-exit-w2-* and pa2-i4-close-w2-*). A request an earlier
7327// recalculation of the bar placed, which the adapter forced onto the extreme
7328// it gap-fills at (coof_fill_forced_: forced_execution_price), fills at the
7329// point itself: they advance to the close, as ab9714be's endpoint rule has
7330// it (corpus bracket-rivet-calc-on-fill-01 trades 350 and 516; the
7331// recalculation fill-order correction).
7332bool PineExecutionAdapter::coof_fill_at_second_extreme() const noexcept {
7333 if (!coof_recalc_active_ || coof_first_open_ || !coof_script_bar_valid_) return false;
7334 const bool high_first = source_path_uses_high_first(coof_script_bar_);
7335 const NativePathPhase second = high_first
7336 ? NativePathPhase::Low : NativePathPhase::High;
7337 const double endpoint = high_first ? coof_script_bar_.low : coof_script_bar_.high;
7338 return coof_context_.coordinate.path_phase == second
7339 && coof_fill_at_path_point(endpoint);
7340}
7341
7342bool PineExecutionAdapter::coof_fill_on_path_point() const noexcept {
7343 // The recalculating fill's matcher cursor sits at a leg end (t 0 or 1),
7344 // i.e. on an O/H/L/C point, rather than inside a leg.
7345 return coof_recalc_active_
7346 && (coof_fill_cursor_t_ == 0.0 || coof_fill_cursor_t_ == 1.0);
7347}
7348
7349bool PineExecutionAdapter::coof_fill_at_path_point(double waypoint) const noexcept {
7350 const auto point = detail::callback_point(require_host());
7351 if (!point) return false;
7352 // ab9714be engine.hpp:1264-1266: an on-grid print books one ULP-exact
7353 // tick. An OFF-grid waypoint books bar_fill_price(waypoint)
7354 // (pine_fills.cpp:7962-7966), a tick a mid-leg fill can also book, so
7355 // that tick names the waypoint only for a fill placed on a path point.
7356 const double tick = staged_.syminfo.mintick;
7357 if (coof_recalc_active_ && detail::script_rule_switches().coof_forced_fill_at_waypoint
7358 && finite_positive(coof_fill_target_print_))
7359 return source_same_point(coof_fill_target_print_, waypoint, tick);
7360 if (source_same_point(point->price, waypoint, tick)) return true;
7361 return finite_positive(tick) && source_bar_fill_tick(waypoint, tick) != waypoint
7362 && source_decimal_tick(point->price, tick) == source_decimal_tick(waypoint, tick)
7363 && coof_fill_on_path_point();
7364}
7365
7366// On a magnified path TradingView's next fill point after an intrabar's
7367// second extreme, or after the chart bar's own open print, is the next
7368// intrabar's opening (PineScheduler::next_input_waypoint). A plain market
7369// order reaches exactly that point -- the kernel fills a newborn market
7370// request at the path's next discrete point -- even when it prints the price
7371// the fill recalculation stands at, where a waypoint trigger would fill at
7372// once.
7373bool PineExecutionAdapter::coof_waits_for_next_sub_bar() const noexcept {
7374 const auto* pine_host = pine_view_of(host_);
7375 if (!pine_host) return false;
7376 const auto next = pine_host->scheduler_.next_input_waypoint(*pine_host, coof_context_);
7377 return next && next->next_open;
7378}
7379
7380// The leg order the kernel walks `bar` in -- the run's declared
7381// NativeRunSpec::path_order, the open-proximity rule under Auto
7382// (NativeExecutionConsumer::path_high_first). The source layer asks rather
7383// than keeping a copy of the rule.
7384bool PineExecutionAdapter::source_path_uses_high_first(const Bar& bar) const noexcept {
7385 auto* pine = pine_view_of(host_);
7386 return pine != nullptr && detail::run_consumer(*pine).path_high_first(bar);
7387}
7388
7389bool PineExecutionAdapter::coof_current_fill_was_forced_waypoint() const noexcept {
7390 // L6b encodes a later COOF MARKET fill as a priced source waypoint. At
7391 // the Applied callback its raw coordinate still names the segment on
7392 // which it was born; advance from the forced waypoint rather than walking
7393 // back to that segment's endpoint (ab9714be pine_scheduler.cpp:398-619).
7394 if (!coof_recalc_active_) return false;
7395 const auto point = detail::callback_point(require_host());
7396 if (!point) return false;
7397 for (const auto& cohort : cohorts_by_id_.held_rows()) {
7398 for (const auto& opening : cohort.second.opened) {
7399 const auto units = cohort.second.live_units_by_origin.find(
7400 opening.incarnation);
7401 if (units == cohort.second.live_units_by_origin.end()
7402 || !(units->second > 0.0)) {
7403 continue;
7404 }
7405 const auto snapshot = placement_.find(opening.incarnation);
7406 if (snapshot != placement_.end() && snapshot->second.opening
7407 && snapshot->second.projection_created_during_coof
7408 && snapshot->second.placement_script_open_ms
7409 == coof_context_.script_bar_open_ms
7410 && same_double_bits(snapshot->second.forced_execution_price,
7411 point->price)) {
7412 return true;
7413 }
7414 }
7415 }
7416 return false;
7417}
7418
7419double PineExecutionAdapter::coof_next_waypoint(int* path_index) const noexcept {
7420 // path_index (optional) receives the returned waypoint's historical path
7421 // index (1/2 = extreme W1/W2, 3 = C), or -1 when it is not an O/H/L/C
7422 // point of the chart bar (lower-timeframe path, no waypoint).
7423 if (path_index) *path_index = -1;
7424 if (!coof_recalc_active_ || !coof_script_bar_valid_) return kNaN;
7425 const auto state = detail::run_state(require_host());
7426 if (state.spec && state.spec->intrabar.lower()) {
7427 if (const auto* pine_host = pine_view_of(&require_host())) {
7428 const auto next = pine_host->scheduler_.next_input_waypoint(
7429 *pine_host, coof_context_);
7430 if (next) return next->price;
7431 }
7432 }
7433 const bool high_first = source_path_uses_high_first(coof_script_bar_);
7434 const NativePathPhase path_phase[] = {
7435 NativePathPhase::Open,
7436 high_first ? NativePathPhase::High : NativePathPhase::Low,
7437 high_first ? NativePathPhase::Low : NativePathPhase::High,
7438 NativePathPhase::Close,
7439 };
7440 const double path_price[] = {
7441 coof_script_bar_.open,
7442 high_first ? coof_script_bar_.high : coof_script_bar_.low,
7443 high_first ? coof_script_bar_.low : coof_script_bar_.high,
7444 coof_script_bar_.close,
7445 };
7446 const bool forced_waypoint = coof_current_fill_was_forced_waypoint();
7447 for (int index = 0; index < 4; ++index) {
7448 if (path_phase[index] != coof_context_.coordinate.path_phase) continue;
7449 const auto point = detail::callback_point(require_host());
7450 // ab9714be pine_fills.cpp:7962-7966 and pine_scheduler.cpp:548-563: a
7451 // fill at a waypoint POINT books bar_fill_price(waypoint), the
7452 // nearest-tick rounding of the raw OHLC price, so the applied price
7453 // identifies that point only on the tick grid. source_bar_fill_tick
7454 // keeps an on-grid n * mintick booking one ULP off the waypoint's
7455 // decimal print, so compare the decimal grid forms.
7456 // An OFF-grid waypoint's booked tick is also reachable mid-leg (a
7457 // limit at 13.26 on the H->L 13.255 leg), so it names the point
7458 // only for a fill the matcher placed on a path point.
7459 const double tick = staged_.syminfo.mintick;
7460 const bool on_grid_waypoint = finite_positive(tick)
7461 && source_bar_fill_tick(path_price[index], tick) == path_price[index];
7463 && finite_positive(coof_fill_target_print_)
7464 ? source_same_point(coof_fill_target_print_, path_price[index], tick)
7465 : point && finite_positive(tick)
7466 ? source_decimal_tick(point->price, tick)
7467 == source_decimal_tick(path_price[index], tick)
7468 && (on_grid_waypoint || coof_fill_on_path_point())
7469 : point && point->price == path_price[index];
7470 if (index > 0 && point && finite_positive(point->price)
7471 && !at_waypoint && !forced_waypoint) {
7472 if (path_index) *path_index = index;
7473 return path_price[index];
7474 }
7475 // The matcher's fill of a resting request at its own level ON an
7476 // extreme ends the leg there: the recalculation's orders act from that
7477 // point, as TradingView books them -- a market order, a close or
7478 // close_all fills at it, a priced order is live at its own level from
7479 // it (lab tv pa3-f4-level-w1-*; the second extreme, H-MEASURE Finding
7480 // 6d, pa2-i2-exit-w2-*). A fill the adapter forced onto the point
7481 // starts the next leg (pa3-f4-point-w1-mkt-*; the extreme-point fill correction).
7482 if (index > 0 && index < 3 && at_waypoint && !forced_waypoint
7483 && !coof_fill_forced_) {
7484 if (path_index) *path_index = index;
7485 return path_price[index];
7486 }
7487 if (index == 0 && coof_open_stop_next_waypoint_
7488 && (finite_positive(tick)
7489 ? source_decimal_tick(path_price[1], tick)
7490 == source_decimal_tick(path_price[0], tick)
7491 : path_price[1] == path_price[0])) {
7492 if (path_index) *path_index = 2;
7493 return path_price[2];
7494 }
7495 if (path_index && index < 3) *path_index = index + 1;
7496 return index < 3 ? path_price[index + 1] : kNaN;
7497 }
7498 return kNaN;
7499}
7500
7501bool PineExecutionAdapter::coof_remaining_recrosses(
7502 double level, bool long_position) const noexcept {
7503 if (!finite_positive(level) || !coof_recalc_active_ || !coof_script_bar_valid_)
7504 return false;
7505 const bool high_first = source_path_uses_high_first(coof_script_bar_);
7506 const NativePathPhase path_phase[] = {
7507 NativePathPhase::Open,
7508 high_first ? NativePathPhase::High : NativePathPhase::Low,
7509 high_first ? NativePathPhase::Low : NativePathPhase::High,
7510 NativePathPhase::Close,
7511 };
7512 const double path_price[] = {
7513 coof_script_bar_.open,
7514 high_first ? coof_script_bar_.high : coof_script_bar_.low,
7515 high_first ? coof_script_bar_.low : coof_script_bar_.high,
7516 coof_script_bar_.close,
7517 };
7518 const bool forced_waypoint = coof_current_fill_was_forced_waypoint();
7519 for (int index = 0; index < 4; ++index) {
7520 if (path_phase[index] != coof_context_.coordinate.path_phase) continue;
7521 const auto point = detail::callback_point(require_host());
7522 int first = index + 1;
7523 if (index > 0 && point && finite_positive(point->price)
7524 && !source_same_point(point->price, path_price[index], staged_.syminfo.mintick)
7525 && !forced_waypoint) {
7526 first = index;
7527 }
7528 bool crossed_adverse = false;
7529 for (int cursor = first; cursor < 4; ++cursor) {
7530 if (long_position) {
7531 if (path_price[cursor] < level) crossed_adverse = true;
7532 else if (crossed_adverse && path_price[cursor] >= level) return true;
7533 } else {
7534 if (path_price[cursor] > level) crossed_adverse = true;
7535 else if (crossed_adverse && path_price[cursor] <= level) return true;
7536 }
7537 }
7538 return false;
7539 }
7540 return false;
7541}
7542
7543// Under calc_on_order_fills TradingView applies what the recalculation of a
7544// mid-leg fill cancels or closes -- strategy.cancel, strategy.cancel_all, a
7545// full strategy.close -- at the end of that leg: an exit resting from before
7546// whose level the rest of the leg reaches still fills there, at its level,
7547// and one the leg does not reach is gone after it (lab tv tapes
7548// tests/fixtures/coof_inflight_cancel td-m7a .. td-m7d; lane TAIL-D).
7549bool PineExecutionAdapter::coof_leg_reaches_exit(
7550 const PlacementSnapshot& row) const noexcept {
7551 if (!coof_recalc_active_ || coof_first_open_ || !coof_script_bar_valid_) return false;
7552 if (row.family != PineOrderFamily::ExitLimit && row.family != PineOrderFamily::ExitStop)
7553 return false;
7554 const auto point = detail::callback_point(require_host());
7555 const double units = detail::run_position(require_host()).signed_units;
7556 const double leg_end = coof_next_waypoint();
7557 if (!point || !finite_positive(point->price) || units == 0.0 || !finite_positive(leg_end))
7558 return false;
7559 const double price = point->price;
7560 const bool closing_long = units > 0.0;
7561 // A sell limit and a buy stop wait above the price, a buy limit and a
7562 // sell stop below it.
7563 const bool above = (row.family == PineOrderFamily::ExitLimit) == closing_long;
7564 const double level = row.family == PineOrderFamily::ExitLimit
7565 ? row.exit_levels.limit : row.exit_levels.stop;
7566 if (!finite_positive(level)) return false;
7567 return above ? level > price && level <= leg_end : level < price && level >= leg_end;
7568}
7569
7570void PineExecutionAdapter::flush_coof_tail(
7571 bool openings_only, bool include_next_open) {
7572 auto queued = std::move(pending_write(pending_coof_requests_));
7573 pending_write(pending_coof_requests_).clear();
7574 for (auto& pending : queued) {
7575 if ((pending.next_open && !include_next_open)
7576 || (openings_only && (!pending.opening
7577 || std::holds_alternative<native_order::Market>(
7578 pending.request.trigger)))) {
7579 pending_write(pending_coof_requests_).push_back(std::move(pending));
7580 continue;
7581 }
7582 const auto accepted = submit_or_replace(std::move(pending.request), std::move(pending.snapshot),
7583 pending.opening, pending.replacement_key);
7584 if (accepted && pending.family_key != 0)
7585 bracket_family(pending.family_key).push_back(*accepted);
7586 }
7587}
7588
7589std::size_t PineExecutionAdapter::accepted_entries_in_cycle(
7590 bool is_long, const SourceId& id) const {
7591 std::size_t accepted_in_cycle = 0;
7592 for (const auto& cohort : cohorts_by_id_.held_rows()) {
7593 for (const auto& origin : cohort.second.opened) {
7594 const auto placement = placement_.find(origin.incarnation);
7595 if (placement != placement_.end() && placement->second.is_long == is_long)
7596 ++accepted_in_cycle;
7597 }
7598 }
7599 for (const auto& handle : live_handles_) {
7600 const auto placement = placement_.find(handle.incarnation);
7601 // ab9714be pine_strategy_commands.cpp:446-464: remove_same_id_pending_orders excludes replaced id before pyramiding check
7602 // TradingView counts a pending same-side MARKET entry against the
7603 // cap and never a pending LIMIT entry (lane W8A-SIGSTATE-1 R-B,
7604 // tests/fixtures/pyramiding_open_order).
7605 if (placement == placement_.end() || !placement->second.opening
7606 || placement->second.is_long != is_long) continue;
7607 if (placement->second.source_id != id && !pure_limit_entry(placement->second)) {
7608 ++accepted_in_cycle;
7609 continue;
7610 }
7611 // A synchronous fill can leave its applied notification queued.
7612 // Such an opening already occupies a slot, even for the same id or
7613 // a limit order. An unfilled replacement has no physical origin.
7614 const auto cohort = cohorts_by_id_.find_resident(placement->second.source_id);
7615 if (cohort != cohorts_by_id_.end()
7616 && std::find(cohort->second.opened.begin(), cohort->second.opened.end(), handle)
7617 != cohort->second.opened.end()) continue;
7618 struct Opening {
7619 std::uint64_t incarnation;
7620 bool settled = false;
7621 } opening{handle.incarnation};
7622 (void)require_host().visit_native_open_lot_values(
7623 &opening, [](void* context, std::uint64_t origin, double, double units) {
7624 auto& opening = *static_cast<Opening*>(context);
7625 if (origin != opening.incarnation || units == 0.0) return true;
7626 opening.settled = true;
7627 return false;
7628 });
7629 if (opening.settled) ++accepted_in_cycle;
7630 }
7631 return accepted_in_cycle;
7632}
7633
7634bool PineExecutionAdapter::opposite_entry_opening_pending(bool is_long) const {
7635 return std::any_of(live_handles_.begin(), live_handles_.end(),
7636 [&](const native_order::RequestHandle& handle) {
7637 const auto existing = placement_.find(handle.incarnation);
7638 return existing != placement_.end() && existing->second.opening
7639 && existing->second.family == PineOrderFamily::Entry
7640 && existing->second.is_long != is_long;
7641 });
7642}
7643
7644void PineExecutionAdapter::entry(const SourceId& id, bool is_long, double limit_price,
7645 double stop_price, double qty, const std::string& comment,
7646 const std::string& oca_name, int oca_type, int qty_type) {
7647 place_entry(id, is_long, limit_price, stop_price, qty, comment, oca_name, oca_type, qty_type);
7648 if (PF_RARE(!id_bound_exits_.empty())) rearm_id_bound_exits(id);
7649}
7650
7651void PineExecutionAdapter::place_entry(const SourceId& id, bool is_long, double limit_price,
7652 double stop_price, double qty, const std::string& comment,
7653 const std::string& oca_name, int oca_type, int qty_type) {
7654 require_cohort_ticket(&id);
7655 native_order::Request request;
7656 // An infinite quantity trades the default quantity, as na does (lab tv
7657 // tailc-a-qty-nonfinite2: +-Infinity and na legs all enter at the
7658 // strategy's default; the placement and recalculation correction).
7659 fold_source_money();
7660 const bool default_sized = !std::isfinite(qty);
7661 const bool priced = !std::isnan(limit_price) || !std::isnan(stop_price);
7662 const bool explicit_fixed = !default_sized
7663 && (qty_type < 0 || qty_type == static_cast<int>(QtyType::FIXED));
7664 const double normalized_qty = explicit_fixed
7665 ? explicit_entry_quantity_floor(std::abs(qty), staged_.quantity_grid) : qty;
7666 const double signed_target = is_long ? normalized_qty : -normalized_qty;
7667 const double current = detail::run_position(require_host()).signed_units;
7668 const auto source_point = detail::callback_point(require_host());
7669 double flat_pending_opposite_market_units = 0.0;
7670 if (!source_point
7671 && detail::run_state(require_host()).kind == NativeLifecycleKind::Unconfigured) {
7672 // The historical source host allowed constructor-time commands to be
7673 // inspected before a run. They have no native decision coordinate
7674 // yet, so retain only their source command projection; the native
7675 // request core is not invoked outside an admitted begin.
7676 request.intent = default_sized
7678 native_order::HostSizedKind::Open,
7679 is_long ? native_order::Side::Long : native_order::Side::Short}}
7681 request.label = id;
7682 request.comment = comment;
7683 request.trigger = trigger_for(limit_price, stop_price);
7684 request.group = group_for(oca_name, oca_type);
7685 PlacementSnapshot snapshot;
7687 snapshot.source_id = id;
7688 snapshot.comment = comment;
7689 snapshot.oca_name = oca_name;
7690 snapshot.oca_type = oca_type;
7691 snapshot.qty_type = qty_type;
7692 snapshot.requested_qty = normalized_qty;
7693 snapshot.is_long = is_long;
7694 snapshot.opening = true;
7695 snapshot.deferred_cohort = default_sized;
7696 snapshot.command_ordinal = ++command_ordinal_;
7697 snapshot.command_sequence = ++source_command_sequence_;
7698 snapshot.source_sequence = ++source_sequence_;
7699 snapshot.exit_levels.limit = limit_price;
7700 snapshot.exit_levels.stop = stop_price;
7701 snapshot.sizing = sizing_snapshot();
7702 pending_write(pending_entries_).push_back(
7703 {std::move(request), std::move(snapshot), id});
7704 return;
7705 }
7706 if (source_point) {
7707 const int bar = source_point->decision.coordinate.interval_index;
7708 if (entry_attempt_bar_ != bar) {
7709 entry_attempt_bar_ = bar;
7710 entry_attempts_on_bar_ = 0;
7711 }
7712 if (entry_attempts_on_bar_ != std::numeric_limits<std::uint32_t>::max())
7713 ++entry_attempts_on_bar_;
7714 }
7715 bool close_precedes_entry = pending_same_bar_close_qty_ > 0.0;
7716 const double preceding_close_qty = pending_same_bar_close_qty_;
7717 // The close-first fact (MarginRuleSwitches::close_first_admission): the
7718 // script closed the position this entry opposes -- strategy.close(<held
7719 // id>) or strategy.close_all() -- earlier on this script bar, however
7720 // that close sized or deferred its own order. TradingView fills such an
7721 // entry flat behind the close at the same point and never checks it on
7722 // the price scale, at placement (rule 5) or at the fill; rule 2 still
7723 // judges it (tests/fixtures/admission_rules cf1..cf10, c3b).
7724 const bool close_first_fact = source_point && current != 0.0
7725 && ((current > 0.0) != is_long)
7726 && close_first_script_bar_ == source_point->decision.script_bar_open_ms
7727 && close_first_side_ == (current > 0.0 ? 1 : -1);
7728 const bool close_first = close_first_fact
7730 native_order::RequestHandle preceding_close_request{};
7731 if (source_point) {
7732 for (auto it = live_handles_.rbegin(); it != live_handles_.rend(); ++it) {
7733 const auto& handle = *it;
7734 const auto prior = placement_.find(handle.incarnation);
7735 if (prior == placement_.end()) continue;
7736 const auto& row = prior->second;
7737 if ((row.family == PineOrderFamily::Close
7738 || row.family == PineOrderFamily::CloseAll)
7739 && !row.immediately
7740 && row.placement_script_open_ms
7741 == source_point->decision.script_bar_open_ms) {
7742 close_precedes_entry = true;
7743 preceding_close_request = handle;
7744 break;
7745 }
7746 }
7747 }
7748 limit_price = source_level_on_price_grid(
7749 limit_price, staged_.syminfo.mintick, value_field::entry_limit_price);
7750 stop_price = source_level_on_price_grid(
7751 stop_price, staged_.syminfo.mintick, value_field::entry_stop_price);
7752 const bool pure_stop_entry = std::isnan(limit_price)
7753 && finite_positive(stop_price);
7754 if (intraday_loss_orders_blocked()
7755 || (source_point && cap_placement_denied(source_point->decision))) {
7756 return;
7757 }
7758 if (pure_stop_entry && preceding_close_qty > 0.0
7759 && !pending_same_bar_commands_.empty()) {
7760 // The fixed-default batching path had retained the earlier close
7761 // outside the core. Publish it before the later priced entry so the
7762 // native book receives the legacy source statement order.
7763 flush_pending_same_bar_commands();
7764 if (source_point) {
7765 for (auto it = live_handles_.rbegin(); it != live_handles_.rend(); ++it) {
7766 const auto prior = placement_.find(it->incarnation);
7767 if (prior == placement_.end()
7768 || prior->second.family != PineOrderFamily::Close
7769 || prior->second.placement_script_open_ms
7770 != source_point->decision.script_bar_open_ms) {
7771 continue;
7772 }
7773 preceding_close_request = *it;
7774 break;
7775 }
7776 }
7777 }
7778 if (config_.calc_on_order_fills && priced && !coof_recalc_active_
7779 && source_point && current != 0.0 && ((current > 0.0) == is_long)) {
7780 std::vector<native_order::RequestHandle> stale_recalc_entries;
7781 for (const auto& handle : live_handles_) {
7782 const auto prior = placement_.find(handle.incarnation);
7783 if (prior == placement_.end()) continue;
7784 const auto& row = prior->second;
7785 if (row.opening && row.family == PineOrderFamily::Entry
7786 && row.is_long == is_long && row.projection_created_during_coof
7787 && row.projection_created_bar
7788 == source_point->decision.coordinate.interval_index) {
7789 stale_recalc_entries.push_back(handle);
7790 }
7791 }
7792 for (const auto& handle : stale_recalc_entries) {
7793 const auto cancelled = require_host().cancel(handle);
7794 if (cancelled.status == native_order::CancelStatus::Cancelled)
7795 retire(handle);
7796 }
7797 }
7798 // Explicit entry quantities have a source placement-time admission
7799 // boundary. In particular, finite values whose required margin
7800 // overflows must never become a live generic request that waits until a
7801 // later matching point to be rejected.
7802 if (!default_sized) {
7803 const bool opposite_live = current != 0.0 && ((current > 0.0) != is_long);
7804 if (!opposite_live) {
7805 // Admission is a source command fact at the signal mark; a
7806 // priced entry's later trigger/gap check remains at fill time.
7807 const double mark = source_point ? source_point->price : kNaN;
7808 const double margin = is_long ? config_.margin_long : config_.margin_short;
7809 const double fx = source_point
7810 ? active_staged_fx(source_point->decision.sub_bar_open_ms) : staged_.account_fx;
7811 const double equity = source_point
7812 ? require_host().native_marked_equity(source_point->price) : kNaN;
7813 // A typed quantity names money, not units: the admission prices
7814 // the units that money buys at the mark -- the conversion its fill
7815 // books (typed_entry_units), taken at the signal. Reading the cash
7816 // as units dropped a 1000-cash entry at 125 on 100 000 of equity.
7817 double units = std::abs(normalized_qty);
7818 if (qty_type == static_cast<int>(QtyType::CASH)) {
7819 units = typed_quantity_units(std::abs(qty), false, mark, fx);
7820 } else if (qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)) {
7821 units = typed_quantity_units(
7822 percent_commission_live_equity(mark) * std::abs(qty) / 100.0, true, mark, fx);
7823 }
7824 // The later of two opposite calls on one flat bar is one
7825 // transaction of its own quantity plus the earlier pending market
7826 // call's, and TradingView costs that transaction at the signal:
7827 // past the equity it drops the later call (lab tv
7828 // w6-f10g-pair-gross; the NQ probe on CME_MINI:NQ1! 15),
7829 // the commission left out (w6b-p1b/p1c-pair-gross-*). A market
7830 // call under pyramiding 2 is the batch's, whose exact pair is
7831 // admitted when it is finalized (p2_explicit_pair).
7832 if (current == 0.0 && explicit_fixed && source_point
7833 && same_point_pair_scope() && !(config_.pyramiding == 2 && !priced)
7834 && (config_.pyramiding <= 1 || same_point_two_leg_book(
7835 id, source_point->decision.script_bar_open_ms))) {
7836 const double pending = pending_opposite_market_units(
7837 is_long, source_point->decision.script_bar_open_ms, id);
7838 if (std::isfinite(pending)) units += pending;
7839 }
7840 // An explicit-quantity MARKET buy from flat, filled at the next
7841 // open, is costed at its signal close's tick plus the slippage
7842 // ticks its fill will carry; a sell at the close's tick, never
7843 // lowered by them (lab tv tailc-b-admission-a-slip2 against -b-fee
7844 // and -c-control; the placement and recalculation correction). Under process_orders_on_close
7845 // it fills at that close, whose own fill-time check decides it
7846 // (the TV controls of test_pooc_money_admission_l4b, phase-b/-c).
7847 const double admission_mark = !priced && current == 0.0 && is_long
7848 && explicit_fixed && config_.slippage > 0 && finite_positive(mark)
7849 && !config_.process_orders_on_close
7850 ? source_bar_fill_tick(mark, staged_.syminfo.mintick)
7851 + config_.slippage * staged_.syminfo.mintick
7852 : mark;
7853 const double required = units * admission_mark
7854 * staged_.syminfo.pointvalue * fx * margin / 100.0;
7855 // Finite explicit overflow is a signal-time refusal below, with
7856 // cancellation and callback continuation. Nonfinite inputs or
7857 // equity still fault before that refusal.
7858 const bool finite_explicit_requirement = std::isfinite(units)
7859 && std::isfinite(admission_mark) && std::isfinite(staged_.syminfo.pointvalue)
7860 && std::isfinite(fx);
7861 if (finite_positive(margin) && margin <= 100.0
7862 && ((!std::isfinite(required) && !finite_explicit_requirement)
7863 || !std::isfinite(equity))) {
7864 engine_command_value_invalid_or_count("margin.required", EngineValueStage::margin,
7865 EngineValueReason::nonfinite);
7866 }
7867 // Margin rule 2: an explicit quantity opening from flat at full
7868 // margin, market, or a stop under process_orders_on_close, is
7869 // judged at its signal close c' =
7870 // ticks x mintick -- on the money scale unslipped, E against
7871 // sig10(Q c'), and on the price scale with the order's slippage,
7872 // sig10(sig10(E) / Q) against c' moved by the slippage ticks
7873 // toward the fill. Dropped whole when either is short; nothing
7874 // else is checked at placement (tests/fixtures/margin_call_rules
7875 // admit, stop and stopband tapes: an F long of 2 at 27.359999
7876 // drops at 13.66 + 2 ticks, an ETH long of 1.5 admitted at
7877 // 2382.5849996 that the slipped money cost dropped, an EUR short
7878 // whose per-unit 1.18081 sits under the ulp-dirty c').
7879 const bool slipped_signal_scope = detail::margin_rule_switches().slipped_signal_admission
7880 && current == 0.0 && explicit_fixed && margin == 100.0
7881 && (!priced || (pure_stop_entry && config_.process_orders_on_close))
7882 && units == std::abs(normalized_qty) && finite_positive(units)
7883 && finite_positive(mark) && finite_positive(staged_.syminfo.mintick)
7884 && staged_.syminfo.pointvalue == 1.0 && fx == 1.0
7885 && staged_.account_fx_effective_from_ms.empty() && std::isfinite(equity);
7886 if (PF_RARE(slipped_signal_scope)) {
7887 const double tick = staged_.syminfo.mintick;
7888 const double ticks = std::floor(mark / tick + 0.5);
7889 const double signal_close = ticks * tick;
7890 const double slipped = (ticks + (is_long ? 1.0 : -1.0) * config_.slippage) * tick;
7891 const double mirrored = gain_loss_signal_equity(nearest_tick(mark, tick));
7892 const double signal_equity = std::isfinite(mirrored) ? mirrored : equity;
7893 const double per_unit = source_money_round(
7894 source_money_round(signal_equity) / units);
7895 if (!(signal_equity < source_money_round(units * signal_close))
7896 && !(per_unit < slipped)) {
7897 // admitted: no other placement check
7898 } else {
7899 if (pure_stop_entry) {
7900 if (const auto prior = live_by_source_key_.find(checked_live_key(id));
7901 prior != live_by_source_key_.end()) {
7902 const auto prior_handle = prior->second;
7903 const auto result = require_host().cancel(prior_handle);
7904 if (result.status == native_order::CancelStatus::Cancelled)
7905 retire(prior_handle);
7906 }
7907 }
7908 return;
7909 }
7910 }
7911 // R4-D L10ad: ab9714be pine_strategy_commands.cpp:344-346 gates the
7912 // whole placement affordability half on margin_pct > 0.0 ("margin_pct
7913 // == 0 disables the check, as it does in TradingView"). A strategy
7914 // declared with margin_long=0 / margin_short=0 therefore keeps taking
7915 // explicit-qty entries after its equity has gone negative; without the
7916 // gate required == 0 > equity rejected every later entry once the
7917 // account traded below zero (the NQ1 probe went flat after a later trade).
7918 if (!slipped_signal_scope && finite_positive(margin) && margin <= 100.0
7919 && (!std::isfinite(required) || !std::isfinite(equity)
7920 || required > equity)) {
7921 if (pure_stop_entry) {
7922 std::optional<native_order::RequestHandle> prior_handle;
7923 if (const auto prior = live_by_source_key_.find(checked_live_key(id));
7924 prior != live_by_source_key_.end()) {
7925 prior_handle = prior->second;
7926 }
7927 if (prior_handle) {
7928 const auto result = require_host().cancel(*prior_handle);
7929 if (result.status == native_order::CancelStatus::Cancelled)
7930 retire(*prior_handle);
7931 }
7932 }
7933 return;
7934 }
7935 }
7936 }
7937 // The admission's placement half (unified_placement), decided at the
7938 // signal itself: a market reversal at full margin -- a 100 % of equity
7939 // default or an explicit quantity -- whose price-scale budget rule 5
7940 // refuses, sig10(sig10(E) / Q) under (ticks(c') + side x slip) x mintick
7941 // while rule 2 passes, is refused whole there. It is still placed, marked
7942 // (PlacementSnapshot::signal_price_refused): the script's later close of
7943 // the held id then stays an ordinary close and executes
7944 // (tests/fixtures/margin_ledger_rules aapl-k0-*-after; admission_rules
7945 // cf11 x2), and the order drops at its fill (resolve_terms). Rule 2, rule
7946 // 5 for an entry from flat and the fill check are taken at the fill on the
7947 // same frozen signal values; a close-first entry escapes rule 5, and a
7948 // book with anything else resting or another opening pending is not
7949 // judged here.
7950 bool signal_price_refused = false;
7951 if (PF_RARE(current != 0.0 && ((current > 0.0) != is_long) && !close_first && !priced
7952 && source_point && detail::margin_rule_switches().unified_placement
7953 && (default_sized
7954 ? config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
7955 && config_.default_qty_value == 100.0
7956 : (qty_type < 0 || qty_type == static_cast<int>(QtyType::FIXED))
7957 && finite_positive(normalized_qty))
7958 && (is_long ? config_.margin_long : config_.margin_short) == 100.0
7959 && config_.commission_value == 0.0 && config_.pyramiding >= 0 && config_.pyramiding <= 1
7960 && staged_.syminfo.pointvalue == 1.0 && staged_.account_fx == 1.0
7961 && staged_.account_fx_effective_from_ms.empty() && oca_name.empty()
7962 && finite_positive(staged_.syminfo.mintick) && !cap.active()
7963 && !(risk_.max_intraday_loss > 0.0) && !(risk_.max_drawdown > 0.0)
7964 && risk_.max_cons_loss_days <= 0 && !(risk_.max_position_size > 0.0)
7965 && pinned_lot_grid() && !config_.calc_on_order_fills && !bar_magnifier_
7966 && unified_placement_book_clear())) {
7967 // The order's own quantity and money, as its snapshot below freezes them.
7968 auto sizing = sizing_snapshot();
7969 double units = std::abs(normalized_qty);
7970 if (default_sized) {
7971 units = kNaN;
7972 if (finite_positive(sizing.mark)) {
7973 sizing.price = default_market_sizing_price(sizing.mark, is_long);
7974#ifdef PINEFORGE_VALUE_INVALID_FIXTURES
7975 sizing.price = fixture_adapter_execution_value("slippage.default.signal", sizing.price);
7976#endif
7977 check_slippage_fill_price(sizing.price, std::isfinite(sizing.mark));
7978 mark_sizing_equity(sizing, sizing.mark);
7979 units = default_sizing_units(sizing);
7980 }
7981 }
7982 if (finite_positive(units) && std::isfinite(sizing.equity)
7983 && finite_positive(sizing.mark)) {
7984 const double tick = staged_.syminfo.mintick;
7985 const double ticks = std::floor(sizing.mark / tick + 0.5);
7986 const bool money_short = sizing.equity < source_money_round(units * (ticks * tick));
7987 const bool price_short = source_money_round(source_money_round(sizing.equity) / units)
7988 < (ticks + (is_long ? 1.0 : -1.0) * config_.slippage) * tick;
7989 signal_price_refused = !money_short && price_short;
7990 }
7991 }
7992 if (explicit_fixed && normalized_qty == 0.0 && current == 0.0 && !priced) {
7993 PlacementSnapshot shadow;
7994 shadow.family = PineOrderFamily::Entry;
7995 shadow.source_id = id;
7996 shadow.comment = comment;
7997 shadow.oca_name = oca_name;
7998 shadow.oca_type = oca_type;
7999 shadow.qty_type = qty_type;
8000 shadow.requested_qty = 0.0;
8001 shadow.is_long = is_long;
8002 shadow.opening = true;
8003 shadow.command_ordinal = ++command_ordinal_;
8004 shadow.command_sequence = ++source_command_sequence_;
8005 shadow.source_sequence = ++source_sequence_;
8006 shadow.projection_created_bar = source_point
8007 ? projection_bar_index(source_point->decision) : -1;
8008 shadow.sizing = sizing_snapshot();
8009 pending_write(source_shadow_pending_).push_back({std::move(shadow), id});
8010 return;
8011 }
8012 if (current == 0.0 && priced && explicit_fixed) {
8013 for (const auto& pending : pending_same_bar_commands_) {
8014 const auto& candidate = pending.snapshot;
8015 if (!pending.opening || candidate.family != PineOrderFamily::Entry
8016 || candidate.is_long == is_long
8017 || !candidate.frozen_market_instruction
8018 || !finite_positive(candidate.frozen_market_own_units)) {
8019 continue;
8020 }
8021 flat_pending_opposite_market_units += candidate.frozen_market_own_units;
8022 }
8023 if (flat_pending_opposite_market_units > 0.0
8024 && pending_same_bar_commands_.size() == 1U)
8025 flush_pending_same_bar_commands(/*flat_pair_follows=*/true);
8026 else if (pending_same_bar_commands_.size() != 1U)
8027 flat_pending_opposite_market_units = 0.0;
8028 }
8029 // A later source entry is outside a previously captured POOC global-exit
8030 // population. Keep the native close live, but stop advertising it as a
8031 // full-live dynamic reservation.
8032 if (has_live_exit_policy()) for (const auto& handle : live_handles_) {
8033 const auto existing = placement_.find(handle.incarnation);
8034 if (existing != placement_.end()
8035 && existing->second.pooc_global_full_exit_dynamic_qty) {
8036 existing->second.pooc_global_full_exit_dynamic_qty = false;
8037 existing->second.pooc_global_full_exit_tracks_bound_adds = false;
8038 }
8039 }
8040 const bool short_seed_final_candidate = current < 0.0 && !is_long
8041 && short_seed_long_candidate_.incarnation != 0;
8042 const bool opposite_opening_pending = opposite_entry_opening_pending(is_long);
8043 // Under process_orders_on_close a pending opposite opening defers the
8044 // pyramiding cap below to the close pass, where ab9714be counted the
8045 // entries then filled (add_to_pyramid_market's position_entry_count_) and
8046 // TradingView counts the trades then open. That pass fills the opposite
8047 // opening BEFORE this entry only when it processes it -- a market entry,
8048 // or one this script bar's own calculation placed -- and the add may be a
8049 // reversal by then. A priced opposite entry resting from an earlier bar is
8050 // not in that pass, so the add is held to the lots open now. (Deferred
8051 // without that bound, every fill's recalculation under the magnifier and
8052 // calc_on_order_fills re-issued such an add past pyramiding=1: 85 lots on
8053 // one bar where TradingView's tape books none -- the order-schedule correction.)
8054 const bool opposite_opening_in_close_pass = std::any_of(
8055 live_handles_.begin(), live_handles_.end(),
8056 [&](const native_order::RequestHandle& handle) {
8057 const auto existing = placement_.find(handle.incarnation);
8058 if (existing == placement_.end()) return false;
8059 const auto& row = existing->second;
8060 if (!row.opening || row.family != PineOrderFamily::Entry
8061 || row.is_long == is_long) {
8062 return false;
8063 }
8064 const bool market = !finite_positive(row.exit_levels.limit)
8065 && !finite_positive(row.exit_levels.stop);
8066 return market
8067 || (source_point
8068 && row.placement_script_open_ms == source_point->decision.script_bar_open_ms);
8069 });
8070 // ab9714be src/compat/pine/market_admission.cpp:33-37: the explicit
8071 // flat-pair scope (explicit_pair_scope) carries no commission term;
8072 // a commissioned pyramiding=2 pair keeps the gross transaction.
8073 const bool p2_flat_market_candidate = current == 0.0
8074 && config_.pyramiding == 2 && !config_.process_orders_on_close
8075 && !config_.calc_on_order_fills && !coof_recalc_active_
8076 && config_.default_qty_type == static_cast<int>(QtyType::FIXED)
8077 && config_.slippage == 0
8078 && risk_.direction == 0 && risk_.max_cons_loss_days == 0
8079 && risk_.max_drawdown <= 0.0 && risk_.max_intraday_loss <= 0.0
8080 && risk_.max_position_size <= 0.0 && !risk_.halted && !cap.active()
8081 && explicit_fixed && !priced && oca_name.empty();
8082 if (std::any_of(pending_same_bar_commands_.begin(), pending_same_bar_commands_.end(),
8083 [&](const PendingSameBarCommand& command) {
8084 return command.unbatched_request && command.replacement_key == id;
8085 })) {
8086 reroute_fixed_entries_before_request();
8087 }
8088 const bool fixed_market_batch_candidate = fixed_unmargined_market_batch_scope()
8089 && !source_batch_mutated_
8090 && std::none_of(live_handles_.begin(), live_handles_.end(), [&](const auto& handle) {
8091 const auto prior = placement_.find(handle.incarnation);
8092 return prior != placement_.end() && prior->second.opening;
8093 })
8094 && std::none_of(pending_entries_.begin(), pending_entries_.end(),
8095 [](const auto& pending) { return pending.snapshot.opening; })
8096 && std::none_of(delayed_market_orders_.begin(), delayed_market_orders_.end(),
8097 [](const auto& pending) { return pending.snapshot.opening; })
8098 && !close_precedes_entry && pending_same_bar_close_qty_ == 0.0
8099 && (!source_point || close_all_pending_script_bar_
8100 != source_point->decision.script_bar_open_ms);
8101 const bool same_bar_market_candidate = (same_bar_market_tx_scope()
8102 || p2_flat_market_candidate || fixed_market_batch_candidate)
8103 && !priced && oca_name.empty()
8104 && (qty_type < 0 || qty_type == static_cast<int>(QtyType::FIXED))
8105 && (default_sized || finite_positive(qty));
8106 if (!same_bar_market_candidate) reroute_fixed_entries_before_request();
8107 if (!same_bar_market_candidate && config_.pyramiding == 2
8108 && !pending_same_bar_commands_.empty()) {
8109 source_batch_mutated_ = true;
8110 flush_pending_same_bar_commands();
8111 }
8112 const bool all_in_percent = default_sized && !priced && oca_name.empty()
8113 && config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
8114 && config_.default_qty_value >= 100.0;
8115 // ab9714be pine_fills.cpp:2789-3020: an all-in default MARKET emitted
8116 // while its side is already at the Pine pyramiding cap remains a broker
8117 // book row until the next opening. A later opposite sibling can make
8118 // that row executable before its turn; otherwise the open-boundary
8119 // adapter retires it without exposing a generic fill.
8120 const bool default_gross_over_cap_candidate = all_in_percent
8121 && std::abs(config_.default_qty_value - 100.0) < 1e-12
8122 && config_.pyramiding == 1 && !config_.process_orders_on_close
8123 && !config_.calc_on_order_fills && config_.slippage == 0
8124 && config_.commission_value == 0.0
8125 && std::abs(config_.margin_long - 100.0) < 1e-12
8126 && std::abs(config_.margin_short - 100.0) < 1e-12
8127 && risk_.direction == 0 && risk_.max_cons_loss_days == 0
8128 && risk_.max_drawdown <= 0.0 && risk_.max_intraday_loss <= 0.0
8129 && risk_.max_position_size <= 0.0 && !risk_.halted && !cap.active();
8130 bool paired_all_in_reentry = false;
8131 if (all_in_percent && current != 0.0 && ((current > 0.0) == is_long) && source_point) {
8132 for (const auto& handle : live_handles_) {
8133 const auto prior = placement_.find(handle.incarnation);
8134 if (prior == placement_.end() || !prior->second.opening
8135 || prior->second.family != PineOrderFamily::Entry
8136 || prior->second.is_long == is_long) {
8137 continue;
8138 }
8139 paired_all_in_reentry = true;
8140 break;
8141 }
8142 }
8143 // Under process_orders_on_close a close the script issued earlier in the
8144 // same pass settles before this entry: an entry after closes that flatten
8145 // the position is not a same-side add, and a same-side entry's margin is
8146 // judged against the position that close flattens (lane W8A-SIGSTATE-1
8147 // R2, tests/fixtures/pooc_same_pass_close).
8148 const bool pooc_pass = config_.process_orders_on_close
8149 && !config_.calc_on_order_fills && !coof_recalc_active_;
8150 const bool pooc_pass_flattens = pooc_pass && close_precedes_entry && current != 0.0
8151 && preceding_close_qty + internal::kQtyEpsilon >= std::abs(current);
8152 if (!same_bar_market_candidate && current != 0.0
8153 && ((current > 0.0) == is_long) && config_.pyramiding == 0
8154 && !(priced && config_.process_orders_on_close) && !pooc_pass_flattens) {
8155 // A zero pyramiding setting permits the flat opening but makes a
8156 // same-direction MARKET reissue a source no-op. It must be dropped
8157 // before native matching so IntradayCap's factor-A policy observes no
8158 // fabricated physical fill.
8159 if (source_point) observe_intraday_cap_noop(is_long, source_point->decision);
8160 return;
8161 }
8162 if (!same_bar_market_candidate && config_.pyramiding > 0 && current != 0.0
8163 && ((current > 0.0) == is_long)
8164 && !(priced && config_.process_orders_on_close
8165 && !coof_recalc_active_)) {
8166 auto accepted_in_cycle = accepted_entries_in_cycle(is_long, id);
8167 // The pyramiding cap: the open close-ledger records and the same-side market
8168 // entries the bar placed before this one, in place of the cycle's
8169 // entry count (open_ledger_records).
8170 if (const auto records = open_ledger_records(); PF_RARE(records.has_value())) {
8171 std::size_t pending = 0;
8172 for (const auto& handle : live_handles_) {
8173 const auto placement = placement_.find(handle.incarnation);
8174 if (placement != placement_.end() && placement->second.opening
8175 && placement->second.source_id != id
8176 && placement->second.is_long == is_long
8177 && !pure_limit_entry(placement->second)) {
8178 ++pending;
8179 }
8180 }
8181 accepted_in_cycle = *records + pending;
8182 }
8183 // Pine's cap is a monotone entry-incarnation count for the current
8184 // position cycle; a partial close does not free a pyramiding slot.
8185 if (!(config_.process_orders_on_close && close_precedes_entry)
8186 && accepted_in_cycle >= static_cast<std::size_t>(config_.pyramiding)
8187 && !short_seed_final_candidate && !paired_all_in_reentry
8188 && !default_gross_over_cap_candidate
8189 && !(config_.process_orders_on_close && opposite_opening_pending
8190 && (opposite_opening_in_close_pass
8191 || detail::run_position(require_host()).lot_count
8192 < static_cast<std::size_t>(config_.pyramiding)))) {
8193 // Source replacement erases the older same-id priced entry before
8194 // judging the replacement's pyramiding admission. A rejected
8195 // over-cap reissue therefore leaves neither the old nor the new
8196 // trigger live (ab9714be:pine_strategy_commands.cpp:302-367).
8197 if (priced) {
8198 const auto prior = live_by_source_key_.find(checked_live_key(id));
8199 if (prior != live_by_source_key_.end()) {
8200 const auto handle = prior->second;
8201 const auto result = require_host().cancel(handle);
8202 if (result.status == native_order::CancelStatus::Cancelled)
8203 retire(handle);
8204 }
8205 }
8206 return;
8207 }
8208 }
8209 const auto current_point = source_point;
8210 const bool close_all_precedes = current_point
8211 && close_all_pending_script_bar_ == current_point->decision.script_bar_open_ms;
8212 native_order::RequestHandle preceding_close_all{};
8213 const bool reverses = current != 0.0 && ((current > 0.0) != is_long) && !close_all_precedes;
8214 if (default_sized
8215 && config_.default_qty_type == static_cast<int>(QtyType::FIXED)
8216 && reverses && !priced && config_.process_orders_on_close
8217 && !close_batch_callsites_.empty()) {
8218 // A later opposite MARKET entry owns the same source transaction.
8219 // The queued close remains a command-boundary observation only; the
8220 // native ReverseTo supplies the one physical close/open settlement.
8221 close_batch_callsites_.clear();
8222 close_batch_bar_ = -1;
8223 close_batch_queue_sequence_ = 0;
8224 close_batch_pending_debt_ = 0.0;
8225 close_batch_admitted_total_ = 0.0;
8226 pending_same_bar_close_qty_ = 0.0;
8227 close_precedes_entry = false;
8228 }
8229 // Default-sized reversal requests are HostSized already; they can carry a
8230 // fill-time close-only shape without changing explicit F7/F8 transaction
8231 // intent. Explicit affordability variants keep their established native
8232 // request shape unless a separately-qualified source family lowers them.
8233 const bool affordability_reversal_candidate = reverses && !priced
8234 && (default_sized
8235 ? (config_.default_qty_type == static_cast<int>(QtyType::FIXED)
8236 || config_.default_qty_type == static_cast<int>(QtyType::CASH)
8237 || (config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
8238 && config_.default_qty_value > 100.0))
8239 // A per-call explicit unit quantity still resolves at the terms
8240 // boundary when it reverses: the source affordability check may
8241 // retain only the closing leg regardless of the configured
8242 // default quantity type (ab9714be pine_fills.cpp:5529-5660).
8243 : true);
8244 const bool direction_blocked = (risk_.direction > 0 && !is_long)
8245 || (risk_.direction < 0 && is_long);
8246 if (default_sized && reverses && current_point) {
8247 for (const auto& handle : live_handles_) {
8248 const auto pending = placement_.find(handle.incarnation);
8249 if (pending == placement_.end()) continue;
8250 const auto& prior = pending->second;
8251 if (prior.family == PineOrderFamily::Entry && prior.opening
8252 && !prior.is_long && prior.is_long == is_long && prior.replaced_opening
8253 && prior.replacement_predecessor_market
8254 && prior.placement_script_open_ms == current_point->decision.script_bar_open_ms) {
8255 // The replacement's transaction owns this source pass; a
8256 // later same-side default market command remains unfilled.
8257 return;
8258 }
8259 }
8260 }
8261 if (default_sized && reverses && is_long && current_point) {
8262 std::vector<native_order::RequestHandle> superseded_buy_replacements;
8263 for (const auto& handle : live_handles_) {
8264 const auto pending = placement_.find(handle.incarnation);
8265 if (pending == placement_.end()) continue;
8266 const auto& prior = pending->second;
8267 if (prior.family == PineOrderFamily::Entry && prior.opening && prior.is_long
8268 && prior.replaced_opening && prior.replacement_predecessor_market
8269 && prior.placement_script_open_ms == current_point->decision.script_bar_open_ms) {
8270 superseded_buy_replacements.push_back(handle);
8271 }
8272 }
8273 for (const auto& handle : superseded_buy_replacements) {
8274 cancel_bracket_origin(handle);
8275 const auto result = require_host().cancel(handle);
8276 if (result.status == native_order::CancelStatus::Cancelled) retire(handle);
8277 }
8278 }
8279 const bool cash_sized = qty_type == static_cast<int>(QtyType::CASH);
8280 const bool percent_sized = qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY);
8281 const bool typed_sized = !default_sized && (cash_sized || percent_sized);
8282 const bool fixed_priced_reverse = reverses && !default_sized && priced && !cash_sized;
8283 const bool cash_priced_reverse = reverses && !default_sized && priced && cash_sized;
8284 // A default-percent stop entry freezes its units at placement whatever
8285 // the percentage (see the freeze below); ab9714be's own placement
8286 // admission for it (pine_strategy_commands.cpp:307-310) is the one the
8287 // percentage caps at 100, and above 100 the affordability arm admits it.
8288 const bool default_stop_freeze = default_sized && pure_stop_entry
8289 && finite_positive(stop_price)
8290 && config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY);
8291 const bool default_stop_scope = default_stop_freeze
8292 && config_.default_qty_value <= 100.0;
8293 double default_stop_sizing_price = kNaN;
8294 if (default_stop_freeze && finite_positive(staged_.syminfo.mintick)) {
8295 stop_price = directional_tick(
8296 stop_price, staged_.syminfo.mintick, is_long, value_field::entry_stop_price);
8297 const double signal = source_point
8298 ? nearest_tick(source_point->price, staged_.syminfo.mintick) : kNaN;
8299 if (config_.process_orders_on_close && source_level_on_price_grid(stop_price, staged_.syminfo.mintick) == source_level_on_price_grid(signal, staged_.syminfo.mintick)) {
8300 stop_price = source_level_on_price_grid(
8301 stop_price, staged_.syminfo.mintick);
8302 }
8303 const bool marketable = finite_positive(signal)
8304 && (is_long ? stop_price <= signal : stop_price >= signal);
8305 // ab9714be pine_strategy_commands.cpp:325-328: pure stop placement sizing price includes directional slippage ticks
8306 const double slip = (is_long ? 1.0 : -1.0) * config_.slippage * staged_.syminfo.mintick;
8307 default_stop_sizing_price = (marketable ? signal : stop_price) + slip;
8308#ifdef PINEFORGE_VALUE_INVALID_FIXTURES
8309 default_stop_sizing_price = fixture_adapter_execution_value(
8310 "slippage.default.stop", default_stop_sizing_price);
8311#endif
8312 check_slippage_fill_price(default_stop_sizing_price,
8313 std::isfinite(marketable ? signal : stop_price));
8314 }
8315 if (default_sized || typed_sized || direction_blocked || affordability_reversal_candidate) {
8316 request.intent = native_order::HostSized{native_order::HostSizedKind::Open,
8317 is_long ? native_order::Side::Long : native_order::Side::Short};
8318 } else if (fixed_priced_reverse || cash_priced_reverse) {
8319 request.intent = native_order::HostSized{native_order::HostSizedKind::Open,
8320 is_long ? native_order::Side::Long : native_order::Side::Short};
8321 } else if (reverses) {
8322 request.intent = native_order::ReverseTo{signed_target};
8323 } else {
8324 request.intent = native_order::Transact{signed_target};
8325 }
8326 if (flat_pending_opposite_market_units > 0.0) {
8327 const double transaction = normalized_qty + flat_pending_opposite_market_units;
8328 request.intent = native_order::Transact{is_long ? transaction : -transaction};
8329 }
8330 // Outside the zero-cost batch the later of two opposite flat market
8331 // calls is still one transaction of its own quantity plus the earlier
8332 // pending market call's (lab tv w6-f10j-pair-commission-per-order MM-LF).
8333 if (!priced && !same_bar_market_candidate && current == 0.0 && explicit_fixed
8334 && finite_positive(qty) && oca_name.empty() && source_point
8335 && same_point_pair_scope()
8336 && (config_.pyramiding <= 1
8337 || same_point_two_leg_book(id, source_point->decision.script_bar_open_ms))) {
8338 const double pending = pending_opposite_market_units(
8339 is_long, source_point->decision.script_bar_open_ms, id);
8340 if (finite_positive(pending)) {
8341 const double transaction = normalized_qty + pending;
8342 request.intent = native_order::Transact{is_long ? transaction : -transaction};
8343 }
8344 }
8345 request.label = id; request.comment = comment;
8346 const auto coof_native_state = detail::run_state(require_host());
8347 const bool coof_lower_path = coof_native_state.spec
8348 && coof_native_state.spec->intrabar.lower();
8349 // A matcher fill AT the bar's second extreme: the recalculation's market
8350 // entry fills there, at that extreme. ab9714be advanced every endpoint
8351 // fill to the following waypoint, the close, which admits no cascade
8352 // order, and filled the entry at the next open; TradingView books it on
8353 // the fill's own bar at the extreme (hm-chart-diff-v2 / -v6 bar 19: 11.72
8354 // / 11.74 at the high, and under process_orders_on_close -v3 / -v7 alike
8355 // -- the order-schedule correction). A fill the adapter forced there still advances
8356 // (coof_fill_at_second_extreme; the recalculation fill-order correction).
8357 bool coof_market_next_open = false;
8358 bool coof_market_at_second_extreme = false;
8359 double coof_second_extreme = kNaN;
8360 if (coof_recalc_active_ && !coof_first_open_ && !coof_lower_path && !priced
8361 && coof_fill_at_second_extreme()) {
8362 coof_second_extreme = source_path_uses_high_first(coof_script_bar_)
8363 ? coof_script_bar_.low : coof_script_bar_.high;
8364 coof_market_next_open = coof_fill_forced_;
8365 coof_market_at_second_extreme = !coof_fill_forced_;
8366 }
8367 const bool coof_market_next_sub_bar = coof_recalc_active_ && !coof_first_open_
8368 && coof_lower_path && !priced && coof_waits_for_next_sub_bar();
8369 double native_limit = limit_price;
8370 double native_stop = stop_price;
8371 if (finite_positive(limit_price) && !finite_positive(stop_price)) {
8372 // ab9714be pine_policy_members.cpp:11-17 + engine.hpp:1374-1381: every
8373 // entry limit, explicit-qty included, is tested against the
8374 // tick-quantized bar, so an off-grid level triggers on the half-up
8375 // rounded print (synthetic NYSE:F limits 25.045 / 26.045 / 27.00).
8376 native_limit = source_trigger_threshold(
8377 limit_price, staged_.syminfo.mintick, is_long, true, value_field::entry_limit_price);
8378 } else if (finite_positive(stop_price) && !finite_positive(limit_price)
8379 && !config_.calc_on_order_fills) {
8380 native_stop = source_trigger_threshold(
8381 stop_price, staged_.syminfo.mintick, is_long, false, value_field::entry_stop_price);
8382 }
8383 const auto* pine_host = pine_view_of(&require_host());
8384 const bool nonpositive_priced = priced && pine_host
8385 && pine_host->scheduler_uses_aux_security_feed()
8386 && !finite_positive(native_limit) && !finite_positive(native_stop);
8387 request.trigger = nonpositive_priced
8388 ? native_order::Trigger{native_order::Limit{
8389 is_long ? std::numeric_limits<double>::min()
8390 : std::numeric_limits<double>::max()}}
8391 : trigger_for(native_limit, native_stop);
8392 double coof_market_fill = kNaN;
8393 double coof_market_target = kNaN;
8394 if (coof_recalc_active_ && !coof_first_open_ && !coof_market_next_open
8395 && !coof_market_next_sub_bar && coof_script_bar_valid_
8396 && std::holds_alternative<native_order::Market>(request.trigger)
8397 && (coof_market_at_second_extreme || !defer_coof_tail())) {
8398 // ab9714be pine_scheduler.cpp:398-619: a MARKET request born by a
8399 // non-first-open fill recalc waits for the next unconsumed waypoint.
8400 // A mid-segment fill retains that segment's endpoint; an endpoint
8401 // fill advances to the following waypoint -- but the second
8402 // extreme's own (above).
8403 int next_extreme_index = -1;
8404 const double next_extreme = coof_market_at_second_extreme
8405 ? coof_second_extreme : coof_next_waypoint(&next_extreme_index);
8406 if (coof_market_at_second_extreme) next_extreme_index = 2;
8407 coof_market_target = next_extreme;
8408 const auto point = detail::callback_point(require_host());
8409 const double current_quote = point ? point->price : kNaN;
8410 coof_market_fill = source_bar_fill_tick(
8411 next_extreme, staged_.syminfo.mintick)
8412 + (is_long ? 1.0 : -1.0) * config_.slippage
8413 * staged_.syminfo.mintick;
8414#ifdef PINEFORGE_VALUE_INVALID_FIXTURES
8415 coof_market_fill = fixture_adapter_execution_value("slippage.recalc.entry", coof_market_fill);
8416#endif
8417 check_slippage_fill_price(coof_market_fill, std::isfinite(next_extreme));
8418 if (finite_positive(coof_market_fill) && finite_positive(current_quote)
8419 && !source_same_point(current_quote, coof_market_fill, staged_.syminfo.mintick)) {
8420 const bool falling = coof_market_fill < current_quote;
8421 // An extreme (W1/W2) target arms at the raw waypoint, so the fill
8422 // lands AT that point (pine_scheduler.cpp:548-563) even when its
8423 // booked tick lies beyond an off-grid extreme or short of it (L
8424 // 11.995 booked 12.00 would otherwise fill mid-leg).
8425 // The booked tick may lie on the wrong side of that raw level,
8426 // so the limit is a touch trigger (fill-through).
8427 const bool extreme_target = next_extreme_index == 1
8428 || next_extreme_index == 2;
8429 const native_order::Limit limit = extreme_target
8430 ? native_order::Limit{next_extreme, coof_market_fill != next_extreme}
8431 : native_order::Limit{coof_market_fill};
8432 if (is_long) {
8433 request.trigger = falling
8435 : native_order::Trigger{native_order::Stop{next_extreme}};
8436 } else {
8437 request.trigger = falling
8438 ? native_order::Trigger{native_order::Stop{next_extreme}}
8440 }
8441 }
8442 // ab9714be pine_fills.cpp:7962-7966 + engine.hpp:1207-1210: the
8443 // waypoint POINT fill books bar_fill_price(waypoint); the trigger
8444 // above keeps the reachable grid level.
8445 coof_market_fill = nearest_tick(next_extreme, staged_.syminfo.mintick)
8446 + (is_long ? 1.0 : -1.0) * config_.slippage
8447 * staged_.syminfo.mintick;
8448#ifdef PINEFORGE_VALUE_INVALID_FIXTURES
8449 coof_market_fill = fixture_adapter_execution_value("slippage.recalc.entry_tick", coof_market_fill);
8450#endif
8451 check_slippage_fill_price(coof_market_fill, std::isfinite(next_extreme));
8452 }
8453 if (pure_stop_entry && explicit_fixed && current != 0.0
8454 && ((current > 0.0) != is_long) && source_point
8455 && !config_.process_orders_on_close && !config_.calc_on_order_fills) {
8456 auto* pine_host = pine_view_of(&require_host());
8457 const auto next = pine_host
8458 ? pine_host->scheduler_.next_source_bar(
8459 source_point->decision.coordinate.interval_index)
8460 : std::optional<Bar>{};
8461 if (next) {
8462 for (const auto& handle : live_handles_) {
8463 const auto found = placement_.find(handle.incarnation);
8464 if (found == placement_.end()) continue;
8465 const auto& prior = found->second;
8466 const bool current_exit_limit =
8467 prior.family == PineOrderFamily::ExitLimit
8468 && prior.projection_created_bar
8469 == source_point->decision.coordinate.interval_index
8470 && finite_positive(prior.exit_levels.limit);
8471 const bool both_reached = current > 0.0
8472 ? next->high >= prior.exit_levels.limit && next->low <= stop_price
8473 : next->low <= prior.exit_levels.limit && next->high >= stop_price;
8474 if (!current_exit_limit || !both_reached) continue;
8475 request.intent = native_order::Transact{signed_target};
8476 request.owner = native_order::WaitForApplied{handle};
8477 break;
8478 }
8479 }
8480 }
8481 request.group = group_for(oca_name, oca_type);
8482 if (close_all_precedes && !priced && current_point
8483 && !config_.process_orders_on_close) {
8484 for (auto it = live_handles_.rbegin(); it != live_handles_.rend(); ++it) {
8485 const auto close = placement_.find(it->incarnation);
8486 if (close == placement_.end()
8487 || close->second.family != PineOrderFamily::CloseAll
8488 || close->second.placement_script_open_ms
8489 != current_point->decision.script_bar_open_ms) {
8490 continue;
8491 }
8492 preceding_close_all = *it;
8493 break;
8494 }
8495 }
8496 const bool sequenced_close_entry = pure_stop_entry
8497 || (!priced && (!staged_.quantity_grid || *staged_.quantity_grid < 1.0));
8498 if (preceding_close_all.incarnation == 0 && sequenced_close_entry
8499 && current != 0.0 && ((current > 0.0) != is_long))
8500 preceding_close_all = preceding_close_request;
8503 snapshot.source_id = id;
8504 snapshot.comment = comment;
8505 snapshot.oca_name = oca_name;
8506 snapshot.oca_type = oca_type; snapshot.qty_type = qty_type;
8507 snapshot.requested_qty = normalized_qty; snapshot.is_long = is_long;
8508 snapshot.opening = true;
8509 snapshot.paired_reversal_parent = preceding_close_all;
8510 snapshot.command_ordinal = ++command_ordinal_;
8511 snapshot.direction_gate = direction_blocked;
8512 snapshot.deferred_cohort = default_sized;
8513 if (reverses && source_point) {
8514 for (const auto& handle : live_handles_) {
8515 const auto prior = placement_.find(handle.incarnation);
8516 if (prior != placement_.end()
8517 && prior->second.family == PineOrderFamily::Close
8518 && !prior->second.from_entry.empty()
8519 && prior->second.projection_created_bar
8520 == source_point->decision.coordinate.interval_index) {
8521 snapshot.projection_after_close = true;
8522 break;
8523 }
8524 }
8525 }
8526 if (flat_pending_opposite_market_units > 0.0) {
8527 snapshot.paired_flat_market_candidate = true;
8528 snapshot.paired_flat_market_own_qty = normalized_qty;
8529 snapshot.paired_flat_market_transaction_qty =
8530 normalized_qty + flat_pending_opposite_market_units;
8531 }
8532 if (source_command_sequence_ == std::numeric_limits<std::uint64_t>::max()) {
8533 throw coded<std::overflow_error>(RunFailureCode::engine_invariant, {},
8534 "Pine source command sequence exhausted");
8535 }
8536 snapshot.command_sequence = ++source_command_sequence_;
8537 // Reuse the durable level tuple for the parent trigger facts. A deferred
8538 // relative exit may safely arm from a non-gap LIMIT parent's known entry
8539 // level before that parent is applied.
8540 snapshot.exit_levels.limit = limit_price;
8541 snapshot.exit_levels.stop = stop_price;
8542 snapshot.birth = capture_order_birth();
8544 snapshot.birth, false);
8545 snapshot.reverse_to = reverses || paired_all_in_reentry;
8546 snapshot.projection_after_close = close_precedes_entry;
8547 snapshot.close_first_entry = close_first_fact;
8548 snapshot.signal_price_refused = signal_price_refused;
8549 snapshot.sizing = sizing_snapshot();
8550 if (finite_positive(coof_market_fill)) {
8551 snapshot.forced_execution_price = coof_market_fill;
8552 snapshot.forced_waypoint_print = coof_market_target;
8553 }
8554 bool coof_priced_next_open = false;
8555 if (coof_recalc_active_ && !coof_first_open_ && !coof_lower_path && priced) {
8556 const auto point = detail::callback_point(require_host());
8557 const double birth = point ? point->price : kNaN;
8558 int waypoint_index = -1;
8559 const double waypoint = coof_next_waypoint(&waypoint_index);
8560 bool reached = false;
8561 bool limit_route = false;
8562 if (finite_positive(stop_price)) {
8563 reached = is_long ? (waypoint >= stop_price && birth < stop_price)
8564 : (waypoint <= stop_price && birth > stop_price);
8565 } else if (finite_positive(limit_price)) {
8566 limit_route = true;
8567 reached = is_long ? (waypoint <= limit_price && birth > limit_price)
8568 : (waypoint >= limit_price && birth < limit_price);
8569 }
8570 if (reached) {
8571 const double slipped = waypoint + (limit_route ? 0.0
8572 : (is_long ? 1.0 : -1.0) * config_.slippage
8573 * staged_.syminfo.mintick);
8574 snapshot.forced_execution_price = nearest_tick(
8575 slipped, staged_.syminfo.mintick);
8576 }
8577 // A pure limit or stop entry that the recalculation of a MID-LEG fill
8578 // places already executable at that fill's price is live only from
8579 // the leg's end, the next extreme: when still executable there it
8580 // fills at the extreme's print (a stop keeps its slippage).
8581 // TradingView, lab tv tapes tests/fixtures/coof_refill_waypoint
8582 // (w8d-coof2-a/b/d/e/f; resting-limit control -c): the level fill
8583 // this path booked at the placement point was never executable
8584 // after it (the entry-eligibility correction). An entry not executable at the
8585 // extreme keeps its level fill: TradingView rests it there (-d), but
8586 // a magnified script's next point is an intrabar one, and resting
8587 // it to the chart bar's extreme delayed the unmagnified
8588 // EMA200 probe's refills by a bar or more. Not pinned under
8589 // process_orders_on_close, on the close leg or on a leg-end fill --
8590 // but a fill the adapter FORCED onto the first extreme starts the
8591 // next leg the same way: its entry fills at the second extreme when
8592 // executable there (lab tv tailc-w2r-c/d, 98 rows; the placement and recalculation correction).
8593 const bool pure_limit = finite_positive(limit_price) && !finite_positive(stop_price);
8594 const bool pure_stop = finite_positive(stop_price) && !finite_positive(limit_price);
8595 const double level = pure_limit ? limit_price : stop_price;
8596 const auto executable_at = [&](double price) {
8597 return pure_limit ? (is_long ? price <= level : price >= level)
8598 : (is_long ? price >= level : price <= level);
8599 };
8600 const bool forced_first_extreme = coof_fill_forced_ && waypoint_index == 2
8601 && !coof_fill_at_second_extreme();
8602 if (!reached && (pure_limit || pure_stop) && !config_.process_orders_on_close
8603 && (waypoint_index == 1 || waypoint_index == 2) && finite_positive(waypoint)
8604 && finite_positive(birth)
8605 && (!coof_fill_on_path_point() || forced_first_extreme)
8606 && executable_at(birth) && executable_at(waypoint)) {
8607 const double tick = staged_.syminfo.mintick;
8608 const double booked = source_bar_fill_tick(waypoint, tick);
8609 const bool falling = waypoint < birth;
8610 request.trigger = is_long == falling
8611 ? native_order::Trigger{native_order::Limit{waypoint, booked != waypoint}}
8612 : native_order::Trigger{native_order::Stop{waypoint}};
8613 snapshot.forced_execution_price = nearest_tick(waypoint
8614 + (pure_limit ? 0.0 : (is_long ? 1.0 : -1.0) * config_.slippage * tick),
8615 tick);
8616 } else if (!reached && (pure_limit || pure_stop) && !config_.process_orders_on_close
8617 && (waypoint_index == 1 || waypoint_index == 2) && finite_positive(waypoint)
8618 && finite_positive(birth)
8619 && (!coof_fill_on_path_point() || forced_first_extreme)
8620 && executable_at(birth) && !executable_at(waypoint)) {
8621 // An entry executable at its birth but not at the extreme rests at
8622 // its own level from the extreme on (w8d-coof2-d/e/f, and after a
8623 // forced first-extreme fill tailc-w2r-a/b: every refill filled
8624 // after the extreme or later, none on the leg before it; the
8625 // placement and recalculation correction). A limit arms at the extreme: StopLimit{extreme, level}.
8626 // A stop becomes a trail armed at the extreme, riding its level's
8627 // distance behind the running best: no print beyond the extreme
8628 // follows on this bar, so it is the stop at its own level, booked
8629 // there; on_bar_open hands the next bar the plain stop back.
8630 if (pure_limit) {
8631 request.trigger = native_order::StopLimit{waypoint, limit_price};
8632 } else {
8633 request.trigger = native_order::Trail{std::abs(stop_price - waypoint), waypoint};
8634 snapshot.forced_execution_price = nearest_tick(stop_price
8635 + (is_long ? 1.0 : -1.0) * config_.slippage * staged_.syminfo.mintick,
8636 staged_.syminfo.mintick);
8637 }
8638 }
8639 // A pure limit or stop entry placed by the recalculation of a fill
8640 // the adapter FORCED onto the bar's second extreme is live only from
8641 // the next bar's open, as that recalculation's market orders are
8642 // (coof_market_next_open): TradingView never fills it on the rest of
8643 // the bar -- at that open when it is executable there, otherwise at
8644 // its own level later -- executable at the forced fill or not, and
8645 // even when the leg to the close crosses its level (lab tv tapes
8646 // tests/fixtures/coof_w2_refill, tailc-w2r-a..f: 1013 refills after
8647 // such a fill, none on its bar; the placement and recalculation correction). Released at the
8648 // recalculation, it would also trade on the part of the leg the
8649 // kernel walks after the forced fill's trigger, which TradingView has
8650 // already passed.
8651 coof_priced_next_open = (pure_limit || pure_stop)
8652 && !config_.process_orders_on_close && coof_fill_forced_
8653 && coof_fill_at_second_extreme();
8654 }
8655 // On a magnified path the recalculation's next fill point is the next
8656 // point of TradingView's own intrabar path (next_input_waypoint), or the
8657 // open itself for the refill of a first open fill. A pure limit or stop
8658 // entry the recalculation places is live only from that point: executable
8659 // there, it fills at the point's print (a stop keeps its slippage); a
8660 // limit that is not rests at its own level from the point on. A stop that
8661 // is not keeps its level: a later intrabar can pass the point, so no
8662 // trigger here rests it (lab tv tailc-mag-coof-refill-leg; the
8663 // placement and recalculation correction, the magnified twin of w8d-coof2-*).
8664 if (coof_recalc_active_ && coof_lower_path && priced && !config_.process_orders_on_close) {
8665 const bool lower_limit = finite_positive(limit_price) && !finite_positive(stop_price);
8666 const bool lower_stop = finite_positive(stop_price) && !finite_positive(limit_price);
8667 const auto lower_point = detail::callback_point(require_host());
8668 const double lower_birth = lower_point ? lower_point->price : kNaN;
8669 std::optional<PineScheduler::InputWaypoint> lower_next;
8670 if (coof_first_open_) {
8671 if (finite_positive(lower_birth))
8672 lower_next = PineScheduler::InputWaypoint{lower_birth, false};
8673 } else if (const auto* lower_host = pine_view_of(&require_host())) {
8674 lower_next = lower_host->scheduler_.next_input_waypoint(*lower_host, coof_context_);
8675 }
8676 // A fill on the chart bar's last intrabar-path point leaves the next
8677 // bar's opening as the next fill point: the entry is live from that
8678 // open, as off the magnified path (tailc-mag-coof-refill-leg; synthetic buy limit
8679 // placed at a 25.01 close print is live at the next bar's open
8680 // and fills at that open's better 25.00 print).
8681 if ((lower_limit || lower_stop) && lower_next && lower_next->next_open
8682 && coof_context_.is_terminal_sub_bar) {
8683 coof_priced_next_open = true;
8684 } else if ((lower_limit || lower_stop) && lower_next
8685 && finite_positive(lower_next->price) && finite_positive(lower_birth)) {
8686 const double level = lower_limit ? limit_price : stop_price;
8687 const double waypoint = lower_next->price;
8688 const double tick = staged_.syminfo.mintick;
8689 const auto executable_at = [&](double price) {
8690 return lower_limit ? (is_long ? price <= level : price >= level)
8691 : (is_long ? price >= level : price <= level);
8692 };
8693 const bool same_point = coof_first_open_ || lower_next->next_open
8694 || source_same_point(lower_birth, waypoint, tick);
8695 if (executable_at(waypoint)) {
8696 if (same_point) {
8697 request.trigger = native_order::Market{};
8698 } else {
8699 const double booked = source_bar_fill_tick(waypoint, tick);
8700 const bool falling = waypoint < lower_birth;
8701 request.trigger = is_long == falling
8702 ? native_order::Trigger{native_order::Limit{waypoint, booked != waypoint}}
8703 : native_order::Trigger{native_order::Stop{waypoint}};
8704 }
8705 snapshot.forced_execution_price = nearest_tick(waypoint
8706 + (lower_limit ? 0.0 : (is_long ? 1.0 : -1.0) * config_.slippage * tick),
8707 tick);
8708 } else if (lower_limit && !same_point && (waypoint > lower_birth) == is_long) {
8709 request.trigger = native_order::StopLimit{waypoint, level};
8710 }
8711 }
8712 }
8713 if (current == 0.0 && priced && current_point) {
8714 const auto is_opposite_market_predecessor = [&](const PlacementSnapshot& prior) {
8715 return prior.opening && prior.family == PineOrderFamily::Entry
8716 && prior.is_long != is_long
8717 && prior.placement_script_open_ms
8718 == current_point->decision.script_bar_open_ms
8719 && !finite_positive(prior.exit_levels.limit)
8720 && !finite_positive(prior.exit_levels.stop)
8721 && !finite_positive(prior.exit_levels.trail_points)
8722 && !finite_positive(prior.exit_levels.trail_price)
8723 && !finite_positive(prior.exit_levels.trail_offset);
8724 };
8725 snapshot.projection_opposite_market_predecessor = std::any_of(
8726 pending_same_bar_commands_.begin(), pending_same_bar_commands_.end(),
8727 [&](const PendingSameBarCommand& prior) {
8728 return is_opposite_market_predecessor(prior.snapshot);
8729 }) || std::any_of(pending_entries_.begin(), pending_entries_.end(),
8730 [&](const PendingEntry& prior) {
8731 return is_opposite_market_predecessor(prior.snapshot);
8732 }) || std::any_of(live_handles_.begin(), live_handles_.end(),
8733 [&](const native_order::RequestHandle& handle) {
8734 const auto prior = placement_.find(handle.incarnation);
8735 return prior != placement_.end()
8736 && is_opposite_market_predecessor(prior->second);
8737 });
8738 // A marketable buy stop outranks the sell market it follows at their
8739 // shared fill point (order_same_point_entries): TradingView fills it
8740 // first, as one buy of its own quantity plus the market's, and the
8741 // market then sells only its own (lab tv w6-f10a-open-pair MS-SF).
8742 const bool fills_ahead = snapshot.projection_opposite_market_predecessor
8743 && same_point_pair_scope() && explicit_fixed && is_long
8744 && pure_stop_entry && finite_positive(current_point->price)
8745 && stop_price <= current_point->price
8746 && (config_.pyramiding <= 1 || same_point_two_leg_book(
8747 id, current_point->decision.script_bar_open_ms));
8748 const double predecessor_units = fills_ahead
8749 ? pending_opposite_market_units(
8750 is_long, current_point->decision.script_bar_open_ms, id)
8751 : kNaN;
8752 if (fills_ahead && finite_positive(predecessor_units)) {
8753 request.intent = native_order::Transact{normalized_qty + predecessor_units};
8754 } else if (snapshot.projection_opposite_market_predecessor) {
8755 // ab9714be pine_pending_intent.hpp:507-537: placement_has_opposite_market_predecessor reverses against same-bar predecessor
8756 request.intent = native_order::HostSized{native_order::HostSizedKind::Open,
8757 is_long ? native_order::Side::Long : native_order::Side::Short};
8758 snapshot.terms_priced_reverse = true;
8759 }
8760 }
8761 if (default_sized && !priced && finite_positive(snapshot.sizing.mark)) {
8762 snapshot.sizing.price = default_market_sizing_price(snapshot.sizing.mark, is_long);
8763#ifdef PINEFORGE_VALUE_INVALID_FIXTURES
8764 snapshot.sizing.price = fixture_adapter_execution_value("slippage.default.snapshot", snapshot.sizing.price);
8765#endif
8766 check_slippage_fill_price(snapshot.sizing.price, std::isfinite(snapshot.sizing.mark));
8767 mark_sizing_equity(snapshot.sizing, snapshot.sizing.mark);
8768 }
8769 const auto predecessor = live_by_source_key_.find(checked_live_key(id));
8770 snapshot.replaced_opening = predecessor != live_by_source_key_.end();
8771 if (snapshot.replaced_opening) {
8772 const auto prior = placement_.find(predecessor->second.incarnation);
8773 snapshot.replacement_predecessor_market = prior != placement_.end()
8774 && !finite_positive(prior->second.exit_levels.limit)
8775 && !finite_positive(prior->second.exit_levels.stop);
8776 }
8777 // R1: a same-id MARKET reissue of a reversal whose predecessor is still
8778 // staged in this callback's same-bar batch is the same replacement; the
8779 // staged predecessor never reached live_by_source_key_. TradingView books
8780 // the reissue as a transaction of its own quantity against the opposite
8781 // position, not as a reversal, on either side and for every quantity kind
8782 // (lab tv tapes tests/fixtures/same_bar_reissue, lane R1-CONSOLIDATE).
8783 bool staged_reversal_replacement = false;
8784 if (same_bar_market_candidate && !snapshot.replaced_opening && reverses) {
8785 staged_reversal_replacement = std::any_of(pending_same_bar_commands_.begin(),
8786 pending_same_bar_commands_.end(), [&](const PendingSameBarCommand& row) {
8787 return row.opening && !row.snapshot.frozen_market_targeted_close
8788 && row.replacement_key == id
8789 && row.snapshot.family == PineOrderFamily::Entry
8790 && row.snapshot.is_long == is_long;
8791 });
8792 snapshot.replaced_opening = staged_reversal_replacement;
8793 snapshot.replacement_predecessor_market = staged_reversal_replacement;
8794 }
8795 const bool special_sell_replacement = default_sized && reverses && !is_long
8796 && snapshot.replaced_opening && snapshot.replacement_predecessor_market;
8797 if (special_sell_replacement && current_point) {
8798 std::vector<native_order::RequestHandle> superseded_siblings;
8799 for (const auto& handle : live_handles_) {
8800 const auto pending = placement_.find(handle.incarnation);
8801 if (pending == placement_.end()) continue;
8802 const auto& prior = pending->second;
8803 if (prior.family == PineOrderFamily::Entry && prior.opening
8804 && prior.source_id != id && prior.is_long == is_long
8805 && prior.placement_script_open_ms == current_point->decision.script_bar_open_ms) {
8806 superseded_siblings.push_back(handle);
8807 }
8808 }
8809 for (const auto& handle : superseded_siblings) {
8810 cancel_bracket_origin(handle);
8811 const auto result = require_host().cancel(handle);
8812 if (result.status == native_order::CancelStatus::Cancelled) retire(handle);
8813 }
8814 std::vector<native_order::RequestHandle> carried_brackets;
8815 std::vector<SourceId> carried_ids;
8816 using CohortRow = std::remove_reference_t<decltype(*cohorts_by_id_.begin())>;
8817 std::vector<const CohortRow*> cohorts;
8818 cohorts.reserve(cohorts_by_id_.held_size());
8819 for (const auto& row : cohorts_by_id_.held_rows()) cohorts.push_back(&row);
8820 // The cancellation order below was lexical; retain it. Collecting
8821 // held rows costs O(H), and this existing sort still costs O(H log H).
8822 std::sort(cohorts.begin(), cohorts.end(),
8823 [](const auto* a, const auto* b) { return a->first < b->first; });
8824 for (const auto* cohort : cohorts) {
8825 for (const auto& origin : cohort->second.opened) {
8826 const auto prior = placement_.find(origin.incarnation);
8827 if (prior != placement_.end() && prior->second.is_long != is_long) {
8828 carried_brackets.push_back(origin);
8829 carried_ids.push_back(cohort->first);
8830 }
8831 }
8832 }
8833 for (const auto& origin : carried_brackets) cancel_bracket_origin(origin);
8834 std::vector<native_order::RequestHandle> dynamic_carried_legs;
8835 for (const auto& handle : live_handles_) {
8836 const auto leg = placement_.find(handle.incarnation);
8837 if (leg == placement_.end()) continue;
8838 const auto family = leg->second.family;
8839 if ((family == PineOrderFamily::ExitLimit || family == PineOrderFamily::ExitStop
8840 || family == PineOrderFamily::ExitTrail)
8841 && std::find(carried_ids.begin(), carried_ids.end(), leg->second.from_entry)
8842 != carried_ids.end()) {
8843 dynamic_carried_legs.push_back(handle);
8844 }
8845 }
8846 for (const auto& handle : dynamic_carried_legs) {
8847 const auto result = require_host().cancel(handle);
8848 if (result.status == native_order::CancelStatus::Cancelled) retire(handle);
8849 }
8850 }
8851 snapshot.terms_priced_reverse = fixed_priced_reverse || cash_priced_reverse
8852 || (reverses && typed_sized);
8853 snapshot.placement_cycle = current_position_cycle_;
8854 if (fixed_priced_reverse) {
8855 snapshot.frozen_reversal_transaction = std::abs(current) + normalized_qty;
8856 }
8857 if (default_stop_freeze && finite_positive(default_stop_sizing_price)) {
8858 // A default-sized stop entry freezes its quantity against the
8859 // directionally snapped level, except an already-marketable stop
8860 // which is a next-open market order and therefore freezes at the
8861 // source close. Neither path re-sizes at its later fill quote --
8862 // above 100 % too, where ab9714be sized at the fill: eight lab tv
8863 // tapes of a 390-400 % stop on NYSE:F (the margin-schedule correction,
8864 // tests/fixtures/margin_entry_bar/pm-m10-*) open the snapped level's
8865 // quotient when the stop was not yet marketable and the signal
8866 // close's when it was, never the fill's.
8867 snapshot.sizing.price = default_stop_sizing_price;
8868 }
8869 bool pooc_fee_sized = false;
8870 if (default_sized && finite_positive(snapshot.sizing.price)) {
8871 if (config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
8872 || config_.default_qty_type == static_cast<int>(QtyType::CASH)) {
8873 snapshot.sizing.frozen_units = default_sizing_units(snapshot.sizing);
8874 }
8875 // pooc_fee_units() answers nothing outside its configuration
8876 // (pooc_fee_configured_), so only a run inside it asks.
8877 if (!priced && pooc_fee_configured_) {
8878 if (const auto fee_units = pooc_fee_units(snapshot.sizing)) {
8879 snapshot.sizing.frozen_units = *fee_units;
8880 pooc_fee_sized = true;
8881 }
8882 }
8883 // ab9714be pine_fills.cpp:7139: non-pure-stop priced entries size at fill time using calc_qty(fill_price)
8884 snapshot.sizing.at_fill = (config_.calc_on_order_fills && coof_recalc_active_)
8885 || (priced && !default_stop_freeze);
8886 }
8887 // Under process_orders_on_close TradingView sizes a default-quantity
8888 // percent_of_equity MARKET short at its slipped fill -- the close less the
8889 // slippage ticks -- but opens it only while those units' margin at the
8890 // close itself fits the equity the calculation marked there: synthetic 99.99 % of
8891 // 100000 at slippage 15 opens (49.9987 x 2000.00 = 99997.40), 99.995 % and
8892 // 100 % do not at that price/lot grid. An unfunded one is dropped from a flat
8893 // book and keeps only its closing leg when it reverses a long, whether a
8894 // strategy.close of that long precedes it or follows it (w4-f14-pooc-v6-slip:
8895 // TradingView closes every long and opens no short; lane
8896 // the third-party NYSE:F, ETH and EURUSD strategies hold no
8897 // short at all, where the engine opened each one and margin-called it on its
8898 // entry bar). A lot step coarse against the slippage absorbs the gap, so
8899 // most such shorts still open on BTC.
8900 if (config_.process_orders_on_close && !config_.calc_on_order_fills
8901 && config_.slippage > 0 && default_sized && !priced && !is_long
8902 && config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
8903 && !snapshot.sizing.at_fill
8904 && finite_positive(snapshot.sizing.frozen_units)
8905 && finite_positive(snapshot.sizing.mark) && std::isfinite(snapshot.sizing.equity)
8906 && snapshot.sizing.fx == 1.0) {
8907 const double required = snapshot.sizing.frozen_units * snapshot.sizing.mark
8908 * staged_.syminfo.pointvalue * config_.margin_short / 100.0;
8909 const double epsilon = std::max(1e-9, std::abs(snapshot.sizing.equity) * 1e-12);
8910 if (required > snapshot.sizing.equity + epsilon) {
8911 if (!reverses) return;
8912 snapshot.affordability_close_only = true;
8913 }
8914 }
8915 // Without process_orders_on_close the same holds for such a short from a
8916 // flat book, which fills at the next open: TradingView sizes it at the
8917 // signal close less the slippage ticks (and the commission) and opens it
8918 // only while those units' margin at the signal close, on its tick, fits
8919 // the equity there, whatever the open. Synthetic NYSE:F at slippage 1:
8920 // all 10000 at a close of 25.005 sizes 400 at 25.00 and does not open
8921 // (400 x 25.01 = 10004.00); the commission's smaller quantity opens where
8922 // the floor left the tick's room, and at slippage 4 none does (lab tv
8923 // tapes tests/fixtures/slipped_short int28fix-adm-s-s and int28fix-ou-s4;
8924 // The slipped-short admission rule). A long, and a short without slippage, are
8925 // sized at or beyond that close and keep their fill-time admission
8926 // (int28fix-adm-l-cs, -adm-s-0, -adm-s-c). A reversal, or a book with an
8927 // entry pending, is not measured.
8928 if (!config_.process_orders_on_close && !config_.calc_on_order_fills
8929 && !coof_recalc_active_ && config_.slippage > 0 && default_sized && !priced
8930 && !is_long && current == 0.0
8931 && config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
8932 && !snapshot.sizing.at_fill
8933 && finite_positive(snapshot.sizing.frozen_units)
8934 && finite_positive(snapshot.sizing.mark) && std::isfinite(snapshot.sizing.equity)
8935 && snapshot.sizing.fx == 1.0
8936 && std::none_of(live_handles_.begin(), live_handles_.end(),
8937 [&](const native_order::RequestHandle& handle) {
8938 const auto existing = placement_.find(handle.incarnation);
8939 return existing != placement_.end() && existing->second.opening
8940 && existing->second.family == PineOrderFamily::Entry;
8941 })) {
8942 const double signal = nearest_tick(snapshot.sizing.mark, staged_.syminfo.mintick);
8943 const double required = snapshot.sizing.frozen_units * signal
8944 * staged_.syminfo.pointvalue * config_.margin_short / 100.0;
8945 const double epsilon = std::max(1e-9, std::abs(snapshot.sizing.equity) * 1e-12);
8946 if (required > snapshot.sizing.equity + epsilon) return;
8947 }
8948 // R5 R2: a declaration-level default quantity whose sizing price IS the
8949 // signal rule and whose quantity is frozen at the command is exactly what
8950 // the core's Sized intent names, so it is lowered onto it here. The
8951 // fill-time paths (at_fill) and the pure-stop path size at a price the
8952 // core cannot name -- the source's own resolved fill quote, and the
8953 // directionally snapped stop level -- and keep their host-resolved shape,
8954 // as does a direction the run's own opening gate would refuse at
8955 // placement. The source keeps its money, its lot floor and its
8956 // placement admission; the core owns the units that settle.
8957 if (default_sized && !priced && !snapshot.sizing.at_fill && !direction_blocked
8958 && !pooc_fee_sized && finite_positive(snapshot.sizing.frozen_units)) {
8959 const auto* host_shape = std::get_if<native_order::HostSized>(&request.intent);
8960 if (host_shape && host_shape->kind == native_order::HostSizedKind::Open) {
8961 if (auto sized = default_sizing_intent(snapshot.sizing, is_long)) {
8962 request.intent = *sized;
8963 }
8964 }
8965 }
8966 // The TV money band is a source policy, not a generic margin rule. Its
8967 // all-in source tuple is judged at placement on ten-significant-digit
8968 // money: a true-flat order is dropped, while a real reversal retains only
8969 // its closing leg. A later price-scale failure drops the whole command.
8970 // This is the direct lowering of pine_fills.cpp:5054-5139 at ab9714be.
8971 const double entry_margin = is_long ? config_.margin_long : config_.margin_short;
8972 const bool tv_money_scope = default_sized && !priced
8973 && config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
8974 && std::abs(config_.default_qty_value - 100.0) < 1e-12
8975 && std::abs(entry_margin - 100.0) < 1e-12
8976 && staged_.quantity_grid && *staged_.quantity_grid > 0.0
8977 && finite_positive(snapshot.sizing.frozen_units)
8978 && finite_positive(snapshot.sizing.price)
8979 && finite_positive(snapshot.sizing.equity)
8980 && finite_positive(snapshot.sizing.fx)
8981 && finite_positive(staged_.syminfo.pointvalue)
8982 && (*staged_.quantity_grid * snapshot.sizing.price * staged_.syminfo.pointvalue
8983 * snapshot.sizing.fx < 1.0);
8984 // The POOC flat-money family is a fill-boundary decision because its
8985 // threshold includes the slipped signal price. validate_precommit owns
8986 // that exact comparison; applying the ordinary signal-price gate here
8987 // drops the tight-but-admitted POOC controls before the candidate exists.
8988 if (tv_money_scope && !config_.process_orders_on_close) {
8989 const double notional_per_price = snapshot.sizing.frozen_units
8990 * staged_.syminfo.pointvalue * snapshot.sizing.fx;
8991 const double rounded_cost = source_money_round(notional_per_price * snapshot.sizing.price);
8992 if (snapshot.sizing.equity + 1e-9 < rounded_cost) {
8993 if (reverses && !snapshot.projection_after_close) {
8994 snapshot.affordability_close_only = true;
8995 snapshot.rounded_signal_cost_close_only = true;
8996 } else if (reverses)
8997 snapshot.affordability_close_only = false;
8998 else return;
8999 } else if (!snapshot.projection_after_close) {
9000 const double affordable_price = source_money_round(
9001 source_money_round(snapshot.sizing.equity) / notional_per_price);
9002 if (std::isfinite(affordable_price) && affordable_price < snapshot.sizing.price) return;
9003 }
9004 }
9005 // pine_strategy_commands.cpp:284-426 placement half. A reversal whose
9006 // proposed opening cannot be funded retains a close-only source request;
9007 // flat/same-side rejection remains owned by their ordinary admission path.
9008 const bool affordability_scope = (!priced || pure_stop_entry) && (default_sized
9009 ? (config_.default_qty_type == static_cast<int>(QtyType::FIXED)
9010 || config_.default_qty_type == static_cast<int>(QtyType::CASH)
9011 || (config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
9012 && config_.default_qty_value > 100.0))
9013 : true);
9014 snapshot.affordability_policy_active = affordability_scope;
9015 if (affordability_scope && finite_positive(snapshot.sizing.mark)) {
9016 const double margin = is_long ? config_.margin_long : config_.margin_short;
9017 const double signal = nearest_tick(snapshot.sizing.mark, staged_.syminfo.mintick);
9018 double own_units = normalized_qty;
9019 if (default_sized) {
9020 own_units = finite_positive(snapshot.sizing.frozen_units)
9021 ? snapshot.sizing.frozen_units : config_.default_qty_value;
9022 } else if (qty_type == static_cast<int>(QtyType::CASH)) {
9023 own_units = typed_quantity_units(std::abs(normalized_qty), false, signal,
9024 snapshot.sizing.fx);
9025 } else if (qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)) {
9026 own_units = typed_quantity_units(
9027 snapshot.sizing.equity * std::abs(normalized_qty) / 100.0, true, signal,
9028 snapshot.sizing.fx);
9029 }
9030 const double held = reverses ? 0.0
9031 : std::max(0.0, std::abs(current) - preceding_close_qty);
9032 double charged_held = held;
9033 if ((pooc_pass && close_all_precedes) || pooc_pass_flattens) {
9034 // The position before this pass's closes: the script's own view,
9035 // which an ordinary close_all froze when it filled at its call.
9036 const auto* pine_host = pine_view_of(&require_host());
9037 const double before = pine_host ? pine_host->signed_position_size() : current;
9038 charged_held = before != 0.0 && ((before > 0.0) == is_long) ? std::abs(before) : 0.0;
9039 }
9040 const double required = (charged_held + std::abs(own_units)) * signal * staged_.syminfo.pointvalue
9041 * snapshot.sizing.fx * margin / 100.0;
9042 // The flat/same-side refusal below also owns finite explicit overflow.
9043 // Default sizing, reversals and nonfinite inputs retain the G fault.
9044 const bool finite_explicit_rejection = !default_sized && !reverses
9045 && std::isfinite(charged_held) && std::isfinite(own_units)
9046 && std::isfinite(signal) && std::isfinite(staged_.syminfo.pointvalue)
9047 && std::isfinite(snapshot.sizing.fx) && std::isfinite(margin);
9048 if (margin > 0.0
9049 && ((!std::isfinite(required) && !finite_explicit_rejection)
9050 || !std::isfinite(snapshot.sizing.equity))) {
9051 engine_command_value_invalid_or_count("margin.required", EngineValueStage::margin,
9052 EngineValueReason::nonfinite);
9053 }
9054 const double epsilon = std::max(1e-9, std::abs(snapshot.sizing.equity) * 1e-12);
9055 snapshot.projection_affordability_equity = snapshot.sizing.equity;
9056 snapshot.projection_affordability_signal_price = signal;
9057 snapshot.projection_affordability_held_qty = held;
9058 if (reverses && margin > 0.0 && std::isfinite(required)
9059 && std::isfinite(snapshot.sizing.equity)
9060 && required > snapshot.sizing.equity + epsilon) {
9061 snapshot.affordability_close_only = true;
9062 } else if (!reverses && margin > 0.0
9063 && (!std::isfinite(required) || !std::isfinite(snapshot.sizing.equity)
9064 || required > snapshot.sizing.equity + epsilon)) {
9065 // The same placement-time rule drops an unaffordable flat or
9066 // same-side entry before it reaches the native request core.
9067 // ab9714be pine_strategy_commands.cpp:148-159, :343-426: a
9068 // rejected same-id pure STOP reissue also removes the older
9069 // resting request; a MARKET has no comparable carry.
9070 if (pure_stop_entry) {
9071 std::optional<native_order::RequestHandle> prior_handle;
9072 if (const auto prior = live_by_source_key_.find(checked_live_key(id));
9073 prior != live_by_source_key_.end()) {
9074 prior_handle = prior->second;
9075 }
9076 if (prior_handle) {
9077 const auto result = require_host().cancel(*prior_handle);
9078 if (result.status == native_order::CancelStatus::Cancelled)
9079 retire(*prior_handle);
9080 }
9081 }
9082 return;
9083 }
9084 }
9085 // A default percent_of_equity (<= 100) MARKET add to a held position of
9086 // its own side is judged at placement against the held units plus its
9087 // own, as the explicit, fixed and cash sizes above are: TradingView drops
9088 // an add whose combined margin exceeds the equity, even where an exit
9089 // empties the position at the open the add would fill at, ahead of it
9090 // (lab tv tapes tests/fixtures/margin_v6 w5-m1-addexit-p100 and -p60 drop
9091 // every add; lane W5-ENG-MARGIN-V6). Without this the add opened from flat
9092 // behind the exit whenever the exit's leg took the open first. An over-cap
9093 // add the cap check above lets through stays a request, as it was: it is
9094 // judged where it could become executable (apply_open_market_admission).
9095 if (!affordability_scope && default_sized && !priced && !reverses
9096 && !same_bar_market_candidate && !short_seed_final_candidate
9097 && !paired_all_in_reentry && !default_gross_over_cap_candidate
9098 && !close_precedes_entry && !close_all_precedes
9099 && !config_.process_orders_on_close && !config_.calc_on_order_fills
9100 && config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
9101 && config_.default_qty_value <= 100.0
9102 && current != 0.0 && ((current > 0.0) == is_long)
9103 && finite_positive(snapshot.sizing.frozen_units)
9104 && finite_positive(snapshot.sizing.mark)
9105 && add_margin_exceeds_equity(std::abs(current), snapshot.sizing,
9106 is_long ? config_.margin_long : config_.margin_short,
9107 staged_.syminfo.mintick, staged_.syminfo.pointvalue)) {
9108 return;
9109 }
9110 // TradingView checks a default-quantity LIMIT entry that opens from flat
9111 // at every call, against the calling bar's close: a call whose quantity
9112 // costs more than the equity there is rejected, and it takes the resting
9113 // order of that id with it, however affordable the order's own level is
9114 // (lane W8A-SIGSTATE-1 R3, tests/fixtures/limit_entry_affordability).
9115 // The fill-time check still applies to an order the call admitted.
9116 if (default_sized && config_.default_qty_type == static_cast<int>(QtyType::FIXED)
9117 && current == 0.0 && finite_positive(limit_price) && std::isnan(stop_price)
9118 && finite_positive(snapshot.sizing.mark)) {
9119 const double margin = is_long ? config_.margin_long : config_.margin_short;
9120 const double signal = nearest_tick(snapshot.sizing.mark, staged_.syminfo.mintick);
9121 const double required = std::abs(config_.default_qty_value) * signal
9122 * staged_.syminfo.pointvalue * snapshot.sizing.fx * margin / 100.0;
9123 const double epsilon = std::max(1e-9, std::abs(snapshot.sizing.equity) * 1e-12);
9124 if (margin > 0.0 && (!std::isfinite(required) || !std::isfinite(snapshot.sizing.equity)
9125 || required > snapshot.sizing.equity + epsilon)) {
9126 if (const auto prior = live_by_source_key_.find(checked_live_key(id));
9127 prior != live_by_source_key_.end()) {
9128 const auto handle = prior->second;
9129 const auto result = require_host().cancel(handle);
9130 if (result.status == native_order::CancelStatus::Cancelled) retire(handle);
9131 }
9132 return;
9133 }
9134 }
9135 // Margin rule 2 for a percent-of-equity stop opening from flat at full
9136 // margin without slippage under process_orders_on_close: judged as an
9137 // explicit quantity is, at the
9138 // signal close c' = ticks x mintick (tests/fixtures/margin_call_rules
9139 // stop-40-percent: per-unit 1.18081 under the ulp-dirty
9140 // 1.1808100000000001 drops it, as it drops the explicit stop).
9141 if (PF_RARE(detail::margin_rule_switches().slipped_signal_admission && default_stop_scope
9142 && current == 0.0 && !reverses && config_.slippage == 0
9143 && config_.process_orders_on_close
9144 && finite_positive(snapshot.sizing.frozen_units)
9145 && finite_positive(snapshot.sizing.mark) && finite_positive(staged_.syminfo.mintick)
9146 && (is_long ? config_.margin_long : config_.margin_short) == 100.0
9147 && staged_.syminfo.pointvalue == 1.0 && snapshot.sizing.fx == 1.0
9148 && staged_.account_fx_effective_from_ms.empty()
9149 && std::isfinite(snapshot.sizing.equity))) {
9150 const double tick = staged_.syminfo.mintick;
9151 const double signal_close = std::floor(snapshot.sizing.mark / tick + 0.5) * tick;
9152 const double units = snapshot.sizing.frozen_units;
9153 const double equity = snapshot.sizing.equity;
9154 if (equity < source_money_round(units * signal_close)
9155 || source_money_round(source_money_round(equity) / units) < signal_close) {
9156 if (const auto prior = live_by_source_key_.find(checked_live_key(id));
9157 prior != live_by_source_key_.end()) {
9158 const auto prior_handle = prior->second;
9159 const auto result = require_host().cancel(prior_handle);
9160 if (result.status == native_order::CancelStatus::Cancelled) retire(prior_handle);
9161 }
9162 return;
9163 }
9164 }
9165 if (default_stop_scope && finite_positive(snapshot.sizing.frozen_units)
9166 && finite_positive(snapshot.sizing.mark)) {
9167 const double margin = is_long ? config_.margin_long : config_.margin_short;
9168 const double required = snapshot.sizing.frozen_units * snapshot.sizing.mark
9169 * staged_.syminfo.pointvalue * snapshot.sizing.fx * margin / 100.0;
9170 // ab9714be pine_strategy_commands.cpp:343-426 prices the default
9171 // percent_of_equity <= 100 pure STOP against placement equity with the
9172 // SAME float guard the explicit/FIXED/CASH/>100 arm uses; an all-in
9173 // stop quantity is floored against tick(close) so its cost lands inside
9174 // one double-rounding of the equity snapshot and must not be dropped.
9175 const double stop_epsilon = std::max(
9176 1e-9, std::abs(snapshot.sizing.equity) * 1e-12);
9177 const bool unfunded = margin > 0.0 && (!std::isfinite(required)
9178 || !std::isfinite(snapshot.sizing.equity)
9179 || required > snapshot.sizing.equity + stop_epsilon);
9180 if (unfunded && reverses) {
9181 // The same placement arm keeps a reversal's closing leg
9182 // (pine_strategy_commands.cpp:408-419): a sell stop sized at its
9183 // level and priced at the higher close cannot fund 100 % of
9184 // equity, so TradingView closes the long at the stop and opens
9185 // nothing (lane TVDEF-DROPS, tests/fixtures/tvdef_drops R1).
9186 snapshot.affordability_close_only = true;
9187 } else if (unfunded) {
9188 // Legacy replacement first removes the prior same-id resting
9189 // stop, then leaves the rejected re-issue absent from the book.
9190 std::optional<native_order::RequestHandle> prior_handle;
9191 if (const auto prior = live_by_source_key_.find(checked_live_key(id));
9192 prior != live_by_source_key_.end()) {
9193 prior_handle = prior->second;
9194 }
9195 if (prior_handle) {
9196 const auto result = require_host().cancel(*prior_handle);
9197 if (result.status == native_order::CancelStatus::Cancelled) retire(*prior_handle);
9198 }
9199 return;
9200 }
9201 }
9202 if (const auto point = detail::callback_point(require_host())) {
9203 snapshot.placement_script_open_ms = point->decision.script_bar_open_ms;
9204 snapshot.placement_sub_open_ms = point->decision.sub_bar_open_ms;
9205 if (default_sized && reverses && !snapshot.replaced_opening) {
9206 for (auto it = live_handles_.rbegin(); it != live_handles_.rend(); ++it) {
9207 const auto prior = placement_.find(it->incarnation);
9208 if (prior == placement_.end()) continue;
9209 auto& prior_snapshot = prior->second;
9210 if (!prior_snapshot.opening || prior_snapshot.family != PineOrderFamily::Entry
9211 || prior_snapshot.is_long != is_long
9212 || prior_snapshot.placement_script_open_ms != point->decision.script_bar_open_ms) {
9213 continue;
9214 }
9215 const std::uint64_t group = prior_snapshot.sequential_group != 0
9216 ? prior_snapshot.sequential_group : ++next_sequential_group_;
9217 prior_snapshot.sequential_group = group; invalidate_retirement();
9218 if (prior_snapshot.sequential_rank == 0) prior_snapshot.sequential_rank = 1;
9219 snapshot.sequential_group = group;
9220 snapshot.sequential_rank = 2;
9221 break;
9222 }
9223 }
9224 }
9225 if (current == 0.0 && config_.process_orders_on_close
9226 && !config_.calc_on_order_fills && default_sized
9227 && std::holds_alternative<native_order::Stop>(request.trigger)
9228 && !finite_positive(limit_price) && oca_name.empty() && source_point) {
9229 stage_flat_children_before_parent(
9230 id, source_point->decision.coordinate.interval_index,
9231 source_point->decision.script_bar_open_ms);
9232 }
9233 const EntrySubmissionContext submission_context{
9234 current, limit_price, stop_price, default_sized, priced, reverses,
9235 opposite_opening_pending, pure_stop_entry, coof_market_next_open,
9236 coof_priced_next_open, coof_market_at_second_extreme, paired_all_in_reentry};
9237 if (close_all_precedes && preceding_close_all.incarnation != 0) {
9238 // A full close moved the prior entry out of the live/batched book.
9239 // Replace it where it waits for that close, before selecting a new
9240 // batch or appending a second definition behind the same parent.
9241 auto queued = std::find_if(pending_entries_.begin(), pending_entries_.end(),
9242 [&](const PendingEntry& prior) {
9243 return prior.replacement_key == id
9244 && prior.snapshot.paired_reversal_parent == preceding_close_all;
9245 });
9246 if (queued != pending_entries_.end()) {
9247 pending_write(*queued) = PendingEntry{std::move(request), std::move(snapshot), id};
9248 return;
9249 }
9250 }
9251 if (same_bar_market_candidate && fixed_market_batch_candidate
9252 && !close_precedes_entry && pending_same_bar_close_qty_ == 0.0
9253 && !close_all_precedes) {
9254 auto unbatched_request = request;
9255 auto unbatched_snapshot = snapshot;
9256 const double own_units = default_sized
9257 ? floor_quantity_grid(config_.default_qty_value, staged_.quantity_grid)
9258 : explicit_entry_quantity_floor(std::abs(qty), staged_.quantity_grid);
9259 if (!finite_positive(own_units)) return;
9260 double projected_units = current;
9261 std::size_t projected_entries = current == 0.0 ? 0U
9262 : accepted_entries_in_cycle(current > 0.0, id);
9263 // A same-id reissue replaces its earlier row; project only the remaining rows.
9264 for (const auto& pending : pending_same_bar_commands_) {
9265 const auto& prior = pending.snapshot;
9266 if (pending.replacement_key == id || !pending.opening
9267 || prior.family != PineOrderFamily::Entry
9268 || !prior.frozen_market_instruction) {
9269 continue;
9270 }
9271 const bool opposite = projected_units != 0.0
9272 && ((projected_units > 0.0) != prior.is_long);
9273 projected_entries = opposite ? 1U : projected_entries + 1U;
9274 projected_units += prior.is_long ? prior.frozen_market_transaction_units
9275 : -prior.frozen_market_transaction_units;
9276 }
9277 const bool same_side = projected_units != 0.0
9278 && ((projected_units > 0.0) == is_long);
9279 const auto entry_cap = static_cast<std::size_t>(std::max(1, config_.pyramiding));
9280 if (same_side && projected_entries >= entry_cap) return;
9281 const double held_opposite = projected_units != 0.0 && !same_side
9282 ? std::abs(projected_units) : 0.0;
9283 const double transaction = own_units + held_opposite;
9284 request.intent = native_order::Transact{is_long ? transaction : -transaction};
9285 snapshot.reverse_to = false;
9286 snapshot.projection_over_pyramiding = false;
9287 snapshot.frozen_market_instruction = true;
9288 snapshot.frozen_market_own_units = own_units;
9289 snapshot.frozen_market_transaction_units = transaction;
9290 auto existing = std::find_if(pending_same_bar_commands_.begin(),
9291 pending_same_bar_commands_.end(), [&](const PendingSameBarCommand& pending) {
9292 return !pending.snapshot.frozen_market_targeted_close
9293 && pending.replacement_key == id;
9294 });
9295 PendingSameBarCommand pending{std::move(request), std::move(snapshot), id, true};
9296 pending.unbatched_request = std::move(unbatched_request);
9297 pending.unbatched_snapshot = std::move(unbatched_snapshot);
9298 pending.unbatched_context = submission_context;
9299 if (existing == pending_same_bar_commands_.end()) {
9300 pending_write(pending_same_bar_commands_).push_back(std::move(pending));
9301 } else {
9302 source_batch_mutated_ = true;
9303 pending_write(*existing) = std::move(pending);
9304 }
9305 return;
9306 }
9307 if (same_bar_market_candidate) {
9308 // Default percent/cash commands are already frozen at their source
9309 // call boundary. The batch's topology must use that physical own
9310 // size, never the public percentage/cash scalar, just as the fixed
9311 // branch uses its explicit unit value.
9312 const double default_own = finite_positive(snapshot.sizing.frozen_units)
9313 ? snapshot.sizing.frozen_units : config_.default_qty_value;
9314 const double own_units = default_sized
9315 ? floor_quantity_grid(default_own, staged_.quantity_grid)
9316 : explicit_entry_quantity_floor(std::abs(qty), staged_.quantity_grid);
9317 bool opposite_market_pending = false;
9318 bool opposite_entry_pending = false;
9319 double opposite_pending_own = 0.0;
9320 const auto inspect_pending = [&](const PlacementSnapshot& prior) {
9321 if (!prior.opening || prior.source_id == id || prior.is_long == is_long) {
9322 return;
9323 }
9324 if (prior.frozen_market_instruction
9325 && finite_positive(prior.frozen_market_own_units)) {
9326 opposite_market_pending = true;
9327 opposite_pending_own += prior.frozen_market_own_units;
9328 } else {
9329 opposite_entry_pending = true;
9330 }
9331 };
9332 for (const auto& pending : pending_same_bar_commands_) inspect_pending(pending.snapshot);
9333 for (const auto& pending : pending_entries_) inspect_pending(pending.snapshot);
9334 if (const auto point = detail::callback_point(require_host())) {
9335 for (const auto& handle : live_handles_) {
9336 const auto prior = placement_.find(handle.incarnation);
9337 if (prior == placement_.end()
9338 || prior->second.placement_script_open_ms
9339 != point->decision.script_bar_open_ms) {
9340 continue;
9341 }
9342 inspect_pending(prior->second);
9343 }
9344 }
9345 std::size_t same_side_pending = 0;
9346 for (const auto& pending : pending_same_bar_commands_) {
9347 if (pending.opening && pending.snapshot.is_long == is_long
9348 && pending.snapshot.source_id != id
9349 && !pending.snapshot.projection_over_pyramiding) {
9350 ++same_side_pending;
9351 }
9352 }
9353 for (const auto& pending : pending_entries_) {
9354 if (pending.snapshot.opening && pending.snapshot.is_long == is_long
9355 && pending.snapshot.source_id != id
9356 && !pending.snapshot.projection_over_pyramiding) {
9357 ++same_side_pending;
9358 }
9359 }
9360 if (const auto point = detail::callback_point(require_host())) {
9361 for (const auto& handle : live_handles_) {
9362 const auto prior = placement_.find(handle.incarnation);
9363 if (prior == placement_.end()
9364 || prior->second.placement_script_open_ms
9365 != point->decision.script_bar_open_ms) {
9366 continue;
9367 }
9368 // R4-D L10z review fix 2: a live row whose cancellation was
9369 // already recorded no longer competes for the pyramiding cap.
9370 if (prior->second.opening && prior->second.is_long == is_long
9371 && prior->second.source_id != id
9372 && !prior->second.projection_over_pyramiding
9373 && prior->second.cancellation.cause
9375 ++same_side_pending;
9376 }
9377 }
9378 }
9379 const bool same_side = (current != 0.0 && ((current > 0.0) == is_long))
9380 || (current == 0.0 && same_side_pending > 0);
9381 const std::size_t current_lots =
9382 (current != 0.0 && ((current > 0.0) == is_long))
9383 ? detail::run_position(require_host()).lot_count
9384 : 0U;
9385 // ab9714be pine_orders.cpp:737-740: add_to_pyramid_market rejects entry when position_entry_count_ >= pyramiding
9386 const std::size_t total_entries = current_lots + same_side_pending;
9387 const bool over_cap = same_side
9388 && ((config_.pyramiding == 0 && total_entries >= 1U)
9389 || (config_.pyramiding > 0
9390 && total_entries >= static_cast<std::size_t>(config_.pyramiding)));
9391 snapshot.projection_over_pyramiding = over_cap;
9392 if (current == 0.0) {
9393 for (const auto& handle : live_handles_) {
9394 const auto found = placement_.find(handle.incarnation);
9395 if (found == placement_.end()) continue;
9396 auto& candidate = found->second;
9397 const bool exit = candidate.family == PineOrderFamily::ExitLimit
9398 || candidate.family == PineOrderFamily::ExitStop
9399 || candidate.family == PineOrderFamily::ExitTrail;
9400 if (exit && candidate.from_entry == id
9401 && !(cohort_exposure_for(id) > 0.0)) {
9402 candidate.reservation_deferred_to_pending_entry = true;
9403 candidate.projection_remaining_qty = kNaN;
9404 }
9405 }
9406 }
9407 if (over_cap && !opposite_market_pending && !opposite_entry_pending) return;
9408 if (!(over_cap && !opposite_market_pending)
9409 && finite_positive(own_units)) {
9410 // R1: a staged reversal's same-id reissue transacts its own units
9411 // only; it does not carry the held opposite side.
9412 const double held_opposite_units = current != 0.0 && ((current > 0.0) != is_long)
9413 ? std::max(0.0, std::abs(current) - preceding_close_qty) : 0.0;
9414 const double held_opposite = staged_reversal_replacement ? 0.0 : held_opposite_units;
9415 const double transaction = own_units + held_opposite + opposite_pending_own;
9416 if (finite_positive(transaction)) {
9417 if (over_cap && opposite_market_pending
9418 && config_.default_qty_type == static_cast<int>(QtyType::FIXED)) {
9419 // The kept over-cap member is admitted at its source
9420 // call as the whole frozen broker movement: held side,
9421 // this member's own leg, and every opposite pending
9422 // MARKET leg. The eventual net position is smaller,
9423 // but using it here would incorrectly admit famS's
9424 // 3-lot ES/NQ census rows.
9425 const double gross_units = std::abs(current) + own_units
9426 + opposite_pending_own;
9427 const double margin = is_long ? config_.margin_long : config_.margin_short;
9428 const double required = gross_units * snapshot.sizing.price
9429 * staged_.syminfo.pointvalue * snapshot.sizing.fx * margin / 100.0;
9430 if (!std::isfinite(required) || !std::isfinite(snapshot.sizing.equity)
9431 || required > snapshot.sizing.equity) {
9432 return;
9433 }
9434 }
9435 snapshot.opening = true;
9436 snapshot.frozen_market_instruction = true;
9437 snapshot.frozen_market_own_units = own_units;
9438 snapshot.frozen_market_transaction_units = transaction;
9439 auto existing = std::find_if(pending_same_bar_commands_.begin(),
9440 pending_same_bar_commands_.end(), [&](const PendingSameBarCommand& row) {
9441 return !row.snapshot.frozen_market_targeted_close
9442 && row.replacement_key == id;
9443 });
9444 PendingSameBarCommand pending{std::move(request), std::move(snapshot), id, true};
9445 if (staged_reversal_replacement) {
9446 pending.staged_reversal = true;
9447 pending.staged_reversal_held_units = held_opposite_units;
9448 }
9449 if (existing == pending_same_bar_commands_.end()) {
9450 pending_write(pending_same_bar_commands_).push_back(std::move(pending));
9451 } else {
9452 source_batch_mutated_ = true;
9453 pending_write(*existing) = std::move(pending);
9454 }
9455 return;
9456 }
9457 }
9458 }
9459 if (preceding_close_all.incarnation != 0) {
9460 pending_write(pending_entries_).push_back({std::move(request), std::move(snapshot), id});
9461 return;
9462 }
9463 submit_entry_with_policy(std::move(request), std::move(snapshot), id, submission_context);
9464}
9465
9466void PineExecutionAdapter::submit_entry_with_policy(
9467 native_order::Request request, PlacementSnapshot snapshot, const SourceId& id,
9468 const EntrySubmissionContext& context) {
9469 const double current = context.current;
9470 const double limit_price = context.limit_price;
9471 const double stop_price = context.stop_price;
9472 const bool default_sized = context.default_sized;
9473 const bool priced = context.priced;
9474 const bool reverses = context.reverses;
9475 const bool opposite_opening_pending = context.opposite_opening_pending;
9476 const bool pure_stop_entry = context.pure_stop_entry;
9477 const bool coof_market_next_open = context.coof_market_next_open;
9478 const bool coof_priced_next_open = context.coof_priced_next_open;
9479 const bool coof_market_at_second_extreme = context.coof_market_at_second_extreme;
9480 const bool paired_all_in_reentry = context.paired_all_in_reentry;
9481 const bool is_long = snapshot.is_long;
9482 const auto oca_name = snapshot.oca_name;
9483 const auto source_point = detail::callback_point(require_host());
9484 const bool source_same_side_market_add = default_sized
9485 && config_.default_qty_type == static_cast<int>(QtyType::FIXED)
9486 && !config_.calc_on_order_fills
9487 && (config_.process_orders_on_close || !opposite_opening_pending)
9488 && current != 0.0
9489 && ((current > 0.0) == is_long)
9490 && std::holds_alternative<native_order::Market>(request.trigger);
9491 const bool close_first_percent_add = default_sized
9492 && config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
9493 && std::abs(config_.default_qty_value - 100.0) < 1e-12
9494 && !config_.calc_on_order_fills && !config_.process_orders_on_close
9495 && config_.pyramiding > 1 && current != 0.0
9496 && ((current > 0.0) == is_long)
9497 && std::holds_alternative<native_order::Market>(request.trigger);
9498 if (source_same_side_market_add || close_first_percent_add) {
9499 auto queued = std::find_if(pending_entries_.begin(), pending_entries_.end(),
9500 [&](const PendingEntry& value) { return value.replacement_key == id; });
9501 PendingEntry pending{std::move(request), std::move(snapshot), id};
9502 if (queued == pending_entries_.end()) pending_write(pending_entries_).push_back(std::move(pending));
9503 else pending_write(*queued) = std::move(pending);
9504 return;
9505 }
9506 if (current == 0.0 && config_.process_orders_on_close && default_sized
9507 && std::holds_alternative<native_order::Stop>(request.trigger)
9508 && !finite_positive(limit_price) && oca_name.empty()) {
9509 const auto token = named_entry_cancel_tokens_.find(id);
9510 if (token != named_entry_cancel_tokens_.end()
9511 && token->second.entry_incarnation != 0
9512 && token->second.surviving_exit_incarnation != 0) {
9513 std::vector<std::uint64_t> child_families;
9514 for (const auto& handle : live_handles_) {
9515 const auto child = placement_.find(handle.incarnation);
9516 if (child == placement_.end() || child->second.from_entry != id) continue;
9517 const auto family = child->second.family;
9518 if (family != PineOrderFamily::ExitLimit
9519 && family != PineOrderFamily::ExitStop
9520 && family != PineOrderFamily::ExitTrail) {
9521 continue;
9522 }
9523 const auto key = key_for(child->second.source_id, child->second.from_entry);
9524 if (std::find(child_families.begin(), child_families.end(), key)
9525 == child_families.end()) {
9526 child_families.push_back(key);
9527 }
9528 }
9529 if (child_families.size() == 1) {
9530 snapshot.recreated_after_named_cancelled_entry_incarnation =
9531 token->second.entry_incarnation;
9532 snapshot.named_cancel_surviving_exit_incarnation =
9533 token->second.surviving_exit_incarnation;
9534 }
9535 snapshot.retained_parent_topology = true;
9536 if (const auto point = detail::callback_point(require_host())) {
9537 snapshot.projection_created_bar = projection_bar_index(point->decision);
9538 snapshot.projection_position_side = static_cast<std::int32_t>(PositionSide::FLAT);
9539 }
9540 snapshot.source_sequence = source_sequence_ + 1;
9541 if (child_families.size() != 1) named_entry_cancel_tokens_.erase(token);
9542 pending_write(pending_entries_).push_back({std::move(request), std::move(snapshot), id});
9543 return;
9544 }
9545 }
9546 if (!priced && reverses) {
9547 auto queued = std::find_if(pending_entries_.begin(), pending_entries_.end(),
9548 [&](const PendingEntry& value) { return value.replacement_key == id; });
9549 if (queued != pending_entries_.end()) {
9550 // A same-callback MARKET reissue replaces the staged priced
9551 // reversal before either definition reaches the generic core.
9552 // Flushing the old priced row after submitting this one reverses
9553 // the replacement and leaves the market instruction unreachable.
9554 pending_write(*queued) = PendingEntry{std::move(request), std::move(snapshot), id};
9555 return;
9556 }
9557 }
9558 if (priced && reverses && !config_.process_orders_on_close
9559 && !config_.calc_on_order_fills) {
9560 auto queued = std::find_if(pending_entries_.begin(), pending_entries_.end(),
9561 [&](const PendingEntry& value) { return value.replacement_key == id; });
9562 PendingEntry pending{std::move(request), std::move(snapshot), id};
9563 if (queued == pending_entries_.end()) pending_write(pending_entries_).push_back(std::move(pending));
9564 else pending_write(*queued) = std::move(pending);
9565 return;
9566 }
9567 if (config_.process_orders_on_close && !close_batch_callsites_.empty()
9568 && !priced && !coof_recalc_active_) {
9569 pending_write(pending_entries_).push_back({std::move(request), std::move(snapshot), id});
9570 return;
9571 }
9572 // The legacy source selector orders a flat COOF book by the first
9573 // reachable priced trigger, not by statement insertion. Queue only this
9574 // bounded source shape until the enclosing source evaluation ends, then
9575 // materialize it in source-policy order before the generic core receives
9576 // any request. Recalc-born entries retain their existing callback path.
9577 const bool coof_flat_priced = config_.calc_on_order_fills && current == 0.0
9578 && priced && !coof_recalc_active_ && oca_name.empty();
9579 const bool ordinary_flat_pure_stop = [&]() {
9580 if (config_.process_orders_on_close || config_.calc_on_order_fills
9581 || current != 0.0 || !pure_stop_entry || !oca_name.empty()
9582 || !source_point) {
9583 return false;
9584 }
9585 const auto* pine_host = pine_view_of(&require_host());
9586 const auto next = pine_host
9587 ? pine_host->scheduler_.next_source_bar(
9588 source_point->decision.coordinate.interval_index)
9589 : std::optional<Bar>{};
9590 return next && source_bar_fill_tick(next->open, staged_.syminfo.mintick)
9591 == stop_price;
9592 }();
9593 if (coof_flat_priced || ordinary_flat_pure_stop) {
9594 pending_write(pending_entries_).push_back({std::move(request), std::move(snapshot), id});
9595 return;
9596 }
9597 if (coof_market_next_open || coof_priced_next_open) {
9598 pending_write(pending_coof_requests_).push_back(
9599 {std::move(request), std::move(snapshot), id, true, 0, true});
9600 return;
9601 }
9602 if (defer_coof_tail() && !coof_market_at_second_extreme) {
9603 pending_write(pending_coof_requests_).push_back({std::move(request), std::move(snapshot), id, true, 0});
9604 return;
9605 }
9606 const auto accepted = submit_or_replace(std::move(request), std::move(snapshot), true, id);
9607 if (accepted) {
9608 if (current < 0.0 && is_long) short_seed_long_candidate_ = *accepted;
9609 const auto entry = placement_.find(accepted->incarnation);
9610 if (entry != placement_.end()) {
9611 const std::uint64_t entry_sequence = entry->second.command_sequence;
9612 // A source entry accepted after a captured POOC global exit is
9613 // outside that exit's live population. Keep any already-bound
9614 // pre-exit add eligible to grow the finite reservation at fill,
9615 // but close the open-population marker now so later entries never
9616 // acquire that reservation merely by arriving before the match.
9617 for (const auto& handle : live_handles_) {
9618 if (handle == *accepted) continue;
9619 const auto existing = placement_.find(handle.incarnation);
9620 if (existing == placement_.end()) continue;
9621 auto& prior = existing->second;
9622 if (!prior.reservation_expansion.capture()
9623 || prior.command_sequence >= entry_sequence) {
9624 continue;
9625 }
9626 prior.pooc_global_full_exit_dynamic_qty = false;
9627 prior.pooc_global_full_exit_tracks_bound_adds = false;
9628 prior.reservation_expansion.close_population(accepted->incarnation);
9629 }
9630 }
9631 if (default_sized && !priced && reverses && source_point
9632 && config_.default_qty_type
9633 == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
9634 && config_.default_qty_value <= 100.0) {
9635 if (auto* pine_host = pine_view_of(&require_host())) {
9636 if (const auto next = pine_host->scheduler_.next_source_bar(
9637 source_point->decision.coordinate.interval_index)) {
9638 NativeDecisionContext next_context = source_point->decision;
9639 ++next_context.coordinate.interval_index;
9640 next_context.coordinate.path_phase = NativePathPhase::Open;
9641 next_context.script_bar_open_ms = next->timestamp;
9642 next_context.sub_bar_open_ms = next->timestamp;
9643 apply_reversal_gap_bracket_policy(
9644 *next, next_context, /*defer_trails=*/true);
9645 }
9646 }
9647 }
9648 } else if (paired_all_in_reentry) {
9649 // The source call is still observable in its current script pass,
9650 // although native max-lot admission has already terminally refused
9651 // it. Preserve that truthful source observer row until the next
9652 // broker open, without retaining a second executable order.
9653 // (The copy is made from the source snapshot before the next call.)
9654 SourceShadowPending shadow;
9655 shadow.snapshot.family = PineOrderFamily::Entry;
9656 shadow.snapshot.source_id = id;
9657 shadow.snapshot.is_long = is_long;
9658 shadow.snapshot.opening = true;
9659 shadow.snapshot.sizing = sizing_snapshot();
9660 shadow.label = id;
9661 pending_write(source_shadow_pending_).push_back(std::move(shadow));
9662 }
9663}
9664
9665bool PineExecutionAdapter::enqueue_pooc_fifo_close(
9666 const SourceId& id, const std::string& comment,
9667 std::uint64_t token, std::uint64_t) {
9668 const auto point = detail::callback_point(require_host());
9669 const int bar = point ? point->decision.coordinate.interval_index : -1;
9670 if (close_batch_bar_ != bar) {
9671 close_batch_bar_ = bar;
9672 close_batch_queue_sequence_ = 0;
9673 close_batch_callsites_.clear();
9674 close_batch_pending_debt_ = 0.0;
9675 close_batch_admitted_total_ = 0.0;
9676 }
9677
9678 auto existing = close_batch_callsites_.find(token);
9679 const CloseCallsiteState* prior = existing == close_batch_callsites_.end()
9680 ? nullptr : &existing->second;
9681 const auto logical = close_logical_units_.find(id);
9682 const double unclosed = logical == close_logical_units_.end()
9683 ? 0.0 : logical->second;
9684
9685 // A call is sized against the position less what the bar's other close
9686 // orders already hold; a site re-sizing its own order counts its own
9687 // claim back unless another site closes that id as well.
9688 double pending_reserved = 0.0;
9689 for (const auto& row : close_batch_callsites_) {
9690 if (row.second.active) pending_reserved += row.second.target;
9691 }
9692 const bool same_id_reissue = prior && prior->active && prior->id == id;
9693 bool replacement_can_reuse_own_claim = false;
9694 if (prior && prior->active && prior->id != id) {
9695 const bool another_targets_prior = std::any_of(
9696 close_batch_callsites_.begin(), close_batch_callsites_.end(),
9697 [&](const auto& row) {
9698 return row.first != token && row.second.active
9699 && row.second.id == prior->id;
9700 });
9701 replacement_can_reuse_own_claim = !another_targets_prior;
9702 }
9703 if (same_id_reissue || replacement_can_reuse_own_claim)
9704 pending_reserved -= prior->target;
9705
9706 const double held = std::abs(detail::run_position(require_host()).signed_units);
9707 const double available = std::max(0.0, held - pending_reserved);
9708 const double target = std::min(unclosed, available);
9709 // A call with nothing to close -- its id has no unbooked units, or the
9710 // bar's other closes hold the whole position -- places nothing and
9711 // leaves its site's order as it was (tapes w3bf02-c1, -c2, -c3).
9712 if (!(target > internal::kQtyEpsilon)) return false;
9713
9714 const double replaced_target = prior && prior->active ? prior->target : 0.0;
9715 close_batch_pending_debt_ += target;
9716 pending_same_bar_close_qty_ += target;
9717 if (token != 0) close_batch_admitted_total_ += target - replaced_target;
9718
9719 auto& site = close_batch_callsites_[token];
9720 if (!site.active) {
9721 site.active = true;
9722 site.token = token;
9723 site.calls = 1;
9724 site.first_id = id;
9725 site.id = id;
9726 site.comment = comment;
9727 site.target = target;
9728 site.queue_sequence = ++close_batch_queue_sequence_;
9729 return true;
9730 }
9731 // A later call of the site re-sizes the site's one order for its own id
9732 // and comment; the fill still books against the site's first id.
9733 if (site.id != id) ++site.calls;
9734 site.id = id;
9735 site.comment = comment;
9736 site.target = target;
9737 return true;
9738}
9739
9741 if (close_batch_callsites_.empty()) return;
9742 std::vector<CloseCallsiteState> sites;
9743 sites.reserve(close_batch_callsites_.size());
9744 for (const auto& row : close_batch_callsites_)
9745 if (row.second.active) sites.push_back(row.second);
9746 std::stable_sort(sites.begin(), sites.end(),
9747 [](const CloseCallsiteState& left, const CloseCallsiteState& right) {
9748 return left.queue_sequence < right.queue_sequence;
9749 });
9750
9751 std::vector<std::pair<std::uint64_t, double>> fifo_lots;
9752 if (const auto* pine = pine_view_of(&require_host())) {
9753 fifo_lots.reserve(pine->pyramid_entries_.size());
9754 for (const auto& lot : pine->pyramid_entries_)
9755 fifo_lots.emplace_back(lot.entry_incarnation, lot.qty);
9756 }
9757 std::size_t fifo_prefix_size = 0;
9758 // ab9714be pine_orders.cpp:29-70 source_fifo_prefix_membership, with its
9759 // accumulation and endpoint ordering; openings resolve to their receipts.
9760 const auto source_fifo_prefix_openings = [&](
9761 const std::vector<std::pair<std::uint64_t, double>>& lots,
9762 double qty_limit) -> std::optional<native_order::BindOpenings> {
9763 fifo_prefix_size = 0;
9764 if (current_position_cycle_ <= 0 || !std::isfinite(qty_limit) || qty_limit <= 0.0)
9765 return std::nullopt;
9766 double qty_closed = 0.0;
9767 std::size_t prefix_size = 0;
9768 for (const auto& lot : lots) {
9769 if (qty_closed >= qty_limit - internal::kQtyEpsilon) break;
9770 if (!std::isfinite(lot.second) || lot.second <= 0.0) return std::nullopt;
9771 const double close_qty = std::min(lot.second, qty_limit - qty_closed);
9772 if (lot.second - close_qty > internal::kQtyEpsilon) return std::nullopt;
9773 ++prefix_size;
9774 qty_closed += close_qty;
9775 }
9776 if (prefix_size == 0 || prefix_size == lots.size()) return std::nullopt;
9777 // The selection is resolved at submission, while the owner resolves
9778 // it at the fill. Keep the late-bound FIFO Reduce whenever it drains
9779 // exactly that prefix anyway; only a drain that would keep a
9780 // sub-epsilon fragment or take one from the next lot needs the
9781 // explicit whole-lot selection.
9782 {
9783 double closed = 0.0;
9784 double left = qty_limit;
9785 bool exact = true;
9786 for (std::size_t index = 0; exact && index < prefix_size; ++index) {
9787 const double amount = std::min(lots[index].second, left);
9788 exact = amount == lots[index].second;
9789 closed += amount;
9790 left = qty_limit - closed;
9791 }
9792 if (exact && left == 0.0) return std::nullopt;
9793 }
9794 native_order::BindOpenings selection{{}, current_position_cycle_};
9795 std::unordered_set<std::uint64_t> included;
9796 for (std::size_t index = 0; index < prefix_size; ++index) {
9797 const auto incarnation = lots[index].first;
9798 if (incarnation == 0) return std::nullopt;
9799 if (!included.insert(incarnation).second) continue;
9800 const native_order::RequestHandle* receipt = nullptr;
9801 for (const auto& cohort : cohorts_by_id_.held_rows()) {
9802 for (const auto& opened : cohort.second.opened)
9803 if (opened.incarnation == incarnation) receipt = &opened;
9804 }
9805 if (!receipt) return std::nullopt;
9806 selection.openings.push_back(*receipt);
9807 }
9808 // Every live fragment of a selected opening must belong to the
9809 // prefix; otherwise the owner settles the close as a Reduce.
9810 for (std::size_t index = prefix_size; index < lots.size(); ++index)
9811 if (included.count(lots[index].first) != 0) return std::nullopt;
9812 fifo_prefix_size = prefix_size;
9813 return selection;
9814 };
9815 // Settlement FIFO arithmetic of one queued close over the local lot view.
9816 const auto drain_source_fifo_lots = [&](
9817 std::vector<std::pair<std::uint64_t, double>>& lots, double qty) {
9818 if (fifo_prefix_size != 0) {
9819 lots.erase(lots.begin(), lots.begin()
9820 + static_cast<std::ptrdiff_t>(fifo_prefix_size));
9821 return;
9822 }
9823 double closed = 0.0;
9824 double left = qty;
9825 std::size_t consumed = 0;
9826 for (auto& lot : lots) {
9827 if (!(left > 0.0)) break;
9828 const double amount = std::min(lot.second, left);
9829 lot.second -= amount;
9830 closed += amount;
9831 left = qty - closed;
9832 if (lot.second == 0.0) ++consumed;
9833 }
9834 lots.erase(lots.begin(), lots.begin() + static_cast<std::ptrdiff_t>(consumed));
9835 // The host drops sub-epsilon survivors after every applied fill.
9836 lots.erase(std::remove_if(lots.begin(), lots.end(), [](const auto& lot) {
9837 return lot.second <= internal::kQtyEpsilon;
9838 }), lots.end());
9839 };
9840 // A carried short's follow-up owed to this close is booked after the script,
9841 // ahead of the close's fills (book_owed_close_follow_up): a close of the
9842 // whole book the script saw keeps that size, the transaction that crosses
9843 // zero on the smaller book and opens the excess long as its own lot
9844 // (switches close_follow_up_after_script and frozen_reversal_close; lab tv
9845 // tapes tests/fixtures/margin_open_rules lots/close-id-*: "Close entry(s)
9846 // order S"). Placed here it keeps its place ahead of the script's later
9847 // orders; where nothing is booked it closes the book exactly.
9848 bool frozen_close = false;
9849 if (PF_RARE(owed_follow_up_bar_ != std::numeric_limits<std::int64_t>::min())) {
9850 const auto point = detail::callback_point(require_host());
9851 const auto& schedule = detail::margin_schedule_switches();
9852 frozen_close = point && owed_follow_up_bar_ == point->decision.script_bar_open_ms
9853 && owed_follow_up_point_ == 3 && config_.process_orders_on_close
9854 && schedule.close_follow_up_after_script && schedule.frozen_reversal_close;
9855 }
9856 for (const auto& site : sites) {
9857 const auto physical = detail::run_position(require_host());
9858 // ab9714be pine_strategy_commands.cpp:694-696: strategy_close returns immediately when position is flat (<= kQtyEpsilon)
9859 if (std::abs(physical.signed_units) <= internal::kQtyEpsilon) continue;
9860 const double available = std::abs(physical.signed_units);
9861 const double target = std::min(site.target, available);
9862 // ab9714be pine_orders.cpp:344: close within kQtyEpsilon of held position executes Flatten action
9863 const bool closes_full = target >= available - internal::kQtyEpsilon;
9864 if (PF_RARE(frozen_close) && closes_full && fifo_lots.size() <= 1) {
9865 cancel_exit_orders_for_full_close(site.id);
9866 native_order::Request request;
9867 request.intent = native_order::Transact{-physical.signed_units};
9868 request.label = "__close__" + site.id;
9869 request.comment = site.comment;
9870 request.owner = native_order::Independent{};
9871 fifo_lots.clear();
9872 PlacementSnapshot snapshot;
9873 snapshot.family = PineOrderFamily::Close;
9874 snapshot.source_id = site.id;
9875 snapshot.comment = site.comment;
9876 snapshot.requested_qty = kNaN;
9877 snapshot.projection_remaining_qty = available;
9878 snapshot.qty_percent = 100.0;
9879 snapshot.is_long = false;
9880 snapshot.crosses_zero = true;
9881 snapshot.sizing = sizing_snapshot();
9882 snapshot.close_callsite_token = site.token;
9883 snapshot.close_batch_calls = static_cast<std::uint32_t>(site.calls);
9884 snapshot.close_first_id = site.first_id;
9885 const SourceId key = "__pine_close_flush__" + std::to_string(site.token);
9886 (void)submit_or_replace(std::move(request), std::move(snapshot), false, key);
9887 continue;
9888 }
9889 if (closes_full) {
9890 // The pass's entries on the closed side survive the close and open
9891 // after it, whichever call came first: entry() already refused the
9892 // ones over the cap (lane W8A-SIGSTATE-1 R2,
9893 // tests/fixtures/pooc_same_pass_close).
9894 cancel_exit_orders_for_full_close(site.id);
9895 }
9896
9897 // ab9714be pine_orders.cpp:29-70 / :340-347: a partial close whose
9898 // FIFO drain consumes only whole lots settles as a Flatten of exactly
9899 // that lot prefix. A binary64 Reduce of the same target can keep a
9900 // one-ULP fragment of the last lot, which moves every later binary64
9901 // availability/ledger target off the owner's arithmetic.
9902 std::optional<native_order::BindOpenings> prefix;
9903 fifo_prefix_size = 0;
9904 if (!closes_full) prefix = source_fifo_prefix_openings(fifo_lots, target);
9905
9906 // A partial target is the owner's binary64 ledger/availability
9907 // arithmetic (ab9714be pine_strategy_commands.cpp:1397-1408), which
9908 // execute_partial_exit_qty drains without re-quantizing. An off-grid
9909 // target cannot be an ExplicitUnits request, so resolve_terms books
9910 // it as explicit units at the fill instead.
9911 const bool off_grid = staged_.quantity_grid
9912 && !native_order::quantity_on_grid(target, *staged_.quantity_grid);
9913 native_order::Request request;
9914 request.intent = closes_full || prefix
9916 : off_grid
9918 native_order::HostSizedKind::Close, std::nullopt}}
9920 request.label = "__close__" + site.id;
9921 request.comment = site.comment;
9922 request.owner = native_order::Independent{};
9923 if (prefix) request.owner = std::move(*prefix);
9924 // Earlier sites of this flush settle first (queue order), so a later
9925 // site's prefix is judged against the lots that survive them.
9926 if (closes_full) fifo_lots.clear();
9927 else drain_source_fifo_lots(fifo_lots, target);
9928 PlacementSnapshot snapshot;
9929 snapshot.family = PineOrderFamily::Close;
9930 snapshot.source_id = site.id;
9931 snapshot.comment = site.comment;
9932 snapshot.requested_qty = closes_full ? kNaN : target;
9933 snapshot.projection_remaining_qty = target;
9934 snapshot.qty_percent = closes_full ? 100.0 : (target / available * 100.0);
9935 snapshot.is_long = false;
9936 snapshot.sizing = sizing_snapshot();
9937 snapshot.close_callsite_token = site.token;
9938 snapshot.close_batch_calls = static_cast<std::uint32_t>(site.calls);
9939 snapshot.close_first_id = site.first_id;
9940 const SourceId key = "__pine_close_flush__" + std::to_string(site.token);
9941 (void)submit_or_replace(
9942 std::move(request), std::move(snapshot), false, key);
9943 }
9944 close_batch_callsites_.clear();
9945 close_batch_bar_ = -1;
9946 close_batch_queue_sequence_ = 0;
9947 close_batch_pending_debt_ = 0.0;
9948 close_batch_admitted_total_ = 0.0;
9949}
9950
9951void PineExecutionAdapter::observe_close_ledger(
9953 const PlacementSnapshot* snapshot) {
9954 // The ledger only sizes the default FIFO close; under the ANY rule a
9955 // close is bound to its id's own entries.
9956 if (config_.close_entries_rule_any) return;
9957 // A fill books against the entry it names -- a close site's first call's
9958 // id, whichever id its last call sized the order for (tapes w3f02-x1,
9959 // -x2, w3bf02-a1, -a2, -b1); strategy.close(id, qty) and strategy.exit's
9960 // from_entry name theirs (w3bf02-f3, -f2) -- and what that id's ledger
9961 // cannot cover spills over the oldest entries'. A fill that names none (a
9962 // strategy.order sell, a margin call) takes the oldest entries' units
9963 // (w3bf02-f1).
9964 SourceId named{};
9965 if (snapshot) {
9966 switch (snapshot->family) {
9968 named = snapshot->close_first_id.empty() ? snapshot->source_id
9969 : snapshot->close_first_id;
9970 break;
9974 named = snapshot->from_entry;
9975 break;
9976 default:
9977 break;
9978 }
9979 }
9980 book_close_ledger(named, event.closed_units);
9981 // An exit leg armed for one opening of its id books that opening's record
9982 // first (bracket_origin; test_exit_bracket_pending_entry_leg's capped
9983 // entry: the re-issued T1 armed over the pending second L books it).
9984 const std::uint64_t armed_for = snapshot
9988 ? snapshot->bracket_origin.incarnation : 0;
9989 book_pyramiding_records(named, armed_for, event.closed_units);
9990}
9991
9992void PineExecutionAdapter::credit_close_ledger(const SourceId& id, double units) {
9993 close_logical_units_[id] += units;
9994 if (config_.close_entries_rule_any) return;
9995 if (!close_ledger_records_.empty() && close_ledger_records_.back().id == id)
9996 close_ledger_records_.back().units += units;
9997 else
9998 close_ledger_records_.push_back({id, units});
9999}
10000
10001void PineExecutionAdapter::book_close_ledger(const SourceId& id, double units) {
10002 if (!(units > 0.0) || close_ledger_records_.empty()) return;
10003 double left = units;
10004 const auto take = [&left](CloseLedgerRecord& record) {
10005 if (!(left > internal::kQtyEpsilon) || !(record.units > 0.0)) return;
10006 const double amount = std::min(record.units, left);
10007 record.units -= amount;
10008 left -= amount;
10009 };
10010 // The id's own entries first, oldest first; then, for what they cannot
10011 // cover, every id's entries in the order they filled.
10012 for (auto& record : close_ledger_records_)
10013 if (record.id == id) take(record);
10014 for (auto& record : close_ledger_records_) take(record);
10015 std::vector<CloseLedgerRecord> kept;
10016 kept.reserve(close_ledger_records_.size());
10017 for (auto& record : close_ledger_records_) {
10018 if (!(record.units > internal::kQtyEpsilon)) continue;
10019 if (!kept.empty() && kept.back().id == record.id)
10020 kept.back().units += record.units;
10021 else
10022 kept.push_back(std::move(record));
10023 }
10024 close_ledger_records_ = std::move(kept);
10025 close_logical_units_.clear();
10026 for (const auto& record : close_ledger_records_)
10027 close_logical_units_[record.id] += record.units;
10028}
10029
10030// The pyramiding cap (pyramiding_ledger_records): TradingView caps
10031// pyramiding on the open records of its close ledger -- one per entry fill
10032// whose units are not all booked yet, counted at the fill after the bar's
10033// earlier fills -- not on the physical lots: a strategy.close(id) books its
10034// id's own record while the book drains first in first out
10035// (tests/fixtures/admission_rules pyr1-records-below-lots,
10036// -pyr2-records-above-lots; pyramiding_count's P8 tape). Kept unmerged, one
10037// record per fill with the incarnation of the opening that filled it, and
10038// booked as the close ledger is -- an exit armed for one opening of its id
10039// books that opening's record first, then the named id's records oldest
10040// first, then every record in fill order. Only under pyramiding above one,
10041// where it can decide.
10042// Pyramiding above one, a close rule other than ANY and a pinned lot grid are
10043// ledger_records_configured_, formed once per configuration.
10044void PineExecutionAdapter::credit_pyramiding_record(const SourceId& id, std::uint64_t incarnation,
10045 double units) {
10046 if (!ledger_records_configured_ || !(units > 0.0)) return;
10047 pyramiding_records_.push_back({id, incarnation, units});
10048}
10049
10050void PineExecutionAdapter::book_pyramiding_records(const SourceId& id, std::uint64_t armed_for,
10051 double units) {
10052 if (!(units > 0.0) || pyramiding_records_.empty()) return;
10053 double left = units;
10054 const auto take = [&left](PyramidingRecord& record) {
10055 if (!(left > internal::kQtyEpsilon) || !(record.units > 0.0)) return;
10056 const double amount = std::min(record.units, left);
10057 record.units -= amount;
10058 left -= amount;
10059 };
10060 if (armed_for != 0) {
10061 for (auto& record : pyramiding_records_)
10062 if (record.incarnation == armed_for && record.id == id) take(record);
10063 }
10064 for (auto& record : pyramiding_records_)
10065 if (record.id == id) take(record);
10066 for (auto& record : pyramiding_records_) take(record);
10067 pyramiding_records_.erase(
10068 std::remove_if(pyramiding_records_.begin(), pyramiding_records_.end(),
10069 [](const PyramidingRecord& record) {
10070 return !(record.units > internal::kQtyEpsilon);
10071 }),
10072 pyramiding_records_.end());
10073}
10074
10075// The open close-ledger records the pyramiding cap counts, or nullopt where
10076// the rule does not decide: its switch off, pyramiding of one or less, the
10077// ANY close rule, or a ledger out of step with the physical book.
10078[[gnu::always_inline]] inline std::optional<std::size_t>
10079PineExecutionAdapter::open_ledger_records() const {
10080 if (!ledger_records_configured_ || !detail::margin_rule_switches().pyramiding_ledger_records)
10081 return std::nullopt;
10082 return held_ledger_records();
10083}
10084
10085// The records against the physical book, inside the rule's configuration.
10086[[gnu::noinline]]
10087std::optional<std::size_t> PineExecutionAdapter::held_ledger_records() const {
10088 const double held = std::abs(detail::run_position(require_host()).signed_units);
10089 double units = 0.0;
10090 for (const auto& record : pyramiding_records_) units += record.units;
10091 if (!(std::abs(units - held) <= std::max(1e-9, held * 1e-12))) return std::nullopt;
10092 return pyramiding_records_.size();
10093}
10094
10095// The incarnation of the newest opening of `from_entry` this cycle, 0 when
10096// it has none: the entry incarnation an exit's tombstone is keyed on.
10097std::uint64_t PineExecutionAdapter::entry_incarnation_of(const SourceId& from_entry) const {
10098 const auto cohort = cohorts_by_id_.find(from_entry);
10099 if (cohort == cohorts_by_id_.end() || cohort->second.opened.empty()) return 0;
10100 return cohort->second.opened.back().incarnation;
10101}
10102
10103// Out of line and cold: a narrow scope's body, kept off the per-bar
10104// path, which is front-end bound (CMakeLists.txt, -falign-functions).
10105[[gnu::noinline, gnu::cold]]
10106bool PineExecutionAdapter::exit_tombstoned(const SourceId& exit_id,
10107 const SourceId& from_entry) const {
10108 if (!detail::margin_rule_switches().exit_child_tombstones || from_entry.empty()
10109 || !pinned_lot_grid()) {
10110 return false;
10111 }
10112 const auto found = exit_tombstones_.find(exit_id + "\x1f" + from_entry);
10113 return found != exit_tombstones_.end() && found->second != 0
10114 && found->second == entry_incarnation_of(from_entry);
10115}
10116
10117// Records the close-first fact entry() reads: on this script bar the script
10118// closed the whole open position -- strategy.close(<held id>) or
10119// strategy.close_all(), not a partial or an `immediately` close -- while it
10120// was open. An entry the same bar then places against that position is a
10121// close-first entry (MarginRuleSwitches::close_first_admission).
10122void PineExecutionAdapter::record_close_first(bool whole_position) {
10123 if (!whole_position) return;
10124 const auto point = detail::callback_point(require_host());
10125 if (!point) return;
10126 const double held = detail::run_position(require_host()).signed_units;
10127 if (held == 0.0) return;
10128 close_first_script_bar_ = point->decision.script_bar_open_ms;
10129 close_first_side_ = held > 0.0 ? 1 : -1;
10130}
10131
10132void PineExecutionAdapter::close(const SourceId& id, const std::string& comment, double qty,
10133 double qty_percent, bool immediately, std::uint64_t callsite_token) {
10134 if (intraday_loss_orders_blocked()) return;
10135 if (const auto point = detail::callback_point(require_host());
10136 point && cap_placement_denied(point->decision)) {
10137 return;
10138 }
10139 require_cohort_ticket();
10140 record_close_first(!immediately && !std::isfinite(qty)
10141 && (!std::isfinite(qty_percent) || qty_percent >= 100.0)
10142 && (id.empty() || open_lot_of(id)));
10143 reroute_fixed_entries_before_request();
10144 // The calc_on_order_fills recalculation of a fill inside a leg has no
10145 // current price to close at: TradingView fills its `immediately` close
10146 // where the same close placed without it fills, at the end of that leg
10147 // (lab tv tapes tests/fixtures/coof_immediate_close td-m1a, -c and -d
10148 // are one trade list; lane TAIL-D). A fill on a point of the path keeps
10149 // the close at that point.
10150 if (immediately && coof_recalc_active_ && !coof_first_open_
10151 && !coof_fill_on_path_point()) {
10152 immediately = false;
10153 }
10154 // The public empty-id spelling is the source route's full-position
10155 // strategy.close form. It is not a cohort lookup (there is no empty
10156 // entry-id cohort), and it retains its caller-supplied report comment.
10157 if (id.empty()) {
10158 // ab9714be pine_strategy_commands.cpp:694-696: strategy_close returns immediately when physical position is flat
10159 if (detail::run_position(require_host()).signed_units == 0.0) return;
10160 const std::uint64_t command_ordinal = ++command_ordinal_;
10161 if (const auto point = detail::callback_point(require_host())) {
10162 close_all_pending_script_bar_ = point->decision.script_bar_open_ms;
10163 }
10164 bool empty_entry = false;
10165 bool opposite_entry = false;
10166 bool empty_is_long = false;
10167 for (const auto& pending : pending_same_bar_commands_) {
10168 if (!pending.opening || pending.snapshot.family != PineOrderFamily::Entry) continue;
10169 if (pending.snapshot.source_id.empty()) {
10170 empty_entry = true;
10171 empty_is_long = pending.snapshot.is_long;
10172 }
10173 }
10174 if (empty_entry) {
10175 for (const auto& pending : pending_same_bar_commands_) {
10176 if (!pending.opening || pending.snapshot.family != PineOrderFamily::Entry) continue;
10177 if (pending.snapshot.is_long != empty_is_long) {
10178 opposite_entry = true;
10179 break;
10180 }
10181 }
10182 }
10183 // A close_all placed after the same bar's default-size reversal entry
10184 // at 100 % of equity belongs to that reversal, as a strategy.close()
10185 // of the held id does (opposite_reversal_pair): TradingView flattens
10186 // the held position through neither. The next open funds the
10187 // reversal, which closes the position and opens the other side, or
10188 // declines it (declined_reversal_at_open) and the position is held;
10189 // the close_all fills in neither case (lab tv tapes
10190 // tests/fixtures/reversal_close_all int28fix-ca2-rev and -frosty;
10191 // The reversal-close rule, on a fractional lot grid; a whole-unit grid
10192 // is not measured). A close_all placed before the entry flattens at
10193 // the open (-first).
10194 bool all_in_reversal = false;
10195 if (!empty_entry && !config_.process_orders_on_close && !config_.calc_on_order_fills
10196 && config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
10197 && config_.default_qty_value >= 100.0 - 1e-9
10198 && (!staged_.quantity_grid || *staged_.quantity_grid < 1.0)) {
10199 const bool held_long = detail::run_position(require_host()).signed_units > 0.0;
10200 const auto point = detail::callback_point(require_host());
10201 const auto reverses = [&](const PlacementSnapshot& candidate) {
10202 return candidate.opening && candidate.family == PineOrderFamily::Entry
10203 && candidate.is_long != held_long && std::isnan(candidate.requested_qty)
10204 && (!point || candidate.placement_script_open_ms
10205 == point->decision.script_bar_open_ms);
10206 };
10207 for (const auto& pending : pending_same_bar_commands_)
10208 all_in_reversal = all_in_reversal || reverses(pending.snapshot);
10209 for (const auto& pending : pending_entries_)
10210 all_in_reversal = all_in_reversal || reverses(pending.snapshot);
10211 for (const auto& handle : live_handles_) {
10212 const auto found = placement_.find(handle.incarnation);
10213 if (found != placement_.end())
10214 all_in_reversal = all_in_reversal || reverses(found->second);
10215 }
10216 }
10217 if ((empty_entry && opposite_entry) || all_in_reversal) {
10218 SourceShadowPending shadow;
10219 shadow.snapshot.family = PineOrderFamily::CloseAll;
10220 shadow.snapshot.source_id = "__pine_close_all";
10221 shadow.snapshot.sizing = sizing_snapshot();
10222 shadow.label = shadow.snapshot.source_id;
10223 pending_write(source_shadow_pending_).push_back(std::move(shadow));
10224 return;
10225 }
10226 native_order::Request request;
10227 request.intent = native_order::Flatten{};
10228 request.label = "__pine_close_all";
10229 request.comment = comment;
10230 // Under process_orders_on_close TradingView checks the book at the
10231 // close after the script, and executes a call there ahead of the
10232 // orders the script placed at that close: a close_all keeps the size
10233 // it was placed with and opens the called unit on the other side
10234 // (w5b-sz-eur-p100-10m; synthetic OANDA:EURUSD close for 6 units, then a
10235 // 2-unit money call at the close, leaves 4 held and opens a short of
10236 // 2 when that unchanged close executes). A call the
10237 // path fired earlier stands before the script, and the close closes
10238 // what it left.
10239 bool close_sized_before_call = false;
10240 if (config_.process_orders_on_close && !immediately && !config_.calc_on_order_fills
10241 && !coof_recalc_active_ && policy_script_bar_valid_) {
10242 if (const auto point = detail::callback_point(require_host())) {
10243 const double held = detail::run_position(require_host()).signed_units;
10244 if (held != 0.0 && last_margin_call_at_script_close_
10245 && last_margin_call_script_bar_ == point->decision.script_bar_open_ms
10246 && last_margin_call_position_cycle_ == current_position_cycle_) {
10247 request.intent = native_order::Transact{
10248 -(held + (held > 0.0 ? 1.0 : -1.0) * last_margin_call_closed_units_)};
10249 close_sized_before_call = true;
10250 }
10251 }
10252 }
10253 double coof_close_all_fill = kNaN;
10254 double coof_close_all_target = kNaN;
10255 if (coof_recalc_active_ && !coof_first_open_ && !immediately
10256 && coof_script_bar_valid_) {
10257 int next_extreme_index = -1;
10258 const double next_extreme = coof_next_waypoint(&next_extreme_index);
10259 coof_close_all_target = next_extreme;
10260 const auto point = detail::callback_point(require_host());
10261 const double current_quote = point ? point->price : kNaN;
10262 const bool buy = detail::run_position(require_host()).signed_units < 0.0;
10263 coof_close_all_fill = source_bar_fill_tick(
10264 next_extreme, staged_.syminfo.mintick)
10265 + (buy ? 1.0 : -1.0) * config_.slippage
10266 * staged_.syminfo.mintick;
10267 if (finite_positive(coof_close_all_fill)
10268 && finite_positive(current_quote)
10269 && !source_same_point(current_quote, coof_close_all_fill,
10270 staged_.syminfo.mintick)) {
10271 // As the close of one id does: the trigger rests at the raw
10272 // extreme the path reaches, and the booked tick rides
10273 // forced_execution_price. An OFF-grid extreme (H 9.445 booked
10274 // 9.45) is otherwise never touched on this bar (lab tv tape
10275 // tests/fixtures/coof_immediate_close td-m1h on NYSE:F; lane
10276 // TAIL-D).
10277 const bool extreme_target = next_extreme_index == 1
10278 || next_extreme_index == 2;
10279 const double level = extreme_target ? next_extreme : coof_close_all_fill;
10280 const native_order::Limit limit{level,
10281 extreme_target && coof_close_all_fill != level};
10282 const bool falling = coof_close_all_fill < current_quote;
10283 if (buy) {
10284 request.trigger = falling
10285 ? native_order::Trigger{limit}
10287 } else {
10288 request.trigger = falling
10290 : native_order::Trigger{limit};
10291 }
10292 }
10293 }
10294 PlacementSnapshot snapshot;
10295 snapshot.family = PineOrderFamily::CloseAll;
10296 snapshot.source_id = request.label;
10297 snapshot.comment = comment;
10298 snapshot.command_ordinal = command_ordinal;
10299 snapshot.immediately = immediately;
10300 snapshot.sizing = sizing_snapshot();
10301 if (immediately && config_.slippage > 0 && coof_recalc_active_
10302 && coof_first_open_ && coof_script_bar_valid_) {
10303 const auto callback_state = detail::run_state(require_host());
10304 if ((!callback_state.spec || callback_state.spec->intrabar.is_none()) && detail::callback_point(require_host()) && detail::callback_point(require_host())->decision.coordinate.path_phase == NativePathPhase::Open) {
10305 const bool close_is_buy = detail::run_position(require_host()).signed_units < 0.0;
10306 coof_close_all_fill = source_bar_fill_tick(
10307 coof_script_bar_.open, staged_.syminfo.mintick)
10308 + (close_is_buy ? 1.0 : -1.0) * config_.slippage * staged_.syminfo.mintick;
10309 }
10310 }
10311 snapshot.forced_execution_price = coof_close_all_fill;
10312 snapshot.forced_waypoint_print = coof_close_all_target;
10313 snapshot.crosses_zero = close_sized_before_call;
10314 (void)qty;
10315 (void)qty_percent;
10316 (void)callsite_token;
10317 const auto accepted = submit_or_replace(
10318 std::move(request), std::move(snapshot), false, "__pine_close_all");
10319 // ab9714be pine_strategy_commands.cpp:789-801: an ordinary POOC
10320 // close_all fills at the call (execute_immediate_close), ahead of any
10321 // same-bar market entry the script placed before it; an under-cap add
10322 // issued earlier in the pass therefore survives the flat
10323 // (pine_strategy_commands.cpp:2437-2446) instead of being flattened
10324 // with the book. The script keeps the pre-close position view.
10325 const bool pooc_ordinary_close_all = config_.process_orders_on_close
10326 && !immediately && !config_.calc_on_order_fills && !coof_recalc_active_;
10327 if (pooc_ordinary_close_all && accepted) {
10328 if (auto* pine_host = pine_view_of(&require_host()))
10329 pine_host->freeze_script_position_view();
10330 }
10331 if ((immediately || pooc_ordinary_close_all) && accepted) {
10332 (void)require_host().execute_current(
10333 {*accepted, NativeCurrentPriceRule::NearestTick});
10334 }
10335 if (!accepted || config_.process_orders_on_close) return;
10336 const auto close = placement_.find(accepted->incarnation);
10337 if (close == placement_.end()) return;
10338 const auto side = static_cast<PositionSide>(close->second.projection_position_side);
10339 for (const auto& handle : live_handles_) {
10340 if (handle == *accepted) continue;
10341 const auto found = placement_.find(handle.incarnation);
10342 if (found == placement_.end()) continue;
10343 auto& pending = found->second;
10344 const bool pure_prior_stop = pending.opening
10345 && pending.family == PineOrderFamily::Entry
10346 && finite_positive(pending.exit_levels.stop)
10347 && !finite_positive(pending.exit_levels.limit)
10348 && !finite_positive(pending.exit_levels.trail_points)
10349 && !finite_positive(pending.exit_levels.trail_price)
10350 && !finite_positive(pending.exit_levels.trail_offset)
10351 && !pending.stop_limit_activated
10352 && pending.projection_created_bar < close->second.projection_created_bar
10353 && pending.projection_position_side == static_cast<std::int32_t>(side)
10354 && pending.is_long == (side == PositionSide::LONG)
10355 && !pending.projection_over_pyramiding;
10356 const auto* pine_host = pine_view_of(&require_host());
10357 const bool has_physical_id = pine_host
10358 && pine_host->adapter_has_open_entry_id(pending.source_id);
10359 if (!pure_prior_stop || !has_physical_id) continue;
10360 pending.preserved_by_close_all = *accepted; invalidate_retirement();
10361 pending.preserved_close_all_bar = close->second.projection_created_bar;
10362 }
10363 const double cur_pos = detail::run_position(require_host()).signed_units;
10364 const auto cur_pt = detail::callback_point(require_host());
10365 if (cur_pos != 0.0 && cur_pt) {
10366 std::vector<native_order::RequestHandle> opposite_entries;
10367 for (const auto& handle : live_handles_) {
10368 if (handle == *accepted) continue;
10369 const auto found = placement_.find(handle.incarnation);
10370 if (found == placement_.end()) continue;
10371 const auto& pend = found->second;
10372 const bool unpriced_market = !finite_positive(pend.exit_levels.limit)
10373 && !finite_positive(pend.exit_levels.stop)
10374 && !finite_positive(pend.exit_levels.trail_points)
10375 && !finite_positive(pend.exit_levels.trail_price)
10376 && !finite_positive(pend.exit_levels.trail_offset);
10377 if (pend.opening && pend.family == PineOrderFamily::Entry
10378 && unpriced_market
10379 && !pend.projection_over_pyramiding
10380 && pend.placement_script_open_ms == cur_pt->decision.script_bar_open_ms) {
10381 opposite_entries.push_back(handle);
10382 }
10383 }
10384 for (const auto& handle : opposite_entries) {
10385 const auto found = placement_.find(handle.incarnation);
10386 if (found == placement_.end()) continue;
10387 PlacementSnapshot entry_snapshot = found->second;
10388 // ab9714be pine_fills.cpp:3848-3856: full market close processes before opposite entry so entry executes after close
10389 entry_snapshot.paired_reversal_parent = *accepted;
10390 entry_snapshot.market_admission = {};
10391 const auto result = require_host().cancel(handle);
10392 if (result.status == native_order::CancelStatus::Cancelled) {
10393 retire(handle);
10395 const double target = entry_snapshot.is_long
10396 ? entry_snapshot.requested_qty : -entry_snapshot.requested_qty;
10397 req.intent = std::isnan(target)
10398 ? native_order::OrderIntent{native_order::HostSized{native_order::HostSizedKind::Open,
10399 entry_snapshot.is_long ? native_order::Side::Long : native_order::Side::Short}}
10401 req.label = entry_snapshot.source_id;
10402 req.comment = entry_snapshot.comment;
10403 const SourceId replacement_key = entry_snapshot.source_id;
10404 pending_write(pending_entries_).push_back({std::move(req), std::move(entry_snapshot), replacement_key});
10405#ifdef PINEFORGE_CR277_DIAGNOSTIC
10406 cr277_pending_close_pushes[0].fetch_add(1, std::memory_order_relaxed);
10407#endif
10408 }
10409 }
10410 for (auto it = pending_same_bar_commands_.begin(); it != pending_same_bar_commands_.end();) {
10411 if (it->opening && it->snapshot.family == PineOrderFamily::Entry
10412 && !it->snapshot.projection_over_pyramiding
10413 && it->snapshot.placement_script_open_ms == cur_pt->decision.script_bar_open_ms) {
10414 it->snapshot.paired_reversal_parent = *accepted;
10415 it->snapshot.market_admission = {};
10416 pending_write(pending_entries_).push_back({std::move(it->request), std::move(it->snapshot), it->replacement_key});
10417 it = pending_write(pending_same_bar_commands_).erase(it);
10418 } else {
10419 ++it;
10420 }
10421 }
10422 }
10423 return;
10424 }
10425 const auto openings = openings_for(id);
10426 // A default FIFO close sizes from the id's close ledger: the units
10427 // entered under the id that no fill has booked, whether or not the FIFO
10428 // rule has already closed the lots that carried it (tapes w3f02-g1,
10429 // w3bf02-d1, -d2, -f2).
10430 const bool ledger_sized = !config_.close_entries_rule_any && !immediately
10431 && std::isnan(qty) && std::isnan(qty_percent);
10432 // The ledger is a running sum of fills: where it differs from the id's own
10433 // live lots only by rounding, the lots are the quantity, so the close
10434 // leaves no dust behind them.
10435 const double ledger_units = [&] {
10436 if (!ledger_sized) return 0.0;
10437 const double ledger = fixture_close_logical_units(id);
10438 const double own = cohort_exposure_for(id);
10439 return own > 0.0 && std::abs(ledger - own) <= internal::kQtyEpsilon ? own : ledger;
10440 }();
10441 // P-DA3: strategy.close against an empty cohort is dropped at the command.
10442 if (openings.empty() && !(ledger_units > 0.0)) {
10443 record_dropped_close(id, comment, qty, qty_percent, immediately, callsite_token);
10444 return;
10445 }
10446 const bool has_pending_entry = std::any_of(
10447 pending_entries_.begin(), pending_entries_.end(),
10448 [&](const PendingEntry& pe) { return pe.snapshot.source_id == id; })
10449 || std::any_of(
10450 pending_same_bar_commands_.begin(), pending_same_bar_commands_.end(),
10451 [&](const PendingSameBarCommand& pc) { return pc.snapshot.source_id == id; });
10452 // ab9714be pine_strategy_commands.cpp:2254-2256: compute_close_target_qty drops close when target unclosed quantity is zero
10453 const double closable = ledger_sized ? ledger_units : cohort_exposure_for(id);
10454 if (!config_.process_orders_on_close && !id.empty() && !(closable > 0.0)
10455 && !has_pending_entry) {
10456 record_dropped_close(id, comment, qty, qty_percent, immediately, callsite_token);
10457 return;
10458 }
10459 const std::uint64_t command_ordinal = ++command_ordinal_;
10460 // A same-side fixed market add is tentatively held until a later
10461 // strategy.exit can contribute its priced legs to the source-priority
10462 // batch. A named strategy.close terminates that command shape instead:
10463 // publish the already-issued add before placing the close, exactly as the
10464 // legacy broker book did. Leaving it staged would make the public
10465 // command-boundary projection lose one of the two surviving rows and
10466 // would incorrectly make the close race an unsubmitted add.
10467 if (!config_.calc_on_order_fills && !config_.process_orders_on_close
10468 && !pending_entries_.empty()) {
10469 const double live = detail::run_position(require_host()).signed_units;
10470 const bool has_opposite = std::any_of(
10471 pending_entries_.begin(), pending_entries_.end(), [&](const PendingEntry& entry) {
10472 return live != 0.0 && entry.snapshot.is_long != (live > 0.0);
10473 });
10474 const bool close_first_percent_add =
10475 config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
10476 && std::abs(config_.default_qty_value - 100.0) < 1e-12
10477 && std::all_of(pending_entries_.begin(), pending_entries_.end(),
10478 [](const PendingEntry& entry) {
10479 return entry.snapshot.family == PineOrderFamily::Entry
10480 && !std::isfinite(entry.snapshot.requested_qty)
10481 && !finite_positive(entry.snapshot.exit_levels.limit)
10482 && !finite_positive(entry.snapshot.exit_levels.stop);
10483 });
10484 if (!has_opposite && !close_first_percent_add) flush_pending_entries();
10485 }
10486 double requested_percent = std::isnan(qty_percent) ? 100.0 : qty_percent;
10487 double effective_qty = qty;
10488 const double current = detail::run_position(require_host()).signed_units;
10489 if (config_.close_entries_rule_any && !immediately && std::isfinite(qty)) {
10490 const double matching = cohort_exposure_for(id);
10491 requested_percent = matching > 1e-10
10492 ? std::clamp(std::abs(qty) / matching * 100.0, 0.0, 100.0)
10493 : 100.0;
10494 // ab9714be:pine_strategy_commands.cpp:2522-2590. An ANY close keeps
10495 // the already-resolved claim as a percentage-bound deferred order;
10496 // its public pending qty remains NaN and the live cohort is resolved
10497 // at the eventual candidate.
10498 effective_qty = kNaN;
10499 }
10500 const bool default_fifo_close = !config_.close_entries_rule_any
10501 && std::isnan(qty) && std::isnan(qty_percent);
10502 bool paired_reversal_close = false;
10503 bool paired_reversal_whole_drop = false;
10504 std::optional<native_order::RequestHandle> paired_reversal_parent;
10505 if (default_fifo_close && current != 0.0) {
10506 const auto point = detail::callback_point(require_host());
10507 for (const auto& handle : live_handles_) {
10508 const auto pending = placement_.find(handle.incarnation);
10509 if (pending == placement_.end()) continue;
10510 const auto& row = pending->second;
10511 if (row.opening && row.family == PineOrderFamily::Entry
10512 && row.is_long != (current > 0.0)
10513 && (!point || row.placement_script_open_ms
10514 == point->decision.script_bar_open_ms)) {
10515 paired_reversal_close = true;
10516 paired_reversal_parent = handle;
10517 // A reversal the unified placement half refused at its
10518 // signal does not take the close with it (entry()).
10519 if (row.signal_price_refused) {
10520 paired_reversal_whole_drop = true;
10521 break;
10522 }
10523 const bool rule5_scope =
10524 config_.default_qty_type
10525 == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
10526 && std::abs(config_.default_qty_value - 100.0) < 1e-12
10527 && staged_.quantity_grid && *staged_.quantity_grid > 0.0
10528 && *staged_.quantity_grid < 1.0
10529 && staged_.syminfo.pointvalue == 1.0
10530 && row.sizing.fx == 1.0
10531 && staged_.account_fx_effective_from_ms.empty()
10532 && config_.commission_value == 0.0 && config_.slippage == 0
10533 && !config_.process_orders_on_close
10534 && !config_.calc_on_order_fills
10535 && finite_positive(row.sizing.frozen_units)
10536 && finite_positive(row.sizing.equity)
10537 && finite_positive(row.sizing.price);
10538 if (rule5_scope) {
10539 const double notional_per_price = row.sizing.frozen_units
10540 * staged_.syminfo.pointvalue * row.sizing.fx;
10541 const double rounded_cost = source_money_round(
10542 notional_per_price * row.sizing.price);
10543 const double affordable_price = source_money_round(
10544 source_money_round(row.sizing.equity)
10545 / notional_per_price);
10546 paired_reversal_whole_drop =
10547 row.sizing.equity + 1e-9 >= rounded_cost
10548 && std::isfinite(affordable_price)
10549 && affordable_price < row.sizing.price;
10550 }
10551 break;
10552 }
10553 }
10554 }
10555 if (immediately) {
10556 const double current = detail::run_position(require_host()).signed_units;
10557 if (current != 0.0) {
10558 pending_write(pending_entries_).erase(std::remove_if(pending_entries_.begin(), pending_entries_.end(),
10559 [&](const PendingEntry& entry) {
10560 return (current > 0.0) == entry.snapshot.is_long;
10561 }), pending_entries_.end());
10562 }
10563 }
10564 // P-DA2: an explicitly percentage-sized deferred ANY close retains
10565 // HostSized and resolves from the live selected cohort at its candidate.
10566 // FIFO's logical id ledger retains its command-time claim; so does the
10567 // separately specified same-bar market transaction artifact.
10568 const bool frozen_same_bar_close = same_bar_market_tx_scope() && !immediately
10569 && current != 0.0;
10570 const bool deferred_percentage = config_.close_entries_rule_any
10571 && std::isnan(effective_qty) && std::isfinite(requested_percent)
10572 && !immediately;
10573 const bool pooc_close_basis = config_.process_orders_on_close;
10574 double frozen_qty = effective_qty;
10575 if (std::isnan(effective_qty) && (!deferred_percentage || immediately || frozen_same_bar_close
10576 || pooc_close_basis)) {
10577 const auto point = detail::callback_point(require_host());
10578 const std::int64_t bar_key = point ? point->decision.script_bar_open_ms
10579 : require_host().native_decision_floor();
10580 const double fallback_basis = std::abs(detail::run_position(require_host()).signed_units);
10581 const bool ledger_basis = ledger_sized && !config_.process_orders_on_close;
10582 const double source_basis = ledger_basis
10583 ? std::min(ledger_units, fallback_basis) : cohort_exposure_for(id);
10584 const double script_basis = source_basis > 0.0 ? source_basis : fallback_basis;
10585 if (pooc_close_basis_count_ == 0 || bar_key != pooc_close_basis_last_bar_) {
10587 fold.h = pooc_close_basis_digest_;
10588 fold.i(bar_key);
10589 fold.d(script_basis);
10590 pooc_close_basis_digest_ = fold.h;
10591 pooc_close_basis_last_bar_ = bar_key;
10592 ++pooc_close_basis_count_;
10593 }
10594 frozen_qty = quantize_close_units(script_basis, requested_percent);
10595 }
10596 const double close_basis = ledger_sized && !config_.process_orders_on_close
10597 && ledger_units > 0.0
10598 ? std::min(ledger_units, std::abs(current))
10599 : cohort_exposure_for(id) > 0.0 ? cohort_exposure_for(id) : std::abs(current);
10600 const bool closes_full_position = close_basis > 0.0
10601 && ((std::isfinite(frozen_qty) && frozen_qty >= close_basis - 1e-10)
10602 || (std::isnan(effective_qty) && requested_percent >= 100.0 - 1e-9));
10603 bool reversal_pair = false;
10604 if (closes_full_position && current != 0.0) {
10605 const auto opposite_entry = [&](const PlacementSnapshot& candidate) {
10606 return candidate.opening && candidate.family == PineOrderFamily::Entry
10607 && candidate.is_long != (current > 0.0)
10608 && (!finite_positive(candidate.exit_levels.limit)
10609 && !finite_positive(candidate.exit_levels.stop));
10610 };
10611 for (const auto& pending : pending_same_bar_commands_)
10612 reversal_pair = reversal_pair || opposite_entry(pending.snapshot);
10613 for (const auto& handle : live_handles_) {
10614 const auto found = placement_.find(handle.incarnation);
10615 if (found != placement_.end())
10616 reversal_pair = reversal_pair || opposite_entry(found->second);
10617 }
10618 }
10619 const bool batched_pooc_fifo = default_fifo_close
10620 && !immediately && config_.process_orders_on_close
10621 && !config_.calc_on_order_fills
10622 && !coof_recalc_active_ && !cap.active()
10623 && !reversal_pair;
10624 if (batched_pooc_fifo) {
10625 const bool enqueued = enqueue_pooc_fifo_close(
10626 id, comment, callsite_token, command_ordinal);
10627 if (enqueued && closes_full_position)
10628 cancel_exit_orders_for_full_close(id);
10629 return;
10630 }
10631 if (closes_full_position && !reversal_pair) {
10632 cancel_exit_orders_for_full_close(id);
10633 }
10634 // A partial source close breaks the exact ShortSeed transaction book.
10635 // Its legacy effect is to leave the two frozen reversal commands on the
10636 // ordinary broker pass; the stale close itself owns no surviving broker
10637 // object. Detect that complete paired batch by source facts rather than
10638 // a generic id heuristic.
10639 if (same_bar_market_tx_scope() && !immediately && current != 0.0
10640 && (!std::isnan(qty) || !std::isnan(qty_percent))) {
10641 const bool variable_default = config_.default_qty_type
10642 != static_cast<int>(QtyType::FIXED);
10643 if (variable_default) {
10644 bool held_reentry = false;
10645 bool opposite_reversal = false;
10646 const bool held_long = current > 0.0;
10647 for (const auto& pending : pending_same_bar_commands_) {
10648 if (!pending.opening || pending.snapshot.family != PineOrderFamily::Entry) continue;
10649 if (pending.snapshot.source_id == id && pending.snapshot.is_long == held_long)
10650 held_reentry = true;
10651 if (pending.snapshot.is_long != held_long) opposite_reversal = true;
10652 }
10653 if (held_reentry && opposite_reversal) {
10654 // The partial close has no executable artifact in the source
10655 // transaction pass. Retain an accepted, dormant source
10656 // handle so the ShortSeed role projection remains truthful;
10657 // its unreachable buy limit prevents it from changing the
10658 // ordinary two-reversal outcome.
10659 if (short_seed_context_is_live() && current < 0.0) {
10660 native_order::Request placeholder;
10661 placeholder.intent = native_order::HostSized{
10662 native_order::HostSizedKind::Close, std::nullopt};
10663 placeholder.label = "__close__" + id;
10664 placeholder.trigger = native_order::Limit{
10665 std::numeric_limits<double>::min()};
10666 placeholder.owner = owner_for_close(id, true);
10667 PlacementSnapshot placeholder_snapshot;
10668 placeholder_snapshot.family = PineOrderFamily::Close;
10669 placeholder_snapshot.source_id = id;
10670 placeholder_snapshot.from_entry = id;
10671 placeholder_snapshot.requested_qty = frozen_qty;
10672 placeholder_snapshot.qty_percent = requested_percent;
10673 placeholder_snapshot.is_long = false;
10674 placeholder_snapshot.deferred_cohort = true;
10675 placeholder_snapshot.command_ordinal = command_ordinal;
10676 placeholder_snapshot.sizing = sizing_snapshot();
10677 (void)submit_or_replace(std::move(placeholder), std::move(placeholder_snapshot),
10678 false, "__short_seed_partial_hold__" + id);
10679 }
10680 return;
10681 }
10682 }
10683 }
10684 if (same_bar_market_tx_scope() && !immediately && id.size() != 0
10685 && std::isnan(qty) && std::isnan(qty_percent) && current != 0.0
10686 && finite_positive(frozen_qty)) {
10687 native_order::Request request;
10688 request.label = "__close__" + id;
10689 request.comment = comment;
10690 PlacementSnapshot snapshot;
10691 snapshot.family = PineOrderFamily::Close;
10692 snapshot.source_id = id;
10693 snapshot.from_entry = id;
10694 snapshot.comment = comment;
10695 snapshot.requested_qty = frozen_qty;
10696 snapshot.qty_percent = requested_percent;
10697 snapshot.command_ordinal = command_ordinal;
10698 snapshot.is_long = false;
10699 snapshot.frozen_market_instruction = true;
10700 snapshot.frozen_market_transaction_units = frozen_qty;
10701 snapshot.frozen_market_targeted_close = true;
10702 snapshot.frozen_market_target_was_long = current > 0.0;
10703 snapshot.birth = capture_order_birth();
10704 snapshot.sizing = sizing_snapshot();
10705 // ab9714be pine_orders.cpp:599-601: exit/close orders bind owner to active position_cycle_seq_
10706 snapshot.placement_cycle = current_position_cycle_;
10707 if (const auto point = detail::callback_point(require_host())) {
10708 snapshot.placement_script_open_ms = point->decision.script_bar_open_ms;
10709 snapshot.placement_sub_open_ms = point->decision.sub_bar_open_ms;
10710 }
10711 const SourceId replacement_key = callsite_token == 0
10712 ? SourceId{}
10713 : "__pine_close_site__" + std::to_string(callsite_token);
10714 pending_write(pending_same_bar_commands_).push_back(
10715 {std::move(request), std::move(snapshot), replacement_key, false});
10716 pending_same_bar_close_qty_ += frozen_qty;
10717 return;
10718 }
10719 // P-DA4: immediate closes keep their command-time cohort. Only an
10720 // ambiguous FIFO boundary uses HostSized to re-read the physical roster
10721 // at fill time; the Pine mirror can hide a <= kQtyEpsilon native fragment.
10722 // Preserve literal Reduce requests for every non-ambiguous FIFO close.
10723 const auto* close_pine_view = pine_view_of(host_);
10724 std::vector<double> preceding_fifo_closes;
10725 if (default_fifo_close && !immediately && close_pine_view
10726 && close_pine_view->pyramid_entries_.size() > 1U) {
10727 const auto point = detail::callback_point(require_host());
10728 for (const auto& handle : live_handles_) {
10729 const auto found = placement_.find(handle.incarnation);
10730 if (found == placement_.end()) continue;
10731 const auto& pending = found->second;
10732 if (pending.family != PineOrderFamily::Close || pending.immediately
10733 || !pending.from_entry.empty()
10734 || pending.frozen_market_instruction
10735 || !point || pending.placement_script_open_ms
10736 != point->decision.script_bar_open_ms) continue;
10737 if (callsite_token != 0) {
10738 const auto prior = live_by_source_key_.find(checked_live_key(
10739 "__pine_close_site__" + std::to_string(callsite_token)));
10740 if (prior != live_by_source_key_.end() && prior->second == handle) continue;
10741 }
10742 preceding_fifo_closes.push_back(pending.projection_remaining_qty);
10743 }
10744 }
10745 const bool fractional_fifo_book = default_fifo_close && close_pine_view
10746 && close_pine_view->pyramid_entries_.size() > 1U
10747 && source_fifo_close_needs_resolution(
10748 frozen_qty, close_pine_view->pyramid_entries_, preceding_fifo_closes);
10749 const bool host_sized = !immediately
10750 && (fractional_fifo_book || (std::isnan(frozen_qty) && !default_fifo_close));
10751 const bool default_full_any = config_.close_entries_rule_any
10752 && std::isnan(qty) && std::isnan(qty_percent) && !immediately;
10753 const bool pooc_cap_full_close = default_fifo_close
10754 && config_.process_orders_on_close && cap.active();
10755 native_order::Request request;
10756 request.intent = host_sized
10757 ? native_order::OrderIntent{native_order::HostSized{native_order::HostSizedKind::Close, std::nullopt}}
10759 // The generic request label is the legacy close transaction signal while
10760 // PlacementSnapshot keeps the public source id for cohorts/readback.
10761 request.label = "__close__" + id;
10762 request.comment = comment;
10763 double coof_close_fill = kNaN;
10764 double coof_close_target = kNaN;
10765 // A matcher fill AT the bar's second extreme: the close fills there, at
10766 // that extreme, as the market entry does (H-MEASURE Finding 6d). ab9714be
10767 // advanced it to the close point, which admits no cascade order, and so to
10768 // the next open; TradingView books it on the fill's own bar at the extreme
10769 // (lab tv pa2-i4-close-w2-long / -pooc). A fill the adapter forced there
10770 // still advances (coof_fill_at_second_extreme; the recalculation fill-order correction).
10771 bool coof_close_next_open = false;
10772 bool coof_close_at_second_extreme = false;
10773 bool coof_close_next_sub_bar = false;
10774 if (coof_recalc_active_ && !coof_first_open_ && !immediately) {
10775 const auto state = detail::run_state(require_host());
10776 const bool lower_path = state.spec && state.spec->intrabar.lower();
10777 if (!lower_path && coof_fill_at_second_extreme()) {
10778 coof_close_next_open = coof_fill_forced_;
10779 coof_close_at_second_extreme = !coof_fill_forced_;
10780 if (coof_close_at_second_extreme) {
10781 const double endpoint = source_path_uses_high_first(coof_script_bar_)
10782 ? coof_script_bar_.low : coof_script_bar_.high;
10783 const bool buy = current < 0.0;
10784 coof_close_target = endpoint;
10785 coof_close_fill = source_bar_fill_tick(endpoint, staged_.syminfo.mintick)
10786 + (buy ? 1.0 : -1.0) * config_.slippage * staged_.syminfo.mintick;
10787 }
10788 } else if (lower_path) {
10789 coof_close_next_sub_bar = coof_waits_for_next_sub_bar();
10790 }
10791 }
10792 if (coof_recalc_active_ && !coof_first_open_ && !immediately
10793 && !coof_close_next_open && !coof_close_at_second_extreme
10794 && !coof_close_next_sub_bar
10795 && coof_script_bar_valid_) {
10796 int next_waypoint_index = -1;
10797 const double next_waypoint = coof_next_waypoint(&next_waypoint_index);
10798 const auto point = detail::callback_point(require_host());
10799 const double current_quote = point ? point->price : kNaN;
10800 const bool buy = current < 0.0;
10801 coof_close_target = next_waypoint;
10802 coof_close_fill = source_bar_fill_tick(
10803 next_waypoint, staged_.syminfo.mintick)
10804 + (buy ? 1.0 : -1.0) * config_.slippage
10805 * staged_.syminfo.mintick;
10806#ifdef PINEFORGE_VALUE_INVALID_FIXTURES
10807 coof_close_fill = fixture_adapter_execution_value("slippage.recalc.close", coof_close_fill);
10808#endif
10809 check_slippage_fill_price(coof_close_fill, std::isfinite(next_waypoint));
10810 if (finite_positive(coof_close_fill) && finite_positive(current_quote)
10811 && !source_same_point(current_quote, coof_close_fill, staged_.syminfo.mintick)) {
10812 // ab9714be pine_scheduler.cpp:541-563 fills the recalc's market
10813 // close AT the next EXTREME waypoint POINT (W1/W2) and books
10814 // bar_fill_price(waypoint) (pine_fills.cpp:7962-7966). Arm that
10815 // trigger at the raw extreme the path actually reaches; the
10816 // booked tick-grid price rides forced_execution_price. An
10817 // off-grid extreme (H 11.445 booked 11.45) is otherwise never
10818 // touched on this bar and the close rolls to the next open.
10819 // The C point admits no cascade order (pine_scheduler.cpp:
10820 // 541-544, 616-620), so a C target keeps the booked level.
10821 // The booked tick may lie on the wrong side of that raw level,
10822 // so an extreme limit is a touch trigger (fill-through).
10823 const bool extreme_target = next_waypoint_index == 1
10824 || next_waypoint_index == 2;
10825 const double level = extreme_target ? next_waypoint : coof_close_fill;
10826 const native_order::Limit limit{level,
10827 extreme_target && coof_close_fill != level};
10828 const bool falling = coof_close_fill < current_quote;
10829 if (buy) {
10830 request.trigger = falling
10831 ? native_order::Trigger{limit}
10833 } else {
10834 request.trigger = falling
10836 : native_order::Trigger{limit};
10837 }
10838 }
10839 }
10840 // ab9714be pine_strategy_commands.cpp:2222-2278 and :2522-2590:
10841 // the default close-entries rule freezes the id's logical quantity, then
10842 // drains the physical book in FIFO order. It is deliberately not bound
10843 // to that id's opening cohort (the ANY rule below is).
10844 request.owner = pooc_cap_full_close
10846 : default_fifo_close
10847 ? (paired_reversal_close && !paired_reversal_whole_drop
10848 && paired_reversal_parent
10850 *paired_reversal_parent}}
10852 : owner_for_close(id, host_sized && !default_full_any);
10853 PlacementSnapshot snapshot;
10854 snapshot.family = PineOrderFamily::Close; snapshot.source_id = id;
10855 snapshot.from_entry = default_fifo_close
10856 && (!paired_reversal_close || paired_reversal_whole_drop) ? SourceId{} : id;
10857 const bool exact_full_dynamic_close = host_sized && std::isnan(qty)
10858 && requested_percent >= 100.0;
10859 snapshot.comment = comment;
10860 snapshot.requested_qty = exact_full_dynamic_close ? kNaN : frozen_qty;
10861 snapshot.qty_percent = requested_percent;
10862 snapshot.projection_remaining_qty = frozen_qty;
10863 snapshot.command_ordinal = command_ordinal;
10864 snapshot.is_long = false;
10865 if ((immediately || cohort_exposure_for(id) >= std::abs(current) - internal::kQtyEpsilon) && config_.slippage > 0 && coof_recalc_active_
10866 && coof_first_open_ && coof_script_bar_valid_) {
10867 const auto callback_state = detail::run_state(require_host());
10868 if ((!callback_state.spec || callback_state.spec->intrabar.is_none()) && detail::callback_point(require_host()) && detail::callback_point(require_host())->decision.coordinate.path_phase == NativePathPhase::Open) {
10869 coof_close_fill = source_bar_fill_tick(
10870 coof_script_bar_.open, staged_.syminfo.mintick)
10871 + (current < 0.0 ? 1.0 : -1.0) * config_.slippage * staged_.syminfo.mintick;
10872 }
10873 }
10874 snapshot.forced_execution_price = coof_close_fill;
10875 snapshot.forced_waypoint_print = coof_close_target;
10876 snapshot.immediately = immediately; snapshot.deferred_cohort = host_sized; snapshot.sizing = sizing_snapshot();
10877 if (paired_reversal_parent && !paired_reversal_whole_drop)
10878 snapshot.paired_reversal_parent = *paired_reversal_parent;
10879 // The all-in source collision retains a same-side re-entry which may be
10880 // rejected only at the next opening. Its close must be a child of that
10881 // candidate: if the re-entry is refused, the legacy close is suppressed
10882 // rather than flattening the carried seed on its own.
10883 const bool all_in_percent = std::isnan(effective_qty)
10884 && requested_percent >= 100.0 - 1e-9
10885 && config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
10886 && config_.default_qty_value >= 100.0;
10887 bool opposite_reversal_pair = false;
10888 if (all_in_percent && current != 0.0 && !staged_.quantity_grid) {
10889 const bool held_long = current > 0.0;
10890 const auto point = detail::callback_point(require_host());
10891 const auto same_bar = [&](const PlacementSnapshot& candidate) {
10892 return candidate.opening && candidate.family == PineOrderFamily::Entry
10893 && candidate.is_long != held_long
10894 && (!point || candidate.placement_script_open_ms
10895 == point->decision.script_bar_open_ms);
10896 };
10897 for (const auto& pending : pending_same_bar_commands_)
10898 opposite_reversal_pair = opposite_reversal_pair || same_bar(pending.snapshot);
10899 for (const auto& pending : pending_entries_)
10900 opposite_reversal_pair = opposite_reversal_pair || same_bar(pending.snapshot);
10901 for (const auto& pending : pending_coof_requests_)
10902 opposite_reversal_pair = opposite_reversal_pair || same_bar(pending.snapshot);
10903 for (const auto& handle : live_handles_) {
10904 const auto found = placement_.find(handle.incarnation);
10905 if (found != placement_.end())
10906 opposite_reversal_pair = opposite_reversal_pair || same_bar(found->second);
10907 }
10908 }
10909 if (opposite_reversal_pair) {
10910 // pine_fills.cpp:5509-5519 treats the entry/strategy.close pair as one
10911 // reversal decision. The entry's native ReverseTo owns the admitted
10912 // close; if it is declined, the paired close must not flatten the held
10913 // cohort independently. Keep only the source observation and place
10914 // the standing bracket behind the pair's lifecycle barrier.
10915 hold_reversal_pair_brackets(id);
10916 pending_write(source_shadow_pending_).push_back({snapshot, "__close__" + id});
10917 return;
10918 }
10919 bool all_in_dependent_close = false;
10920 if (all_in_percent) {
10921 const auto point = detail::callback_point(require_host());
10922 if (point) {
10923 std::optional<native_order::RequestHandle> reentry;
10924 for (const auto& handle : live_handles_) {
10925 const auto found = placement_.find(handle.incarnation);
10926 if (found == placement_.end() || !found->second.opening
10927 || found->second.family != PineOrderFamily::Entry
10928 || found->second.placement_script_open_ms
10929 != point->decision.script_bar_open_ms
10930 || found->second.source_id != id) {
10931 continue;
10932 }
10933 reentry = handle;
10934 }
10935 // ab9714be pine_fills.cpp:2991-3009: a same-side call placed at
10936 // the pyramiding cap moves nothing at the next opening, and the
10937 // open-boundary admission retires it unless an earlier opposite
10938 // same-bar command can move the account first. A close bound to
10939 // that doomed row would be cancelled with it, while the owner's
10940 // close (an ordinary exit order) still flattens the held side.
10941 if (reentry) {
10942 const auto found = placement_.find(reentry->incarnation);
10943 if (found != placement_.end() && found->second.projection_over_pyramiding) {
10944 const auto& held = found->second;
10945 bool earlier_opposite = false;
10946 for (const auto& handle : live_handles_) {
10947 const auto prior = placement_.find(handle.incarnation);
10948 if (prior == placement_.end()) continue;
10949 const auto& row = prior->second;
10950 if (row.opening && row.family == PineOrderFamily::Entry
10951 && row.is_long != held.is_long
10952 && row.placement_script_open_ms == held.placement_script_open_ms
10953 && row.source_sequence < held.source_sequence) {
10954 earlier_opposite = true;
10955 break;
10956 }
10957 }
10958 if (!earlier_opposite) reentry.reset();
10959 }
10960 }
10961 if (reentry) {
10962 request.owner = native_order::WaitForApplied{*reentry};
10963 all_in_dependent_close = true;
10964 hold_reversal_pair_brackets(id);
10965 }
10966 }
10967 }
10968 if (coof_recalc_active_ && !coof_first_open_ && !immediately
10969 && !finite_positive(coof_close_fill)
10970 && coof_script_bar_valid_
10971 && std::holds_alternative<native_order::Market>(request.trigger)) {
10972 const auto point = detail::callback_point(require_host());
10973 const double current_quote = point ? point->price : kNaN;
10974 const double next_waypoint = next_source_path_waypoint(
10975 coof_script_bar_, coof_context_.coordinate.path_phase, current_quote,
10976 source_path_uses_high_first(coof_script_bar_),
10977 staged_.syminfo.mintick, config_.slippage);
10978 const bool buy = detail::run_position(require_host()).signed_units < 0.0;
10979 const double next_fill = nearest_tick(
10980 next_waypoint + (buy ? 1.0 : -1.0) * config_.slippage
10981 * staged_.syminfo.mintick,
10982 staged_.syminfo.mintick);
10983 if (finite_positive(next_fill) && finite_positive(current_quote)
10984 && next_fill != current_quote) {
10985 const bool falling = next_fill < current_quote;
10986 if (buy) {
10987 request.trigger = falling
10990 } else {
10991 request.trigger = falling
10994 }
10995 snapshot.forced_execution_price = next_fill;
10996 }
10997 }
10998 // Only the generated callsite-token form represents source replacement.
10999 // Independent close statements in one evaluation must coexist (P1/P2).
11000 const SourceId replacement_key = default_fifo_close
11001 ? (callsite_token == 0
11002 ? SourceId{}
11003 : "__pine_close_site__" + std::to_string(callsite_token))
11004 : (callsite_token == 0 ? SourceId{}
11005 : id + "#close#" + std::to_string(callsite_token));
11006 if (coof_close_next_open) {
11007 snapshot.forced_execution_price = kNaN;
11008 request.trigger = native_order::Market{};
11009 pending_write(pending_coof_requests_).push_back({
11010 std::move(request), std::move(snapshot), replacement_key,
11011 false, 0, true});
11012 return;
11013 }
11014 // A later call of the same call site on this bar re-sizes the site's
11015 // order; its fill books against the id of the site's first call (tape
11016 // w3bf02-d1).
11017 if (default_fifo_close && !config_.process_orders_on_close && callsite_token != 0) {
11018 const auto prior = live_by_source_key_.find(checked_live_key(replacement_key));
11019 const auto point = detail::callback_point(require_host());
11020 if (prior != live_by_source_key_.end() && point) {
11021 const auto row = placement_.find(prior->second.incarnation);
11022 if (row != placement_.end() && row->second.family == PineOrderFamily::Close
11023 && row->second.placement_script_open_ms == point->decision.script_bar_open_ms) {
11024 const SourceId& first = row->second.close_first_id.empty()
11025 ? row->second.source_id : row->second.close_first_id;
11026 if (first != id) snapshot.close_first_id = first;
11027 }
11028 }
11029 }
11030 const PlacementSnapshot shadow_snapshot = snapshot;
11031 const auto accepted = submit_or_replace(std::move(request), std::move(snapshot), false, replacement_key);
11032 if (!accepted && all_in_dependent_close) {
11033 pending_write(source_shadow_pending_).push_back({shadow_snapshot, "__close__" + id});
11034 }
11035 const bool pooc_immediate_fifo = config_.process_orders_on_close
11036 && !coof_recalc_active_
11037 && !config_.close_entries_rule_any && closes_full_position
11038 && !reversal_pair;
11039 if ((immediately || (config_.process_orders_on_close
11040 && (cap.active() || pooc_immediate_fifo))) && accepted) {
11041 const auto outcome = require_host().execute_current(
11042 {*accepted, NativeCurrentPriceRule::NearestTick});
11043 if (const auto* applied = std::get_if<native_order::ExecutionAppliedEvent>(&outcome)) {
11044 consume_cohort_units(id, *applied);
11045 current_debited_applied_ordinals_.insert(applied->ordinal);
11046 if (applied->terminal) retire(*accepted);
11047 }
11048 }
11049}
11050
11052 if (intraday_loss_orders_blocked()) return;
11053 require_cohort_ticket();
11054 reroute_fixed_entries_before_request();
11055 const double held = detail::run_position(require_host()).signed_units;
11056 if (held == 0.0) return;
11057 const auto* const point = detail::callback_point(require_host());
11058 if (point && cap_placement_denied(point->decision)) return;
11059 // The close-first fact (record_close_first: a whole close of the open
11060 // position) and the bar of the pending close_all, from the point and the
11061 // position read above; nothing between them moves either.
11062 if (point) {
11063 close_first_script_bar_ = point->decision.script_bar_open_ms;
11064 close_first_side_ = held > 0.0 ? 1 : -1;
11065 close_all_pending_script_bar_ = point->decision.script_bar_open_ms;
11066 }
11067 if (config_.calc_on_order_fills) {
11068 const auto point = detail::callback_point(require_host());
11069 const std::int64_t script_open = point ? point->decision.script_bar_open_ms
11070 : require_host().native_decision_floor();
11071 std::vector<native_order::RequestHandle> newborns;
11072 for (const auto& handle : live_handles_) {
11073 const auto placement = placement_.find(handle.incarnation);
11074 if (placement != placement_.end() && placement->second.opening
11075 && placement->second.family == PineOrderFamily::Entry
11076 && placement->second.placement_script_open_ms == script_open) {
11077 newborns.push_back(handle);
11078 }
11079 }
11080 for (const auto& handle : newborns) {
11081 const auto result = require_host().cancel(handle);
11082 if (result.status == native_order::CancelStatus::Cancelled) retire(handle);
11083 }
11084 }
11085 native_order::Request request;
11086 request.intent = native_order::Flatten{}; request.label = "__close__";
11087 PlacementSnapshot snapshot;
11088 snapshot.family = PineOrderFamily::CloseAll;
11089 snapshot.source_id = request.label;
11090 snapshot.sizing = sizing_snapshot();
11091 // A carried short's follow-up owed to this process_orders_on_close close
11092 // is booked after the script (book_owed_close_follow_up): the close_all
11093 // keeps the book the script saw, placed as the transaction that crosses
11094 // zero on the smaller book, ahead of the script's later orders (lab tv
11095 // tape tests/fixtures/margin_open_rules lots/close-all-overfill-gap).
11096 if (PF_RARE(owed_follow_up_bar_ != std::numeric_limits<std::int64_t>::min()) && point
11097 && owed_follow_up_bar_ == point->decision.script_bar_open_ms && owed_follow_up_point_ == 3
11098 && config_.process_orders_on_close && held < 0.0
11099 && detail::margin_schedule_switches().close_follow_up_after_script) {
11100 request.intent = native_order::Transact{-held};
11101 snapshot.crosses_zero = true;
11102 }
11103 const auto accepted = submit_or_replace(
11104 std::move(request), std::move(snapshot), false, "__pine_close_all");
11105 if (!accepted || config_.process_orders_on_close) return;
11106 const auto close = placement_.find(accepted->incarnation);
11107 if (close == placement_.end()) return;
11108 const auto side = static_cast<PositionSide>(close->second.projection_position_side);
11109 for (const auto& handle : live_handles_) {
11110 if (handle == *accepted) continue;
11111 const auto found = placement_.find(handle.incarnation);
11112 if (found == placement_.end()) continue;
11113 auto& pending = found->second;
11114 const bool pure_prior_stop = pending.opening
11115 && pending.family == PineOrderFamily::Entry
11116 && finite_positive(pending.exit_levels.stop)
11117 && !finite_positive(pending.exit_levels.limit)
11118 && !finite_positive(pending.exit_levels.trail_points)
11119 && !finite_positive(pending.exit_levels.trail_price)
11120 && !finite_positive(pending.exit_levels.trail_offset)
11121 && !pending.stop_limit_activated
11122 && pending.projection_created_bar < close->second.projection_created_bar
11123 && pending.projection_position_side == static_cast<std::int32_t>(side)
11124 && pending.is_long == (side == PositionSide::LONG)
11125 && !pending.projection_over_pyramiding;
11126 const auto* pine_host = pine_view_of(&require_host());
11127 const bool has_physical_id = pine_host
11128 && pine_host->adapter_has_open_entry_id(pending.source_id);
11129 if (!pure_prior_stop || !has_physical_id) continue;
11130 pending.preserved_by_close_all = *accepted; invalidate_retirement();
11131 pending.preserved_close_all_bar = close->second.projection_created_bar;
11132 }
11133 const double cur_pos = detail::run_position(require_host()).signed_units;
11134 const auto cur_pt = detail::callback_point(require_host());
11135 if (cur_pos != 0.0 && cur_pt) {
11136 std::vector<native_order::RequestHandle> opposite_entries;
11137 for (const auto& handle : live_handles_) {
11138 if (handle == *accepted) continue;
11139 const auto found = placement_.find(handle.incarnation);
11140 if (found == placement_.end()) continue;
11141 const auto& pend = found->second;
11142 const bool unpriced_market = !finite_positive(pend.exit_levels.limit)
11143 && !finite_positive(pend.exit_levels.stop)
11144 && !finite_positive(pend.exit_levels.trail_points)
11145 && !finite_positive(pend.exit_levels.trail_price)
11146 && !finite_positive(pend.exit_levels.trail_offset);
11147 if (pend.opening && pend.family == PineOrderFamily::Entry
11148 && unpriced_market
11149 && !pend.projection_over_pyramiding
11150 && pend.placement_script_open_ms == cur_pt->decision.script_bar_open_ms) {
11151 opposite_entries.push_back(handle);
11152 }
11153 }
11154 for (const auto& handle : opposite_entries) {
11155 const auto found = placement_.find(handle.incarnation);
11156 if (found == placement_.end()) continue;
11157 PlacementSnapshot entry_snapshot = found->second;
11158 // ab9714be pine_fills.cpp:3848-3856: full market close processes before opposite entry so entry executes after close
11159 entry_snapshot.paired_reversal_parent = *accepted;
11160 entry_snapshot.market_admission = {};
11161 const auto result = require_host().cancel(handle);
11162 if (result.status == native_order::CancelStatus::Cancelled) {
11163 retire(handle);
11165 const double target = entry_snapshot.is_long
11166 ? entry_snapshot.requested_qty : -entry_snapshot.requested_qty;
11167 req.intent = std::isnan(target)
11168 ? native_order::OrderIntent{native_order::HostSized{native_order::HostSizedKind::Open,
11169 entry_snapshot.is_long ? native_order::Side::Long : native_order::Side::Short}}
11171 req.label = entry_snapshot.source_id;
11172 req.comment = entry_snapshot.comment;
11173 const SourceId replacement_key = entry_snapshot.source_id;
11174 pending_write(pending_entries_).push_back({std::move(req), std::move(entry_snapshot), replacement_key});
11175#ifdef PINEFORGE_CR277_DIAGNOSTIC
11176 cr277_pending_close_pushes[1].fetch_add(1, std::memory_order_relaxed);
11177#endif
11178 }
11179 }
11180 for (auto it = pending_same_bar_commands_.begin(); it != pending_same_bar_commands_.end();) {
11181 if (it->opening && it->snapshot.family == PineOrderFamily::Entry
11182 && !it->snapshot.projection_over_pyramiding
11183 && it->snapshot.placement_script_open_ms == cur_pt->decision.script_bar_open_ms) {
11184 it->snapshot.paired_reversal_parent = *accepted;
11185 it->snapshot.market_admission = {};
11186 pending_write(pending_entries_).push_back({std::move(it->request), std::move(it->snapshot), it->replacement_key});
11187 it = pending_write(pending_same_bar_commands_).erase(it);
11188 } else {
11189 ++it;
11190 }
11191 }
11192 }
11193}
11194
11195// The account the binding rule's tapes run on: margin requirements off
11196// (margin_long = margin_short = 0, Pine v5's default), so no margin call ever
11197// acts on the exits the rule keeps. Every tape and its reference model run
11198// there; with a margin requirement the rule is unpinned and the adapter
11199// keeps its former course.
11200bool PineExecutionAdapter::margins_disabled() const noexcept {
11201 return config_.margin_long == 0.0 && config_.margin_short == 0.0;
11202}
11203
11204// The exits the binding rule's tapes cover: the whole position at absolute
11205// levels -- no quantity, no partial percent, no trail, no tick offsets --
11206// without calc_on_order_fills.
11207bool PineExecutionAdapter::whole_level_exit(double limit_price, double stop_price,
11208 double trail_points, double trail_price,
11209 double profit_ticks, double loss_ticks, double qty,
11210 double qty_percent) const noexcept {
11211 return !config_.calc_on_order_fills && std::isnan(qty)
11212 && (std::isnan(qty_percent) || qty_percent >= 100.0)
11213 && std::isnan(trail_points) && std::isnan(trail_price)
11214 && std::isnan(profit_ticks) && std::isnan(loss_ticks)
11215 && (finite_positive(limit_price) || finite_positive(stop_price));
11216}
11217
11218void PineExecutionAdapter::observe_pending_scan(PendingScanCounter& work) const noexcept {
11219 ++work.calls;
11220 work.max_live_handles = std::max<std::uint64_t>(work.max_live_handles, live_handles_.size());
11221 work.max_placement = std::max<std::uint64_t>(work.max_placement, placement_.size());
11222 work.max_pending_bracket_legs = std::max<std::uint64_t>(
11223 work.max_pending_bracket_legs, pending_bracket_legs_.size());
11224 work.max_resident_cohorts = std::max<std::uint64_t>(
11225 work.max_resident_cohorts, cohorts_by_id_.resident_size());
11226 work.max_logical_cohorts = std::max(work.max_logical_cohorts, cohorts_by_id_.size());
11227}
11228
11229// Whether a limit or stop entry order works -- of `id`, or of any id when
11230// `id` is null: a live opening request or one the adapter still holds.
11231bool PineExecutionAdapter::priced_entry_order_working(const SourceId* id) const {
11232 auto& work = pending_scan_work_.priced_entry;
11233 observe_pending_scan(work);
11234 const auto entry_of = [&](const PlacementSnapshot& row) {
11235 return row.opening
11236 && (row.family == PineOrderFamily::Entry || row.family == PineOrderFamily::Order)
11237 && (id == nullptr || row.source_id == *id)
11238 && (finite_positive(row.exit_levels.limit) || finite_positive(row.exit_levels.stop));
11239 };
11240 for (const auto& pending : pending_entries_)
11241 if (entry_of(pending.snapshot)) return true;
11242 for (const auto& pending : pending_same_bar_commands_)
11243 if (entry_of(pending.snapshot)) return true;
11244 for (const auto& handle : live_handles_) {
11245 ++work.live_handle_iterations;
11246 ++work.placement_find_calls;
11247 const auto row = placement_.find(handle.incarnation);
11248 if (row != placement_.end() && entry_of(row->second)) return true;
11249 }
11250 return false;
11251}
11252
11253bool PineExecutionAdapter::entry_order_pending(const SourceId& id) const {
11254 return entry_order_working(&id);
11255}
11256
11257// Whether an entry order of any trigger works -- of `id`, or of any id when
11258// `id` is null: a live opening request or one the adapter still holds.
11259bool PineExecutionAdapter::entry_order_working(const SourceId* id) const {
11260 auto& work = pending_scan_work_.entry;
11261 observe_pending_scan(work);
11262 const auto entry_of = [&](const PlacementSnapshot& row) {
11263 return row.opening && (id == nullptr || row.source_id == *id)
11264 && (row.family == PineOrderFamily::Entry || row.family == PineOrderFamily::Order);
11265 };
11266 for (const auto& pending : pending_entries_)
11267 if (entry_of(pending.snapshot)) return true;
11268 for (const auto& pending : pending_same_bar_commands_)
11269 if (entry_of(pending.snapshot)) return true;
11270 for (const auto& pending : pending_coof_requests_)
11271 if (entry_of(pending.snapshot)) return true;
11272 for (const auto& delayed : delayed_market_orders_)
11273 if (entry_of(delayed.snapshot)) return true;
11274 for (const auto& handle : live_handles_) {
11275 ++work.live_handle_iterations;
11276 ++work.placement_find_calls;
11277 const auto row = placement_.find(handle.incarnation);
11278 if (row != placement_.end() && entry_of(row->second)) return true;
11279 }
11280 return false;
11281}
11282
11283// Whether the book holds a lot `id` opened. The cohort facts cannot answer
11284// alone: a reversal by an entry of the reversed side's own id clears them
11285// after its opening records the new lot (purge_brackets_after_applied_reversal).
11286bool PineExecutionAdapter::open_lot_of(const SourceId& id) const {
11287 auto& work = pending_scan_work_.open_lot;
11288 observe_pending_scan(work);
11289 const auto& host = require_host();
11290 if (detail::run_position(host).lot_count == 0) return false;
11291 for (const auto& lot : host.native_open_lots(kNaN)) {
11292 ++work.open_lot_iterations;
11293 if (lot.entry_label == id) return true;
11294 }
11295 return false;
11296}
11297
11298// Whether the pair (exit_id, from_entry) has an exit a call made while its
11299// entry rested or was open: working, staged for a later submission, or a
11300// relative leg waiting for its entry's fill.
11301bool PineExecutionAdapter::standing_exit(const SourceId& exit_id,
11302 const SourceId& from_entry) const {
11303 auto& work = pending_scan_work_.standing_exit;
11304 observe_pending_scan(work);
11305 const auto standing = [&](const PlacementSnapshot& row) {
11306 const bool exit = row.family == PineOrderFamily::ExitLimit
11307 || row.family == PineOrderFamily::ExitStop
11308 || row.family == PineOrderFamily::ExitTrail;
11309 return exit && !row.void_issue && row.source_id == exit_id
11310 && row.from_entry == from_entry;
11311 };
11312 for (const auto& handle : live_handles_) {
11313 ++work.live_handle_iterations;
11314 ++work.placement_find_calls;
11315 const auto row = placement_.find(handle.incarnation);
11316 if (row != placement_.end() && standing(row->second)) return true;
11317 }
11318 for (const auto& pending : pending_bracket_legs_)
11319 if (standing(pending.snapshot)) return true;
11320 for (const auto& pending : pending_coof_requests_)
11321 if (standing(pending.snapshot)) return true;
11322 for (const auto& delayed : delayed_market_orders_)
11323 if (standing(delayed.snapshot)) return true;
11324 return std::any_of(pending_relative_exits_.begin(), pending_relative_exits_.end(),
11325 [&](const PendingRelativeExit& value) {
11326 return value.exit_id == exit_id && value.from_entry == from_entry;
11327 });
11328}
11329
11330// A call made once its entry exists that leaves a void request of its pair
11331// unchanged makes that request its own, as a re-issue that replaced it would
11332// be: no longer void, and queued from this call (exit_queue_rank).
11333void PineExecutionAdapter::unvoid_exit(PlacementSnapshot& row) {
11334 if (source_sequence_ == std::numeric_limits<std::uint64_t>::max())
11335 throw coded<std::overflow_error>(RunFailureCode::engine_invariant, {},
11336 "Pine source sequence exhausted");
11337 row.void_issue = false;
11338 row.chain_origin_sequence = ++source_sequence_;
11339}
11340
11341// A same-close fill of an exit leg (process_orders_on_close). A void call's
11342// request acts on nothing (PlacementSnapshot::void_issue): its close is
11343// placed as the leg's successor, as any other, so every later request keeps
11344// its incarnation, and withdrawn instead of executed.
11345void PineExecutionAdapter::execute_or_withdraw_close(native_order::RequestHandle close,
11346 bool void_issue) {
11347 if (!void_issue) {
11348 (void)require_host().execute_current({close, NativeCurrentPriceRule::NearestTick});
11349 return;
11350 }
11351 const auto result = require_host().cancel(close);
11352 if (result.status == native_order::CancelStatus::Cancelled) retire(close);
11353}
11354
11355// The opening of `entry_id` withdraws the exits TradingView voided for it
11356// (PlacementSnapshot::void_issue): their calls ran while the entry neither
11357// existed nor rested, so none of them binds to the lot this fill opens.
11358void PineExecutionAdapter::withdraw_void_exits(const SourceId& entry_id) {
11359 const auto voided = [&](const PlacementSnapshot& row) {
11360 return row.void_issue && row.from_entry == entry_id;
11361 };
11362 pending_write(pending_bracket_legs_).erase(
11363 std::remove_if(pending_bracket_legs_.begin(), pending_bracket_legs_.end(),
11364 [&](const PendingBracketLeg& row) { return voided(row.snapshot); }),
11365 pending_bracket_legs_.end());
11366 pending_write(pending_coof_requests_).erase(
11367 std::remove_if(pending_coof_requests_.begin(), pending_coof_requests_.end(),
11368 [&](const PendingCoofRequest& row) { return voided(row.snapshot); }),
11369 pending_coof_requests_.end());
11370 delayed_market_orders_.erase(
11371 std::remove_if(delayed_market_orders_.begin(), delayed_market_orders_.end(),
11372 [&](const DelayedMarketOrder& row) { return voided(row.snapshot); }),
11373 delayed_market_orders_.end());
11374 std::vector<native_order::RequestHandle> handles;
11375 for (const auto& handle : live_handles_) {
11376 const auto row = placement_.find(handle.incarnation);
11377 if (row != placement_.end() && voided(row->second)) handles.push_back(handle);
11378 }
11379 if (handles.empty()) return;
11380 for (const auto& handle : handles) {
11381 const auto result = require_host().cancel(handle);
11382 if (result.status == native_order::CancelStatus::Cancelled) retire(handle);
11383 }
11384 for (auto family = bracket_families_.begin(); family != bracket_families_.end();) {
11385 family->second.remove_all(handles);
11386 if (family->second.empty()) family = bracket_families_.erase(family);
11387 else ++family;
11388 }
11389 refresh_pending_view();
11390}
11391
11392// Under process_orders_on_close TradingView never fills a priced add called
11393// while the position holds `pyramiding` entries, even once the position
11394// shrinks or goes flat (tests/fixtures/exit_binding pyr1, pyr1-close-sibling).
11395// The adapter places it at the call, over the cap (projection_over_pyramiding),
11396// as a later close of the bar may free a slot. At the next opening, that bar's
11397// closes done, an add the position still holds `pyramiding` entries against
11398// leaves the book; each add is judged at that one opening. The tapes pin an
11399// add of an id that holds no lot, without calc_on_order_fills; a same-id add
11400// keeps its course. Where a close of the add's bar frees the slot TradingView
11401// is not pinned (the pin's model rejects the add at the call); the add stays.
11402void PineExecutionAdapter::withdraw_unplaced_cap_adds(const NativeDecisionContext& context) {
11403 const auto physical = detail::run_position(require_host());
11404 if (physical.signed_units == 0.0) return;
11405 const auto records = open_ledger_records();
11406 if ((records ? *records : physical.lot_count)
11407 < static_cast<std::size_t>(config_.pyramiding)) {
11408 return;
11409 }
11410 std::vector<native_order::RequestHandle> unplaced;
11411 for (const auto& handle : live_handles_) {
11412 const auto row = placement_.find(handle.incarnation);
11413 if (row == placement_.end()) continue;
11414 const auto& entry = row->second;
11415 if (!entry.opening || entry.family != PineOrderFamily::Entry
11416 || !entry.projection_over_pyramiding
11417 || entry.is_long != (physical.signed_units > 0.0)
11418 || !(finite_positive(entry.exit_levels.limit)
11419 || finite_positive(entry.exit_levels.stop))
11420 || entry.placement_script_open_ms >= context.script_bar_open_ms
11421 || entry.placement_script_open_ms != last_broker_open_ms_
11422 || cohort_exposure_for(entry.source_id) > 0.0 || open_lot_of(entry.source_id)) {
11423 continue;
11424 }
11425 unplaced.push_back(handle);
11426 }
11427 for (const auto& handle : unplaced) {
11428 const auto result = require_host().cancel(handle);
11429 if (result.status == native_order::CancelStatus::Cancelled) retire(handle);
11430 }
11431}
11432
11433// The fill of `entry_id` ends the binding of its exits to the id
11434// (PlacementSnapshot::pending_bound_exit): from now on they are exits of the
11435// lot this fill opens, and end with it.
11436void PineExecutionAdapter::bind_pending_exits_to_fill(const SourceId& entry_id) {
11437 const auto bind = [&](PlacementSnapshot& row) {
11438 if (row.pending_bound_exit && row.from_entry == entry_id) { row.pending_bound_exit = false; invalidate_retirement(); }
11439 };
11440 for (auto& pending : pending_bracket_legs_) bind(pending.snapshot);
11441 for (auto& pending : pending_coof_requests_) bind(pending.snapshot);
11442 for (auto& delayed : delayed_market_orders_) bind(delayed.snapshot);
11443 for (auto& shadow : source_shadow_pending_) bind(shadow.snapshot);
11444 for (const auto& handle : live_handles_) {
11445 const auto row = placement_.find(handle.incarnation);
11446 if (row != placement_.end()) bind(row->second);
11447 }
11448}
11449
11450// A strategy.cancel of `entry_id` withdraws its working entry order, and the
11451// native requests of the exits bound to the id end with it. TradingView keeps
11452// those exits bound to the id (tests/fixtures/exit_binding cancel-replace,
11453// cancel-flat-replace): their calls are kept and the id's next entry order
11454// re-arms them (rearm_id_bound_exits).
11455void PineExecutionAdapter::stash_id_bound_exits(const SourceId& entry_id) {
11456 if (!detail::exit_binding_rule_switches().pending_bound_exit_survives_flat
11457 || entry_id.empty() || !entry_order_pending(entry_id)) {
11458 return;
11459 }
11460 std::vector<native_order::RequestHandle> handles;
11461 for (const auto& handle : live_handles_) {
11462 const auto row = placement_.find(handle.incarnation);
11463 if (row == placement_.end()) continue;
11464 const auto& snapshot = row->second;
11465 const bool exit = snapshot.family == PineOrderFamily::ExitLimit
11468 if (!exit || !snapshot.pending_bound_exit || snapshot.from_entry != entry_id) continue;
11469 handles.push_back(handle);
11470 const bool kept = std::any_of(id_bound_exits_.begin(), id_bound_exits_.end(),
11471 [&](const IdBoundExit& stashed) {
11472 return stashed.exit_id == snapshot.source_id
11473 && stashed.from_entry == snapshot.from_entry;
11474 });
11475 if (!kept) {
11476 id_bound_exits_.push_back({snapshot.source_id, snapshot.from_entry,
11477 snapshot.exit_levels, snapshot.qty_percent,
11478 snapshot.requested_qty, snapshot.comment,
11479 snapshot.oca_name});
11480 }
11481 }
11482 if (handles.empty()) return;
11483 for (const auto& handle : handles) {
11484 const auto result = require_host().cancel(handle);
11485 if (result.status == native_order::CancelStatus::Cancelled) retire(handle);
11486 }
11487 for (auto family = bracket_families_.begin(); family != bracket_families_.end();) {
11488 family->second.remove_all(handles);
11489 if (family->second.empty()) family = bracket_families_.erase(family);
11490 else ++family;
11491 }
11492 refresh_pending_view();
11493}
11494
11495// The first opening fill settles every strategy.exit TradingView voided as
11496// called while flat with no entry order working (PlacementSnapshot::
11497// void_global_issue). A market entry's fill, which the tapes pin, withdraws
11498// them: none binds to the lot it opens, whatever its id. Only the legs still
11499// working are cancelled. The kernel already ended a leg it reached while the
11500// book was flat, and the opening check skips that leg
11501// (priced_exit_precedes_opening). A stop or limit entry's fill, which no tape
11502// covers, clears the flag instead and leaves the legs their former course.
11503void PineExecutionAdapter::settle_void_global_exits(bool market_entry) {
11504 const auto voided = [](const PlacementSnapshot& row) { return row.void_global_issue; };
11505 if (!market_entry) {
11506 for (auto& row : pending_bracket_legs_) row.snapshot.void_global_issue = false;
11507 for (auto& row : pending_coof_requests_) row.snapshot.void_global_issue = false;
11508 for (auto& row : delayed_market_orders_) row.snapshot.void_global_issue = false;
11509 for (const auto& handle : live_handles_) {
11510 const auto row = placement_.find(handle.incarnation);
11511 if (row != placement_.end() && voided(row->second))
11512 row->second.void_global_issue = false;
11513 }
11514 return;
11515 }
11516 pending_write(pending_bracket_legs_).erase(
11517 std::remove_if(pending_bracket_legs_.begin(), pending_bracket_legs_.end(),
11518 [&](const PendingBracketLeg& row) { return voided(row.snapshot); }),
11519 pending_bracket_legs_.end());
11520 pending_write(pending_coof_requests_).erase(
11521 std::remove_if(pending_coof_requests_.begin(), pending_coof_requests_.end(),
11522 [&](const PendingCoofRequest& row) { return voided(row.snapshot); }),
11523 pending_coof_requests_.end());
11524 delayed_market_orders_.erase(
11525 std::remove_if(delayed_market_orders_.begin(), delayed_market_orders_.end(),
11526 [&](const DelayedMarketOrder& row) { return voided(row.snapshot); }),
11527 delayed_market_orders_.end());
11528 std::vector<native_order::RequestHandle> handles;
11529 for (const auto& handle : live_handles_) {
11530 const auto row = placement_.find(handle.incarnation);
11531 if (row != placement_.end() && voided(row->second)) handles.push_back(handle);
11532 }
11533 if (handles.empty()) return;
11534 const auto working = require_host().native_working_requests();
11535 handles.erase(std::remove_if(handles.begin(), handles.end(),
11536 [&](const native_order::RequestHandle& handle) {
11537 return std::none_of(working.begin(), working.end(),
11538 [&](const NativeWorkingRequest& request) {
11539 return request.definition->handle == handle;
11540 });
11541 }), handles.end());
11542 if (handles.empty()) return;
11543 for (const auto& handle : handles) {
11544 const auto result = require_host().cancel(handle);
11545 if (result.status == native_order::CancelStatus::Cancelled) retire(handle);
11546 }
11547 for (auto family = bracket_families_.begin(); family != bracket_families_.end();) {
11548 family->second.remove_all(handles);
11549 if (family->second.empty()) family = bracket_families_.erase(family);
11550 else ++family;
11551 }
11552 refresh_pending_view();
11553}
11554
11555// The entry order just placed for `entry_id` takes the exits its id kept
11556// bound through a cancel, each called again as it was called.
11557void PineExecutionAdapter::rearm_id_bound_exits(const SourceId& entry_id) {
11558 if (!entry_order_pending(entry_id)) return;
11559 std::vector<IdBoundExit> rearm;
11560 for (auto row = id_bound_exits_.begin(); row != id_bound_exits_.end();) {
11561 if (row->from_entry == entry_id) {
11562 rearm.push_back(std::move(*row));
11563 row = id_bound_exits_.erase(row);
11564 } else {
11565 ++row;
11566 }
11567 }
11568 for (const auto& row : rearm) {
11569 exit(row.exit_id, row.from_entry, row.levels.limit, row.levels.stop,
11570 row.levels.trail_points, row.levels.trail_offset, row.levels.trail_price,
11571 row.qty_percent, row.comment, row.qty, row.oca_name, row.levels.profit_ticks,
11572 row.levels.loss_ticks);
11573 }
11574}
11575
11576void PineExecutionAdapter::exit(const SourceId& exit_id, const SourceId& from_entry,
11577 double limit_price, double stop_price, double trail_points,
11578 double trail_offset, double trail_price, double qty_percent,
11579 const std::string& comment, double qty,
11580 const std::string& oca_name, double profit_ticks,
11581 double loss_ticks) {
11582 const double requested_qty_percent = qty_percent;
11583 if (intraday_loss_orders_blocked()) return;
11584 if (const auto point = detail::callback_point(require_host());
11585 point && cap_placement_denied(point->decision)) {
11586 return;
11587 }
11588 const bool cohort_candidate = !from_entry.empty()
11589 && (!std::isnan(limit_price) || !std::isnan(stop_price)
11590 || !std::isnan(profit_ticks) || !std::isnan(loss_ticks)
11591 || !std::isnan(trail_points) || !std::isnan(trail_price));
11592 require_cohort_ticket(cohort_candidate ? &from_entry : nullptr);
11593 reroute_fixed_entries_before_request();
11594 if (!from_entry.empty()) {
11595 const auto token = named_entry_cancel_tokens_.find(from_entry);
11596 if (token != named_entry_cancel_tokens_.end()) {
11597 bool recreated_parent = std::any_of(
11598 pending_entries_.begin(), pending_entries_.end(),
11599 [&](const PendingEntry& pending) {
11600 return pending.snapshot.opening
11601 && pending.snapshot.source_id == from_entry;
11602 });
11603 for (const auto& handle : live_handles_) {
11604 const auto parent = placement_.find(handle.incarnation);
11605 if (parent != placement_.end() && parent->second.opening
11606 && parent->second.family == PineOrderFamily::Entry
11607 && parent->second.source_id == from_entry) {
11608 recreated_parent = true;
11609 break;
11610 }
11611 }
11612 // The cancellation token belongs only to a retained child that
11613 // survives until the fresh parent is declared. Reissuing the
11614 // child first creates a fresh topology and consumes that token.
11615 if (!recreated_parent) named_entry_cancel_tokens_.erase(token);
11616 }
11617 }
11618 const bool actionable = !std::isnan(limit_price) || !std::isnan(stop_price)
11619 || !std::isnan(profit_ticks) || !std::isnan(loss_ticks)
11620 || !std::isnan(trail_points) || !std::isnan(trail_price);
11621 if (!actionable) {
11622 // ab9714be:pine_strategy_commands.cpp:1637-1660: an all-NaN
11623 // strategy.exit is inert, but a matching reissue still removes the
11624 // prior bracket before returning. trail_offset alone is not an
11625 // actionable leg.
11626 exit_cancel_bracket(exit_id, from_entry, comment);
11627 return;
11628 }
11629 // TradingView voids a strategy.exit whose entry has neither an open
11630 // trade nor an entry order waiting to fill when the call runs: the exit
11631 // does not wait for a later entry of that id (lab tv w3f05-s17, -s19,
11632 // and w3f05-s12, whose exit re-issued while flat would otherwise take
11633 // the queue place of the next trade's first exit). An exit issued while
11634 // its entry's order rests binds to it when it fills (w3f05-s20). The
11635 // void call's request still waits in the book, so every later request
11636 // keeps its incarnation; the entry's opening withdraws it before it can
11637 // bind (withdraw_void_exits).
11638 // A pair that still has an exit issued while its entry rested or was
11639 // open keeps the call's former effect, a re-issue of that exit.
11640 // TradingView leaves the standing exit as it is instead (lab tv
11641 // w3bf05-v1, 04-08: its first level fills, not the void call's), but the
11642 // adapter withdraws an exit together with its cancelled entry, so that
11643 // shape is not reached here (tests/fixtures/exit_queue/README.md).
11644 const bool void_issue = !from_entry.empty() && !(cohort_exposure_for(from_entry) > 0.0)
11645 && !(!config_.close_entries_rule_any
11646 && fixture_close_logical_units(from_entry) > 0.0)
11647 && !entry_order_pending(from_entry) && !open_lot_of(from_entry)
11648 && !standing_exit(exit_id, from_entry);
11649 // A call made in position for an entry that holds no lot but has a limit
11650 // or stop order working binds the exit to the entry id: it waits for that
11651 // id's next fill through closes and the flat (PlacementSnapshot::
11652 // pending_bound_exit; tests/fixtures/exit_binding). The tapes pin a whole
11653 // exit at absolute levels for a parent resting at a level, without
11654 // calc_on_order_fills and with margins off (whole_level_exit,
11655 // margins_disabled); other exits keep their course.
11656 const bool pending_bound_exit = !from_entry.empty() && !void_issue
11658 && margins_disabled()
11659 && whole_level_exit(limit_price, stop_price, trail_points, trail_price, profit_ticks,
11660 loss_ticks, qty, requested_qty_percent)
11661 && detail::run_position(require_host()).signed_units != 0.0
11662 && !(cohort_exposure_for(from_entry) > 0.0)
11663 && !(!config_.close_entries_rule_any && fixture_close_logical_units(from_entry) > 0.0)
11664 && priced_entry_order_working(&from_entry) && !open_lot_of(from_entry);
11665 // TradingView voids a strategy.exit without from_entry called while no
11666 // position is held and no entry order of any id is working: an entry
11667 // placed after it opens a book with no exit, whose opening check is
11668 // booked as with none, and none of its legs ever fills, the
11669 // process_orders_on_close close included (lab tv tapes tests/fixtures/
11670 // margin_open_rules exit-before-entry/). With an entry order working the
11671 // exit binds, and the first fill takes it. As a void named exit's, the
11672 // call's request still waits in the book, so every later request keeps
11673 // its incarnation; the first opening fill, a market entry's, withdraws
11674 // it (settle_void_global_exits). Not under calc_on_order_fills, nor
11675 // where a stop or limit entry fills first: no tape covers either.
11676 const bool void_global_issue = from_entry.empty()
11678 && !config_.calc_on_order_fills
11679 && detail::run_position(require_host()).signed_units == 0.0
11680 && !entry_order_working(nullptr);
11681 // A pending variable short-context entry is only tentatively held for the
11682 // three-object ShortSeed command book. A bracket call proves it belongs
11683 // to an ordinary entry family, so materialize that entry before binding
11684 // the bracket just as the legacy source callback did.
11685 if (!pending_same_bar_commands_.empty()
11686 && config_.default_qty_type != static_cast<int>(QtyType::FIXED)
11687 && detail::run_position(require_host()).signed_units < 0.0) {
11688 flush_pending_same_bar_commands();
11689 }
11690 if (source_command_sequence_ == std::numeric_limits<std::uint64_t>::max()) {
11692 "Pine source command sequence exhausted");
11693 }
11694 const std::uint64_t command_sequence = ++source_command_sequence_;
11695 // Trail point and offset operands are source tick counts, whereas the
11696 // generic native Trail carries prices. Preserve the source operands in
11697 // the placement projection and lower only the executable request here.
11698 // Points ceil away from entry (with the established source tolerance);
11699 // offsets truncate exactly, including the explicit-zero trail shape.
11700 const double source_trail_points = trail_points;
11701 const double source_trail_offset = trail_offset;
11702 const double source_trail_price = trail_price;
11703 const bool has_trail_request = !std::isnan(source_trail_points)
11704 || !std::isnan(source_trail_price);
11705
11706 // Relative levels resolve against a live source cohort. The original tick
11707 // facts remain in the snapshot for deferred/observer projections.
11708 const auto physical = detail::run_position(require_host());
11709 const SourceId partial_exit_key = exit_id + "\x1f" + from_entry;
11710 // ab9714be pine_strategy_commands.cpp:1661-1692: only a re-issue that is
11711 // itself explicitly partial against the live position (net of a same-bar
11712 // pending close) is suppressed; an explicit qty covering the whole
11713 // remaining position is a full exit and re-arms.
11714 const double live_exit_units = std::max(
11715 0.0, std::abs(physical.signed_units) - pending_same_bar_close_qty_);
11716 double reissue_percent = std::isnan(qty_percent)
11717 ? 100.0 : std::clamp(qty_percent, 0.0, 100.0);
11718 if (!std::isnan(qty) && live_exit_units > internal::kQtyEpsilon)
11719 reissue_percent = std::min(qty, live_exit_units) / live_exit_units * 100.0;
11720 const bool partial_reissue = reissue_percent < 100.0 - internal::kFullPercentEps;
11721 if (const auto consumed = consumed_partial_exit_cycles_.lookup(partial_exit_key);
11722 consumed && partial_reissue
11723 && live_exit_units > internal::kQtyEpsilon
11724 && *consumed == current_position_cycle_) {
11725 return;
11726 }
11727 if (partial_reissue && exit_tombstoned(exit_id, from_entry)) return;
11728 // A held global exit with relative legs over two or more entries covers
11729 // them entry by entry (per_entry_exit_call).
11730 if (from_entry.empty()
11731 && per_entry_exit_call(exit_id, from_entry, limit_price, stop_price, source_trail_points,
11732 source_trail_offset, source_trail_price, qty_percent, comment,
11733 qty, oca_name, profit_ticks, loss_ticks, command_sequence)) {
11734 return;
11735 }
11736 double entry_price = require_host().position_avg_price();
11737 const double tick = staged_.syminfo.mintick;
11738 bool parent_long = physical.signed_units > 0.0;
11739 bool known_parent_level = physical.signed_units != 0.0;
11740 // ab9714be src/source/pine_fills.cpp:4560-4564 (pending_order_level_resolved)
11741 // and 7208-7229: an exit's relative operands resolve against the LIVE
11742 // position once its from_entry filled this cycle (W23b); an empty from_entry
11743 // means "every entry", so the entry-bar priced-exit gate reads the opening
11744 // lot (W32a, ab9714be src/source/pine_strategy_commands.cpp:2595-2606,
11745 // pine_fills.cpp:7695-7728).
11746 const bool parent_filled_this_cycle = physical.signed_units != 0.0
11747 && !from_entry.empty()
11748 && [&] {
11749 const auto filled = cohorts_by_id_.find(from_entry);
11750 return filled != cohorts_by_id_.end() && !filled->second.opened.empty();
11751 }();
11752 const auto observe_staged_parent = [&](const PlacementSnapshot& parent) {
11753 if (parent_filled_this_cycle) return;
11754 if (!parent.opening || parent.family != PineOrderFamily::Entry) {
11755 return;
11756 }
11757 if (!from_entry.empty() && parent.source_id != from_entry) {
11758 return;
11759 }
11760 // A global exit called while a position is held binds to that
11761 // position, so an entry order of either side working beside it is
11762 // not its parent. Taking an order of the other side put the limit and
11763 // stop thresholds on the wrong side of the leg that closes the
11764 // position (settled outside its trigger, MatchRejected InvalidTerms);
11765 // taking a same-side add made its resting price the basis of a profit
11766 // or trail leg; and either deferred a leg to a fill that never came.
11767 // lab tv synthetics, NYSE:F 15m: each exit with the resting entry is
11768 // the exit without it, row for row (tests/fixtures/cross_side_exit:
11769 // the other side, with and without a margin requirement;
11770 // tests/fixtures/same_side_exit: a same-side limit, stop or capped
11771 // market add).
11772 if (from_entry.empty() && physical.signed_units != 0.0
11773 && detail::exit_binding_rule_switches().global_exit_binds_held_position) {
11774 return;
11775 }
11776 // A pending same-bar parent is the level basis only while its cohort
11777 // has no live exposure. With a lot of that id still open the exit
11778 // resolves against the LIVE position: ab9714be
11779 // materialize_relative_exit_prices_for_live_position (pine_fills.cpp:
11780 // 7208-7230) binds relative operands to position_entry_price_ of the
11781 // current cycle, never to a not-yet-filled re-entry's resting price.
11782 if (cohort_exposure_for(from_entry) > 0.0
11783 && ((physical.signed_units > 0.0) == parent.is_long)) {
11784 return;
11785 }
11786 parent_long = parent.is_long;
11787 known_parent_level = finite_positive(parent.exit_levels.limit);
11788 if (known_parent_level) entry_price = parent.exit_levels.limit;
11789 };
11790 for (const auto& parent : pending_same_bar_commands_)
11791 observe_staged_parent(parent.snapshot);
11792 for (const auto& parent : pending_entries_)
11793 observe_staged_parent(parent.snapshot);
11794 for (const auto& parent : pending_coof_requests_)
11795 observe_staged_parent(parent.snapshot);
11796 const auto cohort = cohorts_by_id_.find(from_entry);
11797 for (const auto& handle : live_handles_) {
11798 const auto parent = placement_.find(handle.incarnation);
11799 const bool already_opened = cohort != cohorts_by_id_.end()
11800 && std::find(cohort->second.opened.begin(), cohort->second.opened.end(), handle)
11801 != cohort->second.opened.end();
11802 if (parent != placement_.end() && !already_opened)
11803 observe_staged_parent(parent->second);
11804 }
11805 // ab9714be src/source/pine_fills.cpp:4560-4564: a pending same-id origin
11806 // never defers the levels once the id has filled in this cycle.
11807 if (cohort != cohorts_by_id_.end() && !parent_filled_this_cycle) {
11808 for (const auto position : live_origin_positions(cohort->second)) {
11809 const auto& origin = cohort->second.origins[position];
11810 const auto parent = placement_.find(origin.incarnation);
11811 if (parent == placement_.end() || !parent->second.opening
11812 || !origin_is_pending(origin)
11813 || std::find(cohort->second.opened.begin(), cohort->second.opened.end(), origin)
11814 != cohort->second.opened.end()) {
11815 continue;
11816 }
11817 // A prearmed bracket follows its pending MARKET/reversal parent,
11818 // not the opposite physical position still held at placement.
11819 // ab9714be pine_fills.cpp:7788-7800 then evaluates the child with
11820 // the parent's eventual close side and exact-touch direction.
11821 // The prearm serves the FLAT-to-position transition only: while a
11822 // lot of that id is live and the physical side already matches it,
11823 // the bracket resolves against the live position
11824 // (pine_fills.cpp:7208-7230), not against the same-id re-entry that
11825 // is still pending on this bar.
11826 const bool same_side_live = physical.signed_units != 0.0
11827 && (physical.signed_units > 0.0) == parent->second.is_long
11828 && cohort_exposure_for(from_entry) > 0.0;
11829 if (same_side_live) continue;
11830 parent_long = parent->second.is_long;
11831 // Relative profit/loss/trail operands bind to the parent's actual
11832 // fill, not its resting limit. An opening gap may improve that
11833 // fill, so keep these operands unresolved until the Applied event.
11834 known_parent_level = false;
11835 break;
11836 }
11837 }
11838 if (known_parent_level && finite_positive(entry_price) && finite_positive(tick)) {
11839 const bool long_side = physical.signed_units != 0.0 ? physical.signed_units > 0.0 : parent_long;
11840 if (!finite_positive(limit_price) && finite_positive(profit_ticks)) {
11841 limit_price = entry_price + (long_side ? 1.0 : -1.0) * profit_ticks * tick;
11842 if (!std::isfinite(limit_price)) {
11843 engine_command_value_invalid_or_count("relative_price.level",
11844 EngineValueStage::relative_price, EngineValueReason::numeric_overflow);
11845 }
11846 }
11847 if (!finite_positive(stop_price) && finite_positive(loss_ticks)) {
11848 stop_price = entry_price - (long_side ? 1.0 : -1.0) * loss_ticks * tick;
11849 if (!std::isfinite(stop_price)) {
11850 engine_command_value_invalid_or_count("relative_price.level",
11851 EngineValueStage::relative_price, EngineValueReason::numeric_overflow);
11852 }
11853 }
11854 if (std::isfinite(source_trail_points)) {
11855 const double trail_ticks = source_trail_points_ticks(
11856 source_trail_points, tick, value_field::exit_trail_points);
11857 trail_price = directional_tick(entry_price
11858 + (long_side ? 1.0 : -1.0) * trail_ticks * tick,
11859 tick, long_side);
11860 if (std::isfinite(trail_ticks) && !std::isfinite(trail_price)) {
11861 engine_command_value_invalid_or_count("relative_price.level",
11862 EngineValueStage::relative_price, EngineValueReason::numeric_overflow);
11863 }
11864 }
11865 }
11866 limit_price = source_level_on_price_grid(limit_price, tick);
11867 stop_price = source_level_on_price_grid(stop_price, tick);
11868 // A source trail offset is a tick count, which is the kernel's own
11869 // native_order::TrailTicks spelling: acceptance resolves it against the
11870 // run's price tick — the same syminfo mintick this adapter projects into
11871 // NativeRunSpec::price_tick (project()) — and stores a plain price
11872 // distance, so the adapter never multiplies a tick count by a tick.
11873 // Only the explicit-zero shape stays adapter policy: the legacy broker
11874 // rides the TICK-QUANTIZED running best, whereas the kernel's zero offset
11875 // rides the raw best and exits on any adverse ULP. Half a tick is exactly
11876 // that quantization boundary, and it is now spelled in ticks as well.
11877 std::optional<double> native_trail_offset_ticks;
11878 if (has_trail_request && std::isfinite(source_trail_offset)
11879 && source_trail_offset >= 0.0 && finite_positive(tick)) {
11880 const double offset_ticks = std::floor(source_trail_offset);
11881 native_trail_offset_ticks = offset_ticks == 0.0 ? 0.5 : offset_ticks;
11882 }
11883 const bool unresolved_trail = has_trail_request
11884 && !finite_positive(trail_price) && std::isfinite(source_trail_points);
11885 const bool unresolved_ticks =
11886 (!finite_positive(limit_price) && finite_positive(profit_ticks))
11887 || (!finite_positive(stop_price) && finite_positive(loss_ticks));
11888 const bool unresolved_relative = unresolved_trail || unresolved_ticks;
11889 // A void call leaves no relative leg for the entry's fill to resolve.
11890 if (unresolved_relative && !void_issue) {
11891 PendingRelativeExit pending;
11892 pending.exit_id = exit_id; pending.from_entry = from_entry;
11893 pending.trail_points = source_trail_points; pending.trail_offset = source_trail_offset;
11894 pending.trail_price = source_trail_price; pending.qty_percent = qty_percent;
11895 pending.comment = comment; pending.qty = qty; pending.oca_name = oca_name;
11896 pending.profit_ticks = profit_ticks; pending.loss_ticks = loss_ticks;
11897 pending.void_global = void_global_issue;
11898 auto existing = std::find_if(pending_relative_exits_.begin(), pending_relative_exits_.end(),
11899 [&](const PendingRelativeExit& value) {
11900 return value.exit_id == exit_id && value.from_entry == from_entry;
11901 });
11902 if (existing == pending_relative_exits_.end()) {
11903 pending_relative_exits_.push_back(std::move(pending));
11904 } else {
11905 const auto same = [](double left, double right) {
11906 return same_double_bits(left, right) || (std::isnan(left) && std::isnan(right));
11907 };
11908 const bool unchanged = same(existing->trail_points, pending.trail_points)
11909 && same(existing->trail_offset, pending.trail_offset)
11910 && same(existing->trail_price, pending.trail_price)
11911 && same(existing->qty_percent, pending.qty_percent)
11912 && same(existing->qty, pending.qty)
11913 && same(existing->profit_ticks, pending.profit_ticks)
11914 && same(existing->loss_ticks, pending.loss_ticks)
11915 && existing->comment == pending.comment
11916 && existing->oca_name == pending.oca_name;
11917 // The kernel child carries the definition it was anchored from.
11918 if (!unchanged) withdraw_anchored_relative_legs(&exit_id, &from_entry);
11919 *existing = std::move(pending);
11920 }
11921 }
11922 if (unresolved_relative) {
11923 pending_write(source_shadow_pending_).erase(
11924 std::remove_if(source_shadow_pending_.begin(), source_shadow_pending_.end(),
11925 [&](const SourceShadowPending& row) {
11926 return row.snapshot.source_id == exit_id
11927 && row.snapshot.from_entry == from_entry;
11928 }),
11929 source_shadow_pending_.end());
11930 // A known absolute sibling remains executable while the relative
11931 // trail/profit/loss component waits for its MARKET parent's fill.
11932 // ab9714be pine_strategy_commands.cpp:533-537: 0.0 is a present level.
11933 // Kernel validate_levels still refuses non-finite and negative
11934 // levels (A43); those stay command projections.
11935 if (finite_non_negative(limit_price) || finite_non_negative(stop_price)) {
11936 // Continue below and submit the known absolute sibling.
11937 } else {
11938 if (void_issue) return;
11939 PlacementSnapshot shadow;
11940 shadow.family = has_trail_request ? PineOrderFamily::ExitTrail
11941 : (finite_positive(loss_ticks) ? PineOrderFamily::ExitStop
11943 shadow.source_id = exit_id;
11944 shadow.from_entry = from_entry;
11945 shadow.comment = comment;
11946 shadow.oca_name = oca_name;
11947 shadow.requested_qty = qty;
11948 shadow.qty_percent = qty_percent;
11949 shadow.command_sequence = command_sequence;
11950 shadow.source_sequence = ++source_sequence_;
11951 shadow.projection_position_side = static_cast<std::int32_t>(PositionSide::FLAT);
11952 shadow.exit_levels = {limit_price, stop_price, source_trail_points,
11953 source_trail_offset, source_trail_price,
11954 profit_ticks, loss_ticks};
11955 shadow.sizing = sizing_snapshot();
11956 pending_write(source_shadow_pending_).push_back({std::move(shadow), exit_id});
11957 return;
11958 }
11959 }
11960 if (std::isnan(qty) && qty_percent == 100.0) {
11961 const auto cohort = cohorts_by_id_.find(from_entry);
11962 if (cohort != cohorts_by_id_.end() && !cohort->second.origins.members().empty()) {
11963 const auto origin = cohort->second.origins.back();
11964 // An erased origin's flag is read by nobody: a sequential group's
11965 // entries are kept while the group has a live or queued member,
11966 // and only a live entry lets a new one join it
11967 // (erase_retired_rows, K7).
11968 const auto parent = placement_.contains(origin.incarnation)
11969 ? placement_.find(origin.incarnation) : placement_.end();
11970 if (parent != placement_.end() && parent->second.family == PineOrderFamily::Entry)
11971 parent->second.has_full_entry_bracket = true;
11972 }
11973 }
11974 const bool dynamic = std::isnan(qty);
11975 const auto family_key = key_for(exit_id, from_entry);
11976 const bool defer_for_same_bar_priority = !config_.calc_on_order_fills
11977 && std::any_of(pending_entries_.begin(), pending_entries_.end(),
11978 [](const PendingEntry& pending) {
11979 return pending.snapshot.retained_parent_topology;
11980 });
11981 const bool defer_for_same_bar_add_exit = dynamic
11982 && !config_.calc_on_order_fills && !coof_recalc_active_
11983 && physical.signed_units != 0.0 && !from_entry.empty()
11984 && (!std::isfinite(qty_percent) || qty_percent >= 100.0)
11985 && std::any_of(pending_entries_.begin(), pending_entries_.end(),
11986 [&](const PendingEntry& pending) {
11987 const auto& add = pending.snapshot;
11988 return add.opening && add.family == PineOrderFamily::Entry
11989 && add.source_id == from_entry
11990 && add.is_long == (physical.signed_units > 0.0)
11991 && !finite_positive(add.exit_levels.limit)
11992 && !finite_positive(add.exit_levels.stop)
11993 && !finite_positive(add.exit_levels.trail_offset)
11994 && add.oca_name.empty();
11995 });
11996 // The legacy pending book leaves an identical resting bracket untouched.
11997 // In the ordinary source-bar path, a dynamic exit resolves against its
11998 // live cohort only when it fills, so reissuing unchanged levels cannot
11999 // alter its executable terms. Avoid rebuilding two requests, snapshots,
12000 // and replacement events on the common every-bar bracket pattern.
12001 // A normalized full-percent reservation is `units / basis * 100`, which
12002 // can land a few binary64 ULPs below the literal 100% request. The owner
12003 // still treats the re-issued default bracket as the same full-position
12004 // reservation, so those representations are equivalent here.
12005 const auto same_dynamic_percent = [](double prior_percent, double request_percent) {
12006 if (std::isfinite(prior_percent) && std::isfinite(request_percent)
12007 && prior_percent >= 100.0 - 1e-12
12008 && request_percent >= 100.0 - 1e-12) {
12009 return true;
12010 }
12011 return same_double_bits(prior_percent, request_percent);
12012 };
12013 const auto unchanged_dynamic_leg = [&](PineOrderFamily family, double level) {
12014 const SourceId replacement_key = exit_id + "\x1f" + from_entry
12015 + std::to_string(static_cast<int>(family));
12016 const auto live = live_by_source_key_.find(checked_live_key(replacement_key));
12017 if (live == live_by_source_key_.end()) return false;
12018 const auto placement = placement_.find(live->second.incarnation);
12019 if (placement == placement_.end()) return false;
12020 const auto& prior = placement->second;
12021 if (prior.family != family || !prior.deferred_cohort || !std::isnan(prior.requested_qty)
12022 || prior.source_id != exit_id || prior.from_entry != from_entry
12023 || prior.comment != comment || prior.oca_name != oca_name
12024 || prior.oca_type != 0
12025 || !same_dynamic_percent(prior.qty_percent, qty_percent)
12026 || prior.bracket_origin.incarnation != 0
12027 || !same_double_bits(prior.exit_levels.limit, limit_price)
12028 || !same_double_bits(prior.exit_levels.stop, stop_price)
12029 || !std::isnan(prior.exit_levels.trail_points)
12030 || !std::isnan(prior.exit_levels.trail_offset)
12031 || !std::isnan(prior.exit_levels.trail_price)
12032 || !std::isnan(prior.exit_levels.profit_ticks)
12033 || !std::isnan(prior.exit_levels.loss_ticks)
12034 || reissue_revives_declined_exit(prior)) {
12035 return false;
12036 }
12037 return (family == PineOrderFamily::ExitLimit
12038 && same_double_bits(prior.exit_levels.limit, level))
12039 || (family == PineOrderFamily::ExitStop
12040 && same_double_bits(prior.exit_levels.stop, level));
12041 };
12042 // Only a live bracket: a leg whose from_entry holds no lot rested dormant
12043 // on the bars before, so re-issuing it on the calculation that places its
12044 // entry is a new exit of that entry, decided with it (lab tv
12045 // w4-f08r-pooc cells X and Y; wayward-bison's every-bar "Short Stop";
12046 // lane W4-ENG-POOC-SAMEPASS).
12047 const bool plain_dynamic_bracket = dynamic && !coof_recalc_active_
12048 && !materializing_relative_ && !config_.calc_on_order_fills
12049 && (from_entry.empty() || cohort_exposure_for(from_entry) > 0.0)
12050 && physical.signed_units != 0.0 && std::isnan(trail_points)
12051 && std::isnan(trail_offset) && std::isnan(trail_price)
12052 && std::isnan(profit_ticks) && std::isnan(loss_ticks)
12053 && (finite_positive(limit_price) || finite_positive(stop_price));
12054 if (plain_dynamic_bracket
12055 && (!finite_positive(limit_price)
12056 || unchanged_dynamic_leg(PineOrderFamily::ExitLimit, limit_price))
12057 && (!finite_positive(stop_price)
12058 || unchanged_dynamic_leg(PineOrderFamily::ExitStop, stop_price))) {
12059 // Made once the entry exists, the call takes over a void request it
12060 // leaves unchanged (unvoid_exit).
12061 for (const auto family : {PineOrderFamily::ExitLimit, PineOrderFamily::ExitStop}) {
12062 if (void_issue) break;
12063 const auto live = live_by_source_key_.find(checked_live_key(
12064 exit_id + "\x1f" + from_entry + std::to_string(static_cast<int>(family))));
12065 if (live == live_by_source_key_.end()) continue;
12066 if (const auto row = placement_.find(live->second.incarnation);
12067 row != placement_.end() && row->second.void_issue) {
12068 unvoid_exit(row->second);
12069 }
12070 }
12071 return;
12072 }
12073 // Every leg emitted by one source strategy.exit shares its placement-time
12074 // sizing facts. Submitting the first resting sibling cannot alter the
12075 // physical account, so capture them once rather than re-marking equity
12076 // for each leg.
12077 const PineSizingSnapshot exit_sizing = sizing_snapshot();
12078 const native_order::Owner dynamic_owner = dynamic
12079 ? owner_for_close(from_entry, !materializing_relative_)
12081 const SourceId dynamic_group_name = dynamic
12082 ? (oca_name.empty() ? exit_id + "\x1f" + from_entry : oca_name) : SourceId{};
12083 const SourceId dynamic_key_prefix = dynamic ? exit_id + "\x1f" + from_entry : SourceId{};
12084 const auto pending_default_reversal_parent = [&](const PlacementSnapshot& parent) {
12085 if (!parent.opening || parent.family != PineOrderFamily::Entry
12086 || parent.source_id != from_entry) {
12087 return false;
12088 }
12089 // A flat, ordinarily pending parent remains cohort-bound: the
12090 // deferred-ANY contract requires its child to survive a same-id
12091 // parent replacement and grow with the replacement. WaitForApplied
12092 // is only the source reversal-parent relation below.
12093 if (physical.signed_units == 0.0) return false;
12094 return !std::isfinite(parent.requested_qty)
12095 && parent.is_long != (physical.signed_units > 0.0);
12096 };
12097 bool binds_pending_reversal_entry = false;
12098 for (const auto& pending : pending_same_bar_commands_)
12099 binds_pending_reversal_entry = binds_pending_reversal_entry
12100 || pending_default_reversal_parent(pending.snapshot);
12101 for (const auto& pending : pending_entries_)
12102 binds_pending_reversal_entry = binds_pending_reversal_entry
12103 || pending_default_reversal_parent(pending.snapshot);
12104 for (const auto& handle : live_handles_) {
12105 const auto found = placement_.find(handle.incarnation);
12106 if (found != placement_.end()
12107 && pending_default_reversal_parent(found->second)) {
12108 binds_pending_reversal_entry = true;
12109 }
12110 }
12111 double reserved_exit_qty = kNaN;
12112 double pending_parent_units = 0.0;
12113 const auto reservation_point = detail::callback_point(require_host());
12114 const auto observe_pending_parent = [&](const PlacementSnapshot& parent) {
12115 if (!reservation_point || !parent.opening
12116 || parent.family != PineOrderFamily::Entry
12117 || parent.source_id != from_entry
12118 || parent.placement_script_open_ms
12119 != reservation_point->decision.script_bar_open_ms
12120 || parent.projection_over_pyramiding) {
12121 return;
12122 }
12123 const double units = finite_positive(parent.requested_qty)
12124 ? std::abs(parent.requested_qty) : parent.frozen_market_own_units;
12125 if (finite_positive(units)) pending_parent_units += units;
12126 };
12127 for (const auto& handle : live_handles_) {
12128 const auto parent = placement_.find(handle.incarnation);
12129 if (parent != placement_.end()) observe_pending_parent(parent->second);
12130 }
12131 for (const auto& parent : pending_entries_) observe_pending_parent(parent.snapshot);
12132 // TradingView's default strategy.exit(from_entry) under FIFO closes the
12133 // entry's own quantity -- the oldest lots first -- not the whole book
12134 // (tests/test_pyramiding_count_differential.cpp, P10 on its lab tv tape).
12135 const bool fifo_named_entry = !config_.close_entries_rule_any && !from_entry.empty();
12136 // The tests/fixtures/percent_entry_reservation tapes prove named, absolute,
12137 // limit-only targets size from still-live entry ancestry after partial/runner and
12138 // margin slices complete; reservation capacity remains live exposure.
12139 // Two-sided brackets retain main's pending-close/margin basis: those tapes
12140 // do not cover their completed-partial sizing, and the broader rule changed
12141 // physical outcomes in 36 frozen live-state transcripts without TV proof.
12142 const bool ancestry_sized_target = !from_entry.empty() && std::isfinite(limit_price)
12143 && std::isnan(stop_price) && std::isnan(profit_ticks)
12144 && std::isnan(loss_ticks) && !has_trail_request;
12145 // The non-ancestry path keeps W5's margin rule: percent exits include the
12146 // entry's margin slices (tests/fixtures/margin_v6 w5-qp-*; 35% of 62.3911
12147 // despite prior slices of 0.0028 and 1.6712; lane W5-ENG-MARGIN-V6).
12148 double completed_entry_units = 0.0;
12149 if (std::isnan(qty) && std::isfinite(requested_qty_percent)
12150 && requested_qty_percent < 100.0 && physical.signed_units != 0.0
12151 && position_open_script_bar_ != std::numeric_limits<std::int64_t>::min()) {
12152 const auto& host = require_host();
12153 const auto* pine = pine_view_of(host_);
12154 const std::size_t base = pine ? pine->trades_.base() : 0;
12155 observe_position_tail_rows();
12156 if (!position_tail_summary_)
12157 position_tail_summary_ = std::make_shared<detail::AdapterPositionTailSummary>();
12158 auto& summary = *position_tail_summary_;
12159#ifdef PINEFORGE_POSITION_TAIL_BEFORE_QUERY
12160 PINEFORGE_POSITION_TAIL_BEFORE_QUERY(summary);
12161#endif
12162 const bool certified = ancestry_sized_target
12163 ? summary.ancestry(physical.signed_units > 0.0, [&](const auto& include) {
12164 // Membership is the CURRENT live map, not the opening
12165 // roster or a subtotal minus the most recently drained lot.
12166 using CohortRow = std::remove_reference_t<decltype(*cohorts_by_id_.begin())>;
12167 summary.for_each_live_cohort([&](const void* row) {
12168 const auto& cohort = *static_cast<const CohortRow*>(row);
12169 if (fifo_named_entry && cohort.first != from_entry) return;
12170 for (const auto& origin : cohort.second.live_units_by_origin)
12171 include(origin.first);
12172 });
12173 }, completed_entry_units, PINEFORGE_POSITION_TAIL_DIAGNOSTICS{})
12174 : summary.margin(physical.signed_units > 0.0,
12175 fifo_named_entry ? &from_entry : nullptr, completed_entry_units,
12177 const auto include_completed = [&](bool is_long, std::uint64_t incarnation,
12178 bool margin_call, const auto& named_entry_matches) {
12179 if (is_long != (physical.signed_units > 0.0)) return false; const auto held = cohorts_by_id_.held_rows(); // one range: one exactness assert
12180 return ancestry_sized_target
12181 ? std::any_of(held.begin(), held.end(),
12182 [&](const auto& cohort) {
12183 return (!fifo_named_entry || cohort.first == from_entry)
12184 && cohort.second.live_units_by_origin.count(
12185 incarnation) != 0;
12186 })
12187 : margin_call && (!fifo_named_entry || named_entry_matches());
12188 };
12189 if (!certified)
12190 completed_entry_units = detail::replay_position_tail(
12191 host.closed_trade_count(), base,
12192 [&](std::size_t index) -> Trade {
12193 return host.get_trade(static_cast<int>(index));
12194 }, position_tail_spill_.get(), position_open_script_bar_,
12195 include_completed, from_entry, summary.work,
12197#ifdef PINEFORGE_POSITION_TAIL_OBSERVE
12198 // Test-TU observer: the reference walk is never linked into runtime.
12199 PINEFORGE_POSITION_TAIL_OBSERVE(completed_entry_units, certified);
12200#endif
12201 }
12202 const double book_basis = (fifo_named_entry
12203 ? std::min(std::abs(physical.signed_units), cohort_exposure_for(from_entry))
12204 : std::abs(physical.signed_units)) + (ancestry_sized_target ? 0.0 : completed_entry_units);
12205 const double live_reservation_basis = binds_pending_reversal_entry ? 0.0
12206 : std::max(0.0, book_basis
12207 - pending_same_bar_close_qty_ + pending_parent_units);
12208 const double percentage_basis = ancestry_sized_target
12209 ? book_basis + completed_entry_units + pending_parent_units
12210 : live_reservation_basis;
12211 const bool reservation_ok = compute_exit_reservation(exit_id, from_entry, qty, qty_percent,
12212 live_reservation_basis, percentage_basis, reserved_exit_qty);
12213 if (!reservation_ok) {
12214 // clear_existing_exit_order ran before sizing on the legacy route:
12215 // a zero-capacity reissue removes its predecessor as well as refusing
12216 // the replacement (ab9714be:pine_strategy_commands.cpp:1688-1699,
12217 // :2739-2811).
12218 exit_cancel_bracket(exit_id, from_entry, comment);
12219 return;
12220 }
12221 const auto source_point = detail::callback_point(require_host());
12222 const OrderBirth exit_birth = capture_order_birth();
12223 const auto exit_birth_reach = compat::pine::select_historical_birth_reach(
12224 exit_birth, has_trail_request);
12225 const bool historical_cascade =
12227 const bool pooc_short_tick_scope = source_point
12228 && config_.process_orders_on_close && !config_.calc_on_order_fills
12229 && modeled_input() && source_point->decision.sub_count <= 1
12230 && physical.signed_units < 0.0 && physical.lot_count == 1
12231 && position_open_script_bar_ < source_point->decision.script_bar_open_ms
12232 && config_.pyramiding == 0 && config_.slippage == 0
12233 && config_.commission_type == static_cast<int>(CommissionType::PERCENT)
12234 && std::abs(staged_.syminfo.pointvalue - 1.0) < 1e-12
12235 && active_staged_fx(source_point->decision.sub_bar_open_ms) == 1.0
12236 && staged_.account_fx_effective_from_ms.empty()
12237 && std::isnan(qty)
12238 && (!std::isfinite(qty_percent) || qty_percent >= 100.0 - 1e-9)
12239 && oca_name.empty() && !has_trail_request
12240 && finite_positive(staged_.syminfo.mintick);
12241 PineOrderFamily pooc_current_close_family = PineOrderFamily::Entry;
12242 if (pooc_short_tick_scope && source_point) {
12243 const double close = nearest_tick(source_point->price, staged_.syminfo.mintick);
12244 if (finite_positive(stop_price) && close >= stop_price)
12245 pooc_current_close_family = PineOrderFamily::ExitStop;
12246 else if (finite_positive(limit_price) && close <= limit_price)
12247 pooc_current_close_family = PineOrderFamily::ExitLimit;
12248 }
12249 auto submit_leg = [&](PineOrderFamily family, native_order::Trigger trigger) {
12250 bool defer_marketable_coof_stop = false;
12251 // A stop reached at the point its recalculating fill ended the leg on:
12252 // executed there, so never staged behind the recalculation.
12253 bool coof_stop_at_leg_end = false;
12254 bool coof_limit_waypoint_qualified = false;
12255 double coof_limit_waypoint_price = kNaN;
12256 double coof_stop_waypoint_price = kNaN;
12257 if (coof_recalc_active_ && !coof_first_open_ && historical_cascade
12258 && coof_script_bar_valid_) {
12259 const auto native = detail::run_state(require_host());
12260 const bool ordinary_path = !native.spec || native.spec->intrabar.is_none();
12261 const auto point = detail::callback_point(require_host());
12262 if (ordinary_path && point && finite_positive(point->price)) {
12263 const auto phase = coof_context_.coordinate.path_phase;
12264 const bool high_first = source_path_uses_high_first(coof_script_bar_);
12265 double endpoint = next_source_path_waypoint(
12266 coof_script_bar_, phase, point->price, high_first,
12267 staged_.syminfo.mintick, config_.slippage);
12268 // The recalculating fill AT its leg's extreme: the matcher's
12269 // fill at a resting request's own level there ends the leg, so
12270 // no remainder of it is left in flight and a marketable level
12271 // is reached at the point itself (lab tv pa2-i2-level-w1-*,
12272 // pa3-f4-level-w1-xstop-*); a fill the adapter forced onto it
12273 // starts the next leg -- read from the extreme itself, whose
12274 // booked tick an off-grid extreme (11.425 -> 11.43) does not
12275 // equal (pa2-i2-point-w2-long*; the extreme-point fill correction).
12276 const double own_extreme = phase == NativePathPhase::High
12277 ? coof_script_bar_.high
12278 : (phase == NativePathPhase::Low ? coof_script_bar_.low : kNaN);
12279 const bool at_own_extreme = finite_positive(own_extreme)
12280 && coof_fill_at_path_point(own_extreme);
12281 const bool fill_ends_leg = at_own_extreme && !coof_fill_forced_;
12282 if (at_own_extreme && coof_fill_forced_) {
12283 endpoint = next_source_path_waypoint(
12284 coof_script_bar_, phase, own_extreme, high_first,
12285 staged_.syminfo.mintick, config_.slippage);
12286 }
12287
12288 const bool closing_long = physical.signed_units > 0.0;
12289 if (family == PineOrderFamily::ExitLimit
12290 && finite_positive(limit_price) && finite_positive(endpoint)) {
12291 // ab9714be pine_fills.cpp:563-583 (cascade Model S): a priced
12292 // exit born in a mid-bar fill recalculation is held on the
12293 // remainder of its in-flight leg. It gap-fills at the
12294 // leg-end waypoint when its level lies in that remainder
12295 // (engine_path_resolve.cpp:1016-1046) or when it is the
12296 // marketable LIMIT born from the second fill at O
12297 // (pine_strategy_commands.cpp:2009-2015), which is held
12298 // through O->W1 and gets one gap attempt at W1.
12299 const bool marketable = closing_long
12300 ? point->price >= limit_price : point->price <= limit_price;
12301 // The leg end is reached on its tick-built print, the
12302 // level staying raw: a buy limit at 11.608273 is not
12303 // reached by an L of 11.605, which prints 11.61, and a
12304 // sell limit at 10.145 is not reached by an H of 10.145,
12305 // which prints 10.14 (`lab tv` synthetics
12306 // tests/fixtures/callback_lifecycle runner-target-r1,
12307 // long-high-raw and short-low-raw, NYSE:F 15m, 4 identical
12308 // exports each).
12309 const double reach_level =
12311 ? source_trigger_threshold(limit_price, staged_.syminfo.mintick,
12312 !closing_long, true)
12313 : limit_price;
12314 const bool endpoint_satisfies = closing_long
12315 ? endpoint >= reach_level : endpoint <= reach_level;
12316 const bool endpoint_ahead = closing_long
12317 ? endpoint > point->price : endpoint < point->price;
12318 // An exit the recalculation re-issues unchanged was not
12319 // born there: it rests at its own level on the rest of the
12320 // leg, whatever fill recalculated -- a margin call's at the
12321 // high, an add's on the way down (lab tv w4-rex-coof cells
12322 // MC and ADD; lane W4-ENG-POOC-SAMEPASS). One the
12323 // recalculation places first is held (cell NEW).
12324 const bool in_flight_remainder = !fill_ends_leg && !marketable
12325 && endpoint_satisfies && endpoint_ahead
12326 && !unchanged_dynamic_leg(PineOrderFamily::ExitLimit, limit_price);
12327 const bool later_same_open = phase == NativePathPhase::Open
12328 && marketable;
12329 if (in_flight_remainder
12330 || (later_same_open && endpoint_satisfies && endpoint_ahead)) {
12331 // The waypoint fill books bar_fill_price(endpoint)
12332 // (pine_fills.cpp:7962-7966); past an OFF-grid
12333 // endpoint (L 11.195 booked 11.20) the limit is a
12334 // touch trigger or the kernel refuses the booking.
12335 const bool off_grid_endpoint = finite_positive(staged_.syminfo.mintick)
12336 && source_bar_fill_tick(endpoint, staged_.syminfo.mintick) != endpoint;
12337 trigger = native_order::Limit{endpoint, off_grid_endpoint};
12338 coof_limit_waypoint_qualified = true;
12339 coof_limit_waypoint_price = endpoint;
12340 } else if (later_same_open) {
12341 // Armed at W1: it fills there when W1 satisfies the
12342 // level, otherwise on a later leg's cross.
12343 const auto* threshold = std::get_if<native_order::Limit>(&trigger);
12344 trigger = native_order::StopLimit{
12345 endpoint, threshold ? threshold->price : limit_price};
12346 coof_limit_waypoint_qualified = endpoint_satisfies;
12347 } else if (marketable && !endpoint_ahead && !endpoint_satisfies
12348 && coof_remaining_recrosses(limit_price, closing_long)) {
12349 // Any other marketable level is held on the in-flight
12350 // leg and fills only on a later leg's exact cross
12351 // (pine_fills.cpp:576); the recross scope below decides
12352 // whether it stays on this bar.
12353 const auto* threshold = std::get_if<native_order::Limit>(&trigger);
12354 trigger = native_order::StopLimit{
12355 endpoint, threshold ? threshold->price : limit_price};
12356 }
12357 } else if (family == PineOrderFamily::ExitStop
12358 && finite_positive(stop_price)) {
12359 const bool marketable = closing_long
12360 ? point->price <= stop_price : point->price >= stop_price;
12361 if (marketable && fill_ends_leg) {
12362 // Reached at the point the fill ended its leg on: it
12363 // fills there, as a market close does. A stop at the
12364 // point itself, not a market leg: the recalculation
12365 // executes market newborns at their point, which a
12366 // cohort-bound leg (close_entries_rule "any") cannot
12367 // take, so it would book this price on a later bar.
12368 trigger = native_order::Stop{point->price};
12369 coof_stop_waypoint_price = point->price;
12370 coof_stop_at_leg_end = true;
12371 } else if (marketable && phase != NativePathPhase::Open
12372 && std::isfinite(qty) && finite_positive(endpoint)
12373 && !source_same_point(point->price, endpoint, staged_.syminfo.mintick)) {
12374 trigger = native_order::Stop{endpoint};
12375 coof_stop_waypoint_price = endpoint;
12376 } else if (marketable && phase == NativePathPhase::Open
12377 && finite_positive(endpoint)) {
12378 // The recalculation of a LATER fill at the bar's open
12379 // (a market newborn the first fill's recalculation
12380 // executed there) places it already through: live
12381 // from the first extreme, not the open -- filled at
12382 // the extreme's print when still through there, at
12383 // the open when that extreme is the open, otherwise
12384 // at its own level from the extreme on (lab tv
12385 // tailc-1856-open-newborn cells A/B/C; after the
12386 // first open fill's recalculation it fills at the
12387 // open, cell D and the long-add/short-add comparison; the placement and recalculation correction).
12388 const double tick = staged_.syminfo.mintick;
12389 const bool through_at_extreme = closing_long
12390 ? endpoint <= stop_price : endpoint >= stop_price;
12391 if (source_same_point(point->price, endpoint, tick)) {
12392 trigger = native_order::Stop{point->price};
12393 coof_stop_waypoint_price = point->price;
12394 coof_stop_at_leg_end = true;
12395 } else if (through_at_extreme) {
12396 trigger = native_order::Stop{endpoint};
12397 coof_stop_waypoint_price = endpoint;
12398 } else {
12399 trigger = native_order::Trail{std::abs(stop_price - endpoint), endpoint};
12400 coof_stop_waypoint_price = nearest_tick(stop_price
12401 + (closing_long ? -1.0 : 1.0) * config_.slippage * tick, tick);
12402 }
12403 // Live on this bar: never delayed as a wrong-side stop.
12404 coof_stop_at_leg_end = true;
12405 } else {
12406 defer_marketable_coof_stop = marketable;
12407 }
12408 }
12409 }
12410 }
12411 // An ordinary opening market-entry callback, including a carried
12412 // parent or a parent born in that callback, executes a wrong-side
12413 // stop at the coincident opening high for an unslipped short,
12414 // with or without margin
12415 // (tests/fixtures/coof_marketable_stop).
12416 // Other sides, slipped fills and magnified paths keep their existing
12417 // Stop projection; the older first-fill cells stay at their level.
12418 // Both activation and Market conversion use this same taped scope.
12419 const auto opening_parent = placement_.find(coof_market_entry_recalc_incarnation_);
12420 const auto opening_point = detail::callback_point(require_host());
12421 const bool carried_opening_parent = opening_point && opening_parent != placement_.end()
12422 && opening_parent->second.placement_script_open_ms
12423 < opening_point->decision.script_bar_open_ms;
12424 const bool opening_high = coof_script_bar_valid_
12425 && source_decimal_tick(coof_script_bar_.open, staged_.syminfo.mintick)
12426 == source_decimal_tick(coof_script_bar_.high, staged_.syminfo.mintick);
12427 const bool opening_market_short = opening_market_short_scope(family);
12428 if (coof_recalc_active_ && (coof_first_open_ || opening_market_short)
12429 && family == PineOrderFamily::ExitStop
12430 && finite_positive(stop_price) && physical.signed_units != 0.0) {
12431 const auto native = detail::run_state(require_host());
12432 const auto point = detail::callback_point(require_host());
12433 const bool ordinary_path = !native.spec || native.spec->intrabar.is_none();
12434 if (ordinary_path && point && finite_positive(point->price)
12435 && (coof_first_open_ || point->decision.coordinate.path_phase == NativePathPhase::Open)
12436 && (physical.signed_units > 0.0 ? stop_price > point->price
12437 : stop_price < point->price)) {
12438 trigger = native_order::Stop{point->price};
12439 coof_stop_waypoint_price = point->price;
12440 coof_stop_at_leg_end = true;
12441 if (opening_market_short) {
12442 trigger = native_order::Market{};
12443 defer_marketable_coof_stop = false;
12444 }
12445 }
12446 }
12447 // A limit that recalculation places at or through the bar's open
12448 // print fills there at once, at the print and without slippage; one
12449 // short of the print rests at its level on the rest of the entry
12450 // bar. The adapter held both for the next bar: it judged them against
12451 // the slipped fill (lab tv tape tests/fixtures/coof_open_limit
12452 // td-fp17-coof-open cells LA, SB, LE and LG; lane TAIL-D).
12453 bool coof_open_limit_rests = false;
12454 if (coof_recalc_active_ && coof_first_open_ && family == PineOrderFamily::ExitLimit
12455 && finite_positive(limit_price) && physical.signed_units != 0.0
12456 && coof_script_bar_valid_) {
12457 const auto native = detail::run_state(require_host());
12458 const auto point = detail::callback_point(require_host());
12459 const bool ordinary_path = !native.spec || native.spec->intrabar.is_none();
12460 const double open_print = coof_script_bar_.open;
12461 if (ordinary_path && point && finite_positive(point->price)
12462 && finite_positive(open_print)) {
12463 const bool closing_long = physical.signed_units > 0.0;
12464 if (closing_long ? limit_price <= open_print : limit_price >= open_print) {
12465 trigger = native_order::Limit{limit_price};
12466 coof_limit_waypoint_price =
12467 source_bar_fill_tick(open_print, staged_.syminfo.mintick);
12468 coof_stop_at_leg_end = true;
12469 } else {
12470 coof_open_limit_rests = true;
12471 }
12472 }
12473 }
12474 if (pooc_short_tick_scope) {
12475 const double tick = staged_.syminfo.mintick;
12476 // The short exit is a buy. rounded(low) <= limit and
12477 // rounded(high) >= stop are compared on the grid INDEX of the
12478 // quantized extreme (ab9714be include/pineforge/engine.hpp:1268-
12479 // 1312, tick_grid_price: k / (1/mintick), so an on-grid level
12480 // equals its grid point). The k * mintick product sits one ULP
12481 // above a decimal on-grid level, which made an unbounded ULP walk
12482 // toward the level cross a whole tick; source_trigger_threshold
12483 // resolves the same half-tick boundary with a bounded walk.
12484 if (family == PineOrderFamily::ExitLimit && finite_positive(limit_price)) {
12485 trigger = native_order::Limit{source_trigger_threshold(
12486 limit_price, tick, /*is_buy=*/true, /*is_limit=*/true)};
12487 } else if (family == PineOrderFamily::ExitStop
12488 && finite_positive(stop_price)) {
12489 trigger = native_order::Stop{source_trigger_threshold(
12490 stop_price, tick, /*is_buy=*/true, /*is_limit=*/false)};
12491 }
12492 }
12493 auto submit_one = [&](native_order::Owner owner, bool host_sized,
12494 const SourceId& replacement_key, native_order::Group group,
12495 bool defer_new_instance,
12496 native_order::RequestHandle bracket_origin = {}) {
12497 native_order::Request request;
12498 request.intent = host_sized
12499 ? native_order::OrderIntent{native_order::HostSized{
12500 native_order::HostSizedKind::Close, std::nullopt}}
12501 : native_order::OrderIntent{native_order::Reduce{
12502 native_order::ExplicitUnits{qty}}};
12503 request.label = exit_id; request.comment = comment; request.trigger = trigger;
12504 request.owner = std::move(owner);
12505 request.group = std::move(group);
12507 snapshot.family = family;
12508 snapshot.source_id = exit_id;
12509 snapshot.from_entry = from_entry;
12510 snapshot.comment = comment;
12511 snapshot.oca_name = oca_name;
12512 snapshot.requested_qty = qty;
12513 snapshot.qty_percent = qty_percent; snapshot.deferred_cohort = host_sized;
12514 snapshot.reservation_deferred_to_pending_entry =
12515 binds_pending_reversal_entry;
12516 snapshot.void_issue = void_issue;
12517 snapshot.void_global_issue = void_global_issue;
12518 snapshot.pending_bound_exit = pending_bound_exit;
12519 snapshot.is_long = false;
12520 snapshot.command_sequence = command_sequence;
12521 snapshot.bracket_origin = std::move(bracket_origin);
12522 snapshot.exit_levels = {limit_price, stop_price, source_trail_points,
12523 source_trail_offset, source_trail_price,
12524 profit_ticks, loss_ticks};
12525 snapshot.birth = exit_birth;
12526 snapshot.birth_reach = exit_birth_reach;
12527 snapshot.trail_activation_level = trail_price;
12528 snapshot.sizing = exit_sizing;
12529 // ab9714be pine_fills.cpp:2680-2696: update_trail_best_for_bar_open initializes running extreme from placement bar close
12530 if (family == PineOrderFamily::ExitTrail && std::isfinite(exit_sizing.price))
12531 snapshot.retained_trail_best = exit_sizing.price;
12532 snapshot.placement_cycle = current_position_cycle_;
12533 if (source_point) {
12534 snapshot.projection_created_bar =
12535 projection_bar_index(source_point->decision);
12536 snapshot.projection_position_side = physical.signed_units > 0.0
12537 ? static_cast<std::int32_t>(PositionSide::LONG)
12538 : (physical.signed_units < 0.0
12539 ? static_cast<std::int32_t>(PositionSide::SHORT)
12540 : static_cast<std::int32_t>(PositionSide::FLAT));
12541 snapshot.placement_script_open_ms =
12542 source_point->decision.script_bar_open_ms;
12543 snapshot.placement_sub_open_ms = source_point->decision.sub_bar_open_ms;
12544 }
12545 initialize_l4c_policy(snapshot, {});
12546 if (finite_positive(coof_limit_waypoint_price))
12547 snapshot.forced_execution_price = coof_limit_waypoint_price;
12548 else if (finite_positive(coof_stop_waypoint_price))
12549 snapshot.forced_execution_price = coof_stop_waypoint_price;
12550 // A stop leg the recalculation re-issues at the level its live
12551 // order already rests at was not born there: it stays live at
12552 // that level on the rest of the leg, whatever else the re-issue
12553 // changes -- the bracket a resting entry carried, re-issued by
12554 // the recalculation of that entry's mid-leg fill (lab tv tapes
12555 // tests/fixtures/coof_resting_bracket td-m2a and td-m2b; lane
12556 // TAIL-D). One an earlier recalculation holds stays held.
12557 const auto live_stop = live_by_source_key_.find(checked_live_key(replacement_key));
12558 const bool stop_leg_rests = live_stop != live_by_source_key_.end() && [&] {
12559 const auto prior = placement_.find(live_stop->second.incarnation);
12560 return prior != placement_.end()
12561 && prior->second.family == PineOrderFamily::ExitStop
12562 && same_double_bits(prior->second.exit_levels.stop, stop_price)
12563 && !finite_positive(prior->second.forced_execution_price);
12564 }();
12565 if (coof_recalc_active_ && !coof_first_open_ && historical_cascade
12566 && family == PineOrderFamily::ExitStop
12567 && finite_positive(stop_price) && !stop_leg_rests) {
12568 const auto point = detail::callback_point(require_host());
12569 const double birth = point ? point->price : kNaN;
12570 const double waypoint = coof_next_waypoint();
12571 const bool long_position = physical.signed_units > 0.0;
12572 const bool reached_on_next_leg = long_position
12573 ? (birth > stop_price && waypoint <= stop_price)
12574 : (birth < stop_price && waypoint >= stop_price);
12575 if (reached_on_next_leg) {
12576 snapshot.forced_execution_price = source_bar_fill_tick(
12577 waypoint, staged_.syminfo.mintick)
12578 + (long_position ? -1.0 : 1.0) * config_.slippage
12579 * staged_.syminfo.mintick;
12580 }
12581 }
12582 const double source_position = std::abs(detail::run_position(require_host()).signed_units);
12583 if (std::isfinite(reserved_exit_qty)) {
12584 snapshot.projection_remaining_qty = reserved_exit_qty;
12585 const bool foreign_pending_opening = std::any_of(
12586 live_handles_.begin(), live_handles_.end(), [&](const auto& handle) {
12587 const auto candidate = placement_.find(handle.incarnation);
12588 return candidate != placement_.end()
12589 && candidate->second.opening
12590 && candidate->second.source_id != from_entry;
12591 }) || std::any_of(
12592 pending_entries_.begin(), pending_entries_.end(),
12593 [&](const PendingEntry& candidate) {
12594 return candidate.snapshot.opening
12595 && candidate.snapshot.source_id != from_entry;
12596 });
12597 const double percent = std::isfinite(requested_qty_percent)
12598 ? requested_qty_percent : 100.0;
12599 snapshot.fixed_exit_reservation = std::isfinite(snapshot.requested_qty)
12600 || (!binds_pending_reversal_entry
12601 && physical.signed_units != 0.0
12602 && (percent < 100.0 - 1e-9
12603 || reserved_exit_qty < live_reservation_basis - 1e-9
12604 || (source_position > 0.0
12605 && reserved_exit_qty < source_position - 1e-9)
12606 || foreign_pending_opening));
12607 } else if (!binds_pending_reversal_entry && host_sized
12608 && !std::isfinite(snapshot.requested_qty)
12609 && source_position > 0.0) {
12610 const double percent = std::isfinite(snapshot.qty_percent)
12611 ? snapshot.qty_percent : 100.0;
12612 snapshot.projection_remaining_qty = quantize_close_units(source_position, percent);
12613 if (!from_entry.empty()) {
12614 std::map<SourceId, double> reserved_by_exit;
12615 for (const auto& handle : live_handles_) {
12616 const auto existing = placement_.find(handle.incarnation);
12617 if (existing == placement_.end()) continue;
12618 const auto& prior = existing->second;
12619 const bool exit = prior.family == PineOrderFamily::ExitLimit
12620 || prior.family == PineOrderFamily::ExitStop
12621 || prior.family == PineOrderFamily::ExitTrail;
12622 if (!exit || prior.from_entry != from_entry
12623 || prior.source_id == exit_id || prior.void_issue
12624 || !std::isfinite(prior.projection_remaining_qty)) {
12625 continue;
12626 }
12627 auto& held = reserved_by_exit[prior.source_id];
12628 held = std::max(held, prior.projection_remaining_qty);
12629 }
12630 double reserved = 0.0;
12631 for (const auto& row : reserved_by_exit) reserved += row.second;
12632 const double available = std::max(0.0, source_position - reserved);
12633 snapshot.projection_remaining_qty = std::min(
12634 snapshot.projection_remaining_qty, available);
12635 snapshot.fixed_exit_reservation = percent < 100.0
12636 || !reserved_by_exit.empty();
12637 }
12638 }
12639 if (config_.process_orders_on_close && from_entry.empty() && source_position > 0.0) {
12640 double requested = std::isfinite(snapshot.requested_qty)
12641 ? std::abs(snapshot.requested_qty)
12642 : (std::isfinite(snapshot.projection_remaining_qty)
12643 ? snapshot.projection_remaining_qty : source_position);
12644 double reserved = 0.0;
12645 for (const auto& handle : live_handles_) {
12646 const auto existing = placement_.find(handle.incarnation);
12647 if (existing == placement_.end()) continue;
12648 const auto& prior = existing->second;
12649 const bool global_exit = prior.from_entry.empty()
12650 && (prior.family == PineOrderFamily::ExitLimit
12651 || prior.family == PineOrderFamily::ExitStop
12652 || prior.family == PineOrderFamily::ExitTrail);
12653 if (!global_exit || prior.source_id == exit_id
12654 || !std::isfinite(prior.projection_remaining_qty)) {
12655 continue;
12656 }
12657 reserved += std::max(0.0, prior.projection_remaining_qty);
12658 }
12659 const double available = std::max(0.0, source_position - reserved);
12660 requested = std::min(requested, available);
12661 if (!(requested > 0.0)) return;
12662 snapshot.projection_remaining_qty = requested;
12663 }
12664 if (defer_marketable_coof_stop) {
12665 if (broker_open_epoch_ == std::numeric_limits<std::uint64_t>::max())
12666 throw coded<std::overflow_error>(RunFailureCode::engine_invariant, {},
12667 "source delayed market epoch exhausted");
12668 if (const auto point = detail::callback_point(require_host())) {
12669 snapshot.projection_created_bar =
12670 projection_bar_index(point->decision);
12671 snapshot.placement_script_open_ms =
12672 point->decision.script_bar_open_ms;
12673 snapshot.placement_sub_open_ms = point->decision.sub_bar_open_ms;
12674 }
12675 snapshot.projection_created_during_coof = true;
12676 snapshot.projection_coof_at_terminal = coof_context_.is_terminal_sub_bar;
12677 snapshot.projection_coof_mid_bar = !coof_context_.is_terminal_sub_bar;
12678 delayed_market_orders_.push_back({
12679 std::move(request), std::move(snapshot), replacement_key,
12680 broker_open_epoch_ + 1U, true});
12681 return;
12682 }
12683 if (coof_recalc_active_ && physical.signed_units != 0.0) {
12684 const auto point = detail::callback_point(require_host());
12685 const double birth = point ? point->price : kNaN;
12686 const bool long_position = physical.signed_units > 0.0;
12687 const bool wrong_stop = family == PineOrderFamily::ExitStop
12688 && finite_positive(stop_price)
12689 && (long_position ? stop_price > birth : stop_price < birth);
12690 const bool wrong_limit = family == PineOrderFamily::ExitLimit
12691 && finite_positive(limit_price)
12692 && (long_position ? limit_price < birth : limit_price > birth);
12693 // Evaluated only for a wrong-side limit that can still hold:
12694 // the book scans below are otherwise dead on every reissue.
12695 const auto qualified_recross = [&] {
12696 if (coof_limit_waypoint_qualified) return true;
12697 if (!coof_remaining_recrosses(limit_price, long_position)) return false;
12698 const PlacementSnapshot* parent = nullptr;
12699 native_order::RequestHandle parent_handle{};
12700 if (const auto cohort = cohorts_by_id_.find(from_entry);
12701 cohort != cohorts_by_id_.end() && !cohort->second.opened.empty()) {
12702 parent_handle = cohort->second.opened.back();
12703 const auto found = placement_.find(parent_handle.incarnation);
12704 if (found != placement_.end()) parent = &found->second;
12705 }
12706 const bool plain_market_parent = parent
12707 && parent->family == PineOrderFamily::Entry
12708 && !finite_positive(parent->exit_levels.limit)
12709 && !finite_positive(parent->exit_levels.stop)
12710 && !finite_positive(parent->exit_levels.trail_offset)
12711 && !finite_positive(parent->exit_levels.trail_price);
12712 if (!plain_market_parent) return false;
12713 const double next_waypoint = coof_next_waypoint();
12714 const bool reachable_stop = finite_positive(stop_price)
12715 && (long_position
12716 ? (next_waypoint <= stop_price
12717 || coof_script_bar_.close <= stop_price)
12718 : (next_waypoint >= stop_price
12719 || coof_script_bar_.close >= stop_price));
12720 if (reachable_stop) return false;
12721 const bool competing_opening = std::any_of(
12722 live_handles_.begin(), live_handles_.end(), [&](const auto& handle) {
12723 if (handle == parent_handle) return false;
12724 const auto candidate = placement_.find(handle.incarnation);
12725 return candidate != placement_.end()
12726 && candidate->second.opening;
12727 });
12728 if (competing_opening) return false;
12729 const bool direct_partial = point && immediate_close_placed_on(
12730 point->decision.coordinate.interval_index);
12731 return !direct_partial;
12732 };
12733 if ((wrong_stop || (wrong_limit && !coof_open_limit_rests))
12734 && !coof_stop_at_leg_end
12735 && (coof_first_open_ || wrong_stop || !qualified_recross())) {
12736 snapshot.defer_until_post_parent_calculation = true;
12737 delayed_market_orders_.push_back({
12738 std::move(request), std::move(snapshot), replacement_key,
12739 broker_open_epoch_ + 1U});
12740 return;
12741 }
12742 }
12743 const auto native = detail::run_state(require_host());
12744 const bool stage_chart_tick_scope = config_.calc_on_order_fills
12745 && !config_.process_orders_on_close
12746 && coof_recalc_active_ && !defer_coof_tail() && !coof_stop_at_leg_end
12747 && (!native.spec || native.spec->intrabar.is_none())
12748 && (family == PineOrderFamily::ExitLimit
12749 || family == PineOrderFamily::ExitStop);
12750 if (stage_chart_tick_scope) {
12751 snapshot.projection_created_during_coof = true;
12752 snapshot.projection_coof_at_terminal = coof_context_.is_terminal_sub_bar;
12753 snapshot.projection_coof_mid_bar = !coof_context_.is_terminal_sub_bar;
12754 auto queued = std::find_if(
12755 pending_bracket_legs_.begin(), pending_bracket_legs_.end(),
12756 [&](const PendingBracketLeg& row) {
12757 return row.replacement_key == replacement_key;
12758 });
12759 PendingBracketLeg staged{std::move(request), std::move(snapshot),
12760 replacement_key, family_key};
12761 if (queued == pending_bracket_legs_.end())
12762 pending_write(pending_bracket_legs_).push_back(std::move(staged));
12763 else
12764 pending_write(*queued) = std::move(staged);
12765 return;
12766 }
12767 if (auto queued = std::find_if(
12768 pending_bracket_legs_.begin(), pending_bracket_legs_.end(),
12769 [&](const PendingBracketLeg& row) {
12770 return row.replacement_key == replacement_key
12771 && row.snapshot.defer_until_post_parent_calculation;
12772 }); queued != pending_bracket_legs_.end()) {
12773 const std::uint64_t predecessor =
12774 queued->snapshot.legs.target().incarnation != 0
12775 ? queued->snapshot.legs.target().incarnation
12776 : queued->snapshot.projection_predecessor;
12777 snapshot.projection_predecessor = predecessor;
12778 if (!queued->snapshot.void_issue || snapshot.void_issue)
12779 snapshot.source_sequence = queued->snapshot.source_sequence;
12780 snapshot.defer_until_post_parent_calculation = true;
12781 if (const auto point = detail::callback_point(require_host())) {
12782 snapshot.projection_created_bar =
12783 projection_bar_index(point->decision);
12784 snapshot.projection_position_side =
12785 static_cast<std::int32_t>(PositionSide::FLAT);
12786 }
12787 pending_write(*queued) = PendingBracketLeg{std::move(request), std::move(snapshot),
12788 replacement_key, family_key};
12789 return;
12790 }
12791 if (defer_coof_tail() && !finite_positive(coof_stop_waypoint_price)) {
12792 pending_write(pending_coof_requests_).push_back({std::move(request), std::move(snapshot), replacement_key,
12793 false, family_key, true});
12794 return;
12795 }
12796 if (snapshot.reservation_deferred_to_pending_entry
12797 && physical.signed_units == 0.0
12798 && !(cohort_exposure_for(snapshot.from_entry) > 0.0)) {
12799 auto queued = std::find_if(pending_bracket_legs_.begin(),
12800 pending_bracket_legs_.end(), [&](const PendingBracketLeg& row) {
12801 return row.replacement_key == replacement_key;
12802 });
12803 PendingBracketLeg staged{std::move(request), std::move(snapshot),
12804 replacement_key, family_key};
12805 if (queued == pending_bracket_legs_.end())
12806 pending_write(pending_bracket_legs_).push_back(std::move(staged));
12807 else
12808 pending_write(*queued) = std::move(staged);
12809 return;
12810 }
12811 bool defer_for_live_parent = false;
12812 // ab9714be src/source/pine_fills.cpp:7461-7468: while flat, an exit
12813 // created while flat is skipped rather than removed, and waits in
12814 // the book for the position to open. Defer the leg until its parent
12815 // entry applies so the native core does not erase it while flat.
12816 // src/source/pine_strategy_commands.cpp:2595-2606 makes an empty
12817 // from_entry mean "every entry", so any pending opening entry is
12818 // its parent; a named parent keeps the (id, from_entry) key that
12819 // carries the queue position across a reissue
12820 // (src/source/pine_strategy_commands.cpp:2627-2648).
12821 const auto existing_leg = live_by_source_key_.find(checked_live_key(replacement_key));
12822 if (!defer_for_same_bar_priority
12823 && detail::run_position(require_host()).signed_units == 0.0) {
12824 const auto matches_parent = [&](const PlacementSnapshot& row) {
12825 return row.opening && row.family == PineOrderFamily::Entry
12826 && (snapshot.from_entry.empty()
12827 || row.source_id == snapshot.from_entry);
12828 };
12829 bool has_pending_parent = false;
12830 for (const auto& entry : pending_entries_) {
12831 if (matches_parent(entry.snapshot)) {
12832 has_pending_parent = true;
12833 break;
12834 }
12835 }
12836 for (const auto& entry : pending_same_bar_commands_) {
12837 if (matches_parent(entry.snapshot)) {
12838 has_pending_parent = true;
12839 break;
12840 }
12841 }
12842 for (const auto& live : live_handles_) {
12843 const auto parent = placement_.find(live.incarnation);
12844 if (parent == placement_.end()) continue;
12845 if (!matches_parent(parent->second)) continue;
12846 has_pending_parent = true;
12847 break;
12848 }
12849 if (has_pending_parent
12850 && (existing_leg != live_by_source_key_.end()
12851 || snapshot.from_entry.empty())) {
12852 defer_for_live_parent = true;
12853 }
12854 }
12855 if (defer_for_same_bar_priority || defer_for_live_parent) {
12856 if (const auto existing = live_by_source_key_.find(checked_live_key(replacement_key));
12857 existing != live_by_source_key_.end()) {
12858 const auto predecessor = existing->second;
12859 const auto previous = placement_.find(predecessor.incarnation);
12860 if (previous != placement_.end()) {
12861 snapshot.projection_predecessor = predecessor.incarnation;
12862 if (!previous->second.void_issue || snapshot.void_issue)
12863 snapshot.source_sequence = previous->second.source_sequence;
12864 }
12865 (void)require_host().cancel(predecessor);
12866 retire(predecessor);
12867 }
12868 if (const auto point = detail::callback_point(require_host())) {
12869 snapshot.projection_created_bar = projection_bar_index(point->decision);
12870 snapshot.projection_created_bar_pinned = true;
12871 snapshot.projection_position_side = static_cast<std::int32_t>(PositionSide::FLAT);
12872 }
12873 auto queued = std::find_if(pending_bracket_legs_.begin(), pending_bracket_legs_.end(),
12874 [&](const PendingBracketLeg& row) {
12875 return row.replacement_key == replacement_key;
12876 });
12877 PendingBracketLeg staged{std::move(request), std::move(snapshot), replacement_key,
12878 family_key};
12879 if (queued == pending_bracket_legs_.end())
12880 pending_write(pending_bracket_legs_).push_back(std::move(staged));
12881 else
12882 pending_write(*queued) = std::move(staged);
12883 return;
12884 }
12885 if (defer_new_instance && live_by_source_key_.find(checked_live_key(replacement_key))
12886 == live_by_source_key_.end()) {
12887 auto queued = std::find_if(pending_bracket_legs_.begin(), pending_bracket_legs_.end(),
12888 [&](const PendingBracketLeg& row) { return row.replacement_key == replacement_key; });
12889 PendingBracketLeg staged{std::move(request), std::move(snapshot), replacement_key, family_key};
12890 if (queued == pending_bracket_legs_.end()) pending_write(pending_bracket_legs_).push_back(std::move(staged));
12891 else pending_write(*queued) = std::move(staged);
12892 return;
12893 }
12894 // ab9714be src/source/pine_strategy_commands.cpp:2627-2648 keys a
12895 // source bracket by (exit id, from_entry) and a reissue replaces
12896 // the stored leg rather than adding a second one. A leg that was
12897 // staged for a pending parent is that stored leg, so the freshly
12898 // submitted instance supersedes it; otherwise the stale staged
12899 // levels resurrect when a later parent entry applies.
12900 for (auto stale = pending_bracket_legs_.begin();
12901 stale != pending_bracket_legs_.end();) {
12902 if (stale->replacement_key == replacement_key)
12903 stale = pending_write(pending_bracket_legs_).erase(stale);
12904 else
12905 ++stale;
12906 }
12907 const std::uint64_t placement_high_water = placement_.high_water();
12908 const bool opening_stop_now = family == PineOrderFamily::ExitStop
12909 && opening_market_short && coof_stop_at_leg_end
12910 && opening_point && opening_point->decision.coordinate.path_phase == NativePathPhase::Open && std::holds_alternative<native_order::Market>(request.trigger);
12911 snapshot.coof_open_stop_next_waypoint = opening_stop_now && !coof_first_open_
12912 && opening_high && !carried_opening_parent;
12913 const bool continue_after_open = snapshot.coof_open_stop_next_waypoint;
12914 const auto accepted = submit_or_replace(std::move(request), std::move(snapshot), false,
12915 replacement_key);
12916 if (accepted) {
12917 auto& family = bracket_family(family_key);
12918 // Every family member was remembered before this submission,
12919 // so a successor above the prior placement high-water cannot
12920 // already be present; only a returned existing handle needs
12921 // the membership scan.
12922 if (accepted->incarnation > placement_high_water || !family.holds(*accepted)) {
12923 family.push_back(*accepted);
12924 }
12925 if (opening_stop_now) {
12926 const auto outcome = require_host().execute_current(
12927 {*accepted, NativeCurrentPriceRule::NearestTick});
12928 if (continue_after_open
12929 && std::holds_alternative<native_order::ExecutionAppliedEvent>(outcome)) {
12930 coof_open_stop_next_waypoint_ = true;
12931 }
12932 }
12933 }
12934 };
12935
12936 if (dynamic) {
12937 native_order::RequestHandle pending_origin{};
12938 bool defer_until_parent = false;
12939 if (!from_entry.empty()) {
12940 const auto cohort = cohorts_by_id_.find(from_entry);
12941 if (cohort != cohorts_by_id_.end() && cohort->second.opened.empty()) {
12942 for (const auto position : live_origin_positions(cohort->second)) {
12943 const auto& origin = cohort->second.origins[position];
12944 if (origin_is_pending(origin)) {
12945 pending_origin = origin;
12946 defer_until_parent = true;
12947 break;
12948 }
12949 }
12950 }
12951 }
12952 native_order::Owner owner = dynamic_owner;
12953 if (defer_until_parent) {
12954 const auto cohort = cohorts_by_id_.find(from_entry);
12955 if (cohort != cohorts_by_id_.end())
12956 owner = native_order::BindCohort{cohort->second.handle};
12957 defer_until_parent = false;
12958 }
12959 submit_one(std::move(owner), true,
12960 dynamic_key_prefix + std::to_string(static_cast<int>(family)),
12961 group_for(dynamic_group_name, 1, static_cast<std::int64_t>(family)),
12962 defer_until_parent || defer_for_same_bar_add_exit,
12963 pending_origin);
12964 return;
12965 }
12966
12967 // An explicit global exit is still a source-sized close over the
12968 // generic book. There is no empty-id cohort to bind; terms supplies
12969 // the literal units at the native candidate.
12970 if (from_entry.empty()) {
12971 const auto group_name = oca_name.empty() ? exit_id + "\x1f" + from_entry : oca_name;
12972 submit_one(native_order::Independent{}, true,
12973 exit_id + "\x1f" + from_entry + std::to_string(static_cast<int>(family)),
12974 group_for(group_name, 1, static_cast<std::int64_t>(family)), false);
12975 return;
12976 }
12977
12978 // An explicit bracket quantity is one independently persistent leg
12979 // for every source entry provenance, including an origin that is
12980 // still pending. BindCohort keeps that pending-origin leg deferred
12981 // without inventing a source id in the generic core; the source key
12982 // gives re-issues replacement semantics per (exit, from_entry, leg,
12983 // origin) rather than accidentally replacing a carried instance.
12984 const auto cohort = cohort_for(from_entry);
12985 const auto found = cohorts_by_id_.find(from_entry);
12986 std::vector<native_order::RequestHandle> origins;
12987 if (found != cohorts_by_id_.end()) {
12988 // ab9714be keys (exit id, from_entry) per position cycle
12989 // (cycle_filled_entry_ids_ is cleared on flat). Stale origins
12990 // from a prior cycle are neither pending nor opened; keeping
12991 // them prevents the empty-origins origin-zero fallback a first
12992 // cycle uses while the new parent is still in
12993 // pending_same_bar_commands_ (probe9 A v0 / the pending-parent probe).
12994 // Retired origins cannot be pending or opened; collect global
12995 // positions from the retained index and keep roster order.
12996 const auto& roster = found->second.origins;
12997 std::vector<std::size_t> positions;
12998 if (const auto* zero = roster.positions_of(0))
12999 positions.insert(positions.end(), zero->begin(), zero->end());
13000 for (const auto& live : live_handles_)
13001 if (origin_is_pending(live)) roster.append_positions_of(live, positions);
13002 for (const auto& opened : found->second.opened)
13003 roster.append_positions_of(opened, positions);
13004 std::sort(positions.begin(), positions.end());
13005 positions.erase(std::unique(positions.begin(), positions.end()), positions.end());
13006 for (const auto position : positions) origins.push_back(roster[position]);
13007 }
13008 if (origins.empty()) origins.push_back({});
13009 for (const auto& origin : origins) {
13010 const std::string origin_key = std::to_string(origin.incarnation);
13011 const auto replacement_key = exit_id + "\x1f" + from_entry + "\x1f"
13012 + std::to_string(static_cast<int>(family)) + "\x1f" + origin_key;
13013 const auto group_name = oca_name.empty()
13014 ? exit_id + "\x1f" + from_entry + "\x1f" + origin_key
13015 : oca_name + (origin.incarnation != 0 ? "\x1f" + origin_key : "");
13016 const bool has_live_leg = live_by_source_key_.find(checked_live_key(replacement_key))
13017 != live_by_source_key_.end();
13018 const bool origin_opened = found != cohorts_by_id_.end()
13019 && std::find(found->second.opened.begin(), found->second.opened.end(), origin)
13020 != found->second.opened.end();
13021 // The consumed-leg lookup decides the skip only: a bracket
13022 // reissued on every bar keeps its live leg and never reaches it.
13023 const auto consumed_origin_leg = [&] {
13024 if (const auto archived = settled_bracket_families_.find(family_key);
13025 archived != settled_bracket_families_.end()) {
13026 PINEFORGE_LOOKUP_INDEX_WORK("archived_family_probe", this, run_counter_, 1);
13027 PINEFORGE_LOOKUP_INDEX_WORK("archived_family_index_eligible", this, run_counter_,
13028 archived->second.count > kIndexedFrom ? 1 : 0);
13029 // Restore only the requested immutable prefix, as an append
13030 // does. Its logical size selects the existing index path;
13031 // no retired handles or placement rows are materialized.
13032 (void)bracket_family(family_key);
13033 }
13034 return origin_leg_consumed(family_key, family, origin);
13035 };
13036 if (origin.incarnation != 0 && !has_live_leg && !origin_is_pending(origin)
13037 && (!origin_opened || consumed_origin_leg())) {
13038 continue;
13039 }
13040 native_order::Owner owner = !void_issue && !config_.close_entries_rule_any
13041 && origin_opened
13042 ? native_order::Owner{native_order::Independent{}}
13043 : native_order::Owner{native_order::BindCohort{cohort}};
13044 // ab9714be pine_orders.cpp:483-488: cancel_oca_group scopes cancellation strictly to matching oca_name group
13045 submit_one(std::move(owner), true, replacement_key,
13046 group_for(group_name, 1, static_cast<std::int64_t>(family)),
13047 !has_live_leg, origin);
13048 }
13049 };
13050 const bool exit_is_buy = !parent_long;
13051 // ab9714be pine_fills.cpp:7672-7675: an EXIT whose ``from_entry`` never
13052 // filled in the CURRENT position cycle is Removed before it can rest, and
13053 // the id set is cleared the moment the book goes flat or flips. A limit
13054 // level that is only marketable against the side held at issue time is
13055 // therefore never a live leg while its parent id belongs to a cycle that
13056 // has not opened yet; without the removal the leg rests on its cohort
13057 // binding and flattens the reversal's fresh lot at that lot's own entry
13058 // price on the same bar.
13059 const auto cycle_cohort = from_entry.empty()
13060 ? cohorts_by_id_.end() : cohorts_by_id_.find(from_entry);
13061 const bool from_entry_filled_this_cycle = from_entry.empty()
13062 || (cycle_cohort != cohorts_by_id_.end()
13063 && !cycle_cohort->second.opened.empty());
13064 bool placed_absolute_leg = false;
13065 if (finite_non_negative(limit_price)) {
13066 submit_leg(PineOrderFamily::ExitLimit, native_order::Limit{
13067 exit_limit_trigger(limit_price, tick, exit_is_buy)});
13068 placed_absolute_leg = true;
13069 } else if (std::isfinite(limit_price) && limit_price < 0.0
13070 && physical.signed_units > 0.0 && from_entry_filled_this_cycle) {
13071 // Sell limit < 0 is always marketable; A43 still refuses negatives.
13072 submit_leg(PineOrderFamily::ExitLimit, native_order::Market{});
13073 placed_absolute_leg = true;
13074 }
13075 // ab9714be pine_strategy_commands.cpp:1933 stores a negative stop level
13076 // verbatim: a buy stop below zero is crossed by every print and fills at
13077 // the next reachable point like any gapped stop. A43 refuses negative
13078 // triggers, so it rests at 0.0 (source_trigger_threshold's floor), which
13079 // every print also crosses. A negative sell stop is never reachable, yet
13080 // the legacy book still rests it and it reserves its share of the
13081 // position against later partial exits (ab9714be pine_strategy_commands.cpp:
13082 // 1929-1953); at 0.0 it rests the same way and no positive print crosses it.
13083 if (finite_non_negative(stop_price)
13084 || (std::isfinite(stop_price) && stop_price < 0.0)) {
13085 const double native_stop = source_trigger_threshold(
13086 stop_price, tick, exit_is_buy, false);
13087 submit_leg(PineOrderFamily::ExitStop, native_order::Stop{native_stop});
13088 placed_absolute_leg = true;
13089 }
13090 bool trail_one_shot = false;
13091 if (has_trail_request && finite_positive(trail_price)) {
13092 // The legacy broker compares trail activation against tick-quantized
13093 // OHLC extremes while retaining the raw running best. The native
13094 // geometric matcher receives raw segments, so move only the arm
13095 // threshold by half a tick (toward the reachable side); placement
13096 // facts and the eventual source fill remain on the original level.
13097 double native_trail_price = trail_price;
13098 bool trail_already_reached = false;
13099 double placement_print = kNaN;
13100 const bool zero_distance = native_trail_offset_ticks
13101 && std::isfinite(source_trail_offset)
13102 && std::floor(source_trail_offset) == 0.0;
13103 if (finite_positive(tick)) {
13104 // ab9714be pine_fills.cpp:4592-4593: exit direction uses physical position side or exit intent direction
13105 const bool buy_close = physical.signed_units != 0.0
13106 ? physical.signed_units < 0.0 : exit_is_buy;
13107 const auto point = detail::callback_point(require_host());
13108 // The placement print is tested on the quantized path like every
13109 // later one: a close whose tick is the activation has reached it.
13110 const bool already_reached = point
13111 && source_trail_reached_at(point->price, trail_price, tick, buy_close);
13112 trail_already_reached = already_reached;
13113 if (point) placement_print = point->price;
13114 const bool no_trailing_distance = !native_trail_offset_ticks || zero_distance;
13115 // An omitted offset and an explicit offset that truncates to zero
13116 // are both one-shot activation legs until the activation is
13117 // reached. Once a zero-distance trail is already armed at the
13118 // placement point, retain the generic Trail so its raw best can
13119 // ride subsequent bars.
13120 trail_one_shot = no_trailing_distance && !already_reached;
13121 if (zero_distance && point) {
13122 // Once the activation is already reached at placement, the
13123 // explicit-zero trail's first live print is its carried
13124 // running best. Arm on that print's grid image so a later
13125 // adverse leg does not incorrectly ride a raw sub-tick high.
13126 if (already_reached) {
13127 native_trail_price = nearest_tick(point->price, tick);
13128 }
13129 }
13130 }
13131 if (trail_one_shot) {
13132 const double one_shot_level = one_shot_trail_trigger(
13133 native_trail_price, source_trail_offset, tick, exit_is_buy,
13134 !std::isfinite(source_trail_offset) || zero_distance);
13135 // ab9714be engine_path_resolve.cpp:984-987 books the crossed
13136 // activation as a TRAIL event (fill.is_limit = false), and
13137 // pine_fills.cpp:4396-4411 / pine_policy_members.cpp:45-53 then
13138 // route it through apply_slippage, not apply_limit_fill: the fill
13139 // lands slippage ticks WORSE than the activation. An explicit
13140 // sub-tick offset keeps the unslipped activation as its touch
13141 // level (a slipped level would fire early), so the leg is a
13142 // fill-through limit whose settlement may pass that level.
13143 const bool slipped_touch = zero_distance && config_.slippage > 0
13144 && finite_positive(tick);
13145 submit_leg(PineOrderFamily::ExitTrail,
13146 native_order::Limit{one_shot_level, slipped_touch});
13147 } else if (native_trail_offset_ticks) {
13148 // The trailing leg's arm is reached on the quantized path too
13149 // (source_trail_arm_level); the source level stays the activation
13150 // every placement fact and booking reads.
13151 std::optional<double> native_arm_price =
13152 source_trail_arm_level(native_trail_price, tick, exit_is_buy);
13153 // ab9714be engine_path_resolve.cpp:464-479: trailing exit already reached at placement arms immediately
13154 if (trail_already_reached)
13155 native_arm_price.reset();
13156 native_order::Trail native_trail{
13157 0.0, native_arm_price,
13158 native_order::TrailTicks{*native_trail_offset_ticks}};
13159 // TradingView's running best starts AT the activation, never
13160 // short of it: every exit of lane E5's six NYSE:F tapes books
13161 // activation +/- 1 tick, not the raw extreme snapped. The generic
13162 // arm gives the kernel a worse start in both shapes -- the arm
13163 // threshold, half a tick short, when a crossing arms it, and the
13164 // next bar's first print when the placement close already reached
13165 // it (lane E9's eight tapes) -- so the leg names the level its
13166 // ride starts at. Already armed, the placement print is part of
13167 // that ride and joins it on the favourable side: the same carried
13168 // best the explicit-zero trail's sibling stop rests at, raw.
13169 // The NYSE:F short controls pin the raw reading: synthetic
13170 // placement closes sit half a tick past the activation (25.005
13171 // under 25.01, 26.005 under 26.01) and TradingView's stop is
13172 // that close + the offset, reached by a high the activation's own
13173 // stop never would be.
13174 double best_seed = trail_price;
13175 if (trail_already_reached && std::isfinite(placement_print)) {
13176 best_seed = exit_is_buy ? std::min(best_seed, placement_print)
13177 : std::max(best_seed, placement_print);
13178 }
13179 if (finite_positive(best_seed)) native_trail.best_seed = best_seed;
13180 submit_leg(PineOrderFamily::ExitTrail, native_trail);
13181 } else if (trail_already_reached) {
13182 // ab9714be engine_path_resolve.cpp:634-639: omitted-offset trail already active at placement is marketable at next open
13183 // An omitted offset that was already activated at placement is
13184 // marketable at the next open.
13185 submit_leg(PineOrderFamily::ExitTrail, native_order::Market{});
13186 } else {
13187 // An omitted source offset exits at activation. A generic limit
13188 // is the same one-shot direction for either close side. Its
13189 // executable level includes the legacy stop-style slippage so
13190 // the generic limit constraint and the source fill agree.
13191 const double slipped = trail_price + (exit_is_buy ? 1.0 : -1.0)
13192 * config_.slippage * tick;
13193 submit_leg(PineOrderFamily::ExitTrail, native_order::Limit{
13194 directional_tick(slipped, tick, exit_is_buy)});
13195 }
13196 }
13197 const bool exit_at_activation_trail = has_trail_request
13198 && (!std::isfinite(source_trail_offset)
13199 || std::floor(source_trail_offset) == 0.0);
13200 if (!trail_one_shot && exit_at_activation_trail && !finite_positive(stop_price)
13201 && finite_positive(trail_price)) {
13202 if (const auto point = detail::callback_point(require_host())) {
13203 const bool long_side = detail::run_position(require_host()).signed_units > 0.0;
13204 const bool already_armed = source_trail_reached_at(
13205 point->price, trail_price, tick, !long_side);
13206 if (already_armed) {
13207 // The explicit-zero trail is already active at the source
13208 // placement close. A sibling generic stop preserves the
13209 // next-open print decision; the Trail request still owns a
13210 // favourable-gap ride and all later path tracking. Its level
13211 // is the carried best, which never starts short of the
13212 // activation (a close reaching it only on its tick).
13213 const double carried_best = long_side
13214 ? std::max(point->price, trail_price)
13215 : std::min(point->price, trail_price);
13216 submit_leg(PineOrderFamily::ExitStop, native_order::Stop{carried_best});
13217 }
13218 }
13219 }
13220 // A call that names a level replaces the whole exit order: the price leg
13221 // it leaves na (neither the absolute level nor its tick distance) is
13222 // withdrawn, not carried from the previous call (lab tv tape
13223 // tests/fixtures/exit_reissue_na fran-x1-exit-reissue-na-limit: a re-issue
13224 // with stop = na or limit = na drops that leg; lane TAIL-D). Trailing legs
13225 // are left as they are.
13226 if (placed_absolute_leg && !has_trail_request) {
13227 const bool drop_limit = std::isnan(limit_price) && std::isnan(profit_ticks);
13228 const bool drop_stop = std::isnan(stop_price) && std::isnan(loss_ticks);
13229 const auto dropped = [&](const PlacementSnapshot& row) {
13230 return row.source_id == exit_id && row.from_entry == from_entry
13231 && !std::isfinite(row.trail_activation_level)
13232 && ((drop_limit && row.family == PineOrderFamily::ExitLimit)
13233 || (drop_stop && row.family == PineOrderFamily::ExitStop));
13234 };
13235 if (drop_limit || drop_stop) {
13236 std::vector<native_order::RequestHandle> withdrawn;
13237 for (const auto& handle : live_handles_) {
13238 const auto found = placement_.find(handle.incarnation);
13239 if (found != placement_.end() && dropped(found->second))
13240 withdrawn.push_back(handle);
13241 }
13242 for (const auto& handle : withdrawn) {
13243 const auto result = require_host().cancel(handle);
13244 if (result.status == native_order::CancelStatus::Cancelled) retire(handle);
13245 }
13246 pending_write(pending_bracket_legs_).erase(std::remove_if(pending_bracket_legs_.begin(),
13247 pending_bracket_legs_.end(), [&](const PendingBracketLeg& leg) {
13248 return dropped(leg.snapshot);
13249 }), pending_bracket_legs_.end());
13250 delayed_market_orders_.erase(std::remove_if(delayed_market_orders_.begin(),
13251 delayed_market_orders_.end(), [&](const DelayedMarketOrder& order) {
13252 return dropped(order.snapshot);
13253 }), delayed_market_orders_.end());
13254 }
13255 }
13256 if (defer_for_same_bar_priority && !from_entry.empty())
13257 named_entry_cancel_tokens_.erase(from_entry);
13258 if (pooc_short_tick_scope && source_point) {
13259 bool competing_entry = false;
13260 for (const auto& handle : live_handles_) {
13261 const auto found = placement_.find(handle.incarnation);
13262 if (found != placement_.end() && found->second.opening) {
13263 competing_entry = true;
13264 break;
13265 }
13266 }
13267 if (!competing_entry) {
13268 const double close = nearest_tick(
13269 source_point->price, staged_.syminfo.mintick);
13270 const PineOrderFamily selected = pooc_current_close_family;
13271 if (selected != PineOrderFamily::Entry) {
13272 const SourceId replacement_key = exit_id + "\x1f" + from_entry
13273 + std::to_string(static_cast<int>(selected));
13274 const auto live = live_by_source_key_.find(checked_live_key(replacement_key));
13275 if (live != live_by_source_key_.end()) {
13276 const auto found = placement_.find(live->second.incarnation);
13277 if (found != placement_.end()
13278 && found->second.projection_predecessor != 0
13279 && cohort_exposure_for(from_entry) > 0.0) {
13280 PlacementSnapshot immediate = found->second;
13281 cancel_bracket_siblings(live->second);
13282 native_order::Request request;
13283 request.intent = native_order::Flatten{};
13284 request.label = exit_id;
13285 request.comment = comment;
13286 request.trigger = native_order::Market{};
13287 immediate.forced_execution_price = close;
13288 immediate.projection_predecessor = live->second.incarnation;
13289 immediate.projection_predecessor_exit = true;
13290 const auto accepted = submit_or_replace(
13291 std::move(request), std::move(immediate), false,
13292 "__pooc_current_exit__" + exit_id + "\x1f" + from_entry);
13293 if (accepted) {
13294 // TradingView fills this exit at the close, after
13295 // the pass that placed it: the rest of the pass
13296 // keeps the pre-fill strategy.position_size and
13297 // strategy.position_avg_price, as an ordinary
13298 // close_all keeps the size. lab tv
13299 // synthetics tests/fixtures/pooc_close_fill_view.
13300 const auto& view = detail::pooc_close_fill_view_switches();
13301 auto* pine_host = view.current_exit_keeps_position_view
13302 ? pine_view_of(&require_host()) : nullptr;
13303 const double pre_fill_average = require_host().position_avg_price();
13304 if (pine_host) pine_host->freeze_script_position_view();
13305 (void)require_host().execute_current(
13306 {*accepted, NativeCurrentPriceRule::NearestTick});
13307 if (pine_host && view.current_exit_keeps_average_price)
13308 pine_host->hold_script_average_price(pre_fill_average);
13309 }
13310 }
13311 }
13312 }
13313 }
13314 }
13315
13316 if (!placed_absolute_leg
13317 && !(has_trail_request && finite_positive(trail_price))) {
13318 // ab9714be pine_strategy_commands.cpp:533-537: only a NaN operand is
13319 // absent. Preserve a command projection for all-NaN absolute levels
13320 // with no trail arm; it never participates in matching or settlement.
13321 exit_cancel_bracket(exit_id, from_entry, comment);
13322 pending_write(source_shadow_pending_).erase(
13323 std::remove_if(source_shadow_pending_.begin(), source_shadow_pending_.end(),
13324 [&](const SourceShadowPending& row) {
13325 return row.snapshot.source_id == exit_id
13326 && row.snapshot.from_entry == from_entry;
13327 }),
13328 source_shadow_pending_.end());
13330 snapshot.family = has_trail_request ? PineOrderFamily::ExitTrail
13331 : (!std::isnan(stop_price) || !std::isnan(loss_ticks)
13333 snapshot.source_id = exit_id;
13334 snapshot.from_entry = from_entry;
13335 snapshot.comment = comment;
13336 snapshot.oca_name = oca_name;
13337 snapshot.requested_qty = qty;
13338 snapshot.projection_remaining_qty = reserved_exit_qty;
13339 snapshot.qty_percent = qty_percent;
13340 snapshot.command_sequence = command_sequence;
13341 snapshot.source_sequence = ++source_sequence_;
13342 snapshot.projection_created_bar = detail::callback_point(require_host())
13343 ? projection_bar_index(detail::callback_point(require_host())->decision) : -1;
13344 snapshot.projection_position_side = physical.signed_units > 0.0
13345 ? static_cast<std::int32_t>(PositionSide::LONG)
13346 : (physical.signed_units < 0.0
13347 ? static_cast<std::int32_t>(PositionSide::SHORT)
13348 : static_cast<std::int32_t>(PositionSide::FLAT));
13349 snapshot.placement_cycle = current_position_cycle_;
13350 snapshot.exit_levels = {limit_price, stop_price, source_trail_points,
13351 source_trail_offset, source_trail_price,
13352 profit_ticks, loss_ticks};
13353 snapshot.sizing = sizing_snapshot();
13354 pending_write(source_shadow_pending_).push_back({std::move(snapshot), exit_id});
13355 }
13356}
13357
13359 native_order::RequestHandle just_applied, bool post_calculation,
13360 bool pre_script_drain) {
13361 auto queued = std::move(pending_write(pending_bracket_legs_));
13362 pending_write(pending_bracket_legs_).clear();
13363 // ab9714be src/source/pine_scheduler.cpp:242 runs step 1 over the orders
13364 // that were already resting, and that pass walks a shared fill phase in
13365 // creation order (src/source/pine_fills.cpp:3782-3788). A resting bracket
13366 // settles on this bar ahead of the source body only when that walk reaches
13367 // its parent entry first: a leg the pass visits while the position is still
13368 // flat is skipped there (src/source/pine_fills.cpp:7461-7468) and only
13369 // revisited later, which on this route is the flush below the body.
13370 const auto point = detail::callback_point(require_host());
13371 // A parent that filled in the open phase sat in phase 0 with the open-tick
13372 // marketables, so the pass walked it ahead of every priced bracket of the
13373 // bar. A parent that filled later on the path shares the bracket's phase,
13374 // and there only the book's creation order decides.
13375 const bool parent_walked_after_leg
13376 = point != nullptr
13377 && position_open_bar_index_
13378 == point->decision.coordinate.interval_index
13379 && position_open_phase_ != NativePathPhase::Open;
13380 const auto resolve_parent_handle
13381 = [&](const PlacementSnapshot& row)
13382 -> std::optional<native_order::RequestHandle> {
13383 auto origin = row.bracket_origin;
13384 if (origin.incarnation == 0 && !row.from_entry.empty()) {
13385 const auto cohort = cohorts_by_id_.find(row.from_entry);
13386 if (cohort != cohorts_by_id_.end()
13387 && !cohort->second.opened.empty()) {
13388 origin = cohort->second.opened.back();
13389 }
13390 }
13391 if (placement_.find(origin.incarnation) == placement_.end()) return std::nullopt;
13392 return origin;
13393 };
13394 const auto resolve_parent = [&](const PlacementSnapshot& row)
13395 -> const PlacementSnapshot* {
13396 const auto handle = resolve_parent_handle(row);
13397 if (!handle) return nullptr;
13398 return &placement_.find(handle->incarnation)->second;
13399 };
13400 // The owner's pass orders a shared fill phase by the order's creation
13401 // sequence (ab9714be src/source/pine_fills.cpp:3779-3785), the stamp the
13402 // book keeps across a same-id replacement
13403 // (src/source/pine_strategy_commands.cpp:481-482,
13404 // src/source/pine_strategy_commands.cpp:2627-2648). This route carries the
13405 // same fact on the snapshot's source sequence, which a re-issued bracket
13406 // inherits (src/source/pine_adapter.cpp:6601) and the book walk already
13407 // ranks on (src/source/pine_adapter.cpp:11288).
13408 const auto parent_precedes_leg
13409 = [](const PlacementSnapshot& leg, const PlacementSnapshot& parent) {
13410 return parent.source_sequence < leg.source_sequence;
13411 };
13412 // A parent recreated after a named entry cancellation is the retained
13413 // topology: its surviving bracket keeps the pre-cancellation book slot, so
13414 // the creation-order walk reaches the leg first and skips it while the
13415 // position is still flat (src/source/pine_fills.cpp:7461-7468). The owner's
13416 // one exception ranks that fresh parent ahead of the retained child
13417 // (ab9714be src/compat/pine/order_priority.cpp:31-85, declared by the
13418 // flat_retained_child_fresh_parent_order metadata). The child of that pair
13419 // has no native handle while this route still stages it, so the pair is
13420 // read from the placement snapshots and the three evidence boundaries of
13421 // the owner's predicate are carried as their source facts: the pass-time
13422 // gates of ab9714be src/compat/pine/order_priority.cpp:11-15 become "this
13423 // bar's position opened on this bar" (the owner ranks the pair while the
13424 // broker is still flat, and here the walk ran ahead of this drain) plus
13425 // "the pair is the whole book this route carries"; the incarnation
13426 // adjacency of src/compat/pine/order_priority.cpp:85 becomes two
13427 // consecutive source commands; and the owner's single exit order is the
13428 // pair of legs this route splits it into, so either leg may carry the
13429 // surviving-slot stamp for its bracket.
13430 const bool retained_parent_first
13431 = priority.attached() && priority.retained_parent_first();
13432 const auto exact_retained_child = [&](const PlacementSnapshot& row,
13433 std::uint64_t surviving) {
13434 const double percent = std::isfinite(row.qty_percent)
13435 ? row.qty_percent : 100.0;
13436 return (row.family == PineOrderFamily::ExitStop
13438 && !row.from_entry.empty()
13439 && row.projection_predecessor != 0
13440 && row.projection_predecessor == surviving
13442 == static_cast<std::int32_t>(PositionSide::FLAT)
13443 && point && row.projection_created_bar
13444 == point->decision.coordinate.interval_index - 1
13445 && !row.birth.from_fill()
13448 && !std::isfinite(row.requested_qty)
13449 && percent >= 100.0 - internal::kFullPercentEps
13450 && std::isfinite(row.exit_levels.stop)
13451 && std::isfinite(row.exit_levels.limit)
13452 && !std::isfinite(row.exit_levels.profit_ticks)
13453 && !std::isfinite(row.exit_levels.loss_ticks)
13454 && !std::isfinite(row.exit_levels.trail_points)
13455 && !std::isfinite(row.exit_levels.trail_price)
13456 && !std::isfinite(row.exit_levels.trail_offset)
13457 && row.oca_name.empty() && row.oca_type == 0;
13458 };
13459 const auto exact_fresh_parent = [&](const PlacementSnapshot& parent,
13460 std::uint64_t parent_incarnation,
13461 std::uint64_t child_predecessor) {
13462 const auto cancelled
13464 const auto surviving
13466 return parent.opening
13467 && parent.family == PineOrderFamily::Entry
13468 && parent.projection_position_side
13469 == static_cast<std::int32_t>(PositionSide::FLAT)
13470 && parent.projection_predecessor == 0
13471 && cancelled != 0 && cancelled < parent_incarnation
13472 && cancelled != child_predecessor
13473 && surviving > cancelled && surviving < parent_incarnation
13474 && point && parent.projection_created_bar
13475 == point->decision.coordinate.interval_index - 1
13476 && !std::isfinite(parent.requested_qty)
13477 && !parent.birth.from_fill()
13478 && !parent.projection_after_close
13480 && !parent.stop_limit_activated
13481 && std::isfinite(parent.exit_levels.stop)
13482 && !std::isfinite(parent.exit_levels.limit)
13483 && !std::isfinite(parent.exit_levels.trail_points)
13484 && !std::isfinite(parent.exit_levels.trail_price)
13485 && !std::isfinite(parent.exit_levels.trail_offset)
13486 && parent.oca_name.empty() && parent.oca_type == 0;
13487 };
13488 // The staged brackets of this drain that are the ranked child of a fresh
13489 // parent entry.
13490 std::vector<std::pair<std::string, std::string>> ranked_retained_brackets;
13491 if (pre_script_drain && retained_parent_first && point
13492 && position_open_bar_index_
13493 == point->decision.coordinate.interval_index
13494 && config_.process_orders_on_close && !config_.calc_on_order_fills
13495 && !coof_recalc_active_ && point->decision.sub_count <= 1) {
13496 std::vector<std::pair<std::string, std::string>> book_keys;
13497 // TradingView's close-time script sees the stop entry AND its bracket
13498 // exit filled inside the bar whatever exit the book still holds for an
13499 // entry that is neither open nor working -- a strategy.exit re-issued
13500 // after its entry closed, or kept after the entry was cancelled
13501 // ("Sell Exit" of a cancelled "Sell" re-arm on 2025-08-02;
13502 // tests/fixtures/pooc_close_bar_fills, the two dormant-exit-history
13503 // tapes on BINANCE:BTCUSDT 15m). Such an exit cannot fill before its
13504 // entry does, so it is not part of the book this pair is ranked
13505 // against (ScriptRuleSwitches).
13506 const auto inert_exit = [&](const PlacementSnapshot& row) {
13507 if (!detail::script_rule_switches().pooc_bracket_skips_inert_exits)
13508 return false;
13512 return false;
13513 const auto held = from_entry_units(row.from_entry);
13514 if (!held || *held > 0.0) return false;
13515 const auto names_entry = [&](const PlacementSnapshot& other) {
13516 return other.source_id == row.from_entry;
13517 };
13518 for (const auto& live : live_handles_) {
13519 const auto other = placement_.find(live.incarnation);
13520 if (other != placement_.end() && names_entry(other->second)) return false;
13521 }
13522 for (const auto& staged : queued)
13523 if (names_entry(staged.snapshot)) return false;
13524 for (const auto& entry : pending_entries_)
13525 if (names_entry(entry.snapshot)) return false;
13526 for (const auto& command : pending_same_bar_commands_)
13527 if (names_entry(command.snapshot)) return false;
13528 for (const auto& pending : pending_coof_requests_)
13529 if (names_entry(pending.snapshot)) return false;
13530 for (const auto& delayed : delayed_market_orders_)
13531 if (names_entry(delayed.snapshot)) return false;
13532 return true;
13533 };
13534 const auto carry_key = [&](const PlacementSnapshot& row) {
13535 if (inert_exit(row)) return;
13536 const auto key = std::make_pair(row.source_id, row.from_entry);
13537 if (std::find(book_keys.begin(), book_keys.end(), key)
13538 == book_keys.end())
13539 book_keys.push_back(key);
13540 };
13541 for (const auto& live : live_handles_) {
13542 const auto row = placement_.find(live.incarnation);
13543 if (row != placement_.end()) carry_key(row->second);
13544 }
13545 for (const auto& staged : queued) carry_key(staged.snapshot);
13546 for (const auto& entry : pending_entries_) carry_key(entry.snapshot);
13547 for (const auto& command : pending_same_bar_commands_)
13548 carry_key(command.snapshot);
13549 for (const auto& pending : pending_coof_requests_)
13550 carry_key(pending.snapshot);
13551 for (const auto& delayed : delayed_market_orders_)
13552 carry_key(delayed.snapshot);
13553 for (const auto& leg : queued) {
13554 const auto bracket = std::make_pair(leg.snapshot.source_id,
13555 leg.snapshot.from_entry);
13556 if (book_keys.size() != 1 || book_keys.front() != bracket) continue;
13557 const auto handle = resolve_parent_handle(leg.snapshot);
13558 if (!handle) continue;
13559 const auto parent = placement_.find(handle->incarnation);
13560 if (parent == placement_.end()
13561 || !parent->second.retained_parent_topology) continue;
13562 if (leg.snapshot.from_entry != parent->second.source_id) continue;
13563 if (leg.snapshot.command_sequence
13564 != parent->second.command_sequence + 1) continue;
13565 if (!exact_fresh_parent(parent->second, handle->incarnation,
13566 leg.snapshot.projection_predecessor))
13567 continue;
13568 for (const auto& sibling : queued) {
13569 if (sibling.snapshot.source_id != bracket.first
13570 || sibling.snapshot.from_entry != bracket.second) continue;
13571 if (!exact_retained_child(sibling.snapshot,
13573 continue;
13574 if (std::find(ranked_retained_brackets.begin(),
13575 ranked_retained_brackets.end(), bracket)
13576 == ranked_retained_brackets.end())
13577 ranked_retained_brackets.push_back(bracket);
13578 break;
13579 }
13580 }
13581 }
13582 // True when the owner's step-1 walk reaches this leg's parent entry before
13583 // the leg, so the leg settles with the position open ahead of the body. A
13584 // leg whose parent entry was the fill that opened the position has already
13585 // had that parent's calculation, so the walk also satisfies the deferred
13586 // post-parent-calculation parking flag (pine_adapter.cpp:7393).
13587 const auto parent_walks_first
13588 = [&](const PlacementSnapshot& leg) {
13589 if (!pre_script_drain) return false;
13590 const auto* parent = resolve_parent(leg);
13591 if (parent == nullptr) return false;
13592 if (parent->retained_parent_topology) {
13593 return std::find(ranked_retained_brackets.begin(),
13594 ranked_retained_brackets.end(),
13595 std::make_pair(leg.source_id, leg.from_entry))
13596 != ranked_retained_brackets.end();
13597 }
13598 return !parent_walked_after_leg
13599 || parent_precedes_leg(leg, *parent);
13600 };
13601 if (pre_script_drain) {
13602 std::vector<PendingBracketLeg> resting;
13603 resting.reserve(queued.size());
13604 for (auto& leg : queued) {
13605 const bool has_parent = resolve_parent(leg.snapshot) != nullptr;
13606 if (has_parent && !parent_walks_first(leg.snapshot)) {
13607 pending_write(pending_bracket_legs_).push_back(std::move(leg));
13608 } else {
13609 resting.push_back(std::move(leg));
13610 }
13611 }
13612 queued = std::move(resting);
13613 }
13614 // Re-issued explicit brackets are one leg family per entry instance.
13615 // The legacy book walked instances first (T1/T2 for opening A, then
13616 // T1/T2 for opening B), not every T1 across all openings before T2.
13617 // Preserve original order for unbound/deferred rows (origin zero).
13618 std::stable_sort(queued.begin(), queued.end(), [](const PendingBracketLeg& left,
13619 const PendingBracketLeg& right) {
13620 const auto left_origin = left.snapshot.bracket_origin.incarnation;
13621 const auto right_origin = right.snapshot.bracket_origin.incarnation;
13622 if (left_origin == right_origin) return false;
13623 return left_origin < right_origin;
13624 });
13625 // A stop or limit leg staged in a fill recalculation keeps the trigger
13626 // exit() installed whatever else the book holds: TradingView has no
13627 // "competing chart tick" exclusion, so beside another order it reaches
13628 // what it reaches alone (lab tv synthetics tests/fixtures/
13629 // coof_competing_tick).
13630 for (auto& leg : queued) {
13631 // A global exit called while the book is flat is bound to the limit
13632 // and stop entry orders working then and waits for their fill
13633 // (materialized by the opening, materialize_pending_bracket_legs):
13634 // submitted to a flat book it would end at once (tests/fixtures/
13635 // global_exit_children pending-89). One beside a market entry keeps
13636 // its course (tests/fixtures/exit_queue w3f05-s11).
13637 if (leg.snapshot.from_entry.empty() && leg.snapshot.bracket_origin.incarnation == 0
13638 && detail::exit_binding_rule_switches().global_exit_binds_working_entries
13639 && margins_disabled()
13640 && whole_level_exit(leg.snapshot.exit_levels.limit, leg.snapshot.exit_levels.stop,
13641 leg.snapshot.exit_levels.trail_points,
13642 leg.snapshot.exit_levels.trail_price,
13643 leg.snapshot.exit_levels.profit_ticks,
13644 leg.snapshot.exit_levels.loss_ticks,
13645 leg.snapshot.requested_qty, leg.snapshot.qty_percent)
13646 && detail::run_position(require_host()).signed_units == 0.0
13647 && priced_entry_order_working(nullptr)) {
13648 pending_write(pending_bracket_legs_).push_back(std::move(leg));
13649 continue;
13650 }
13652 && !(cohort_exposure_for(leg.snapshot.from_entry) > 0.0)) {
13653 pending_write(pending_bracket_legs_).push_back(std::move(leg));
13654 continue;
13655 }
13656 // ab9714be pine_fills.cpp:7679-7700 evaluates a filled parent's priced
13657 // exits on its entry bar, whichever source batch placed the parent.
13658 // An explicit leg issued after its flat pyramiding=2 MARKET parent,
13659 // while that parent was still held in the same-bar batch, took origin
13660 // zero; the batch has now submitted the parent, so bind the leg to it
13661 // exactly as exit() binds a leg issued after a live pending parent:
13662 // it waits for the parent's opening instead of pre-arming on the
13663 // signal bar.
13664 if (leg.snapshot.bracket_origin.incarnation == 0 && config_.pyramiding == 2
13665 && std::isfinite(leg.snapshot.requested_qty)
13666 && !leg.snapshot.from_entry.empty()) {
13667 const auto cohort = cohorts_by_id_.find(leg.snapshot.from_entry);
13668 if (cohort != cohorts_by_id_.end() && cohort->second.opened.empty()) {
13669 for (const auto& origin : cohort->second.origins) {
13670 // Only a pending (live) origin qualifies, and its row is held.
13671 if (!origin_is_pending(origin)) continue;
13672 const auto parent = placement_.find(origin.incarnation);
13673 if (parent == placement_.end() || !parent->second.opening
13674 || parent->second.family != PineOrderFamily::Entry
13675 || parent->second.placement_script_open_ms
13676 != leg.snapshot.placement_script_open_ms
13677 || parent->second.command_sequence
13678 >= leg.snapshot.command_sequence) {
13679 continue;
13680 }
13681 const std::string origin_key = std::to_string(origin.incarnation);
13682 const std::string family_tag = std::to_string(
13683 static_cast<int>(leg.snapshot.family));
13684 leg.snapshot.bracket_origin = origin;
13685 leg.replacement_key = leg.snapshot.source_id + "\x1f"
13686 + leg.snapshot.from_entry + "\x1f" + family_tag + "\x1f" + origin_key;
13687 const auto group_name = leg.snapshot.oca_name.empty()
13688 ? leg.snapshot.source_id + "\x1f" + leg.snapshot.from_entry
13689 + "\x1f" + origin_key
13690 : leg.snapshot.oca_name + "\x1f" + origin_key;
13691 leg.request.group = group_for(group_name, 1,
13692 static_cast<std::int64_t>(leg.snapshot.family));
13693 break;
13694 }
13695 }
13696 // The parent may not be in the book yet: the reversal entry the
13697 // same bar places beside strategy.close() of the position it
13698 // reverses is still held (pending_entries_). The leg waits for it
13699 // as for a live pending parent, and the parent's opening
13700 // materializes it against the whole position (FIFO); submitted
13701 // now it binds the id's cohort, and a FIFO close by another id's
13702 // exit then leaves it no lot, where TradingView still fills it on
13703 // its own reservation (lab tv tape tests/fixtures/exit_fifo_cross
13704 // td-m6g; lane TAIL-D).
13705 const bool parent_held = cohort != cohorts_by_id_.end()
13706 && cohort->second.opened.empty()
13707 && leg.snapshot.bracket_origin.incarnation == 0
13708 && std::any_of(pending_entries_.begin(), pending_entries_.end(),
13709 [&](const PendingEntry& held) {
13710 return held.snapshot.opening
13711 && held.snapshot.family == PineOrderFamily::Entry
13712 && held.snapshot.source_id == leg.snapshot.from_entry
13713 && held.snapshot.placement_script_open_ms
13714 == leg.snapshot.placement_script_open_ms
13715 && held.snapshot.command_sequence
13716 < leg.snapshot.command_sequence;
13717 });
13718 if (parent_held) {
13719 // It keeps the bar the script placed it on, as a leg staged
13720 // for a flat book's pending parent does, whatever bar the
13721 // held parent reaches the book on.
13722 leg.snapshot.projection_created_bar_pinned = true;
13723 pending_write(pending_bracket_legs_).push_back(std::move(leg));
13724 continue;
13725 }
13726 }
13727 bool retained_parent_pending = false;
13728 if ((leg.snapshot.projection_predecessor != 0
13730 && !leg.snapshot.from_entry.empty()) {
13731 const auto cohort = cohorts_by_id_.find(leg.snapshot.from_entry);
13732 if (cohort != cohorts_by_id_.end()) {
13733 // A physically flat book has no open lot for this id even
13734 // when the Applied of the fill that flattened it (a bracket
13735 // executed at its level in the pre-script drain) is not yet
13736 // observed, so the cohort's opened roster is still stale.
13737 // ab9714be pine_fills.cpp:7464-7467 only Skips an exit
13738 // created while flat, and the flat purge keeps it for its
13739 // pending parent (pine_fills.cpp:431-437,
13740 // pine_orders.cpp:507-527): it waits here for that parent.
13741 const bool no_open_lot = cohort->second.opened.empty()
13742 || detail::run_position(require_host()).signed_units == 0.0;
13743 const auto live = no_open_lot
13744 ? live_origin_positions(cohort->second) : std::vector<std::size_t>{};
13745 retained_parent_pending = std::any_of(live.begin(), live.end(),
13746 [&](std::size_t position) {
13747 return origin_is_pending(cohort->second.origins[position]);
13748 });
13749 }
13750 }
13751 if ((leg.snapshot.bracket_origin.incarnation != 0
13752 && leg.snapshot.bracket_origin != just_applied
13753 && origin_is_pending(leg.snapshot.bracket_origin))
13754 || retained_parent_pending) {
13755 pending_write(pending_bracket_legs_).push_back(std::move(leg));
13756 continue;
13757 }
13759 && !post_calculation && !parent_walks_first(leg.snapshot)) {
13760 pending_write(pending_bracket_legs_).push_back(std::move(leg));
13761 continue;
13762 }
13763 bool execute_after_calculation = false;
13764 // ab9714be src/source/pine_fills.cpp:7695-7728 evaluates a priced exit on
13765 // the bar its parent entry fills, and a leg this route parked until
13766 // post-calculation still gets that bar's touch at its level price. The
13767 // stop sits on the far leg of the assumed intrabar path, so it only
13768 // fires when the parent's fill leg came first; the limit sits beyond the
13769 // fill on the parent's own leg, so the fill always precedes it. A level
13770 // on the wrong side of the fill price is skipped, as in the entry-bar
13771 // gate of src/source/pine_fills.cpp:7715-7727.
13772 const bool priced_exit_leg
13773 = leg.snapshot.family == PineOrderFamily::ExitStop
13774 || leg.snapshot.family == PineOrderFamily::ExitLimit;
13775 if ((leg.snapshot.projection_predecessor != 0
13777 && priced_exit_leg && policy_script_bar_valid_ && !leg.snapshot.void_issue) {
13778 const auto cohort = cohorts_by_id_.find(leg.snapshot.from_entry);
13779 if (cohort != cohorts_by_id_.end() && !cohort->second.opened.empty()) {
13780 const auto parent = placement_.find(cohort->second.opened.back().incarnation);
13781 if (parent != placement_.end() && parent->second.opening) {
13782 const bool is_stop_leg
13783 = leg.snapshot.family == PineOrderFamily::ExitStop;
13784 const double level = is_stop_leg
13785 ? leg.snapshot.exit_levels.stop
13786 : leg.snapshot.exit_levels.limit;
13787 const bool high_first = source_path_uses_high_first(policy_script_bar_);
13788 const bool parent_before_child = is_stop_leg
13789 ? (parent->second.is_long ? high_first : !high_first) : true;
13790 // ab9714be pine_fills.cpp:7711-7713 + pine_policy_members.cpp:11-17
13791 // + engine.hpp:1374-1381: the entry-bar exit is tested
13792 // against the tick-quantized bar, so an on-grid level is
13793 // touched by a raw extreme half a tick short of it
13794 // (synthetic NYSE:F high 25.045 -> 25.05 reaches a 25.05 sell limit).
13795 // Only the calc_on_order_fills scheduler compares raw.
13796 const double tick = staged_.syminfo.mintick;
13797 const bool raw_bar = config_.calc_on_order_fills
13798 || !finite_positive(tick);
13799 const double bar_high = raw_bar ? policy_script_bar_.high
13800 : source_decimal_tick(policy_script_bar_.high, tick);
13801 const double bar_low = raw_bar ? policy_script_bar_.low
13802 : source_decimal_tick(policy_script_bar_.low, tick);
13803 const bool touched = is_stop_leg
13804 ? (parent->second.is_long ? bar_low <= level
13805 : bar_high >= level)
13806 : (parent->second.is_long ? bar_high >= level
13807 : bar_low <= level);
13808 const double fill_price
13809 = detail::run_position(require_host()).average_price;
13810 const bool right_side = is_stop_leg
13811 || (parent->second.is_long ? level >= fill_price
13812 : level <= fill_price);
13813 execute_after_calculation = parent_before_child && touched && right_side;
13814 if (execute_after_calculation) {
13815 const double units = std::abs(
13816 detail::run_position(require_host()).signed_units);
13817 leg.snapshot.forced_execution_price = level;
13818 leg.snapshot.immediately = true;
13819 leg.snapshot.requested_qty = units;
13820 leg.snapshot.deferred_cohort = false;
13821 leg.request.intent = native_order::Reduce{
13823 leg.request.trigger = native_order::Market{};
13824 leg.request.owner = native_order::Independent{};
13825 leg.request.group = native_order::NoGroup{};
13826 }
13827 }
13828 }
13829 }
13830 const auto accepted = submit_or_replace(std::move(leg.request), std::move(leg.snapshot), false,
13831 leg.replacement_key);
13832 if (accepted) {
13833 bracket_family(leg.family_key).push_back(*accepted);
13834 if (execute_after_calculation) {
13835 (void)require_host().execute_current(
13836 {*accepted, NativeCurrentPriceRule::NearestTick});
13837 }
13838 }
13839 }
13840}
13841
13842void PineExecutionAdapter::materialize_pending_bracket_legs(
13844 const auto parent_it = placement_.find(event.handle().incarnation);
13845 const PlacementSnapshot* parent = parent_it == placement_.end()
13846 ? nullptr : &parent_it->second;
13847 const bool retained_parent = parent && parent->opening
13848 && parent->retained_parent_topology;
13849 if (parent && parent->opening && parent->oca_type == 1
13850 && !parent->oca_name.empty()) {
13851 pending_write(pending_bracket_legs_).erase(std::remove_if(
13852 pending_bracket_legs_.begin(), pending_bracket_legs_.end(),
13853 [&](const PendingBracketLeg& leg) {
13854 return leg.snapshot.oca_name == parent->oca_name;
13855 }), pending_bracket_legs_.end());
13856 }
13857 std::uint64_t retained_family = 0;
13858 bool materialize_retained = false;
13859 if (retained_parent && parent->recreated_after_named_cancelled_entry_incarnation != 0
13862 const PendingBracketLeg* representative = nullptr;
13863 bool one_family = true;
13864 for (const auto& leg : pending_bracket_legs_) {
13865 if (leg.snapshot.from_entry != parent->source_id) continue;
13866 if (!representative) {
13867 representative = &leg;
13868 retained_family = leg.family_key;
13869 } else if (leg.family_key != retained_family) {
13870 one_family = false;
13871 }
13872 }
13873 bool foreign = false;
13874 if (representative) {
13875 // Every placement ever made: the retained rows, and the source
13876 // ids the erased ones leave in the table (the retired-row compaction).
13877 foreign = placement_.erased_non_margin_id_outside(
13878 parent->source_id, representative->snapshot.source_id);
13879 // The record answers for every erased row the scan would accept.
13880 if (!foreign) {
13882 [&](std::uint64_t, const PlacementSnapshot& snapshot) {
13883 return snapshot.source_id != parent->source_id
13884 && snapshot.source_id != representative->snapshot.source_id
13885 && snapshot.family != PineOrderFamily::Margin;
13886 });
13887 }
13888 for (const auto& row : placement_) {
13889 if (foreign) break;
13890 const auto& snapshot = row.second;
13891 if (snapshot.source_id != parent->source_id
13892 && snapshot.source_id != representative->snapshot.source_id
13893 && snapshot.family != PineOrderFamily::Margin) {
13894 foreign = true;
13895 }
13896 }
13897 }
13898 const bool exact_child = representative
13899 && representative->snapshot.projection_predecessor
13901 && representative->snapshot.source_sequence < parent->source_sequence
13902 && !std::isfinite(representative->snapshot.requested_qty)
13903 && (!std::isfinite(representative->snapshot.qty_percent)
13904 || representative->snapshot.qty_percent >= 100.0)
13905 && finite_positive(representative->snapshot.exit_levels.stop)
13906 && finite_positive(representative->snapshot.exit_levels.limit)
13907 && representative->snapshot.oca_name.empty()
13908 && representative->snapshot.oca_type == 0;
13909 materialize_retained = one_family && !foreign && exact_child
13910 && live_handles_.size() == 1 && live_handles_.front() == event.handle();
13911 }
13912 if (retained_parent && !materialize_retained) {
13913 for (auto& leg : pending_bracket_legs_) {
13914 if (leg.snapshot.from_entry == parent->source_id)
13915 leg.snapshot.defer_until_post_parent_calculation = true;
13916 }
13917 }
13918 std::vector<PendingBracketLeg> ready;
13919 for (auto it = pending_bracket_legs_.begin(); it != pending_bracket_legs_.end();) {
13920 const bool matches_parent = it->snapshot.from_entry.empty()
13921 || (parent && it->snapshot.from_entry == parent->source_id);
13922 const bool selected = retained_parent
13923 ? (materialize_retained && it->family_key == retained_family
13924 && it->snapshot.from_entry == parent->source_id)
13925 : (!it->snapshot.defer_until_post_parent_calculation
13926 && (it->snapshot.bracket_origin == event.handle()
13927 || (it->snapshot.bracket_origin.incarnation == 0 && matches_parent)));
13928 if (selected) {
13929 pending_write(it->snapshot).bracket_origin = event.handle();
13930 ready.push_back(std::move(*it));
13931 it = pending_write(pending_bracket_legs_).erase(it);
13932 } else {
13933 ++it;
13934 }
13935 }
13936 for (auto& leg : ready) {
13937 // ab9714be pine_fills.cpp:7650-7674 (finding-347), as in
13938 // flush_pending_bracket_legs: the opened parent leaves FIFO to settle
13939 // an explicit leg against the whole position.
13940 if (!retained_parent && leg.snapshot.bracket_origin == event.handle()
13941 && std::isfinite(leg.snapshot.requested_qty)
13942 && std::holds_alternative<native_order::BindCohort>(leg.request.owner)
13943 && !config_.close_entries_rule_any) {
13944 leg.request.owner = owner_for_close(leg.snapshot.from_entry, false);
13945 }
13946 const auto accepted = submit_or_replace(std::move(leg.request), std::move(leg.snapshot),
13947 false, leg.replacement_key);
13948 if (accepted) bracket_family(leg.family_key).push_back(*accepted);
13949 }
13950}
13951
13953 bool explicit_brackets_only, double current_open) {
13954 require_cohort_ticket();
13955 auto delayed = std::move(delayed_market_orders_);
13956 delayed_market_orders_.clear();
13957 for (auto& order : delayed) {
13958 const auto family = order.snapshot.family;
13959 const bool explicit_bracket = (order.snapshot.defer_until_post_parent_calculation
13960 || (std::isfinite(order.snapshot.requested_qty)
13961 && order.snapshot.bracket_origin.incarnation != 0))
13962 && (family == PineOrderFamily::ExitLimit
13963 || family == PineOrderFamily::ExitStop);
13964 const bool coof_delayed_price = order.snapshot.projection_created_during_coof
13965 && (family == PineOrderFamily::ExitStop
13966 || family == PineOrderFamily::ExitLimit);
13967 if (order.release_open_epoch <= broker_open_epoch_
13968 && (!explicit_brackets_only || explicit_bracket || coof_delayed_price)) {
13969 // A newer same-(id, from_entry) placement already superseded this
13970 // delayed leg: the owner drops same-id pending orders at replacement
13971 // time (ab9714be pine_strategy_commands.cpp:446), so the stale
13972 // command never reaches the book and must not resurrect a finished
13973 // cycle's levels against the new lot.
13974 const auto live_existing = live_by_source_key_.find(checked_live_key(order.replacement_key));
13975 if (live_existing != live_by_source_key_.end()) {
13976 const auto found_p = placement_.find(live_existing->second.incarnation);
13977 if (found_p != placement_.end()
13978 && found_p->second.command_sequence > order.snapshot.command_sequence) {
13979 continue;
13980 }
13981 }
13982 const bool execute_coof_open = order.execute_at_open
13983 && finite_positive(current_open) && !order.snapshot.void_issue;
13984 if (execute_coof_open) {
13985 order.request.trigger = native_order::Market{};
13986 order.snapshot.forced_execution_price = current_open;
13987 }
13988 const auto family_key = key_for(
13989 order.snapshot.source_id, order.snapshot.from_entry);
13990 const auto accepted = submit_or_replace(
13991 std::move(order.request), std::move(order.snapshot),
13992 family == PineOrderFamily::Entry || family == PineOrderFamily::Order,
13993 order.replacement_key);
13994 if (accepted && (family == PineOrderFamily::ExitLimit
13995 || family == PineOrderFamily::ExitStop
13996 || family == PineOrderFamily::ExitTrail)) {
13997 bracket_family(family_key).push_back(*accepted);
13998 }
13999 if (accepted && execute_coof_open) {
14000 (void)require_host().execute_current(
14001 {*accepted, NativeCurrentPriceRule::NearestTick});
14002 }
14003 } else {
14004 delayed_market_orders_.push_back(std::move(order));
14005 }
14006 }
14007}
14008
14010 require_cohort_ticket();
14011 // Quiet (pine_quiet_bar.hpp): no delayed order, same-bar batch or held
14012 // entry is queued; the batch's close total is still reset, as the flush
14013 // below always does.
14015 host_ != nullptr && delayed_market_orders_.empty()
14016 && pending_same_bar_commands_.empty()
14017 && pending_entries_.empty())) {
14018 pending_same_bar_close_qty_ = 0.0;
14019 return;
14020 }
14021 reroute_incomplete_fixed_batch(false);
14023 flush_pending_same_bar_commands(false, false);
14024 auto queued = std::move(pending_write(pending_entries_));
14025 pending_write(pending_entries_).clear();
14026 auto deferred = std::remove_if(queued.begin(), queued.end(), [&](PendingEntry& entry) {
14027 const auto parent = entry.snapshot.paired_reversal_parent;
14028 if (parent.incarnation == 0) return false;
14029 const auto found = placement_.find(parent.incarnation);
14030 if (found == placement_.end()
14031 || (found->second.family != PineOrderFamily::Close
14032 && found->second.family != PineOrderFamily::CloseAll)) {
14033 return false;
14034 }
14035 pending_write(pending_entries_).push_back(std::move(entry));
14036 return true;
14037 });
14038 queued.erase(deferred, queued.end());
14039 const bool recreated_parent = std::any_of(queued.begin(), queued.end(),
14040 [](const PendingEntry& entry) {
14041 return entry.snapshot.retained_parent_topology;
14042 });
14043 if (recreated_parent) {
14044 for (auto& entry : queued) {
14045 (void)submit_or_replace(std::move(entry.request), std::move(entry.snapshot), true,
14046 entry.replacement_key);
14047 }
14048 return;
14049 }
14050 if (!queued.empty() && !pending_bracket_legs_.empty()) {
14051 auto brackets = std::move(pending_write(pending_bracket_legs_));
14052 pending_write(pending_bracket_legs_).clear();
14053 struct Candidate {
14054 int rank = 4;
14055 std::size_t index = 0;
14056 bool entry = false;
14057 };
14058 std::vector<Candidate> ordered;
14059 ordered.reserve(queued.size() + brackets.size());
14060 for (std::size_t index = 0; index < queued.size(); ++index) {
14061 ordered.push_back({queued[index].snapshot.is_long ? 1 : 2, index, true});
14062 }
14063 const double queued_position = detail::run_position(require_host()).signed_units;
14064 for (std::size_t index = 0; index < brackets.size(); ++index) {
14065 const auto& snapshot = brackets[index].snapshot;
14066 int rank = 4;
14067 if (snapshot.family == PineOrderFamily::ExitStop) {
14068 rank = queued_position < 0.0 ? 1 : 2;
14069 } else if (snapshot.family == PineOrderFamily::ExitLimit) {
14070 rank = 3;
14071 }
14072 ordered.push_back({rank, index, false});
14073 }
14074 std::stable_sort(ordered.begin(), ordered.end(), [](const Candidate& left,
14075 const Candidate& right) {
14076 return left.rank < right.rank;
14077 });
14078 for (const auto& candidate : ordered) {
14079 if (candidate.entry) {
14080 auto& entry = queued[candidate.index];
14081 (void)submit_or_replace(std::move(entry.request), std::move(entry.snapshot), true,
14082 entry.replacement_key);
14083 continue;
14084 }
14085 auto& leg = brackets[candidate.index];
14086 const auto accepted = submit_or_replace(std::move(leg.request), std::move(leg.snapshot),
14087 false, leg.replacement_key);
14088 if (accepted) bracket_family(leg.family_key).push_back(*accepted);
14089 }
14090 return;
14091 }
14092 std::stable_sort(queued.begin(), queued.end(), [](const PendingEntry& left,
14093 const PendingEntry& right) {
14094 const auto* left_stop = std::get_if<native_order::Stop>(&left.request.trigger);
14095 const auto* right_stop = std::get_if<native_order::Stop>(&right.request.trigger);
14096 const int left_rank = !left_stop ? 2 : (left.snapshot.is_long ? 0 : 1);
14097 const int right_rank = !right_stop ? 2 : (right.snapshot.is_long ? 0 : 1);
14098 return left_rank < right_rank;
14099 });
14100 for (auto& entry : queued) {
14101 (void)submit_or_replace(std::move(entry.request), std::move(entry.snapshot), true,
14102 entry.replacement_key);
14103 }
14104}
14105
14106void PineExecutionAdapter::reroute_fixed_entries_before_request(bool invalidate_batch) {
14107 require_cohort_ticket();
14108 const auto point = detail::callback_point(require_host());
14109 if (!point) return;
14110 const auto script_open = point->decision.script_bar_open_ms;
14111 const bool frozen_entry = std::any_of(pending_same_bar_commands_.begin(),
14112 pending_same_bar_commands_.end(), [&](const PendingSameBarCommand& command) {
14113 return command.unbatched_request && command.unbatched_snapshot
14114 && command.snapshot.placement_script_open_ms == script_open;
14115 });
14116 if (!frozen_entry) return;
14117 if (invalidate_batch) source_batch_mutated_ = true;
14118 auto queued = std::move(pending_write(pending_same_bar_commands_));
14119 pending_write(pending_same_bar_commands_).clear();
14120 pending_same_bar_close_qty_ = 0.0;
14121 // Dependent requests break the entry-only projection. Restore exact placement
14122 // requests in source order; begin_source_evaluation resets the invalidation.
14123 for (auto& command : queued) {
14124 const bool restore_command = command.unbatched_request && command.unbatched_snapshot
14125 && command.snapshot.placement_script_open_ms == script_open;
14126 if (restore_command) {
14127 command.request = std::move(*command.unbatched_request);
14128 command.snapshot = std::move(*command.unbatched_snapshot);
14129 // Restore the ordinary flat-pair sizing against preceding restored
14130 // requests, which were not live when this batch member was staged.
14131 const auto& snapshot = command.snapshot;
14132 if (detail::run_position(require_host()).signed_units == 0.0
14133 && finite_positive(snapshot.requested_qty)
14134 && (snapshot.qty_type < 0
14135 || snapshot.qty_type == static_cast<int>(QtyType::FIXED))
14136 && same_point_pair_scope() && same_point_two_leg_book(
14137 snapshot.source_id, snapshot.placement_script_open_ms)) {
14138 const double pending = pending_opposite_market_units(
14139 snapshot.is_long, snapshot.placement_script_open_ms,
14140 snapshot.source_id);
14141 if (finite_positive(pending)) {
14142 const double transaction = floor_quantity_grid(
14143 snapshot.requested_qty, staged_.quantity_grid) + pending;
14144 command.request.intent = native_order::Transact{
14145 snapshot.is_long ? transaction : -transaction};
14146 }
14147 }
14148 }
14149 if (restore_command) {
14150 auto context = command.unbatched_context;
14151 context.opposite_opening_pending = opposite_entry_opening_pending(
14152 command.snapshot.is_long);
14153 submit_entry_with_policy(std::move(command.request), std::move(command.snapshot),
14154 command.replacement_key, context);
14155 } else {
14156 (void)submit_or_replace(std::move(command.request), std::move(command.snapshot),
14157 command.opening, command.replacement_key);
14158 }
14159 }
14160}
14161
14162void PineExecutionAdapter::reroute_incomplete_fixed_batch(bool invalidate_batch) {
14163 bool fixed_long = false;
14164 bool fixed_short = false;
14165 for (const auto& command : pending_same_bar_commands_) {
14166 if (!command.unbatched_request) continue;
14167 if (command.snapshot.is_long) fixed_long = true;
14168 else fixed_short = true;
14169 }
14170 if ((fixed_long || fixed_short)
14171 && (!(fixed_long && fixed_short) || !fixed_unmargined_market_batch_scope())) {
14172 reroute_fixed_entries_before_request(invalidate_batch);
14173 }
14174}
14175
14176void PineExecutionAdapter::flush_pending_same_bar_commands(
14177 bool flat_pair_follows, bool invalidate_batch) {
14178 require_cohort_ticket();
14179 reroute_incomplete_fixed_batch(invalidate_batch);
14180 std::vector<PendingSameBarCommand> queued;
14181 queued.swap(pending_write(pending_same_bar_commands_));
14182 // The queue's storage goes back to the member when the flush ends (its
14183 // commands, submitted, are destroyed as before), so the next bar's
14184 // commands do not allocate it again -- unless a command was queued
14185 // meanwhile.
14186 struct KeepStorage {
14187 std::vector<PendingSameBarCommand>& queued;
14188 std::vector<PendingSameBarCommand>& member;
14189 ~KeepStorage() {
14190 queued.clear();
14191 if (member.empty() && member.capacity() == 0) member.swap(queued);
14192 }
14193 } keep_storage{queued, pending_same_bar_commands_};
14194 pending_same_bar_close_qty_ = 0.0;
14195 if (queued.empty()) return;
14196
14197 const double batch_start = detail::run_position(require_host()).signed_units;
14198 const auto apply_known_reversal_gap = [&](const auto& accepted) {
14199 if (!accepted || batch_start == 0.0
14200 || config_.default_qty_type
14201 != static_cast<int>(QtyType::PERCENT_OF_EQUITY)
14202 || config_.default_qty_value > 100.0) {
14203 return;
14204 }
14205 const auto placed = placement_.find(accepted->incarnation);
14206 if (placed == placement_.end()
14207 || ((batch_start > 0.0) == placed->second.is_long)) {
14208 return;
14209 }
14210 const auto point = detail::callback_point(require_host());
14211 auto* pine_host = pine_view_of(&require_host());
14212 if (!point || !pine_host) return;
14213 const auto next = pine_host->scheduler_.next_source_bar(
14214 point->decision.coordinate.interval_index);
14215 if (!next) return;
14216 NativeDecisionContext next_context = point->decision;
14217 ++next_context.coordinate.interval_index;
14218 next_context.coordinate.path_phase = NativePathPhase::Open;
14219 next_context.script_bar_open_ms = next->timestamp;
14220 next_context.sub_bar_open_ms = next->timestamp;
14221 apply_reversal_gap_bracket_policy(
14222 *next, next_context, /*defer_trails=*/true);
14223 };
14224 const bool variable_short_context = batch_start < 0.0
14225 && config_.default_qty_type != static_cast<int>(QtyType::FIXED);
14226 const bool full_short_seed = queued.size() == 3U
14227 && queued[0].opening && queued[0].snapshot.family == PineOrderFamily::Entry
14228 && queued[0].snapshot.is_long
14229 && queued[1].opening && queued[1].snapshot.family == PineOrderFamily::Entry
14230 && !queued[1].snapshot.is_long
14231 && queued[2].snapshot.frozen_market_targeted_close
14232 && queued[0].snapshot.source_id != queued[1].snapshot.source_id
14233 && queued[0].snapshot.source_id != queued[2].snapshot.source_id
14234 && queued[1].snapshot.source_id == queued[2].snapshot.source_id;
14235 const bool partial_short_seed = queued.size() == 2U
14236 && queued[0].opening && queued[0].snapshot.family == PineOrderFamily::Entry
14237 && queued[0].snapshot.is_long
14238 && queued[1].opening && queued[1].snapshot.family == PineOrderFamily::Entry
14239 && !queued[1].snapshot.is_long
14240 && std::any_of(live_handles_.begin(), live_handles_.end(), [&](const auto& handle) {
14241 const auto found = placement_.find(handle.incarnation);
14242 if (found == placement_.end()) return false;
14243 const auto& placeholder = found->second;
14244 return placeholder.family == PineOrderFamily::Close
14245 && placeholder.deferred_cohort
14246 && placeholder.source_id == queued[1].snapshot.source_id
14247 && placeholder.from_entry == queued[1].snapshot.source_id
14248 && placeholder.command_ordinal > queued[1].snapshot.command_ordinal;
14249 });
14250 const bool potential_short_seed = full_short_seed || partial_short_seed;
14251 // R1 stays outside the round-8 short-seed book: its model yields
14252 // TradingView's close-lot artifact only from the full reversal
14253 // (tests/oracle/test_oracle_short_seed.cpp, GateControl::SameIdReplacement),
14254 // so a Long reissued over the seed gets back the held units R1 left out.
14255 if (potential_short_seed && batch_start < 0.0) {
14256 for (auto& command : queued) {
14257 if (!command.staged_reversal) continue;
14258 command.snapshot.frozen_market_transaction_units += command.staged_reversal_held_units;
14259 command.snapshot.replaced_opening = false;
14260 command.snapshot.replacement_predecessor_market = false;
14261 }
14262 }
14263 // ab9714be src/compat/pine/market_admission.cpp:33-37 (explicit_pair_scope)
14264 // carries no commission term.
14265 const bool p2_candidate_scope = config_.pyramiding == 2
14266 && !config_.process_orders_on_close && !config_.calc_on_order_fills
14267 && !coof_recalc_active_ && config_.slippage == 0
14268 && config_.default_qty_type == static_cast<int>(QtyType::FIXED)
14269 && std::abs(config_.margin_long - 100.0) < 1e-12
14270 && std::abs(config_.margin_short - 100.0) < 1e-12
14271 && risk_.direction == 0 && risk_.max_cons_loss_days == 0
14272 && risk_.max_drawdown <= 0.0 && risk_.max_intraday_loss <= 0.0
14273 && risk_.max_position_size <= 0.0 && !risk_.halted && !cap.active();
14274 const bool prior_entry_like = std::any_of(
14275 live_handles_.begin(), live_handles_.end(), [&](const auto& handle) {
14276 const auto found = placement_.find(handle.incarnation);
14277 return found != placement_.end()
14278 && (found->second.family == PineOrderFamily::Entry
14279 || found->second.family == PineOrderFamily::Order);
14280 });
14281 const bool p2_explicit_pair = p2_candidate_scope && !source_batch_mutated_
14282 && !prior_entry_like && queued.size() == 2U
14283 && queued[0].opening && queued[1].opening
14284 && queued[0].snapshot.family == PineOrderFamily::Entry
14285 && queued[1].snapshot.family == PineOrderFamily::Entry
14286 && queued[0].snapshot.source_id != queued[1].snapshot.source_id
14287 && queued[0].snapshot.is_long != queued[1].snapshot.is_long
14288 && queued[0].snapshot.command_ordinal < queued[1].snapshot.command_ordinal
14289 && finite_positive(queued[0].snapshot.requested_qty)
14290 && finite_positive(queued[1].snapshot.requested_qty)
14291 && queued[0].snapshot.oca_name.empty()
14292 && queued[1].snapshot.oca_name.empty()
14293 && queued[0].snapshot.frozen_market_instruction
14294 && queued[1].snapshot.frozen_market_instruction;
14295 if (config_.pyramiding == 2 && !p2_explicit_pair && !potential_short_seed
14296 && !fixed_unmargined_market_batch_scope()) {
14297 // Exact pair finalization is a whole-source-batch decision. Any
14298 // replacement/cancel, third entry-like instruction, prior resting
14299 // entry, or live risk/config deviation sends every survivor through
14300 // ordinary source order with only its own frozen quantity.
14301 std::stable_sort(queued.begin(), queued.end(), [&](const auto& left,
14302 const auto& right) {
14303 const bool left_replaces = live_by_source_key_.find(
14304 checked_live_key(left.snapshot.source_id)) != live_by_source_key_.end();
14305 const bool right_replaces = live_by_source_key_.find(
14306 checked_live_key(right.snapshot.source_id)) != live_by_source_key_.end();
14307 return left_replaces && !right_replaces;
14308 });
14309 // From flat, a call with an opposite entry on its bar trades its
14310 // frozen transaction -- its own quantity plus any pending opposite
14311 // market's -- and is never re-sized at the fill into a reversal of
14312 // what the other leg opened first (lab tv w6b-p2a-open-pair-pyramiding2
14313 // SM-LF and MS-SF, the tape of w6-f10a-open-pair; lane W6B-ENG-PAIRS).
14314 // Only the exact book the tapes measure: one market call beside one
14315 // priced opposite call of its bar, nothing replaced or cancelled and
14316 // no entry resting from an earlier bar; every other book keeps the
14317 // ordinary route below.
14318 std::vector<bool> flat_pair_member(queued.size(), false);
14319 if (batch_start == 0.0 && same_point_pair_scope() && !source_batch_mutated_
14320 && queued.size() == 1U && queued[0].opening
14321 && queued[0].snapshot.family == PineOrderFamily::Entry) {
14322 const auto& row = queued[0].snapshot;
14323 std::size_t opposite = 0;
14324 bool foreign = false;
14325 for (const auto& handle : live_handles_) {
14326 const auto found = placement_.find(handle.incarnation);
14327 if (found == placement_.end() || !found->second.opening
14328 || (found->second.family != PineOrderFamily::Entry
14329 && found->second.family != PineOrderFamily::Order)) {
14330 continue;
14331 }
14332 const auto& leg = found->second;
14333 const bool priced_leg = finite_positive(leg.exit_levels.stop)
14334 != finite_positive(leg.exit_levels.limit);
14335 const bool this_bar = leg.family == PineOrderFamily::Entry
14336 && leg.placement_script_open_ms == row.placement_script_open_ms
14337 && leg.is_long != row.is_long && priced_leg
14338 && finite_positive(leg.requested_qty) && leg.oca_name.empty()
14339 && !leg.birth.from_fill()
14340 && (leg.qty_type < 0 || leg.qty_type == static_cast<int>(QtyType::FIXED));
14341 if (this_bar) ++opposite;
14342 else foreign = true;
14343 }
14344 flat_pair_member[0] = !foreign
14345 && (flat_pair_follows ? opposite == 0 : opposite == 1);
14346 }
14347 for (std::size_t i = 0; i < queued.size(); ++i) {
14348 auto& command = queued[i];
14349 auto request = std::move(command.request);
14350 auto snapshot = std::move(command.snapshot);
14351 snapshot.paired_flat_market_candidate = false;
14352 snapshot.paired_flat_market_own_qty = kNaN;
14353 snapshot.paired_flat_market_peer_seq = 0;
14354 snapshot.paired_flat_market_transaction_qty = kNaN;
14355 snapshot.frozen_market_instruction = false;
14356 const double own = finite_positive(snapshot.frozen_market_own_units)
14357 ? snapshot.frozen_market_own_units : snapshot.requested_qty;
14358 if (command.opening && finite_positive(own) && flat_pair_member[i]) {
14359 const double units = finite_positive(snapshot.frozen_market_transaction_units)
14360 ? snapshot.frozen_market_transaction_units : own;
14361 request.intent = native_order::Transact{snapshot.is_long ? units : -units};
14362 snapshot.reverse_to = false;
14363 } else if (command.opening && finite_positive(own)) {
14364 request.intent = native_order::HostSized{
14365 native_order::HostSizedKind::Open,
14366 snapshot.is_long ? native_order::Side::Long
14367 : native_order::Side::Short};
14368 snapshot.reverse_to = true;
14369 }
14370 (void)submit_or_replace(std::move(request), std::move(snapshot),
14371 command.opening, command.replacement_key);
14372 }
14373 return;
14374 }
14375 if (variable_short_context && !potential_short_seed) {
14376 // A variable-size source callback is tentatively staged because the
14377 // exact ShortSeed book is only recognizable after all commands return.
14378 // A nonmatching batch must go back through ordinary native requests in
14379 // source order; it must never inherit the frozen transaction behavior.
14380 for (auto& command : queued) {
14381 auto request = std::move(command.request);
14382 auto snapshot = std::move(command.snapshot);
14383 if (snapshot.frozen_market_targeted_close) {
14384 request.intent = native_order::HostSized{
14385 native_order::HostSizedKind::Close, std::nullopt};
14386 request.owner = owner_for_close(snapshot.from_entry, true);
14387 snapshot.deferred_cohort = true;
14388 snapshot.frozen_market_targeted_close = false;
14389 snapshot.frozen_market_instruction = false;
14390 } else {
14391 snapshot.frozen_market_instruction = false;
14392 }
14393 const auto accepted = submit_or_replace(
14394 std::move(request), std::move(snapshot), command.opening,
14395 command.replacement_key);
14396 apply_known_reversal_gap(accepted);
14397 }
14398 return;
14399 }
14400
14401 // Legacy `finalize_same_bar_market_tx_book` retains command order within
14402 // each broker-side pass but moves every BUY member before every SELL
14403 // member. The generic request core keeps submission order on an equal
14404 // point, so materialising the source batch in that order is sufficient
14405 // and does not add a source branch to generic matching.
14406 const bool cap_close_continuation =
14407 cap.configuration().count_pooc_full_close && queued.size() == 2U
14408 && std::count_if(queued.begin(), queued.end(), [](const auto& command) {
14409 return command.snapshot.frozen_market_targeted_close;
14410 }) == 1;
14411 // A single command keeps its place; std::stable_sort would still take a
14412 // temporary buffer and move the command through it and back.
14413 if (queued.size() > 1) {
14414 std::stable_sort(queued.begin(), queued.end(), [&](const PendingSameBarCommand& left,
14415 const PendingSameBarCommand& right) {
14416 const auto buy_rank = [](const PendingSameBarCommand& command) {
14417 if (command.snapshot.frozen_market_targeted_close)
14418 return command.snapshot.frozen_market_target_was_long ? 1 : 0;
14419 return command.snapshot.is_long ? 0 : 1;
14420 };
14421 const int left_rank = buy_rank(left);
14422 const int right_rank = buy_rank(right);
14423 if (left_rank != right_rank) return left_rank < right_rank;
14424 if (cap_close_continuation
14425 && left.snapshot.frozen_market_targeted_close
14426 != right.snapshot.frozen_market_targeted_close) {
14427 return left.snapshot.frozen_market_targeted_close;
14428 }
14429 return false;
14430 });
14431 }
14432
14433 const bool single_entry = queued.size() == 1
14434 && !queued.front().snapshot.frozen_market_targeted_close;
14435 const bool one_entry_one_close = queued.size() == 2U
14436 && std::count_if(queued.begin(), queued.end(), [](const auto& command) {
14437 return command.opening && !command.snapshot.frozen_market_targeted_close;
14438 }) == 1
14439 && std::count_if(queued.begin(), queued.end(), [](const auto& command) {
14440 return command.snapshot.frozen_market_targeted_close;
14441 }) == 1;
14442 double simulated = batch_start;
14443 std::optional<native_order::RequestHandle> short_seed_long;
14444 std::optional<native_order::RequestHandle> short_seed_materialize;
14445 std::optional<native_order::RequestHandle> short_seed_final;
14446 std::optional<native_order::RequestHandle> cap_close_parent;
14447 for (std::size_t i = 0; i < queued.size(); ++i) {
14448 auto& command = queued[i];
14449 auto request = std::move(command.request);
14450 const bool targeted_close = command.snapshot.frozen_market_targeted_close;
14451 auto snapshot = std::move(command.snapshot);
14452 bool opening = command.opening;
14453 const bool long_candidate = batch_start < 0.0 && opening
14454 && snapshot.family == PineOrderFamily::Entry && snapshot.is_long;
14455 const bool final_short_candidate = batch_start < 0.0 && opening
14456 && snapshot.family == PineOrderFamily::Entry && !snapshot.is_long;
14457 const bool materialize_candidate = batch_start < 0.0
14458 && snapshot.frozen_market_targeted_close
14459 && !snapshot.frozen_market_target_was_long;
14460
14461 if (!snapshot.frozen_market_targeted_close) {
14462 double units = snapshot.frozen_market_transaction_units;
14463 if (cap_close_continuation && simulated == 0.0
14464 && finite_positive(snapshot.frozen_market_own_units)) {
14465 units = snapshot.frozen_market_own_units;
14466 }
14467 if (!finite_positive(units)) continue;
14468 if (p2_explicit_pair
14469 && units > snapshot.frozen_market_own_units + 1e-10) {
14470 const double margin = snapshot.is_long
14471 ? config_.margin_long : config_.margin_short;
14472 const double required = units * snapshot.sizing.price
14473 * staged_.syminfo.pointvalue * snapshot.sizing.fx
14474 * margin / 100.0;
14475 if (!std::isfinite(required) || !std::isfinite(snapshot.sizing.equity)
14476 || required > snapshot.sizing.equity) {
14477 continue;
14478 }
14479 }
14480 if (single_entry && !fixed_unmargined_market_batch_scope()) {
14481 const auto accepted = submit_or_replace(std::move(request), std::move(snapshot), opening,
14482 command.replacement_key);
14483 apply_known_reversal_gap(accepted);
14484 if (accepted && long_candidate) short_seed_long = *accepted;
14485 if (accepted && final_short_candidate) short_seed_final = *accepted;
14486 continue;
14487 }
14488 const bool affordability_reversal = snapshot.affordability_policy_active
14489 && batch_start != 0.0
14490 && ((batch_start > 0.0) != snapshot.is_long)
14491 && (snapshot.affordability_close_only || one_entry_one_close);
14492 if (affordability_reversal) {
14493 request.intent = native_order::HostSized{
14494 native_order::HostSizedKind::Open,
14495 snapshot.is_long ? native_order::Side::Long
14496 : native_order::Side::Short};
14497 } else {
14499 snapshot.is_long ? units : -units};
14500 }
14501 simulated += snapshot.is_long ? units : -units;
14502 } else {
14503 const double target = snapshot.requested_qty;
14504 if (!finite_positive(target) || simulated == 0.0) continue;
14505 const bool target_long = snapshot.frozen_market_target_was_long;
14506 const bool still_target_side = (simulated > 0.0) == target_long;
14507 const double units = std::min(target, std::abs(simulated));
14508 if (!finite_positive(units)) continue;
14509 if (still_target_side) {
14510 request.intent = cap_close_continuation
14511 && units >= std::abs(simulated) - 1e-10
14513 : native_order::OrderIntent{native_order::Reduce{
14514 native_order::ExplicitUnits{units}}};
14515 simulated += simulated > 0.0 ? -units : units;
14516 } else {
14517 // A default-FIFO close whose original side was consumed may
14518 // become the legacy artifact only when its same-id frozen
14519 // MARKET entry is still later in the sorted broker pass.
14520 bool artifact = false;
14521 for (std::size_t later = i + 1; later < queued.size(); ++later) {
14522 const auto& sibling = queued[later];
14523 if (!sibling.snapshot.frozen_market_targeted_close
14524 && sibling.snapshot.frozen_market_instruction
14525 && sibling.snapshot.source_id == snapshot.source_id
14526 && sibling.snapshot.is_long == target_long) {
14527 artifact = true;
14528 break;
14529 }
14530 }
14531 if (!artifact) continue;
14532 const double signed_units = simulated > 0.0 ? units : -units;
14533 request.intent = native_order::Transact{signed_units};
14534 simulated += signed_units;
14535 // This is a broker-created artifact lot carrying the close
14536 // label, not a new source-id cohort member.
14537 opening = false;
14538 }
14539 }
14540 if (cap_close_continuation && opening && cap_close_parent)
14541 request.owner = native_order::WaitForApplied{*cap_close_parent};
14542 const auto accepted = submit_or_replace(std::move(request), std::move(snapshot), opening,
14543 command.replacement_key);
14544 if (accepted && targeted_close)
14545 cap_close_parent = *accepted;
14546 apply_known_reversal_gap(accepted);
14547 if (accepted && long_candidate) short_seed_long = *accepted;
14548 if (accepted && materialize_candidate) short_seed_materialize = *accepted;
14549 if (accepted && final_short_candidate) short_seed_final = *accepted;
14550 }
14551 if (short_seed_long && short_seed_materialize && short_seed_final) {
14552 const auto materialize_snapshot = placement_.find(short_seed_materialize->incarnation);
14553 const auto long_snapshot = placement_.find(short_seed_long->incarnation);
14554 const auto final_snapshot = placement_.find(short_seed_final->incarnation);
14555 if (materialize_snapshot != placement_.end() && long_snapshot != placement_.end()
14556 && final_snapshot != placement_.end()) {
14558 plan.long_entry = *short_seed_long;
14559 plan.materialize_long = *short_seed_materialize;
14560 plan.final_short = *short_seed_final;
14561 plan.seed_id = materialize_snapshot->second.source_id;
14562 plan.long_entry_id = long_snapshot->second.source_id;
14563 plan.final_short_id = final_snapshot->second.source_id;
14564 plan.materialize_label = "__close__" + plan.seed_id;
14565 plan.seed_qty = std::abs(batch_start);
14566 plan.seed_cycle = current_position_cycle_;
14567 pending_short_seed_ = {std::move(plan), broker_open_epoch_ + 1U, true};
14568 // This immutable receipt is available to the fixture's historical
14569 // handle probe even when a finite run ends before the next broker
14570 // open. PendingIntentView still exposes roles only after the
14571 // next-open live qualification below.
14572 short_seed_ = pending_short_seed_.plan;
14573 maybe_activate_short_seed_plan();
14574 }
14575 }
14576}
14577
14578void PineExecutionAdapter::materialize_relative_exits(
14579 PlacementSnapshot opening, const native_order::ExecutionAppliedEvent& event) {
14580 if (pending_relative_exits_.empty() || !finite_positive(staged_.syminfo.mintick)) {
14581 // No queued definition is served by this fill, so no child the
14582 // kernel armed on it has a source request to become.
14583 withdraw_anchored_relative_legs(nullptr, &opening.source_id);
14584 return;
14585 }
14586 std::vector<PendingRelativeExit> pending;
14587 std::vector<PendingRelativeExit> global_pending;
14588 // An unnamed relative exit binds to the first market opening after its call,
14589 // unless the call was void (made flat with no entry order working): the flag is
14590 // kept on the definition because the shadow rows of the calling execution are
14591 // gone when the entry fills. Not under calc_on_order_fills: no tape covers it.
14592 const bool market_opening = !config_.calc_on_order_fills
14593 && !finite_positive(opening.exit_levels.limit) && !finite_positive(opening.exit_levels.stop);
14594 for (auto it = pending_relative_exits_.begin(); it != pending_relative_exits_.end();) {
14595 if (it->from_entry == opening.source_id) {
14596 pending.push_back(std::move(*it));
14597 it = pending_relative_exits_.erase(it);
14598 } else if (market_opening && it->from_entry.empty() && !it->void_global) {
14599 global_pending.push_back(std::move(*it));
14600 it = pending_relative_exits_.erase(it);
14601 } else {
14602 ++it;
14603 }
14604 }
14605 // Decided once for the whole parent: either every armed child is the
14606 // request this fill point submits, or all of them leave first and the
14607 // re-run below is the unchanged source pipeline.
14608 if (!armed_relative_legs_adoptable(opening, event, pending))
14609 withdraw_anchored_relative_legs(nullptr, &opening.source_id);
14610 for (auto& value : global_pending) pending.push_back(std::move(value));
14611 const double tick = staged_.syminfo.mintick;
14612 for (const auto& value : pending) {
14613 double limit = kNaN;
14614 double stop = kNaN;
14615 double offset = value.trail_offset;
14616 // `ticks` from the parent's fill, snapped to the grid on the side that
14617 // keeps the level at least that far away (the kernel's Directional
14618 // anchor rounding spells the same ladder point for an armed child).
14619 const double side = opening.is_long ? 1.0 : -1.0;
14620 if (finite_positive(value.profit_ticks)) {
14621 limit = directional_tick(event.resolved_price + side * value.profit_ticks * tick,
14622 tick, opening.is_long);
14623 if (std::isfinite(event.resolved_price) && !std::isfinite(limit)) {
14624 engine_command_value_invalid_or_count("relative_price.level",
14625 EngineValueStage::relative_price, EngineValueReason::numeric_overflow);
14626 }
14627 }
14628 if (finite_positive(value.loss_ticks)) {
14629 stop = directional_tick(event.resolved_price - side * value.loss_ticks * tick,
14630 tick, !opening.is_long);
14631 if (std::isfinite(event.resolved_price) && !std::isfinite(stop)) {
14632 engine_command_value_invalid_or_count("relative_price.level",
14633 EngineValueStage::relative_price, EngineValueReason::numeric_overflow);
14634 }
14635 }
14636 // An omitted trail_offset is a one-shot activation leg in Pine. Do
14637 // not synthesize a trailing distance from trail_points here; an
14638 // explicit zero/sub-tick offset remains distinguishable and is
14639 // lowered by exit()'s native sentinel policy.
14640 materializing_relative_ = true;
14641 materializing_parent_ = event.handle();
14642 try {
14643 exit(value.exit_id, value.from_entry, limit, stop, value.trail_points, offset,
14644 value.trail_price, value.qty_percent, value.comment, value.qty, value.oca_name,
14645 value.from_entry.empty() ? value.profit_ticks : kNaN,
14646 value.from_entry.empty() ? value.loss_ticks : kNaN);
14647 } catch (...) {
14648 materializing_relative_ = false;
14649 materializing_parent_ = {};
14650 throw;
14651 }
14652 materializing_relative_ = false;
14653 materializing_parent_ = {};
14654 }
14655 // Structural, not counted: no child armed on this parent outlives the
14656 // parent's own fill notification. One the re-run did not adopt is not the
14657 // request this fill point asks for (another trigger, a staged or refused
14658 // leg), the source pipeline above already did what it does, and a kernel
14659 // request without a placement row must never reach a match.
14660 withdraw_anchored_relative_legs(nullptr, &opening.source_id);
14661}
14662
14663void PineExecutionAdapter::withdraw_anchored_relative_legs(
14664 const SourceId* exit_id, const SourceId* from_entry) {
14665 const SourceId empty;
14666 const auto exit_text = detail::exit_scratch_text(exit_id ? *exit_id : empty);
14667 const auto entry_text = detail::exit_scratch_text(from_entry ? *from_entry : empty);
14668 withdraw_anchored_relative_readers(exit_id ? &exit_text : nullptr,
14669 from_entry ? &entry_text : nullptr);
14670}
14671
14672void PineExecutionAdapter::withdraw_anchored_relative_readers(
14673 const detail::ScratchByteSource* exit_id, const detail::ScratchByteSource* from_entry) {
14674 for (std::size_t index = 0; index < anchored_relative_legs_.size();) {
14675 const auto& leg = anchored_relative_legs_[index];
14676 if ((exit_id && !detail::exit_scratch_equal(
14677 detail::exit_scratch_text(leg.exit_id), *exit_id))
14678 || (from_entry && !detail::exit_scratch_equal(
14679 detail::exit_scratch_text(leg.from_entry), *from_entry))) {
14680 ++index;
14681 continue;
14682 }
14683 // Cancel first: a host that refuses the command throws, and the
14684 // record of a child that may still be live must survive that.
14685 const native_order::RequestHandle handle = leg.handle;
14686 (void)require_host().cancel(handle);
14687 anchored_relative_legs_.erase(anchored_relative_legs_.begin()
14688 + static_cast<std::ptrdiff_t>(index));
14689 ++anchored_relative_stats_.withdrawn;
14690 }
14691}
14692
14693bool PineExecutionAdapter::anchorable_relative_exit(
14694 const PendingRelativeExit& value, native_order::RequestHandle& parent,
14695 bool& parent_long) const {
14696 // The kernel arms the child as a host-sized close of the book its
14697 // parent's fill leaves (NativeArmScope::Book), which is this adapter's
14698 // fill-point leg under the FIFO close rule. The ANY rule binds that leg
14699 // to the named cohort instead, a scope the arm does not spell; and an
14700 // explicit quantity is not born at the fill point at all -- exit() stages
14701 // it per origin (pending_bracket_legs_) and submits it at a later flush.
14702 if (value.from_entry.empty() || config_.close_entries_rule_any || !std::isnan(value.qty))
14703 return false;
14704 if (!finite_positive(staged_.syminfo.mintick)) return false;
14705 const auto staged_same_id = [&](const PlacementSnapshot& row) {
14706 return row.opening && row.source_id == value.from_entry;
14707 };
14708 for (const auto& row : pending_same_bar_commands_)
14709 if (staged_same_id(row.snapshot)) return false;
14710 for (const auto& row : pending_entries_)
14711 if (staged_same_id(row.snapshot)) return false;
14712 for (const auto& row : pending_coof_requests_)
14713 if (staged_same_id(row.snapshot)) return false;
14714 for (const auto& row : delayed_market_orders_)
14715 if (staged_same_id(row.snapshot)) return false;
14716 std::size_t parents = 0;
14717 native_order::RequestHandle only{};
14718 bool only_long = true;
14719 for (const auto& handle : live_handles_) {
14720 const auto found = placement_.find(handle.incarnation);
14721 if (found == placement_.end() || !found->second.opening
14722 || found->second.source_id != value.from_entry) {
14723 continue;
14724 }
14725 if (found->second.family != PineOrderFamily::Entry) return false;
14726 only = handle;
14727 only_long = found->second.is_long;
14728 ++parents;
14729 }
14730 if (parents != 1) return false;
14731 // A trailing offset of a tick or more is the one relative leg exit()
14732 // seeds: its running best starts AT the activation (Trail::best_seed), as
14733 // TradingView's does on 13 of 13 pending-entry tapes
14734 // (tests/test_pending_entry_trail_tapes.cpp). The anchored child cannot
14735 // carry that seed -- the kernel's FromOwnerFill anchor installs the arm
14736 // level alone -- and adopted, it rode from the raw arm print and exited 1
14737 // to 24 bars late. So the definition is not anchorable, and the parent's
14738 // fill point submits the seeded leg (the order-schedule correction).
14739 for (const auto& shape : relative_leg_shapes(value, only_long))
14740 if (std::holds_alternative<native_order::Trail>(shape.trigger)) return false;
14741 parent = only;
14742 parent_long = only_long;
14743 return true;
14744}
14745
14746std::vector<PineExecutionAdapter::RelativeLegShape>
14747PineExecutionAdapter::relative_leg_shapes(const PendingRelativeExit& value,
14748 bool parent_long) const {
14749 // The legs exit() emits for the relative operands once they resolve, in
14750 // its own order (limit, stop, trail), each spelled as an anchored trigger:
14751 // the level placeholder the kernel fills at the arm and the signed tick
14752 // distance from the parent's fill.
14753 std::vector<RelativeLegShape> shapes;
14754 const double side = parent_long ? 1.0 : -1.0;
14755 if (finite_positive(value.profit_ticks)) {
14756 shapes.push_back({PineOrderFamily::ExitLimit, native_order::Limit{0.0},
14757 side * value.profit_ticks, value.profit_ticks});
14758 }
14759 if (finite_positive(value.loss_ticks)) {
14760 shapes.push_back({PineOrderFamily::ExitStop, native_order::Stop{0.0},
14761 -side * value.loss_ticks, value.loss_ticks});
14762 }
14763 if (std::isfinite(value.trail_points) && !finite_positive(value.trail_price)) {
14764 const double trail_ticks = source_trail_points_ticks(
14765 value.trail_points, staged_.syminfo.mintick);
14766 const bool has_offset = std::isfinite(value.trail_offset) && value.trail_offset >= 0.0;
14767 const double offset_ticks = has_offset ? std::floor(value.trail_offset) : kNaN;
14768 if (trail_ticks >= 1.0 && has_offset && offset_ticks >= 1.0) {
14769 // Never placed: exit() seeds this leg, which the anchored child
14770 // cannot carry, so anchorable_relative_exit refuses its definition.
14771 shapes.push_back({PineOrderFamily::ExitTrail,
14772 native_order::Trail{0.0, 0.0,
14773 native_order::TrailTicks{offset_ticks}},
14774 side * trail_ticks, trail_ticks});
14775 } else if (trail_ticks >= 1.0 && (has_offset || !std::isfinite(value.trail_offset))) {
14776 // Omitted or explicit-zero offset: the one-shot activation touch,
14777 // a fill-through limit when the zero shape slips.
14778 shapes.push_back({PineOrderFamily::ExitTrail,
14779 native_order::Limit{0.0, has_offset && config_.slippage > 0},
14780 side * trail_ticks, trail_ticks});
14781 }
14782 }
14783 return shapes;
14784}
14785
14787 if (anchored_relative_legs_.empty() && pending_relative_exits_.empty()) return;
14788 // An armed child is adopted or withdrawn inside its parent's own fill
14789 // notification. One that is still here was armed by a fill this adapter
14790 // never materialized, and a kernel request without a placement row must
14791 // never reach a match.
14792 for (std::size_t index = 0; index < anchored_relative_legs_.size();) {
14793 if (!anchored_relative_legs_[index].armed) { ++index; continue; }
14794 const SourceId exit_id = anchored_relative_legs_[index].exit_id;
14795 const SourceId from_entry = anchored_relative_legs_[index].from_entry;
14796 withdraw_anchored_relative_legs(&exit_id, &from_entry);
14797 index = 0;
14798 }
14799 // A child whose definition is gone waits for nothing.
14800 for (std::size_t index = 0; index < anchored_relative_legs_.size();) {
14801 const auto& leg = anchored_relative_legs_[index];
14802 const bool queued = std::any_of(
14803 pending_relative_exits_.begin(), pending_relative_exits_.end(),
14804 [&](const PendingRelativeExit& value) {
14805 return value.exit_id == leg.exit_id && value.from_entry == leg.from_entry;
14806 });
14807 if (queued) { ++index; continue; }
14808 const SourceId exit_id = leg.exit_id;
14809 const SourceId from_entry = leg.from_entry;
14810 withdraw_anchored_relative_legs(&exit_id, &from_entry);
14811 index = 0;
14812 }
14813 if (pending_relative_exits_.empty()) return;
14814 // One decision per parent id: the armed children of a fill are adopted
14815 // together or not at all, so every queued exit of that id must be
14816 // expressible, or none is anchored and the fill point submits them all.
14817 std::vector<SourceId> parents;
14818 for (const auto& value : pending_relative_exits_) {
14819 if (std::find(parents.begin(), parents.end(), value.from_entry) == parents.end())
14820 parents.push_back(value.from_entry);
14821 }
14822 for (const auto& from_entry : parents) {
14824 bool parent_long = true;
14825 bool anchorable = true;
14826 for (const auto& value : pending_relative_exits_) {
14827 if (value.from_entry != from_entry) continue;
14828 anchorable = anchorable && anchorable_relative_exit(value, parent, parent_long);
14829 }
14830 const bool anchored = std::any_of(
14831 anchored_relative_legs_.begin(), anchored_relative_legs_.end(),
14832 [&](const AnchoredRelativeLeg& leg) { return leg.from_entry == from_entry; });
14833 if (anchored) {
14834 // A changed definition already withdrew its legs in exit(); what
14835 // is left here waits on this parent or on a parent that is gone.
14836 const bool complete = anchorable && std::all_of(
14837 pending_relative_exits_.begin(), pending_relative_exits_.end(),
14838 [&](const PendingRelativeExit& value) {
14839 if (value.from_entry != from_entry) return true;
14840 const auto shapes = relative_leg_shapes(value, parent_long);
14841 const auto legs = static_cast<std::size_t>(std::count_if(
14842 anchored_relative_legs_.begin(), anchored_relative_legs_.end(),
14843 [&](const AnchoredRelativeLeg& leg) {
14844 return leg.exit_id == value.exit_id
14845 && leg.from_entry == from_entry && leg.parent == parent;
14846 }));
14847 return legs == shapes.size();
14848 });
14849 if (complete) continue;
14850 withdraw_anchored_relative_legs(nullptr, &from_entry);
14851 }
14852 if (!anchorable) continue;
14853 for (const auto& value : pending_relative_exits_) {
14854 if (value.from_entry != from_entry) continue;
14855 const SourceId group_name = value.oca_name.empty()
14856 ? value.exit_id + "\x1f" + value.from_entry : value.oca_name;
14857 for (auto& shape : relative_leg_shapes(value, parent_long)) {
14858 AnchoredRelativeLeg leg;
14859 leg.exit_id = value.exit_id;
14860 leg.from_entry = value.from_entry;
14861 leg.family = shape.family;
14862 leg.parent = parent;
14863 leg.parent_long = parent_long;
14864 leg.operand_ticks = shape.operand_ticks;
14865 leg.trail_offset = value.trail_offset;
14866 leg.request.intent = native_order::HostSized{
14867 native_order::HostSizedKind::Close, std::nullopt};
14868 leg.request.label = value.exit_id;
14869 leg.request.comment = value.comment;
14870 leg.request.trigger = std::move(shape.trigger);
14871 // The leg exit() submits in the parent's fill callback is
14872 // born after the fill print and closes the book; so does this
14873 // child (AfterArmPrint, Book).
14874 leg.request.owner = native_order::WaitForApplied{
14875 parent, native_order::NativeArmVisibility::PendingUntilArmed,
14876 native_order::NativeArmFirstMatch::AfterArmPrint,
14877 native_order::NativeArmScope::Book};
14878 leg.request.group = group_for(group_name, 1,
14879 static_cast<std::int64_t>(shape.family));
14880 if (auto* member = std::get_if<native_order::Member>(&leg.request.group)) {
14881 // The kernel tells OCA siblings apart by cohort, so every
14882 // member needs its own. A submitted sibling carries its
14883 // source sequence (counting up from one); an anchored one
14884 // counts down from minus one.
14885 if (anchored_cohort_sequence_
14886 == std::numeric_limits<std::int64_t>::max()) {
14888 "Pine anchored OCA member sequence exhausted");
14889 }
14890 member->cohort = -(++anchored_cohort_sequence_);
14891 }
14892 leg.request.anchor = native_order::FromOwnerFill{
14893 shape.signed_ticks, true, native_order::NativeAnchorRounding::Directional};
14894 const auto result = require_host().submit(leg.request);
14895 if (result.status != native_order::SubmitStatus::Accepted || !result.handle)
14896 continue;
14897 leg.handle = *result.handle;
14898 anchored_relative_legs_.push_back(std::move(leg));
14899 ++anchored_relative_stats_.anchored;
14900 }
14901 }
14902 }
14903}
14904
14905bool PineExecutionAdapter::armed_relative_legs_adoptable(
14906 const PlacementSnapshot& opening, const native_order::ExecutionAppliedEvent& event,
14907 const std::vector<PendingRelativeExit>& pending) const {
14908 // The fill-point re-run submits exactly the queued legs, at once, only
14909 // when nothing it would stage them behind is waiting.
14910 if (!pending_entries_.empty() || !pending_same_bar_commands_.empty()
14911 || !delayed_market_orders_.empty() || !pending_coof_requests_.empty()
14912 || coof_recalc_active_) {
14913 return false;
14914 }
14915 // Leg for leg: every shape of every queued definition has its own armed
14916 // child on this parent, and this parent carries no other child.
14917 std::size_t expected = 0;
14918 for (const auto& value : pending) {
14919 for (const auto& shape : relative_leg_shapes(value, opening.is_long)) {
14920 const auto children = std::count_if(
14921 anchored_relative_legs_.begin(), anchored_relative_legs_.end(),
14922 [&](const AnchoredRelativeLeg& leg) {
14923 return leg.exit_id == value.exit_id && leg.from_entry == value.from_entry
14924 && leg.family == shape.family
14925 && leg.request.trigger.index() == shape.trigger.index();
14926 });
14927 if (children != 1) return false;
14928 ++expected;
14929 }
14930 }
14931 std::size_t armed = 0;
14932 for (const auto& leg : anchored_relative_legs_) {
14933 if (leg.from_entry != opening.source_id) continue;
14934 if (leg.parent != event.handle() || !leg.armed || leg.parent_long != opening.is_long
14935 || !std::isfinite(leg.installed_level)) {
14936 return false;
14937 }
14938 ++armed;
14939 }
14940 return armed == expected && armed != 0;
14941}
14942
14943namespace {
14944// resting_limit_tick_reach's scope (exit_limit_trigger): an on-grid limit
14945// under calc_on_order_fills rests at its tick-built threshold only here.
14946bool resting_limit_tick_reach(const NativeStrategyHost* host, double threshold,
14947 bool exit_is_buy) noexcept {
14950 return false;
14951 }
14952 // The threshold sits half a tick toward the side the leg closes, so it
14953 // holds only for the position held now: a leg placed for a parent not held
14954 // on that side keeps the raw level, which reads the same from either side.
14955 // Held the other way, the threshold would take the pending parent's side.
14956 // Placed flat, the leg is re-armed at its parent's opening, where this is
14957 // decided again with the side known.
14958 const double held = detail::run_position(*host).signed_units;
14959 if (held == 0.0 || (held < 0.0) != exit_is_buy) return false;
14960 // Pinned on the chart path, as the in-flight reach is (exit()); a
14961 // magnifier's intrabars keep the raw level.
14962 const auto native = detail::run_state(*host);
14963 if (native.spec && !native.spec->intrabar.is_none()) return false;
14964 // A limit the point it is placed at already reaches is decided there by
14965 // the raw level, as before. For a point that stands on the raw level the
14966 // matcher books the level at that point, while a threshold the point has
14967 // passed would wait for the next one. A point that is a fill booked at the
14968 // print of a half-tick-short open is not on the raw path, which then never
14969 // reaches the level; TradingView fills such a limit at once
14970 // (callback_lifecycle repeated-rounded, its trades 18-19, recorded).
14971 const auto* point = detail::callback_point(*host);
14972 if (point && finite_positive(point->price)
14973 && (exit_is_buy ? point->price <= threshold : point->price >= threshold)) {
14974 return false;
14975 }
14976 return true;
14977}
14978} // namespace
14979
14980double PineExecutionAdapter::exit_limit_trigger(double limit_price, double tick,
14981 bool exit_is_buy) const noexcept {
14982 // ab9714be pine_policy_members.cpp:11-17 + engine.hpp:1374-1381: an
14983 // exit limit is tested against the tick-quantized bar, so an ON-grid
14984 // level is reached by a raw extreme half a tick short of it (NYSE:F
14985 // synthetic high 25.045 -> 25.05 fills a 25.05 sell limit). Under
14986 // calc_on_order_fills an on-grid level stays raw, except for a limit
14987 // left resting on the chart path that closes the position held when it
14988 // is placed (resting_limit_tick_reach): a synthetic 25.05 sell limit born in the
14989 // opening fill's recalculation fills on that bar, whose H of 25.045
14990 // prints 25.05 (`lab tv` synthetics tests/fixtures/callback_lifecycle
14991 // newborn-limit-*, NYSE:F 15m, 4 identical exports each).
14992 if (!finite_positive(tick)) return limit_price;
14993 const double threshold = source_trigger_threshold(limit_price, tick, exit_is_buy, true);
14994 if (config_.calc_on_order_fills && nearest_tick(limit_price, tick) == limit_price
14995 && !resting_limit_tick_reach(host_, threshold, exit_is_buy)) {
14996 return limit_price;
14997 }
14998 return threshold;
14999}
15000
15001double PineExecutionAdapter::one_shot_trail_trigger(double activation, double source_trail_offset,
15002 double tick, bool exit_is_buy,
15003 bool quantized_activation) const noexcept {
15004 double one_shot_level = activation;
15005 if (!std::isfinite(source_trail_offset)) {
15006 const double slipped = one_shot_level
15007 + (exit_is_buy ? 1.0 : -1.0) * config_.slippage * tick;
15008 one_shot_level = directional_tick(slipped, tick, exit_is_buy);
15009 }
15010 // A sell one-shot keeps the k / (1 / mintick) grid spelling so a
15011 // chart print sitting exactly on the activation satisfies the
15012 // generic `print >= level` bit-for-bit. A buy one-shot keeps the
15013 // directional k * mintick spelling instead: ab9714be books the
15014 // crossed activation as a TRAIL event (engine_path_resolve.cpp:
15015 // 984-987, fill.is_limit = false), so apply_slippage snaps it
15016 // with round_to_mintick_directional, which materializes that same
15017 // product, and the immutable generic buy limit accepts it because
15018 // fill <= level holds by construction. Re-spelling the buy level
15019 // onto the division grid handed back a value one binary64 ULP
15020 // away from the owner's fill and that ULP rode the equity path
15021 // into printed PnL
15022 // (a synthetic illustration of the one-shot trailing-stop rule:
15023 // tick count 115 at tick 0.01 gives the product 1.1500000000000001,
15024 // while the division grid re-spells it as 1.15).
15025 // ab9714be pine_fills.cpp:4607-4610: snap_trail_level_to_tick_grid
15026 // aligns the trail level to the tick grid (sell one-shot).
15027 if (!exit_is_buy) one_shot_level = source_level_on_price_grid(one_shot_level, tick);
15028 // ab9714be engine_path_resolve.cpp:297-308 and 746-766: an
15029 // omitted-offset trail's dormant activation is reached on the
15030 // tick-quantized path (design-trail-activation-tick-bar), so a
15031 // raw extreme half a tick short of the level already fires it.
15032 // Only the arm threshold moves; settlement books the source
15033 // level (source_trail_one_shot_fill).
15034 // ab9714be engine_path_resolve.cpp:292-308 + 746-766 test the
15035 // explicit-zero trail's dormant activation on the same
15036 // tick-quantized path, so its arm threshold is the same half-up
15037 // projection boundary: a buy one-shot is reached only strictly
15038 // below activation + half a tick (synthetic NYSE:F low
15039 // 25.005 prints 25.01 and does not reach the 25.00 activation).
15040 if (quantized_activation && finite_positive(tick)) {
15041 one_shot_level = source_trigger_threshold(one_shot_level, tick, exit_is_buy, true);
15042 }
15043 return one_shot_level;
15044}
15045
15047 const NativeAnchoredLevelView& view) const {
15048 const auto found = std::find_if(
15049 anchored_relative_legs_.begin(), anchored_relative_legs_.end(),
15050 [&](const AnchoredRelativeLeg& leg) { return leg.handle == view.leg; });
15051 if (found == anchored_relative_legs_.end()) return std::nullopt;
15052 auto& leg = *found;
15053 const double tick = staged_.syminfo.mintick;
15054 leg.armed = true;
15055 leg.installed_level = kNaN;
15056 // Without a usable tick there is no projection to restate; the kernel
15057 // keeps its own level and the fill point withdraws the child.
15058 if (!finite_positive(tick)) return std::nullopt;
15059 // The kernel owns the fill-relative arithmetic: view.kernel_level is
15060 // fill + ticks on the tick ladder (FromOwnerFill, Directional), the very
15061 // ladder point materialize_relative_exits hands the fill-point exit().
15062 // What stays here is TradingView's spelling of that point and its
15063 // trigger projection, shared with exit().
15064 const bool exit_is_buy = !leg.parent_long;
15065 double installed = kNaN;
15066 if (leg.family == PineOrderFamily::ExitLimit) {
15067 installed = exit_limit_trigger(
15068 source_level_on_price_grid(view.kernel_level, tick), tick, exit_is_buy);
15069 } else if (leg.family == PineOrderFamily::ExitStop) {
15070 installed = source_trigger_threshold(
15071 source_level_on_price_grid(view.kernel_level, tick), tick, exit_is_buy, false);
15072 } else {
15073 const double trail_price = directional_tick(view.kernel_level, tick, leg.parent_long);
15074 installed = std::holds_alternative<native_order::Trail>(leg.request.trigger)
15075 ? source_trail_arm_level(trail_price, tick, exit_is_buy)
15076 : one_shot_trail_trigger(trail_price, leg.trail_offset, tick, exit_is_buy, true);
15077 }
15078 // The kernel refuses a level its trigger cannot hold and fails the run.
15079 // A projection that lands there (a short's relative level below zero) is
15080 // never the request the fill-point re-run submits, so the child is armed
15081 // on a representable placeholder. That is safe for one reason only: its
15082 // NaN installed level makes it unadoptable, and materialize_relative_exits
15083 // withdraws every unadopted child inside this same fill notification,
15084 // before the path resumes.
15085 const bool trail_arm = std::holds_alternative<native_order::Trail>(leg.request.trigger);
15086 if (!(trail_arm ? finite_positive(installed) : finite_non_negative(installed))) {
15087 leg.installed_level = kNaN;
15088 return tick;
15089 }
15090 leg.installed_level = installed;
15091 return installed;
15092}
15093
15095 const SourceId& from_entry,
15096 const std::string&) {
15097 reroute_fixed_entries_before_request();
15098 const auto key = key_for(exit_id, from_entry);
15099 pending_relative_exits_.erase(std::remove_if(pending_relative_exits_.begin(), pending_relative_exits_.end(),
15100 [&](const PendingRelativeExit& value) {
15101 return value.exit_id == exit_id && value.from_entry == from_entry;
15102 }), pending_relative_exits_.end());
15103 if (!materializing_relative_) withdraw_anchored_relative_legs(&exit_id, &from_entry);
15104 pending_write(pending_bracket_legs_).erase(std::remove_if(pending_bracket_legs_.begin(), pending_bracket_legs_.end(),
15105 [&](const PendingBracketLeg& leg) { return leg.family_key == key; }), pending_bracket_legs_.end());
15106 // Cancellation can synchronously change source state. Copy the family
15107 // roster and release the map iterator before issuing any host operation.
15108 std::vector<native_order::RequestHandle> handles;
15109 std::size_t settled = 0;
15110 native_order::RunIdentity settled_run{};
15111 bool found_family = false;
15112 if (const auto found = bracket_families_.find(key); found != bracket_families_.end()) {
15113 handles = found->second.members();
15114 settled = found->second.settled_count();
15115 settled_run = found->second.run_identity();
15116 bracket_families_.erase(found);
15117 found_family = true;
15118 } else if (const auto archived = settled_bracket_families_.find(key);
15119 archived != settled_bracket_families_.end()) {
15120 settled = archived->second.count;
15121 settled_run = archived->second.run;
15122 settled_bracket_digest_ ^= settled_bracket_hash(key, archived->second);
15123 settled_bracket_families_.erase(archived);
15124 found_family = true;
15125 }
15126 if (!found_family) return;
15127 // Source bracket-cancel target contract (L19h). The pin below covers the
15128 // non-working tail, archived prefix, and mixed filled/live rosters.
15129 // Every non-working member, including the settled prefix, records one
15130 // NotWorking outcome targeting same-run UINT64_MAX in roster order.
15131 // Settled members retain only count/digest state, so normalize the tail: the
15132 // recorded target must not depend on placement-retirement timing.
15133 // Working members still target their own handles. Pinned by
15134 // test_bracket_cancel_not_working_target; no hash domain is changed.
15135 constexpr std::uint64_t kNeverIssued = std::numeric_limits<std::uint64_t>::max();
15136 for (std::size_t index = 0; index < settled; ++index)
15137 (void)require_host().cancel(native_order::RequestHandle{settled_run, kNeverIssued});
15138 IExecutionConsumer* consumer = bound_consumer();
15139 for (const auto& handle : handles) {
15140 const bool working = consumer == nullptr
15141 || as_native_consumer(*consumer).request_core().find_live(handle) != nullptr;
15142 const auto target = working ? handle
15143 : native_order::RequestHandle{handle.run, kNeverIssued};
15144 const auto result = require_host().cancel(target);
15145 if (result.status == native_order::CancelStatus::Cancelled) retire(handle);
15146 }
15147}
15148
15150 reroute_fixed_entries_before_request();
15151 stash_id_bound_exits(id);
15152 if (PF_RARE(!id_bound_exits_.empty())) {
15153 id_bound_exits_.erase(std::remove_if(id_bound_exits_.begin(), id_bound_exits_.end(),
15154 [&](const IdBoundExit& row) { return row.exit_id == id; }), id_bound_exits_.end());
15155 }
15156 NamedEntryCancelToken token;
15157 for (const auto& handle : live_handles_) {
15158 const auto snapshot = placement_.find(handle.incarnation);
15159 if (snapshot == placement_.end()) continue;
15160 const auto family = snapshot->second.family;
15161 if (family == PineOrderFamily::Entry && snapshot->second.source_id == id) {
15162 token.entry_incarnation = handle.incarnation;
15163 } else if ((family == PineOrderFamily::ExitLimit || family == PineOrderFamily::ExitStop
15164 || family == PineOrderFamily::ExitTrail)
15165 && snapshot->second.from_entry == id
15166 && token.surviving_exit_incarnation == 0) {
15167 token.surviving_exit_incarnation = handle.incarnation;
15168 }
15169 }
15170 if (token.entry_incarnation != 0) named_entry_cancel_tokens_[id] = token;
15171 else named_entry_cancel_tokens_.erase(id);
15172 const auto same_bar_before = pending_same_bar_commands_.size();
15173 pending_write(pending_same_bar_commands_).erase(std::remove_if(pending_same_bar_commands_.begin(),
15174 pending_same_bar_commands_.end(), [&](const PendingSameBarCommand& command) {
15175 return command.snapshot.source_id == id;
15176 }), pending_same_bar_commands_.end());
15177 if (pending_same_bar_commands_.size() != same_bar_before)
15178 source_batch_mutated_ = true;
15179 pending_same_bar_close_qty_ = 0.0;
15180 for (const auto& command : pending_same_bar_commands_) {
15181 if (command.snapshot.frozen_market_targeted_close)
15182 pending_same_bar_close_qty_ += command.snapshot.requested_qty;
15183 }
15184 pending_relative_exits_.erase(std::remove_if(pending_relative_exits_.begin(), pending_relative_exits_.end(),
15185 [&](const PendingRelativeExit& value) { return value.exit_id == id || value.from_entry == id; }),
15186 pending_relative_exits_.end());
15187 withdraw_anchored_relative_legs(&id, nullptr);
15188 withdraw_anchored_relative_legs(nullptr, &id);
15189 pending_write(pending_entries_).erase(std::remove_if(pending_entries_.begin(), pending_entries_.end(),
15190 [&](const PendingEntry& entry) { return entry.snapshot.source_id == id; }), pending_entries_.end());
15191 pending_write(pending_bracket_legs_).erase(std::remove_if(pending_bracket_legs_.begin(), pending_bracket_legs_.end(),
15192 [&](const PendingBracketLeg& leg) { return leg.snapshot.source_id == id; }),
15193 pending_bracket_legs_.end());
15194 // A gapped stop waiting for a margin-call revival is withdrawn with the
15195 // rest of the id (retire_cancelled_exits).
15196 pending_margin_revivals_.erase(std::remove_if(pending_margin_revivals_.begin(),
15197 pending_margin_revivals_.end(), [&](const PendingMarginRevival& revival) {
15198 return revival.snapshot.source_id == id;
15199 }), pending_margin_revivals_.end());
15200 std::vector<native_order::RequestHandle> matches;
15201 for (const auto& handle : live_handles_) {
15202 const auto snapshot = placement_.find(handle.incarnation);
15203 if (snapshot != placement_.end() && snapshot->second.source_id == id
15204 && !coof_leg_reaches_exit(snapshot->second)) {
15205 matches.push_back(handle);
15206 }
15207 }
15208 std::vector<std::pair<std::uint64_t, std::uint64_t>> cancelled;
15209 for (const auto& handle : matches) {
15210 const auto result = require_host().cancel(handle);
15211 if (result.status == native_order::CancelStatus::Cancelled) {
15212 retire(handle);
15213 cancelled.emplace_back(handle.incarnation, result.event_ordinal);
15214 }
15215 }
15216 retire_cancelled_exits(&id, cancelled);
15217 // A cancelled exit's entry brackets went with its orders above.
15218 per_entry_exits_.erase(std::remove_if(per_entry_exits_.begin(), per_entry_exits_.end(),
15219 [&](const PerEntryExit& exit) { return exit.exit_id == id; }), per_entry_exits_.end());
15220}
15221
15223 reroute_fixed_entries_before_request();
15224 id_bound_exits_.clear();
15225 // The margin call a breach at this bar's close owes is an order the
15226 // script's cancel_all() withdraws too (close_point_margin_call).
15227 if (const auto point = detail::callback_point(require_host()))
15228 close_margin_cancelled_bar_ = point->decision.script_bar_open_ms;
15229 const auto handles = live_handles_;
15230 std::vector<std::pair<std::uint64_t, std::uint64_t>> cancelled;
15231 for (const auto& handle : handles) {
15232 const auto row = placement_.find(handle.incarnation);
15233 if (row != placement_.end() && coof_leg_reaches_exit(row->second)) continue;
15234 const auto result = require_host().cancel(handle);
15235 if (result.status == native_order::CancelStatus::Cancelled) {
15236 retire(handle);
15237 cancelled.emplace_back(handle.incarnation, result.event_ordinal);
15238 }
15239 }
15240 retire_cancelled_exits(nullptr, cancelled);
15241 pending_margin_revivals_.clear();
15242 clear_bracket_families();
15243 pending_write(pending_bracket_legs_).clear();
15244 pending_write(pending_entries_).clear();
15245 delayed_market_orders_.clear();
15246 deferred_open_marketable_sells_.clear();
15247 throttled_reopen_rearm_.clear();
15248 pending_write(pending_same_bar_commands_).clear();
15249 pending_same_bar_close_qty_ = 0.0;
15250 pending_relative_exits_.clear();
15251 withdraw_anchored_relative_legs(nullptr, nullptr);
15252 per_entry_exits_.clear();
15253}
15254
15255void PineExecutionAdapter::order(const SourceId& id, bool is_long, double qty,
15256 double limit_price, double stop_price,
15257 const std::string& oca_name, int oca_type) {
15258 if (intraday_loss_orders_blocked()) return;
15259 if (const auto point = detail::callback_point(require_host());
15260 point && cap_placement_denied(point->decision)) {
15261 return;
15262 }
15263 require_cohort_ticket(&id);
15264 reroute_fixed_entries_before_request();
15265 // ab9714be pine_strategy_commands.cpp:2120-2203: strategy.order appends to pending book after earlier script commands
15266 if (!pending_same_bar_commands_.empty()) {
15267 source_batch_mutated_ = true;
15268 flush_pending_same_bar_commands();
15269 }
15270 // ab9714be pine_strategy_commands.cpp:2120-2203: strategy.order commits after preceding pending entries in source order
15271 if (!pending_entries_.empty()) {
15273 }
15274 if (id == "__close__") {
15275 const auto point = detail::callback_point(require_host());
15276 std::vector<native_order::RequestHandle> replaced_close_all;
15277 for (const auto& handle : live_handles_) {
15278 const auto found = placement_.find(handle.incarnation);
15279 if (found != placement_.end()
15280 && found->second.family == PineOrderFamily::CloseAll
15281 && (!point || found->second.placement_script_open_ms
15282 == point->decision.script_bar_open_ms)) {
15283 replaced_close_all.push_back(handle);
15284 }
15285 }
15286 for (const auto& handle : replaced_close_all) {
15287 const auto result = require_host().cancel(handle);
15288 if (result.status != native_order::CancelStatus::Cancelled) continue;
15289 for (auto row : placement_) {
15290 if (row.second.preserved_by_close_all == handle) {
15291 row.second.preserved_by_close_all = {}; invalidate_retirement();
15292 row.second.preserved_close_all_bar = -1;
15293 }
15294 }
15295 retire(handle);
15296 }
15297 }
15298 if (has_live_exit_policy()) for (const auto& handle : live_handles_) {
15299 const auto existing = placement_.find(handle.incarnation);
15300 if (existing != placement_.end()
15301 && existing->second.pooc_global_full_exit_dynamic_qty) {
15302 existing->second.pooc_global_full_exit_dynamic_qty = false;
15303 existing->second.pooc_global_full_exit_tracks_bound_adds = false;
15304 }
15305 }
15306 native_order::Request request;
15307 double risk_coof_forced_price = kNaN;
15308 double coof_market_fill = kNaN;
15309 bool delay_after_default_pair = false;
15310 limit_price = source_level_on_price_grid(
15311 limit_price, staged_.syminfo.mintick, value_field::order_limit_price);
15312 stop_price = source_level_on_price_grid(
15313 stop_price, staged_.syminfo.mintick, value_field::order_stop_price);
15314 const bool default_sized = std::isnan(qty);
15315 const double normalized_qty = default_sized ? qty
15316 : floor_quantity_grid(std::abs(qty), staged_.quantity_grid);
15317 // The CANCEL group is source-gated by its original requested quantity:
15318 // an opposite-side partial fill must not erase its sibling. Resolve that
15319 // shape through host terms so the adapter can retain the source operand.
15320 // Existing explicit non-cancel RAW orders stay on the generic Transact
15321 // path, including native OCA-reduce's working-reservation semantics.
15322 request.intent = (default_sized || oca_type == 1)
15323 ? native_order::OrderIntent{native_order::HostSized{native_order::HostSizedKind::Open,
15324 is_long ? native_order::Side::Long : native_order::Side::Short}}
15325 : native_order::OrderIntent{native_order::Transact{is_long ? normalized_qty : -normalized_qty}};
15326 request.label = id;
15327 double native_limit = limit_price;
15328 double native_stop = stop_price;
15329 if (finite_positive(limit_price) && !finite_positive(stop_price)) {
15330 native_limit = source_trigger_threshold(
15331 limit_price, staged_.syminfo.mintick, is_long, true, value_field::order_limit_price);
15332 } else if (finite_positive(stop_price) && !finite_positive(limit_price)
15333 && !config_.calc_on_order_fills) {
15334 native_stop = source_trigger_threshold(
15335 stop_price, staged_.syminfo.mintick, is_long, false, value_field::order_stop_price);
15336 }
15337 request.trigger = trigger_for(native_limit, native_stop);
15338 if (coof_recalc_active_ && coof_first_open_) {
15339 const auto point = detail::callback_point(require_host());
15340 if (point && finite_positive(limit_price)
15341 && (is_long ? limit_price >= point->price
15342 : limit_price <= point->price)) {
15343 coof_market_fill = source_bar_fill_tick(
15344 point->price, staged_.syminfo.mintick);
15345 }
15346 }
15347 if (std::holds_alternative<native_order::Market>(request.trigger)) {
15348 const auto point = detail::callback_point(require_host());
15349 if (coof_recalc_active_ && !coof_first_open_) {
15350 const double next_waypoint = coof_next_waypoint();
15351 const double current_quote = point ? point->price : kNaN;
15352 coof_market_fill = source_bar_fill_tick(
15353 next_waypoint, staged_.syminfo.mintick)
15354 + (is_long ? 1.0 : -1.0) * config_.slippage
15355 * staged_.syminfo.mintick;
15356#ifdef PINEFORGE_VALUE_INVALID_FIXTURES
15357 coof_market_fill = fixture_adapter_execution_value("slippage.recalc.order", coof_market_fill);
15358#endif
15359 check_slippage_fill_price(coof_market_fill, std::isfinite(next_waypoint));
15360 if (finite_positive(next_waypoint) && finite_positive(current_quote)
15361 && !source_same_point(current_quote, next_waypoint, staged_.syminfo.mintick)) {
15362 const bool falling = next_waypoint < current_quote;
15363 if (is_long) {
15364 request.trigger = falling
15366 : native_order::Trigger{native_order::Stop{next_waypoint}};
15367 } else {
15368 request.trigger = falling
15371 }
15372 }
15373 }
15374 if (coof_recalc_active_ && !coof_first_open_
15375 && risk_.max_intraday_loss > 0.0) {
15376 double target = kNaN;
15377 if (point) {
15378 switch (point->decision.coordinate.path_phase) {
15379 case NativePathPhase::Open: {
15380 const bool high_first = source_path_uses_high_first(coof_script_bar_);
15381 target = high_first ? coof_script_bar_.high : coof_script_bar_.low;
15382 break;
15383 }
15384 case NativePathPhase::High: target = coof_script_bar_.high; break;
15385 case NativePathPhase::Low: target = coof_script_bar_.low; break;
15386 default: break;
15387 }
15388 if (finite_positive(target)
15389 && !source_same_point(point->price, target, staged_.syminfo.mintick)
15390 && risk_coof_direct_script_bar_
15391 != point->decision.script_bar_open_ms) {
15392 risk_coof_forced_price = target;
15393 risk_coof_direct_script_bar_ = point->decision.script_bar_open_ms;
15394 }
15395 }
15396 }
15397 std::vector<std::pair<std::uint64_t, native_order::RequestHandle>> same_bar_defaults;
15398 for (const auto& handle : live_handles_) {
15399 const auto pending = placement_.find(handle.incarnation);
15400 if (pending == placement_.end()) continue;
15401 const auto& row = pending->second;
15402 if (row.opening && row.family == PineOrderFamily::Entry
15403 && !std::isfinite(row.requested_qty)
15404 && (!point || row.placement_script_open_ms
15405 == point->decision.script_bar_open_ms)
15406 && !finite_positive(row.exit_levels.limit)
15407 && !finite_positive(row.exit_levels.stop)) {
15408 same_bar_defaults.push_back({row.source_sequence, handle});
15409 }
15410 }
15411 if (same_bar_defaults.size() >= 2) {
15412 delay_after_default_pair = true;
15413 }
15414 }
15415 request.group = group_for(oca_name, oca_type);
15416 if (oca_type == 1) {
15417 // A Pine RAW cancel group fires only when the source request itself
15418 // completely fills. The generic request has no source requested-size
15419 // operand once an opposite close is bounded to live exposure, so the
15420 // adapter applies that source receipt from on_applied instead.
15421 request.group = native_order::NoGroup{};
15422 }
15423 if (default_sized && oca_type == 2) {
15424 if (auto* member = std::get_if<native_order::Member>(&request.group)) {
15425 // Pine's default-sized RAW sibling is cancelled after an OCA
15426 // reduce member fills; only an explicit quantity consumes the
15427 // group reduction as a residual working amount.
15428 member->effect = native_order::GroupEffect::Cancel;
15429 }
15430 }
15431 PlacementSnapshot snapshot;
15432 snapshot.family = PineOrderFamily::Order; snapshot.source_id = id; snapshot.oca_name = oca_name;
15433 snapshot.oca_type = oca_type;
15434 // strategy.order's source quantity is verbatim (unlike strategy.entry's
15435 // lot-grid floor); retain that literal in the adapter projection even
15436 // when the generic accepted request needs its separately normalized
15437 // executable operand.
15438 snapshot.requested_qty = qty;
15439 snapshot.is_long = is_long;
15440 snapshot.exit_levels.limit = limit_price;
15441 snapshot.exit_levels.stop = stop_price;
15442 snapshot.forced_execution_price = finite_positive(risk_coof_forced_price)
15443 ? risk_coof_forced_price : coof_market_fill;
15444 if (source_command_sequence_ == std::numeric_limits<std::uint64_t>::max()) {
15446 "Pine source command sequence exhausted");
15447 }
15448 snapshot.command_sequence = ++source_command_sequence_;
15449 snapshot.sizing = sizing_snapshot();
15450 if (default_sized && finite_positive(snapshot.sizing.price)
15451 && (config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
15452 || config_.default_qty_type == static_cast<int>(QtyType::CASH))) {
15453 snapshot.sizing.frozen_units = default_sizing_units(snapshot.sizing);
15454 snapshot.sizing.at_fill = config_.calc_on_order_fills && coof_recalc_active_;
15455 }
15456 if (delay_after_default_pair) {
15457 if (broker_open_epoch_ == std::numeric_limits<std::uint64_t>::max())
15459 "source delayed market epoch exhausted");
15460 if (const auto point = detail::callback_point(require_host())) {
15461 snapshot.projection_created_bar = projection_bar_index(point->decision);
15462 snapshot.placement_script_open_ms = point->decision.script_bar_open_ms;
15463 snapshot.placement_sub_open_ms = point->decision.sub_bar_open_ms;
15464 }
15465 snapshot.projection_position_side = static_cast<std::int32_t>(PositionSide::FLAT);
15466 delayed_market_orders_.push_back({std::move(request), std::move(snapshot), id,
15467 broker_open_epoch_ + 1U});
15468 return;
15469 }
15470 const auto accepted = submit_or_replace(std::move(request), std::move(snapshot), true, id);
15471 if (!accepted) return;
15472 const auto entry = placement_.find(accepted->incarnation);
15473 if (entry == placement_.end()) return;
15474 const std::uint64_t entry_sequence = entry->second.command_sequence;
15475 if (has_live_exit_policy()) for (const auto& handle : live_handles_) {
15476 if (handle == *accepted) continue;
15477 const auto existing = placement_.find(handle.incarnation);
15478 if (existing == placement_.end()) continue;
15479 auto& prior = existing->second;
15480 if (!prior.reservation_expansion.capture()
15481 || prior.command_sequence >= entry_sequence) {
15482 continue;
15483 }
15484 prior.pooc_global_full_exit_dynamic_qty = false;
15485 prior.pooc_global_full_exit_tracks_bound_adds = false;
15486 prior.reservation_expansion.close_population(accepted->incarnation);
15487 }
15488}
15489
15491 const NativeExecutionTermsFacts& facts) const {
15492 auto result = resolve_source_terms(facts);
15493 if (PF_RARE(close_call_after_script_bar_ != std::numeric_limits<std::int64_t>::min())) {
15494 const auto snapshot = placement_.find(facts.target.incarnation);
15495 if (snapshot != placement_.end())
15496 apply_frozen_reversal_close(snapshot->second, facts, result);
15497 }
15498 return result;
15499}
15500
15501// A reversal strategy.entry the script placed at a close where a margin call
15502// was then booked after the script keeps the close quantity the script saw
15503// (switch frozen_reversal_close): at its fill it buys that quantity against
15504// the smaller book and the excess opens on the other side, its own open leg
15505// on top when that leg was affordable. Synthetic NYSE:F at slippage 1: a short of
15506// 400 shares that a reversal long of 700 (unaffordable) was placed against
15507// gives up 4 to a follow-up at the close; the next open closes 396 and
15508// opens a long of 4, and an affordable reversal of 50 opens 54 (lab tv tapes
15509// tests/fixtures/margin_schedule_rules frozen/; the analogous rule for a
15510// close_all).
15511void PineExecutionAdapter::apply_frozen_reversal_close(
15513 native_order::ExecutionTerms& result) const {
15514 if (!detail::margin_schedule_switches().frozen_reversal_close
15515 || source.family != PineOrderFamily::Entry || !source.opening
15516 || source.placement_script_open_ms != close_call_after_script_bar_
15517 || !result.units || !std::isfinite(*result.units)
15518 || facts.position.signed_units == 0.0
15519 || (facts.position.signed_units > 0.0) == source.is_long
15520 || (result.shape != native_order::OpeningShape::CloseOpposite
15521 && result.shape != native_order::OpeningShape::ReverseTo)) {
15522 return;
15523 }
15524 const auto placed = static_cast<PositionSide>(source.projection_position_side);
15525 if (placed == PositionSide::FLAT || (placed == PositionSide::LONG) == source.is_long) return;
15526 const double frozen = close_call_after_script_book_;
15527 const double book = std::abs(facts.position.signed_units);
15528 const double guard = std::max(1e-9, frozen * 1e-12);
15529 // Only the book that call shrank: nothing else filled since.
15530 if (!finite_positive(frozen) || !(frozen > book + guard)
15531 || std::abs(frozen - close_call_after_script_units_ - book) > guard) {
15532 return;
15533 }
15534 const double own = result.shape == native_order::OpeningShape::ReverseTo ? *result.units : 0.0;
15535 result.units = frozen + own;
15536 result.shape = native_order::OpeningShape::Transact;
15537}
15538
15539native_order::ExecutionTerms PineExecutionAdapter::resolve_source_terms(
15540 const NativeExecutionTermsFacts& facts) const {
15541 native_order::ExecutionTerms result{facts.default_resolved_price, std::nullopt,
15542 native_order::OpeningShape::Transact};
15543 const auto snapshot = placement_.find(facts.target.incarnation);
15544 if (snapshot == placement_.end()) {
15545 // R5: a kernel-originated liquidation carries no source placement row.
15546 // Its fill price is still TradingView's: the fire price it was rested
15547 // at, on the chart tick ladder, plus the EXIT side's own market
15548 // slippage (ab9714be pine_fills.cpp:1712-1726). A short's checkpoint
15549 // marked on the ladder, so its fire price is rounded there first.
15550 if (facts.definition
15551 && facts.definition->origin == native_order::RequestOrigin::KernelLiquidation) {
15552 double fire = facts.trigger_level ? *facts.trigger_level : facts.raw_price;
15553 if (facts.is_buy) fire = nearest_tick(fire, staged_.syminfo.mintick);
15554 if (finite_positive(fire))
15555 result.resolved_price = source_margin_fill_price(fire, facts.is_buy);
15556 }
15557 return result;
15558 }
15559 const auto& source = snapshot->second;
15560 // The already-armed explicit-zero trail's sibling generic stop (exit():
15561 // "A sibling generic stop preserves the next-open print decision") is the
15562 // same owner TRAIL leg (ab9714be engine_path_resolve.cpp:620-705,
15563 // try_exit_open_gap_fill), so it settles on the zero-offset trail policy.
15564 const bool zero_trail_sibling_stop = source.family == PineOrderFamily::ExitStop
15565 && !price_present(source.exit_levels.stop)
15566 && finite_positive(source.trail_activation_level);
15567 const bool explicit_zero_trail =
15568 (source.family == PineOrderFamily::ExitTrail || zero_trail_sibling_stop)
15569 && std::isfinite(source.exit_levels.trail_offset)
15570 && std::floor(source.exit_levels.trail_offset) == 0.0;
15571 const auto host_state = detail::run_state(require_host());
15572 const bool sampled_one_price_gap = host_state.spec
15573 && !host_state.spec->intrabar.is_none()
15574 && facts.cursor.point.provenance == NativePriceProvenance::ModeledOHLCOpen
15575 && facts.price_kind == native_order::NativeCandidatePriceKind::PointPrice;
15576 const bool trail_limit_one_shot = source.family == PineOrderFamily::ExitTrail
15577 && std::holds_alternative<native_order::Limit>(facts.definition->request.trigger);
15578 const auto* one_shot_rest = trail_limit_one_shot
15579 ? std::get_if<native_order::Limit>(&facts.definition->request.trigger) : nullptr;
15580 const auto* trail_active = std::get_if<native_order::TrailActive>(&facts.trigger_state);
15581 const auto retained_trail_source_price = [&]() -> std::optional<double> {
15582 // ab9714be engine_path_resolve.cpp:494-510: explicit zero trail rides running best and does not offset trigger level
15583 if (!std::isfinite(source.retained_trail_best)
15584 || !std::isfinite(source.exit_levels.trail_offset)
15585 || source.exit_levels.trail_offset < 0.0
15586 || explicit_zero_trail
15587 || !finite_positive(staged_.syminfo.mintick)
15588 || facts.cursor.point.path_phase == NativePathPhase::Open) {
15589 return std::nullopt;
15590 }
15591 double best = source.retained_trail_best;
15592 if (trail_active && std::isfinite(trail_active->best_at_trigger)) {
15593 best = facts.is_buy ? std::min(best, trail_active->best_at_trigger)
15594 : std::max(best, trail_active->best_at_trigger);
15595 }
15596 // An armed trail's running best starts at its activation: the E5
15597 // tapes book activation -/+ offset while the raw extreme stays inside
15598 // the activation's tick cell (synthetic 26.041 -> 26.05, 25.049 -> 25.04),
15599 // whereas the kernel's best starts at the half-tick arm
15600 // (source_trail_arm_level). On the grid the two never differ.
15601 if (std::isfinite(source.trail_activation_level)) {
15602 best = facts.is_buy ? std::min(best, source.trail_activation_level)
15603 : std::max(best, source.trail_activation_level);
15604 }
15605 const double offset = std::floor(source.exit_levels.trail_offset)
15606 * staged_.syminfo.mintick;
15607 const double level = best + (facts.is_buy ? offset : -offset);
15608 const double slipped = level + (facts.is_buy ? 1.0 : -1.0)
15609 * config_.slippage * staged_.syminfo.mintick;
15610 return directional_tick(slipped, staged_.syminfo.mintick, facts.is_buy);
15611 }();
15612 const auto observed_tick_trail_price = [&]() -> std::optional<double> {
15614 || facts.cursor.point.provenance != NativePriceProvenance::ObservedPrint) {
15615 return std::nullopt;
15616 }
15617 return facts.default_resolved_price;
15618 }();
15619 const bool placement_reached_trail_activation =
15620 std::isfinite(source.sizing.price) && std::isfinite(source.trail_activation_level)
15621 && source_trail_reached_at(source.sizing.price, source.trail_activation_level,
15622 staged_.syminfo.mintick, facts.is_buy);
15623 const bool zero_trail_first_activation = explicit_zero_trail && trail_active
15624 && !placement_reached_trail_activation
15625 && std::isfinite(source.trail_activation_level)
15626 && ((facts.is_buy
15627 && trail_active->best_at_trigger <= source.trail_activation_level
15628 + staged_.syminfo.mintick * 1e-6)
15629 || (!facts.is_buy
15630 && trail_active->best_at_trigger >= source.trail_activation_level
15631 - staged_.syminfo.mintick * 1e-6));
15632 const auto zero_trail_source_price = [&]() -> std::optional<double> {
15633 if (!explicit_zero_trail || sampled_one_price_gap || !facts.trigger_level
15634 || !policy_script_bar_valid_ || !std::isfinite(source.trail_activation_level)) {
15635 return std::nullopt;
15636 }
15637 const double open = policy_script_bar_.open;
15638 const double placement = source.sizing.price;
15639 const double activation = source.trail_activation_level;
15640 const bool reached_at_placement = source_trail_reached_at(
15641 placement, activation, staged_.syminfo.mintick, facts.is_buy);
15642 const bool open_beyond = facts.is_buy
15643 ? open <= activation : open >= activation;
15644 const bool high_first = source_path_uses_high_first(policy_script_bar_);
15645 const bool adverse_first = facts.is_buy ? high_first : !high_first;
15646 const bool same_open = std::isfinite(placement)
15647 && std::abs(open - placement) <= staged_.syminfo.mintick * 0.5;
15648 if (reached_at_placement && same_open)
15649 return nearest_tick(open, staged_.syminfo.mintick);
15650 if (!reached_at_placement && !open_beyond)
15651 return nearest_tick(activation, staged_.syminfo.mintick);
15652 if (!reached_at_placement && open_beyond && adverse_first)
15653 return directional_tick(open, staged_.syminfo.mintick, facts.is_buy);
15654 return std::nullopt;
15655 };
15656 const auto zero_trail_policy_price = [&]() -> std::optional<double> {
15657 if (!explicit_zero_trail || !policy_script_bar_valid_
15658 || !std::isfinite(source.trail_activation_level)
15659 || !finite_positive(staged_.syminfo.mintick)) {
15660 return std::nullopt;
15661 }
15662 const double tick = staged_.syminfo.mintick;
15663 const bool long_side = !facts.is_buy;
15664 const double open = policy_script_bar_.open;
15665 const double activation = source.trail_activation_level;
15666 const double placement = source.sizing.price;
15667 const auto print = [&](double value) {
15668 return std::floor(value / tick + 0.5) * tick;
15669 };
15670 const auto level = [&](double value) {
15671 return directional_tick(value, tick, facts.is_buy);
15672 };
15673 const bool placement_armed = std::isfinite(placement)
15674 && (long_side ? nearest_tick(placement, tick) >= activation
15675 : nearest_tick(placement, tick) <= activation);
15676 const bool open_reaches = long_side
15677 ? open >= activation : open <= activation;
15678 const bool open_favorable = std::isfinite(placement)
15679 && (long_side ? open > placement : open < placement);
15680 const auto preopen = trail_state_at_open_.find(facts.target.incarnation);
15681 if (preopen != trail_state_at_open_.end() && preopen->second.activated
15682 && std::isfinite(preopen->second.current_level)) {
15683 const bool adverse_gap = long_side
15684 ? open <= preopen->second.current_level
15685 : open >= preopen->second.current_level;
15686 if (adverse_gap)
15687 return directional_tick(open, tick, facts.is_buy);
15688 }
15689 const bool high_first = source_path_uses_high_first(policy_script_bar_);
15690 const bool favorable_first = long_side ? high_first : !high_first;
15691 const double open_print = source_bar_fill_tick(open, tick);
15692 // A placement close that reached the activation only on its tick
15693 // carries the activation as its best: TradingView's starts there.
15694 const double placement_best = placement_armed
15695 ? (long_side ? std::max(placement, activation) : std::min(placement, activation))
15696 : placement;
15697 const double carried_best = preopen != trail_state_at_open_.end()
15698 && preopen->second.activated
15699 && std::isfinite(preopen->second.best_price)
15700 ? preopen->second.best_price : placement_best;
15701 const bool carried_armed = (preopen != trail_state_at_open_.end()
15702 && preopen->second.activated) || placement_armed;
15703 if (carried_armed && std::isfinite(carried_best)) {
15704 const bool adverse_gap = long_side ? open <= carried_best
15705 : open >= carried_best;
15706 if (adverse_gap) return open_print;
15707 }
15708 const bool zero_offset_open_arms = carried_armed
15709 ? (long_side ? open > carried_best : open < carried_best)
15710 : open_reaches;
15711 const bool print_at_open_level = zero_offset_open_arms
15712 && (long_side ? open_print <= open : open_print >= open);
15713 if (print_at_open_level)
15714 return directional_tick(open, tick, facts.is_buy);
15715 if (zero_offset_open_arms) {
15716 if (!favorable_first) return level(open);
15717 return level(long_side ? policy_script_bar_.high
15718 : policy_script_bar_.low);
15719 }
15720 bool armed = placement_armed;
15721 bool armed_from_open = false;
15722 double best = placement;
15723 if (!std::isfinite(best)) best = open;
15724 if (!armed && open_reaches && open_favorable) {
15725 armed = true;
15726 armed_from_open = true;
15727 best = open;
15728 }
15729 if (armed) {
15730 if (!armed_from_open && long_side && open <= best) return print(open);
15731 if (!armed_from_open && !long_side && open >= best) return print(open);
15732 if (armed_from_open || open_favorable) {
15733 best = open;
15734 if (print(open) == level(open)) return print(open);
15735 }
15736 }
15737 double path[4];
15738 path[0] = open;
15739 if (high_first) {
15740 path[1] = policy_script_bar_.high;
15741 path[2] = policy_script_bar_.low;
15742 } else {
15743 path[1] = policy_script_bar_.low;
15744 path[2] = policy_script_bar_.high;
15745 }
15746 path[3] = policy_script_bar_.close;
15747 for (int i = 1; i < 4; ++i) {
15748 const double from = path[i - 1];
15749 const double to = path[i];
15750 if (!armed) {
15751 const bool reached = long_side
15752 ? (to >= activation && to > from)
15753 : (to <= activation && to < from);
15754 if (reached) {
15755 return one_shot_rest
15756 ? source_one_shot_reach_tick(one_shot_rest->price, tick, facts.is_buy)
15757 : level(activation);
15758 }
15759 continue;
15760 }
15761 const bool favorable = long_side ? to > best : to < best;
15762 if (favorable) {
15763 best = to;
15764 continue;
15765 }
15766 const double stop = level(best);
15767 const bool crossed = long_side
15768 ? (to <= stop && from > stop)
15769 : (to >= stop && from < stop);
15770 if (crossed) return stop;
15771 }
15772 if (trail_active && std::isfinite(trail_active->best_at_trigger))
15773 return level(trail_active->best_at_trigger);
15774 return std::nullopt;
15775 };
15776 const auto& trigger = facts.definition->request.trigger;
15777 const bool limit_fill = std::holds_alternative<native_order::Limit>(trigger)
15778 || std::holds_alternative<native_order::StopLimit>(trigger);
15779 const auto owner_tick_fill = [&](double price) {
15780 // ab9714be engine.hpp:1207-1210 + 1264-1266: a fill at a raw bar or
15781 // waypoint print books floor(p / mintick + 0.5) * mintick. Keep that
15782 // binary64 form unless it lies past the immutable limit the generic
15783 // kernel checks (consume_matched_request's InvalidTerms refusal); there
15784 // the decimal grid form, since source_bar_fill_tick returns an on-grid
15785 // n * mintick unchanged.
15786 const double owner_form = nearest_tick(price, staged_.syminfo.mintick);
15787 std::optional<double> level;
15788 if (const auto* limit = std::get_if<native_order::Limit>(&trigger)) {
15789 level = limit->price;
15790 } else if (const auto* stop_limit = std::get_if<native_order::StopLimit>(&trigger)) {
15791 level = stop_limit->limit;
15792 }
15793 const bool within = !level
15794 || (facts.is_buy ? owner_form <= *level : owner_form >= *level);
15795 return within ? owner_form : source_decimal_tick(price, staged_.syminfo.mintick);
15796 };
15797 const auto source_forced_fill = [&](double forced) {
15798 return nearest_tick(forced, staged_.syminfo.mintick) == forced
15799 ? owner_tick_fill(forced)
15800 : source_bar_fill_tick(forced, staged_.syminfo.mintick);
15801 };
15802 const bool non_open = facts.cursor.point.path_phase != NativePathPhase::Open;
15803 const bool source_gap_point = !non_open
15804 || facts.cursor.point.provenance == NativePriceProvenance::ObservedPrint;
15805 const auto source_stop_fill = [&]() {
15806 const double source_level = source.family == PineOrderFamily::ExitTrail
15807 ? source.exit_levels.trail_price : source.exit_levels.stop;
15808 const double level = finite_positive(source_level) ? source_level
15809 : (facts.trigger_level ? *facts.trigger_level : facts.default_resolved_price);
15810 const double slipped = level + (facts.is_buy ? 1.0 : -1.0)
15811 * config_.slippage * staged_.syminfo.mintick;
15812 return directional_tick(slipped, staged_.syminfo.mintick, facts.is_buy);
15813 };
15814 const auto source_bar_fill = [&]() {
15815 // ab9714be engine.hpp:1160-1180 and pine_policy_members.cpp:45-53:
15816 // a raw bar print is rounded half-up first; slippage then rides on
15817 // that grid price and the directional projection is an identity.
15818 const double rounded = source_bar_fill_tick(
15819 facts.raw_price, staged_.syminfo.mintick);
15820 const double slipped = rounded + (facts.is_buy ? 1.0 : -1.0)
15821 * config_.slippage * staged_.syminfo.mintick;
15822 return directional_tick(slipped, staged_.syminfo.mintick, facts.is_buy);
15823 };
15824 // A named stop whose entry filled at this very point is matched from that
15825 // fill on (the kernel's cause floor), and the kernel reaches one already
15826 // through there at the fill's own print. TradingView fills it at once at
15827 // that print with the stop's slippage, never at a level the bar crossed
15828 // before the entry filled (TradingView tapes: tests/fixtures/global_exit_children
15829 // eth-member and eth-fresh, a short limit entry whose break-even stop the
15830 // bar rose through before the entry filled). Scoped to the pinned shape:
15831 // no calc_on_order_fills, no intrabar feed.
15832 const auto newborn_stop_through = [&]() -> std::optional<double> {
15833 if (source.family != PineOrderFamily::ExitStop || source.from_entry.empty()
15834 || config_.calc_on_order_fills
15835 || facts.price_kind != native_order::NativeCandidatePriceKind::PointPrice
15836 || facts.cursor.point.provenance != NativePriceProvenance::Confirmed
15837 || facts.cursor.point.path_phase == NativePathPhase::Open
15838 || facts.cursor.point.path_phase == NativePathPhase::None
15839 || !host_state.spec || !host_state.spec->intrabar.is_none()) {
15840 return std::nullopt;
15841 }
15842 const auto* pine = pine_view_of(&require_host());
15843 if (!pine) return std::nullopt;
15844 const PyramidEntry* newest = nullptr;
15845 for (const auto& lot : pine->pyramid_entries_) {
15846 if (lot.entry_id == source.from_entry
15847 && (!newest || lot.entry_incarnation > newest->entry_incarnation)) {
15848 newest = &lot;
15849 }
15850 }
15851 if (!newest || newest->entry_bar_index != facts.cursor.point.interval_index)
15852 return std::nullopt;
15853 // Already through at that point: a buy stop at or below it, a sell
15854 // stop at or above it.
15855 const double stop = source.exit_levels.stop;
15856 if (!finite_positive(stop) || (facts.is_buy ? stop > facts.raw_price : stop < facts.raw_price))
15857 return std::nullopt;
15858 // The entry is a limit order that filled here: the point is its
15859 // trigger threshold, half a tick from the level it booked, and that
15860 // booked fill is the print the stop fills at.
15861 const auto opening = placement_.find(newest->entry_incarnation);
15862 if (opening == placement_.end() || opening->second.family != PineOrderFamily::Entry
15863 || !finite_positive(opening->second.exit_levels.limit)
15864 || finite_positive(opening->second.exit_levels.stop)
15865 || std::abs(newest->price - facts.raw_price)
15866 > 0.5 * staged_.syminfo.mintick + 1e-9 * std::abs(facts.raw_price)) {
15867 return std::nullopt;
15868 }
15869 return newest->price;
15870 };
15871 const auto source_stop_resolved = [&]() {
15872 // ab9714be pine_fills.cpp:8002-8018: a realtime print gaps to the
15873 // observed price; an opening print already through the stop books
15874 // the open; otherwise the fill is the stop level (not the open, and
15875 // not a same-pass close's fill price).
15876 if (facts.cursor.point.provenance == NativePriceProvenance::ObservedPrint)
15877 return source_bar_fill();
15878 if (const auto print = newborn_stop_through()) {
15879 const double slipped = *print + (facts.is_buy ? 1.0 : -1.0)
15880 * config_.slippage * staged_.syminfo.mintick;
15881 return directional_tick(slipped, staged_.syminfo.mintick, facts.is_buy);
15882 }
15883 const double source_level = source.family == PineOrderFamily::ExitTrail
15884 ? source.exit_levels.trail_price : source.exit_levels.stop;
15885 const double level = finite_positive(source_level) ? source_level
15886 : (facts.trigger_level ? *facts.trigger_level : facts.default_resolved_price);
15887 // ab9714be pine_scheduler.cpp:911-916 keeps current_bar_.open as the
15888 // script-bar open while magnifier samples only update H/L/C, so
15889 // try_exit_open_gap_fill (engine_path_resolve.cpp:905-927) tests that
15890 // script open. Synthesized/distribution samples arrive as one-price
15891 // opens; a later sample through the stop is a path cross at the stop
15892 // level, not a fresh gap at the sample quote.
15893 double open_px = facts.raw_price;
15894 if (policy_script_bar_valid_
15895 && facts.cursor.point.path_phase == NativePathPhase::Open
15896 && facts.cursor.point.provenance == NativePriceProvenance::ModeledOHLCOpen
15897 && host_state.spec) {
15898 const auto* synthesized = host_state.spec->intrabar.synthesized_path();
15899 const auto* lower = host_state.spec->intrabar.lower();
15900 const bool one_price = synthesized != nullptr
15901 || (lower && lower->sample_eligibility
15903 if (one_price) open_px = policy_script_bar_.open;
15904 }
15905 // ab9714be pine_fills.cpp:4136-4240 + 7795-7800: a stop armed with its
15906 // pending MARKET parent and already breached at the parent's fill
15907 // open scratches at bar_fill_price(open). The leg becomes live only
15908 // after that opening fill, so the driver first presents it on the
15909 // next path segment, crossed at the segment's start: the script-bar
15910 // open itself. That is the same opening gap, not a path cross.
15911 const bool armed_after_open_fill = non_open
15913 && policy_script_bar_valid_
15914 && !config_.process_orders_on_close && !config_.calc_on_order_fills
15915 && host_state.spec && host_state.spec->intrabar.is_none()
15916 && facts.cursor.point.provenance == NativePriceProvenance::Confirmed
15917 && facts.raw_price == policy_script_bar_.open;
15918 if (armed_after_open_fill) open_px = policy_script_bar_.open;
15919 const bool open_gapped = (!non_open || armed_after_open_fill)
15920 && std::isfinite(level)
15921 && (facts.is_buy ? open_px >= level : open_px <= level);
15922 // The pre-open margin slice rests at the raw adverse print
15923 // (schedule_preopen_margin_slice) but is TradingView's margin call, a
15924 // MARKET execution at that print: the print's nearest tick, then the
15925 // exit side's slippage (source_margin_fill_price), not a stop level's
15926 // adverse tick -- synthetic 25.005 books 25.01 (lab tv tapes pm-m10-f-k4-1007
15927 // and -gapup-0501, the intrabar margin correction). A stop crossed at its own
15928 // off-grid LEVEL keeps the directional rule (pm2-round-f, -es).
15929 if (!open_gapped) {
15930 return source.family == PineOrderFamily::Margin
15931 ? source_margin_fill_price(level, facts.is_buy)
15932 : source_stop_fill();
15933 }
15934 // ab9714be try_exit_open_gap_fill books bar.open (the script-bar
15935 // open), even when the matching sample is a later one-price tick.
15936 const double rounded = source_bar_fill_tick(
15937 open_px, staged_.syminfo.mintick);
15938 const double slipped = rounded + (facts.is_buy ? 1.0 : -1.0)
15939 * config_.slippage * staged_.syminfo.mintick;
15940 return directional_tick(slipped, staged_.syminfo.mintick, facts.is_buy);
15941 };
15942 const auto source_limit_fill = [&]() {
15943 // ab9714be pine_fills.cpp:7733-8072 + pine_policy_members.cpp:55-58:
15944 // an open gap receives the raw open with nearest-tick rounding;
15945 // otherwise a LIMIT receives its level, snapped limit-or-better and
15946 // never slipped.
15947 if (source.family == PineOrderFamily::ExitTrail && facts.trigger_level
15948 && non_open) {
15949 const double ticked = directional_tick(*facts.trigger_level, staged_.syminfo.mintick,
15950 !facts.is_buy);
15951 // ab9714be pine_fills.cpp:7970-7988: limit fill is unslipped limit-or-better clamped to trigger to preserve limit invariant
15952 return facts.is_buy ? std::min(ticked, *facts.trigger_level)
15953 : std::max(ticked, *facts.trigger_level);
15954 }
15955 const bool deferred_open_gap = source.defer_until_post_parent_calculation
15956 && facts.cursor.point.provenance == NativePriceProvenance::Confirmed;
15957 // ab9714be pine_fills.cpp:4136-4240 + 7795-7800: the limit leg of a
15958 // bracket armed with its pending MARKET parent and already marketable
15959 // at the parent's fill open scratches at bar_fill_price(open), exactly
15960 // like the stop leg (source_stop_resolved). The driver first presents
15961 // it on the next path segment, crossed at that segment's start: the
15962 // script-bar open itself.
15963 const bool limit_armed_after_open_fill = non_open
15965 && policy_script_bar_valid_
15966 && !config_.process_orders_on_close && !config_.calc_on_order_fills
15967 && host_state.spec && host_state.spec->intrabar.is_none()
15968 && facts.cursor.point.provenance == NativePriceProvenance::Confirmed
15969 && facts.raw_price == policy_script_bar_.open;
15970 if (!non_open || deferred_open_gap || limit_armed_after_open_fill) {
15971 const bool raw_oca_reduce = source.family == PineOrderFamily::Order
15972 && source.oca_type == 2;
15973 if (raw_oca_reduce) {
15974 return nearest_tick(facts.raw_price, staged_.syminfo.mintick);
15975 }
15976 // ab9714be pine_fills.cpp:7943-7945: an exit gapped at the open
15977 // books bar_fill_price(open).
15978 if (source.family == PineOrderFamily::ExitLimit) {
15979 return owner_tick_fill(facts.raw_price);
15980 }
15981 return source_bar_fill_tick(facts.raw_price, staged_.syminfo.mintick);
15982 }
15983 // ab9714be engine_path_resolve.cpp:365-367: stop-limit fills at unslipped stop activation price when marketable against limit
15984 // A pure limit entry armed at its leg's extreme (entry(): a limit
15985 // resting from the extreme) is no stop-limit: it books its level.
15986 const bool armed_pure_limit_entry = source.family == PineOrderFamily::Entry
15987 && !finite_positive(source.exit_levels.stop);
15988 if (std::holds_alternative<native_order::StopLimit>(trigger) && !armed_pure_limit_entry) {
15989 const double projected = directional_tick(facts.raw_price, staged_.syminfo.mintick,
15990 !facts.is_buy);
15991 // The repair the limit below makes: this projection can sit one
15992 // binary64 ULP past the immutable limit (a buy limit at 99.99 on
15993 // a 0.01 grid projects to 9999 * 0.01 = 99.990000000000009),
15994 // which the generic kernel refuses. Only there, and only for a
15995 // print on the chart grid, the fill books that print's grid
15996 // spelling; an admitted projection is kept bit for bit.
15997 const double limit = std::get<native_order::StopLimit>(trigger).limit;
15998 if (facts.is_buy ? projected > limit : projected < limit) {
15999 const double print_on_grid = source_level_on_price_grid(
16000 facts.raw_price, staged_.syminfo.mintick);
16001 if (source_bar_fill_tick(print_on_grid, staged_.syminfo.mintick)
16002 == print_on_grid) {
16003 return print_on_grid;
16004 }
16005 }
16006 return projected;
16007 }
16008 const double level = finite_positive(source.exit_levels.limit)
16009 ? source.exit_levels.limit
16010 : (facts.trigger_level ? *facts.trigger_level : facts.raw_price);
16011 // Preserve a source level that is on the chart grid (including a
16012 // computed level only ULPs away from it). `directional_tick` can
16013 // return a value one binary64 ULP on the wrong side of that level
16014 // (2409.49 -> 2409.4900000000002 for a buy limit), which the generic
16015 // kernel then correctly rejects against its immutable limit.
16016 const double projected = directional_tick(
16017 level, staged_.syminfo.mintick, !facts.is_buy);
16018 const double level_on_grid = source_level_on_price_grid(
16019 level, staged_.syminfo.mintick);
16020 const bool grid_level = source_bar_fill_tick(
16021 level_on_grid, staged_.syminfo.mintick) == level_on_grid;
16022 const bool wrong_side = facts.is_buy
16023 ? projected > level
16024 : projected < level;
16025 // ab9714be pine_policy_members.cpp:53-58 books that projection as is
16026 // (a buy limit at 1.18054 fills 1.1805400000000001). Substitute the
16027 // grid level only where the native limit itself refuses it; a
16028 // half-tick-shifted exit threshold admits the owner's spelling.
16029 const auto* native_limit = std::get_if<native_order::Limit>(&trigger);
16030 const bool native_admits = native_limit
16031 && (facts.is_buy ? projected <= native_limit->price
16032 : projected >= native_limit->price);
16033 if (grid_level && wrong_side && !native_admits) {
16034 return level_on_grid;
16035 }
16036 return projected;
16037 };
16038 const auto source_trail_one_shot_fill = [&]() {
16039 // ab9714be pine_fills.cpp:7936-7958: an omitted-offset trail is a
16040 // stop-style activation print. When the next bar opens through the
16041 // raw activation, Pine projects that print directionally; it is not a
16042 // nearest-tick LIMIT gap. Explicit offsets and non-open crossings
16043 // keep the L6b limit-or-better projection.
16044 const double tick = staged_.syminfo.mintick;
16045 if (!std::isfinite(source.exit_levels.trail_offset)
16046 && finite_positive(tick) && finite_positive(source.trail_activation_level)) {
16047 // The native arm threshold sits half a tick inside the source
16048 // one-shot level (exit()). ab9714be engine_path_resolve.cpp:
16049 // 667-705 gap-fills only a RAW open at or past the activation;
16050 // an open short of it is reached on the tick path and books the
16051 // activation itself (engine_path_resolve.cpp:980-987).
16052 const double slipped = source.trail_activation_level
16053 + (facts.is_buy ? 1.0 : -1.0) * config_.slippage * tick;
16054 double level = directional_tick(slipped, tick, facts.is_buy);
16055 if (!facts.is_buy) level = source_level_on_price_grid(level, tick);
16056 const bool open_through = facts.is_buy
16057 ? facts.raw_price <= level : facts.raw_price >= level;
16058 if (facts.cursor.point.path_phase == NativePathPhase::Open && open_through) {
16059 return directional_tick(
16060 facts.default_resolved_price, tick, facts.is_buy);
16061 }
16062 const double ticked = directional_tick(level, tick, !facts.is_buy);
16063 return facts.is_buy ? std::min(ticked, level) : std::max(ticked, level);
16064 }
16065 if (!std::isfinite(source.exit_levels.trail_offset)
16066 && facts.cursor.point.path_phase == NativePathPhase::Open) {
16067 return directional_tick(
16068 facts.default_resolved_price, staged_.syminfo.mintick,
16069 facts.is_buy);
16070 }
16071 return source_limit_fill();
16072 };
16073 // Explicit native intents already carry their canonical trigger/fill
16074 // price. Limits retain their immutable generic value. The generic consumer
16075 // has already applied the one market slippage step; source projection only
16076 // rounds that resulting quote to the ordinary chart tick.
16077 // A core-sized opening is a host-resolved shape too: the core owns the
16078 // QUANTITY, the source still owns the fill price and every admission
16079 // policy below. Only an intent that also carries its own price -- a
16080 // literal transaction, a reversal target -- takes the explicit path.
16081 const bool core_sized = std::holds_alternative<native_order::Sized>(
16082 facts.definition->request.intent);
16083 if (!core_sized
16084 && !std::holds_alternative<native_order::HostSized>(facts.definition->request.intent)) {
16085 if (std::holds_alternative<native_order::Market>(trigger)) {
16086 result.resolved_price = source_bar_fill();
16087 } else if (limit_fill) {
16088 result.resolved_price = source_limit_fill();
16089 // ab9714be pine_fills.cpp:8004-8020: Order-family stop triggers resolve fill price via source stop resolution
16090 } else if ((source.family == PineOrderFamily::Entry
16091 || source.family == PineOrderFamily::Order)
16092 && std::holds_alternative<native_order::Stop>(trigger)
16093 && facts.trigger_level) {
16094 // pine_stream.cpp:278-303 at ab9714be presents each realtime trade
16095 // as a one-price broker point. A stop crossed by that print gaps
16096 // to the observed price; it is not interpolated back to its level.
16097 result.resolved_price = source_stop_resolved();
16098 }
16099 if (finite_positive(source.forced_execution_price)) {
16100 // ab9714be pine_fills.cpp:1712-1726: margin slices use the exact fire
16101 // price (W34a); every other forced fill goes through the chained-fill
16102 // tick-grid helper (W35a).
16104 ? source.forced_execution_price
16105 : source_forced_fill(source.forced_execution_price);
16106 }
16107 if (source.family == PineOrderFamily::ExitLimit && facts.trigger_level
16108 && facts.cursor.point.path_phase != NativePathPhase::Open) {
16109 result.resolved_price = directional_tick(
16110 result.resolved_price, staged_.syminfo.mintick, !facts.is_buy);
16111 const double constraint = finite_positive(source.exit_levels.limit)
16112 ? source.exit_levels.limit : *facts.trigger_level;
16113 result.resolved_price = facts.is_buy
16114 ? std::min(result.resolved_price, constraint)
16115 : std::max(result.resolved_price, constraint);
16116 } else if ((source.family == PineOrderFamily::ExitStop
16118 && facts.trigger_level
16119 && facts.cursor.point.path_phase != NativePathPhase::Open) {
16120 result.resolved_price = directional_tick(
16121 result.resolved_price, staged_.syminfo.mintick, facts.is_buy);
16122 }
16123 return result;
16124 }
16125 const bool live_direction_gate = source.family == PineOrderFamily::Entry
16126 && ((risk_.direction > 0 && !source.is_long)
16127 || (risk_.direction < 0 && source.is_long));
16128 if (live_direction_gate) {
16129 const bool opposite = facts.position.signed_units != 0.0
16130 && ((facts.position.signed_units > 0.0) != source.is_long);
16131 if (opposite) {
16132 result.units = facts.opposite_book_units;
16133 result.shape = native_order::OpeningShape::CloseOpposite;
16134 } else {
16135 // `allow_entry_in` rejects a flat/same-side forbidden instruction;
16136 // unlike an opposite fill it never manufactures a close at the
16137 // command boundary.
16138 result.units = 0.0;
16139 }
16140 return result;
16141 }
16142 const bool market_like = std::holds_alternative<native_order::Market>(trigger);
16143 // NativeRunSpec carries the generic slippage ticks, so its candidate
16144 // default is already the one-slippage source fill. The adapter only owns
16145 // the frozen source sizing basis; applying it again here would double-slip
16146 // a market order after the on-tick calculation.
16147 if (market_like) {
16148 result.resolved_price = source_bar_fill();
16149 } else if (limit_fill) {
16150 result.resolved_price = source_limit_fill();
16151 }
16152 if (finite_positive(source.forced_execution_price)) {
16153 result.resolved_price = source_forced_fill(source.forced_execution_price);
16154 }
16155 // Source stop/trail exits crossed inside a modeled path settle at their
16156 // armed level, whereas an open gap retains the presented open quote. The
16157 // generic driver deliberately exposes both facts; selecting this source
16158 // policy here preserves the non-gap relative-parent lifecycle.
16159 if ((source.family == PineOrderFamily::ExitLimit
16162 && facts.trigger_level
16163 && facts.price_kind == native_order::NativeCandidatePriceKind::TriggerLevel) {
16165 && facts.cursor.point.path_phase == NativePathPhase::Open) {
16166 result.resolved_price = source_bar_fill();
16167 } else if (observed_tick_trail_price) {
16168 result.resolved_price = *observed_tick_trail_price;
16169 } else if (retained_trail_source_price) {
16170 result.resolved_price = *retained_trail_source_price;
16171 } else if (const auto source_price = zero_trail_policy_price()) {
16172 result.resolved_price = *source_price;
16173 } else if (const auto source_price = zero_trail_source_price()) {
16174 result.resolved_price = *source_price;
16175 } else if (trail_limit_one_shot) {
16176 result.resolved_price = source_trail_one_shot_fill();
16177 } else if (explicit_zero_trail) {
16178 if (zero_trail_first_activation) {
16179 result.resolved_price = directional_tick(
16180 source.trail_activation_level, staged_.syminfo.mintick, facts.is_buy);
16181 } else {
16182 const double best = trail_active
16183 ? trail_active->best_at_trigger : facts.default_resolved_price;
16184 result.resolved_price = directional_tick(
16185 best, staged_.syminfo.mintick, facts.is_buy);
16186 }
16187 } else if ((source.family == PineOrderFamily::ExitStop || source.family == PineOrderFamily::Order)
16188 && facts.cursor.point.path_phase == NativePathPhase::Open) {
16189 result.resolved_price = source_bar_fill();
16190 } else if (source.family == PineOrderFamily::ExitLimit
16191 || (source.family == PineOrderFamily::Order && finite_positive(source.exit_levels.limit))) {
16192 result.resolved_price = source_limit_fill();
16193 } else if (source.family == PineOrderFamily::ExitTrail) {
16194 result.resolved_price = directional_tick(
16195 facts.default_resolved_price, staged_.syminfo.mintick, facts.is_buy);
16196 } else if ((source.family == PineOrderFamily::ExitStop || source.family == PineOrderFamily::Order)
16197 && std::isfinite(source.exit_levels.stop)) {
16198 result.resolved_price = directional_tick(
16199 source.exit_levels.stop, staged_.syminfo.mintick, facts.is_buy);
16200 } else {
16201 result.resolved_price = directional_tick(
16202 *facts.trigger_level, staged_.syminfo.mintick, facts.is_buy);
16203 }
16204 } else if (observed_tick_trail_price) {
16205 result.resolved_price = *observed_tick_trail_price;
16206 } else if (retained_trail_source_price) {
16207 result.resolved_price = *retained_trail_source_price;
16208 } else if (const auto source_price = zero_trail_policy_price()) {
16209 result.resolved_price = *source_price;
16210 } else if (const auto source_price = zero_trail_source_price()) {
16211 result.resolved_price = *source_price;
16212 } else if (trail_limit_one_shot && facts.trigger_level && !sampled_one_price_gap) {
16213 result.resolved_price = source_trail_one_shot_fill();
16214 } else if (explicit_zero_trail && facts.trigger_level && !sampled_one_price_gap) {
16215 // Native's positive sentinel offset keeps the generic trail alive;
16216 // source settlement prints the carried raw best on the directional
16217 // chart grid. A first activation is the source activation level.
16218 result.resolved_price = zero_trail_first_activation
16219 ? directional_tick(source.trail_activation_level, staged_.syminfo.mintick, facts.is_buy)
16220 : directional_tick(trail_active ? trail_active->best_at_trigger
16221 : facts.default_resolved_price,
16222 staged_.syminfo.mintick, facts.is_buy);
16223 } else if (source.family == PineOrderFamily::ExitStop && facts.trigger_level
16224 && facts.price_kind == native_order::NativeCandidatePriceKind::PointPrice
16225 && (facts.cursor.point.path_phase == NativePathPhase::Open
16227 && facts.cursor.point.provenance
16228 == NativePriceProvenance::Confirmed))) {
16229 // A resting stop crossed by an adverse opening gap books the raw
16230 // opening print, then applies the ordinary nearest chart-tick print
16231 // projection (distinct from a non-gap trigger-level fill).
16232 result.resolved_price = nearest_tick(
16233 facts.default_resolved_price, staged_.syminfo.mintick);
16234 }
16235 if ((source.family == PineOrderFamily::ExitLimit
16236 || (source.family == PineOrderFamily::Order && finite_positive(source.exit_levels.limit)))
16237 && facts.trigger_level && facts.cursor.point.path_phase != NativePathPhase::Open) {
16238 // ab9714be:pine_policy_members.cpp:53-58. A computed LIMIT close is
16239 // limit-or-better: buys floor and sells ceil to the price grid. The
16240 // generic trigger level remains raw for reachability; only the booked
16241 // source fill receives this directional limit snap.
16242 if (finite_positive(source.exit_levels.limit)
16243 && !finite_positive(source.forced_execution_price)) {
16244 result.resolved_price = source_limit_fill();
16245 } else {
16246 result.resolved_price = directional_tick(
16247 result.resolved_price, staged_.syminfo.mintick, !facts.is_buy);
16248 const double constraint = finite_positive(source.exit_levels.limit)
16249 ? source.exit_levels.limit : *facts.trigger_level;
16250 result.resolved_price = facts.is_buy
16251 ? std::min(result.resolved_price, constraint)
16252 : std::max(result.resolved_price, constraint);
16253 }
16254 }
16256 && std::isfinite(source.exit_levels.trail_offset)
16257 && std::floor(source.exit_levels.trail_offset) == 0.0
16258 && policy_script_bar_valid_
16259 && facts.cursor.point.path_phase != NativePathPhase::Open) {
16260 const bool closing_long = facts.position.signed_units > 0.0;
16261 double activation = source.exit_levels.trail_price;
16262 if (!finite_positive(activation)
16263 && std::isfinite(source.exit_levels.trail_points)
16264 && finite_positive(staged_.syminfo.mintick)) {
16265 const double ticks = source_trail_points_ticks(
16266 source.exit_levels.trail_points, staged_.syminfo.mintick);
16267 activation = require_host().position_avg_price()
16268 + (closing_long ? 1.0 : -1.0) * ticks * staged_.syminfo.mintick;
16269 activation = directional_tick(
16270 activation, staged_.syminfo.mintick, closing_long);
16271 }
16272 const bool armed_inside_this_bar = closing_long
16273 ? policy_script_bar_.open < activation
16274 : policy_script_bar_.open > activation;
16275 if (finite_positive(activation) && armed_inside_this_bar) {
16276 // ab9714be:test_fills_edge.cpp:827-858 and the zero-offset trail
16277 // owner: an activation first reached inside a path is a one-shot
16278 // fill at that activation. A favourable opening gap keeps the
16279 // generic trail ride and is deliberately excluded here. A resting
16280 // one-shot books the tick its limit stands for: a sub-tick
16281 // activation rounds away from the position, not toward it.
16282 result.resolved_price = one_shot_rest
16283 ? source_one_shot_reach_tick(
16284 one_shot_rest->price, staged_.syminfo.mintick, facts.is_buy)
16285 : directional_tick(activation, staged_.syminfo.mintick, facts.is_buy);
16286 }
16287 }
16288 if ((std::holds_alternative<native_order::Stop>(trigger)
16289 || std::holds_alternative<native_order::Trail>(trigger)
16290 || source.family == PineOrderFamily::Margin)
16291 && facts.trigger_level && !explicit_zero_trail && !trail_limit_one_shot
16293 && facts.cursor.point.provenance == NativePriceProvenance::Confirmed)
16294 && !retained_trail_source_price && !observed_tick_trail_price) {
16295 result.resolved_price = std::holds_alternative<native_order::Stop>(trigger)
16296 ? source_stop_resolved()
16297 : (source_gap_point ? source_bar_fill() : source_stop_fill());
16298 }
16299 if (finite_positive(source.forced_execution_price)) {
16300 result.resolved_price = source_forced_fill(source.forced_execution_price);
16301 } else if (const auto* touch = std::get_if<native_order::Limit>(&trigger);
16302 touch && touch->fill_through && trail_limit_one_shot
16303 && std::isfinite(result.resolved_price)) {
16304 // ab9714be pine_policy_members.cpp:45-53 (apply_slippage): the
16305 // zero-offset one-shot trail's source print moves slippage ticks
16306 // against the exit and is re-snapped directionally
16307 // (round_to_mintick_directional), exactly as the owner's TRAIL fill.
16308 const double tick = staged_.syminfo.mintick;
16309 result.resolved_price = directional_tick(
16310 result.resolved_price + (facts.is_buy ? 1.0 : -1.0) * config_.slippage * tick,
16311 tick, facts.is_buy);
16312 }
16313 // A strategy.close(id) sized when it was placed (flush_pending_closes) is
16314 // the transaction it was given: it crosses zero on the smaller book.
16315 if (PF_RARE(source.crosses_zero) && source.family == PineOrderFamily::Close) return result;
16318 || source.family == PineOrderFamily::Margin) {
16319 const bool explicit_source_exit = finite_positive(source.requested_qty)
16322 || source.family == PineOrderFamily::ExitTrail);
16323 if (explicit_source_exit
16324 || (source.family == PineOrderFamily::Close
16325 && source.close_batch_calls != 0)) {
16326 result.grid_policy = native_order::ExecutionGridPolicy::ExplicitUnits;
16327 }
16328 if ((source.family == PineOrderFamily::ExitLimit
16331 && !(facts.scope_exposure_units > 0.0)) {
16332 result.units = 0.0;
16333 return result;
16334 }
16335 // One entry's bracket of a per-entry global exit closes that entry's
16336 // units, the oldest lots first (global_exit_per_entry_levels).
16337 if (PF_RARE(source.per_entry_origin != 0)) {
16338 result.grid_policy = native_order::ExecutionGridPolicy::ExplicitUnits;
16339 result.units = std::min(source.per_entry_units, facts.scope_exposure_units);
16340 return result;
16341 }
16342 // ab9714be pine_fills.cpp:1618: whole-position coverage is spelled
16343 // `qty - kQtyEpsilon`, and pine_fills.cpp:1717-1723 then routes that
16344 // covered residual through the WHOLE-position exit rather than a
16345 // sized reduction. A reservation subtracted from the live basis in
16346 // binary64 can overshoot the exposure that actually remains at fill
16347 // time by a few ULPs; without this snap the leg is over-sized
16348 // against its scope, never fills, and strands a sub-lot remainder
16349 // that the 1x-margin path later fragments.
16350 const auto cover_full_scope = [&](double units) {
16351 if (source.family == PineOrderFamily::Close
16352 && !config_.close_entries_rule_any
16353 && std::holds_alternative<execution::Book>(facts.scope)) {
16354 const double close_units = std::min(units, facts.scope_exposure_units);
16355 const double boundary_units = source_fifo_close_units(
16356 close_units, require_host().native_open_lots(kNaN));
16357 if (boundary_units != close_units) {
16358 result.grid_policy = native_order::ExecutionGridPolicy::ExplicitUnits;
16359 return boundary_units;
16360 }
16361 units = close_units;
16362 }
16363 if (facts.scope_exposure_units > 0.0
16364 && units >= facts.scope_exposure_units - internal::kQtyEpsilon) {
16365 // The covered residual IS the literal selected exposure, which
16366 // an earlier percentage close can have left a few binary64
16367 // steps off the run's quantity grid. The owner books such an
16368 // exit through the whole-position path without re-quantizing
16369 // it, so authenticate the literal units for the pure reduction
16370 // instead of letting the grid reject the leg and strand the
16371 // dust (ab9714be pine_fills.cpp:1717-1723).
16372 if (const auto* host_close = std::get_if<native_order::HostSized>(
16373 &facts.definition->request.intent);
16374 host_close
16375 && host_close->kind == native_order::HostSizedKind::Close) {
16376 result.grid_policy =
16377 native_order::ExecutionGridPolicy::ExplicitUnits;
16378 }
16379 return facts.scope_exposure_units;
16380 }
16381 return units;
16382 };
16383 if (fill_time_global_exit(source)) {
16384 const double percent = std::isfinite(source.qty_percent) ? source.qty_percent : 100.0;
16385 if (percent >= 100.0 - internal::kFullPercentEps) {
16386 result.units = cover_full_scope(facts.scope_exposure_units);
16387 return result;
16388 }
16389 // A partial exit sized when it fills still keeps the reservation
16390 // its placement split off: the fill takes in the lots an add
16391 // opened since (w4-f19a cell E), never less than a sibling exit
16392 // that filled first left it (lab tv w3bf05-g8, lane W3B-ENG-GRID).
16393 double units = quantize_close_units(facts.scope_exposure_units, percent);
16394 if (finite_positive(source.projection_remaining_qty))
16395 units = std::max(units, source.projection_remaining_qty);
16396 result.units = cover_full_scope(units);
16397 return result;
16398 }
16399 const bool has_projected_remaining =
16400 (source.from_entry.empty() || source.fixed_exit_reservation)
16401 && std::isfinite(source.projection_remaining_qty);
16402 if (has_projected_remaining) {
16403 // ab9714be pine_fills.cpp:1708-1723: an exit reduction executes its
16404 // exact reservation without re-quantizing against the quantity grid,
16405 // so binary64 rounding of available = live_basis - reserved cannot
16406 // cause the native matcher to reject the leg as off-grid.
16407 if (const auto* host_close = std::get_if<native_order::HostSized>(
16408 &facts.definition->request.intent);
16409 host_close && host_close->kind == native_order::HostSizedKind::Close) {
16410 result.grid_policy =
16411 native_order::ExecutionGridPolicy::ExplicitUnits;
16412 }
16413 result.units = cover_full_scope(
16414 std::max(0.0, source.projection_remaining_qty));
16415 if ((source.family == PineOrderFamily::ExitLimit
16418 && source.qty_percent >= 100.0 - 1e-9
16419 && facts.scope_exposure_units > 0.0
16420 && (!source.from_entry.empty()
16422 // An adopted anchored leg is the same book close, armed.
16423 const auto& owner = facts.definition->request.owner;
16424 const auto* armed = std::get_if<native_order::WaitForApplied>(&owner);
16425 const bool book_owner = std::holds_alternative<native_order::Independent>(owner)
16426 || (armed && armed->scope == native_order::NativeArmScope::Book);
16427 const bool whole_scope_owner = !book_owner || source.from_entry.empty();
16428 if (whole_scope_owner) result.units = std::isfinite(source.requested_qty)
16429 ? std::min(*result.units, facts.scope_exposure_units)
16430 : cover_full_scope(facts.scope_exposure_units);
16431 }
16432 return result;
16433 }
16434 // A source full close is an all-live-cohort operation, not a stale
16435 // placement-sized reduction. The generic selected scope is the
16436 // authoritative physical sum at this candidate. Reusing the source
16437 // snapshot can differ by one binary64 rounding step after a previous
16438 // percentage close; that leaves a positive dust lot which the next
16439 // percent entry cannot represent alongside its new units. The fixed
16440 // quantity source transaction retains its captured own/transaction
16441 // facts, so it must not use this percent-sizing projection.
16442 // pine_strategy_commands.cpp:2775-2790 leaves a full-percentage exit
16443 // exact so it always flattens its live source scope. Do not send a
16444 // 100% HostSized close through the multiply/divide and quantity-grid
16445 // path: after an earlier margin slice that can floor one extra step
16446 // and leave a dust lot behind.
16447 if (source.deferred_cohort && !source.fixed_exit_reservation
16448 && !source.frozen_market_instruction
16449 && !std::isfinite(source.requested_qty)
16450 && (std::isnan(source.qty_percent) || source.qty_percent >= 100.0)) {
16451 result.units = cover_full_scope(facts.scope_exposure_units);
16452 return result;
16453 }
16454 if (finite_positive(source.requested_qty)) {
16455 result.units = source.requested_qty;
16456 return result;
16457 }
16458 double percent = source.qty_percent;
16459 if (std::isnan(percent)) percent = 100.0;
16460 const bool selected_exit =
16464 && std::holds_alternative<native_order::SelectedExposure>(facts.scope);
16465 result.units = selected_exit && percent == 100.0
16466 ? cover_full_scope(facts.scope_exposure_units)
16467 : cover_full_scope(
16468 quantize_close_units(facts.scope_exposure_units, percent));
16469 return result;
16470 }
16471 if (source.family == PineOrderFamily::Order && std::isfinite(source.requested_qty)) {
16472 result.units = std::max(0.0, source.requested_qty);
16473 const bool opposite = facts.position.signed_units != 0.0
16474 && ((facts.position.signed_units > 0.0) != source.is_long);
16475 if (opposite) {
16476 result.units = std::min(*result.units, facts.opposite_book_units);
16477 result.shape = native_order::OpeningShape::CloseOpposite;
16478 }
16479 return result;
16480 }
16481 // A typed quantity (qty_type cash / percent_of_equity) names its own money.
16482 // The source chooses that money and floors the units onto its grid; the
16483 // conversion cash / (fill price * point value * fx) and a percentage's fee
16484 // reserve are the kernel's (native_sized_units), as for a default
16485 // quantity. typed_entry_units is also what the placement admission prices
16486 // and what the fill-quantity probe answers.
16487 const bool typed_quantity = source.qty_type == static_cast<int>(QtyType::CASH)
16488 || source.qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY);
16489 if (source.family == PineOrderFamily::Entry && source.terms_priced_reverse) {
16490 const bool opposite_now = facts.position.signed_units != 0.0
16491 && ((facts.position.signed_units > 0.0) != source.is_long);
16492 if (source.affordability_close_only) {
16493 const bool stopped_flat_reversal = !opposite_now
16494 && facts.position.signed_units == 0.0
16495 && std::holds_alternative<native_order::Stop>(trigger)
16496 && finite_positive(source.projection_tv_carry_qty);
16497 result.units = opposite_now ? facts.opposite_book_units
16498 : (stopped_flat_reversal ? source.projection_tv_carry_qty : 0.0);
16499 result.shape = opposite_now ? native_order::OpeningShape::CloseOpposite
16500 : native_order::OpeningShape::Transact;
16501 return result;
16502 }
16503 double own_units = source.requested_qty;
16504 if (typed_quantity) {
16505 const auto typed = typed_entry_units(source, result.resolved_price,
16506 facts.active_fx, facts.target);
16507 if (!typed) return result;
16508 own_units = *typed;
16509 }
16510 result.units = own_units;
16511 const auto created_side = static_cast<PositionSide>(
16513 if (facts.position.signed_units == 0.0
16514 && finite_positive(source.projection_tv_carry_qty)
16515 && created_side != PositionSide::FLAT
16516 && ((created_side == PositionSide::LONG) != source.is_long)) {
16517 result.units = std::abs(own_units) + source.projection_tv_carry_qty;
16518 result.shape = native_order::OpeningShape::Transact;
16519 return result;
16520 }
16521 if (finite_positive(source.frozen_reversal_transaction)
16522 && source.placement_cycle == current_position_cycle_
16523 && std::abs(facts.position.signed_units - (source.is_long
16525 < 1e-12) {
16526 result.units = source.frozen_reversal_transaction;
16527 result.shape = native_order::OpeningShape::CloseOpposite;
16528 } else {
16529 // A co-queued source close can have flattened the placement-time
16530 // opposite side before this priced entry reaches its trigger.
16531 // ab9714be pine_fills.cpp:4618-4664 then executes an ordinary
16532 // opening, not a reversal against an already-consumed book.
16533 result.shape = opposite_now ? native_order::OpeningShape::ReverseTo
16534 : native_order::OpeningShape::Transact;
16535 }
16536 return result;
16537 }
16538 if (source.family == PineOrderFamily::Entry && finite_positive(source.requested_qty)) {
16539 result.units = typed_quantity
16540 ? typed_entry_units(source, result.resolved_price, facts.active_fx, facts.target)
16541 .value_or(0.0)
16542 : source.requested_qty;
16543 } else if (core_sized) {
16544 // The core resolved cash / (signal price * point value * fx) with the
16545 // fee reserve at acceptance and published the quotient as this
16546 // request's remaining units; only the source lot floor is left.
16547 // default_sizing_intent emits Sized only where that acceptance is
16548 // resolvable, so a missing quotient is a broken invariant (R5 N11).
16549 const auto* published = std::get_if<native_order::RemainingUnits>(&facts.remaining);
16550 if (!published) {
16551 throw coded<std::logic_error>(RunFailureCode::engine_invariant, {},
16552 "core-sized quantity without the core's quotient");
16553 }
16554 result.units = default_sizing_lot_floor(published->q);
16555 // A margin call that filled after the command, on its calculation's
16556 // bar, revised the source sizing to the equity after it
16557 // (refresh_pending_sizing_after_margin); the quotient the core froze
16558 // at acceptance is the equity before it. TradingView sizes the entry
16559 // on the equity after the call (lane W4-ENG-POOC-SAMEPASS).
16561 && finite_positive(source.sizing.frozen_units)) {
16562 result.units = source.sizing.frozen_units;
16563 }
16564 } else if (finite_positive(source.sizing.frozen_units) && !source.sizing.at_fill
16565 && (!(source.family == PineOrderFamily::Entry
16566 && finite_positive(source.exit_levels.stop)
16567 && !finite_positive(source.exit_levels.limit))
16568 || config_.default_qty_type
16569 == static_cast<int>(QtyType::PERCENT_OF_EQUITY))) {
16570 result.units = source.sizing.frozen_units;
16571 } else if (config_.default_qty_type == static_cast<int>(QtyType::FIXED)) {
16572 result.units = config_.default_qty_value;
16573 } else {
16574 const double price = source.sizing.at_fill ? result.resolved_price : source.sizing.price;
16575 const double fx = source.sizing.at_fill ? facts.active_fx : source.sizing.fx;
16576 PineSizingSnapshot sizing;
16577 sizing.price = price;
16578 sizing.fx = fx;
16579 sizing.mark = price;
16580 if (source.sizing.at_fill) {
16581 mark_sizing_equity(sizing, result.resolved_price, facts.active_fx);
16582 } else {
16583 sizing.equity = source.sizing.equity;
16584 sizing.strategy_equity = source.sizing.strategy_equity;
16585 }
16586 // ab9714be pine_policy_members.cpp:282: default sizing units divides equity by price, pointvalue, and currency fx
16587 result.units = default_sizing_units(sizing);
16588 }
16589 const auto created_side = static_cast<PositionSide>(source.projection_position_side);
16590 if (source.family == PineOrderFamily::Entry) {
16591 const bool opposite = facts.position.signed_units != 0.0
16592 && ((facts.position.signed_units > 0.0) != source.is_long);
16593 const bool keep_mc_close_surplus = [&]() {
16594 if (!source.affordability_keep_mc_close_surplus) return false;
16595 bool receipt_origin_live = false;
16596 for (const auto& cohort : cohorts_by_id_.held_rows()) {
16597 const auto units = cohort.second.live_units_by_origin.find(
16599 if (units != cohort.second.live_units_by_origin.end()
16600 && units->second > 0.0) {
16601 receipt_origin_live = true;
16602 break;
16603 }
16604 }
16605 const auto state = detail::run_state(require_host());
16606 const auto* pine_host = pine_view_of(&require_host());
16607 const bool magnifier = pine_host
16608 && pine_host->scheduler_.bar_magnifier_enabled();
16609 const double close_surplus = source.projection_tv_carry_qty
16610 - std::abs(facts.position.signed_units);
16611 return source.signal_close_mc_bar == source.projection_created_bar
16612 && source.projection_created_bar
16613 == facts.cursor.point.interval_index - 1
16615 && last_margin_call_event_ordinal_ == last_applied_ordinal_
16616 && pine_host
16617 && source.signal_close_mc_fill_seq
16618 == pine_host->adapter_broker_fill_event_sequence()
16619 && !config_.process_orders_on_close && !config_.calc_on_order_fills
16620 && !coof_recalc_active_ && !magnifier
16621 && std::holds_alternative<native_order::Market>(trigger)
16622 && !source.is_long && !std::isfinite(source.requested_qty)
16623 && !source.projection_after_close && !source.birth.from_fill()
16624 && source.projection_position_side
16625 == static_cast<std::int32_t>(PositionSide::LONG)
16626 && source.placement_cycle == current_position_cycle_
16627 && facts.position.signed_units > 0.0
16628 && facts.position.lot_count == 1U && receipt_origin_live
16629 && facts.position.signed_units
16631 && std::isfinite(close_surplus)
16632 && std::abs(close_surplus - 1.0) < 1e-6;
16633 }();
16634 // The unified admission (tests/fixtures/margin_call_rules,
16635 // margin_ledger_rules and admission_rules): at its signal, placement
16636 // rule 2 on the money scale -- E < sig10(Q c' m) drops an opening from
16637 // flat and keeps only a reversal's closing leg -- and rule 5 on the
16638 // price scale at full margin -- sig10(sig10(E) / Q) under (ticks(c') +
16639 // side x slip) x mintick drops it whole (switch unified_placement); at
16640 // its fill only the price scale, every order type at every margin:
16641 // sig10(sig10(E) / (Q m)) under the tick-built fill quote moved by the
16642 // slippage ticks drops the whole order, a reversal keeping its old
16643 // position (switch fill_price_recheck). E is the signal's, frozen. No
16644 // money check is taken at the fill quote, and no tie band bounds
16645 // either half. A close-first entry takes rule 2 alone: it fills flat
16646 // behind the close it follows (close_first_admission).
16647 const bool market_or_stop = std::holds_alternative<native_order::Market>(trigger)
16648 || std::holds_alternative<native_order::Stop>(trigger);
16649 bool unified_admit = false;
16650 const auto& admission_rules = detail::margin_rule_switches();
16651 if (PF_RARE(source.signal_price_refused && admission_rules.unified_placement)) {
16652 result.units = 0.0;
16653 result.shape = native_order::OpeningShape::Transact;
16654 return result;
16655 }
16656 // The scope answers false outside its configuration
16657 // (unified_configured_); the terms are formed out of line, inside it.
16658 if (PF_RARE(unified_configured_ && !keep_mc_close_surplus
16659 && (admission_rules.unified_placement
16660 || admission_rules.fill_price_recheck))
16661 && unified_admission_terms(source, facts, market_or_stop, opposite, result,
16662 unified_admit)) {
16663 return result;
16664 }
16665 // ab9714be pine_fills.cpp:6577-6598: a default MARKET request carries
16666 // frozen_default_qty into execute_market_entry as a prequantized
16667 // quantity. Only per-call typed percentage requests use the
16668 // hypothetical-Flatten sizing path above.
16669 const bool default_money_candidate = !unified_admit
16670 && std::holds_alternative<native_order::Market>(trigger)
16671 && !std::isfinite(source.requested_qty)
16672 && config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
16673 && std::abs(config_.default_qty_value - 100.0) < 1e-12
16674 && std::abs((source.is_long ? config_.margin_long : config_.margin_short) - 100.0)
16675 < 1e-12
16676 && staged_.quantity_grid && *staged_.quantity_grid > 0.0
16677 && result.units && finite_positive(*result.units)
16678 && finite_positive(source.sizing.price)
16679 && finite_positive(source.sizing.equity)
16680 && finite_positive(source.sizing.fx)
16681 && finite_positive(staged_.syminfo.pointvalue);
16682 if (default_money_candidate) {
16683 bool ordinary_book = true;
16684 for (const auto& handle : live_handles_) {
16685 if (handle == facts.target) continue;
16686 const auto peer = placement_.find(handle.incarnation);
16687 if (peer == placement_.end()) continue;
16688 const auto& row = peer->second;
16689 const bool unpriced_close = (row.family == PineOrderFamily::Close
16690 || row.family == PineOrderFamily::CloseAll)
16691 && !finite_positive(row.exit_levels.limit)
16692 && !finite_positive(row.exit_levels.stop)
16693 && !finite_positive(row.exit_levels.trail_points)
16694 && !finite_positive(row.exit_levels.trail_price);
16695 if (!unpriced_close) {
16696 ordinary_book = false;
16697 break;
16698 }
16699 }
16700 const bool low_value_lot = *staged_.quantity_grid * source.sizing.price
16701 * staged_.syminfo.pointvalue * source.sizing.fx < 1.0;
16702 const bool ordinary_fractional = *staged_.quantity_grid < 1.0
16703 && staged_.syminfo.pointvalue == 1.0 && source.sizing.fx == 1.0
16704 && staged_.account_fx_effective_from_ms.empty()
16705 && config_.commission_value == 0.0 && config_.slippage == 0
16706 && !config_.process_orders_on_close && !config_.calc_on_order_fills
16707 && config_.pyramiding >= 0 && config_.pyramiding <= 1
16708 && !cap.active() && risk_.max_intraday_loss <= 0.0
16709 && risk_.max_drawdown <= 0.0 && risk_.max_cons_loss_days == 0
16710 && ordinary_book;
16711 const bool whole_lot_tie_scope = *staged_.quantity_grid == 1.0
16712 && staged_.syminfo.pointvalue == 1.0 && source.sizing.fx == 1.0
16713 && staged_.account_fx_effective_from_ms.empty()
16714 && config_.commission_value == 0.0 && config_.slippage == 0
16715 && !config_.process_orders_on_close && !config_.calc_on_order_fills
16716 && config_.pyramiding >= 0 && config_.pyramiding <= 1
16717 && !cap.active() && risk_.max_intraday_loss <= 0.0
16718 && risk_.max_drawdown <= 0.0 && risk_.max_cons_loss_days == 0
16719 && ordinary_book;
16720 // Flat money ties use rounded notional, with TAIL-D's settled
16721 // agreement or no-gap qualification preserved (source_buying_power
16722 // flat-*/pooc-* and whole_lot_tie). Pyramiding does not affect a
16723 // genuinely flat book with no competing opening.
16724 const bool whole_flat_money = *staged_.quantity_grid == 1.0
16725 && staged_.syminfo.pointvalue == 1.0 && source.sizing.fx == 1.0
16726 && staged_.account_fx_effective_from_ms.empty()
16727 && config_.commission_value == 0.0 && config_.slippage == 0
16728 && !config_.calc_on_order_fills
16729 && finite_positive(staged_.syminfo.mintick)
16730 && !source.birth.from_fill() && source.oca_name.empty()
16731 && !cap.active() && risk_.max_intraday_loss <= 0.0
16732 && risk_.max_drawdown <= 0.0 && risk_.max_cons_loss_days == 0
16733 && source.projection_position_side
16734 == static_cast<std::int32_t>(PositionSide::FLAT)
16735 && !source.projection_after_close && source.projection_predecessor == 0
16736 && facts.position.signed_units == 0.0
16737 && std::none_of(live_handles_.begin(), live_handles_.end(),
16738 [&](const auto& handle) {
16739 if (handle == facts.target) return false;
16740 const auto peer = placement_.find(handle.incarnation);
16741 return peer != placement_.end() && peer->second.opening;
16742 });
16743 if (whole_flat_money) {
16744 const double rounded_cost = source_money_round(
16745 *result.units * source.sizing.price);
16746 double settled = source.sizing.equity;
16747 const auto* pine = pine_view_of(host_);
16748 if (pine && std::isfinite(pine->net_profit_sum_)
16749 && same_double_bits(percent_commission_live_equity(source.sizing.mark),
16750 source.sizing.equity)) {
16751 settled = pine->initial_capital_
16752 + source_money_round(pine->net_profit_sum_);
16753 const double direction = pine->position_side_ == PositionSide::SHORT
16754 ? -1.0 : 1.0;
16755 for (const auto& lot : pine->pyramid_entries_) {
16756 settled += direction * (source.sizing.mark - lot.price) * lot.qty
16757 * staged_.syminfo.pointvalue * source.sizing.fx
16758 - pine->open_entry_commission(lot);
16759 }
16760 }
16761 const bool no_gap_tie = nearest_tick(
16762 result.resolved_price, staged_.syminfo.mintick) == source.sizing.price
16763 && rounded_cost == source_money_round(source.sizing.equity);
16764 if (std::isfinite(rounded_cost) && source.sizing.equity < rounded_cost
16765 && (settled < rounded_cost || no_gap_tie)) {
16766 result.units = 0.0;
16767 result.shape = native_order::OpeningShape::Transact;
16768 return result;
16769 }
16770 }
16771 const auto native_state = detail::run_state(require_host());
16772 const bool pooc_flat_money = config_.process_orders_on_close
16773 && source.projection_position_side
16774 == static_cast<std::int32_t>(PositionSide::FLAT)
16775 && !source.projection_after_close && source.projection_predecessor == 0
16776 && facts.position.signed_units == 0.0
16777 && source.projection_created_bar == projection_bar_index(facts.cursor.point)
16778 && !source.birth.from_fill() && source.oca_name.empty()
16779 && config_.pyramiding >= 0 && config_.pyramiding <= 1
16780 && config_.commission_value == 0.0 && config_.slippage >= 0
16781 && finite_positive(staged_.syminfo.mintick)
16782 && *staged_.quantity_grid < 1.0
16783 && staged_.syminfo.pointvalue == 1.0 && source.sizing.fx == 1.0
16784 && staged_.account_fx == 1.0
16785 && staged_.account_fx_effective_from_ms.empty()
16786 && (!native_state.spec || native_state.spec->intrabar.is_none())
16787 && !cap.active()
16788 && risk_.max_intraday_loss <= 0.0
16789 && risk_.max_drawdown <= 0.0 && risk_.max_cons_loss_days == 0
16790 && nearest_tick(facts.raw_price, staged_.syminfo.mintick)
16791 == nearest_tick(source.sizing.mark, staged_.syminfo.mintick);
16792 // Whole lots: the signal's equity against the cost of the whole
16793 // lots it sized, the units times the signal close, compared
16794 // exactly and whatever the next open does. Short of it by a
16795 // rounding tie, TradingView drops a flat entry and keeps only the
16796 // closing leg of a reversal (lab tv tapes
16797 // tests/fixtures/whole_lot_tie jd-rev-tie-pair, -bare, -gapdown,
16798 // -nogap, jd-flat-tie-gapdown and the control jd-rev-notie-pair).
16799 // That equity is the capital plus the realized profit plus the
16800 // open profit in floating point, whose residue decides an exact
16801 // tie (jd-drift-under and jd-drift-over: the capital and a
16802 // realized loss sum one ulp under and over their cent value). A
16803 // running sum of many closed profits carries a residue of its own
16804 // the engine does not reproduce: where the engine's sum and that
16805 // sum on the money grid disagree on the tie, TradingView takes it
16806 // on some books and not on others (declined on third-party
16807 // NASDAQ:AAPL 15 and NYSE:F 15 strategies, taken on another
16808 // third-party NYSE:F 15 strategy). Those differing ledgers
16809 // do not establish a broader rule, so the adapter keeps the no-gap
16810 // rule it had there (ab9714be pine_fills.cpp:5190-5219). While the
16811 // book is still the signal's the settled sum is read (lane TAIL-D).
16812 if (whole_lot_tie_scope) {
16813 const double cost = *result.units * source.sizing.price;
16814 double settled = source.sizing.equity;
16815 const auto* pine = pine_view_of(host_);
16816 if (pine && std::isfinite(pine->net_profit_sum_)
16817 && same_double_bits(percent_commission_live_equity(source.sizing.mark),
16818 source.sizing.equity)) {
16819 settled = pine->initial_capital_
16820 + source_money_round(pine->net_profit_sum_);
16821 const double direction = pine->position_side_ == PositionSide::SHORT
16822 ? -1.0 : 1.0;
16823 for (const auto& lot : pine->pyramid_entries_) {
16824 settled += direction * (source.sizing.mark - lot.price) * lot.qty
16825 * staged_.syminfo.pointvalue * source.sizing.fx
16826 - pine->open_entry_commission(lot);
16827 }
16828 }
16829 const bool short_raw = source.sizing.equity < cost;
16830 const bool short_settled = settled < cost;
16831 if (std::isfinite(cost) && short_raw && short_settled) {
16832 result.units = opposite ? facts.opposite_book_units : 0.0;
16833 result.shape = opposite ? native_order::OpeningShape::CloseOpposite
16834 : native_order::OpeningShape::Transact;
16835 return result;
16836 }
16837 if (std::isfinite(cost) && short_raw
16838 && nearest_tick(result.resolved_price, staged_.syminfo.mintick)
16839 == source.sizing.price
16840 && cost == source_money_round(source.sizing.equity)) {
16841 result.units = 0.0;
16842 result.shape = native_order::OpeningShape::Transact;
16843 return result;
16844 }
16845 }
16846 if (low_value_lot || ordinary_fractional || pooc_flat_money) {
16847 const double notional_per_price = *result.units
16848 * staged_.syminfo.pointvalue * source.sizing.fx;
16849 const double rounded_cost = source_money_round(
16850 notional_per_price
16851 * (pooc_flat_money ? source.sizing.mark : source.sizing.price));
16852 // The no-epsilon tape proves ordinary low-value opposite
16853 // market entries with pyramiding zero or one, including its
16854 // default (source_buying_power forex and px-fx tapes).
16855 // Preserve the prior guard for fractional POOC, costs, fill
16856 // births, other pyramiding, and projected-close books.
16857 const bool exact_money_scope = low_value_lot && ordinary_fractional
16858 && opposite && !source.projection_after_close
16859 && !source.birth.from_fill() && source.oca_name.empty()
16860 && config_.pyramiding <= 1;
16861 const double money_guard = exact_money_scope ? 0.0 : 1e-9;
16862 if (source.sizing.equity + money_guard < rounded_cost) {
16863 result.units = keep_mc_close_surplus ? 1.0
16864 : (opposite ? facts.opposite_book_units : 0.0);
16865 result.shape = keep_mc_close_surplus
16866 ? native_order::OpeningShape::ReverseTo
16867 : (opposite ? native_order::OpeningShape::CloseOpposite
16868 : native_order::OpeningShape::Transact);
16869 return result;
16870 }
16871 if (!source.projection_after_close) {
16872 const double affordable_price = source_money_round(
16873 source_money_round(source.sizing.equity) / notional_per_price);
16874 if (std::isfinite(affordable_price)
16875 && affordable_price < source.sizing.price) {
16876 result.units = 0.0;
16877 result.shape = native_order::OpeningShape::Transact;
16878 return result;
16879 }
16880 }
16881 }
16882 }
16883 bool affordability_close_only = !unified_admit && source.affordability_close_only;
16884 if (!unified_admit && !affordability_close_only && source.affordability_policy_active
16885 && opposite) {
16886 const double margin = source.is_long ? config_.margin_long : config_.margin_short;
16887 const double own = result.units ? *result.units : 0.0;
16888 const double fill = nearest_tick(result.resolved_price, staged_.syminfo.mintick);
16889 const double required = own * fill * staged_.syminfo.pointvalue * facts.active_fx
16890 * margin / 100.0;
16891 // The source affordability tuple freezes its MTM equity at the
16892 // signal. The entry's later gap changes the cost, not the
16893 // carried-position mark; this is the NQ/rampatel close-only rule.
16894 const auto* pine_host = pine_view_of(&require_host());
16895 const double equity = finite_positive(source.sizing.equity) ? source.sizing.equity
16896 : pine_host ? pine_host->marked_equity_at(fill, facts.active_fx)
16897 : require_host().native_marked_equity(fill);
16898 const double epsilon = std::max(
16899 1e-9, std::abs(equity) * 1e-12);
16900 affordability_close_only = margin > 0.0 && std::isfinite(required)
16901 && (!std::isfinite(equity) || required > equity + epsilon);
16902 }
16903 if (affordability_close_only) {
16904 if (!opposite) {
16905 result.units = 0.0;
16906 return result;
16907 }
16908 result.units = keep_mc_close_surplus ? 1.0
16909 : facts.opposite_book_units;
16910 result.shape = keep_mc_close_surplus
16911 ? native_order::OpeningShape::ReverseTo
16912 : native_order::OpeningShape::CloseOpposite;
16913 return result;
16914 }
16915 }
16916 if (source.family == PineOrderFamily::Order) {
16917 const bool opposite = facts.position.signed_units != 0.0
16918 && ((facts.position.signed_units > 0.0) != source.is_long);
16919 if (opposite) {
16920 result.units = std::min(*result.units, facts.opposite_book_units);
16921 result.shape = native_order::OpeningShape::CloseOpposite;
16922 }
16923 return result;
16924 }
16925 if (source.family == PineOrderFamily::Entry && source.sequential_group != 0
16926 && source.sequential_rank != 0 && source.has_full_entry_bracket) {
16927 bool paired = false;
16928 // The group's entries are kept while a member is live or queued
16929 // (erase_retired_rows, K7).
16930 const auto pairs = [&](const PlacementSnapshot& peer) {
16931 return peer.family == PineOrderFamily::Entry
16932 && peer.sequential_group == source.sequential_group
16933 && peer.sequential_rank != 0 && peer.sequential_rank != source.sequential_rank
16934 && peer.has_full_entry_bracket
16935 && !(source.is_long && peer.replaced_opening
16936 && peer.replacement_predecessor_market);
16937 };
16939 [&](std::uint64_t, const PlacementSnapshot& peer) { return pairs(peer); });
16940 for (const auto& row : placement_) {
16941 if (pairs(row.second)) {
16942 paired = true;
16943 break;
16944 }
16945 }
16946 if (paired) {
16947 const bool opposite = facts.position.signed_units != 0.0
16948 && ((facts.position.signed_units > 0.0) != source.is_long);
16949 if (source.sequential_rank == 1 && opposite)
16950 result.units = std::max(std::abs(facts.position.signed_units), *result.units);
16951 result.shape = native_order::OpeningShape::Transact;
16952 return result;
16953 }
16954 }
16955 // A same-id replacement of a live MARKET entry over an opposite open book
16956 // is a source transaction (reduce the carried side by its own size, open
16957 // any remainder), not the ordinary auto-reversal shape. The replacement
16958 // fact is captured before submit_or_replace retires its predecessor.
16959 // TradingView sizes a MARKET reissue against the position its predecessor
16960 // would leave, so it carries no reversal addend on either side and for
16961 // every quantity kind: default percent, fixed and cash, and an explicit
16962 // qty (R1, tests/fixtures/same_bar_reissue). The sell default-percent
16963 // scope before it is kept for a priced reissue.
16964 const bool opposite_at_fill = facts.position.signed_units != 0.0
16965 && ((facts.position.signed_units > 0.0) != source.is_long);
16966 const auto live_side = facts.position.signed_units > 0.0
16967 ? PositionSide::LONG : (facts.position.signed_units < 0.0
16969 const bool prior_cycle_close_only = opposite_at_fill
16970 && created_side != live_side
16972 && (finite_positive(source.exit_levels.stop)
16973 || finite_positive(source.exit_levels.limit));
16974 const bool deferred_flip_from_flat = facts.position.signed_units == 0.0
16975 && source.projection_position_side
16976 != static_cast<std::int32_t>(PositionSide::FLAT)
16977 && ((source.projection_position_side
16978 == static_cast<std::int32_t>(PositionSide::LONG)) != source.is_long)
16979 && (finite_positive(source.exit_levels.stop)
16980 || finite_positive(source.exit_levels.limit))
16981 && finite_positive(source.projection_tv_carry_qty)
16982 && result.units && finite_positive(*result.units);
16983 if (deferred_flip_from_flat) {
16984 // ab9714be pine_orders.cpp:676-696: an opposite priced entry
16985 // which outlives the position it was placed against opens its own
16986 // units plus the captured carried side.
16987 result.units = *result.units + source.projection_tv_carry_qty;
16988 result.shape = native_order::OpeningShape::Transact;
16989 }
16990 if (opposite_at_fill) {
16991 const bool source_close_precedes = source.projection_after_close;
16992 const bool replacement_transaction = source.reverse_to
16993 && source.replaced_opening
16995 && (std::holds_alternative<native_order::Market>(trigger)
16996 || (!source.is_long && !std::isfinite(source.requested_qty)
16997 && config_.default_qty_type
16998 == static_cast<int>(QtyType::PERCENT_OF_EQUITY)));
16999 // A flat stop entry is kept while a live or queued entry shares its
17000 // placement bar (erase_retired_rows, K8).
17001 const auto dual_stop_peer = [&](std::uint64_t incarnation, const PlacementSnapshot& peer) {
17002 return incarnation != facts.target.incarnation && peer.opening
17003 && peer.family == PineOrderFamily::Entry
17004 && peer.projection_position_side
17005 == static_cast<std::int32_t>(PositionSide::FLAT)
17006 && peer.projection_created_bar == source.projection_created_bar
17007 && peer.is_long != source.is_long
17008 && finite_positive(peer.exit_levels.stop)
17009 && !finite_positive(peer.exit_levels.limit);
17010 };
17011 const bool flat_dual_candidate = source.projection_position_side
17012 == static_cast<std::int32_t>(PositionSide::FLAT)
17013 && finite_positive(source.exit_levels.stop)
17014 && !finite_positive(source.exit_levels.limit);
17015 if (flat_dual_candidate)
17016 PINEFORGE_AUDIT_PLACEMENT_SCAN(placement_, dual_stop_peer);
17017 const bool flat_dual_stop = flat_dual_candidate
17018 && std::any_of(placement_.begin(), placement_.end(),
17019 [&](const auto& row) { return dual_stop_peer(row.first, row.second); });
17020 result.shape = source_close_precedes || replacement_transaction
17021 ? native_order::OpeningShape::Transact
17022 : prior_cycle_close_only
17023 ? native_order::OpeningShape::CloseOpposite
17024 : flat_dual_stop
17025 ? native_order::OpeningShape::Transact
17026 : native_order::OpeningShape::ReverseTo;
17027 }
17028 return result;
17029}
17030
17031bool PineExecutionAdapter::partial_exit_sibling_live(
17032 const PlacementSnapshot& source, native_order::RequestHandle target) const {
17033 return std::any_of(live_handles_.begin(), live_handles_.end(), [&](const auto& handle) {
17034 if (handle == target) return false;
17035 const auto sibling = placement_.find(handle.incarnation);
17036 if (sibling == placement_.end()) return false;
17037 const auto family = sibling->second.family;
17038 // One bracket's stop/limit/trail legs are one owner order. Keep the
17039 // existing consumed-cycle predicate, shared with on_applied below.
17040 if (sibling->second.command_sequence == source.command_sequence
17041 && sibling->second.bracket_origin == source.bracket_origin) return false;
17042 return sibling->second.source_id == source.source_id
17043 && sibling->second.from_entry == source.from_entry
17044 && (family == PineOrderFamily::ExitLimit || family == PineOrderFamily::ExitStop
17045 || family == PineOrderFamily::ExitTrail);
17046 });
17047}
17048
17051 || !(view.inspected_closed_units > 0.0) || view.account.signed_units_after == 0.0)
17052 return;
17053 const auto found = placement_.find(view.target.incarnation);
17054 if (found == placement_.end()) return;
17055 const auto& source = found->second;
17058 || !((std::isfinite(source.requested_qty) && source.requested_qty > 0.0)
17059 || (std::isfinite(source.qty_percent) && source.qty_percent < 100.0 - 1e-9))
17060 || partial_exit_sibling_live(source, view.target)) return;
17061 // Still ONE single-string key with one separator. This borrowed view
17062 // performs no allocation, publication or financial/source mutation.
17063 consumed_partial_exit_cycles_.preflight_joined_single(source.source_id, '\x1f', source.from_entry);
17064}
17065
17067 require_tree_precommit(view);
17068 const auto snapshot = placement_.find(view.target.incarnation);
17069 if (snapshot != placement_.end()) {
17070 const auto& source = snapshot->second;
17071 const auto physical = detail::run_position(require_host());
17072 // ab9714be pine_fills.cpp:7449-7459: a deferred strategy.close belongs
17073 // to the position cycle it was issued in. When the live position
17074 // opened on a bar later than the close's creation bar — the next open's
17075 // opposite entry applied first and flipped the book — the legacy book
17076 // Removes the stale close instead of flattening the freshly opened lot
17077 // at its own entry price. The two legacy exclusions are the short-seed
17078 // MATERIALIZE_LONG close and the same-bar market transaction's targeted
17079 // close artifact.
17080 const bool stale_close_for_new_position =
17082 || source.family == PineOrderFamily::CloseAll)
17083 && static_cast<PositionSide>(source.projection_position_side)
17085 && physical.signed_units != 0.0
17086 && position_open_bar_index_ > source.projection_created_bar
17087 && !source.frozen_market_targeted_close
17088 && !(short_seed_.active && view.target == short_seed_.materialize_long);
17089 if (stale_close_for_new_position) return NativePrecommitVerdict::Refuse;
17090 // ab9714be pine_fills.cpp:7483-7537: priced (stop/limit) entries are
17091 // throttled to one opening from flat per bar after an earlier entry
17092 // fill. A same-direction pyramid while still in position is the
17093 // exception and is admitted below by the not-flat check. Not gated on
17094 // process_orders_on_close (the owner applies it at the POOC fill
17095 // point too); COOF stays excluded.
17096 const bool leftover_flat_stop = view.definition
17097 && std::holds_alternative<native_order::Stop>(view.definition->request.trigger)
17098 && !finite_positive(source.exit_levels.limit);
17099 // ab9714be pine_fills.cpp:3687-3788: an older same-direction flip-prep
17100 // stop that this bar also touches fills first and consumes the
17101 // from-flat carry. Skip this later stop until that sibling opens,
17102 // then re-arm it (probe 72/93 S then S2).
17103 if (source.family == PineOrderFamily::Entry
17104 && view.account.would_open
17105 && physical.signed_units == 0.0
17106 && leftover_flat_stop
17107 && policy_script_bar_valid_
17108 && !config_.calc_on_order_fills
17109 && modeled_input()) {
17110 for (const auto& handle : live_handles_) {
17111 if (handle == view.target) continue;
17112 const auto found = placement_.find(handle.incarnation);
17113 if (found == placement_.end()) continue;
17114 const auto& prior = found->second;
17115 if (!prior.opening || prior.family != PineOrderFamily::Entry
17116 || prior.is_long != source.is_long
17117 || !finite_positive(prior.exit_levels.stop)
17118 || finite_positive(prior.exit_levels.limit)
17119 // ab9714be pine_fills.cpp:3895-3899: stable_sort orders touched entry stops by created_seq (source_sequence)
17120 || prior.source_sequence >= source.source_sequence) {
17121 continue;
17122 }
17123 const auto created = static_cast<PositionSide>(
17124 prior.projection_position_side);
17125 // ab9714be pine_fills.cpp:3687-3788 / 7470-7516: the pending
17126 // scan is book order, not path order. An older same-direction
17127 // stop that this bar also touches fills first, including a
17128 // leftover flat-armed leg (probe 80 morning LE then afternoon
17129 // LE2). Same-side-created pyramid adds are not leftovers.
17130 if (created != PositionSide::FLAT
17131 && (created == PositionSide::LONG) == prior.is_long) {
17132 continue;
17133 }
17134 // Same-bar siblings keep fill_phase (open-tick vs path). A
17135 // leftover from an earlier bar that this bar also touches
17136 // is the book-order override (LE then LE2).
17137 if (prior.projection_created_bar == source.projection_created_bar) {
17138 continue;
17139 }
17140 const bool source_open_mkt = source.is_long
17141 ? policy_script_bar_.open >= source.exit_levels.stop
17142 : policy_script_bar_.open <= source.exit_levels.stop;
17143 const bool prior_open_mkt = prior.is_long
17144 ? policy_script_bar_.open >= prior.exit_levels.stop
17145 : policy_script_bar_.open <= prior.exit_levels.stop;
17146 if (source_open_mkt != prior_open_mkt) continue;
17147 const bool prior_touched = prior.is_long
17148 ? policy_script_bar_.high >= prior.exit_levels.stop
17149 : policy_script_bar_.low <= prior.exit_levels.stop;
17150 if (!prior_touched) continue;
17151 if (!throttled_rearm_already_queued(throttled_reopen_rearm_, source))
17152 throttled_reopen_rearm_.push_back(source);
17153 return NativePrecommitVerdict::Refuse;
17154 }
17155 }
17156 if (source.family == PineOrderFamily::Entry
17157 && view.account.would_open
17158 && physical.signed_units == 0.0
17159 && entry_openings_this_interval_ > 0
17160 && view.cursor.point.interval_index == entry_openings_interval_index_
17161 && leftover_flat_stop
17162 && !config_.calc_on_order_fills
17163 && modeled_input()) {
17164 // The legacy throttle is OrderEligibility::Skip for the bar; the
17165 // order keeps resting. A generic Refuse is terminal, so re-arm the
17166 // original stop at the bar close (L9g).
17167 if (!throttled_rearm_already_queued(throttled_reopen_rearm_, source))
17168 throttled_reopen_rearm_.push_back(source);
17169 return NativePrecommitVerdict::Refuse;
17170 }
17171 const bool opposite_entry = source.family == PineOrderFamily::Entry
17172 && physical.signed_units != 0.0
17173 && ((physical.signed_units > 0.0) != source.is_long);
17174 // ab9714be pine_risk.cpp:111-118, called only from
17175 // pine_orders.cpp:221 and pine_fills.cpp:4720-4727: these latches
17176 // gate a same-side/flat ENTRY at its fill. They never gate closes,
17177 // exits, RAW orders, or the closing half of an opposite entry.
17178 if (source.family == PineOrderFamily::Entry && !opposite_entry) {
17179 if (risk_.halted) return NativePrecommitVerdict::Refuse;
17180 if (risk_.max_cons_loss_days > 0
17181 && day_ledger_.consecutive_loss_days >= risk_.max_cons_loss_days) {
17182 return NativePrecommitVerdict::Refuse;
17183 }
17184 // pine_risk.cpp:115 compares the live source position, not the
17185 // candidate's resulting quantity. Equality blocks a further add.
17186 if (risk_.max_position_size > 0.0 && view.account.would_open
17187 && std::abs(physical.signed_units) >= risk_.max_position_size) {
17188 return NativePrecommitVerdict::Refuse;
17189 }
17190 }
17191 const bool exit = source.family == PineOrderFamily::ExitLimit
17194 if (exit && !source.immediately) {
17195 if (!config_.close_entries_rule_any && !source.from_entry.empty()) {
17196 const auto cohort = cohorts_by_id_.find(source.from_entry);
17197 if (cohort == cohorts_by_id_.end() || cohort->second.opened.empty()) {
17198 return NativePrecommitVerdict::Refuse;
17199 }
17200 }
17201 const bool restored_after_margin = source.restored_after_margin
17202 || (source.legs.last_action()
17203 && source.legs.last_action()->cause.phase
17204 == exit_legs::Phase::AfterMargin
17205 && !source.legs.dormant());
17206 const bool retained_trail = source.family == PineOrderFamily::ExitTrail
17207 && !source.legs.retired(exit_legs::Leg::Trail);
17208 if (!retained_trail
17209 && (source.legs.dormant()
17210 || (!restored_after_margin
17211 && follows_same_bar_declined_reversal(source, view)))) {
17212 return NativePrecommitVerdict::Refuse;
17213 }
17214 }
17215 }
17216 // All exit-specific refusal gates above have passed. A storage refusal
17217 // is a coded exception, never a trading-policy Refuse verdict.
17219 if (!view.account.would_open) return NativePrecommitVerdict::Admit;
17220 if (snapshot == placement_.end()) return NativePrecommitVerdict::Refuse;
17221 const auto& source = snapshot->second;
17222 if (cap.active() && cap.budget().latched()
17223 && source.source_id != "__intraday_cap_close__") {
17224 const auto& transfer = cap.budget().transfer();
17225 const bool inherited = transfer
17226 && transfer->inheritor == view.target.incarnation
17227 && transfer->close_fill == cap_latest_fill_;
17228 if (!inherited) return NativePrecommitVerdict::Refuse;
17229 }
17230 const auto physical_now = detail::run_position(require_host());
17231 // A market entry the script placed at a process_orders_on_close close where
17232 // a follow-up was then booked after it (book_owed_close_follow_up), filling
17233 // at that close on the book the frozen close's over-fill lot left on its
17234 // side, buys its own leg: the pyramiding-0 refusal does not read it (lab tv
17235 // tapes tests/fixtures/margin_open_rules lots/close-id-*,
17236 // close-all-overfill-gap: the entry "L" opens beside the over-fill lot), and
17237 // a default-sized one is not re-checked for affordability at the fill
17238 // either (synthetic OANDA:EURUSD 15m: a default-sized "LONG" of
17239 // 40000 opens beside 0.20; the over-fill controls cover the ordering).
17240 // Other shapes beside the lot -- a priced entry, a fill on a later bar --
17241 // keep every gate: no tape decides them.
17242 const auto beside_frozen_close_lot = [&]() {
17243 if (last_margin_call_script_bar_ != source.placement_script_open_ms
17244 || !config_.process_orders_on_close || !source.opening
17245 || !detail::margin_schedule_switches().close_follow_up_after_script
17246 || !view.definition
17247 || !std::holds_alternative<native_order::Market>(view.definition->request.trigger)
17248 || finite_positive(source.exit_levels.limit) || finite_positive(source.exit_levels.stop)
17249 || projection_bar_index(view.cursor.point) != source.projection_created_bar) {
17250 return false;
17251 }
17252 const auto lots = require_host().native_open_lots(kNaN);
17253 if (lots.empty()) return false;
17254 const auto first = placement_.find(lots.front().entry_incarnation);
17255 return first != placement_.end() && first->second.crosses_zero
17256 && (first->second.family == PineOrderFamily::Close
17257 || first->second.family == PineOrderFamily::CloseAll)
17258 && first->second.placement_script_open_ms == source.placement_script_open_ms;
17259 };
17260 if (source.family == PineOrderFamily::Entry && view.account.would_open
17261 && physical_now.signed_units != 0.0
17262 && ((physical_now.signed_units > 0.0) == source.is_long)) {
17263 const bool beside = !source.affordability_close_only && PF_RARE(beside_frozen_close_lot());
17264 if (beside && source.deferred_cohort && !std::isfinite(source.requested_qty))
17265 return NativePrecommitVerdict::AdmitWithHostMargin;
17266 if (config_.pyramiding == 0 && !beside) return NativePrecommitVerdict::Refuse;
17267 }
17268 // ab9714be pine_fills.cpp:5551-5554,5564-5664: the high-level MARKET
17269 // affordability gate never owned RAW strategy.order. A36 places host
17270 // admission before the generic gate so this explicit exclusion can remain
17271 // source policy while the native core still owns the resulting fill and
17272 // any post-fill margin checkpoint.
17273 if (source.family == PineOrderFamily::Order) {
17274 return NativePrecommitVerdict::AdmitWithHostMargin;
17275 }
17276 // Nor an order sized when it was placed (close_point_margin_call): a
17277 // margin call or close whose book shrank before it filled opens the rest
17278 // on the other side, or beside the lot the other side already holds, as
17279 // placed -- no entry gate, pyramiding included, reads it (w13-b1s-close-f; synthetic:
17280 // the close_all's 6 units and the follow-up's 2, once the short is flat,
17281 // both open long at the next 25.00 open).
17282 // The scope includes the current batch phase. Cross-phase frozen sizing
17283 // remains outside this route; persisting placement scope also needs the
17284 // snapshot's state-hash and placement-receipt representation.
17285 if (source.crosses_zero
17286 || (source.frozen_market_instruction && fixed_unmargined_market_batch_scope())) {
17287 return NativePrecommitVerdict::AdmitWithHostMargin;
17288 }
17289 // The unified admission's fill half (fill_price_recheck), at this fill's
17290 // own quote; no money check follows here (resolve_terms took the signal
17291 // half). A close-first entry is never checked on the price scale.
17292 const bool close_first_admitted = source.close_first_entry
17294 if (PF_RARE(unified_configured_ && view.definition
17295 && (detail::margin_rule_switches().fill_price_recheck || close_first_admitted)
17296 && unified_admission_scope(
17297 source,
17298 std::holds_alternative<native_order::Market>(view.definition->request.trigger)
17299 || std::holds_alternative<native_order::Stop>(view.definition->request.trigger),
17300 physical_now.signed_units, std::abs(view.inspected_opened_units)))) {
17301 if (close_first_admitted) return NativePrecommitVerdict::AdmitWithHostMargin;
17302 const double quote = finite_positive(view.raw_price) ? view.raw_price : view.resolved_price;
17303 return unified_fill_admits(source, std::abs(view.inspected_opened_units), quote)
17304 ? NativePrecommitVerdict::AdmitWithHostMargin : NativePrecommitVerdict::Refuse;
17305 }
17306 const double placed_reversal_units = finite_positive(source.projection_affordability_held_qty)
17307 ? source.projection_affordability_held_qty : source.projection_tv_carry_qty;
17308 if (source.family == PineOrderFamily::Entry && source.reverse_to
17309 && std::holds_alternative<native_order::Stop>(view.definition->request.trigger)
17310 && physical_now.signed_units == 0.0
17311 && source.projection_position_side != static_cast<std::int32_t>(PositionSide::FLAT)
17312 && (source.projection_position_side == static_cast<std::int32_t>(PositionSide::LONG))
17313 != source.is_long
17314 && finite_positive(placed_reversal_units)
17315 && finite_positive(source.sizing.equity)) {
17316 const double own_units = std::abs(view.inspected_opened_units)
17317 - placed_reversal_units;
17318 const double margin_pct = source.is_long ? config_.margin_long : config_.margin_short;
17319 const double required = own_units * view.resolved_price * staged_.syminfo.pointvalue
17320 * active_staged_fx(view.cursor.point.effective_time_ms) * margin_pct / 100.0;
17321 const double guard = std::max(1e-9, std::abs(source.sizing.equity) * 1e-12);
17322 if (std::abs(margin_pct - 100.0) < 1e-12
17323 && finite_positive(own_units) && std::isfinite(required)
17324 && required <= source.sizing.equity + guard) {
17325 return NativePrecommitVerdict::AdmitWithHostMargin;
17326 }
17327 }
17328 // Entries a bar placed while the book was flat all fill where they reach
17329 // their fill point, the later ones behind a book of their own side that
17330 // only the earlier ones opened: TradingView judges each on its own cost
17331 // against the equity it was sized from, as it judged the first, and
17332 // margin-calls the combined book once they have all filled (lab tv tapes
17333 // tests/fixtures/margin_residual w5b-sb-market-60x2, -pooc-60x2,
17334 // -default-x3 and -pooc-default-x3: every cell's entries fill, and the
17335 // book is called at the fill, or at the next open under
17336 // process_orders_on_close; lane W5B-ENG-MARGIN-RESIDUAL).
17337 if (flat_sibling_book(source, view.cursor.point.interval_index,
17338 view.cursor.point.path_phase)) {
17339 if (config_.pyramiding > 0
17340 && physical_now.lot_count >= static_cast<std::size_t>(config_.pyramiding)) {
17341 return NativePrecommitVerdict::Refuse;
17342 }
17343 const double margin_pct = source.is_long ? config_.margin_long : config_.margin_short;
17344 if (!(margin_pct > 0.0) || !std::isfinite(margin_pct))
17345 return NativePrecommitVerdict::AdmitWithHostMargin;
17346 const double fx = active_staged_fx(view.cursor.point.effective_time_ms);
17347 const double own_units = finite_positive(source.sizing.frozen_units)
17348 && !std::isfinite(source.requested_qty)
17349 ? source.sizing.frozen_units : std::abs(view.inspected_opened_units);
17350 const double unit_margin = view.resolved_price * staged_.syminfo.pointvalue * fx
17351 * margin_pct / 100.0;
17352 const double equity = finite_positive(source.sizing.equity)
17353 ? source.sizing.equity : view.account.marked_equity;
17354 const double guard = std::max(
17355 {1e-9, std::abs(equity) * 1e-12,
17356 staged_.quantity_grid ? *staged_.quantity_grid * unit_margin : 0.0});
17357 if (!finite_positive(own_units) || !std::isfinite(unit_margin)
17358 || !std::isfinite(equity) || own_units * unit_margin > equity + guard) {
17359 return NativePrecommitVerdict::Refuse;
17360 }
17361 return NativePrecommitVerdict::AdmitWithHostMargin;
17362 }
17363
17364 // Under process_orders_on_close a default percent_of_equity (at most 100)
17365 // stop entry placed from flat and not yet marketable is sized at its
17366 // level. When a later bar opens through that level, TradingView fills it
17367 // at the open only if the equity it was sized from pays for its units
17368 // there; otherwise it refuses the order, which is then gone (lane
17369 // W10-DIAG-UNKNOWN rule GAPSTOP, tests/fixtures/pooc_gap_stop). A fill at
17370 // the level itself costs no more than that equity by the sizing's floor.
17371 const double gap_stop_slip = (source.is_long ? 1.0 : -1.0) * config_.slippage
17372 * staged_.syminfo.mintick;
17373 if (source.family == PineOrderFamily::Entry && config_.process_orders_on_close
17374 && std::holds_alternative<native_order::Stop>(view.definition->request.trigger)
17375 && !std::isfinite(source.requested_qty)
17376 && config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
17377 && config_.default_qty_value <= 100.0 && !source.sizing.at_fill
17378 && source.projection_position_side == static_cast<std::int32_t>(PositionSide::FLAT)
17379 && physical_now.signed_units == 0.0
17380 && finite_positive(source.sizing.frozen_units)
17381 && finite_positive(source.sizing.equity) && finite_positive(source.exit_levels.stop)
17382 && source.sizing.price == source.exit_levels.stop + gap_stop_slip
17383 && (source.is_long ? view.resolved_price > source.sizing.price
17384 : view.resolved_price < source.sizing.price)) {
17385 const double margin_pct = source.is_long ? config_.margin_long : config_.margin_short;
17386 const double cost = source.sizing.frozen_units * view.resolved_price
17387 * staged_.syminfo.pointvalue * source.sizing.fx * margin_pct / 100.0;
17388 const double epsilon = std::max(1e-9, std::abs(source.sizing.equity) * 1e-12);
17389 if (!std::isfinite(cost) || cost > source.sizing.equity + epsilon)
17390 return NativePrecommitVerdict::Refuse;
17391 }
17392
17393 // ab9714be pine_policy_members.cpp:153-210 and pine_fills.cpp:5627-5644:
17394 // a same-bar process-on-close long uses ten-significant-digit signal money
17395 // and the slipped signal threshold. This is the source host's complete
17396 // margin decision for the candidate; a genuine post-fill shortfall is
17397 // admitted and becomes the observable opening-margin event.
17398 const auto native_state = detail::run_state(require_host());
17399 const bool pooc_default_all_in = !std::isfinite(source.requested_qty)
17400 && config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
17401 && config_.default_qty_value == 100.0
17402 && finite_positive(source.sizing.frozen_units)
17403 && finite_positive(source.sizing.equity) && source.sizing.fx == 1.0;
17404 const bool pooc_explicit_fixed = finite_positive(source.requested_qty)
17405 && (source.qty_type < 0 || source.qty_type == static_cast<int>(QtyType::FIXED))
17406 && finite_positive(source.projection_affordability_equity)
17407 && source.projection_affordability_held_qty == 0.0;
17408 const bool pooc_money_scope = config_.process_orders_on_close
17409 && source.family == PineOrderFamily::Entry
17410 && std::holds_alternative<native_order::Market>(view.definition->request.trigger)
17411 && source.is_long && source.opening
17412 && source.projection_created_bar == projection_bar_index(view.cursor.point)
17413 && source.projection_position_side == static_cast<std::int32_t>(PositionSide::FLAT)
17414 && !source.projection_after_close && !source.birth.from_fill()
17415 && source.projection_predecessor == 0 && !source.replaced_opening
17416 && source.oca_name.empty() && source.oca_type == 0
17417 && physical_now.signed_units == 0.0
17418 && config_.pyramiding >= 0 && config_.pyramiding <= 1
17419 && config_.margin_long == 100.0 && config_.commission_value == 0.0
17420 && config_.slippage >= 0 && finite_positive(staged_.syminfo.mintick)
17421 && staged_.quantity_grid && *staged_.quantity_grid > 0.0
17422 && *staged_.quantity_grid < 1.0
17423 && staged_.syminfo.pointvalue == 1.0 && staged_.account_fx == 1.0
17424 && source.sizing.fx == 1.0 && staged_.account_fx_effective_from_ms.empty()
17425 && (!native_state.spec || native_state.spec->intrabar.is_none())
17426 && !cap.active()
17427 && risk_.max_intraday_loss <= 0.0 && risk_.max_drawdown <= 0.0
17428 && risk_.max_cons_loss_days == 0
17429 && (pooc_default_all_in || pooc_explicit_fixed);
17430 if (pooc_money_scope) {
17431 const double units = pooc_default_all_in
17432 ? source.sizing.frozen_units : source.requested_qty;
17433 const double equity = pooc_default_all_in
17434 ? source.sizing.equity : source.projection_affordability_equity;
17435 const double signal = pooc_default_all_in
17436 ? source.sizing.mark : source.projection_affordability_signal_price;
17437 const double admission_price = pooc_default_all_in
17438 ? source.sizing.price
17439 : nearest_tick(signal + config_.slippage * staged_.syminfo.mintick,
17440 staged_.syminfo.mintick);
17441 const double rounded_cost = source_money_round(units * signal);
17442 const double affordable_price = source_money_round(
17443 source_money_round(equity) / units);
17444 if (!finite_positive(units) || !finite_positive(equity)
17445 || !finite_positive(signal) || !finite_positive(admission_price)
17446 || equity + 1e-9 < rounded_cost
17447 || (std::isfinite(affordable_price)
17448 && affordable_price < admission_price)) {
17449 return NativePrecommitVerdict::Refuse;
17450 }
17451 return NativePrecommitVerdict::AdmitWithHostMargin;
17452 }
17453 if (source.family == PineOrderFamily::Entry && source.affordability_policy_active) {
17454 const double margin_pct = source.is_long ? config_.margin_long : config_.margin_short;
17455 const double fx = active_staged_fx(view.cursor.point.effective_time_ms);
17456 const auto physical = detail::run_position(require_host());
17457 // ab9714be pine_fills.cpp:5560-5622: an ordinary explicit fixed
17458 // MARKET opening at fractional-lot resolution first passes the
17459 // rounded-money signal-cost and affordable-price checks. This is a
17460 // source broker precommit policy; the configured default sizing mode
17461 // does not participate in its decision.
17462 const double explicit_units = std::abs(view.inspected_opened_units);
17463 const bool explicit_money_scope =
17464 std::holds_alternative<native_order::Market>(view.definition->request.trigger)
17465 && std::isfinite(source.requested_qty) && source.requested_qty > 0.0
17466 && (source.qty_type < 0
17467 || source.qty_type == static_cast<int>(QtyType::FIXED))
17468 && physical.signed_units == 0.0
17469 && source.projection_position_side
17470 == static_cast<std::int32_t>(PositionSide::FLAT)
17471 && !source.projection_after_close && source.projection_predecessor == 0
17472 && source.oca_type == 0 && source.oca_name.empty()
17473 && projection_bar_index(view.cursor.point) == source.projection_created_bar + 1
17474 && live_handles_.size() == 1
17475 && config_.margin_long == 100.0 && config_.margin_short == 100.0
17476 && staged_.quantity_grid && *staged_.quantity_grid > 0.0
17477 && *staged_.quantity_grid < 1.0
17478 && staged_.syminfo.pointvalue == 1.0 && source.sizing.fx == 1.0
17479 && staged_.account_fx == 1.0
17480 && staged_.account_fx_effective_from_ms.empty()
17481 && config_.slippage == 0
17482 && config_.commission_type == static_cast<int>(CommissionType::PERCENT)
17483 && !config_.process_orders_on_close && !config_.calc_on_order_fills
17484 && config_.pyramiding >= 0 && config_.pyramiding <= 1
17485 && !cap.active() && risk_.max_intraday_loss <= 0.0
17486 && risk_.max_drawdown <= 0.0 && risk_.max_cons_loss_days == 0
17487 && finite_positive(source.projection_affordability_equity)
17488 && finite_positive(source.projection_affordability_signal_price)
17489 && finite_positive(explicit_units)
17490 && *staged_.quantity_grid * source.projection_affordability_signal_price < 1.0;
17491 if (explicit_money_scope) {
17492 const double equity = source.projection_affordability_equity;
17493 const double signal = source.projection_affordability_signal_price;
17494 const double rounded_cost = source_money_round(explicit_units * signal);
17495 const double affordable_price = source_money_round(
17496 source_money_round(equity) / explicit_units);
17497 if (equity + 1e-9 < rounded_cost
17498 || (std::isfinite(affordable_price) && affordable_price < signal)) {
17499 return NativePrecommitVerdict::Refuse;
17500 }
17501 }
17502 const bool same_side = physical.signed_units != 0.0
17503 && ((physical.signed_units > 0.0) == source.is_long);
17504 const bool reversal = physical.signed_units != 0.0
17505 && ((physical.signed_units > 0.0) != source.is_long);
17506 double units = same_side
17507 ? std::abs(view.account.resulting_abs_notional)
17508 / (view.resolved_price * staged_.syminfo.pointvalue * fx)
17509 : std::abs(view.inspected_opened_units);
17510 // TradingView judges a MARKET entry filled at the open after the bar
17511 // that placed it against the position held at its placement, not
17512 // against a sibling entry that filled at the same open a moment
17513 // earlier: two entries that each fit the equity alone both fill, and
17514 // the excess is margin-called at the fill, first in first out (lane
17515 // W10-DIAG-UNKNOWN rule COQ, tests/fixtures/coqueued_open_margin).
17516 // Units held beyond the placement's own held quantity can only be
17517 // such same-open fills, so only they leave the requirement. An entry
17518 // the open trades already hold at the pyramiding cap is TradingView's
17519 // to refuse at this check (H-MEASURE's first-eligible-point count),
17520 // so it keeps the whole-book requirement. Under
17521 // process_orders_on_close the adds a bar placed fill at its close and
17522 // are judged alike, each beside the position held when it was placed
17523 // (lab tv tapes tests/fixtures/add_sibling_margin tailb-adds-pooc-*;
17524 // lane TAIL-B).
17525 const auto placed_side = static_cast<PositionSide>(source.projection_position_side);
17526 const double placed_held = source.projection_affordability_held_qty;
17527 const bool same_point_as_placed =
17528 projection_bar_index(view.cursor.point) == source.projection_created_bar + 1
17529 || (config_.process_orders_on_close && !config_.calc_on_order_fills
17530 && projection_bar_index(view.cursor.point) == source.projection_created_bar
17531 && view.cursor.point.path_phase == NativePathPhase::Close);
17532 if (same_side
17533 && std::holds_alternative<native_order::Market>(view.definition->request.trigger)
17534 && (placed_side == PositionSide::FLAT
17535 || (placed_side == PositionSide::LONG) == source.is_long)
17536 && std::isfinite(placed_held) && placed_held >= 0.0
17537 && same_point_as_placed
17538 && std::abs(physical.signed_units) > placed_held + 1e-10
17539 && config_.pyramiding > 0
17540 && physical.lot_count < static_cast<std::size_t>(config_.pyramiding)) {
17541 units = placed_held + std::abs(view.inspected_opened_units);
17542 }
17543 // Margin rule 2 at a next-bar fill: an explicit quantity market long
17544 // that opens from flat at full margin is dropped only when the equity
17545 // is below sig10(Q x the fill's UNSLIPPED quote); its slippage was
17546 // judged at placement, on the price scale (tests/fixtures/
17547 // margin_call_rules admit-eth-nonpooc-s2-m04u; synthetic 2.5 at the 200.11
17548 // open fits 500.32499996 though its slipped 200.13 does not;
17549 // admit-eth-nonpooc-s2-gapup: a gap-up open whose unslipped
17550 // cost exceeds the equity drops). Pinned with slippage only: without
17551 // it TradingView fills such an entry and calls it at the open
17552 // (test_high_value_fractional_money_margin_l4a's capital controls).
17553 // Inside the unified admission's scope the fill re-check replaces it
17554 // (margin_call_rules factorial/c1-a3-exp-long-s1-gapup1): with
17555 // fill_price_recheck on, that check has already decided such an order
17556 // above and this one is never reached. It stays for the orders outside
17557 // that scope (a commission, pyramiding above one, another order
17558 // resting, a risk rule), which no tape decides.
17559 if (PF_RARE(detail::margin_rule_switches().slipped_signal_admission
17560 && source.family == PineOrderFamily::Entry && margin_pct == 100.0
17561 && source.is_long && !config_.process_orders_on_close && config_.slippage > 0
17562 && std::holds_alternative<native_order::Market>(view.definition->request.trigger)
17563 && std::isfinite(source.requested_qty)
17564 && (source.qty_type < 0 || source.qty_type == static_cast<int>(QtyType::FIXED))
17565 && source.projection_position_side == static_cast<std::int32_t>(PositionSide::FLAT)
17566 && physical.signed_units == 0.0 && view.inspected_closed_units == 0.0
17567 && staged_.syminfo.pointvalue == 1.0 && fx == 1.0 && source.sizing.fx == 1.0
17568 && staged_.account_fx_effective_from_ms.empty()
17569 && finite_positive(staged_.syminfo.mintick) && finite_positive(view.resolved_price)
17570 && std::isfinite(source.sizing.equity) && finite_positive(units))) {
17571 const double tick = staged_.syminfo.mintick;
17572 // The fill's own price decides (switch open_fill_admission): the
17573 // order is dropped where sig10(sig10(E) / Q) is below the slipped
17574 // open tick it fills at. Synthetic BINANCE:ETHUSDT.P at slippage 2 and
17575 // commission 0.05 %: a long of 4 placed on 1000.04 fills
17576 // at no price above 250.01, and the 250.00 open fills at
17577 // 250.02: dropped; 0.05 more cash admits it (lab tv tapes
17578 // tests/fixtures/margin_open_rules admission/). A tie on the ten
17579 // significant digits admits: synthetic 2.5 at the 200.11 open, filled at
17580 // 200.13, on 500.32499996 (admit-eth-nonpooc-s2-m04u).
17581 if (PF_RARE(detail::margin_opening_switches().open_fill_admission)) {
17582 const double exec = std::floor(view.resolved_price / tick + 0.5) * tick;
17583 const double per_unit = source_money_round(
17584 source_money_round(source.sizing.equity) / units);
17585 return per_unit < exec
17586 ? NativePrecommitVerdict::Refuse : NativePrecommitVerdict::AdmitWithHostMargin;
17587 }
17588 const double quote = (std::floor(view.resolved_price / tick + 0.5)
17589 - (source.is_long ? 1.0 : -1.0) * config_.slippage) * tick;
17590 return source.sizing.equity < source_money_round(units * quote)
17591 ? NativePrecommitVerdict::Refuse : NativePrecommitVerdict::AdmitWithHostMargin;
17592 }
17593 // A same-side add at full margin is judged as the whole book at the
17594 // tick-built signal close, not at its slipped fill, against the
17595 // fee-charged equity there, with and without process_orders_on_close
17596 // (switch add_signal_close): on NYSE:F 1D at slippage 3 and commission 0.1 %
17597 // synthetic 200 shares plus an add of 10 cost 5250.00 at a 25.00 signal
17598 // close and fit fee-charged equity of 5253.00, where the 25.03 fill's
17599 // 5256.30 does not (lab tv tapes tests/fixtures/margin_schedule_rules
17600 // add/ establish the signal-close rule).
17601 const double add_price = PF_RARE(same_side && source.family == PineOrderFamily::Entry
17602 && detail::margin_schedule_switches().add_signal_close && margin_pct == 100.0
17603 && finite_positive(source.projection_affordability_signal_price))
17604 ? source.projection_affordability_signal_price : view.resolved_price;
17605 const double required = units * add_price * staged_.syminfo.pointvalue * fx
17606 * margin_pct / 100.0;
17607 // The placement tuple deliberately excludes the prospective opening
17608 // commission. Use its source-time MTM equity for fixed/cash/explicit
17609 // affordability instead of the native post-open projection.
17610 double equity = reversal
17611 ? view.account.marked_equity
17612 : (finite_positive(source.sizing.equity)
17613 ? source.sizing.equity : view.account.marked_equity);
17614 const bool pooc_slipped_signal = config_.process_orders_on_close
17615 && source.projection_created_bar == projection_bar_index(view.cursor.point)
17616 && source.projection_position_side
17617 == static_cast<std::int32_t>(PositionSide::FLAT)
17618 && physical.signed_units == 0.0
17619 && std::holds_alternative<native_order::Market>(
17620 view.definition->request.trigger)
17621 && finite_positive(source.projection_affordability_signal_price);
17622 if (pooc_slipped_signal) {
17623 const double signal_fill = nearest_tick(
17624 source.projection_affordability_signal_price
17625 + (source.is_long ? 1.0 : -1.0) * config_.slippage
17626 * staged_.syminfo.mintick,
17627 staged_.syminfo.mintick);
17628 const double signal_threshold = units * signal_fill
17629 * staged_.syminfo.pointvalue * fx * margin_pct / 100.0;
17630 if (std::isfinite(signal_threshold))
17631 equity = std::max(equity, signal_threshold);
17632 }
17633 const double epsilon = std::max(1e-9, std::abs(equity) * 1e-12);
17634 if (!(margin_pct > 0.0) || !std::isfinite(margin_pct)) {
17635 return NativePrecommitVerdict::AdmitWithHostMargin;
17636 }
17637 // ab9714be pine_fills.cpp:5564-5663: a MARKET reversal is costed as its
17638 // own new side at max(tick(close(S)), tick(fill)) against the PLACEMENT
17639 // equity snapshot, fees excluded. The post-close marked book already
17640 // carries the reversal's closing fee and the carried side's gap, so it
17641 // declined fee-only shortfalls that the owner admits and then trims at
17642 // the fill (pine_fills.cpp:5999-6005, 1386-1482).
17643 const bool market_reversal = reversal
17644 && std::holds_alternative<native_order::Market>(view.definition->request.trigger)
17645 && finite_positive(source.projection_affordability_equity)
17646 && finite_positive(source.projection_affordability_signal_price);
17647 if (market_reversal) {
17648 const double raw_fill = finite_positive(view.raw_price)
17649 ? view.raw_price : view.resolved_price;
17650 const double admit_price = std::max(
17651 source.projection_affordability_signal_price,
17652 nearest_tick(raw_fill, staged_.syminfo.mintick));
17653 const double placement_equity = source.projection_affordability_equity;
17654 const double reversal_required = units * admit_price
17655 * staged_.syminfo.pointvalue * fx * margin_pct / 100.0;
17656 const double guard = std::max(1e-9, std::abs(placement_equity) * 1e-12);
17657 if (!std::isfinite(reversal_required)
17658 || reversal_required > placement_equity + guard) {
17659 return NativePrecommitVerdict::Refuse;
17660 }
17661 return NativePrecommitVerdict::AdmitWithHostMargin;
17662 }
17663 if (!std::isfinite(required) || !std::isfinite(equity) || required > equity + epsilon) {
17664 return NativePrecommitVerdict::Refuse;
17665 }
17666 return NativePrecommitVerdict::AdmitWithHostMargin;
17667 }
17668 const bool exit = source.family == PineOrderFamily::ExitLimit
17670 if (exit) {
17671 // A bound exit whose selected scope has no remaining physical units is
17672 // a stale source leg, not a zero-quantity trade. The legacy pending
17673 // book removed that sibling before settlement; refusing the native
17674 // candidate preserves the same observable trade roster.
17675 if (!view.account.would_open && !(view.inspected_closed_units > 0.0))
17676 return NativePrecommitVerdict::Refuse;
17677 if (PF_RARE(((std::isfinite(source.requested_qty) && source.requested_qty > 0.0)
17678 || (std::isfinite(source.qty_percent) && source.qty_percent < 100.0 - 1e-9))
17679 && exit_tombstoned(source.source_id, source.from_entry))) {
17680 return NativePrecommitVerdict::Refuse;
17681 }
17682 const auto& bounds = source.leg_activation.bounds();
17683 const bool stop_leg = source.family == PineOrderFamily::ExitStop;
17684 const bool limit_leg = source.family == PineOrderFamily::ExitLimit;
17685 if (bounds && current_position_cycle_ > 0) {
17686 const bool ready = stop_leg
17687 ? source.leg_activation.stop_ready(current_position_cycle_, view.cursor.point.interval_index)
17688 : (limit_leg ? source.leg_activation.limit_ready(
17689 current_position_cycle_, view.cursor.point.interval_index) : true);
17690 if (!ready) return NativePrecommitVerdict::Refuse;
17691 }
17692 if (source.legs.retired(exit_legs::Leg::Stop)
17693 && source.legs.retired(exit_legs::Leg::Limit)
17694 && source.legs.retired(exit_legs::Leg::Trail)) {
17695 return NativePrecommitVerdict::Refuse;
17696 }
17697 }
17698 if (source.family == PineOrderFamily::Entry && source.reverse_to
17699 && !source.projection_after_close
17700 && config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
17701 && config_.default_qty_value >= 100.0
17702 && finite_positive(source.sizing.frozen_units)) {
17703 // pine_fills.cpp:5497-5519 prices an all-in reversal against the
17704 // source-time frozen equity even under COOF. The native marked book
17705 // includes the carried side's gap PnL and would otherwise admit the
17706 // +1-gap reversal that the source broker declines.
17707 const double margin = source.is_long ? config_.margin_long : config_.margin_short;
17708 const double fx = active_staged_fx(view.cursor.point.effective_time_ms);
17709 const double required = source.sizing.frozen_units * view.resolved_price
17710 * staged_.syminfo.pointvalue * fx * margin / 100.0;
17711 const double equity = source.sizing.equity;
17712 const double guard = std::max(1e-9, std::abs(equity) * 1e-12);
17713 const bool nested_price_gap_scope = margin == 100.0
17714 && std::holds_alternative<native_order::Market>(view.definition->request.trigger)
17715 && finite_positive(staged_.syminfo.mintick)
17716 && staged_.quantity_grid && *staged_.quantity_grid > 0.0
17717 && *staged_.quantity_grid < 1.0
17718 && staged_.syminfo.pointvalue == 1.0 && source.sizing.fx == 1.0
17719 && staged_.account_fx_effective_from_ms.empty()
17720 && config_.commission_type == static_cast<int>(CommissionType::PERCENT)
17721 && config_.commission_value == 0.0 && config_.slippage == 0
17722 && !config_.process_orders_on_close && !config_.calc_on_order_fills
17723 && view.resolved_price > source.sizing.price;
17724 const bool nested_price_gap_affordable = nested_price_gap_scope
17725 && source_price_band_affordable(equity, source.sizing.frozen_units,
17726 view.resolved_price, staged_.syminfo.mintick);
17727 if (!std::isfinite(required) || !std::isfinite(equity)
17728 || (nested_price_gap_scope && !nested_price_gap_affordable)
17729 || (required > equity + guard && !nested_price_gap_affordable)) {
17730 return NativePrecommitVerdict::Refuse;
17731 }
17732 }
17733 const bool variable_default = config_.default_qty_type != static_cast<int>(QtyType::FIXED);
17734 // The pyramiding cap: a same-side add is refused at its fill once the open
17735 // close-ledger records reach pyramiding, whatever its quantity type; a
17736 // LIMIT entry the cap admitted at placement fills however many are open.
17737 const auto fill_records = open_ledger_records();
17738 if (PF_RARE(fill_records && source.family == PineOrderFamily::Entry && source.opening
17739 && config_.pyramiding > 0 && view.inspected_closed_units == 0.0
17740 && !pure_limit_entry(source))) {
17741 // The position at the fill, read only where the cap can decide.
17742 const double held_at_fill = detail::run_position(require_host()).signed_units;
17743 if (held_at_fill != 0.0 && ((held_at_fill > 0.0) == source.is_long)
17744 && *fill_records >= static_cast<std::size_t>(config_.pyramiding)) {
17745 return NativePrecommitVerdict::Refuse;
17746 }
17747 }
17748 // A LIMIT entry the cap admitted when it was placed fills however many
17749 // lots are open by then (lane W8A-SIGSTATE-1 R-B,
17750 // tests/fixtures/pyramiding_open_order).
17751 if (variable_default && !source.frozen_market_instruction && config_.pyramiding > 0
17752 && view.inspected_closed_units == 0.0
17753 && !(source.family == PineOrderFamily::Entry && pure_limit_entry(source))
17754 && (fill_records ? *fill_records : detail::run_position(require_host()).lot_count)
17755 >= static_cast<std::size_t>(config_.pyramiding)) {
17756 return NativePrecommitVerdict::Refuse;
17757 }
17758 const double margin_pct = view.account.incoming_short
17759 ? config_.margin_short : config_.margin_long;
17760 if (!(margin_pct > 0.0) || !std::isfinite(margin_pct))
17761 return NativePrecommitVerdict::AdmitWithHostMargin;
17762 const double fraction = margin_pct / 100.0;
17763 if (finite_positive(source.sizing.frozen_units) && !source.sizing.at_fill) {
17764 const double frozen_required = std::abs(source.sizing.frozen_units)
17765 * source.sizing.price * staged_.syminfo.pointvalue * source.sizing.fx * fraction;
17766 // ab9714be pine_fills.cpp:4814-4897: a reversal rechecks its frozen
17767 // quantity at the actual fill price. A lower fill can therefore admit
17768 // a signal tuple that is fractionally over budget; flat/same-side
17769 // openings still require their placement tuple to be valid.
17770 if (!std::isfinite(frozen_required) || !std::isfinite(source.sizing.equity)) {
17771 return NativePrecommitVerdict::Refuse;
17772 }
17773 const auto state = detail::run_state(require_host());
17774 const bool live_slippage_changed = state.spec
17775 && config_.slippage != static_cast<int>(state.spec->slippage_ticks);
17776 const bool placement_sized_stop = source.family == PineOrderFamily::Entry
17777 && finite_positive(source.exit_levels.stop)
17778 && !finite_positive(source.exit_levels.limit)
17779 && !std::isfinite(source.requested_qty);
17780 if (placement_sized_stop && live_slippage_changed) {
17781 // The adapter owns this admission (the placement tuple was frozen
17782 // at a different slippage); say so, or the run spec's own per-side
17783 // initial fraction would decline the opening TradingView takes.
17784 return NativePrecommitVerdict::AdmitWithHostMargin;
17785 }
17786 // The frozen tuple protects a rate rollover (the FX opening checkpoint
17787 // owns that later adjustment), but an ordinary price gap is still
17788 // rechecked at the fill just as the legacy admission path does.
17789 const double active_fx = active_staged_fx(view.cursor.point.effective_time_ms);
17790 if (active_fx == source.sizing.fx) {
17791 double fill_required = view.account.resulting_abs_notional * fraction;
17792 const auto physical = detail::run_position(require_host());
17793 const bool reversal = physical.signed_units != 0.0
17794 && ((physical.signed_units > 0.0) != source.is_long);
17795 // A MARKET entry placed while flat or on its own side that reverses
17796 // the position an earlier market entry of its bar opened at this
17797 // same open is judged by TradingView against its placement equity
17798 // with that position still margined: its own side plus the held
17799 // side must fit, or it is refused and the held position stays
17800 // (lane W10-DIAG-UNKNOWN rule SAMEOPEN-REV,
17801 // tests/fixtures/same_open_reversal). With slippage TradingView
17802 // runs such a reversal close-only instead (the -slip tape), which
17803 // this rule does not model. An entry placed on its own side is
17804 // held to the book its tapes pin, the bar's market entries and
17805 // unpriced closes: a priced or raw order, or one carried in from
17806 // an earlier bar, leaves it to the ordinary reversal, as the
17807 // live-position rule of test_live_position_market_gross_admission
17808 // does.
17809 const auto bare_same_bar_book = [&] {
17810 if (!delayed_market_orders_.empty()) return false;
17811 for (const auto& handle : live_handles_) {
17812 if (handle == view.target) continue;
17813 const auto found = placement_.find(handle.incarnation);
17814 if (found == placement_.end()) continue;
17815 const auto& row = found->second;
17816 if (row.family == PineOrderFamily::Order) return false;
17817 if (row.family != PineOrderFamily::Entry) continue;
17818 if (finite_positive(row.exit_levels.limit)
17819 || finite_positive(row.exit_levels.stop)
17820 || row.projection_created_bar != source.projection_created_bar)
17821 return false;
17822 }
17823 return true;
17824 };
17825 const bool same_open_reversal = reversal
17826 && source.family == PineOrderFamily::Entry
17827 && std::holds_alternative<native_order::Market>(
17828 view.definition->request.trigger)
17829 && (source.projection_position_side
17830 == static_cast<std::int32_t>(PositionSide::FLAT)
17831 || (source.projection_position_side == static_cast<std::int32_t>(
17833 && bare_same_bar_book()))
17834 && !source.projection_after_close
17835 && projection_bar_index(view.cursor.point) == source.projection_created_bar + 1
17836 && position_open_bar_index_ == view.cursor.point.interval_index
17837 && !position_open_priced_ && config_.slippage == 0
17838 && !config_.process_orders_on_close && !config_.calc_on_order_fills
17839 ;
17840 if (same_open_reversal) {
17841 const double held_fraction = (physical.signed_units > 0.0
17842 ? config_.margin_long : config_.margin_short) / 100.0;
17843 fill_required += std::abs(physical.signed_units) * view.resolved_price
17844 * staged_.syminfo.pointvalue * active_fx * held_fraction;
17845 }
17846 const bool variable_batch = source.frozen_market_instruction
17847 && (config_.default_qty_type == static_cast<int>(QtyType::CASH)
17848 || (config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
17849 && config_.default_qty_value < 100.0));
17850 // An all-in source reversal remains admitted against the source
17851 // call snapshot. Re-marking the carried side at a later gap
17852 // would manufacture buying power that the legacy precommit did
17853 // not grant (the ShortSeed all-in rejection controls).
17854 const bool all_in_reversal = reversal
17855 && config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
17856 && config_.default_qty_value >= 100.0;
17857 const bool close_then_open_margin_checkpoint = all_in_reversal
17858 && source.projection_after_close && margin_pct == 100.0
17859 && std::holds_alternative<native_order::Market>(
17860 view.definition->request.trigger);
17861 // ab9714be pine_fills.cpp:5288-5390 (gap-reject), :5392-5526
17862 // (frozen sizing) and engine_fills.cpp:4618
17863 // (stop_entry_margin_admission_declines): a TRUE-FLAT all-in
17864 // default percent_of_equity==100 opening is costed against the
17865 // PRE-commission placement equity snapshot. Commission is
17866 // EXCLUDED from fill-time affordability -- a fee-only (or one-tick
17867 // grid-rounding) overage admits here and the entry-bar
17868 // margin-call trim downstream books the observable residual lot
17869 // that closes again on the entry bar. Judging the same opening
17870 // against the post-commission marked equity instead declines the
17871 // whole entry and loses both lots.
17872 const bool all_in_true_flat_opening = !reversal
17873 && source.family == PineOrderFamily::Entry
17874 && !std::isfinite(source.requested_qty)
17875 && physical.signed_units == 0.0
17876 && source.projection_position_side
17877 == static_cast<std::int32_t>(PositionSide::FLAT)
17878 && !source.projection_after_close
17879 && config_.default_qty_type
17880 == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
17881 && std::abs(config_.default_qty_value - 100.0) < 1e-12
17882 && std::abs(margin_pct - 100.0) < 1e-12
17883 && finite_positive(source.sizing.equity);
17884 const double fill_equity = (variable_batch || all_in_reversal
17885 || all_in_true_flat_opening || same_open_reversal)
17886 ? source.sizing.equity : view.account.marked_equity;
17887 const double float_guard = std::max(
17888 1e-9, std::abs(source.sizing.equity) * 1e-12);
17889 bool paired_market_opening = false;
17890 for (const auto& handle : live_handles_) {
17891 if (handle == view.target) continue;
17892 const auto peer = placement_.find(handle.incarnation);
17893 if (peer == placement_.end()) continue;
17894 const auto& row = peer->second;
17895 if (row.opening && row.family == PineOrderFamily::Entry
17896 && row.placement_script_open_ms == source.placement_script_open_ms
17897 && !finite_positive(row.exit_levels.limit)
17898 && !finite_positive(row.exit_levels.stop)) {
17899 paired_market_opening = true;
17900 break;
17901 }
17902 }
17903 const auto native_state = detail::run_state(require_host());
17904 const bool magnified = native_state.spec
17905 && !native_state.spec->intrabar.is_none();
17906 const bool price_gap_scope = source.family == PineOrderFamily::Entry
17907 && std::holds_alternative<native_order::Market>(view.definition->request.trigger)
17908 && finite_positive(staged_.syminfo.mintick)
17909 && !std::isfinite(source.requested_qty)
17910 && config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
17911 && config_.default_qty_value == 100.0
17912 && std::abs((source.is_long ? config_.margin_long : config_.margin_short)
17913 - 100.0) < 1e-12
17914 && staged_.quantity_grid && *staged_.quantity_grid > 0.0
17915 && *staged_.quantity_grid < 1.0
17916 && staged_.syminfo.pointvalue == 1.0 && source.sizing.fx == 1.0
17917 && staged_.account_fx_effective_from_ms.empty()
17918 && config_.commission_type == static_cast<int>(CommissionType::PERCENT)
17919 && config_.commission_value == 0.0 && config_.slippage == 0
17920 && !config_.process_orders_on_close && !config_.calc_on_order_fills
17921 && !magnified && !source.birth.from_fill()
17922 && !source.projection_after_close
17923 && view.resolved_price > source.sizing.price
17924 && ((physical.signed_units == 0.0
17925 && source.projection_position_side
17926 == static_cast<std::int32_t>(PositionSide::FLAT)
17927 && !paired_market_opening)
17928 || reversal);
17929 const bool price_gap_affordable = price_gap_scope
17930 && source_price_band_affordable(source.sizing.equity,
17931 source.sizing.frozen_units, view.resolved_price, staged_.syminfo.mintick);
17932 if (price_gap_scope && !price_gap_affordable)
17933 return NativePrecommitVerdict::Refuse;
17934 const bool true_flat_gap_scope = source.family == PineOrderFamily::Entry
17935 && source.projection_position_side
17936 == static_cast<std::int32_t>(PositionSide::FLAT)
17937 && !source.projection_after_close
17938 && physical.signed_units == 0.0
17939 && config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
17940 && config_.default_qty_value == 100.0
17941 && !config_.process_orders_on_close;
17942 if (true_flat_gap_scope
17943 && fill_required > source.sizing.equity + float_guard
17944 && !price_gap_affordable) {
17945 return NativePrecommitVerdict::Refuse;
17946 }
17947 if (source.family == PineOrderFamily::Entry
17948 && source.projection_after_close && physical.signed_units == 0.0) {
17949 // ab9714be pine_fills.cpp:4814-4897 / AG-C1: after an earlier
17950 // same-tick close, the new flat opening is admitted on its
17951 // frozen sizing notional. A worse fill is handled by the
17952 // post-opening margin slice, not by declining the entry.
17953 // AdmitWithHostMargin, not a bare admission: the adapter is
17954 // taking responsibility for this opening's margin check, and
17955 // the run spec now carries a per-side initial fraction that
17956 // would otherwise decline it.
17957 return NativePrecommitVerdict::AdmitWithHostMargin;
17958 }
17959 // A flat opening keeps one lot of slack for the floor its
17960 // quantity was sized on; an add to a held position of its side
17961 // keeps none, as it has none when it is placed
17962 // (add_margin_exceeds_equity; lab tv tape
17963 // tests/fixtures/margin_residual w5b-pa-f-pooc-p50, a
17964 // process_orders_on_close add judged at its close fill; lane
17965 // W5B-ENG-MARGIN-RESIDUAL).
17966 const bool same_side_add = physical.signed_units != 0.0
17967 && (physical.signed_units > 0.0) == source.is_long;
17968 // Such an add filled at the open after its bar is judged, as an
17969 // explicit one is (rule COQ above), against the position held
17970 // when it was placed: a sibling that filled at this open a moment
17971 // earlier does not count, so two default adds that each fit on
17972 // top of the held position both fill and the book is called at
17973 // the fill (lab tv tape tests/fixtures/int29_compositions
17974 // int29-c1-open-pairs, cells D and E; composing placement-time
17975 // equity checks with the same-side add's residual-margin check).
17976 const auto placed_side = static_cast<PositionSide>(source.projection_position_side);
17977 const double placed_held = source.projection_tv_carry_qty;
17978 if (same_side_add && source.family == PineOrderFamily::Entry
17979 && std::holds_alternative<native_order::Market>(view.definition->request.trigger)
17980 && (placed_side == PositionSide::FLAT
17981 || (placed_side == PositionSide::LONG) == source.is_long)
17982 && std::isfinite(placed_held) && placed_held >= 0.0
17983 && projection_bar_index(view.cursor.point) == source.projection_created_bar + 1
17984 && std::abs(physical.signed_units) > placed_held + 1e-10
17985 && config_.pyramiding > 0
17986 && physical.lot_count < static_cast<std::size_t>(config_.pyramiding)) {
17987 fill_required = (placed_held + std::abs(view.inspected_opened_units))
17988 * view.resolved_price * staged_.syminfo.pointvalue * active_fx * fraction;
17989 }
17990 double admission_guard = float_guard;
17991 if (!reversal && !same_side_add && staged_.quantity_grid) {
17992 admission_guard = std::max(admission_guard,
17993 *staged_.quantity_grid * view.resolved_price
17994 * staged_.syminfo.pointvalue * active_fx * fraction);
17995 }
17996 if (!std::isfinite(fill_required) || !std::isfinite(fill_equity)
17997 || (fill_required > fill_equity + admission_guard
17998 && (same_open_reversal || !price_gap_affordable)
17999 && !close_then_open_margin_checkpoint)) {
18000 return NativePrecommitVerdict::Refuse;
18001 }
18002 }
18003 return NativePrecommitVerdict::AdmitWithHostMargin;
18004 }
18005 const double required = view.account.resulting_abs_notional * fraction;
18006 // A 1x LONG opening may be admitted against its pre-entry realized
18007 // budget even when the entry commission makes the post-entry marked
18008 // equity fractionally short. The adapter immediately runs the
18009 // opening-price margin checkpoint from on_native_applied, which produces
18010 // the source-required 4x/one-contract reduction before any same-bar
18011 // exit. Rejecting here would erase that observable margin event.
18012 const bool opening_margin_checkpoint =
18013 source.family == PineOrderFamily::Entry && margin_pct == 100.0
18014 && finite_positive(source.requested_qty)
18015 && std::isfinite(view.account.realized_balance)
18016 && required <= view.account.realized_balance;
18017 if (opening_margin_checkpoint) return NativePrecommitVerdict::AdmitWithHostMargin;
18018 if (!std::isfinite(required) || !std::isfinite(view.account.marked_equity)
18019 || required > view.account.marked_equity) {
18020 return NativePrecommitVerdict::Refuse;
18021 }
18022 return NativePrecommitVerdict::AdmitWithHostMargin;
18023}
18024
18025namespace {
18026
18027constexpr std::int64_t kChartDayCivilSpan = std::int64_t{1} << 40;
18028
18029// chart_day_key on a UTC chart for a second `secs` inside the arithmetic's
18030// span: gmtime_r's day and month, the kernel's civil date of the floor day
18031// (native_calendar::native_civil_date). Everything below reads the floor day
18032// alone, so the day's seconds all answer its key, and `index` (when there is
18033// one) keeps them with it.
18034[[gnu::noinline]] std::int64_t civil_chart_day_key(AdapterLookupIndex* index,
18035 std::int64_t secs) noexcept {
18036 const std::int64_t days = secs / 86400 - (secs % 86400 < 0 ? 1 : 0);
18038 const std::int64_t key = std::int64_t{date.day} * 100 + date.month;
18039 if (index != nullptr) {
18040 index->day_lo = std::max(days * 86400, -kChartDayCivilSpan + 1);
18041 index->day_hi = std::min(days * 86400 + 86400, kChartDayCivilSpan);
18042 index->day_key = key;
18043 }
18044 return key;
18045}
18046
18047// chart_day_key's libc half, for a second the civil arithmetic does not answer:
18048// localtime_r under a chart timezone, gmtime_r on a UTC chart, each falling
18049// back to gmtime_r as it always did.
18050[[gnu::noinline]] std::int64_t libc_chart_day_key(const std::string& timezone, bool utc_chart,
18051 std::time_t seconds) noexcept {
18052 std::tm fields{};
18053 const auto utc = [&]() {
18054 return ::gmtime_r(&seconds, &fields) != nullptr;
18055 };
18056 if (utc_chart) {
18057 if (!utc()) return std::numeric_limits<std::int64_t>::min();
18058 } else {
18059 try {
18060 tz_util::ScopedTimezone guard(timezone);
18061 if (::localtime_r(&seconds, &fields) == nullptr) {
18062 if (!utc()) return std::numeric_limits<std::int64_t>::min();
18063 }
18064 } catch (...) {
18065 // A malformed staged timezone must not turn a source-policy
18066 // read into an unhashable partial state. The run-spec validator
18067 // owns rejection; preserve the legacy UTC fallback meanwhile.
18068 if (!utc()) return std::numeric_limits<std::int64_t>::min();
18069 }
18070 }
18071 return static_cast<std::int64_t>(fields.tm_mday) * 100
18072 + static_cast<std::int64_t>(fields.tm_mon + 1);
18073}
18074
18075} // namespace
18076
18077std::int64_t PineExecutionAdapter::chart_day_key(std::int64_t timestamp_ms) const noexcept {
18078 const std::time_t seconds = static_cast<std::time_t>(timestamp_ms / 1000);
18079 const std::string& timezone = staged_.chart_timezone;
18080 const bool utc_chart = timezone.empty() || timezone == "UTC" || timezone == "Etc/UTC";
18081 if (utc_chart) {
18082 // gmtime_r's day and month without the libc call every bar paid:
18083 // civil arithmetic on the floor day, exact for every second within
18084 // +/-2^40 of the epoch. gmtime_r keeps the rest.
18085 const std::int64_t secs = static_cast<std::int64_t>(seconds);
18086 if (secs > -kChartDayCivilSpan && secs < kChartDayCivilSpan) {
18087 // A second of the day last read here answers that day's key: the
18088 // day is kept with the consumer that keeps this adapter's lookup
18089 // state for the run (the UTC chart-day memo). An unbound adapter, or a
18090 // consumer that holds no cache, computes every read as it always did.
18091 AdapterLookupIndex* index = parked_lookup_index(host_, this, run_counter_);
18092 if (index != nullptr && index->day_lo <= secs && secs < index->day_hi)
18093 return index->day_key;
18094 return civil_chart_day_key(index, secs);
18095 }
18096 }
18097 return libc_chart_day_key(timezone, utc_chart, seconds);
18098}
18099
18100compat::pine::CapClock PineExecutionAdapter::cap_clock(
18101 const NativeDecisionContext& context) const {
18102 const std::int64_t key = chart_day_key(context.sub_bar_open_ms);
18103 return {context.sub_bar_open_ms,
18104 staged_.syminfo.session.empty() ? "24x7" : staged_.syminfo.session,
18105 staged_.syminfo.timezone.empty() ? "UTC" : staged_.syminfo.timezone,
18106 static_cast<int>(key / 100), static_cast<int>(key % 100)};
18107}
18108
18109compat::pine::Calculation PineExecutionAdapter::cap_calculation(
18110 const NativeDecisionContext& context) const {
18111 const auto state = detail::run_state(require_host());
18112 const auto* pine_host = pine_view_of(&require_host());
18113 const bool magnifier = pine_host
18114 ? pine_host->scheduler_.bar_magnifier_enabled()
18115 : context.sub_count > 1;
18116 return {config_.process_orders_on_close, config_.calc_on_order_fills,
18117 coof_recalc_active_, magnifier,
18118 !modeled_input() && state.phase == NativeRunPhase::Warmup,
18119 modeled_input(),
18120 !config_.close_entries_rule_any,
18121 context.coordinate.interval_index};
18122}
18123
18124compat::pine::MatchedAttempt PineExecutionAdapter::cap_attempt(
18125 const PlacementSnapshot& snapshot, std::uint64_t incarnation,
18126 const native_order::ExecutionAppliedEvent* applied) const {
18128 if (snapshot.family == PineOrderFamily::Entry) {
18129 kind = (finite_positive(snapshot.exit_levels.limit)
18130 || finite_positive(snapshot.exit_levels.stop))
18132 }
18133 const auto position = detail::run_position(require_host());
18134 compat::pine::Side side = position.signed_units > 0.0
18136 : (position.signed_units < 0.0 ? compat::pine::Side::Short
18138 std::size_t prefill_entries = position.lot_count;
18139 if (applied) {
18140 if (applied->closed_units > 0.0) {
18141 // The matched ENTRY saw the side opposite its requested side.
18142 // Factor A only needs that exact side to avoid misclassifying a
18143 // reversal as a same-side no-op.
18144 side = snapshot.is_long ? compat::pine::Side::Short
18146 } else if (applied->opened_units != 0.0
18147 && applied->cycle_before != applied->cycle_after) {
18149 prefill_entries = 0;
18150 } else if (applied->opened_units != 0.0
18151 && applied->opened_lot_incarnation != 0
18152 && prefill_entries > 0) {
18153 // A same-side add created exactly one new physical opening.
18154 --prefill_entries;
18155 }
18156 } else {
18157 const auto projected = static_cast<PositionSide>(snapshot.projection_position_side);
18158 if (projected == PositionSide::FLAT) {
18160 prefill_entries = 0;
18161 }
18162 }
18163 const int live_entries = prefill_entries
18164 > static_cast<std::size_t>(std::numeric_limits<int>::max())
18165 ? std::numeric_limits<int>::max() : static_cast<int>(prefill_entries);
18166 return {kind, incarnation, snapshot.projection_created_bar,
18167 snapshot.is_long, side, live_entries,
18168 config_.pyramiding};
18169}
18170
18171bool PineExecutionAdapter::cap_placement_denied(const NativeDecisionContext& context) {
18172 return cap.active() && cap.placement(cap_clock(context)) == compat::pine::Placement::Deny;
18173}
18174
18175bool PineExecutionAdapter::intraday_loss_orders_blocked() const noexcept {
18176 return risk_.intraday_block_day != std::numeric_limits<std::int64_t>::min()
18177 && risk_.intraday_block_day == day_ledger_.current_day;
18178}
18179
18180void PineExecutionAdapter::update_risk_state(double mark_price) {
18181 if (std::isfinite(mark_price)) {
18182 const double equity = require_host().native_marked_equity(mark_price);
18183 if (std::isfinite(equity)) {
18184 if (!std::isfinite(risk_.observed_peak_equity)
18185 || equity > risk_.observed_peak_equity) {
18186 risk_.observed_peak_equity = equity;
18187 }
18188 const double drawdown = risk_.observed_peak_equity - equity;
18189 if (drawdown > risk_.observed_max_drawdown)
18190 risk_.observed_max_drawdown = drawdown;
18191 }
18192 }
18193 if (risk_.halted) return;
18194 if (risk_.max_drawdown > 0.0 && std::isfinite(risk_.observed_peak_equity)) {
18195 const double threshold = risk_.max_drawdown_percent
18196 ? risk_.observed_peak_equity * risk_.max_drawdown / 100.0
18197 : risk_.max_drawdown;
18198 if (std::isfinite(risk_.max_drawdown) && !std::isfinite(threshold)) {
18199 engine_command_value_invalid_or_count("risk.threshold",
18200 EngineValueStage::risk, EngineValueReason::numeric_overflow);
18201 }
18202 if (risk_.observed_max_drawdown >= threshold) {
18203 risk_.halted = true;
18204 return;
18205 }
18206 }
18207 if (risk_.max_cons_loss_days > 0
18208 && day_ledger_.consecutive_loss_days >= risk_.max_cons_loss_days) {
18209 risk_.halted = true;
18210 }
18211}
18212
18213bool PineExecutionAdapter::intraday_loss_breached(double mark_price) const {
18214 if (!(risk_.max_intraday_loss > 0.0) || intraday_loss_orders_blocked()
18215 || !std::isfinite(day_ledger_.intraday_start_equity)
18216 || !std::isfinite(mark_price)) {
18217 return false;
18218 }
18219 const double equity = require_host().native_marked_equity(mark_price);
18220 const double loss = day_ledger_.intraday_start_equity - equity;
18221 const double threshold = risk_.max_intraday_loss_percent
18222 ? day_ledger_.intraday_start_equity * risk_.max_intraday_loss / 100.0
18223 : risk_.max_intraday_loss;
18224 if (std::isfinite(risk_.max_intraday_loss) && !std::isfinite(threshold)) {
18225 engine_command_value_invalid_or_count("risk.threshold",
18226 EngineValueStage::risk, EngineValueReason::numeric_overflow);
18227 }
18228 if (!(threshold > 0.0) || !(loss > 0.0) || !std::isfinite(loss)) return false;
18229 const double epsilon = 1e-9 * std::max(1.0, std::abs(threshold));
18230 return loss + epsilon >= threshold;
18231}
18232
18233// =========================================================== R5 margin policy
18234// The kernel (src/native_execution_consumer.cpp, the kernel execution sampler) owns the
18235// margin MECHANISM: when the requirement is tested, the liquidation request it
18236// originates, its Superseded re-pricing, its receipt and its MarginCallEvent.
18237// The answers below are the only TradingView-specific things left in that
18238// loop -- when TradingView would check, what money it compares, and how many
18239// units it takes -- and each is answered on the kernel's own facts.
18240
18241// TradingView's money at one mark, which is what `required > equity` is made
18242// of. This is the single implementation: `submit_margin_call_slice` (the
18243// checkpoints the kernel has no point for) and `resolve_margin_requirement`
18244// (the kernel's own path check) both read it, so the two routes cannot drift.
18246 double mark_price, std::int64_t sub_bar_open_ms) const {
18247 SourceMarginMoney money;
18248 const auto position = detail::run_position(require_host());
18249 if (position.signed_units < 0.0) {
18250 // ab9714be pine_fills.cpp: a short's checkpoint marks on the chart
18251 // tick, while the slice still rests at the raw waypoint.
18252 mark_price = nearest_tick(mark_price, staged_.syminfo.mintick);
18253 }
18254 money.mark = mark_price;
18255 money.held = std::abs(position.signed_units);
18256 const double margin_pct = position.signed_units > 0.0
18257 ? config_.margin_long : config_.margin_short;
18258 if (!source_margin_call_enabled_ || !(money.held > 0.0)
18259 || !finite_positive(mark_price) || !finite_positive(margin_pct)
18260 || !finite_positive(staged_.syminfo.pointvalue)) {
18261 return money;
18262 }
18263 const double fx = active_staged_fx(sub_bar_open_ms);
18264 const double fraction = margin_pct / 100.0;
18265 money.scale = staged_.syminfo.pointvalue * fx * fraction;
18266 money.short_book = position.signed_units < 0.0;
18267 money.unit_margin = mark_price * staged_.syminfo.pointvalue * fx * fraction;
18268 money.exact_required = money.held * money.unit_margin;
18269 money.required = money.exact_required;
18270 if (staged_.quantity_grid && *staged_.quantity_grid > 0.0
18271 && staged_.account_fx_effective_from_ms.empty()) {
18272 const double lot_value = *staged_.quantity_grid * mark_price
18273 * staged_.syminfo.pointvalue * fx;
18274 if (std::isfinite(lot_value) && lot_value < 1.0) {
18275 // ab9714be pine_fills.cpp:11491-11499: a sub-unit lot puts the
18276 // requirement on TradingView's ten-significant-digit money ladder.
18277 money.required = source_money_round(money.exact_required);
18278 }
18279 }
18280 // ab9714be pine_fills.cpp:1411-1423: fee-adjusted live equity -- on
18281 // TradingView's own money (gain_loss_money) where the mirror holds.
18282 money.equity = percent_commission_live_equity(mark_price);
18283 if (const double mirrored = gain_loss_signal_equity(mark_price); std::isfinite(mirrored))
18284 money.equity = mirrored;
18285 // Cash-per-contract margin charges the open entry fees. Cash-per-order
18286 // retains the legacy restoring basis: cash_fee_sizing/pcf-order-p100-m100
18287 // shows TradingView charging fees too, but its distinct margin schedule
18288 // remains a recorded divergence, not covered by the contract-fee rule.
18289 if (config_.commission_type == static_cast<int>(CommissionType::CASH_PER_CONTRACT)
18290 && config_.commission_value > 0.0) {
18291 money.equity = strategy_equity_at(mark_price, std::nullopt);
18292 }
18293 money.valid = finite_positive(money.unit_margin) && std::isfinite(money.equity);
18294 return money;
18295}
18296
18297// TradingView's slice quantity from that money: the lot-floored restore taken
18298// four times, floored to the lot again, and the family R whole-drop band for a
18299// restore that floors below one lot.
18301 const SourceMarginMoney& money, bool opening_checkpoint, bool gated, double print) const {
18302 if (!money.valid || !(money.required > money.equity)) return 0.0;
18303 const bool opening_basis = opening_checkpoint && money.required == money.exact_required;
18304 const double raw_minimum = opening_basis
18305 ? money.held - money.equity / money.unit_margin
18306 : (money.required - money.equity) / money.unit_margin;
18307 if (!(raw_minimum > 0.0) || !std::isfinite(raw_minimum)) return 0.0;
18308 // ab9714be pine_fills.cpp:1572-1575: a dust-sized restore requirement is
18309 // not a broker action. It must be discarded before lot quantization, so
18310 // floating-point residue at a 1x full-margin opening cannot become a
18311 // 4x epsilon Reduce (and a phantom trade row).
18312 // On TradingView's own money a shortfall of any size is a call: a dust
18313 // restore falls to the one-unit call below (tests/fixtures/
18314 // margin_ledger_rules and margin_call_rules: one unit of an NYSE:F long
18315 // ledger and of a margin-50 ETH long admitted one ulp under its cost).
18316 const bool dust_unit = detail::margin_rule_switches().dust_unit_call
18317 && std::isfinite(gain_loss_signal_equity(money.mark));
18318 if (raw_minimum <= internal::kQtyEpsilon && !dust_unit) return 0.0;
18319 double minimum = raw_minimum;
18320 if (staged_.quantity_grid) {
18321 // The lots the restore floors to count the shortfall as TradingView's
18322 // money, on its ten-significant-digit ladder: a shortfall of exactly
18323 // two units' margin floors to two, not one below it for binary64
18324 // residue (synthetic NYSE:F book: 600 shares 50.00 short at the 25.00
18325 // mark take 8 at the close, where an unrounded difference of
18326 // 49.99999999999999 floors to one unit and calls 4 instead;
18327 // the margin_opposite fixtures establish the rounding rule).
18328 const double floor_basis = opening_basis
18329 ? raw_minimum
18330 : source_money_round(money.required - money.equity) / money.unit_margin;
18331 minimum = std::floor(floor_basis / *staged_.quantity_grid)
18332 * *staged_.quantity_grid;
18333 }
18334 double units = minimum > 0.0 ? 4.0 * minimum : 0.0;
18335 if (units > 0.0 && staged_.quantity_grid) {
18336 units = std::floor(units / *staged_.quantity_grid + 1e-6)
18337 * *staged_.quantity_grid;
18338 }
18339 if (!(units > 0.0) && staged_.quantity_grid
18340 && *staged_.quantity_grid <= 1.0
18341 && (raw_minimum > internal::kQtyEpsilon || dust_unit) && raw_minimum < 1.0) {
18342 const double candidate = std::min(1.0, money.held);
18343 const double rounded = floor_quantity_grid(candidate, staged_.quantity_grid);
18344 const double guard = std::max(1e-12, std::abs(candidate) * 1e-12);
18345 // The one unit is taken only where it restores the book at the call's
18346 // own fill: TradingView prices the whole position's requirement at the
18347 // liquidation's slipped print -- a short's buy is the slippage ticks
18348 // above the mark, a long's sell below it -- against the equity at the
18349 // mark, and closes nothing when that requirement exceeds the equity by
18350 // a unit's margin or more. A long's sell never does, nor does a call at
18351 // zero slippage. A synthetic NYSE:F short of 1000 shares filled
18352 // at 20.00, 15.00 short at 20.01, takes no one-unit call at slippage 1:
18353 // 1000 x 20.02 exceeds equity by 25.00, over one unit's margin.
18354 // At slippage 3, 400 shares filled at 20.00, 10.00 short at 20.03,
18355 // lack 22.00 at the 20.06 fill and also take no one-unit call.
18356 // These synthetic slipped_short books illustrate the slipped-short margin rule;
18357 // an otherwise similar book inside the band can take the call.
18358 const double slip = config_.slippage > 0
18359 ? config_.slippage * staged_.syminfo.mintick : 0.0;
18360 const bool short_book = detail::run_position(require_host()).signed_units < 0.0;
18361 const double fill_required = money.exact_required
18362 * (finite_positive(print) ? print : money.mark + (short_book ? slip : -slip))
18363 / money.mark;
18364 if ((candidate >= money.held - guard || std::abs(rounded - candidate) <= guard)
18365 && fill_required - money.equity < money.unit_margin)
18366 units = candidate;
18367 }
18368 units = std::min(money.held, units);
18369 // ab9714be pine_fills.cpp:1708: the final slice quantity carries the same
18370 // slack gate, so a floored-to-dust restore closes nothing at all.
18371 if (!(units > internal::kQtyEpsilon) || !std::isfinite(units)) return 0.0;
18372 if (PF_RARE(money.short_book) && gated
18373 && short_call_vetoed(money, units, opening_checkpoint, print))
18374 return 0.0;
18375 return units;
18376}
18377
18378// TradingView takes a short's call only where it restores the book at the
18379// call's own slipped print p': the units called, priced there, must exceed
18380// the whole position's requirement at p' less the equity at the mark (switch
18381// short_call_gate). The one-unit band above is its one-unit case. A long's
18382// sell never binds, nor does a call at zero slippage, whose print is its mark;
18383// with slippage it suppresses the calls whose deficit is below about a third
18384// of the position's slippage cost: on OANDA:EURUSD at slippage 1 a short of
18385// synthetic 50000 units found 0.10 short at a 2.00000 high would call 0.20,
18386// worth 0.400002 at the 2.00001 fill, below the 0.60 the book lacks there,
18387// so nothing is called until a later high needs a larger call (lab tv tapes
18388// tests/fixtures/margin_call_rules short-cutoff-gate/, 67 tapes, and
18389// tests/fixtures/margin_schedule_rules gate/). An opening checkpoint at a
18390// fill keeps the earlier rules.
18391bool PineExecutionAdapter::short_call_vetoed(
18392 const SourceMarginMoney& money, double units, bool opening_checkpoint, double print) const {
18393 if (!detail::margin_schedule_switches().short_call_gate || opening_checkpoint
18394 || !money.short_book || !(config_.slippage > 0) || !finite_positive(money.scale)
18395 || !finite_positive(staged_.syminfo.mintick)) {
18396 return false;
18397 }
18398 const double fill = finite_positive(print) ? print : source_margin_fill_price(money.mark, true);
18399 if (!finite_positive(fill)) return false;
18400 return !(units * fill * money.scale > money.held * fill * money.scale - money.equity);
18401}
18402
18403// The follow-up TradingView takes after a margin call of `called` units on a
18404// long of several lots, sized on `money`, the book before that call, and
18405// filled at `fill`. Its broker checks the book again after each lot the call
18406// closes, first in first out, on a state that has taken the lot out of the
18407// book but not yet booked its P&L -- the lot relieves the shortfall by its
18408// cost (its entry fee was booked when it opened) -- while each lot before it,
18409// booked, relieves it by what its sale fetched net of its exit fee. The last
18410// of those checks that is still short is called again: four times its
18411// lot-floored shortfall. So a call whose first lots cost less than the
18412// shortfall is followed; one that starts inside a lot costing more is not
18413// (lab tv tapes tests/fixtures/add_sibling_margin tailb-lots12-pooc-c0s0,
18414// tailb-lots30-pooc-c10s3 and tailb-lots30-mkt-c10s2; the grid bots'
18415// calls on BINANCE BTC and ETH daily bars; lane TAIL-B). Only on a
18416// fractional lot grid, at a percent commission or none: a whole-unit book's
18417// follow-up is whole_unit_follow_up_due's.
18419 const std::vector<NativeOpenLot>& lots, const SourceMarginMoney& money, double called,
18420 double fill, std::int64_t sub_bar_open_ms) const {
18421 // A whole-share grid takes it too (switch whole_share_lagged_follow_up):
18422 // on NYSE:F at slippage 3 and commission 0.1 %, synthetic lots of
18423 // 5, 15 and 500 shares can require a follow-up after the open's call;
18424 // the last still-short FIFO check sizes it (margin_schedule_rules fixtures).
18426 && staged_.quantity_grid && *staged_.quantity_grid == 1.0;
18427 if (!money.valid || !(money.required > money.equity) || !(called > internal::kQtyEpsilon)
18428 || !staged_.quantity_grid || !(*staged_.quantity_grid > 0.0)
18429 || !(*staged_.quantity_grid < 1.0 || whole_share) || !finite_positive(fill)
18430 || (config_.commission_value != 0.0
18431 && config_.commission_type != static_cast<int>(CommissionType::PERCENT))) {
18432 return 0.0;
18433 }
18434 const double scale = staged_.syminfo.pointvalue * active_staged_fx(sub_bar_open_ms);
18435 const double fee = config_.commission_value / 100.0;
18436 const double shortfall = money.required - money.equity;
18437 double remaining = called;
18438 double booked = 0.0;
18439 double last_short = 0.0;
18440 for (const auto& lot : lots) {
18441 if (!(remaining > internal::kQtyEpsilon)) break;
18442 if (!(lot.signed_units > 0.0)) return 0.0;
18443 const double taken = std::min(remaining, lot.signed_units);
18444 remaining -= taken;
18445 const double lagged = shortfall - booked - taken * lot.entry_price * scale;
18446 if (lagged > 0.0) last_short = lagged;
18447 booked += taken * fill * scale * (1.0 - fee);
18448 }
18449 if (!(last_short > 0.0) || !std::isfinite(last_short)) return 0.0;
18450 const double grid = *staged_.quantity_grid;
18451 const double minimum = std::floor(source_money_round(last_short) / money.unit_margin / grid)
18452 * grid;
18453 double units = std::floor(4.0 * minimum / grid + 1e-6) * grid;
18454 units = std::min(units, money.held - called);
18455 if (!(units > internal::kQtyEpsilon) || !std::isfinite(units)) return 0.0;
18456 return units;
18457}
18458
18459// The forced execution price of a liquidation that fired at `fire`: the fire
18460// price on the chart tick ladder, then the EXIT side's own market slippage
18461// (ab9714be pine_fills.cpp:1712-1726 and :2649-2658). Reducing a long is a
18462// sell (slippage subtracts); reducing a short is a buy (it adds). A margin
18463// call is a MARKET execution at the print it fired at, so an off-ladder print
18464// (a half-cent low) books its nearest tick, as every TradingView market fill
18465// at a print does -- synthetic 25.005 books 25.01; 1.005 books 1.00 (lab tv tapes
18466// tests/fixtures/margin_entry_bar/pm-m10-f-k4-1007, -gapup-0501, -gapup-0424
18467// and, beyond margin calls, pm2-round-f-print; the intrabar margin correction). A print
18468// on the ladder is its own tick (source_bar_fill_tick is idempotent there).
18469double PineExecutionAdapter::source_margin_fill_price(double fire, bool close_is_buy) const {
18470 if (config_.slippage == 0) return source_bar_fill_tick(fire, staged_.syminfo.mintick);
18471 const double rounded = source_bar_fill_tick(fire, staged_.syminfo.mintick);
18472 const double slipped = rounded + (close_is_buy ? 1.0 : -1.0)
18473 * config_.slippage * staged_.syminfo.mintick;
18474 return directional_tick(slipped, staged_.syminfo.mintick, close_is_buy);
18475}
18476
18477// ab9714be pine_fills.cpp:5159-5221: a rounded whole-lot tie is rejected only
18478// for the SOLE opening. A coexisting resting entry excludes that rejection,
18479// and the lot that was admitted on the rounded tie must not then be
18480// immediately liquidated for the same sub-lot representation residue. This is
18481// a rule about when TradingView does not check at all, so it vetoes the
18482// adapter's own checkpoints and, through margin_check_allowed, the kernel's
18483// check point too.
18485 if (!staged_.quantity_grid || *staged_.quantity_grid != 1.0
18486 || config_.default_qty_type != static_cast<int>(QtyType::PERCENT_OF_EQUITY)
18487 || config_.default_qty_value != 100.0 || config_.commission_value != 0.0) {
18488 return false;
18489 }
18490 bool rounded_tie_opening = false;
18491 for (const auto& cohort : cohorts_by_id_.held_rows()) {
18492 for (const auto& origin : cohort.second.opened) {
18493 const auto opening = placement_.find(origin.incarnation);
18494 if (opening == placement_.end()) continue;
18495 const auto& row = opening->second;
18496 const double cost = row.sizing.frozen_units * row.sizing.price
18497 * staged_.syminfo.pointvalue * row.sizing.fx;
18498 if (row.opening && std::isfinite(cost)
18499 && cost == source_money_round(row.sizing.equity)
18500 && cost > row.sizing.equity) {
18501 rounded_tie_opening = true;
18502 break;
18503 }
18504 }
18505 if (rounded_tie_opening) break;
18506 }
18507 if (!rounded_tie_opening) return false;
18508 for (const auto& handle : live_handles_) {
18509 const auto pending = placement_.find(handle.incarnation);
18510 if (pending == placement_.end()) continue;
18511 const auto& row = pending->second;
18512 if (row.opening && (finite_positive(row.exit_levels.limit)
18513 || finite_positive(row.exit_levels.stop))) {
18514 return true;
18515 }
18516 }
18517 return false;
18518}
18519
18520// TradingView's scheduling (MG-I). The kernel offers its own check points;
18521// this admits exactly the ones the adapter's own scheduling decided to check
18522// at, and refuses the rest. A refused point is inert: the kernel measures
18523// nothing, re-arms nothing and withdraws nothing there, so the state stays as
18524// the last admitted point left it. The decision itself is taken where
18525// TradingView takes it -- inside on_bar_open, with the bar's competing
18526// orders in front of it -- and only carried here.
18528 const NativeMarginCheckPoint& point) const {
18529 // The kernel's FX-roll point is not TradingView's rollover: that is the
18530 // broker-open checkpoint of apply_fx_open_margin_slice, measured AT the
18531 // open and executed there. The roll point measures at the remaining
18532 // path's adverse mark and rests the slice on it -- on a step bar O 100
18533 // L 97 a carried 1x long books 0.4116 @ 97 there against TradingView's
18534 // pinned 0.3996 @ 100 (test_native_margin_hooks_twin MG-FX, the margin and cancellation comparison).
18535 // Refused by kind, ahead of the ordinal test, because a roll can share
18536 // its driver point with an armed BarOpen check.
18537 if (point.kind == NativeMarginCheckKind::FxRoll) return false;
18538 // A magnified run's later samples. TradingView's magnified broker checks
18539 // the book at every lower-timeframe bar and books the call at the first
18540 // sub-bar low that crosses, sized there, then again at each later one
18541 // that crosses the reduced book's line (lab tv tapes
18542 // tests/fixtures/intrabar_margin/pm-i3-eth-mag-*, the margin-schedule correction);
18543 // the kernel's IntrabarSample point is that check.
18544 if (point.kind == NativeMarginCheckKind::IntrabarSample) {
18546 }
18547 // The kernel's BarOpen point and its post-fill AfterApplied re-arm are
18548 // both points TradingView checks at -- schedule_margin_call_path is
18549 // called from on_bar_open and from on_applied -- so the admitted point is
18550 // named by the driver point the decision was taken at, not by its kind.
18551 const std::uint64_t ordinal = point.cursor.point.ordinal;
18552 if (kernel_margin_resize_point_ == ordinal) {
18553 // The post-exit re-size point (on_applied, a priced bracket leg of
18554 // this script bar). The legacy broker cancelled the live path slice
18555 // there and re-scheduled from the reduced book, and did NOTHING when
18556 // no slice was live -- so this point is TradingView's only while one
18557 // rests. With one resting the kernel withdraws it and re-rests
18558 // whatever schedule_margin_call_path armed on the reduced book; with
18559 // the book flat the same admission withdraws it and rests nothing.
18560 if (!point.liquidation_resting) return false;
18561 } else if (kernel_margin_path_point_ != ordinal) {
18562 return false;
18563 }
18564 // The rounded-tie veto is re-read here rather than at the arming site: the
18565 // book it inspects is the one standing at the check point.
18567}
18568
18569// Where the per-sample check is TradingView's as measured, on a magnified run
18570// with no slice of the adapter's own resting (the pre-open slice keeps its
18571// verdict for its bar, as on the chart path): a leveraged long, plain, under
18572// process_orders_on_close or under calc_on_order_fills, and a short at any
18573// margin, plain or under calc_on_order_fills (lab tv tapes
18574// tests/fixtures/intrabar_margin: pm-i3-eth-mag-* for the plain leveraged long,
18575// The margin-schedule correction; pm2-i3-pooc-mag-*, pm2-i3-coof-* and the full-margin
18576// short pm2-i3-s1x-mag-*, the intrabar margin correction). Not a full-margin long, whose
18577// one-contract money call owns the book (resolve_margin_call_units refuses
18578// it), nor a short under process_orders_on_close, whose carried slice is the
18579// post-script checkpoint's (schedule_margin_call_path), unmeasured here. A
18580// flat book is admitted: the kernel then withdraws what it had resting.
18582 const NativeMarginCheckPoint& point) const {
18583 if (!bar_magnifier_) return false;
18584 const double held = point.position.signed_units;
18585 if (held == 0.0) return true;
18586 if (held > 0.0 ? !(config_.margin_long < 100.0) : config_.process_orders_on_close)
18587 return false;
18588 return !any_live_row(live_handles_, placement_, [](const PlacementSnapshot& row) {
18589 return row.family == PineOrderFamily::Margin;
18590 });
18591}
18592
18593// TradingView's money (MG-A + MG-B), restated for the kernel's breach test at
18594// the mark it measured. nullopt would keep the kernel's own unrounded
18595// requirement against its own marked equity, which is a different broker.
18596std::optional<NativeMarginDecision> PineExecutionAdapter::resolve_margin_requirement(
18597 const NativeMarginRequirementView& view) const {
18598 const auto money = source_margin_money(view.mark, view.cursor.point.open_ms);
18599 if (!money.valid) return std::nullopt;
18600 NativeMarginDecision decision;
18601 decision.required = money.required;
18602 decision.equity = money.equity;
18603 return decision;
18604}
18605
18606// TradingView's sizing (MG-F + MG-G), on the kernel's own facts. This hook is
18607// the whole sizing authority for every call the kernel makes on the adapter's
18608// behalf, which is why project() declares no kernel sizing knob for the model.
18610 const NativeMarginCallView& view) const {
18611 // Always an answer, never nullopt. A non-positive answer is the kernel's
18612 // documented refusal, which withdraws the reduction instead of booking one.
18613 //
18614 // TradingView never takes the adverse-path slice on a 1x long: the
18615 // one-contract money call owns that book instead
18616 // (submit_tv_money_long_margin_call). Every dispatch site guards on that
18617 // before it schedules, but the book can turn into a 1x long between the
18618 // scheduling decision and the check point -- a reversal at the same
18619 // driver point does exactly that -- and the book that decides is the one
18620 // the call would be made against. This is a refusal of the CALL, not of
18621 // the check point: the point still runs, so a reduction resting from an
18622 // earlier book is withdrawn here rather than left behind.
18623 if (view.position.signed_units > 0.0 && std::isfinite(config_.margin_long)
18624 && std::abs(config_.margin_long - 100.0) < 1e-12) {
18625 return 0.0;
18626 }
18627 // A point whose only adverse mark is the close: the call is taken there
18628 // after the script (close_point_margin_call), not rested at the close.
18629 if (view.cursor.point.ordinal == close_margin_point_) return 0.0;
18630 const auto money = source_margin_money(view.mark, view.cursor.point.open_ms);
18631 return source_margin_units(money, false);
18632}
18633
18634// A TradingView margin checkpoint the kernel has no check point for (MG-I):
18635// the bar-open mark, the script-close pass, the stream tick, the opening
18636// print. The money, the mark's chart-tick rounding and the sizing are the
18637// shared source_margin_* rules -- the same ones the kernel's own path check
18638// reads through the requirement and units hooks -- and the slice is a market
18639// execution at the current point, never a resting order: the kernel owns
18640// every resting liquidation now.
18641bool PineExecutionAdapter::submit_margin_call_slice(
18642 double mark_price, const NativeDecisionContext& context,
18643 bool opening_checkpoint, std::optional<double> resolved_execution_price) {
18644 const auto position = detail::run_position(require_host());
18645 const auto money = source_margin_money(mark_price, context.sub_bar_open_ms);
18646 mark_price = money.mark;
18647 if (!money.valid) return false;
18648 if (source_margin_rounded_tie_veto()) return false;
18649 // A repeat at an already-resolved price -- the call TradingView repeats
18650 // at one fill until the book is covered (on_applied, recheck_at_fill) --
18651 // is a follow-up, not a fresh call, and the gate never reaches it.
18652 const double units = source_margin_units(money, opening_checkpoint,
18653 !resolved_execution_price.has_value());
18654 if (!(units > 0.0)) return false;
18655
18656 // ab9714be pine_fills.cpp:1712-1726 books the checkpoint's residual
18657 // against bar_fill_price(fire) and only then applies the EXIT side's own
18658 // market slippage. The opening checkpoint hands this helper the
18659 // already-SLIPPED opening print, so the entry-side slippage step is undone
18660 // here first and submit_margin_call_units re-applies the exit side on top
18661 // of the raw chart fill. At zero slippage the reconstruction is the
18662 // identity.
18663 double close_base = mark_price;
18664 if (opening_checkpoint && std::isfinite(config_.slippage)
18665 && config_.slippage != 0.0) {
18666 close_base = source_bar_fill_tick(
18667 mark_price - (position.signed_units > 0.0 ? 1.0 : -1.0)
18668 * config_.slippage * staged_.syminfo.mintick,
18669 staged_.syminfo.mintick);
18670 }
18671 return submit_margin_call_units(close_base, context, units, true, resolved_execution_price);
18672}
18673
18674bool PineExecutionAdapter::submit_margin_call_units(
18675 double mark_price, const NativeDecisionContext& context, double units,
18676 bool force_execution_price, std::optional<double> resolved_execution_price) {
18677 const auto position = detail::run_position(require_host());
18678 const double held = std::abs(position.signed_units);
18679 if (!(units > 0.0) || !std::isfinite(units) || !(held > 0.0)
18680 || !finite_positive(mark_price)) {
18681 return false;
18682 }
18683 units = std::min(units, held);
18684 native_order::Request request;
18685 request.intent = native_order::Reduce{native_order::ExplicitUnits{units}};
18686 request.label = kMarginCallLabel;
18687 request.comment = "Margin call";
18688 PlacementSnapshot snapshot;
18690 snapshot.source_id = request.label;
18691 snapshot.requested_qty = units;
18692 if (force_execution_price) {
18693 // ab9714be pine_fills.cpp:1712-1726 and :2649-2658: ordinary margin-call
18694 // closes book bar_fill_price(fire), then apply EXIT-side market slippage,
18695 // as does the adverse-extreme cascade. The default branch reproduces
18696 // that slippage before pinning: subtract for a long's sell, add for a
18697 // short's buy. An already-resolved recheck fill bypasses slippage and
18698 // is pinned unchanged; TradingView books repeated calls at one price
18699 // (tests/fixtures/margin_residual px-f2b-eth-short-0/1, -eth-slip-3).
18700 // The generic forced-execution fact rounds to the chart tick; at zero
18701 // slippage the default remains the identity.
18702 snapshot.forced_execution_price =
18703 resolved_execution_price ? *resolved_execution_price
18704 : source_margin_fill_price(mark_price, position.signed_units < 0.0);
18705 }
18706 snapshot.sizing = sizing_snapshot();
18707 const auto accepted = submit_or_replace(std::move(request), std::move(snapshot), false,
18709 if (!accepted) return false;
18710 if (PF_RARE(open_call_sequence_)) unrepeated_margin_calls_.push_back(accepted->incarnation);
18711 (void)require_host().execute_current({*accepted, NativeCurrentPriceRule::NearestTick});
18712 return true;
18713}
18714
18715// TradingView's realized money (switch gain_loss_money): the profit of every
18716// closed trade, margin calls included, in booking order, summed into gains
18717// and into losses apart; strategy.netprofit is G + L, not one running sum.
18718// A synthetic NYSE:F ledger reaches 9999.999999999998 where the running
18719// sum reaches 10000 and is dropped close-only at that cost (tests/fixtures/
18720// margin_ledger_rules). Folded as trades book; the state hash carries both.
18721// Most asks find nothing booked since the last fold: they stop at the count,
18722// inline in the per-bar callers.
18723[[gnu::always_inline]] inline void PineExecutionAdapter::fold_source_money() {
18724 const auto* pine = pine_view_of(host_);
18725 if (!pine) return;
18726 const std::uint64_t count = pine->closed_trade_count();
18727 if (count == source_money_folded_) return;
18728 fold_source_money_through(*pine, count);
18729}
18730
18731// The fold itself, over the trades booked since the last one (`count`
18732// trades in all).
18733[[gnu::noinline]]
18734void PineExecutionAdapter::fold_source_money_through(const PineStrategyHost& pine,
18735 std::uint64_t count) {
18736 if (count < source_money_folded_) {
18737 const auto& prefix = detail::run_consumer(pine).closed_trade_prefix();
18738 if (count < prefix.base)
18739 throw coded<std::logic_error>(RunFailureCode::engine_invariant, {},
18740 "source-money rewind precedes the sealed prefix");
18741 source_gains_ = prefix.source_gains;
18742 source_losses_ = prefix.source_losses;
18743 source_money_folded_ = prefix.base;
18744 }
18745 for (; source_money_folded_ < count; ++source_money_folded_) {
18746 const double profit = pine.closed_trade(source_money_folded_).pnl;
18747 if (profit > 0.0) source_gains_ += profit;
18748 else source_losses_ += profit;
18749 }
18750}
18751
18752// The G + L mirror's regime: the one predicate that scopes it. The regime
18753// the margin_ledger_rules and margin_call_rules tapes pin is the switch on;
18754// no fee at all (none configured, none in the run's fee model, none recorded
18755// at an opening); point value 1; no FX series; a power-of-ten lot step
18756// (every tape's); and at most one open lot (the ledger tapes hold one).
18757// The configuration (no commission, point value 1, no FX series, a
18758// power-of-ten lot step) is gain_loss_configured_, formed once per
18759// configuration; every condition here is a plain read, so their order only
18760// decides how soon a run outside the regime answers.
18761[[gnu::always_inline]] inline bool PineExecutionAdapter::gain_loss_regime() const {
18762 return gain_loss_configured_ && detail::margin_rule_switches().gain_loss_money
18763 && gain_loss_run_regime();
18764}
18765
18766// The run's part of the regime: the fee model, the recorded fees, the book.
18767[[gnu::noinline]]
18768bool PineExecutionAdapter::gain_loss_run_regime() const {
18769 const auto* pine = pine_view_of(host_);
18770 if (pine == nullptr || host_ == nullptr) return false;
18771 const auto* spec = detail::run_spec(*host_);
18772 if (spec == nullptr || spec->fee_value != 0.0) return false;
18773 for (const auto& fact : open_entry_fees_) {
18774 if (fact.nonpercent_fee != 0.0) return false;
18775 }
18776 return pine->pyramid_entries_.size() <= 1;
18777}
18778
18779// The signal's equity on TradingView's own money, inside gain_loss_regime().
18780// There sizing reads it through sig10(E) and the margin money floors sig10
18781// of its deficit to the lot step, so it parts from the engine's own equity
18782// only at the ten-digit ties the tapes pin. Elsewhere it is NaN and the
18783// engine's own equity stands: a commission written into a live configuration
18784// (the open entry's fee the mirror never charges), recorded cash fees, a
18785// pyramided book or a continuous or non-decimal lot moved the publication
18786// witness's fuzz runs off the engine's equity
18787// (tests/test_publication_witness.cpp).
18788double PineExecutionAdapter::gain_loss_signal_equity(double mark) const {
18789 return gain_loss_regime() ? gain_loss_mirror_equity(mark) : kNaN;
18790}
18791
18792// The closed part of that money, initial + (G + L), for a commission-free
18793// long's one-unit call after a trade history (switch long_call_gain_loss):
18794// Synthetic G + L money of 49.99999999999999 is 7.1e-15 under a 2 x 25.00
18795// cost and calls, where the running sum need not (lab tv tapes
18796// tests/fixtures/margin_schedule_rules money/: histories whose sequential
18797// sum, initial + sequential net and G + L sums can straddle the cost of 400
18798// synthetic NYSE:F shares at 25.00). NaN outside gain_loss_regime() -- a fee-bearing
18799// book keeps the engine's closed-trade equity, which no tape decides yet.
18800double PineExecutionAdapter::gain_loss_closed_equity() const {
18801 if (!detail::margin_schedule_switches().long_call_gain_loss || !gain_loss_regime()) return kNaN;
18802 const auto* pine = pine_view_of(host_);
18803 if (!pine) return kNaN;
18804 double gains = source_gains_;
18805 double losses = source_losses_;
18806 const std::uint64_t count = pine->closed_trade_count();
18807 if (count < source_money_folded_) return kNaN;
18808 for (std::uint64_t index = source_money_folded_; index < count; ++index) {
18809 const double profit = pine->closed_trade(index).pnl;
18810 if (profit > 0.0) gains += profit;
18811 else losses += profit;
18812 }
18813 const double equity = pine->initial_capital_ + (gains + losses);
18814 return std::isfinite(equity) ? equity : kNaN;
18815}
18816
18817// The mirror's equity itself: (initial + (G + L)) plus the open profit
18818// side x (c' - AP) x Q, AP the lots' (price x Q) / Q; NaN where it cannot be
18819// formed. Its callers scope it with gain_loss_regime().
18820// Out of line: inlined into its one caller, it widened that per-bar
18821// function's fetch footprint (CMakeLists.txt, -falign-functions).
18822[[gnu::noinline]]
18823double PineExecutionAdapter::gain_loss_mirror_equity(double mark) const {
18824 const auto* pine = pine_view_of(host_);
18825 if (!pine || !finite_positive(mark)) return kNaN;
18826 double gains = source_gains_;
18827 double losses = source_losses_;
18828 const std::uint64_t count = pine->closed_trade_count();
18829 if (count < source_money_folded_) return kNaN;
18830 for (std::uint64_t index = source_money_folded_; index < count; ++index) {
18831 const double profit = pine->closed_trade(index).pnl;
18832 if (profit > 0.0) gains += profit;
18833 else losses += profit;
18834 }
18835 double open = 0.0;
18836 if (pine->position_side_ != PositionSide::FLAT) {
18837 double weighted = 0.0;
18838 double units = 0.0;
18839 for (const auto& lot : pine->pyramid_entries_) {
18840 weighted += lot.price * lot.qty;
18841 units += lot.qty;
18842 }
18843 if (!finite_positive(units) || !std::isfinite(weighted)) return kNaN;
18844 const double side = pine->position_side_ == PositionSide::LONG ? 1.0 : -1.0;
18845 open = side * (mark - weighted / units) * units;
18846 }
18847 const double equity = (pine->initial_capital_ + (gains + losses)) + open;
18848 return std::isfinite(equity) ? equity : kNaN;
18849}
18850
18851// The shapes the unified admission (placement rules 2 and 5, the fill
18852// re-check) is pinned on: one market or stop strategy.entry, from flat or
18853// reversing, at a percent-of-equity 100 default or an explicit quantity,
18854// commission-free, point value 1, no FX, pyramiding at most one, nothing else
18855// resting but unpriced closes, no risk rule. Each half has its own switch,
18856// read by the callers (unified_placement, fill_price_recheck), and a
18857// close-first entry (PlacementSnapshot::close_first_entry) takes rule 2 alone.
18858bool PineExecutionAdapter::unified_admission_scope(
18859 const PlacementSnapshot& source, bool market_or_stop, double signed_units,
18860 double units, bool beside_exits) const {
18861 // The tapes pin it on lot-stepped instruments, without
18862 // calc_on_order_fills and without the bar magnifier; a continuous
18863 // quantity (test_tv_money_precision_l4b, test_l10ac_full_equity_single_lot),
18864 // a fill recalculation or a magnified run keeps the earlier rules
18865 // (test_taro_price_gap_admission_l4d's scope controls).
18866 if (source.family != PineOrderFamily::Entry || !market_or_stop
18867 || !finite_positive(units) || finite_positive(source.exit_levels.limit)
18868 || !source.oca_name.empty() || source.birth.from_fill() || source.crosses_zero
18869 || !pinned_lot_grid()
18870 || config_.calc_on_order_fills || bar_magnifier_
18871 || config_.commission_value != 0.0 || config_.pyramiding < 0 || config_.pyramiding > 1
18872 || staged_.syminfo.pointvalue != 1.0 || source.sizing.fx != 1.0
18873 || staged_.account_fx != 1.0 || !staged_.account_fx_effective_from_ms.empty()
18874 || !finite_positive(staged_.syminfo.mintick) || !finite_positive(source.sizing.mark)
18875 || !std::isfinite(source.sizing.equity) || cap.active()
18876 || risk_.max_intraday_loss > 0.0 || risk_.max_drawdown > 0.0
18877 || risk_.max_cons_loss_days > 0 || risk_.max_position_size > 0.0) {
18878 return false;
18879 }
18880 if (signed_units != 0.0 && ((signed_units > 0.0) == source.is_long)) return false;
18881 // From flat or reversing as placed: an entry placed on a flat book is
18882 // judged only while the book is still flat when it fills -- a same-open
18883 // pair's later leg that finds the earlier one filled is the engine's
18884 // earlier rule (tests/fixtures/same_open_reversal) -- and one placed
18885 // against the other side only while that side, or flat behind its close,
18886 // is what it meets.
18887 const auto placed = static_cast<PositionSide>(source.projection_position_side);
18888 if (placed == PositionSide::FLAT ? signed_units != 0.0
18889 : ((placed == PositionSide::LONG) == source.is_long)) {
18890 return false;
18891 }
18892 // A stop is pinned from flat only (factorial/c1-s1-exp-stop-long-s1, the
18893 // stop and stopband tapes); a stop that reverses keeps the earlier
18894 // fill-time admission of the closing leg (test_m_admission_36_l4d).
18895 if (finite_positive(source.exit_levels.stop) && placed != PositionSide::FLAT) return false;
18896 // Nor is a default-sized stop: the stop the tapes pin through the fill
18897 // check is an explicit quantity (factorial/c1-s1-exp-stop-long-s1); a
18898 // percent stop is sized at its level and keeps the earlier admission, which
18899 // its tapes match (stop/stop-11-percent, -38-percent and -40-percent;
18900 // test_stop_entry_placement_open_qty_l4b), and no tape decides a percent
18901 // stop entry under the fill check (a population script's percent stops,
18902 // a third-party OANDA:EURUSD strategy, booked other trades there).
18903 if (finite_positive(source.exit_levels.stop) && !std::isfinite(source.requested_qty))
18904 return false;
18905 const bool default_all_in = !std::isfinite(source.requested_qty)
18906 && config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
18907 && config_.default_qty_value == 100.0;
18908 const bool explicit_units = std::isfinite(source.requested_qty)
18909 && (source.qty_type < 0 || source.qty_type == static_cast<int>(QtyType::FIXED));
18910 if (!default_all_in && !explicit_units) return false;
18911 const double margin = source.is_long ? config_.margin_long : config_.margin_short;
18912 if (!finite_positive(margin) || margin > 100.0) return false;
18913 for (const auto& handle : live_handles_) {
18914 const auto peer = placement_.find(handle.incarnation);
18915 if (peer == placement_.end()) continue;
18916 const auto& row = peer->second;
18917 if (row.source_id == source.source_id && row.source_sequence == source.source_sequence)
18918 continue;
18919 const bool unpriced_close = (row.family == PineOrderFamily::Close
18920 || row.family == PineOrderFamily::CloseAll)
18921 && !finite_positive(row.exit_levels.limit) && !finite_positive(row.exit_levels.stop)
18922 && !finite_positive(row.exit_levels.trail_points)
18923 && !finite_positive(row.exit_levels.trail_price);
18924 // `beside_exits`: strategy.exit stop and trail legs may rest too (the
18925 // tie in unified_admission_terms); a limit leg is not pinned there.
18926 const bool stop_or_trail_leg = row.family == PineOrderFamily::ExitStop
18927 || row.family == PineOrderFamily::ExitTrail;
18928 if (!unpriced_close && row.family != PineOrderFamily::Margin
18929 && !(beside_exits && stop_or_trail_leg)) {
18930 return false;
18931 }
18932 }
18933 return true;
18934}
18935
18936// The unified admission at resolve_terms, out of line: true when the terms
18937// are final (resolve_terms returns `result`), false when the order is outside
18938// the scope or, a reversal admitted (`admitted`), takes the ordinary reversal
18939// shape below with no money check on the way. Its rules: at its signal,
18940// placement rule 2 on the money scale -- E < sig10(Q c' m) drops an opening
18941// from flat and keeps only a reversal's closing leg -- and rule 5 on the price
18942// scale at full margin; at its fill only the price scale (resolve_terms).
18943[[gnu::noinline]]
18944bool PineExecutionAdapter::unified_admission_terms(
18945 const PlacementSnapshot& source, const NativeExecutionTermsFacts& facts,
18946 bool market_or_stop, bool opposite, native_order::ExecutionTerms& result,
18947 bool& admitted) const {
18948 const auto& admission_rules = detail::margin_rule_switches();
18949 const double unified_units = result.units && finite_positive(*result.units)
18950 ? *result.units
18951 : (std::isfinite(source.requested_qty)
18952 ? floor_quantity_grid(std::abs(source.requested_qty), staged_.quantity_grid)
18953 : kNaN);
18954 const bool book_clear = unified_admission_scope(source, market_or_stop,
18955 facts.position.signed_units, unified_units);
18956 if (!book_clear
18957 && !(opposite && admission_rules.tie_reversal_beside_exits
18958 && admission_rules.unified_placement
18959 && unified_admission_scope(source, market_or_stop, facts.position.signed_units,
18960 unified_units, /*beside_exits=*/true))) {
18961 return false;
18962 }
18963 const double tick = staged_.syminfo.mintick;
18964 const double units = unified_units;
18965 const double side = source.is_long ? 1.0 : -1.0;
18966 const double margin = (source.is_long ? config_.margin_long : config_.margin_short) / 100.0;
18967 const double equity = source.sizing.equity;
18968 const double signal_ticks = std::floor(source.sizing.mark / tick + 0.5);
18969 const double signal_close = signal_ticks * tick;
18970 // Beside resting strategy.exit stop and trail legs (the reversed
18971 // position's, the new entry's own) only rule 2's exact tie is judged: a
18972 // default 100 % reversal at full margin on a whole-unit lot whose
18973 // binary64 E is under sig10(Q c') while sig10(E) reaches it keeps only
18974 // its close leg, as on a clear book (tests/fixtures/admission_rules
18975 // reversal_tie). Off that tie, beside a limit leg and on a fractional lot
18976 // the engine's earlier rules stand: no tape decides there.
18977 if (!book_clear) {
18978 const double cost = source_money_round(units * signal_close * margin);
18979 const bool default_all_in = !std::isfinite(source.requested_qty)
18980 && config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
18981 && config_.default_qty_value == 100.0;
18982 const bool whole_units = staged_.quantity_grid && *staged_.quantity_grid == 1.0;
18983 if (!(default_all_in && whole_units && margin == 1.0 && equity < cost
18984 && !(source_money_round(equity) < cost))) {
18985 return false;
18986 }
18987 result.units = facts.opposite_book_units;
18988 result.shape = native_order::OpeningShape::CloseOpposite;
18989 return true;
18990 }
18991 if (admission_rules.unified_placement
18992 && equity < source_money_round(units * signal_close * margin)) {
18993 result.units = opposite ? facts.opposite_book_units : 0.0;
18994 result.shape = opposite ? native_order::OpeningShape::CloseOpposite
18995 : native_order::OpeningShape::Transact;
18996 return true;
18997 }
18998 // A close-first entry passed rule 2 and is never checked on the price
18999 // scale: it fills flat behind its close with the quantity it was sized at,
19000 // and the margin rules trim it there (tests/fixtures/admission_rules cf1,
19001 // cf2, cf5, cf9, cf10; c3b).
19002 if (!(source.close_first_entry && admission_rules.close_first_admission)) {
19003 const double per_unit = margin == 1.0
19004 ? source_money_round(source_money_round(equity) / units)
19005 : source_money_round(source_money_round(equity) / (units * margin));
19006 const bool placement_short = admission_rules.unified_placement
19007 && margin == 1.0
19008 && per_unit < (signal_ticks + side * config_.slippage) * tick;
19009 const double quote = finite_positive(facts.raw_price) ? facts.raw_price
19010 : result.resolved_price;
19011 const bool fill_short = admission_rules.fill_price_recheck
19012 && !unified_fill_admits(source, units, quote);
19013 if (placement_short || fill_short) {
19014 result.units = 0.0;
19015 result.shape = native_order::OpeningShape::Transact;
19016 return true;
19017 }
19018 }
19019 result.units = units;
19020 if (!opposite) return true;
19021 admitted = true;
19022 return false;
19023}
19024
19025// The instruments these rules are pinned on: a lot grid that is a power
19026// of ten (every tape's -- 1, 0.01, 0.0001, 1e-05 -- and every population
19027// member's). A continuous quantity or another grid keeps the engine's earlier
19028// rules: no tape decides there, and the synthetic fuzz batteries that run
19029// there (tests/test_publication_witness.cpp's 0.25 lot,
19030// test_adapter_live_state_equivalence's continuous one) book their trades as
19031// before.
19032// Formed once per staged configuration (set_staged_configuration).
19033[[gnu::always_inline]] inline bool PineExecutionAdapter::pinned_lot_grid() const {
19034 return lot_grid_pinned_;
19035}
19036
19037// Whether nothing rests or waits beside an entry the script is placing now
19038// but unpriced closes and margin rows: the book the unified admission's
19039// placement half is pinned on (unified_admission_scope's, at placement).
19040// Out of line and cold: a narrow scope's body, kept off the per-bar
19041// path, which is front-end bound (CMakeLists.txt, -falign-functions).
19042[[gnu::noinline, gnu::cold]]
19043bool PineExecutionAdapter::unified_placement_book_clear() const {
19044 for (const auto& handle : live_handles_) {
19045 const auto peer = placement_.find(handle.incarnation);
19046 if (peer == placement_.end()) continue;
19047 const auto& row = peer->second;
19048 const bool unpriced_close = (row.family == PineOrderFamily::Close
19049 || row.family == PineOrderFamily::CloseAll)
19050 && !finite_positive(row.exit_levels.limit) && !finite_positive(row.exit_levels.stop)
19051 && !finite_positive(row.exit_levels.trail_points)
19052 && !finite_positive(row.exit_levels.trail_price);
19053 if (!unpriced_close && row.family != PineOrderFamily::Margin) return false;
19054 }
19055 for (const auto& pending : pending_same_bar_commands_)
19056 if (pending.opening) return false;
19057 for (const auto& pending : pending_entries_)
19058 if (pending.snapshot.opening) return false;
19059 for (const auto& pending : delayed_market_orders_)
19060 if (pending.snapshot.opening) return false;
19061 return true;
19062}
19063
19064// The fill half: sig10(sig10(E) / (Q m)) against the tick-built fill quote
19065// moved by the slippage ticks (the next open; max/min(open, stop) for a stop;
19066// the signal close under process_orders_on_close), E frozen at the signal.
19067// Out of line and cold: a narrow scope's body, kept off the per-bar
19068// path, which is front-end bound (CMakeLists.txt, -falign-functions).
19069[[gnu::noinline, gnu::cold]]
19070bool PineExecutionAdapter::unified_fill_admits(const PlacementSnapshot& source, double units,
19071 double quote) const {
19072 const double tick = staged_.syminfo.mintick;
19073 if (!finite_positive(units) || !finite_positive(quote) || !finite_positive(tick)) return true;
19074 const double margin = (source.is_long ? config_.margin_long : config_.margin_short) / 100.0;
19075 const double equity = source.sizing.equity;
19076 const double per_unit = margin == 1.0
19077 ? source_money_round(source_money_round(equity) / units)
19078 : source_money_round(source_money_round(equity) / (units * margin));
19079 const double side = source.is_long ? 1.0 : -1.0;
19080 const double exec = (std::floor(quote / tick + 0.5) + side * config_.slippage) * tick;
19081 return !(per_unit < exec);
19082}
19083
19084// Margin rule 3's scope, as pinned: one commission-free long lot that a
19085// strategy.entry opened, at full margin with slippage, point value 1 and no
19086// FX series, on a lot grid of at most one, pyramiding at most one, no risk
19087// rule, and no order of the script resting beside it but the close_all of
19088// the bar. Without slippage the residual checkpoint
19089// (submit_tv_money_long_margin_call) owns the long.
19090// The configuration before the book: plain reads, so their order only decides
19091// how soon an out-of-scope run answers (every bar open asks). Full margin, no
19092// commission, slippage, a lot step of at most one, pyramiding at most one,
19093// point value 1 and no FX series are slipped_long_configured_, formed once
19094// per configuration.
19095[[gnu::always_inline]] inline bool PineExecutionAdapter::slipped_long_margin_scope() const {
19096 return slipped_long_configured_ && detail::margin_rule_switches().long_open_close_checks
19097 && source_margin_call_enabled_ && slipped_long_book_scope();
19098}
19099
19100// The book's part of the scope.
19101[[gnu::noinline]]
19102bool PineExecutionAdapter::slipped_long_book_scope() const {
19103 const auto position = detail::run_position(require_host());
19104 if (!(position.signed_units > 0.0) || position.lot_count != 1 || cap.active()
19105 || risk_.max_intraday_loss > 0.0 || risk_.max_drawdown > 0.0
19106 || risk_.max_cons_loss_days > 0 || pine_view_of(&require_host()) == nullptr
19107 || !pending_bracket_legs_.empty() || !pending_entries_.empty()
19108 || !pending_same_bar_commands_.empty() || !pending_relative_exits_.empty()
19109 || !source_shadow_pending_.empty()) {
19110 return false;
19111 }
19112 for (const auto& handle : live_handles_) {
19113 const auto found = placement_.find(handle.incarnation);
19114 if (found != placement_.end() && found->second.family != PineOrderFamily::Margin
19115 && found->second.family != PineOrderFamily::CloseAll) {
19116 return false;
19117 }
19118 }
19119 const CohortFacts* sole_cohort = nullptr;
19120 for (const auto& row : cohorts_by_id_.held_rows()) {
19121 if (row.second.opened.empty()) continue;
19122 if (sole_cohort != nullptr) return false;
19123 sole_cohort = &row.second;
19124 }
19125 if (sole_cohort == nullptr || sole_cohort->opened.size() != 1) return false;
19126 const auto placement = placement_.find(sole_cohort->opened.front().incarnation);
19127 return placement != placement_.end()
19128 && placement->second.family == PineOrderFamily::Entry
19129 && placement->second.opening && placement->second.is_long
19130 && !finite_positive(placement->second.exit_levels.limit)
19131 && !finite_positive(placement->second.exit_levels.stop)
19132 && !finite_positive(placement->second.exit_levels.trail_offset);
19133}
19134
19135// The units TradingView calls off such a long marked at `quote`, as it calls a
19136// short: the requirement is the position's value at the mark on the
19137// ten-significant-digit money ladder, the shortfall is that against the cash
19138// plus the open profit, four times its lot-floored quotient is called, and a
19139// shortfall under one unit's worth calls one unit (tests/fixtures/
19140// margin_call_rules: a synthetic F long of 2 at 25.00 with 49.999999999 cash
19141// is 1e-9 short at a 24.98 open and sells 1 at 24.96). The cash is the
19142// closed-trade equity -- the running net profit, not the G + L money -- so
19143// after a trade history a ten-digit tie can fall on either side of the cost:
19144// on a population NYSE:F 15-minute strategy TradingView's G + L money,
19145// illustrated by 49.99999999999999, is 7.1e-15 under 2 x 25.00 and calls, where a
19146// running sum need not (reconstructed from the trade set, not replayed). The
19147// money basis of the long call after a history is unpinned.
19148// Out of line and cold: a narrow scope's body, kept off the per-bar
19149// path, which is front-end bound (CMakeLists.txt, -falign-functions).
19150[[gnu::noinline, gnu::cold]]
19151double PineExecutionAdapter::slipped_long_margin_units(double quote) const {
19152 const auto* pine = pine_view_of(&require_host());
19153 const auto position = detail::run_position(require_host());
19154 const double held = position.signed_units;
19155 const double step = *staged_.quantity_grid;
19156 if (!pine || !finite_positive(quote) || !(held > 0.0)) return 0.0;
19157 const double required = source_money_round(held * quote);
19158 const double gain_loss = gain_loss_closed_equity();
19159 const double marked = (std::isfinite(gain_loss) ? gain_loss : pine->closed_trade_equity())
19160 + held * (quote - pine->position_entry_price_);
19161 const double deficit = required - marked;
19162 if (!(deficit > 0.0) || !std::isfinite(deficit)) return 0.0;
19163 // Only the one-unit call is pinned for a long: a shortfall whose
19164 // lot-floored quotient is a lot or more (four times it called) stays
19165 // with the engine's existing checkpoints.
19166 if (std::floor(source_money_round(deficit) / quote / step) > 0.0 || !(deficit / quote < 1.0))
19167 return 0.0;
19168 // Margin rule 3: the unit is called on negative free cash too; switched
19169 // off, a book whose cash is below its entry cost keeps it.
19170 const double free_cash = pine->closed_trade_equity() - held * pine->position_entry_price_;
19171 if (!detail::margin_rule_switches().negative_free_cash_call && free_cash < 0.0) return 0.0;
19172 return std::min(1.0, held);
19173}
19174
19175// Whether such a long marked at `quote` is short by less than one lot's
19176// worth of its ten-digit money -- the one-unit call's shortfall, which
19177// TradingView takes at the next open rather than at the fill that opened it
19178// (tests/fixtures/margin_call_rules long-admit-m10n: 1e-8 short at the
19179// 20:30 close fill, called at the 20:45 open).
19180// Out of line and cold: a narrow scope's body, kept off the per-bar
19181// path, which is front-end bound (CMakeLists.txt, -falign-functions).
19182[[gnu::noinline, gnu::cold]]
19183bool PineExecutionAdapter::slipped_long_unit_shortfall(double quote) const {
19184 const auto* pine = pine_view_of(&require_host());
19185 const double held = detail::run_position(require_host()).signed_units;
19186 if (!pine || !finite_positive(quote) || !(held > 0.0)) return false;
19187 const double gain_loss = gain_loss_closed_equity();
19188 const double deficit = source_money_round(held * quote)
19189 - ((std::isfinite(gain_loss) ? gain_loss : pine->closed_trade_equity())
19190 + held * (quote - pine->position_entry_price_));
19191 return deficit > 0.0 && deficit / quote < 1.0
19192 && std::floor(source_money_round(deficit) / quote / *staged_.quantity_grid) == 0.0;
19193}
19194
19195// Margin rule 4: a call at an extreme of the bar whose script closes the
19196// position, under calc_on_order_fills, recalculates the script there, and its
19197// close fills at the bar's next point -- the other extreme, or the close
19198// after the second one -- with the close's slippage (tests/fixtures/
19199// margin_call_rules coof-low-call-on: 2293.8 at the high after the
19200// low's call).
19201// Out of line and cold: a narrow scope's body, kept off the per-bar
19202// path, which is front-end bound (CMakeLists.txt, -falign-functions).
19203[[gnu::noinline, gnu::cold]]
19204bool PineExecutionAdapter::close_all_at_next_point(
19205 const Bar& bar, int fired,
19206 const std::vector<std::pair<native_order::RequestHandle, PlacementSnapshot>>& closes,
19207 const NativeDecisionContext& context) {
19208 if (!detail::margin_rule_switches().coof_next_point_close || !config_.calc_on_order_fills
19209 || !finite_positive(staged_.syminfo.mintick))
19210 return false;
19211 const auto position = detail::run_position(require_host());
19212 if (!(position.signed_units > 0.0)) return false;
19213 bool live = false;
19214 for (const auto& close : closes) {
19215 if (std::find(live_handles_.begin(), live_handles_.end(), close.first)
19216 != live_handles_.end()) {
19217 live = true;
19218 }
19219 }
19220 if (!live) return false;
19221 const bool high_first = source_path_uses_high_first(bar);
19222 const double next = fired == 1 ? (high_first ? bar.low : bar.high) : bar.close;
19223 const double tick = staged_.syminfo.mintick;
19224 const double quote = std::floor(next / tick + 0.5) * tick;
19225 native_order::Request request;
19226 request.intent = native_order::Flatten{};
19227 request.label = "__close__";
19228 PlacementSnapshot snapshot;
19230 snapshot.source_id = request.label;
19231 snapshot.forced_execution_price = source_margin_fill_price(quote, false);
19232 snapshot.sizing = sizing_snapshot();
19233 const auto accepted = submit_or_replace(std::move(request), std::move(snapshot), false,
19234 "__pine_close_all");
19235 if (!accepted) return false;
19236 (void)require_host().execute_current({*accepted, NativeCurrentPriceRule::NearestTick});
19237 return true;
19238}
19239
19240bool PineExecutionAdapter::submit_tv_money_long_margin_call(
19241 const Bar& bar, const NativeDecisionContext& context, int* fired_waypoint) {
19242 // The one-contract 10-significant-digit money residual is an adapter
19243 // policy over the native position and its ordinary chart path. It is not
19244 // a second matching loop: the resulting reduction is still a generic
19245 // current execution with an immutable source terms fact.
19246 const auto grid = staged_.quantity_grid;
19247 // Quiet: out of scope by the run's configuration alone, which the test
19248 // below would refuse before it reads the book.
19250 host_ != nullptr
19251 && (!source_margin_call_enabled_
19252 || std::abs(config_.margin_long - 100.0) > 1e-12
19253 || config_.commission_value != 0.0 || config_.slippage != 0
19254 || !grid || !(*grid > 0.0) || *grid > 1.0
19255 || std::abs(staged_.syminfo.pointvalue - 1.0) > 1e-12
19256 || !staged_.account_fx_effective_from_ms.empty()
19257 || cap.active() || risk_.max_intraday_loss > 0.0
19258 || risk_.max_drawdown > 0.0 || risk_.max_cons_loss_days > 0
19259 || last_margin_call_script_bar_ == context.script_bar_open_ms))) {
19260 return false;
19261 }
19262 // A one-lot book under process_orders_on_close takes the call whatever
19263 // pyramiding the strategy declares: at pyramiding 2 TradingView books the
19264 // same fourteen one-unit calls, trade for trade, as at 1 (lab tv tapes
19265 // tests/fixtures/margin_residual w5b-ms-eur-p100-pyr2 and
19266 // w5b-sz-eur-p100-10m-0410; none at 99 %, w5b-ms-eur-p99-pyr2; lane
19267 // W5B-ENG-MARGIN-RESIDUAL). ab9714be scoped it to pyramiding 1.
19268 const auto position = detail::run_position(require_host());
19269 if (!source_margin_call_enabled_
19270 || position.signed_units <= 0.0 || position.lot_count != 1
19271 || std::abs(config_.margin_long - 100.0) > 1e-12
19272 || config_.commission_value != 0.0 || config_.slippage != 0
19273 || !grid || !(*grid > 0.0) || *grid > 1.0
19274 || std::abs(staged_.syminfo.pointvalue - 1.0) > 1e-12
19275 || active_staged_fx(context.sub_bar_open_ms) != 1.0
19276 || !staged_.account_fx_effective_from_ms.empty()
19277 || cap.active() || risk_.max_intraday_loss > 0.0
19278 || risk_.max_drawdown > 0.0 || risk_.max_cons_loss_days > 0
19279 || last_margin_call_script_bar_ == context.script_bar_open_ms) {
19280 return false;
19281 }
19282 const double lot_value = *grid * bar.close * staged_.syminfo.pointvalue
19283 * active_staged_fx(context.sub_bar_open_ms);
19284 if (!config_.process_orders_on_close
19285 && (config_.pyramiding < 0 || config_.pyramiding > 1)
19286 && std::isfinite(lot_value) && lot_value >= 1.0) {
19287 // The high-value fractional extension is pinned only for a single
19288 // opening slot; low-value rounded-money path checks remain valid with
19289 // larger source pyramiding limits (open-money-before-priced-exit).
19290 return false;
19291 }
19292 if (position_open_priced_
19293 && (!std::isfinite(lot_value) || lot_value >= 1.0)) {
19294 return false;
19295 }
19296 // Margin rule 5: under process_orders_on_close the close_all the script
19297 // placed at this close waits behind the close point's call, sized before
19298 // it (the caller freezes it; tests/fixtures/margin_call_rules
19299 // size-eth-06-22-t1).
19300 const bool close_point_reversal = detail::margin_rule_switches().close_point_reversal
19301 && fired_waypoint != nullptr;
19302 if (config_.process_orders_on_close) {
19303 for (const auto& handle : live_handles_) {
19304 const auto found = placement_.find(handle.incarnation);
19305 if (found != placement_.end()
19306 && found->second.family != PineOrderFamily::Margin
19307 && !(close_point_reversal && found->second.family == PineOrderFamily::CloseAll
19308 && found->second.placement_script_open_ms == context.script_bar_open_ms)) {
19309 return false;
19310 }
19311 }
19312 }
19313
19314 int begin = 0;
19315 if (position_open_script_bar_ == context.script_bar_open_ms) {
19316 // A new position can see only the suffix after its actual native
19317 // opening point. The high-value residual witnesses deliberately
19318 // cover a true market opening at O; a close-time/priced entry cannot
19319 // retrospectively inspect this bar.
19320 if (position_open_phase_ != NativePathPhase::Open) return false;
19321 begin = 0;
19322 }
19323 const bool high_first = source_path_uses_high_first(bar);
19324 const double path[] = {bar.open, high_first ? bar.high : bar.low,
19325 high_first ? bar.low : bar.high, bar.close};
19326 const double quantity = position.signed_units;
19327 const double point_value = staged_.syminfo.pointvalue;
19328 // ab9714be pine_fills.cpp:1929-1965: a single-entry, commission-free book
19329 // whose cached net profit carries tracked addition roundoff narrows the
19330 // 1e-7 guard to what the current evaluation and that history support, so
19331 // a genuine 1e-7 rounding deficit fires (synthetic Q 10000 at 2.00000,
19332 // mark 2.00010: equity 20000.9999999 against required money 20001.00).
19333 const auto* pine = pine_view_of(&require_host());
19334 const bool supported_guard_scope = pine
19335 && pine->position_entry_count_ == 1 && pine->pyramid_entries_.size() == 1
19336 && pine->net_profit_sum_ == pine->net_profit_roundoff_value_
19337 && std::isfinite(pine->net_profit_roundoff_bound_);
19338 // Margin rule 3 on a book whose net profit carries no tracked roundoff:
19339 // TradingView checks the open, the low and the close -- not the high --
19340 // at the tick-built price c' = ticks x mintick, and calls one unit when
19341 // the free cash is below the ten-digit residual of the position's value,
19342 // sig10(Q c') - Q c', compared exactly (tests/fixtures/margin_call_rules:
19343 // 7.3e-12 at an ETH low calls, as a 5e-6 residual over 3e-6 of free cash
19344 // at a close c' one ULP above its decimal does). Negative free cash calls
19345 // nothing in this scope -- the loop moves on, and the guarded check below
19346 // is not reached: the call on negative free cash is pinned for the slipped
19347 // long only.
19348 const auto& rules = detail::margin_rule_switches();
19349 const bool exact_residual = rules.long_open_close_checks
19350 && finite_positive(staged_.syminfo.mintick)
19351 && supported_guard_scope && pine->net_profit_roundoff_bound_ == 0.0;
19352 for (int index = begin; index != 4; ++index) {
19353 if (exact_residual) {
19354 if ((index == 1 || index == 2) && path[index] == bar.high
19355 && bar.high != bar.low) {
19356 continue;
19357 }
19358 const double tick = staged_.syminfo.mintick;
19359 const double price = std::floor(path[index] / tick + 0.5) * tick;
19360 if (!finite_positive(price)) continue;
19361 const double gain_loss = gain_loss_closed_equity();
19362 const double free_cash = (std::isfinite(gain_loss) ? gain_loss
19363 : pine->closed_trade_equity())
19364 - quantity * pine->position_entry_price_;
19365 const double residual = source_money_round(quantity * price) - quantity * price;
19366 if (!(free_cash < residual) || free_cash < 0.0) continue;
19367 const double units = std::min(1.0, quantity);
19368 const double rounded_units = std::round(units / *grid) * *grid;
19369 const double guard = std::max({1e-12, std::abs(units) * 1e-12,
19370 std::abs(*grid) * 1e-9});
19371 if (units < quantity - guard && std::abs(rounded_units - units) > guard)
19372 return false;
19373 if (fired_waypoint) *fired_waypoint = index;
19374 return submit_margin_call_units(price, context, units, true);
19375 }
19376 const double price = path[index];
19377 if (!finite_positive(price)) continue;
19378 const double exact_value = quantity * price * point_value;
19379 const double equity = require_host().native_marked_equity(price);
19380 const double rounded_value = source_money_round(exact_value);
19381 // This trigger is exclusively for an exact-funded book whose
19382 // 10-significant-digit account valuation is fractionally larger.
19383 // Preserve the base 1e-7 guard for ordinary historical arithmetic.
19384 double arithmetic_guard = 1e-7;
19385 if (supported_guard_scope) {
19386 const double entry_value = quantity * pine->position_entry_price_ * point_value;
19387 const double money_scale = std::max({
19388 std::abs(pine->closed_trade_equity()),
19389 std::abs(pine->BacktestEngine::open_profit(price)),
19390 std::abs(entry_value), std::abs(exact_value), std::abs(equity),
19391 std::abs(rounded_value)});
19392 const double evaluation_guard = 8.0
19393 * std::numeric_limits<double>::epsilon() * money_scale;
19394 const double supported_guard = std::nextafter(
19395 evaluation_guard + pine->net_profit_roundoff_bound_,
19396 std::numeric_limits<double>::infinity());
19397 if (std::isfinite(supported_guard))
19398 arithmetic_guard = std::min(arithmetic_guard, supported_guard);
19399 }
19400 if (!std::isfinite(exact_value) || !std::isfinite(equity)
19401 || equity + arithmetic_guard < exact_value
19402 || !(equity + arithmetic_guard < rounded_value)) {
19403 continue;
19404 }
19405 const double units = std::min(1.0, quantity);
19406 const double rounded_units = std::round(units / *grid) * *grid;
19407 const double guard = std::max({1e-12, std::abs(units) * 1e-12,
19408 std::abs(*grid) * 1e-9});
19409 if (units < quantity - guard && std::abs(rounded_units - units) > guard)
19410 return false;
19411 // ab9714be pine_fills.cpp:1985-2020 books the one contract at
19412 // bar_fill_price(fire), the path point that fired, not wherever the
19413 // checkpoint callback happens to stand (the script-bar close).
19414 if (fired_waypoint) *fired_waypoint = index;
19415 return submit_margin_call_units(price, context, units, true);
19416 }
19417 return false;
19418}
19419
19420bool PineExecutionAdapter::slipped_pooc_opening_money_scope(
19421 const Bar& bar, const NativeDecisionContext& context) const {
19422 // pine_fills.cpp:1753-1775 and :1823-1868 at ab9714be: a terminal
19423 // process_orders_on_close MARKET entry with positive slippage is the sole
19424 // owner of the deferred opening-money check. Recover the retired lot
19425 // provenance from durable cohort and placement receipts.
19426 const auto position = detail::run_position(require_host());
19427 const auto grid = staged_.quantity_grid;
19428 if (!source_margin_call_enabled_ || !modeled_input()
19429 || !config_.process_orders_on_close || config_.slippage <= 0
19430 || position.signed_units <= 1.0 || position.lot_count != 1
19431 || std::abs(config_.margin_long - 100.0) > 1e-12
19432 || config_.pyramiding < 0 || config_.pyramiding > 1
19433 || config_.commission_value != 0.0
19434 || !grid || !(*grid > 0.0) || *grid >= 1.0
19435 || std::abs(staged_.syminfo.pointvalue - 1.0) > 1e-12
19436 || active_staged_fx(context.sub_bar_open_ms) != 1.0
19437 || !staged_.account_fx_effective_from_ms.empty()
19438 || context.driver_statistics.intrabar_path_enabled
19439 || cap.active() || risk_.max_intraday_loss > 0.0
19440 || risk_.max_drawdown > 0.0 || risk_.max_cons_loss_days > 0
19441 || position_open_phase_ != NativePathPhase::Close
19442 || position_open_priced_
19443 || position_open_script_bar_ == std::numeric_limits<std::int64_t>::min()
19444 || !live_handles_.empty() || !pending_bracket_legs_.empty()
19445 || !pending_entries_.empty() || !pending_same_bar_commands_.empty()
19446 || !pending_relative_exits_.empty() || !pending_coof_requests_.empty()
19447 || !source_shadow_pending_.empty()) {
19448 return false;
19449 }
19450
19451 const CohortFacts* sole_cohort = nullptr;
19452 for (const auto& row : cohorts_by_id_.held_rows()) {
19453 if (row.second.opened.empty()) continue;
19454 if (sole_cohort != nullptr) return false;
19455 sole_cohort = &row.second;
19456 }
19457 if (sole_cohort == nullptr || sole_cohort->opened.size() != 1
19458 || sole_cohort->live_units_by_origin.size() != 1) {
19459 return false;
19460 }
19461 const auto origin = sole_cohort->opened.front();
19462 const auto live_units = sole_cohort->live_units_by_origin.find(origin.incarnation);
19463 const auto placement = placement_.find(origin.incarnation);
19464 if (live_units == sole_cohort->live_units_by_origin.end()
19465 || !(live_units->second > 1.0)
19466 || placement == placement_.end()
19467 || placement->second.family != PineOrderFamily::Entry
19468 || !placement->second.opening || !placement->second.is_long
19469 || finite_positive(placement->second.exit_levels.limit)
19470 || finite_positive(placement->second.exit_levels.stop)
19471 || finite_positive(placement->second.exit_levels.trail_offset)
19472 || placement->second.placement_script_open_ms
19473 != position_open_script_bar_) {
19474 return false;
19475 }
19476
19477 const double lot_value = *grid * bar.close * staged_.syminfo.pointvalue;
19478 return std::isfinite(lot_value) && lot_value < 1.0;
19479}
19480
19481bool PineExecutionAdapter::submit_slipped_pooc_opening_money_call(
19482 const Bar& bar, const NativeDecisionContext& context) {
19483 // Quiet: out of scope by the run's configuration alone, which
19484 // slipped_pooc_opening_money_scope refuses before it reads the book.
19486 host_ != nullptr
19487 && (!source_margin_call_enabled_ || !modeled_input()
19488 || !config_.process_orders_on_close || config_.slippage <= 0
19489 || config_.commission_value != 0.0))) {
19490 return false;
19491 }
19492 if (!slipped_pooc_opening_money_scope(bar, context)
19493 || position_open_script_bar_ == context.script_bar_open_ms
19494 || last_margin_call_script_bar_ == context.script_bar_open_ms) {
19495 return false;
19496 }
19497 const auto position = detail::run_position(require_host());
19498 const double exact_value = position.signed_units * bar.open
19499 * staged_.syminfo.pointvalue;
19500 const double equity = require_host().native_marked_equity(bar.open);
19501 const double rounded_value = source_money_round(exact_value);
19502 if (!std::isfinite(exact_value) || !std::isfinite(equity)
19503 || equity < exact_value || !(equity < rounded_value)) {
19504 return false;
19505 }
19506
19507 return submit_margin_call_units(
19508 bar.open, context, std::min(1.0, position.signed_units), false);
19509}
19510
19511bool PineExecutionAdapter::schedule_tv_money_long_margin_before_trail(
19512 const Bar& bar, const NativeDecisionContext& context) {
19513 const auto grid = staged_.quantity_grid;
19514 // Quiet: out of scope by the run's configuration alone, which the test
19515 // below would refuse before it reads the book.
19517 host_ != nullptr
19518 && (!source_margin_call_enabled_ || config_.calc_on_order_fills || !modeled_input()
19519 || std::abs(config_.margin_long - 100.0) > 1e-12
19520 || config_.commission_value != 0.0 || config_.slippage != 0
19521 || config_.pyramiding < 0 || config_.pyramiding > 1
19522 || !grid || !(*grid > 0.0) || *grid >= 1.0
19523 || std::abs(staged_.syminfo.pointvalue - 1.0) > 1e-12
19524 || !staged_.account_fx_effective_from_ms.empty()
19525 || cap.active() || risk_.max_intraday_loss > 0.0
19526 || risk_.max_drawdown > 0.0 || risk_.max_cons_loss_days > 0
19527 || last_margin_call_script_bar_ == context.script_bar_open_ms))) {
19528 return false;
19529 }
19530 const auto position = detail::run_position(require_host());
19531 if (!source_margin_call_enabled_ || config_.calc_on_order_fills || !modeled_input()
19532 || position.signed_units <= 1.0 || position.lot_count != 1
19533 || std::abs(config_.margin_long - 100.0) > 1e-12
19534 || config_.commission_value != 0.0 || config_.slippage != 0
19535 || config_.pyramiding < 0 || config_.pyramiding > 1
19536 || !grid || !(*grid > 0.0) || *grid >= 1.0
19537 || std::abs(staged_.syminfo.pointvalue - 1.0) > 1e-12
19538 || active_staged_fx(context.sub_bar_open_ms) != 1.0
19539 || !staged_.account_fx_effective_from_ms.empty()
19540 || cap.active() || risk_.max_intraday_loss > 0.0
19541 || risk_.max_drawdown > 0.0 || risk_.max_cons_loss_days > 0
19542 || last_margin_call_script_bar_ == context.script_bar_open_ms) {
19543 return false;
19544 }
19545 const double lot_value = *grid * bar.close * staged_.syminfo.pointvalue
19546 * active_staged_fx(context.sub_bar_open_ms);
19547 if (position_open_priced_
19548 && (!std::isfinite(lot_value) || lot_value >= 1.0)) {
19549 return false;
19550 }
19551
19552 const PlacementSnapshot* owned_trail = nullptr;
19553 std::uint64_t owned_trail_incarnation = 0;
19554 for (const auto& handle : live_handles_) {
19555 const auto found = placement_.find(handle.incarnation);
19556 if (found == placement_.end()) continue;
19557 const auto& candidate = found->second;
19558 if (candidate.family == PineOrderFamily::Margin) continue;
19559 const bool exit = candidate.family == PineOrderFamily::ExitLimit
19560 || candidate.family == PineOrderFamily::ExitStop
19561 || candidate.family == PineOrderFamily::ExitTrail;
19562 if (exit && !candidate.from_entry.empty()
19563 && cohort_exposure_for(candidate.from_entry) == 0.0) {
19564 continue;
19565 }
19566 if (!exit || candidate.family != PineOrderFamily::ExitTrail
19567 || owned_trail != nullptr) {
19568 return false;
19569 }
19570 owned_trail = &candidate;
19571 owned_trail_incarnation = handle.incarnation;
19572 }
19573 if (owned_trail) {
19574 const bool full = !std::isfinite(owned_trail->requested_qty)
19575 && (!std::isfinite(owned_trail->qty_percent)
19576 || owned_trail->qty_percent >= 100.0);
19577 const bool relative = std::isfinite(owned_trail->exit_levels.trail_points)
19578 && !std::isfinite(owned_trail->exit_levels.trail_price)
19579 && std::isfinite(owned_trail->exit_levels.trail_offset)
19580 && owned_trail->exit_levels.trail_offset > 0.0;
19581 if (!full || !relative || !owned_trail->oca_name.empty()) return false;
19582 }
19583
19584 const bool high_first = source_path_uses_high_first(bar);
19585 const double path[] = {bar.open, high_first ? bar.high : bar.low,
19586 high_first ? bar.low : bar.high, bar.close};
19587 double fire_price = kNaN;
19588 int fire_point = -1;
19589 constexpr double kArithmeticGuard = 1e-7;
19590 for (int point = 0; point != 4; ++point) {
19591 const double price = path[point];
19592 if (!finite_positive(price)) continue;
19593 const double exact_value = position.signed_units * price
19594 * staged_.syminfo.pointvalue;
19595 const double equity = require_host().native_marked_equity(price);
19596 const double rounded_value = source_money_round(exact_value);
19597 if (std::isfinite(exact_value) && std::isfinite(equity)
19598 && equity + kArithmeticGuard >= exact_value
19599 && equity + kArithmeticGuard < rounded_value) {
19600 fire_price = price;
19601 fire_point = point;
19602 break;
19603 }
19604 }
19605 if (!finite_positive(fire_price)) return false;
19606 // A call the close fires is taken there after the script
19607 // (close_point_margin_call), not rested for the path to reach its price
19608 // first. Under process_orders_on_close it rests as before: it fills ahead
19609 // of the orders the script places at that close, which is where
19610 // TradingView executes it, and a close_all placed there keeps the size it
19611 // had before it (close).
19612 if (fire_point == 3 && !config_.process_orders_on_close) return false;
19613 if (config_.process_orders_on_close && owned_trail) {
19614 // ab9714be pine_fills.cpp:378-384, 1843-1846 and 1935-1937: under
19615 // POOC a resting order suppresses the carried rounded-money check
19616 // unless it is the owned trailing exit filling on THIS bar, and then
19617 // only a path point strictly before that fill may fire. Walk the
19618 // trail over the same waypoints from its state at the open.
19619 const double tick = staged_.syminfo.mintick;
19620 double activation = owned_trail->trail_activation_level;
19621 if (!finite_positive(activation) && finite_positive(tick)) {
19622 activation = require_host().position_avg_price()
19623 + source_trail_points_ticks(
19624 owned_trail->exit_levels.trail_points, tick) * tick;
19625 }
19626 const double offset = compat::pine::trail_offset_to_ticks(
19627 owned_trail->exit_levels.trail_offset) * tick;
19628 if (!finite_positive(activation) || !std::isfinite(offset)) return false;
19629 const auto state = trail_state_at_open_.find(owned_trail_incarnation);
19630 bool armed = state != trail_state_at_open_.end() && state->second.activated
19631 && finite_positive(state->second.best_price);
19632 double best = armed ? state->second.best_price : kNaN;
19633 int fill_point = -1;
19634 for (int point = 0; point != 4 && fill_point < 0; ++point) {
19635 const double price = path[point];
19636 if (!finite_positive(price)) continue;
19637 if (!armed) {
19638 if (price >= activation) {
19639 armed = true;
19640 best = price;
19641 }
19642 continue;
19643 }
19644 if (offset > 0.0 ? price <= best - offset : price < best) {
19645 fill_point = point;
19646 } else {
19647 best = std::max(best, price);
19648 }
19649 }
19650 if (fill_point < 0 || !(fire_point < fill_point)) return false;
19651 }
19652
19653 native_order::Request request;
19654 request.intent = native_order::Reduce{native_order::ExplicitUnits{
19655 std::min(1.0, position.signed_units)}};
19656 request.label = kMarginCallLabel;
19657 request.comment = "Margin call";
19658 request.trigger = fire_price <= bar.open
19659 ? native_order::Trigger{native_order::Stop{fire_price}}
19660 : native_order::Trigger{native_order::Limit{fire_price}};
19661 PlacementSnapshot snapshot;
19663 snapshot.source_id = request.label;
19664 snapshot.requested_qty = std::min(1.0, position.signed_units);
19665 snapshot.sizing = sizing_snapshot();
19666 return static_cast<bool>(submit_or_replace(
19667 std::move(request), std::move(snapshot), false,
19668 "__tv_money_margin_path__"));
19669}
19670
19671namespace {
19672
19673// A MARKET entry: no limit, stop or trail level. A flat bar's priced entries
19674// keep their own route (a stop pair that fires on one bar pyramids past the
19675// cap, TradingView probe 80, test_integration's flat-armed stops).
19676bool market_entry_row(const PlacementSnapshot& row) noexcept {
19677 return !finite_positive(row.exit_levels.limit) && !finite_positive(row.exit_levels.stop)
19678 && !finite_positive(row.exit_levels.trail_points)
19679 && !finite_positive(row.exit_levels.trail_price)
19680 && !finite_positive(row.exit_levels.trail_offset);
19681}
19682
19683// An add: an opening MARKET entry placed while the book held its own side.
19684bool add_entry_row(const PlacementSnapshot& row) noexcept {
19685 return row.family == PineOrderFamily::Entry && row.opening && market_entry_row(row)
19686 && row.projection_position_side
19687 == static_cast<std::int32_t>(row.is_long ? PositionSide::LONG : PositionSide::SHORT);
19688}
19689
19690// Two adds one script bar placed on the same side.
19691bool add_siblings(const PlacementSnapshot& a, const PlacementSnapshot& b) noexcept {
19692 return add_entry_row(a) && add_entry_row(b) && a.is_long == b.is_long
19693 && a.placement_script_open_ms == b.placement_script_open_ms;
19694}
19695
19696} // namespace
19697
19698// The script bar every live lot of the book was placed on, when each is an
19699// opening MARKET entry of the book's side placed while the book was flat on
19700// that one bar, and there are at least `min_lots` of them: a book only one
19701// bar's flat market entries opened (lane W5B-ENG-MARGIN-RESIDUAL;
19702// validate_precommit, on_applied and on_bar_open).
19703std::optional<std::int64_t> PineExecutionAdapter::flat_sibling_placement(
19704 std::size_t min_lots) const {
19705 const auto position = detail::run_position(require_host());
19706 if (position.signed_units == 0.0 || position.lot_count < min_lots) return std::nullopt;
19707 std::optional<std::int64_t> placed;
19708 for (const auto& cohort : cohorts_by_id_.held_rows()) {
19709 for (const auto& [incarnation, units] : cohort.second.live_units_by_origin) {
19710 if (!(units > 0.0)) continue;
19711 const auto found = placement_.find(incarnation);
19712 if (found == placement_.end()) return std::nullopt;
19713 const auto& lot = found->second;
19714 if (lot.family != PineOrderFamily::Entry || !lot.opening || !market_entry_row(lot)
19715 || lot.is_long != (position.signed_units > 0.0)
19716 || lot.projection_position_side
19717 != static_cast<std::int32_t>(PositionSide::FLAT)
19718 || (placed && *placed != lot.placement_script_open_ms)) {
19719 return std::nullopt;
19720 }
19721 placed = lot.placement_script_open_ms;
19722 }
19723 }
19724 return placed;
19725}
19726
19727// Whether `source`, a MARKET entry placed while the book was flat, meets at
19728// the point `interval_index`/`phase` a book of its own side that only its own
19729// bar's earlier flat market entries opened, there.
19730bool PineExecutionAdapter::flat_sibling_book(const PlacementSnapshot& source,
19731 std::int32_t interval_index,
19732 NativePathPhase phase) const {
19733 if (source.family != PineOrderFamily::Entry || !source.opening || !market_entry_row(source)
19734 || source.projection_position_side != static_cast<std::int32_t>(PositionSide::FLAT)) {
19735 return false;
19736 }
19737 const auto position = detail::run_position(require_host());
19738 if (position.signed_units == 0.0 || (position.signed_units > 0.0) != source.is_long
19739 || position_open_bar_index_ != interval_index || position_open_phase_ != phase) {
19740 return false;
19741 }
19742 const auto placed = flat_sibling_placement(1);
19743 return placed && *placed == source.placement_script_open_ms;
19744}
19745
19746// Whether another entry of `filled`'s bar, placed while flat on its side,
19747// still waits to fill at the same point `filled` just filled at: TradingView
19748// checks the margin of such a book once they have all filled (lane
19749// W5B-ENG-MARGIN-RESIDUAL, the opening checkpoint in on_applied).
19750bool PineExecutionAdapter::flat_sibling_fill_follows(
19751 const PlacementSnapshot& filled, const native_order::RequestHandle& filled_handle) const {
19752 if (filled.family != PineOrderFamily::Entry || !filled.opening || !market_entry_row(filled)
19753 || filled.projection_position_side != static_cast<std::int32_t>(PositionSide::FLAT)) {
19754 return false;
19755 }
19756 const auto position = detail::run_position(require_host());
19757 if (config_.pyramiding > 0
19758 && position.lot_count >= static_cast<std::size_t>(config_.pyramiding)) {
19759 return false;
19760 }
19761 return std::any_of(live_handles_.begin(), live_handles_.end(),
19762 [&](const native_order::RequestHandle& handle) {
19763 if (handle == filled_handle) return false;
19764 const auto found = placement_.find(handle.incarnation);
19765 if (found == placement_.end()) return false;
19766 const auto& row = found->second;
19767 return row.family == PineOrderFamily::Entry && row.opening
19768 && row.is_long == filled.is_long
19769 && row.projection_position_side
19770 == static_cast<std::int32_t>(PositionSide::FLAT)
19771 && row.placement_script_open_ms == filled.placement_script_open_ms
19772 && market_entry_row(row);
19773 });
19774}
19775
19776// Whether another add of `filled`'s bar -- a MARKET entry placed, as `filled`
19777// was, while the book held their side -- still waits to fill at the point
19778// `filled` just filled at: TradingView margins the grown book once they have
19779// all filled (lab tv tapes tests/fixtures/add_sibling_margin; lane TAIL-B).
19780bool PineExecutionAdapter::add_sibling_fill_follows(
19781 const PlacementSnapshot& filled, const native_order::RequestHandle& filled_handle) const {
19782 if (!add_entry_row(filled)) return false;
19783 const auto position = detail::run_position(require_host());
19784 if (config_.pyramiding > 0
19785 && position.lot_count >= static_cast<std::size_t>(config_.pyramiding)) {
19786 return false;
19787 }
19788 return std::any_of(live_handles_.begin(), live_handles_.end(),
19789 [&](const native_order::RequestHandle& handle) {
19790 if (handle == filled_handle) return false;
19791 const auto found = placement_.find(handle.incarnation);
19792 return found != placement_.end() && add_siblings(filled, found->second);
19793 });
19794}
19795
19796// The book's live lots that the adds the script bar `placed` placed on the
19797// book's side opened.
19798std::size_t PineExecutionAdapter::add_sibling_lots(std::int64_t placed) const {
19799 const auto position = detail::run_position(require_host());
19800 if (position.signed_units == 0.0) return 0;
19801 std::size_t count = 0;
19802 for (const auto& cohort : cohorts_by_id_.held_rows()) {
19803 for (const auto& [incarnation, units] : cohort.second.live_units_by_origin) {
19804 if (!(units > 0.0)) continue;
19805 const auto found = placement_.find(incarnation);
19806 if (found == placement_.end()) continue;
19807 const auto& lot = found->second;
19808 if (add_entry_row(lot) && lot.is_long == (position.signed_units > 0.0)
19809 && lot.placement_script_open_ms == placed) {
19810 ++count;
19811 }
19812 }
19813 }
19814 return count;
19815}
19816
19817// Whether TradingView follows a margin call of `called_units` on the book as it
19818// stands before that call with a call of one more unit at its next check
19819// point: on a whole-unit lot grid, a call of four times a floored restore of
19820// at least one lot (not the one unit a sub-lot restore takes) that leaves part
19821// of the book, when the book's oldest lot is one unit. Long or short, the
19822// oldest lot called whole or in part, at a market opening's fill (the next
19823// path point books it) or under process_orders_on_close at the next open (that
19824// open books it): lab tv tapes tests/fixtures/margin_residual
19825// w5b-sb-es-fixed-n, -pooc-fixed-n, w5b-es-short-q111111, -q1122,
19826// -pooc-q1122, -q115 (CME_MINI:ES1!) and w5b-f-q1x12 (NYSE:F). A book whose
19827// oldest lot is larger takes no follow-up (w5b-es-q21111, -pooc-q21111,
19828// -q222), nor a one-unit oldest lot on a finer grid (w5b-eth-q111, 1 ETH on
19829// 0.0001 lots); lane W5B-ENG-MARGIN-RESIDUAL.
19830bool PineExecutionAdapter::whole_unit_follow_up_due(double called_units, double mark) const {
19831 if (!staged_.quantity_grid || *staged_.quantity_grid != 1.0) return false;
19832 const double held = std::abs(detail::run_position(require_host()).signed_units);
19833 if (!(called_units >= 4.0) || !(called_units < held)) return false;
19834 const auto lots = require_host().native_open_lots(mark);
19835 return !lots.empty() && std::abs(std::abs(lots.front().signed_units) - 1.0) < 1e-9;
19836}
19837
19838// The runs whose margin calls TradingView follows (margin_follow_up_units):
19839// slipped, with no commission or a percent one, on a whole-unit lot grid --
19840// or on any lot grid under a percent commission: on OANDA:EURUSD's 0.01 lots
19841// at slippage 2 and commission 0.05 %, a synthetic short's call of 0.20
19842// can leave a deficit at its fill; if that deficit is 0.30 at a 2.00002
19843// fill, the next-point follow-up is 4 x 0.14 = 0.56 units on that lot grid
19844// (the margin_open_print fixtures establish the follow-up rule).
19845bool PineExecutionAdapter::margin_follow_up_scope() const noexcept {
19846 if (!(config_.slippage > 0) || !staged_.quantity_grid
19847 || !(*staged_.quantity_grid > 0.0)) {
19848 return false;
19849 }
19850 const bool percent = config_.commission_type == static_cast<int>(CommissionType::PERCENT);
19851 if (config_.commission_value != 0.0 && !percent) return false;
19852 return *staged_.quantity_grid == 1.0 || (config_.commission_value > 0.0 && percent);
19853}
19854
19855// What TradingView's follow-up to a margin call of `called_units` filled at
19856// `fill` takes. The book the call left is re-marked at that fill: without a
19857// commission the follow-up is one unit, due while that book is short by more
19858// than two of the slippage steps -- the called book short by more than the
19859// unit's margin and those steps (the follow-up rule; a close's call,
19860// followed so at the next open). Under a percent commission a call of any
19861// size is followed while the book it left, re-marked there, is short by more
19862// than two slippage steps for each unit called and the exit fees of those
19863// units and of one more, and the follow-up takes four times the shortfall's
19864// lot floor over the fill, or one unit where that floors to none: on NYSE:F
19865// at slippage 2 and commission 0.05 %, use synthetic check snapshots.
19866// A re-marked deficit of 30.00 at a 25.02 fill, above the fee/slip band,
19867// calls 4 x floor(30.00 / 25.02) = 4 whole shares at the next point.
19868// A sub-unit restore takes the one-unit fallback if it restores the book.
19869// A deficit of 0.01 below the fee/slip band takes no follow-up instead.
19870// These illustrate the margin_open_print fixtures without trade records;
19871// each snapshot supplies its own deficit. Zero where none is due.
19872double PineExecutionAdapter::margin_follow_up_units(
19873 double called_units, double fill, std::int64_t sub_bar_open_ms) const {
19874 const auto money = source_margin_money(fill, sub_bar_open_ms);
19875 if (!money.valid || !staged_.quantity_grid || !(*staged_.quantity_grid > 0.0)
19876 || !(called_units > internal::kQtyEpsilon)) {
19877 return 0.0;
19878 }
19879 const double fx = active_staged_fx(sub_bar_open_ms);
19880 const double step = config_.slippage * staged_.syminfo.mintick
19881 * staged_.syminfo.pointvalue * fx;
19882 const double shortfall = money.required - money.equity;
19883 const bool commissioned = config_.commission_value > 0.0
19884 && config_.commission_type == static_cast<int>(CommissionType::PERCENT);
19885 if (!commissioned) return shortfall > 2.0 * step ? std::min(1.0, money.held) : 0.0;
19886 // The book after the call has paid the called units' exit fees; the book
19887 // it called, re-marked at the fill, had not -- and the follow-up's own
19888 // unit pays one more.
19889 const double exit_fees = (called_units + 1.0) * fill * staged_.syminfo.pointvalue * fx
19890 * config_.commission_value / 100.0;
19891 if (!(shortfall - exit_fees > 2.0 * step * called_units)) return 0.0;
19892 const double grid = *staged_.quantity_grid;
19893 const double minimum = std::floor(
19894 source_money_round(shortfall) / money.unit_margin / grid) * grid;
19895 double units = std::floor(4.0 * minimum / grid + 1e-6) * grid;
19896 if (!(units > internal::kQtyEpsilon)) units = 1.0;
19897 units = std::min(units, money.held);
19898 return units > internal::kQtyEpsilon && std::isfinite(units) ? units : 0.0;
19899}
19900
19901// TradingView follows a margin call on a whole-unit lot grid at the bar's
19902// next path point while the call did not restore the book at its own fill
19903// (margin_follow_up_units) -- without a commission a one-unit call only: one
19904// of four called at a synthetic 25.00 high, 0.10 short at its 25.01 fill, is
19905// not followed at the 24.94 low (the margin_opposite fixtures establish
19906// the one-unit restriction). That point books the follow-up where its own
19907// check calls nothing and its print is inside the call's fill (below it for a
19908// short); a print at or beyond the fill is judged by its own check alone. A
19909// follow-up so taken is followed alike, and the close is the last point a bar
19910// offers. A long's call is never followed -- its sell lowers the requirement
19911// with the equity -- nor is a call at zero slippage, whose fill is its mark.
19912// Synthetic NYSE:F snapshots at slippage 1 illustrate the three cases:
19913// a one-share call at 25.00 leaving a 0.10 re-marked deficit can owe one
19914// more at a later low of 24.97; a call at 26.00 leaving a 0.01 surplus
19915// owes none. A call at 27.00 followed by a 27.005 high reaches a point
19916// beyond that fill: its own check, rather than the owed follow-up, decides.
19917// If that check calls, a later low of 26.95 may follow the new call.
19918// These are independent synthetic checks of the follow-up rule in slipped_short.
19919// `called_units` is the call's size, `fill` its own fill,
19920// `current` the print its check point fired at.
19921void PineExecutionAdapter::follow_margin_call(
19922 double called_units, double fill, double current,
19923 const NativeDecisionContext& context) {
19924 if (!(called_units > internal::kQtyEpsilon) || !margin_follow_up_scope()) return;
19925 if (config_.commission_value == 0.0 && std::abs(called_units - 1.0) > 1e-9) return;
19926 const Bar& bar = policy_script_bar_;
19927 const bool high_first = source_path_uses_high_first(bar);
19928 const double tick = staged_.syminfo.mintick;
19929 const double guard = tick * 1e-6;
19930 auto phase = context.coordinate.path_phase;
19931 double at = current;
19932 double called = called_units;
19933 while (finite_positive(fill)) {
19934 const auto position = detail::run_position(require_host());
19935 if (position.signed_units == 0.0) return;
19936 const bool short_book = position.signed_units < 0.0;
19937 const double units = margin_follow_up_units(called, fill, context.sub_bar_open_ms);
19938 if (!(units > 0.0)) return;
19939 double next = next_source_path_waypoint(bar, phase, at, high_first);
19940 auto next_phase = NativePathPhase::None;
19941 if (phase == NativePathPhase::Open) {
19942 next_phase = high_first ? NativePathPhase::High : NativePathPhase::Low;
19943 } else if (std::isfinite(next)) {
19944 next_phase = next == bar.high ? NativePathPhase::High : NativePathPhase::Low;
19945 } else if (phase == NativePathPhase::High || phase == NativePathPhase::Low) {
19946 next = bar.close;
19947 next_phase = NativePathPhase::Close;
19948 }
19949 if (!finite_positive(next) || next_phase == NativePathPhase::None) return;
19950 const double next_mark = nearest_tick(next, tick);
19951 if (short_book ? next_mark >= fill - guard : next_mark <= fill + guard) return;
19952 if (source_margin_units(source_margin_money(next, context.sub_bar_open_ms), false) > 0.0)
19953 return;
19954 // A follow-up that falls on the close is booked there after the
19955 // script (close_point_margin_call).
19956 if (next_phase == NativePathPhase::Close && close_point_margin_scope()) {
19957 close_margin_follow_up_bar_ = context.script_bar_open_ms;
19958 close_margin_follow_up_units_ = units;
19959 return;
19960 }
19961 if (!submit_margin_call_units(next, context, units, true)) return;
19962 fill = source_margin_fill_price(next, short_book);
19963 called = units;
19964 at = next;
19965 phase = next_phase;
19966 }
19967}
19968
19969// The runs whose short calls take the lagged follow-up (switch
19970// lagged_short_follow_up): a fractional lot grid, at a percent commission or
19971// none, point money on one FX rate. A whole-share book keeps its pinned
19972// follow-ups (whole_unit_follow_up_due, margin_follow_up_units).
19973bool PineExecutionAdapter::lagged_short_scope() const noexcept {
19975 && source_margin_call_enabled_ && staged_.quantity_grid
19976 && *staged_.quantity_grid > 0.0 && *staged_.quantity_grid < 1.0
19977 && staged_.account_fx_effective_from_ms.empty()
19978 && (config_.commission_value == 0.0
19979 || config_.commission_type == static_cast<int>(CommissionType::PERCENT));
19980}
19981
19982// The follow-up TradingView takes after a short's call (switch
19983// lagged_short_follow_up). Its broker checks the book again at the call's fill
19984// `fill`, lot by lot, first in first out, on the state that has taken the
19985// called units of a lot out of the book but not yet booked their P&L or exit
19986// fee -- each earlier lot booked, at the fill, net of its exit fee -- and the
19987// last of those checks still short is called again: four times its
19988// lot-floored restore at the fill, at the bar's next path point and ungated.
19989// `taken` lists the units the call took from each lot with the lot's entry
19990// price, `equity_before` is the equity at the fill before the call and
19991// `held_before` the units held then. On OANDA:EURUSD at slippage 1 a short of
19992// synthetic 50000 units can remain short after a 0.40 call at 2.00001:
19993// the last still-short unbooked-lot check, not the final booked state,
19994// decides the next call. At slippage 0, synthetic lots of 0.25 and 40000
19995// can likewise leave the first lot's check short after that lot is removed;
19996// its lot-floored restore is followed at the close (lab tv tapes tests/fixtures/margin_call_rules
19997// short-cutoff-gate/cutoff-*, tests/fixtures/margin_schedule_rules lag/).
19998double PineExecutionAdapter::lagged_short_follow_up_units(
19999 const std::vector<std::pair<double, double>>& taken, double equity_before,
20000 double held_before, double fill, std::int64_t sub_bar_open_ms) const {
20001 if (!lagged_short_scope() || taken.empty() || !finite_positive(fill)
20002 || !std::isfinite(equity_before) || !finite_positive(held_before)
20003 || !finite_positive(config_.margin_short)) {
20004 return 0.0;
20005 }
20006 const double grid = *staged_.quantity_grid;
20007 const double fx = active_staged_fx(sub_bar_open_ms);
20008 const double point = staged_.syminfo.pointvalue * fx;
20009 const double unit_margin = fill * point * (config_.margin_short / 100.0);
20010 const double fee = config_.commission_value / 100.0;
20011 if (!finite_positive(unit_margin)) return 0.0;
20012 const double lot_value = grid * fill * point;
20013 double out = 0.0;
20014 double booked_fees = 0.0;
20015 double last_short = 0.0;
20016 for (const auto& [units, entry] : taken) {
20017 if (!(units > 0.0)) continue;
20018 out += units;
20019 const double remaining = held_before - out;
20020 if (!(remaining > internal::kQtyEpsilon)) break;
20021 const double exact = remaining * unit_margin;
20022 const double required = std::isfinite(lot_value) && lot_value < 1.0
20023 ? source_money_round(exact) : exact;
20024 // The called units are out of the book: their open P&L at the fill,
20025 // (entry - fill) a unit for a short, leaves the equity unbooked.
20026 const double equity = equity_before - booked_fees + (fill - entry) * units * point;
20027 const double deficit = required - equity;
20028 if (deficit > 0.0) last_short = deficit;
20029 booked_fees += units * fill * point * fee;
20030 }
20031 if (!(last_short > 0.0) || !std::isfinite(last_short)) return 0.0;
20032 const double minimum = std::floor(source_money_round(last_short) / unit_margin / grid) * grid;
20033 double units = std::floor(4.0 * minimum / grid + 1e-6) * grid;
20034 units = std::min(units, held_before - out);
20035 if (!(units > internal::kQtyEpsilon) || !std::isfinite(units)) return 0.0;
20036 return units;
20037}
20038
20039// The opening-call schedule's scope (MarginOpeningSwitches): a full-margin
20040// position of one lot that a process_orders_on_close close fill opened, at a
20041// percent commission with slippage, on a lot grid of at most one, point value
20042// 1 on one FX rate -- the shape the tapes under
20043// tests/fixtures/margin_open_rules pin. The caller checks that the fill was
20044// the previous bar's close; a book two or more entries or adds of that bar
20045// opened keeps its own rules, except for the call sized at that close on a
20046// fractional lot grid (`several_lots`, switch lot_by_lot_open_follow_up):
20047// every lot the close's fills opened is in its book -- an over-fill lot a
20048// frozen close opened (close_follow_up_after_script) and the entry beside it,
20049// or several entries -- and its follow-up is checked lot by lot
20050// (open_print_lot_follow_up_units; lab tv tapes
20051// tests/fixtures/margin_open_rules lots/).
20052bool PineExecutionAdapter::close_sized_open_call_scope(bool short_book, bool several_lots) const {
20053 const double margin = short_book ? config_.margin_short : config_.margin_long;
20054 if (!config_.process_orders_on_close || config_.calc_on_order_fills
20055 || !(config_.slippage > 0) || !source_margin_call_enabled_
20056 || config_.commission_type != static_cast<int>(CommissionType::PERCENT)
20057 || !(config_.commission_value > 0.0) || std::abs(margin - 100.0) > 1e-12
20058 || !staged_.quantity_grid || !(*staged_.quantity_grid > 0.0)
20059 || *staged_.quantity_grid > 1.0 || staged_.syminfo.pointvalue != 1.0
20060 || staged_.account_fx != 1.0 || !staged_.account_fx_effective_from_ms.empty()
20061 || !finite_positive(staged_.syminfo.mintick)) {
20062 return false;
20063 }
20064 const auto position = detail::run_position(require_host());
20065 if (position.signed_units == 0.0 || (position.signed_units < 0.0) != short_book) return false;
20066 // Several lots on a fractional lot grid only: a whole-share book keeps its
20067 // one-unit follow-ups (whole_unit_follow_up_due), which no tape has decided
20068 // beside this schedule.
20069 return position.lot_count == 1
20070 || (several_lots && position.lot_count > 1 && *staged_.quantity_grid < 1.0
20071 && PF_RARE(detail::margin_opening_switches().lot_by_lot_open_follow_up));
20072}
20073
20074// The call TradingView sizes at the source close c' (the tick-built close the
20075// position filled at, slippage aside) and executes at the next open's print
20076// `print`: money and unit margin at c', long or short (switch
20077// close_sized_long_call for a long), its one-unit band and the short gate
20078// priced at that print (switch close_call_gate_at_print). On OANDA:EURUSD at
20079// slippage 1, a synthetic deficit of 50.0005 at a 2.00000 close takes 4 x
20080// floor(25.00025 / 0.01) x 0.01 = 100.00; a 2.00004 open gives 99.96. A
20081// synthetic NYSE:F short at slippage 2 can pass the band at a 25.00 open
20082// print and fail it at the close's own 25.02 print (lab tv tapes
20083// tests/fixtures/margin_open_rules po/ and replica/).
20084double PineExecutionAdapter::close_sized_open_units(
20085 double close_mark, double print, std::int64_t sub_bar_open_ms) const {
20086 const auto money = source_margin_money(close_mark, sub_bar_open_ms);
20087 if (!money.valid) return 0.0;
20089 return source_margin_units(money, false, true, at_print ? print : kNaN);
20090}
20091
20092// The follow-up TradingView takes after a call of `called` units filled at
20093// `fill` on a one-lot book: the book checked again at the fill with the
20094// called units out of it but their P&L and exit fee unbooked
20095// (`equity_before` is the equity at the fill before the call, `entry` the
20096// lot's entry price), four times its lot-floored restore at the fill, or one
20097// unit where that floors to none and the unit restores the book there -- the
20098// one-unit fallback, on any lot grid (lab tv tapes
20099// tests/fixtures/margin_open_rules po/short-follow-up-one-unit: 0.0004 and
20100// then 1 ETH at one print; lag/whole-share-follow-up: 1 + 1 NYSE:F shares).
20101double PineExecutionAdapter::open_print_follow_up_units(
20102 double held_before, double equity_before, double entry, double called, double fill,
20103 bool short_book, std::int64_t sub_bar_open_ms) const {
20104 if (!staged_.quantity_grid || !(*staged_.quantity_grid > 0.0) || !finite_positive(fill)
20105 || !finite_positive(entry) || !std::isfinite(equity_before)
20106 || !(called > internal::kQtyEpsilon)) {
20107 return 0.0;
20108 }
20109 const double remaining = held_before - called;
20110 if (!(remaining > internal::kQtyEpsilon)) return 0.0;
20111 const double grid = *staged_.quantity_grid;
20112 const double point = staged_.syminfo.pointvalue * active_staged_fx(sub_bar_open_ms);
20113 const double margin = (short_book ? config_.margin_short : config_.margin_long) / 100.0;
20114 const double unit_margin = fill * point * margin;
20115 if (!finite_positive(unit_margin)) return 0.0;
20116 const double exact = remaining * unit_margin;
20117 const double lot_value = grid * fill * point;
20118 const double required = std::isfinite(lot_value) && lot_value < 1.0
20119 ? source_money_round(exact) : exact;
20120 // The called units' open P&L at the fill leaves the equity unbooked.
20121 const double side = short_book ? -1.0 : 1.0;
20122 const double equity = equity_before - side * (fill - entry) * called * point;
20123 const double deficit = required - equity;
20124 if (!(deficit > 0.0) || !std::isfinite(deficit)) return 0.0;
20125 const double minimum = std::floor(source_money_round(deficit) / unit_margin / grid) * grid;
20126 double units = std::floor(4.0 * minimum / grid + 1e-6) * grid;
20127 if (!(units > internal::kQtyEpsilon) && exact - equity < unit_margin)
20128 units = std::min(1.0, remaining);
20129 units = std::min(units, remaining);
20130 return units > internal::kQtyEpsilon && std::isfinite(units) ? units : 0.0;
20131}
20132
20133// open_print_follow_up_units on a book of several lots (switch
20134// lot_by_lot_open_follow_up): the call's units are taken first in first out,
20135// `taken` listing the units it took from each lot with the lot's entry price,
20136// and the book is checked after each lot as the lagged follow-up checks it
20137// (lagged_short_follow_up_units), long or short -- that lot's called units out
20138// of the book with their P&L and exit fee unbooked, the earlier lots' booked
20139// at the fill net of their exit fees, the rest re-marked there -- and the
20140// last check still short is called four times its lot-floored restore at the
20141// fill, with the one-unit fallback. Synthetic OANDA:EURUSD longs of 10
20142// and 40000 units can have their initial call cross a lot boundary;
20143// with lots of 5, 10 and 40000, it can cross two boundaries instead.
20144// In either case the last still-short check sizes the follow-up (lab tv tapes
20145// tests/fixtures/margin_open_rules lots/two-longs-one-close,
20146// lots/three-longs-last-short).
20147double PineExecutionAdapter::open_print_lot_follow_up_units(
20148 const std::vector<std::pair<double, double>>& taken, double equity_before,
20149 double held_before, double fill, bool short_book, std::int64_t sub_bar_open_ms) const {
20150 if (!staged_.quantity_grid || !(*staged_.quantity_grid > 0.0) || taken.empty()
20151 || !finite_positive(fill) || !std::isfinite(equity_before)
20152 || !finite_positive(held_before)) {
20153 return 0.0;
20154 }
20155 const double grid = *staged_.quantity_grid;
20156 const double point = staged_.syminfo.pointvalue * active_staged_fx(sub_bar_open_ms);
20157 const double margin = (short_book ? config_.margin_short : config_.margin_long) / 100.0;
20158 const double unit_margin = fill * point * margin;
20159 if (!finite_positive(unit_margin)) return 0.0;
20160 const double fee = config_.commission_value / 100.0;
20161 const double side = short_book ? -1.0 : 1.0;
20162 const double lot_value = grid * fill * point;
20163 double out = 0.0;
20164 double booked_fees = 0.0;
20165 double last_short = 0.0;
20166 double last_exact = 0.0;
20167 double last_equity = 0.0;
20168 double last_remaining = 0.0;
20169 for (const auto& [units, entry] : taken) {
20170 if (!(units > 0.0)) continue;
20171 out += units;
20172 const double remaining = held_before - out;
20173 if (!(remaining > internal::kQtyEpsilon)) break;
20174 const double exact = remaining * unit_margin;
20175 const double required = std::isfinite(lot_value) && lot_value < 1.0
20176 ? source_money_round(exact) : exact;
20177 const double equity = equity_before - booked_fees - side * (fill - entry) * units * point;
20178 const double deficit = required - equity;
20179 if (deficit > 0.0) {
20180 last_short = deficit;
20181 last_exact = exact;
20182 last_equity = equity;
20183 last_remaining = remaining;
20184 }
20185 booked_fees += units * fill * point * fee;
20186 }
20187 if (!(last_short > 0.0) || !std::isfinite(last_short)) return 0.0;
20188 const double minimum = std::floor(source_money_round(last_short) / unit_margin / grid) * grid;
20189 double units = std::floor(4.0 * minimum / grid + 1e-6) * grid;
20190 if (!(units > internal::kQtyEpsilon) && last_exact - last_equity < unit_margin)
20191 units = std::min(1.0, last_remaining);
20192 units = std::min(units, held_before - out);
20193 return units > internal::kQtyEpsilon && std::isfinite(units) ? units : 0.0;
20194}
20195
20196// The calls the open takes after a call sized at the source close
20197// (close_sized_open_units): that call at the open's print, then the book it
20198// booked checked at the open's own mark -- unit margin at the open, its
20199// one-unit band and the short gate priced at the same print (switch
20200// booked_open_recheck; short there with nothing booked, the sequence ends)
20201// -- and then the follow-up of the last call at that print
20202// (open_print_follow_up_units, switch open_print_follow_up), except that a
20203// short's re-check that booked owes it to the bar's first extreme (switch
20204// short_point_drops_owed). On
20205// a synthetic OANDA:EURUSD short at slippage 1 can give up 0.20 at an open
20206// three ticks above the close, then 0.12 on a still-short booked book;
20207// a larger weekend gap can enlarge both calls (lab tv tapes
20208// tests/fixtures/margin_open_rules po/short-booked-recheck,
20209// po/short-weekend-gap). TradingView does not repeat these calls at one
20210// price: on_applied's recheck_at_fill stands down while they book.
20211bool PineExecutionAdapter::run_close_sized_open_calls(
20212 double units, double opening_mark, const NativeDecisionContext& context) {
20213 const auto position = detail::run_position(require_host());
20214 if (position.signed_units == 0.0 || !(units > 0.0)) return false;
20215 const bool short_book = position.signed_units < 0.0;
20216 const double print = source_margin_fill_price(opening_mark, short_book);
20217 const auto& rules = detail::margin_opening_switches();
20218 // A book of several lots keeps each lot's units and entry price, first in
20219 // first out, for the follow-up's lot-by-lot check (switch
20220 // lot_by_lot_open_follow_up).
20221 struct Before {
20222 double held = 0.0, equity = kNaN, entry = kNaN;
20223 std::vector<std::pair<double, double>> lots;
20224 };
20225 const auto before_call = [&]() {
20226 Before b;
20227 b.held = std::abs(detail::run_position(require_host()).signed_units);
20228 b.equity = source_margin_money(print, context.sub_bar_open_ms).equity;
20229 const auto lots = require_host().native_open_lots(print);
20230 if (lots.size() == 1) {
20231 b.entry = lots.front().entry_price;
20232 } else if (PF_RARE(rules.lot_by_lot_open_follow_up)) {
20233 for (const auto& lot : lots)
20234 b.lots.emplace_back(std::abs(lot.signed_units), lot.entry_price);
20235 }
20236 return b;
20237 };
20238 const auto called_since = [&](const Before& b) {
20239 return b.held - std::abs(detail::run_position(require_host()).signed_units);
20240 };
20241 open_call_sequence_ = true;
20242 Before last = before_call();
20243 if (!submit_margin_call_units(opening_mark, context, units)) {
20244 open_call_sequence_ = false;
20245 return false;
20246 }
20247 double last_called = called_since(last);
20248 bool rechecked = false; // the booked re-check booked units
20249 if (rules.booked_open_recheck && detail::run_position(require_host()).signed_units != 0.0) {
20250 // The open's check of the booked book is a check like any other: its
20251 // call passes the short gate at the print (short_call_vetoed), and
20252 // where it finds the book short but books nothing the open is a point
20253 // short itself, so the follow-up the call before it owes is dropped
20254 // as at a path point (execute_owed_short_follow_up). On OANDA:EURUSD
20255 // at slippage 1, consider an independent synthetic re-check snapshot:
20256 // 50000 units 0.12 short at a 2.00003 mark would call 0.20.
20257 // At the 2.00004 fill those units cover about 0.40, below the
20258 // book's 0.62 deficit, so the short gate vetoes the re-check.
20259 // Neither that call nor the preceding call's owed follow-up books;
20260 // the next high is decided by its own check.
20261 const auto booked = source_margin_money(opening_mark, context.sub_bar_open_ms);
20262 const double again = source_margin_units(booked, false, true, print);
20263 if (again > 0.0) {
20264 const Before before = before_call();
20265 if (submit_margin_call_units(opening_mark, context, again)) {
20266 last = before;
20267 last_called = called_since(before);
20268 rechecked = true;
20269 }
20270 } else if (booked.valid && booked.required > booked.equity) {
20271 open_call_sequence_ = false;
20272 return true;
20273 }
20274 }
20275 // A booked re-check on a book of several lots is followed by nothing: the
20276 // lot-by-lot follow-up is pinned only where the re-check books nothing (lab
20277 // tv tape lots/two-shorts-one-close), and no tape has decided the one a
20278 // short's booked re-check owes to the first extreme there.
20279 if (PF_RARE(rechecked && !last.lots.empty())) {
20280 open_call_sequence_ = false;
20281 return true;
20282 }
20283 if (rules.open_print_follow_up && detail::run_position(require_host()).signed_units != 0.0) {
20284 double more = 0.0;
20285 if (last.lots.empty()) {
20286 more = open_print_follow_up_units(
20287 last.held, last.equity, last.entry, last_called, print, short_book,
20288 context.sub_bar_open_ms);
20289 } else {
20290 std::vector<std::pair<double, double>> taken;
20291 double left = last_called;
20292 for (const auto& [size, entry] : last.lots) {
20293 if (!(left > internal::kQtyEpsilon)) break;
20294 const double take = std::min(left, size);
20295 taken.emplace_back(take, entry);
20296 left -= take;
20297 }
20298 more = open_print_lot_follow_up_units(taken, last.equity, last.held, print,
20299 short_book, context.sub_bar_open_ms);
20300 }
20301 if (more > 0.0) {
20302 // A re-check that booked owes its follow-up to the bar's first
20303 // extreme, where a point short itself drops it (rule 7), as the
20304 // open's own call does (open_call_owing_follow_up). On
20305 // synthetic OANDA:EURUSD at slippage 1: a re-check of 0.20 at a 2.00001
20306 // print can owe 0.12 to the low, booked at 1.99981; where
20307 // the first extreme is a high short itself, TradingView drops the
20308 // owed 0.24 and books the high's own 40.00 instead (lab tv tapes
20309 // tests/fixtures/margin_open_rules po/short-recheck-owes-low,
20310 // po/short-recheck-owed-dropped-high).
20311 if (rechecked && short_book && PF_RARE(rules.short_point_drops_owed)
20312 && short_path_points_scope(context)) {
20313 owed_follow_up_bar_ = context.script_bar_open_ms;
20314 owed_follow_up_point_ = 1;
20315 owed_follow_up_units_ = more;
20316 } else {
20317 (void)submit_margin_call_units(opening_mark, context, more);
20318 }
20319 }
20320 }
20321 open_call_sequence_ = false;
20322 return true;
20323}
20324
20325// A short's call of `units` at `mark` taken by the adapter itself, executed at
20326// the mark's print with the exit side's slippage; with the lagged follow-up in
20327// scope, the follow-up it owes the bar's next path point (zero for none). The
20328// book and the money at the fill are read before the call books. On a
20329// whole-share book the follow-up is whole_share_lag_units' (switch
20330// whole_share_lagged_short for a point's own call, chained_follow_up for an
20331// owed follow-up booked there, `owed`).
20332double PineExecutionAdapter::call_short_with_lagged_follow_up(
20333 double mark, double units, const NativeDecisionContext& context, bool owed) {
20334 const auto before = detail::run_position(require_host());
20335 if (!(before.signed_units < 0.0)) {
20336 (void)submit_margin_call_units(mark, context, units, true);
20337 return 0.0;
20338 }
20339 const double fill = source_margin_fill_price(mark, true);
20340 const auto& opening = detail::margin_opening_switches();
20341 const bool whole = PF_RARE(whole_share_lag_scope())
20342 && (owed ? opening.chained_follow_up : opening.whole_share_lagged_short);
20343 const bool lagged = (whole || lagged_short_scope()) && finite_positive(fill);
20344 const auto money = lagged ? source_margin_money(fill, context.sub_bar_open_ms)
20346 const auto lots = lagged ? require_host().native_open_lots(fill) : std::vector<NativeOpenLot>{};
20347 if (!submit_margin_call_units(mark, context, units, true)) return 0.0;
20348 if (!lagged || !money.valid) return 0.0;
20349 const double held_before = std::abs(before.signed_units);
20350 const double called = held_before - std::abs(detail::run_position(require_host()).signed_units);
20351 std::vector<std::pair<double, double>> taken;
20352 double left = called;
20353 for (const auto& lot : lots) {
20354 if (!(left > internal::kQtyEpsilon)) break;
20355 const double size = std::abs(lot.signed_units);
20356 const double take = std::min(left, size);
20357 taken.emplace_back(take, lot.entry_price);
20358 left -= take;
20359 }
20360 return whole ? whole_share_lag_units(taken, money.equity, held_before, fill,
20361 context.sub_bar_open_ms)
20362 : lagged_short_follow_up_units(taken, money.equity, held_before, fill,
20363 context.sub_bar_open_ms);
20364}
20365
20366// The whole-share books whose short calls take the lagged follow-up
20367// (switch whole_share_lagged_short, with lagged_short_follow_up): a lot grid
20368// of one share, at a percent commission or none, point money on one FX rate.
20369bool PineExecutionAdapter::whole_share_lag_scope() const noexcept {
20371 && source_margin_call_enabled_ && staged_.quantity_grid
20372 && *staged_.quantity_grid == 1.0 && staged_.account_fx_effective_from_ms.empty()
20373 && (config_.commission_value == 0.0
20374 || config_.commission_type == static_cast<int>(CommissionType::PERCENT));
20375}
20376
20377// lagged_short_follow_up_units on a whole-share book, with the one-unit
20378// fallback: where the last still-short check's restore floors to no lot, one
20379// share is called if it restores that book at the fill. Synthetic NYSE:F:
20380// at full margin, 200 shares at 25.00 require 5000.00; an unbooked prior unit
20381// leaves 199 at 4975.00. With adjusted equity 4965.00, the 10.00 shortfall
20382// floors to zero whole units and calls one more; an eagerly booked
20383// close would use another money basis (lab tv tapes
20384// tests/fixtures/margin_open_rules lag/whole-share-chain,
20385// replica/f-path-follow-up-0209).
20386double PineExecutionAdapter::whole_share_lag_units(
20387 const std::vector<std::pair<double, double>>& taken, double equity_before,
20388 double held_before, double fill, std::int64_t sub_bar_open_ms) const {
20389 if (taken.empty() || !finite_positive(fill) || !std::isfinite(equity_before)
20390 || !finite_positive(held_before) || !finite_positive(config_.margin_short)) {
20391 return 0.0;
20392 }
20393 const double point = staged_.syminfo.pointvalue * active_staged_fx(sub_bar_open_ms);
20394 const double unit_margin = fill * point * (config_.margin_short / 100.0);
20395 const double fee = config_.commission_value / 100.0;
20396 if (!finite_positive(unit_margin)) return 0.0;
20397 const double lot_value = fill * point;
20398 double out = 0.0;
20399 double booked_fees = 0.0;
20400 double last_short = 0.0;
20401 double last_remaining = 0.0;
20402 double last_equity = 0.0;
20403 for (const auto& [units, entry] : taken) {
20404 if (!(units > 0.0)) continue;
20405 out += units;
20406 const double remaining = held_before - out;
20407 if (!(remaining > internal::kQtyEpsilon)) break;
20408 const double exact = remaining * unit_margin;
20409 const double required = std::isfinite(lot_value) && lot_value < 1.0
20410 ? source_money_round(exact) : exact;
20411 const double equity = equity_before - booked_fees + (fill - entry) * units * point;
20412 const double deficit = required - equity;
20413 if (deficit > 0.0) {
20414 last_short = deficit;
20415 last_remaining = remaining;
20416 last_equity = equity;
20417 }
20418 booked_fees += units * fill * point * fee;
20419 }
20420 if (!(last_short > 0.0) || !std::isfinite(last_short)) return 0.0;
20421 const double minimum = std::floor(source_money_round(last_short) / unit_margin);
20422 double units = std::floor(4.0 * minimum + 1e-6);
20423 if (!(units > internal::kQtyEpsilon) && last_remaining * unit_margin - last_equity < unit_margin)
20424 units = 1.0;
20425 units = std::min(units, held_before - out);
20426 if (!(units > internal::kQtyEpsilon) || !std::isfinite(units)) return 0.0;
20427 return units;
20428}
20429
20430// The open's own check of a carried process_orders_on_close short in the
20431// path walk's scope (short_path_points_scope) and the opening-call scope
20432// (close_sized_open_call_scope): its call at the open's print,
20433// whose lagged follow-up is owed to the bar's first extreme rather than
20434// repeated at that print (switch short_point_drops_owed, with the follow-up's
20435// own scope); there it is dropped if that point's own check finds the book
20436// short (execute_owed_short_follow_up). Synthetic OANDA:EURUSD at slippage 1:
20437// a short called 0.20 at a 2.00001 open print can owe 0.12 to the high.
20438// If that high is short itself, its own check sizes its call,
20439// instead of booking 0.12 at the open and then checking the high (lab tv tapes
20440// tests/fixtures/margin_open_rules lag/owed-dropped-fractional,
20441// lag/whole-share-follow-up: on a bar opening at its low one share owed
20442// there books at the open's print).
20443bool PineExecutionAdapter::open_call_owing_follow_up(
20444 double opening_mark, const NativeDecisionContext& context) {
20445 const auto before = detail::run_position(require_host());
20446 const auto& opening = detail::margin_opening_switches();
20447 const bool whole = whole_share_lag_scope() && opening.whole_share_lagged_short;
20448 if (!PF_RARE(opening.short_point_drops_owed) || !(before.signed_units < 0.0)
20449 || !(whole || lagged_short_scope()) || !short_path_points_scope(context)
20450 || !close_sized_open_call_scope(true)) {
20451 return submit_margin_call_slice(opening_mark, context);
20452 }
20453 const double fill = source_margin_fill_price(opening_mark, true);
20454 const auto money = source_margin_money(fill, context.sub_bar_open_ms);
20455 const auto lots = require_host().native_open_lots(fill);
20456 open_call_sequence_ = true;
20457 const bool called = submit_margin_call_slice(opening_mark, context);
20458 open_call_sequence_ = false;
20459 if (!called || !money.valid || !finite_positive(fill)) return called;
20460 const double held_before = std::abs(before.signed_units);
20461 const double taken_units = held_before - std::abs(detail::run_position(require_host()).signed_units);
20462 if (!(std::abs(detail::run_position(require_host()).signed_units) > internal::kQtyEpsilon))
20463 return called;
20464 std::vector<std::pair<double, double>> taken;
20465 double left = taken_units;
20466 for (const auto& lot : lots) {
20467 if (!(left > internal::kQtyEpsilon)) break;
20468 const double take = std::min(left, std::abs(lot.signed_units));
20469 taken.emplace_back(take, lot.entry_price);
20470 left -= take;
20471 }
20472 const double more = whole
20473 ? whole_share_lag_units(taken, money.equity, held_before, fill, context.sub_bar_open_ms)
20474 : lagged_short_follow_up_units(taken, money.equity, held_before, fill,
20475 context.sub_bar_open_ms);
20476 if (more > 0.0) {
20477 owed_follow_up_bar_ = context.script_bar_open_ms;
20478 owed_follow_up_point_ = 1;
20479 owed_follow_up_units_ = more;
20480 }
20481 return called;
20482}
20483
20484// A carried process_orders_on_close short checked over its bar's path before
20485// the script (on_bar_close_before_script): a commissioned or slipped one with
20486// nothing resting beside it, which schedule_margin_call_path leaves to the
20487// adapter (switch short_path_points).
20488bool PineExecutionAdapter::short_path_points_scope(const NativeDecisionContext& context) const {
20489 if (!detail::margin_schedule_switches().short_path_points || !config_.process_orders_on_close
20490 || config_.calc_on_order_fills || !source_margin_call_enabled_
20491 || (config_.commission_value == 0.0 && config_.slippage == 0)
20492 || !staged_.account_fx_effective_from_ms.empty()) {
20493 return false;
20494 }
20495 if (!(detail::run_position(require_host()).signed_units < 0.0)
20496 || position_open_script_bar_ == std::numeric_limits<std::int64_t>::min()
20497 || position_open_script_bar_ == context.script_bar_open_ms
20498 || !pending_bracket_legs_.empty() || !pending_coof_requests_.empty()
20499 || !delayed_market_orders_.empty() || !pending_entries_.empty()) {
20500 return false;
20501 }
20502 // A void call's request rests for no one (void_issue).
20503 return !std::any_of(live_handles_.begin(), live_handles_.end(), [&](const auto& handle) {
20504 const auto found = placement_.find(handle.incarnation);
20505 return found == placement_.end()
20506 || (found->second.family != PineOrderFamily::Margin && !found->second.void_issue);
20507 });
20508}
20509
20510// The follow-up owed at `point` of this script bar (a close owes 4: the next
20511// bar's open), booked at `price`'s print with the exit side's slippage ahead
20512// of the point's own check.
20513void PineExecutionAdapter::execute_owed_short_follow_up(
20514 int point, double price, const NativeDecisionContext& context) {
20515 if (owed_follow_up_bar_ == std::numeric_limits<std::int64_t>::min()) return;
20516 const bool due = owed_follow_up_bar_ == context.script_bar_open_ms
20517 && owed_follow_up_point_ == point;
20518 if (!due) {
20519 // A follow-up whose point has passed is dead.
20520 if (owed_follow_up_bar_ != context.script_bar_open_ms || owed_follow_up_point_ < point) {
20521 owed_follow_up_bar_ = std::numeric_limits<std::int64_t>::min();
20522 owed_follow_up_point_ = -1;
20523 owed_follow_up_units_ = 0.0;
20524 }
20525 return;
20526 }
20527 const double units = owed_follow_up_units_;
20528 owed_follow_up_bar_ = std::numeric_limits<std::int64_t>::min();
20529 owed_follow_up_point_ = -1;
20530 owed_follow_up_units_ = 0.0;
20531 const auto position = detail::run_position(require_host());
20532 if (!(position.signed_units < 0.0) || !(units > 0.0) || !finite_positive(price)) return;
20533 const double mark = nearest_tick(price, staged_.syminfo.mintick);
20534 // The point's own check comes first: where it finds the book short --
20535 // whether its call books or is vetoed -- the owed follow-up is dropped
20536 // (switch short_point_drops_owed; lab tv tapes
20537 // tests/fixtures/margin_open_rules lag/: a synthetic NYSE:F high 12.00 short
20538 // with its unit vetoed books nothing owed; an OANDA:EURUSD high can call 40.00).
20539 if (PF_RARE(detail::margin_opening_switches().short_point_drops_owed)) {
20540 const auto here = source_margin_money(price, context.sub_bar_open_ms);
20541 if (here.valid && here.required > here.equity) return;
20542 }
20543 const double more = call_short_with_lagged_follow_up(
20544 mark, std::min(units, std::abs(position.signed_units)), context, true);
20545 if (more > 0.0 && point < 3) {
20546 owed_follow_up_bar_ = context.script_bar_open_ms;
20547 owed_follow_up_point_ = point + 1;
20548 owed_follow_up_units_ = more;
20549 }
20550}
20551
20552// The follow-up owed to the close of a carried process_orders_on_close short
20553// (switch close_follow_up_after_script): the close's own check still comes
20554// first, and where it finds the book short the follow-up is dropped (switch
20555// short_point_drops_owed); otherwise it waits for the script and is booked
20556// after it (book_owed_close_follow_up).
20557void PineExecutionAdapter::hold_owed_close_follow_up(
20558 double close, const NativeDecisionContext& context) {
20559 if (owed_follow_up_bar_ == std::numeric_limits<std::int64_t>::min()) return;
20560 const auto drop = [&]() {
20561 owed_follow_up_bar_ = std::numeric_limits<std::int64_t>::min();
20562 owed_follow_up_point_ = -1;
20563 owed_follow_up_units_ = 0.0;
20564 };
20565 if (owed_follow_up_bar_ != context.script_bar_open_ms || owed_follow_up_point_ != 3) {
20566 // A follow-up whose point has passed is dead.
20567 if (owed_follow_up_bar_ != context.script_bar_open_ms || owed_follow_up_point_ < 3) drop();
20568 return;
20569 }
20570 if (!(detail::run_position(require_host()).signed_units < 0.0)
20571 || !(owed_follow_up_units_ > 0.0) || !finite_positive(close)) {
20572 drop();
20573 return;
20574 }
20575 if (PF_RARE(detail::margin_opening_switches().short_point_drops_owed)) {
20576 const auto here = source_margin_money(close, context.sub_bar_open_ms);
20577 if (here.valid && here.required > here.equity) drop();
20578 }
20579}
20580
20581// A carried process_orders_on_close short's follow-up owed to this close
20582// (hold_owed_close_follow_up), booked after the script at the close's print,
20583// ahead of the fills of the orders the script placed there (switch
20584// close_follow_up_after_script). The script read the book before it, and its
20585// orders keep the sizes they were placed with: a strategy.close(id) or
20586// close_all() buys the units the script saw and opens the excess long as its
20587// own lot (flush_pending_closes, size_close_alls_at_placement), a
20588// reversal entry's excess joins the entry's own lot
20589// (apply_frozen_reversal_close). A synthetic OANDA:EURUSD short of
20590// 40000 owing 0.20 at a 2.00000 close books them at 2.00001 after
20591// the script's strategy.close("S"), which then buys 40000: it closes
20592// 39999.80 and opens 0.20 long, its entry the close order, ahead of the entry
20593// "L" (lab tv tapes tests/fixtures/margin_open_rules lots/).
20594void PineExecutionAdapter::book_owed_close_follow_up(
20595 const Bar& bar, const NativeDecisionContext& context) {
20596 if (owed_follow_up_bar_ != context.script_bar_open_ms || owed_follow_up_point_ != 3) return;
20597 const double owed_units = owed_follow_up_units_;
20598 owed_follow_up_bar_ = std::numeric_limits<std::int64_t>::min();
20599 owed_follow_up_point_ = -1;
20600 owed_follow_up_units_ = 0.0;
20601 const auto held = detail::run_position(require_host());
20602 if (!(held.signed_units < 0.0) || !(owed_units > 0.0) || !source_margin_call_enabled_
20603 || !finite_positive(bar.close)) {
20604 return;
20605 }
20606 // A close_all placed after the follow-up came due holds its size already.
20607 auto closes = same_bar_close_alls(context);
20608 closes.erase(std::remove_if(closes.begin(), closes.end(), [](const auto& close) {
20609 return close.second.crosses_zero;
20610 }), closes.end());
20611 const double units = std::min(owed_units, std::abs(held.signed_units));
20612 if (!submit_margin_call_units(nearest_tick(bar.close, staged_.syminfo.mintick), context,
20613 units, true)) {
20614 return;
20615 }
20616 const double left = std::abs(detail::run_position(require_host()).signed_units);
20617 record_close_call_after_script(std::abs(held.signed_units),
20618 std::abs(held.signed_units) - left, context);
20619 size_close_alls_at_placement(closes, held.signed_units);
20620}
20621
20622// TradingView checks a carried process_orders_on_close short at every point
20623// of its bar's path before the script runs at the close -- the open
20624// (on_bar_open), the nearer extreme, the farther one, the close -- each on the
20625// book the earlier points left, a follow-up owed at a point booked there
20626// first (switches short_path_points, lagged_short_follow_up; lab tv tapes
20627// tests/fixtures/margin_call_rules short-cutoff-gate and
20628// tests/fixtures/margin_schedule_rules: on a high-first bar a call at the high
20629// is followed at the low's print), except one owed to the close, which is
20630// booked after the script there (hold_owed_close_follow_up). A follow-up the
20631// close owes is booked at the next bar's open.
20632void PineExecutionAdapter::walk_short_path_points(
20633 const Bar& bar, const NativeDecisionContext& context) {
20634 const bool high_first = source_path_uses_high_first(bar);
20635 const double path[] = {high_first ? bar.high : bar.low, high_first ? bar.low : bar.high,
20636 bar.close};
20637 for (int index = 0; index < 3; ++index) {
20638 const int point = index + 1;
20639 const double price = path[index];
20640 if (!finite_positive(price)) continue;
20641 if (point == 3 && PF_RARE(detail::margin_schedule_switches().close_follow_up_after_script))
20642 hold_owed_close_follow_up(price, context);
20643 else
20644 execute_owed_short_follow_up(point, price, context);
20645 const auto position = detail::run_position(require_host());
20646 if (!(position.signed_units < 0.0)) return;
20647 const auto money = source_margin_money(price, context.sub_bar_open_ms);
20648 if (!money.valid || source_margin_rounded_tie_veto()) continue;
20649 const double units = std::min(source_margin_units(money, false),
20650 std::abs(position.signed_units));
20651 if (!(units > 0.0)) continue;
20652 const double more = call_short_with_lagged_follow_up(money.mark, units, context);
20653 if (more > 0.0) {
20654 owed_follow_up_bar_ = context.script_bar_open_ms;
20655 owed_follow_up_point_ = point + 1;
20656 owed_follow_up_units_ = more;
20657 }
20658 }
20659}
20660
20661// A short's call the kernel or a rested path point booked at a path extreme
20662// outside process_orders_on_close (switch lagged_short_follow_up): the
20663// follow-up it owes the bar's next path point -- the other extreme, rested
20664// there, or the close, booked there after the script ahead of the close's own
20665// check (close_point_margin_call). The book before the call is the one after
20666// it with the called units back: the event's closed trades, first in first
20667// out, and their exit fees (synthetic OANDA:EURUSD 15m, margin 1 %: a short of
20668// several lots can owe a follow-up at the close after a high-point call;
20669// its size comes from the last still-short unbooked-lot check).
20670void PineExecutionAdapter::schedule_lagged_short_follow_up(
20671 const native_order::ExecutionAppliedEvent& event, const NativeDecisionContext& context) {
20672 const auto phase = context.coordinate.path_phase;
20673 if (phase != NativePathPhase::High && phase != NativePathPhase::Low) return;
20674 // A slipped run under a percent commission keeps the follow-up its
20675 // pinned rule takes (follow_margin_call, margin_follow_up_units: lab tv
20676 // tape tests/fixtures/margin_open_print taili-mop-eur).
20677 if (config_.process_orders_on_close || !close_point_margin_scope() || !lagged_short_scope()
20678 || margin_follow_up_scope()
20679 || !policy_script_bar_valid_ || policy_script_bar_.timestamp != context.script_bar_open_ms
20680 || event.closed_trade_count == 0 || !finite_positive(event.resolved_price)) {
20681 return;
20682 }
20683 const auto position = detail::run_position(require_host());
20684 if (!(position.signed_units < 0.0)) return;
20685 const auto& host = require_host();
20686 const double fill = event.resolved_price;
20687 const double fx = active_staged_fx(context.sub_bar_open_ms);
20688 const double point = staged_.syminfo.pointvalue * fx;
20689 const double fee = config_.commission_value / 100.0;
20690 std::vector<std::pair<double, double>> taken;
20691 double called = 0.0;
20692 double fees = 0.0;
20693 for (std::size_t i = 0; i < event.closed_trade_count; ++i) {
20694 const auto index = event.first_trade_index + i;
20695 if (index >= static_cast<std::size_t>(host.trade_count())) return;
20696 const Trade& trade = host.get_trade(static_cast<int>(index));
20697 if (trade.is_long) return;
20698 taken.emplace_back(trade.qty, trade.entry_price);
20699 called += trade.qty;
20700 fees += trade.qty * fill * point * fee;
20701 }
20702 const auto after = source_margin_money(fill, context.sub_bar_open_ms);
20703 if (!after.valid) return;
20704 const double more = lagged_short_follow_up_units(
20705 taken, after.equity + fees, std::abs(position.signed_units) + called, fill,
20706 context.sub_bar_open_ms);
20707 if (!(more > 0.0)) return;
20708 const Bar& bar = policy_script_bar_;
20709 const bool high_first = source_path_uses_high_first(bar);
20710 const bool first_extreme = high_first ? phase == NativePathPhase::High
20711 : phase == NativePathPhase::Low;
20712 if (!first_extreme) {
20713 owed_follow_up_bar_ = context.script_bar_open_ms;
20714 owed_follow_up_point_ = 3;
20715 owed_follow_up_units_ = more;
20716 return;
20717 }
20718 // The other extreme: rested at its print, reached from this one.
20719 const double next = high_first ? bar.low : bar.high;
20720 const double from = high_first ? bar.high : bar.low;
20721 if (!finite_positive(next) || !finite_positive(from)) return;
20722 native_order::Request request;
20723 request.intent = native_order::Reduce{native_order::ExplicitUnits{
20724 std::min(more, std::abs(position.signed_units))}};
20725 request.label = kMarginCallLabel;
20726 request.comment = "Margin call";
20727 request.trigger = next < from ? native_order::Trigger{native_order::Limit{next, true}}
20728 : native_order::Trigger{native_order::Stop{next}};
20729 PlacementSnapshot snapshot;
20731 snapshot.source_id = request.label;
20732 snapshot.requested_qty = more;
20733 snapshot.is_long = true;
20734 snapshot.forced_execution_price = source_margin_fill_price(next, true);
20735 snapshot.waypoint_margin_call = true;
20736 snapshot.sizing = sizing_snapshot();
20737 (void)submit_or_replace(std::move(request), std::move(snapshot), false,
20738 "__margin_follow_up__");
20739}
20740
20741// A deficit the open's check found and vetoed -- the one-unit band's veto or
20742// the gate (short_call_vetoed) -- stays pending, and while it is pending
20743// TradingView checks margin at the first path point whose segment touches a
20744// resting exit, before that exit (switch pending_veto_first). A call there
20745// re-issues the exit for the reduced book, which, marketable there, fills at
20746// that point's print with the exit's slippage. Synthetic NYSE:F at slippage 1:
20747// 400 shares 24.00 short at a 25.00 open, its unit vetoed there, with a
20748// buy stop at 25.06015 resting, fall to 24.95, then call 16 at the
20749// 25.10 high's 25.11 print and fill the stop's 384 there, where the stop
20750// alone fills 400 at 25.08 (the margin_schedule_rules fixtures
20751// establish the pending-veto ordering). With nothing pending
20752// a touched level fills at its level first. Called only where the open's own
20753// check ran on this book and called nothing.
20754bool PineExecutionAdapter::pending_veto_point_first(
20755 const Bar& bar, const NativeDecisionContext& context, double opening_mark) {
20756 if (!detail::margin_schedule_switches().pending_veto_first || config_.process_orders_on_close
20757 || !close_point_margin_scope() || !(config_.slippage > 0) || !source_margin_call_enabled_
20758 || !finite_positive(staged_.syminfo.mintick) || !finite_positive(bar.open)) {
20759 return false;
20760 }
20761 const auto position = detail::run_position(require_host());
20762 if (!(position.signed_units < 0.0)) return false;
20763 const double held = std::abs(position.signed_units);
20764 const auto at_open = source_margin_money(opening_mark, context.sub_bar_open_ms);
20765 if (!at_open.valid || !(at_open.required > at_open.equity)
20766 || source_margin_units(at_open, false) > 0.0) {
20767 return false;
20768 }
20769 const double tick = staged_.syminfo.mintick;
20770 const bool high_first = source_path_uses_high_first(bar);
20771 // The open and the two extremes in path order, on the tick ladder.
20772 const double points[] = {nearest_tick(bar.open, tick),
20773 nearest_tick(high_first ? bar.high : bar.low, tick),
20774 nearest_tick(high_first ? bar.low : bar.high, tick)};
20775 if (!finite_positive(points[1]) || !finite_positive(points[2])) return false;
20776 // The whole protective exit of the short a segment of the path touches
20777 // first: a buy stop on the way up, a buy limit on the way down.
20778 std::optional<std::pair<native_order::RequestHandle, PlacementSnapshot>> touched;
20779 int touched_point = 3;
20780 for (const auto& handle : live_handles_) {
20781 const auto found = placement_.find(handle.incarnation);
20782 if (found == placement_.end()) continue;
20783 const auto& row = found->second;
20784 if ((row.family != PineOrderFamily::ExitStop && row.family != PineOrderFamily::ExitLimit)
20785 || row.legs.dormant() || row.void_issue
20786 || row.projection_created_bar >= projection_bar_index(context)
20787 || finite_positive(row.exit_levels.trail_offset)
20788 || finite_positive(row.exit_levels.trail_points)
20789 || finite_positive(row.exit_levels.trail_price)
20790 || (std::isfinite(row.requested_qty) && std::abs(row.requested_qty) < held - 1e-9)
20791 || (std::isfinite(row.qty_percent) && row.qty_percent < 100.0 - 1e-9)) {
20792 continue;
20793 }
20794 const double owned = row.from_entry.empty() ? held
20795 : from_entry_units(row.from_entry).value_or(0.0);
20796 if (!(owned >= held - 1e-9)) continue;
20797 const bool stop = row.family == PineOrderFamily::ExitStop;
20798 const double level = stop ? row.exit_levels.stop : row.exit_levels.limit;
20799 if (!finite_positive(level)) continue;
20800 // Not already through at the open (the open fills it first).
20801 if (stop ? points[0] >= level : points[0] <= level) continue;
20802 int point = 3;
20803 for (int segment = 1; segment < 3 && point == 3; ++segment) {
20804 if (stop ? points[segment - 1] < level && points[segment] >= level
20805 : points[segment - 1] > level && points[segment] <= level) {
20806 point = segment;
20807 }
20808 }
20809 if (point == 3) continue;
20810 if (point < touched_point) {
20811 touched.emplace(handle, row);
20812 touched_point = point;
20813 } else if (point == touched_point) {
20814 return false;
20815 }
20816 }
20817 if (!touched) return false;
20818 // The shape the tapes pin: the touched exit (and the other leg of its
20819 // strategy.exit) is the only order the book holds, so nothing the path
20820 // reaches earlier can change the book that point's check sees.
20821 if (!pending_bracket_legs_.empty() || !pending_coof_requests_.empty()
20822 || !delayed_market_orders_.empty() || !pending_entries_.empty()
20823 || !pending_same_bar_commands_.empty()) {
20824 return false;
20825 }
20826 const auto& exit_row = touched->second;
20827 for (const auto& handle : live_handles_) {
20828 const auto found = placement_.find(handle.incarnation);
20829 if (found == placement_.end()) return false;
20830 const auto& row = found->second;
20831 const bool same_exit = (row.family == PineOrderFamily::ExitStop
20832 || row.family == PineOrderFamily::ExitLimit)
20833 && row.source_id == exit_row.source_id && row.from_entry == exit_row.from_entry;
20834 if (!same_exit && row.family != PineOrderFamily::Margin) return false;
20835 }
20836 // A point before it that calls ends the veto there: the earlier rules.
20837 if (touched_point == 2) {
20838 const auto before = source_margin_money(points[1], context.sub_bar_open_ms);
20839 if (before.valid && source_margin_units(before, false) > 0.0) return false;
20840 }
20841 const double first = points[touched_point];
20842 const auto money = source_margin_money(first, context.sub_bar_open_ms);
20843 if (!money.valid || source_margin_rounded_tie_veto()) return false;
20844 const double units = std::min(source_margin_units(money, false), held);
20845 if (!(units > 0.0)) return false;
20846 if (!submit_margin_call_units(money.mark, context, units, true)) return false;
20847 const auto left = detail::run_position(require_host());
20848 if (!(left.signed_units < 0.0)) return true;
20849 const auto& [handle, row] = *touched;
20850 if (std::find(live_handles_.begin(), live_handles_.end(), handle) == live_handles_.end())
20851 return true;
20852 cancel_bracket_siblings(handle);
20853 native_order::Request request;
20854 request.intent = native_order::Flatten{};
20855 request.label = row.source_id;
20856 request.comment = row.comment;
20857 request.trigger = native_order::Market{};
20858 PlacementSnapshot immediate = row;
20859 const bool stop_close = row.family == PineOrderFamily::ExitStop;
20860 immediate.forced_execution_price = nearest_tick(
20861 nearest_tick(first, tick) + (stop_close ? config_.slippage * tick : 0.0), tick);
20862 immediate.projection_predecessor = handle.incarnation;
20863 immediate.projection_predecessor_exit = true;
20864 const auto accepted = submit_or_replace(
20865 std::move(request), std::move(immediate), false,
20866 row.source_id + "\x1f" + row.from_entry + std::to_string(static_cast<int>(row.family)));
20867 if (accepted) execute_or_withdraw_close(*accepted, row.void_issue);
20868 return true;
20869}
20870
20871// A margin call booked at a close after the script: the book the script saw
20872// there and the units called (switch frozen_reversal_close).
20873void PineExecutionAdapter::record_close_call_after_script(
20874 double book_before, double called, const NativeDecisionContext& context) {
20875 if (!detail::margin_schedule_switches().frozen_reversal_close || !(called > 0.0)) return;
20876 // Kept only while a reversal the script placed at this close waits: the
20877 // one order that reads it.
20878 const auto position = detail::run_position(require_host());
20879 const bool reversal_waits = position.signed_units != 0.0
20880 && any_live_row(live_handles_, placement_, [&](const PlacementSnapshot& row) {
20881 return row.family == PineOrderFamily::Entry && row.opening
20882 && row.placement_script_open_ms == context.script_bar_open_ms
20883 && row.is_long != (position.signed_units > 0.0);
20884 });
20885 if (!reversal_waits) return;
20886 if (close_call_after_script_bar_ != context.script_bar_open_ms) {
20887 close_call_after_script_bar_ = context.script_bar_open_ms;
20888 close_call_after_script_book_ = book_before;
20889 close_call_after_script_units_ = 0.0;
20890 }
20891 close_call_after_script_units_ += called;
20892}
20893
20894bool PineExecutionAdapter::schedule_priced_opening_margin(
20895 const Bar& bar, const NativeDecisionContext& context, double fill) {
20896 const auto position = detail::run_position(require_host());
20897 if (position.signed_units == 0.0 || !source_margin_call_enabled_) return false;
20898 const bool buy = position.signed_units < 0.0;
20899 const double mark = next_source_path_waypoint(
20900 bar, context.coordinate.path_phase, fill, source_path_uses_high_first(bar));
20901 if (!finite_positive(mark)) return false;
20902 const double units = source_margin_units(
20903 source_margin_money(mark, context.sub_bar_open_ms), false);
20904 if (!(units > 0.0)) return false;
20905 native_order::Request request;
20906 request.intent = native_order::Reduce{native_order::ExplicitUnits{units}};
20907 request.label = kMarginCallLabel;
20908 request.comment = "Margin call";
20909 request.trigger = (buy ? mark > fill : mark < fill)
20910 ? native_order::Trigger{native_order::Stop{mark}}
20911 : native_order::Trigger{native_order::Limit{mark, true}};
20912 PlacementSnapshot snapshot;
20914 snapshot.source_id = request.label;
20915 snapshot.requested_qty = units;
20916 snapshot.forced_execution_price = source_margin_fill_price(mark, buy);
20917 snapshot.waypoint_margin_call = true;
20918 snapshot.sizing = sizing_snapshot();
20919 return submit_or_replace(std::move(request), std::move(snapshot), false,
20920 "__margin_priced_opening__").has_value();
20921}
20922
20923bool PineExecutionAdapter::schedule_margin_call_path(
20924 const Bar& bar, const NativeDecisionContext& context) {
20925 const auto position = detail::run_position(require_host());
20926 if (position.signed_units == 0.0) return false;
20927 if (config_.process_orders_on_close) {
20928 // A carried process_orders_on_close short is checked over its bar's
20929 // path before the script runs at the close, as a long is, whatever
20930 // else the book holds: a pending entry beside it, a bracket that
20931 // fills later on the path, a close, add or reversal at the close, and
20932 // at any money class (lab tv tapes tests/fixtures/margin_residual
20933 // w5b-pc-seen-size and -seen-size-parked: the script reads the called
20934 // size, a parked entry beside the short or not; w5b-pc-carried-close,
20935 // -reverse and -bracket-tp; lane W5B-ENG-MARGIN-RESIDUAL). ab9714be
20936 // took no path slice beside a competing entry, and none on a
20937 // rounded-money short, slicing after the script instead. The one
20938 // exception left is a commissioned or slipped short with nothing
20939 // resting: its slice is on_bar_close_before_script's, at the high
20940 // before the script (lane W5-ENG-MARGIN-V6, C1), and a path slice
20941 // here would take it twice.
20942 // A void call's request rests for no one (void_issue).
20943 const bool resting_order = std::any_of(
20944 live_handles_.begin(), live_handles_.end(), [&](const auto& handle) {
20945 const auto found = placement_.find(handle.incarnation);
20946 return found == placement_.end()
20947 || (found->second.family != PineOrderFamily::Margin
20948 && !found->second.void_issue);
20949 });
20950 if (!config_.calc_on_order_fills && modeled_input()
20951 && position.signed_units < 0.0
20952 && (config_.commission_value != 0.0 || config_.slippage != 0)
20953 && position_open_script_bar_ != std::numeric_limits<std::int64_t>::min()
20954 && position_open_script_bar_ != context.script_bar_open_ms
20955 && !resting_order && pending_bracket_legs_.empty()
20956 && pending_coof_requests_.empty() && delayed_market_orders_.empty()
20957 && pending_entries_.empty()) {
20958 return false;
20959 }
20960 }
20961 // ab9714be pine_fills.cpp:1025-1063, :1314-1339: an entry-bar margin
20962 // pass sees only the OHLC suffix after the actual opening point. Later
20963 // bars enter here from Open and retain the ordinary remaining path. A
20964 // fill matched at a waypoint was reached on the segment into it, so that
20965 // waypoint is still ahead of it (the kernel's post-fill point measures
20966 // from it too, NativeExecutionConsumer::margin_segment_origin): a limit
20967 // filled on the way down to the low still faces the low (the
20968 // intrabar margin correction).
20969 const bool high_first = source_path_uses_high_first(bar);
20970 struct Waypoint { NativePathPhase phase; double price; };
20971 const Waypoint path[] = {
20972 {NativePathPhase::Open, bar.open},
20973 {high_first ? NativePathPhase::High : NativePathPhase::Low,
20974 high_first ? bar.high : bar.low},
20975 {high_first ? NativePathPhase::Low : NativePathPhase::High,
20976 high_first ? bar.low : bar.high},
20977 {NativePathPhase::Close, bar.close},
20978 };
20979 int current = -1;
20980 for (int index = 0; index < 4; ++index) {
20981 if (path[index].phase == context.coordinate.path_phase) {
20982 current = index;
20983 break;
20984 }
20985 }
20986 double adverse = kNaN;
20987 for (int index = std::max(current, 0); index < 4; ++index) {
20988 if (!finite_positive(path[index].price)) continue;
20989 if (!std::isfinite(adverse)
20990 || (position.signed_units > 0.0
20991 ? path[index].price < adverse : path[index].price > adverse)) {
20992 adverse = path[index].price;
20993 }
20994 }
20995 if (!finite_positive(adverse)) return false;
20996 // A fill between the open and the close whose remaining path is adverse
20997 // only at the close: TradingView checks that close after the script has
20998 // run there (close_point_margin_call), so the kernel's own check at this
20999 // point rests nothing (resolve_margin_call_units).
21000 bool close_alone = current == 1 || current == 2;
21001 close_alone = close_alone && close_point_margin_scope() && finite_positive(bar.close);
21002 for (int index = std::max(current, 0); close_alone && index < 3; ++index) {
21003 if (!finite_positive(path[index].price)) continue;
21004 close_alone = position.signed_units > 0.0 ? path[index].price > bar.close
21005 : path[index].price < bar.close;
21006 }
21007 if (close_alone) close_margin_point_ = context.coordinate.ordinal;
21008 // TradingView checks every point of the path, not only its adverse
21009 // extreme: a point the path reaches before that extreme can already be
21010 // short of margin -- a short filled the slippage ticks under its print is
21011 // still short at a low above its fill -- and so can one after it, where
21012 // the one-unit band's call restores the book that the extreme's own print
21013 // would not: a synthetic full-margin short of 200 shares, 22.00 short at
21014 // the 25.00 high, takes no unit (a 25.02 fill leaves 26.00 short), then
21015 // gives one up at the 24.99 low, 18.00 short; the same 0.02 slippage
21016 // makes one unit restorative there where it was not at the high (taili-mop-f3).
21017 // The first point that calls is rested there, and the rest
21018 // of the path is checked again on the book it leaves.
21019 withdraw_waypoint_margin_calls();
21020 if (close_point_margin_scope()) {
21021 if (rest_waypoint_margin_call(bar, current <= 0 ? 1 : current, 3, context)) {
21022 kernel_margin_path_point_ = context.coordinate.ordinal;
21023 close_margin_point_ = context.coordinate.ordinal;
21024 waypoint_chain_bar_ = context.script_bar_open_ms;
21025 return true;
21026 }
21027 }
21028 // R5: the placement is the kernel's. TradingView's scheduling decision has
21029 // just been taken above -- with this bar's competing orders in front of
21030 // it, exactly where the legacy broker takes it -- so all that is left is
21031 // to admit the kernel's own check point for this driver point. The kernel
21032 // then measures the same adverse mark (margin_sizing_price), asks this
21033 // adapter for TradingView's money and units, and rests the reduction at
21034 // that mark itself (NativeLiquidationCheck::PathAdverseExtremeMark).
21035 kernel_margin_path_point_ = context.coordinate.ordinal;
21036 // The answer the caller needs is whether a call will be made, which is the
21037 // same money the kernel is about to ask for.
21038 return source_margin_units(
21039 source_margin_money(adverse, context.sub_bar_open_ms), false) > 0.0;
21040}
21041
21042// The one unit a restore that floors below one lot takes (source_margin_units'
21043// band), before its fill is tested; zero for any other restore.
21044double PineExecutionAdapter::source_margin_one_unit(const SourceMarginMoney& money) const {
21045 if (!money.valid || !(money.required > money.equity) || !staged_.quantity_grid
21046 || !(*staged_.quantity_grid <= 1.0)) {
21047 return 0.0;
21048 }
21049 const double raw = (money.required - money.equity) / money.unit_margin;
21050 if (!(raw > internal::kQtyEpsilon) || !(raw < 1.0)) return 0.0;
21051 const double floor_basis = source_money_round(money.required - money.equity)
21052 / money.unit_margin;
21053 if (std::floor(floor_basis / *staged_.quantity_grid) * *staged_.quantity_grid > 0.0)
21054 return 0.0;
21055 const double candidate = std::min(1.0, money.held);
21056 const double rounded = floor_quantity_grid(candidate, staged_.quantity_grid);
21057 const double guard = std::max(1e-12, std::abs(candidate) * 1e-12);
21058 return candidate >= money.held - guard || std::abs(rounded - candidate) <= guard
21059 ? candidate : 0.0;
21060}
21061
21062// Whether that one unit restores the book at `fill`, the call's own fill: the
21063// whole position's requirement at the fill against the equity at the mark
21064// (source_margin_units, the slipped-short margin rule).
21065bool PineExecutionAdapter::source_margin_unit_restores(
21066 const SourceMarginMoney& money, double fill) const {
21067 return money.valid && finite_positive(fill)
21068 && money.exact_required * fill / money.mark - money.equity < money.unit_margin;
21069}
21070
21071// Where the close's own check is the one after the script: a run whose
21072// orders fill at the next open, with no fill recalculation or magnifier.
21073bool PineExecutionAdapter::close_point_margin_scope() const noexcept {
21074 return !config_.process_orders_on_close && !config_.calc_on_order_fills
21075 && !coof_recalc_active_ && !bar_magnifier_ && modeled_input();
21076}
21077
21078// TradingView checks a book whose path after a fill is adverse only at the
21079// bar's close at that close, after the script has run there: the script
21080// reads the book before the call, and the call is an order sized at the
21081// close's mark. Left standing it executes at the close, ahead of the orders
21082// the script placed there, and those keep the size they were placed with: a
21083// strategy.close_all() placed on that bar closes the called units again and
21084// opens them on the other side at the next open (lab tv tape w13-b1-close-f).
21085// The script's strategy.cancel_all() withdraws it too; it is then placed again
21086// behind the script's orders and executes after them at the next open as a
21087// plain market order -- on the book the script's close_all flattened it opens
21088// the called units on the other side (w13-b2-cancel-close-f; the population
21089// probes pf-probe-ki62-margin-deferral on NYSE:F and OANDA:XAUUSD), on a book
21090// still held it reduces it (w13-b4-cancel-f). A one-unit call (a restore
21091// that floors below one lot) is taken only at a fill where the unit restores
21092// the book, so one its fill at this close would not restore is tried again
21093// at the next open, and a re-placed one is tried at that open's fill alone
21094// (close_point_margin_call_at_open).
21095bool PineExecutionAdapter::close_point_margin_call(
21096 const Bar& bar, const NativeDecisionContext& context, bool cancelled) {
21097 const bool follow_up = close_margin_follow_up_bar_ == context.script_bar_open_ms;
21098 const double follow_up_units = close_margin_follow_up_units_;
21099 close_margin_follow_up_bar_ = std::numeric_limits<std::int64_t>::min();
21100 close_margin_follow_up_units_ = 0.0;
21101 // A short's lagged follow-up owed at this close (schedule_lagged_short_follow_up)
21102 // is booked first, after the script, ahead of the close's own check; the
21103 // close_all orders the script placed here keep the size they were placed
21104 // with, as behind any call at the close.
21105 if (PF_RARE(owed_follow_up_bar_ != std::numeric_limits<std::int64_t>::min())) {
21106 const bool owed = owed_follow_up_bar_ == context.script_bar_open_ms
21107 && owed_follow_up_point_ == 3;
21108 const double owed_units = owed_follow_up_units_;
21109 owed_follow_up_bar_ = std::numeric_limits<std::int64_t>::min();
21110 owed_follow_up_point_ = -1;
21111 owed_follow_up_units_ = 0.0;
21112 const auto held = detail::run_position(require_host());
21113 if (owed && held.signed_units < 0.0 && owed_units > 0.0 && source_margin_call_enabled_) {
21114 auto closes = same_bar_close_alls(context);
21115 const double units = std::min(owed_units, std::abs(held.signed_units));
21116 if (submit_margin_call_units(nearest_tick(bar.close, staged_.syminfo.mintick),
21117 context, units, true)) {
21118 const double left = std::abs(detail::run_position(require_host()).signed_units);
21119 record_close_call_after_script(std::abs(held.signed_units),
21120 std::abs(held.signed_units) - left, context);
21121 size_close_alls_at_placement(closes, held.signed_units);
21122 }
21123 }
21124 }
21125 const auto position = detail::run_position(require_host());
21126 if (position.signed_units == 0.0 || !source_margin_call_enabled_) return false;
21127 // A full-margin long's call is the one-contract money call's
21128 // (resolve_margin_call_units).
21129 if (position.signed_units > 0.0 && std::abs(config_.margin_long - 100.0) < 1e-12)
21130 return false;
21131 const auto money = source_margin_money(bar.close, context.sub_bar_open_ms);
21132 if (!money.valid || source_margin_rounded_tie_veto()) return false;
21133 const double one_unit = source_margin_one_unit(money);
21134 double units = std::min(source_margin_units(money, false),
21135 std::abs(position.signed_units));
21136 if (units > 0.0 && !cancelled) return book_close_point_call(money.mark, units, context, false);
21137 if (one_unit > 0.0) return book_close_point_call(money.mark, one_unit, context, true);
21138 if (units > 0.0) return book_close_point_call(money.mark, units, context, true);
21139 // The close's own check calls nothing: a follow-up owed there is booked
21140 // after the script (follow_margin_call).
21141 if (follow_up && follow_up_units > 0.0)
21142 return book_close_point_call(money.mark, follow_up_units, context, cancelled);
21143 return false;
21144}
21145
21146// A call booked at the close after the script: at the close, after which
21147// every strategy.close_all() the script placed on the bar keeps the size it
21148// was placed with; or, `queued`, as a plain market order behind the script's
21149// orders for the next open.
21150bool PineExecutionAdapter::book_close_point_call(
21151 double mark, double units, const NativeDecisionContext& context, bool queued,
21152 double reference_mark) {
21153 const auto position = detail::run_position(require_host());
21154 units = std::min(units, std::abs(position.signed_units));
21155 if (!(units > 0.0)) return false;
21156 if (!queued) {
21157 auto closes = same_bar_close_alls(context);
21158 if (!submit_margin_call_units(mark, context, units, true)) return false;
21159 if (PF_RARE(detail::margin_schedule_switches().frozen_reversal_close)) {
21160 const double left = std::abs(detail::run_position(require_host()).signed_units);
21161 record_close_call_after_script(std::abs(position.signed_units),
21162 std::abs(position.signed_units) - left, context);
21163 }
21164 size_close_alls_at_placement(closes, position.signed_units);
21165 close_point_follow_up(mark, units, context);
21166 return true;
21167 }
21168 const bool buy = position.signed_units < 0.0;
21169 close_margin_open_bar_ = context.script_bar_open_ms;
21170 native_order::Request request;
21171 request.intent = native_order::Transact{buy ? units : -units};
21172 request.label = kMarginCallLabel;
21173 request.comment = "Margin call";
21174 PlacementSnapshot snapshot;
21176 snapshot.source_id = request.label;
21177 snapshot.requested_qty = units;
21178 snapshot.is_long = buy;
21179 snapshot.crosses_zero = true;
21180 snapshot.sizing = sizing_snapshot();
21181 snapshot.follow_up_fill = reference_mark;
21182 return submit_or_replace(std::move(request), std::move(snapshot), false,
21183 kMarginCallLabel).has_value();
21184}
21185
21186// The first of the bar's path points in [from, to) where TradingView's check
21187// calls the book as it stands, rested at that point: a touch of its print
21188// from the side the path reaches it from, executed at the print's tick with
21189// the exit side's slippage. The close is not one of them: its check is taken
21190// after the script (close_point_margin_call).
21191bool PineExecutionAdapter::rest_waypoint_margin_call(
21192 const Bar& bar, int from, int to, const NativeDecisionContext& context) {
21193 const auto position = detail::run_position(require_host());
21194 if (position.signed_units == 0.0) return false;
21195 // Measured on shorts slipped on whole lots (TradingView exports
21196 // int28fix-ou-s1 and -ou-s2, NYSE:F shares at slippage 1 and 2). A
21197 // full-margin long's call is the one-contract money call
21198 // (submit_tv_money_long_margin_call), and neither a long nor a fractional
21199 // lot is checked here: resting their calls at an earlier point takes calls
21200 // TradingView does not (lane W13-ENG-MARGIN-OPP's population replay).
21201 if (position.signed_units > 0.0 || !(config_.slippage > 0) || !staged_.quantity_grid
21202 || *staged_.quantity_grid != 1.0) {
21203 return false;
21204 }
21205 const bool high_first = source_path_uses_high_first(bar);
21206 const double path[] = {bar.open, high_first ? bar.high : bar.low,
21207 high_first ? bar.low : bar.high, bar.close};
21208 const bool buy = position.signed_units < 0.0;
21209 for (int index = std::max(from, 1); index < std::min(to, 3); ++index) {
21210 const double price = path[index];
21211 if (!finite_positive(price) || !finite_positive(path[index - 1])) continue;
21212 const double units = std::min(
21213 source_margin_units(source_margin_money(price, context.sub_bar_open_ms), false),
21214 std::abs(position.signed_units));
21215 if (!(units > 0.0)) continue;
21216 const bool falling = price < path[index - 1];
21217 native_order::Request request;
21218 request.intent = native_order::Reduce{native_order::ExplicitUnits{units}};
21219 request.label = kMarginCallLabel;
21220 request.comment = "Margin call";
21221 request.trigger = falling == buy
21222 ? native_order::Trigger{native_order::Limit{price, true}}
21223 : native_order::Trigger{native_order::Stop{price}};
21224 PlacementSnapshot snapshot;
21226 snapshot.source_id = request.label;
21227 snapshot.requested_qty = units;
21228 snapshot.is_long = buy;
21229 snapshot.forced_execution_price = source_margin_fill_price(price, buy);
21230 snapshot.waypoint_margin_call = true;
21231 snapshot.sizing = sizing_snapshot();
21232 return submit_or_replace(std::move(request), std::move(snapshot), false,
21233 "__margin_waypoint__").has_value();
21234 }
21235 return false;
21236}
21237
21238// A new schedule, or the bar's end, supersedes a path-point margin call.
21239void PineExecutionAdapter::withdraw_waypoint_margin_calls() {
21240 if (!has_live_family(PineOrderFamily::Margin)) return;
21241 std::vector<native_order::RequestHandle> stale;
21242 for (const auto& handle : live_handles_) {
21243 const auto found = placement_.find(handle.incarnation);
21244 if (found != placement_.end() && found->second.waypoint_margin_call)
21245 stale.push_back(handle);
21246 }
21247 for (const auto& handle : stale) {
21248 if (require_host().cancel(handle).status == native_order::CancelStatus::Cancelled)
21249 retire(handle);
21250 }
21251}
21252
21253// This bar's strategy.close_all() orders still waiting for the next open.
21254std::vector<std::pair<native_order::RequestHandle, PlacementSnapshot>>
21255PineExecutionAdapter::same_bar_close_alls(const NativeDecisionContext& context) const {
21256 std::vector<std::pair<native_order::RequestHandle, PlacementSnapshot>> closes;
21257 if (!has_live_family(PineOrderFamily::CloseAll)) return closes;
21258 for (const auto& handle : live_handles_) {
21259 const auto found = placement_.find(handle.incarnation);
21260 if (found != placement_.end() && found->second.family == PineOrderFamily::CloseAll
21261 && found->second.placement_script_open_ms == context.script_bar_open_ms) {
21262 closes.emplace_back(handle, found->second);
21263 }
21264 }
21265 return closes;
21266}
21267
21268// Those orders keep the size they were placed with, the book `placed` before
21269// a call at the close shrank it: each becomes the plain market transaction of
21270// that size, which crosses zero on the smaller book (close_point_margin_call).
21271void PineExecutionAdapter::size_close_alls_at_placement(
21272 std::vector<std::pair<native_order::RequestHandle, PlacementSnapshot>>& closes,
21273 double placed) {
21274 for (auto& close : closes) {
21275 if (std::find(live_handles_.begin(), live_handles_.end(), close.first)
21276 == live_handles_.end()) {
21277 continue;
21278 }
21279 native_order::Request request;
21280 request.intent = native_order::Transact{-placed};
21281 request.label = close.second.source_id;
21282 request.comment = close.second.comment;
21283 close.second.crosses_zero = true;
21284 (void)submit_or_replace(std::move(request), std::move(close.second), false,
21285 "__pine_close_all");
21286 }
21287}
21288
21289// A call taken at the close whose book, re-marked at the call's own fill, is
21290// still short of margin by more than two slippage steps (its exit fee added
21291// back) is followed at the next path point, as a call inside a bar is
21292// (follow_margin_call, margin_follow_up_units): after the close that point is
21293// the next open, where the follow-up waits behind the orders the script placed
21294// at the close and is taken only where that open's print is not beyond the
21295// call's fill (close_point_margin_call_at_open). A call of any size is followed
21296// so: a synthetic NYSE:F call at slippage 1 can leave 200 shares 0.10 short
21297// at its 25.01 fill off a 25.00 close; a 24.995 next open can take one more,
21298// behind the script's close_all. The follow-up is owed at the next point,
21299// rather than being booked ahead of the script's queued orders.
21300void PineExecutionAdapter::close_point_follow_up(
21301 double mark, double called_units, const NativeDecisionContext& context) {
21302 if (!margin_follow_up_scope()) return;
21303 const auto position = detail::run_position(require_host());
21304 if (position.signed_units == 0.0) return;
21305 const bool buy = position.signed_units < 0.0;
21306 const double fill = source_margin_fill_price(mark, buy);
21307 const double units = margin_follow_up_units(called_units, fill, context.sub_bar_open_ms);
21308 if (!(units > 0.0)) return;
21309 (void)book_close_point_call(mark, units, context, true, fill);
21310}
21311
21312// The next open of a close that owed a one-unit call (close_point_margin_call):
21313// the book is the one that close checked, so its money is re-read at that
21314// close's mark and the unit is tested at this open's fill. One the close left
21315// standing executes here, ahead of the orders of that bar (a measured TradingView run, shown with synthetic numbers; a short:
21316// 100 shares, a 2.00 margin shortfall at a 25.01 close mark; buying one
21317// at 25.01 off the next 25.00 open leaves 99 shares and restores margin);
21318// one the script cancelled and placed again is withdrawn here
21319// when its fill would not restore the book (w13-b2-cancel-close-f), rather than being forced
21320// through on the strength of the close's earlier one-unit verdict.
21321bool PineExecutionAdapter::close_point_margin_call_at_open(
21322 const Bar& bar, const Bar& prior, const NativeDecisionContext& context) {
21323 if (close_margin_open_bar_ == std::numeric_limits<std::int64_t>::min()) return false;
21324 const bool owed = close_margin_open_bar_ == prior.timestamp;
21325 close_margin_open_bar_ = std::numeric_limits<std::int64_t>::min();
21326 std::vector<native_order::RequestHandle> queued;
21327 bool behind_orders = false;
21328 for (const auto& handle : live_handles_) {
21329 const auto found = placement_.find(handle.incarnation);
21330 if (found == placement_.end() || found->second.placement_script_open_ms != prior.timestamp)
21331 continue;
21332 if (found->second.family == PineOrderFamily::Margin && found->second.crosses_zero) {
21333 queued.push_back(handle);
21334 } else if (!finite_positive(found->second.exit_levels.limit)
21335 && !finite_positive(found->second.exit_levels.stop)
21336 && !finite_positive(found->second.exit_levels.trail_offset)
21337 && !finite_positive(found->second.exit_levels.trail_points)) {
21338 behind_orders = true;
21339 }
21340 }
21341 const auto withdraw = [&](native_order::RequestHandle handle) {
21342 if (require_host().cancel(handle).status == native_order::CancelStatus::Cancelled)
21343 retire(handle);
21344 };
21345 const auto position = detail::run_position(require_host());
21346 if (!owed || position.signed_units == 0.0) {
21347 for (const auto& handle : queued) withdraw(handle);
21348 return false;
21349 }
21350 // A one-unit call is tested at this open's fill against the money of the
21351 // book at that close; a follow-up is taken where this open's print is not
21352 // beyond the fill of the call it follows (w13-b1s-close-f; synthetic short:
21353 // opens at or inside a 25.00 fill take their unit; an open at 25.04
21354 // takes none).
21355 const bool buy = position.signed_units < 0.0;
21356 const double fill = source_margin_fill_price(bar.open, buy);
21357 const double open_mark = nearest_tick(bar.open, staged_.syminfo.mintick);
21358 const double guard = staged_.syminfo.mintick * 1e-6;
21359 const auto money = source_margin_money(prior.close, context.sub_bar_open_ms);
21360 std::vector<native_order::RequestHandle> kept;
21361 double units = 0.0;
21362 for (const auto& handle : queued) {
21363 const auto found = placement_.find(handle.incarnation);
21364 if (found == placement_.end()) continue;
21365 const double followed = found->second.follow_up_fill;
21366 const bool taken = finite_positive(followed)
21367 ? (buy ? open_mark <= followed + guard : open_mark >= followed - guard)
21368 : (!(source_margin_one_unit(money) > 0.0)
21369 || source_margin_unit_restores(money, fill));
21370 if (!taken) {
21371 withdraw(handle);
21372 continue;
21373 }
21374 kept.push_back(handle);
21375 if (finite_positive(found->second.requested_qty)) units += found->second.requested_qty;
21376 }
21377 if (kept.empty()) return false;
21378 // Behind the orders the script placed at that close, the calls keep their
21379 // place and fill after them in this open's matching pass; with none, they
21380 // are this open's first execution, ahead of the open's own check.
21381 if (behind_orders) return true;
21382 for (const auto& handle : kept) withdraw(handle);
21383 if (!submit_margin_call_units(bar.open, context, units, true)) return false;
21384 // A follow-up so taken is followed alike, at this same open while the
21385 // book re-marked at its fill is still short by more than two slippage
21386 // steps (margin_follow_up_units; w13-b5s-none-f; synthetic next open at 25.00 with
21387 // one-tick buy slippage: successive unit buys each fill at 25.01).
21388 for (int more = 0; more < 8 && margin_follow_up_scope(); ++more) {
21389 if (detail::run_position(require_host()).signed_units == 0.0) break;
21390 const double follow_up = margin_follow_up_units(units, fill, context.sub_bar_open_ms);
21391 if (!(follow_up > 0.0)) break;
21392 if (!submit_margin_call_units(bar.open, context, follow_up, true)) break;
21393 units = follow_up;
21394 }
21395 return false;
21396}
21397
21398bool PineExecutionAdapter::declined_reversal_at_open(const Bar& bar) const {
21399 const auto position = detail::run_position(require_host());
21400 if (position.signed_units == 0.0) return false;
21401 for (const auto& handle : live_handles_) {
21402 const auto found = placement_.find(handle.incarnation);
21403 if (found == placement_.end()) continue;
21404 const auto& candidate = found->second;
21405 // Only a default quantity can be declined here: ab9714be scopes the
21406 // fill-time decline to a frozen default quantity
21407 // (pine_fills.cpp:5227-5235, set by pine_strategy_commands.cpp:596-601
21408 // only when qty is omitted). An explicit qty is never priced as the
21409 // default value (lane TVDEF-DROPS, tests/fixtures/tvdef_drops R2).
21410 if (!candidate.opening || candidate.family != PineOrderFamily::Entry
21411 || candidate.is_long == (position.signed_units > 0.0)
21412 || !candidate.reverse_to || candidate.projection_after_close
21413 || std::isfinite(candidate.requested_qty)) {
21414 continue;
21415 }
21416 double units = candidate.sizing.frozen_units;
21417 if (!finite_positive(units)) units = config_.default_qty_value;
21418 const double fill = nearest_tick(bar.open, staged_.syminfo.mintick);
21419 const double margin = candidate.is_long ? config_.margin_long : config_.margin_short;
21420 const double required = units * fill * staged_.syminfo.pointvalue
21421 * active_staged_fx(bar.timestamp) * margin / 100.0;
21422 const double equity = candidate.sizing.equity;
21423 const double epsilon = std::max(1e-9, std::abs(equity) * 1e-12);
21424 if (finite_positive(units) && std::isfinite(required)
21425 && std::isfinite(equity) && required > equity + epsilon) {
21426 return true;
21427 }
21428 }
21429 return false;
21430}
21431
21432void PineExecutionAdapter::defer_declined_reversal_exits_at_adverse(
21433 const Bar& bar, const NativeDecisionContext&, bool margin_scheduled) {
21434 const auto position = detail::run_position(require_host());
21435 if (position.signed_units == 0.0) return;
21436 const double adverse = position.signed_units > 0.0 ? bar.low : bar.high;
21437 if (!finite_positive(adverse)) return;
21438
21439 const PlacementSnapshot* declined_reversal = nullptr;
21440 for (const auto& handle : live_handles_) {
21441 const auto found = placement_.find(handle.incarnation);
21442 if (found == placement_.end()) continue;
21443 const auto& candidate = found->second;
21444 // As in declined_reversal_at_open: an explicit qty is never declined.
21445 if (!candidate.opening || candidate.family != PineOrderFamily::Entry
21446 || candidate.is_long == (position.signed_units > 0.0)
21447 || !candidate.reverse_to || candidate.projection_after_close
21448 || std::isfinite(candidate.requested_qty)) {
21449 continue;
21450 }
21451 double units = candidate.sizing.frozen_units;
21452 if (!finite_positive(units)) units = config_.default_qty_value;
21453 const double fill = nearest_tick(bar.open, staged_.syminfo.mintick);
21454 const double margin = candidate.is_long ? config_.margin_long : config_.margin_short;
21455 const double required = units * fill * staged_.syminfo.pointvalue
21456 * active_staged_fx(bar.timestamp) * margin / 100.0;
21457 const double equity = candidate.sizing.equity;
21458 const double epsilon = std::max(1e-9, std::abs(equity) * 1e-12);
21459 if (finite_positive(units) && std::isfinite(required)
21460 && std::isfinite(equity) && required > equity + epsilon) {
21461 declined_reversal = &candidate;
21462 break;
21463 }
21464 }
21465 if (!declined_reversal) return;
21466
21467 // ab9714be:pine_fills.cpp:5509-5519,7232-7277. A whole-position
21468 // strategy.close created after this same-bar reversal is the reversal's
21469 // dependent closing leg. If the opening half is unaffordable, remove
21470 // that close before the generic open candidate is visited; otherwise its
21471 // ordinary MARKET trigger would flatten the held position independently
21472 // and erase the margin/revival chronology the source command specified.
21473 std::vector<native_order::RequestHandle> dependent_closes;
21474 const double held = std::abs(position.signed_units);
21475 for (const auto& handle : live_handles_) {
21476 const auto found = placement_.find(handle.incarnation);
21477 if (found == placement_.end()) continue;
21478 const auto& snapshot = found->second;
21479 const bool full = std::isfinite(snapshot.projection_remaining_qty)
21480 ? snapshot.projection_remaining_qty >= held - 1e-10
21481 : (std::isfinite(snapshot.requested_qty)
21482 ? std::abs(snapshot.requested_qty) >= held - 1e-10
21483 : std::isfinite(snapshot.qty_percent)
21484 && snapshot.qty_percent >= 100.0 - 1e-9);
21485 if (snapshot.family == PineOrderFamily::Close && full
21486 && snapshot.command_ordinal > declined_reversal->command_ordinal
21487 && snapshot.projection_created_bar
21488 == declined_reversal->projection_created_bar) {
21489 dependent_closes.push_back(handle);
21490 }
21491 }
21492 for (const auto& handle : dependent_closes) {
21493 const auto result = require_host().cancel(handle);
21494 if (result.status == native_order::CancelStatus::Cancelled) retire(handle);
21495 }
21496
21497 struct DeferredStop {
21498 native_order::RequestHandle handle;
21499 PlacementSnapshot snapshot;
21500 };
21501 std::vector<DeferredStop> stops;
21502 for (const auto& handle : live_handles_) {
21503 const auto found = placement_.find(handle.incarnation);
21504 if (found == placement_.end()) continue;
21505 const auto& snapshot = found->second;
21507 || snapshot.from_entry.empty() || snapshot.void_issue
21508 || !finite_positive(snapshot.exit_levels.stop)
21509 || std::isfinite(snapshot.requested_qty)
21510 || !(snapshot.qty_percent >= 100.0 - 1e-9)) {
21511 continue;
21512 }
21513 const bool adverse_reaches = position.signed_units > 0.0
21514 ? adverse <= snapshot.exit_levels.stop
21515 : adverse >= snapshot.exit_levels.stop;
21516 // A stop the open is already through fills at that open, ahead of
21517 // the declined reversal: TradingView exits there (lab tv
21518 // w6-f12b-stop-priority-default B2, lane W6-ENG-FILL-ORDER).
21519 const bool through_at_open = position.signed_units > 0.0
21520 ? bar.open <= snapshot.exit_levels.stop
21521 : bar.open >= snapshot.exit_levels.stop;
21522 if (adverse_reaches && !through_at_open) stops.push_back({handle, snapshot});
21523 }
21524 for (auto& deferred : stops) {
21525 if (!margin_scheduled) {
21526 const auto result = require_host().cancel(deferred.handle);
21527 if (result.status == native_order::CancelStatus::Cancelled)
21528 retire(deferred.handle);
21529 continue;
21530 }
21531 native_order::Request request;
21532 request.intent = native_order::HostSized{
21533 native_order::HostSizedKind::Close, std::nullopt};
21534 request.label = deferred.snapshot.source_id;
21535 request.comment = deferred.snapshot.comment;
21536 request.trigger = native_order::Stop{adverse};
21537 request.owner = owner_for_close(deferred.snapshot.from_entry, true);
21538 const std::string group_name = deferred.snapshot.oca_name.empty()
21539 ? deferred.snapshot.source_id + "\x1f" + deferred.snapshot.from_entry
21540 : deferred.snapshot.oca_name;
21541 request.group = group_for(group_name, 1);
21542 deferred.snapshot.forced_execution_price = adverse;
21543 const SourceId replacement_key = deferred.snapshot.source_id + "\x1f"
21544 + deferred.snapshot.from_entry
21545 + std::to_string(static_cast<int>(PineOrderFamily::ExitStop));
21546 const auto accepted = submit_or_replace(
21547 std::move(request), std::move(deferred.snapshot), false,
21548 replacement_key);
21549 if (accepted) {
21550 bracket_family(key_for(
21551 placement_.at(accepted->incarnation).source_id,
21552 placement_.at(accepted->incarnation).from_entry)).push_back(*accepted);
21553 }
21554 }
21555}
21556
21557bool PineExecutionAdapter::submit_intraday_loss_close(
21558 double mark_price, const NativeDecisionContext& context, bool execute_current) {
21559 if (!intraday_loss_breached(mark_price)
21560 || detail::run_position(require_host()).signed_units == 0.0) {
21561 return false;
21562 }
21563 native_order::Request request;
21564 request.intent = native_order::Flatten{};
21565 // The legacy forced-close report has an empty exit id and this exact
21566 // comment. An empty generic label is supported by the request algebra.
21567 request.comment = kIntradayLossComment;
21568 if (!execute_current) request.trigger = native_order::Stop{mark_price};
21569 PlacementSnapshot snapshot;
21571 snapshot.source_id = "__intraday_loss__";
21572 snapshot.comment = request.comment;
21573 snapshot.sizing = sizing_snapshot();
21574 const auto accepted = submit_or_replace(std::move(request), std::move(snapshot), false,
21575 "__intraday_loss_close__");
21576 if (!accepted) return false;
21577 if (execute_current) {
21578 const auto result = require_host().execute_current(
21579 {*accepted, NativeCurrentPriceRule::NearestTick});
21580 if (const auto* applied = std::get_if<native_order::ExecutionAppliedEvent>(&result);
21581 applied && applied->closed_units > 0.0) {
21582 risk_.intraday_block_day = chart_day_key(context.sub_bar_open_ms);
21583 risk_.intraday_cancel_pending = true;
21584 }
21585 }
21586 return true;
21587}
21588
21589void PineExecutionAdapter::schedule_intraday_loss_path(
21590 const Bar& bar, const NativeDecisionContext& context) {
21591 const auto position = detail::run_position(require_host());
21592 if (position.signed_units == 0.0) return;
21593 const double adverse = position.signed_units > 0.0 ? bar.low : bar.high;
21594 if (!finite_positive(adverse) || adverse == bar.open) return;
21595 if (position.signed_units > 0.0 ? !(adverse < bar.open) : !(adverse > bar.open))
21596 return;
21597 (void)submit_intraday_loss_close(adverse, context, false);
21598}
21599
21600void PineExecutionAdapter::execute_due_cap_close(const NativeDecisionContext& context) {
21601 const auto due = cap.due_cause();
21602 if (!due || context.coordinate.interval_index <= due->trigger_bar
21603 || detail::run_position(require_host()).signed_units == 0.0) {
21604 return;
21605 }
21606 native_order::Request request;
21607 request.intent = native_order::Flatten{};
21608 request.comment = "Close Position (Max number of filled orders in one day)";
21609 PlacementSnapshot snapshot;
21611 snapshot.source_id = "__intraday_cap_close__";
21612 snapshot.comment = request.comment;
21613 const auto accepted = submit_or_replace(std::move(request), std::move(snapshot), false,
21614 "__intraday_cap_close__");
21615 if (accepted) {
21616 (void)require_host().execute_current({*accepted, NativeCurrentPriceRule::NearestTick});
21617 cap.after_immediate_close_attempt();
21618 }
21619}
21620
21621void PineExecutionAdapter::execute_cap_close_now(const compat::pine::CloseNow& close) {
21622 if (detail::run_position(require_host()).signed_units == 0.0) {
21623 cap.after_immediate_close_attempt();
21624 return;
21625 }
21626 native_order::Request request;
21627 request.intent = native_order::Flatten{};
21628 request.comment = close.request.comment;
21629 PlacementSnapshot snapshot;
21631 snapshot.source_id = "__intraday_cap_close__";
21632 snapshot.comment = close.request.comment;
21633 const bool closing_long = detail::run_position(require_host()).signed_units > 0.0;
21634 snapshot.forced_execution_price = nearest_tick(
21635 close.price + (closing_long ? -1.0 : 1.0) * config_.slippage * staged_.syminfo.mintick,
21636 staged_.syminfo.mintick);
21637 const auto accepted = submit_or_replace(std::move(request), std::move(snapshot), false,
21638 "__intraday_cap_close__");
21639 if (accepted) {
21640 (void)require_host().execute_current({*accepted, NativeCurrentPriceRule::NearestTick});
21641 }
21642 cap.after_immediate_close_attempt();
21643}
21644
21645void PineExecutionAdapter::observe_intraday_cap(
21646 const native_order::ExecutionAppliedEvent& event,
21647 const PlacementSnapshot& snapshot, const NativeDecisionContext& context) {
21648 if (!cap.active()) return;
21650 || snapshot.source_id == "__intraday_cap_close__")
21651 return;
21652 const auto clock = cap_clock(context);
21653 const auto calculation = cap_calculation(context);
21654 if (snapshot.family == PineOrderFamily::Close
21655 || snapshot.family == PineOrderFamily::CloseAll) {
21656 const bool full = event.closed_units > 0.0
21657 && detail::run_position(require_host()).signed_units == 0.0;
21658 const bool observe_close = cap.direct_close_routing(calculation, full)
21660 const bool ordinary_full_close = full && calculation.process_on_close
21661 && !calculation.calc_on_fills && !calculation.coof_scheduler
21662 && !calculation.magnifier && !calculation.stream_warmup
21663 && calculation.stream_idle && calculation.fifo;
21664 if (observe_close || ordinary_full_close) {
21666 if (event.closed_trade_count > 0
21667 && event.first_trade_index
21668 < static_cast<std::size_t>(require_host().trade_count())) {
21669 before = require_host().get_trade(
21670 static_cast<int>(event.first_trade_index)).is_long
21672 }
21673 std::vector<compat::pine::ContinuationCandidate> candidates;
21674 std::optional<native_order::RequestHandle> continuation;
21675 std::optional<PlacementSnapshot> continuation_snapshot;
21676 std::size_t continuation_index = 0;
21677 std::uint64_t continuation_sequence =
21678 std::numeric_limits<std::uint64_t>::max();
21679 for (const auto& handle : live_handles_) {
21680 if (handle == event.handle()) continue;
21681 const auto found = placement_.find(handle.incarnation);
21682 if (found == placement_.end()) continue;
21683 const auto& candidate = found->second;
21684 const bool market = candidate.family == PineOrderFamily::Entry
21685 && !finite_positive(candidate.exit_levels.limit)
21686 && !finite_positive(candidate.exit_levels.stop)
21687 && !finite_positive(candidate.exit_levels.trail_offset);
21688 candidates.push_back({
21690 : (candidate.family == PineOrderFamily::Entry
21693 candidate.projection_created_bar, candidate.is_long,
21694 static_cast<std::int64_t>(candidate.command_sequence),
21695 handle.incarnation});
21696 if (market && candidate.is_long != (before == compat::pine::Side::Long)
21697 && candidate.projection_created_bar == calculation.bar
21698 && candidate.command_sequence < continuation_sequence) {
21699 continuation = handle;
21700 continuation_snapshot = candidate;
21701 continuation_index = candidates.size() - 1U;
21702 continuation_sequence = candidate.command_sequence;
21703 }
21704 }
21705 double continuation_units = continuation_snapshot
21706 ? continuation_snapshot->frozen_market_own_units : kNaN;
21707 if (continuation_snapshot && !finite_positive(continuation_units)) {
21708 if (finite_positive(continuation_snapshot->requested_qty)) {
21709 continuation_units = std::abs(continuation_snapshot->requested_qty);
21710 } else if (config_.default_qty_type == static_cast<int>(QtyType::FIXED)) {
21711 continuation_units = config_.default_qty_value;
21712 } else {
21713 continuation_units = continuation_snapshot->sizing.frozen_units;
21714 }
21715 }
21716 if (candidates.size() != 1U) continuation.reset();
21717 if (candidates.size() == 1U && continuation && continuation_snapshot
21718 && finite_positive(continuation_units)) {
21719 const auto old = *continuation;
21720 const auto cancelled = require_host().cancel(old);
21721 if (cancelled.status == native_order::CancelStatus::Cancelled) retire(old);
21722 native_order::Request request;
21724 continuation_snapshot->is_long
21725 ? continuation_units : -continuation_units};
21726 request.label = continuation_snapshot->source_id;
21727 request.comment = continuation_snapshot->comment;
21728 request.trigger = native_order::Market{};
21729 continuation_snapshot->projection_predecessor = old.incarnation;
21730 continuation_snapshot->projection_predecessor_market = true;
21731 // This is an adapter-created continuation request, not a
21732 // second binding of the original immutable admission draft.
21733 // Give the successor its own journal event while retaining
21734 // predecessor provenance on the placement snapshot.
21735 continuation_snapshot->market_admission = {};
21736 const auto accepted = submit_or_replace(
21737 std::move(request), *continuation_snapshot, true,
21738 continuation_snapshot->source_id);
21739 if (accepted) {
21740 continuation = *accepted;
21741 candidates[continuation_index].incarnation = accepted->incarnation;
21742 } else {
21743 continuation.reset();
21744 candidates.erase(candidates.begin()
21745 + static_cast<std::ptrdiff_t>(continuation_index));
21746 }
21747 }
21748 if (observe_close) {
21749 // Only an opposite entry the script placed BEFORE this close
21750 // inherits its slot: that entry's reversal is what closed the
21751 // position, so TradingView books the bar as one fill. One
21752 // placed after the close opens from flat and is a fill of its
21753 // own (lane W10-DIAG-UNKNOWN rule CAP-ORDER,
21754 // tests/fixtures/intraday_cap_tv).
21755 std::vector<compat::pine::ContinuationCandidate> inheritors;
21756 for (const auto& candidate : candidates) {
21757 if (static_cast<std::uint64_t>(candidate.created_seq)
21758 < snapshot.command_sequence) {
21759 inheritors.push_back(candidate);
21760 }
21761 }
21762 cap.committed_close(clock, calculation, before, event.ordinal, inheritors);
21763 }
21764 cap_latest_fill_ = event.ordinal;
21765 if (continuation) {
21766 (void)require_host().execute_current(
21767 {*continuation, NativeCurrentPriceRule::NearestTick});
21768 }
21769 }
21770 // Direct closes are candidate C, not ordinary matched-attempt factor
21771 // A. Unsupported calculation modes and metadata-off runs deliberately
21772 // remain uncounted.
21773 return;
21774 }
21775 const auto attempt = cap_attempt(snapshot, event.handle().incarnation, &event);
21776 const auto origin = cap.origin(clock, calculation, event.handle().incarnation, cap_latest_fill_);
21777 const auto admission = cap.pre_dispatch(clock, calculation, attempt, cap_latest_fill_);
21779 cap.decline(event.handle().incarnation);
21780 return;
21781 }
21782 const bool primary_fill_applied = event.closed_units > 0.0
21783 || event.opened_units != 0.0 || event.closed_trade_count > 0
21784 || event.opened_lot_incarnation != 0
21785 || event.cycle_before != event.cycle_after;
21786 cap.outcome(primary_fill_applied ? compat::pine::FillOutcome::Committed
21788 origin);
21789 const auto position = detail::run_position(require_host());
21790 Bar prices = policy_script_bar_valid_ ? policy_script_bar_ : coof_script_bar_;
21791 if (const auto* pine_host = pine_view_of(&require_host())) {
21792 if (const auto broker = pine_host->scheduler_.broker_bar(*pine_host, context))
21793 prices = *broker;
21794 }
21795 const auto decision = cap.post_dispatch(admission, calculation, attempt,
21796 position.signed_units > 0.0 ? compat::pine::Side::Long
21797 : (position.signed_units < 0.0 ? compat::pine::Side::Short
21799 current_position_cycle_,
21800 {((context.coordinate.path_phase == NativePathPhase::None || context.coordinate.path_phase == NativePathPhase::Close)
21801 && config_.process_orders_on_close)
21802 ? prices.close : event.resolved_price,
21803 prices.open, prices.high, prices.low});
21804 if (const auto* now = std::get_if<compat::pine::CloseNow>(&decision)) {
21805 execute_cap_close_now(*now);
21806 }
21807 cap_latest_fill_ = event.ordinal;
21808}
21809
21810void PineExecutionAdapter::observe_intraday_cap_noop(
21811 bool is_long, const NativeDecisionContext& context) {
21812 if (!cap.active()) return;
21813 PlacementSnapshot snapshot;
21815 snapshot.is_long = is_long;
21816 snapshot.projection_position_side = is_long
21817 ? static_cast<std::int32_t>(PositionSide::LONG)
21818 : static_cast<std::int32_t>(PositionSide::SHORT);
21819 snapshot.projection_created_bar = projection_bar_index(context);
21820 snapshot.source_sequence = source_sequence_;
21821 const auto clock = cap_clock(context);
21822 const auto calculation = cap_calculation(context);
21823 const auto attempt = cap_attempt(snapshot, 0);
21824 const auto origin = cap.origin(clock, calculation, 0, cap_latest_fill_);
21825 const auto admission = cap.pre_dispatch(clock, calculation, attempt, cap_latest_fill_);
21827 cap.decline(0);
21828 return;
21829 }
21831 const auto position = detail::run_position(require_host());
21832 Bar prices = policy_script_bar_valid_ ? policy_script_bar_ : coof_script_bar_;
21833 if (const auto* pine_host = pine_view_of(&require_host())) {
21834 if (const auto broker = pine_host->scheduler_.broker_bar(*pine_host, context))
21835 prices = *broker;
21836 }
21837 const auto decision = cap.post_dispatch(admission, calculation, attempt,
21838 position.signed_units > 0.0 ? compat::pine::Side::Long
21839 : (position.signed_units < 0.0 ? compat::pine::Side::Short
21841 current_position_cycle_,
21842 {context.coordinate.path_phase == NativePathPhase::None ? prices.close
21843 : detail::callback_point(require_host())->price,
21844 prices.open, prices.high, prices.low});
21845 if (const auto* now = std::get_if<compat::pine::CloseNow>(&decision)) {
21846 execute_cap_close_now(*now);
21847 }
21848}
21849
21851 cap.source_batch_end();
21852}
21853
21854// `bar` is the projection bar (projection_bar_index), the bar space of the
21855// command observations the review receipts name.
21856void PineExecutionAdapter::record_market_review(
21857 admission::Checkpoint checkpoint, int bar,
21858 const std::vector<native_order::RequestHandle>& handles) {
21859 std::vector<native_order::RequestHandle> selected;
21860 selected.reserve(handles.size());
21861 for (const auto& handle : handles) {
21862 const auto found = placement_.find(handle.incarnation);
21863 if (found == placement_.end()) continue;
21864 const auto& draft = found->second.market_admission;
21865 if (!draft.observation() || draft.review()) continue;
21866 const bool belongs = checkpoint == admission::Checkpoint::TerminalGross
21867 || (checkpoint == admission::Checkpoint::DefaultGross
21869 || (checkpoint == admission::Checkpoint::ExplicitPair
21871 if (belongs) selected.push_back(handle);
21872 }
21873 if (selected.empty()) return;
21874
21875 auto allocation = admission_journal.reserve();
21877 review.receipt = {allocation.sequence(), checkpoint, bar, 0};
21878 review.open_price = policy_script_bar_valid_ ? policy_script_bar_.open : kNaN;
21879 const auto physical = detail::run_position(require_host());
21880 review.position_side = physical.signed_units > 0.0
21881 ? static_cast<int>(PositionSide::LONG)
21882 : (physical.signed_units < 0.0 ? static_cast<int>(PositionSide::SHORT)
21883 : static_cast<int>(PositionSide::FLAT));
21884 review.position_cycle = current_position_cycle_;
21885 for (const auto& handle : selected) {
21886 auto found = placement_.find(handle.incarnation);
21887 if (found == placement_.end()) continue;
21888 auto& snapshot = found->second;
21889 const auto& origin = snapshot.market_admission.observation();
21890 if (!origin) continue;
21891 admission::BookObservation book;
21892 book.incarnation = handle.incarnation;
21893 book.priority = static_cast<std::int64_t>(snapshot.source_sequence);
21894 book.bar = snapshot.projection_created_bar;
21895 book.type = snapshot.family == PineOrderFamily::Order ? 3 : 0;
21896 book.placement_side = snapshot.projection_position_side;
21897 book.buy = snapshot.is_long;
21898 book.id = snapshot.source_id;
21899 book.oca_name = snapshot.oca_name;
21900 book.oca_type = snapshot.oca_type;
21901 book.birth = snapshot.birth;
21902 book.prices = {snapshot.exit_levels.limit, snapshot.exit_levels.stop,
21903 snapshot.exit_levels.trail_points,
21904 snapshot.exit_levels.trail_price,
21905 snapshot.exit_levels.trail_offset};
21906 book.draft = snapshot.market_admission;
21907 review.book.push_back(book);
21908 review.reviewed.push_back(book);
21909 snapshot.market_admission.reviewed(
21910 {allocation.sequence(), checkpoint, bar, origin->command});
21911 if (review.reviewed.size() == 1U) review.configuration = origin->configuration;
21912 }
21913 admission_journal.append(std::move(review));
21914}
21915
21916void PineExecutionAdapter::refresh_pending_sizing_after_margin(
21917 const native_order::ExecutionAppliedEvent& event,
21918 const NativeDecisionContext& context) {
21919 const double mark = policy_script_bar_valid_
21920 ? policy_script_bar_.close : event.resolved_price;
21921 PineSizingSnapshot marked;
21922 mark_sizing_equity(marked, nearest_tick(mark, staged_.syminfo.mintick));
21923 const double active_fx = active_staged_fx(context.sub_bar_open_ms);
21924 const std::uint64_t cause_fill =
21925 static_cast<PineStrategyHost&>(require_host()).broker_fill_event_seq_;
21926 for (const auto& handle : live_handles_) {
21927 auto found = placement_.find(handle.incarnation);
21928 if (found == placement_.end()) continue;
21929 auto& snapshot = found->second;
21930 if (snapshot.projection_created_bar != projection_bar_index(context)
21931 || !snapshot.market_admission.observation()) {
21932 continue;
21933 }
21934 const admission::SizingObservation before{
21935 snapshot.sizing.frozen_units, snapshot.sizing.equity,
21936 snapshot.sizing.price, snapshot.sizing.mark, snapshot.sizing.fx};
21937 const double affordability_before = snapshot.projection_affordability_equity;
21938 bool revised = false;
21939 const bool market_entry = snapshot.opening
21940 && (snapshot.family == PineOrderFamily::Entry
21941 || snapshot.family == PineOrderFamily::Order)
21942 && !finite_positive(snapshot.exit_levels.limit)
21943 && !finite_positive(snapshot.exit_levels.stop);
21944 if (market_entry && std::isfinite(snapshot.sizing.frozen_units)) {
21945 snapshot.sizing.equity = marked.equity;
21946 snapshot.sizing.strategy_equity = marked.strategy_equity;
21947 snapshot.sizing.fx = active_fx;
21948 snapshot.sizing.frozen_units = default_sizing_units(snapshot.sizing);
21949 revised = true;
21950 }
21951 if (market_entry
21952 && std::isfinite(snapshot.projection_affordability_equity)) {
21953 snapshot.projection_affordability_equity =
21954 require_host().native_marked_equity(mark);
21955 revised = true;
21956 }
21957 if (!revised || cause_fill == 0) continue;
21958
21959 const auto& origin = snapshot.market_admission.observation();
21960 auto allocation = admission_journal.reserve();
21961 admission::SizingEvent sizing;
21962 // A receipt's bar is in its command's bar space: the observation
21963 // bound the row's projection bar, the input slot, which an
21964 // aggregated run keeps apart from the script-bar interval_index
21965 // (lane FIX-E1E2: a receipt at interval_index preceded its command).
21966 sizing.receipt = {allocation.sequence(), cause_fill,
21967 projection_bar_index(context), origin->command};
21968 sizing.incarnation = handle.incarnation;
21969 sizing.before = before;
21970 sizing.after = {snapshot.sizing.frozen_units, snapshot.sizing.equity,
21971 snapshot.sizing.price, snapshot.sizing.mark,
21972 snapshot.sizing.fx};
21973 sizing.affordability_equity_before = affordability_before;
21974 sizing.affordability_equity_after =
21975 snapshot.projection_affordability_equity;
21976 snapshot.market_admission.sizing_revised(sizing.receipt);
21977 admission_journal.append(std::move(sizing));
21978 }
21979}
21980
21981void PineExecutionAdapter::apply_open_market_admission(
21982 const NativeDecisionContext& context) {
21983 const bool began_flat = detail::run_position(require_host()).signed_units == 0.0;
21984 const int source_bar = context.coordinate.interval_index - 1;
21985 struct Candidate {
21986 native_order::RequestHandle handle;
21987 const PlacementSnapshot* snapshot = nullptr;
21988 };
21989 std::vector<Candidate> market;
21990 bool foreign_live_order = false;
21991 // The historical placement scan is consumed only by the two-candidate
21992 // pair rule below; evaluate it there rather than on every broker open.
21993 const auto commands_on_bar = [&]() {
21994 std::size_t count = 0;
21995 // An entry or raw order is kept while its placement bar is the
21996 // previous one or later (erase_retired_rows, K5).
21998 [&](std::uint64_t, const PlacementSnapshot& snapshot) {
21999 return snapshot.projection_created_bar == source_bar
22000 && (snapshot.family == PineOrderFamily::Entry
22001 || snapshot.family == PineOrderFamily::Order);
22002 });
22003 for (const auto& row : placement_) {
22004 const auto& snapshot = row.second;
22005 if (snapshot.projection_created_bar != source_bar
22006 || (snapshot.family != PineOrderFamily::Entry
22007 && snapshot.family != PineOrderFamily::Order)) {
22008 continue;
22009 }
22010 ++count;
22011 }
22012 for (const auto& delayed : delayed_market_orders_) {
22013 if (delayed.snapshot.projection_created_bar == source_bar)
22014 ++count;
22015 }
22016 return count;
22017 };
22018 for (const auto& handle : live_handles_) {
22019 const auto found = placement_.find(handle.incarnation);
22020 if (found == placement_.end()) continue;
22021 const auto& snapshot = found->second;
22022 const bool unpriced_entry = snapshot.projection_created_bar == source_bar
22023 && snapshot.family == PineOrderFamily::Entry
22024 && !finite_positive(snapshot.exit_levels.limit)
22025 && !finite_positive(snapshot.exit_levels.stop)
22026 && !finite_positive(snapshot.exit_levels.trail_points)
22027 && !finite_positive(snapshot.exit_levels.trail_price)
22028 && !finite_positive(snapshot.exit_levels.trail_offset)
22029 && !snapshot.birth.from_fill() && !snapshot.birth.at_terminal_fill()
22031 if (unpriced_entry) {
22032 market.push_back({handle, &snapshot});
22033 continue;
22034 }
22035 const bool same_bar_unpriced_close = snapshot.projection_created_bar == source_bar
22036 && snapshot.family == PineOrderFamily::Close
22037 && snapshot.oca_name.empty()
22038 && !finite_positive(snapshot.exit_levels.limit)
22039 && !finite_positive(snapshot.exit_levels.stop)
22040 && !finite_positive(snapshot.exit_levels.trail_points)
22041 && !finite_positive(snapshot.exit_levels.trail_price)
22042 && !finite_positive(snapshot.exit_levels.trail_offset)
22043 && !snapshot.birth.from_fill() && !snapshot.birth.at_terminal_fill()
22045 if (!same_bar_unpriced_close) foreign_live_order = true;
22046 }
22047 const bool family_s_command_order =
22048 config_.default_qty_type == static_cast<int>(QtyType::FIXED)
22049 && config_.pyramiding == 1 && config_.slippage == 0
22050 && config_.commission_value == 0.0;
22051 std::stable_sort(market.begin(), market.end(), [&](const Candidate& left,
22052 const Candidate& right) {
22053 const std::uint64_t left_key = family_s_command_order
22054 ? left.snapshot->command_sequence : left.snapshot->source_sequence;
22055 const std::uint64_t right_key = family_s_command_order
22056 ? right.snapshot->command_sequence : right.snapshot->source_sequence;
22057 if (left_key != right_key) return left_key < right_key;
22058 if (left.snapshot->source_sequence != right.snapshot->source_sequence)
22059 return left.snapshot->source_sequence < right.snapshot->source_sequence;
22060 return left.handle.incarnation < right.handle.incarnation;
22061 });
22062
22063 std::vector<native_order::RequestHandle> cancellations;
22064 const auto cancel_later = [&](const native_order::RequestHandle& handle) {
22065 if (std::find(cancellations.begin(), cancellations.end(), handle)
22066 == cancellations.end()) {
22067 cancellations.push_back(handle);
22068 }
22069 };
22070
22071 if (market.size() == 2 && !foreign_live_order && commands_on_bar() == 2) {
22072 const auto& first = *market[0].snapshot;
22073 const auto& second = *market[1].snapshot;
22074 if (first.projection_predecessor == 0 && second.projection_predecessor == 0) {
22075 const bool default_pair =
22076 compat::pine::awaits_default_review(first.market_admission)
22077 && compat::pine::awaits_default_review(second.market_admission)
22078 && first.source_id != second.source_id
22079 && first.is_long != second.is_long;
22080 if (default_pair) {
22081 // ab9714be pine_fills.cpp:3005-3009: an earlier command that
22082 // was already at the entry cap contributes no broker movement
22083 // to the later call's all-in gross cost.
22084 const double first_units = first.projection_over_pyramiding
22085 ? 0.0 : first.sizing.frozen_units;
22086 const double first_margin = first.is_long
22087 ? config_.margin_long : config_.margin_short;
22088 const double second_margin = second.is_long
22089 ? config_.margin_long : config_.margin_short;
22090 const double required = first_units * first.sizing.price
22091 * staged_.syminfo.pointvalue * first.sizing.fx
22092 * first_margin / 100.0
22093 + second.sizing.frozen_units * second.sizing.price
22094 * staged_.syminfo.pointvalue * second.sizing.fx
22095 * second_margin / 100.0;
22096 const double equity = std::min(first.sizing.equity, second.sizing.equity);
22097 const double guard = std::max(1e-9, std::abs(equity) * 1e-12);
22098 if (std::isfinite(required) && std::isfinite(equity)
22099 && required > equity + guard) {
22100 cancel_later(market[1].handle);
22101 }
22102 } else if (began_flat && first.source_id != second.source_id
22103 && first.is_long != second.is_long && config_.pyramiding == 2
22104 && finite_positive(first.requested_qty)
22105 && finite_positive(second.requested_qty)
22106 && first.qty_type < 0 && second.qty_type < 0
22107 && first.oca_name.empty() && second.oca_name.empty()) {
22108 const double required = (first.requested_qty + second.requested_qty)
22109 * first.sizing.price * staged_.syminfo.pointvalue * first.sizing.fx;
22110 if (std::isfinite(required) && std::isfinite(first.sizing.equity)
22111 && required > first.sizing.equity) {
22112 cancel_later(market[1].handle);
22113 }
22114 }
22115 }
22116 }
22117
22118 // A same-side-at-placement request is executable only if an earlier
22119 // opposite command in this broker batch can move the account before its
22120 // turn. This retires the live-LONG pair's first no-op while preserving
22121 // the ordinary priced/raw/carried-book controls where the later request
22122 // becomes a reversal after its earlier sibling fills. In a batch of this
22123 // bar's market entries and closes alone -- no priced, raw or carried order
22124 // beside them, the controls that keep their ordinary fills -- a default
22125 // percent_of_equity (<= 100) add whose margin on top of the held side's
22126 // exceeded the equity when it was placed is not one of them: TradingView
22127 // dropped it there (entry(): the same rule inside the cap), so it never
22128 // reverses the book back once its sibling has reversed it (the
22129 // reversal-candle probe's tapes).
22130 const double held = detail::run_position(require_host()).signed_units;
22131 const auto dropped_at_placement = [&](const PlacementSnapshot& row) {
22132 return !foreign_live_order
22133 && !config_.process_orders_on_close && !config_.calc_on_order_fills
22134 && config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
22135 && config_.default_qty_value <= 100.0
22136 && row.deferred_cohort && !std::isfinite(row.requested_qty)
22137 && !row.projection_after_close
22138 && close_all_pending_script_bar_ != row.placement_script_open_ms
22139 && held != 0.0 && (held > 0.0) == row.is_long
22140 && finite_positive(row.sizing.frozen_units) && finite_positive(row.sizing.mark)
22141 && add_margin_exceeds_equity(std::abs(held), row.sizing,
22142 row.is_long ? config_.margin_long : config_.margin_short,
22143 staged_.syminfo.mintick, staged_.syminfo.pointvalue)
22144 && std::none_of(delayed_market_orders_.begin(), delayed_market_orders_.end(),
22145 [&](const DelayedMarketOrder& delayed) {
22146 return delayed.snapshot.projection_created_bar == source_bar;
22147 });
22148 };
22149 for (std::size_t index = 0; index < market.size(); ++index) {
22150 if (!market[index].snapshot->projection_over_pyramiding) continue;
22151 bool earlier_opposite = false;
22152 for (std::size_t prior = 0; prior < index; ++prior) {
22153 if (market[prior].snapshot->is_long != market[index].snapshot->is_long) {
22154 earlier_opposite = true;
22155 break;
22156 }
22157 }
22158 if (!earlier_opposite || dropped_at_placement(*market[index].snapshot))
22159 cancel_later(market[index].handle);
22160 }
22161 std::vector<native_order::RequestHandle> review_handles;
22162 review_handles.reserve(market.size());
22163 for (const auto& candidate : market) review_handles.push_back(candidate.handle);
22164 record_market_review(admission::Checkpoint::DefaultGross,
22165 projection_bar_index(context), review_handles);
22166 record_market_review(admission::Checkpoint::ExplicitPair,
22167 projection_bar_index(context), review_handles);
22168 for (const auto& handle : cancellations) {
22169 const auto result = require_host().cancel(handle);
22170 if (result.status == native_order::CancelStatus::Cancelled) retire(handle);
22171 }
22172}
22173
22174void PineExecutionAdapter::defer_open_marketable_sells(const Bar& bar) {
22175 if (config_.calc_on_order_fills || !modeled_input())
22176 return;
22177 if (detail::run_position(require_host()).signed_units != 0.0) return;
22178 const bool high_first = source_path_uses_high_first(bar);
22179 // ab9714be pine_fills.cpp:3687-3860 is not gated on process_orders_on_close.
22180 // Under POOC the fill point of a marketable order is the bar close
22181 // (pine_fills.cpp:7964-7965), not the open.
22182 const double fill_point_price = config_.process_orders_on_close ? bar.close : bar.open;
22183 struct Candidate {
22184 native_order::RequestHandle handle;
22185 const PlacementSnapshot* snapshot = nullptr;
22186 double path_position = 0.0;
22187 bool open_marketable = false;
22188 bool touched = false;
22189 };
22190 std::vector<Candidate> buys;
22191 std::vector<Candidate> sells;
22192 for (const auto& handle : live_handles_) {
22193 const auto found = placement_.find(handle.incarnation);
22194 if (found == placement_.end()) continue;
22195 const auto& snapshot = found->second;
22196 if (snapshot.family != PineOrderFamily::Entry
22197 || !finite_positive(snapshot.exit_levels.stop)
22198 || finite_positive(snapshot.exit_levels.limit)
22199 || finite_positive(snapshot.exit_levels.trail_points)
22200 || finite_positive(snapshot.exit_levels.trail_price)
22201 || finite_positive(snapshot.exit_levels.trail_offset)) {
22202 continue;
22203 }
22204 Candidate row;
22205 row.handle = handle;
22206 row.snapshot = &snapshot;
22207 row.open_marketable = pure_stop_entry_marketable_at(snapshot, fill_point_price);
22209 bar, high_first, snapshot.exit_levels.stop, snapshot.is_long,
22210 &row.path_position);
22211 if (snapshot.is_long) buys.push_back(row);
22212 else sells.push_back(row);
22213 }
22214 if (buys.empty() || sells.empty()) return;
22215 std::uint64_t min_open_buy_incarnation = std::numeric_limits<std::uint64_t>::max();
22216 bool open_buy = false;
22217 for (const auto& buy : buys) {
22218 if (!buy.open_marketable) continue;
22219 open_buy = true;
22220 min_open_buy_incarnation = std::min(min_open_buy_incarnation, buy.handle.incarnation);
22221 }
22222 if (!open_buy) return;
22223 std::vector<Candidate> deferred;
22224 bool deferred_open_sell = false;
22225 for (const auto& sell : sells) {
22226 if (sell.open_marketable && sell.handle.incarnation < min_open_buy_incarnation) {
22227 deferred.push_back(sell);
22228 deferred_open_sell = true;
22229 }
22230 }
22231 if (!deferred_open_sell) return;
22232 for (const auto& sell : sells) {
22233 if (sell.open_marketable) continue;
22234 // ab9714be: a resting stop the bar path never reaches is not an
22235 // order of this bar; it stays in the book for a later bar (L9d).
22236 if (!sell.touched) continue;
22237 deferred.push_back(sell);
22238 }
22239 std::stable_sort(deferred.begin(), deferred.end(),
22240 [](const Candidate& left, const Candidate& right) {
22241 return left.handle.incarnation < right.handle.incarnation;
22242 });
22243 deferred.erase(std::unique(deferred.begin(), deferred.end(),
22244 [](const Candidate& left, const Candidate& right) {
22245 return left.handle.incarnation == right.handle.incarnation;
22246 }),
22247 deferred.end());
22248 for (const auto& sell : deferred) {
22249 DeferredOpenMarketableSell row;
22250 row.snapshot = *sell.snapshot;
22251 row.replacement_key = sell.snapshot->source_id;
22252 row.fill_price = sell.open_marketable ? fill_point_price : sell.snapshot->exit_levels.stop;
22253 row.path_position = sell.path_position;
22254 row.open_marketable = sell.open_marketable;
22255 const auto result = require_host().cancel(sell.handle);
22256 if (result.status != native_order::CancelStatus::Cancelled) continue;
22257 retire(sell.handle);
22258 deferred_open_marketable_sells_.push_back(std::move(row));
22259 }
22260}
22261
22262void PineExecutionAdapter::admit_deferred_open_marketable_sells() {
22263 require_cohort_ticket();
22264 auto queued = std::move(deferred_open_marketable_sells_);
22265 deferred_open_marketable_sells_.clear();
22266 std::stable_sort(queued.begin(), queued.end(),
22267 [](const DeferredOpenMarketableSell& left,
22268 const DeferredOpenMarketableSell& right) {
22269 if (left.open_marketable != right.open_marketable)
22270 return !left.open_marketable && right.open_marketable;
22271 if (left.path_position != right.path_position)
22272 return left.path_position < right.path_position;
22273 return left.snapshot.command_sequence < right.snapshot.command_sequence;
22274 });
22275 for (auto& row : queued) {
22276 native_order::Request request;
22277 if (row.snapshot.deferred_cohort || !std::isfinite(row.snapshot.requested_qty)) {
22278 request.intent = native_order::HostSized{
22279 native_order::HostSizedKind::Open, native_order::Side::Short};
22280 } else {
22281 request.intent = native_order::Transact{-std::abs(row.snapshot.requested_qty)};
22282 }
22283 request.label = row.snapshot.source_id;
22284 request.comment = row.snapshot.comment;
22285 request.trigger = native_order::Market{};
22286 request.group = group_for(row.snapshot.oca_name, row.snapshot.oca_type);
22287 row.snapshot.forced_execution_price = row.fill_price;
22288 // ab9714be pine_fills.cpp:8517-8538: the second of a flat dual-stop
22289 // pair is a Transact against the already-opened side, not ReverseTo.
22290 row.snapshot.projection_after_close = true;
22291 row.snapshot.cancellation = {};
22292 row.snapshot.market_admission = {};
22293 const SourceId key = row.replacement_key;
22294 // ab9714be pine_fills.cpp:7483-7537: once a priced entry filled on
22295 // this bar, an entry that would open from flat is skipped for the
22296 // bar and keeps resting. Re-arm the original stop for the next bar
22297 // instead of filling it at the deferred price (L9d).
22298 const bool throttled_reopen =
22299 detail::run_position(require_host()).signed_units == 0.0
22300 && entry_openings_this_interval_ > 0;
22301 if (throttled_reopen) {
22302 request.trigger = native_order::Stop{row.snapshot.exit_levels.stop};
22303 row.snapshot.forced_execution_price = kNaN;
22304 row.snapshot.projection_after_close = false;
22305 (void)submit_or_replace(std::move(request), std::move(row.snapshot), true, key);
22306 continue;
22307 }
22308 const auto accepted = submit_or_replace(
22309 std::move(request), std::move(row.snapshot), true, key);
22310 if (accepted) {
22311 (void)require_host().execute_current(
22312 {*accepted, NativeCurrentPriceRule::NearestTick});
22313 }
22314 }
22315}
22316
22317void PineExecutionAdapter::apply_reversal_gap_bracket_policy(
22318 const Bar& bar, const NativeDecisionContext& context, bool defer_trails) {
22319 const auto physical = detail::run_position(require_host());
22320 if (physical.signed_units == 0.0) return;
22321 const int source_bar = context.coordinate.interval_index - 1;
22322 bool opposite_market = false;
22323 bool gap_decline = false;
22324 for (const auto& handle : live_handles_) {
22325 const auto found = placement_.find(handle.incarnation);
22326 if (found == placement_.end()) continue;
22327 const auto& entry = found->second;
22328 const bool opposite = (physical.signed_units > 0.0) != entry.is_long;
22329 if (!opposite || entry.family != PineOrderFamily::Entry || !entry.opening
22330 || entry.projection_created_bar != source_bar
22331 || std::isfinite(entry.requested_qty)
22332 || finite_positive(entry.exit_levels.limit)
22333 || finite_positive(entry.exit_levels.stop)
22334 || config_.default_qty_type
22335 != static_cast<int>(QtyType::PERCENT_OF_EQUITY)
22336 || config_.default_qty_value > 100.0
22337 || !finite_positive(entry.sizing.frozen_units)
22338 || !finite_positive(entry.sizing.equity)
22339 || !finite_positive(entry.sizing.price)
22340 || !finite_positive(entry.sizing.fx)) {
22341 continue;
22342 }
22343 const double margin = entry.is_long ? config_.margin_long : config_.margin_short;
22344 if (!finite_positive(margin) || !finite_positive(staged_.syminfo.pointvalue))
22345 continue;
22346 const double notional_per_price = entry.sizing.frozen_units
22347 * staged_.syminfo.pointvalue * entry.sizing.fx;
22348 if (!finite_positive(notional_per_price)) continue;
22349
22350 // ab9714be pine_fills.cpp:5010-5164: a rule-5 signal-price whole
22351 // rejection leaves the held position's bracket alone. Only a call
22352 // which passed that placement boundary and was then refused by the
22353 // adverse opening gap owns the declined-reversal bracket suspension.
22354 const bool money_scope = staged_.quantity_grid
22355 && *staged_.quantity_grid > 0.0 && *staged_.quantity_grid < 1.0
22356 && staged_.syminfo.pointvalue == 1.0 && entry.sizing.fx == 1.0
22357 && staged_.account_fx_effective_from_ms.empty()
22358 && config_.commission_value == 0.0 && config_.slippage == 0
22359 && !config_.process_orders_on_close && !config_.calc_on_order_fills
22360 ;
22361 double affordable_price = kNaN;
22362 if (money_scope) {
22363 const double rounded_cost = source_money_round(
22364 notional_per_price * entry.sizing.price);
22365 affordable_price = source_money_round(
22366 source_money_round(entry.sizing.equity) / notional_per_price);
22367 if (entry.sizing.equity + 1e-9 < rounded_cost
22368 || (std::isfinite(affordable_price)
22369 && affordable_price < entry.sizing.price)) {
22370 continue;
22371 }
22372 }
22373 opposite_market = true;
22374 const double fill = source_bar_fill_tick(bar.open, staged_.syminfo.mintick)
22375 + (entry.is_long ? 1.0 : -1.0)
22376 * config_.slippage * staged_.syminfo.mintick;
22377 const double required = notional_per_price * fill * margin / 100.0;
22378 const double guard = std::max(1e-9, std::abs(entry.sizing.equity) * 1e-12);
22379 const bool price_band_admitted = money_scope
22380 && std::isfinite(affordable_price) && affordable_price >= fill;
22381 if (std::isfinite(required)
22382 && required > entry.sizing.equity + guard
22383 && !price_band_admitted) {
22384 gap_decline = true;
22385 break;
22386 }
22387 }
22388 if (!opposite_market) return;
22389
22390 const double held_margin = physical.signed_units > 0.0
22391 ? config_.margin_long : config_.margin_short;
22392 const double open_fill = source_bar_fill_tick(bar.open, staged_.syminfo.mintick);
22393 const double open_fx = active_staged_fx(bar.timestamp);
22394 const double held_required = std::abs(physical.signed_units) * open_fill
22395 * staged_.syminfo.pointvalue * open_fx * held_margin / 100.0;
22396 const double held_equity = require_host().native_marked_equity(open_fill);
22397 const bool opening_margin_slice = source_margin_call_enabled_
22398 && finite_positive(held_margin) && std::isfinite(held_required)
22399 && std::isfinite(held_equity) && held_required > held_equity;
22400
22401 struct RetiredLeg {
22402 native_order::RequestHandle handle;
22403 std::optional<PendingBracketLeg> delayed_leg;
22404 std::optional<PlacementSnapshot> margin_revival;
22405 bool release_at_open = false;
22406 };
22407 std::vector<RetiredLeg> retired_legs;
22408 for (const auto& handle : live_handles_) {
22409 const auto found = placement_.find(handle.incarnation);
22410 if (found == placement_.end()) continue;
22411 const auto& leg = found->second;
22412 const bool stop_or_limit = leg.family == PineOrderFamily::ExitStop
22413 || leg.family == PineOrderFamily::ExitLimit;
22414 double trail_activation = leg.exit_levels.trail_price;
22415 if (!finite_positive(trail_activation)
22416 && finite_positive(leg.exit_levels.trail_points)
22417 && finite_positive(staged_.syminfo.mintick)) {
22418 trail_activation = require_host().position_avg_price()
22419 + (physical.signed_units > 0.0 ? 1.0 : -1.0)
22420 * leg.exit_levels.trail_points * staged_.syminfo.mintick;
22421 trail_activation = directional_tick(
22422 trail_activation, staged_.syminfo.mintick,
22423 physical.signed_units > 0.0);
22424 }
22425 const bool omitted_offset_trail = leg.family == PineOrderFamily::ExitTrail
22426 && !std::isfinite(leg.exit_levels.trail_offset)
22427 && finite_positive(trail_activation);
22428 const bool exit_is_buy = physical.signed_units < 0.0;
22429 const double priced_level = leg.family == PineOrderFamily::ExitStop
22430 ? leg.exit_levels.stop : leg.exit_levels.limit;
22431 const bool gapped_priced_leg = stop_or_limit && finite_positive(priced_level)
22432 && (physical.signed_units > 0.0
22433 ? (leg.family == PineOrderFamily::ExitStop
22434 ? bar.open <= priced_level : bar.open >= priced_level)
22435 : (leg.family == PineOrderFamily::ExitStop
22436 ? bar.open >= priced_level : bar.open <= priced_level));
22437 const bool reorder_priced_leg = gapped_priced_leg
22438 && (!gap_decline || opening_margin_slice);
22439 // A declined reversal makes a standing non-gapped bracket dormant; it
22440 // does not delete it. Only a leg already marketable at this opening
22441 // needs the L5b reorder/retirement path. The broader condition erased
22442 // REVIVE-B's later margin restoration before the margin event existed.
22443 const bool retire_priced_leg = gapped_priced_leg
22444 && (gap_decline || reorder_priced_leg);
22445 const bool retire_trail = gap_decline && omitted_offset_trail;
22446 if ((!retire_priced_leg && !retire_trail)
22447 || (!leg.from_entry.empty()
22448 && !(cohort_exposure_for(leg.from_entry) > 0.0))) {
22449 continue;
22450 }
22451 std::optional<PendingBracketLeg> delayed;
22452 std::optional<PlacementSnapshot> margin_revival;
22453 bool release_at_open = false;
22454 if (reorder_priced_leg) {
22455 native_order::Request request;
22456 request.intent = native_order::HostSized{
22457 native_order::HostSizedKind::Close, std::nullopt};
22458 request.label = leg.source_id;
22459 request.comment = leg.comment;
22460 // Defer the gapped bracket until the first later waypoint so the
22461 // already-accepted opposite MARKET is adjudicated at O first.
22462 // The source report still settles at the saved gap-open price.
22463 const double deferred_level = physical.signed_units > 0.0
22464 ? bar.low : bar.high;
22465 request.trigger = native_order::Stop{source_trigger_threshold(
22466 deferred_level, staged_.syminfo.mintick, exit_is_buy, false)};
22467 request.owner = owner_for_close(leg.from_entry, true);
22468 const std::string group_name = leg.oca_name.empty()
22469 ? leg.source_id + "\x1f" + leg.from_entry : leg.oca_name;
22470 request.group = group_for(group_name, 1);
22471 const std::string replacement_key = leg.source_id + "\x1f"
22472 + leg.from_entry + std::to_string(static_cast<int>(leg.family));
22473 PlacementSnapshot reordered = leg;
22474 reordered.forced_execution_price = open_fill;
22475 delayed.emplace(PendingBracketLeg{
22476 std::move(request), std::move(reordered), replacement_key,
22477 key_for(leg.source_id, leg.from_entry)});
22478 release_at_open = true;
22479 } else if (retire_trail) {
22480 const bool long_position = physical.signed_units > 0.0;
22481 const bool activated_at_open = long_position
22482 ? bar.open >= trail_activation
22483 : bar.open <= trail_activation;
22484 if (!activated_at_open) {
22485 native_order::Request request;
22486 request.intent = native_order::HostSized{
22487 native_order::HostSizedKind::Close, std::nullopt};
22488 request.label = leg.source_id;
22489 request.comment = leg.comment;
22490 request.trigger = native_order::Limit{
22491 directional_tick(trail_activation,
22492 staged_.syminfo.mintick,
22493 exit_is_buy)};
22494 request.owner = owner_for_close(leg.from_entry, true);
22495 const std::string group_name = leg.oca_name.empty()
22496 ? leg.source_id + "\x1f" + leg.from_entry : leg.oca_name;
22497 request.group = group_for(group_name, 1);
22498 const std::string replacement_key = leg.source_id + "\x1f"
22499 + leg.from_entry
22500 + std::to_string(static_cast<int>(PineOrderFamily::ExitTrail));
22501 delayed.emplace(PendingBracketLeg{
22502 std::move(request), leg, replacement_key,
22503 key_for(leg.source_id, leg.from_entry)});
22504 }
22505 }
22506 if (gap_decline && leg.family == PineOrderFamily::ExitStop
22507 && !reorder_priced_leg) {
22508 margin_revival = leg;
22509 }
22510 retired_legs.push_back({handle, std::move(delayed),
22511 std::move(margin_revival), release_at_open});
22512 }
22513 for (auto& retired : retired_legs) {
22514 const auto result = require_host().cancel(retired.handle);
22515 if (result.status != native_order::CancelStatus::Cancelled) continue;
22516 if (const auto found = placement_.find(retired.handle.incarnation);
22517 found != placement_.end()) {
22518 found->second.cancellation = {
22520 retired.handle.incarnation,
22521 static_cast<std::int64_t>(found->second.source_sequence),
22522 retired.handle.incarnation, found->second.placement_cycle,
22523 found->second.legs.revision(), found->second.requested_qty, kNaN};
22524 }
22525 retire(retired.handle);
22526 if (retired.margin_revival) {
22527 pending_margin_revivals_.push_back({
22528 std::move(*retired.margin_revival),
22529 context.coordinate.interval_index});
22530 }
22531 if (retired.delayed_leg) {
22532 if (retired.release_at_open) {
22533 auto reordered = std::move(*retired.delayed_leg);
22534 const auto accepted = submit_or_replace(
22535 std::move(reordered.request), std::move(reordered.snapshot),
22536 false, reordered.replacement_key);
22537 if (accepted)
22538 bracket_family(reordered.family_key).push_back(*accepted);
22539 } else if (defer_trails) {
22540 auto delayed = std::move(*retired.delayed_leg);
22541 delayed_market_orders_.push_back({
22542 std::move(delayed.request), std::move(delayed.snapshot),
22543 std::move(delayed.replacement_key), broker_open_epoch_ + 1U});
22544 } else {
22545 pending_write(pending_bracket_legs_).push_back(std::move(*retired.delayed_leg));
22546 }
22547 }
22548 }
22549}
22550
22551void PineExecutionAdapter::reaccept_gapped_bracket_behind_same_id_add(
22552 const Bar& bar, const NativeDecisionContext& context) {
22553 // ab9714be pine_fills.cpp:3799-3845 (samebar_add_exit_first): a
22554 // from_entry PRICED bracket leg gapped through at the open and its own
22555 // same-id pure MARKET pyramid add both fill at the opening tick, where
22556 // the owner's priority is buy-market(1) > sell-market(2) > gapped
22557 // limit(3); a long's gapped stop is a sell (2), a short's a buy (1).
22558 // Unless the leg strictly precedes, the add is judged first against the
22559 // still-open position (pyramiding cap, pine_orders.cpp:738, and margin)
22560 // and the leg then closes the id, covering the add
22561 // (pine_fills.cpp:7026-7033). The native matcher breaks a same-point tie
22562 // by incarnation, so the older leg would flatten the book and the add
22563 // would open from flat. Re-accept the unchanged leg at this opening: its
22564 // fresh incarnation orders it behind the add at the same open point.
22565 if (config_.process_orders_on_close || !modeled_input() || coof_recalc_active_
22566 || context.sub_index != 0) {
22567 return;
22568 }
22569 const auto physical = detail::run_position(require_host());
22570 if (physical.signed_units == 0.0) return;
22571 const bool long_position = physical.signed_units > 0.0;
22572 // A from_entry "" exit that sizes on the position it fills against
22573 // (fill_time_global_exit) covers an add of ANY id the same way: TradingView
22574 // judges the market add first at the shared open and then closes both lots
22575 // with the exit, whether the script placed the exit before or after the add
22576 // (lab tv w4-f19a-plain cells A, B and G; lane W4-ENG-POOC-SAMEPASS).
22577 const auto pure_market_add = [&](const PlacementSnapshot& add,
22578 const SourceId& from_entry) {
22579 return add.opening
22580 && (add.family == PineOrderFamily::Entry
22581 || add.family == PineOrderFamily::Order)
22582 && (from_entry.empty() || add.source_id == from_entry)
22583 && add.is_long == long_position
22584 && !finite_positive(add.exit_levels.limit)
22585 && !finite_positive(add.exit_levels.stop)
22586 && !finite_positive(add.exit_levels.trail_points)
22587 && !finite_positive(add.exit_levels.trail_price)
22588 && !finite_positive(add.exit_levels.trail_offset)
22589 && !add.birth.from_fill() && !add.birth.at_terminal_fill();
22590 };
22591 std::vector<native_order::RequestHandle> gapped;
22592 for (const auto& handle : live_handles_) {
22593 const auto found = placement_.find(handle.incarnation);
22594 if (found == placement_.end()) continue;
22595 const auto& leg = found->second;
22596 const bool global_leg = leg.from_entry.empty() && fill_time_global_exit(leg);
22597 // The same-id order is the owner's without calc_on_order_fills
22598 // only; TradingView's from_entry "" order holds with it too
22599 // (w4-f19a-plain-coof cell B).
22600 if (config_.calc_on_order_fills && !global_leg) continue;
22601 if ((leg.family != PineOrderFamily::ExitStop
22602 && leg.family != PineOrderFamily::ExitLimit)
22603 || (leg.from_entry.empty() && !global_leg) || !leg.deferred_cohort
22604 || std::isfinite(leg.requested_qty)
22605 || (!global_leg && std::isfinite(leg.qty_percent)
22606 && leg.qty_percent < 100.0 - 1e-9)
22607 || finite_positive(leg.exit_levels.trail_points)
22608 || finite_positive(leg.exit_levels.trail_price)
22609 || finite_positive(leg.exit_levels.trail_offset)
22610 || (!global_leg && !(cohort_exposure_for(leg.from_entry) > 0.0))) {
22611 continue;
22612 }
22613 int exit_priority = 0;
22614 if (leg.family == PineOrderFamily::ExitStop && finite_positive(leg.exit_levels.stop)
22615 && (long_position ? bar.open <= leg.exit_levels.stop
22616 : bar.open >= leg.exit_levels.stop)) {
22617 exit_priority = long_position ? 2 : 1;
22618 } else if (leg.family == PineOrderFamily::ExitLimit
22619 && finite_positive(leg.exit_levels.limit)
22620 && (long_position ? bar.open >= leg.exit_levels.limit
22621 : bar.open <= leg.exit_levels.limit)) {
22622 exit_priority = 3;
22623 }
22624 const int add_priority = long_position ? 1 : 2;
22625 if (exit_priority == 0 || exit_priority < add_priority) continue;
22626 const bool add_waits = std::any_of(live_handles_.begin(), live_handles_.end(),
22627 [&](const native_order::RequestHandle& live) {
22628 if (live.incarnation < handle.incarnation) return false;
22629 const auto add = placement_.find(live.incarnation);
22630 return add != placement_.end()
22631 && pure_market_add(add->second, leg.from_entry);
22632 });
22633 if (add_waits) gapped.push_back(handle);
22634 }
22635 const bool exit_is_buy = !long_position;
22636 const double tick = staged_.syminfo.mintick;
22637 for (const auto& handle : gapped) {
22638 const auto found = placement_.find(handle.incarnation);
22639 if (found == placement_.end()) continue;
22640 PlacementSnapshot leg = found->second;
22641 const auto result = require_host().cancel(handle);
22642 if (result.status != native_order::CancelStatus::Cancelled) continue;
22643 found->second.cancellation = {
22644 PineCancellationCause::Dependency, 1, 0, handle.incarnation,
22645 static_cast<std::int64_t>(leg.source_sequence), handle.incarnation,
22646 leg.placement_cycle, leg.legs.revision(), leg.requested_qty, kNaN};
22647 retire(handle);
22648 native_order::Request request;
22649 request.intent = native_order::HostSized{
22650 native_order::HostSizedKind::Close, std::nullopt};
22651 request.label = leg.source_id;
22652 request.comment = leg.comment;
22653 if (leg.family == PineOrderFamily::ExitStop) {
22654 request.trigger = native_order::Stop{source_trigger_threshold(
22655 leg.exit_levels.stop, tick, exit_is_buy, false)};
22656 } else {
22657 const double snapped = nearest_tick(leg.exit_levels.limit, tick);
22658 request.trigger = native_order::Limit{
22659 !finite_positive(tick) || snapped == leg.exit_levels.limit
22660 ? leg.exit_levels.limit
22661 : source_trigger_threshold(leg.exit_levels.limit, tick,
22662 exit_is_buy, true)};
22663 // A limit accepted at this opening would book its own level; the
22664 // owner's gapped limit fills limit-or-better at the open print.
22665 leg.forced_execution_price = source_bar_fill_tick(bar.open, tick);
22666 }
22667 request.owner = owner_for_close(leg.from_entry, true);
22668 const std::string group_name = leg.oca_name.empty()
22669 ? leg.source_id + "\x1f" + leg.from_entry : leg.oca_name;
22670 request.group = group_for(group_name, 1, static_cast<std::int64_t>(leg.family));
22671 leg.cancellation = {};
22672 const std::uint64_t family_key = key_for(leg.source_id, leg.from_entry);
22673 const SourceId replacement_key = leg.source_id + "\x1f" + leg.from_entry
22674 + std::to_string(static_cast<int>(leg.family));
22675 const auto accepted = submit_or_replace(
22676 std::move(request), std::move(leg), false, replacement_key);
22677 if (accepted) bracket_family(family_key).push_back(*accepted);
22678 }
22679}
22680
22681void PineExecutionAdapter::order_open_marketable_limit_entries(
22682 const Bar& bar, const NativeDecisionContext& context) {
22683 // TradingView fills the orders executable at a bar's opening price in
22684 // this order: every MARKET order first, entries and exits alike, then the
22685 // buy LIMIT entries the open has already reached, lowest limit first,
22686 // whether a limit was placed at the previous close or has rested since an
22687 // earlier bar; neither the order they were placed in nor their ids decide
22688 // (R-A, tests/fixtures/open_fill_order). The native matcher breaks a
22689 // same-point tie by queue order, so re-pricing each such entry in place
22690 // (ReplaceOptions::keep_handle), lowest limit first, takes them behind
22691 // every other request in that order. Each still books the opening print,
22692 // as it would have at its own place in the queue.
22693 if (config_.process_orders_on_close || config_.calc_on_order_fills
22694 || !modeled_input() || coof_recalc_active_ || context.sub_index != 0) {
22695 return;
22696 }
22697 const double open_fill = source_bar_fill_tick(bar.open, staged_.syminfo.mintick);
22698 const auto marketable = [&](const PlacementSnapshot& row) {
22699 return row.family == PineOrderFamily::Entry && row.opening && row.is_long
22700 && finite_positive(row.exit_levels.limit) && !finite_positive(row.exit_levels.stop)
22701 && !finite_positive(row.exit_levels.trail_points)
22702 && !finite_positive(row.exit_levels.trail_price)
22703 && !finite_positive(row.exit_levels.trail_offset)
22704 && open_fill <= row.exit_levels.limit;
22705 };
22706 if (!has_live_family(PineOrderFamily::Entry) || !any_live_row(live_handles_, placement_, marketable)) return;
22707 // A flat book holding MARKET entries of both sides is the opposite-pair
22708 // family, whose own route decides its open: TradingView fills that pair
22709 // before the limit too, and also books the pair's later call as one
22710 // transaction admitted at its gross (tape r1c-ra-prior-limit cells M and
22711 // N), which this route does not model.
22712 const auto market_entry = [](const PlacementSnapshot& row, bool is_long) {
22713 return row.family == PineOrderFamily::Entry && row.opening && row.is_long == is_long
22714 && !finite_positive(row.exit_levels.limit) && !finite_positive(row.exit_levels.stop);
22715 };
22716 if (detail::run_position(require_host()).signed_units == 0.0
22717 && any_live_row(live_handles_, placement_,
22718 [&](const PlacementSnapshot& row) { return market_entry(row, true); })
22719 && any_live_row(live_handles_, placement_,
22720 [&](const PlacementSnapshot& row) { return market_entry(row, false); })) {
22721 return;
22722 }
22723 auto& host = require_host();
22724 struct Marketable {
22725 native_order::RequestHandle handle;
22726 native_order::Request request;
22727 double level = 0.0;
22728 std::uint64_t priority = 0;
22729 };
22730 std::vector<Marketable> limits;
22731 std::uint64_t last_market = 0;
22732 for (const auto& working : host.native_working_requests()) {
22733 const auto& definition = *working.definition;
22734 const std::uint64_t priority = definition.priority != 0
22735 ? definition.priority : definition.handle.incarnation;
22736 if (std::holds_alternative<native_order::Market>(definition.request.trigger)) {
22737 last_market = std::max(last_market, priority);
22738 continue;
22739 }
22740 const auto* limit = std::get_if<native_order::Limit>(&definition.request.trigger);
22741 if (!limit || open_fill > limit->price) continue;
22742 const auto found = placement_.find(definition.handle.incarnation);
22743 if (found == placement_.end() || !marketable(found->second)) continue;
22744 limits.push_back({definition.handle, definition.request, limit->price, priority});
22745 }
22746 std::sort(limits.begin(), limits.end(), [](const auto& left, const auto& right) {
22747 return left.priority < right.priority;
22748 });
22749 bool in_order = true;
22750 for (std::size_t i = 0; i < limits.size(); ++i) {
22751 if (limits[i].priority < last_market
22752 || (i > 0 && limits[i].level < limits[i - 1].level)) {
22753 in_order = false;
22754 }
22755 }
22756 if (in_order) return;
22757 std::stable_sort(limits.begin(), limits.end(), [](const auto& left, const auto& right) {
22758 return left.level < right.level;
22759 });
22760 // A priced entry keeps a host-resolved quantity (entry(): only an
22761 // unpriced default quantity is lowered onto the core's Sized intent), so
22762 // the same request re-prices with nothing frozen to re-measure.
22763 native_order::ReplaceOptions reprice;
22764 reprice.keep_handle = true;
22765 for (const auto& row : limits) {
22766 const auto result = host.replace(row.handle, row.request, reprice);
22767 if (result.status != native_order::ReplaceStatus::Replaced) continue;
22768 if (const auto found = placement_.find(row.handle.incarnation);
22769 found != placement_.end()) {
22770 found->second.forced_execution_price = open_fill;
22771 }
22772 }
22773}
22774
22775void PineExecutionAdapter::apply_terminal_explicit_market_policy(
22776 const NativeDecisionContext& context) {
22777 apply_terminal_explicit_market_policy(context, true);
22778}
22779
22780// With `execute_now` false the pass only submits: the reconciled entries stay
22781// working for the matcher, which fills them where a source replay of the last
22782// pre-T bar opens. The cancellations and re-submissions are the same either way.
22783void PineExecutionAdapter::apply_terminal_explicit_market_policy(
22784 const NativeDecisionContext& context, bool execute_now) {
22785 if (!config_.process_orders_on_close
22786 || config_.pyramiding != 0 || !modeled_input()) {
22787 return;
22788 }
22789 struct Candidate {
22790 native_order::RequestHandle handle;
22791 PlacementSnapshot snapshot;
22792 std::uint64_t priority = 0;
22793 };
22794 std::vector<Candidate> candidates;
22795 for (const auto& handle : live_handles_) {
22796 const auto found = placement_.find(handle.incarnation);
22797 if (found == placement_.end()) continue;
22798 const auto& row = found->second;
22799 if (!row.opening || row.family != PineOrderFamily::Entry
22800 || row.projection_created_bar != projection_bar_index(context)
22801 || !finite_positive(row.requested_qty)
22802 || finite_positive(row.exit_levels.limit)
22803 || finite_positive(row.exit_levels.stop)
22804 || !row.oca_name.empty() || row.oca_type != 0
22805 || row.birth.from_fill() || row.birth.at_terminal_fill()
22806 || compat::pine::historical_cascade_reach(row.birth_reach)) {
22807 continue;
22808 }
22809 // The source sequence of the oldest row the predecessor chain
22810 // reaches, fixed when the row was remembered (its older rows may
22811 // have been erased since).
22812#if PINEFORGE_PLACEMENT_AUDIT
22813 {
22814 // The walk the field replaces, over the retained and the erased
22815 // rows alike: a difference is an audit hit on this line.
22816 const PlacementSnapshot* origin = &row;
22817 std::unordered_set<std::uint64_t> seen;
22818 const auto* stones = detail::placement_audit_tombstones(&placement_);
22819 while (origin->projection_predecessor != 0
22820 && seen.insert(origin->projection_predecessor).second) {
22821 const auto prior = placement_.find(origin->projection_predecessor);
22822 if (prior != placement_.end()) {
22823 origin = &prior->second;
22824 continue;
22825 }
22826 const PlacementSnapshot* stone = nullptr;
22827 if (stones) {
22828 for (const auto& entry : *stones)
22829 if (entry.first == origin->projection_predecessor) stone = entry.second;
22830 }
22831 if (!stone) break;
22832 origin = stone;
22833 }
22834 if (origin->source_sequence != row.chain_origin_sequence)
22835 detail::placement_audit_scan_hit(&placement_, handle.incarnation, __LINE__);
22836 }
22837#endif
22838 candidates.push_back({handle, row, row.chain_origin_sequence});
22839 }
22840 if (candidates.size() < 2) return;
22841 std::stable_sort(candidates.begin(), candidates.end(),
22842 [](const Candidate& left, const Candidate& right) {
22843 if (left.priority != right.priority) return left.priority < right.priority;
22844 return left.snapshot.source_sequence < right.snapshot.source_sequence;
22845 });
22846 std::vector<native_order::RequestHandle> terminal_review_handles;
22847 terminal_review_handles.reserve(candidates.size());
22848 for (const auto& candidate : candidates)
22849 terminal_review_handles.push_back(candidate.handle);
22850
22851 const auto& first = candidates[0].snapshot;
22852 const auto& second = candidates[1].snapshot;
22853 // The gross decline holds under process_orders_on_close alone and with
22854 // costs as well: a flat pair of opposite fixed-quantity calls whose gross
22855 // is over the equity keeps only its earlier call (lab tv tapes
22856 // tests/fixtures/pooc_pair_gross hel-h1-pooc-pair-gross-slip, with
22857 // slippage 1 and a 0.01 % commission, and hel-h2-pooc-pair-gross-zero;
22858 // lane TAIL-D).
22859 const bool clean_pair = candidates.size() == 2
22860 && config_.margin_long == 100.0 && config_.margin_short == 100.0
22861 && entry_attempt_bar_ == context.coordinate.interval_index
22862 && entry_attempts_on_bar_ == 2
22863 && first.projection_predecessor == 0 && second.projection_predecessor == 0
22864 && first.recreated_after_named_cancelled_entry_incarnation == 0
22865 && second.recreated_after_named_cancelled_entry_incarnation == 0
22866 && first.source_id != second.source_id && first.is_long != second.is_long
22867 && compat::pine::explicit_qualification(first.market_admission)
22868 && compat::pine::explicit_qualification(second.market_admission);
22869 if (clean_pair) {
22870 const double required = (first.requested_qty + second.requested_qty)
22871 * second.sizing.price * staged_.syminfo.pointvalue * second.sizing.fx;
22872 const double equity = std::min(first.sizing.equity, second.sizing.equity);
22873 const double guard = std::max(1e-9, std::abs(equity) * 1e-12);
22874 if (std::isfinite(required) && std::isfinite(equity)
22875 && required > equity + guard) {
22876 const auto result = require_host().cancel(candidates[1].handle);
22877 if (result.status == native_order::CancelStatus::Cancelled)
22878 retire(candidates[1].handle);
22879 candidates.resize(1);
22880 }
22881 }
22882 record_market_review(admission::Checkpoint::TerminalGross,
22883 projection_bar_index(context),
22884 terminal_review_handles);
22885
22886 // ab9714be pine_fills.cpp:3023-3270 and pine_orders.cpp:193-276:
22887 // outside the exact gross-decline book, explicit opposite entry calls
22888 // retain source order and each later opposite call is a full ReverseTo,
22889 // not a pair of net Transact deltas from the shared flat placement state.
22890 for (const auto& candidate : candidates) {
22891 const auto result = require_host().cancel(candidate.handle);
22892 if (result.status == native_order::CancelStatus::Cancelled)
22893 retire(candidate.handle);
22894 }
22895 // TradingView fills the buy of a flat sell-then-buy pair first at the
22896 // close (order_same_point_entries' order): the later buy is one
22897 // transaction of its own quantity plus the pending sell's, and the sell
22898 // then trades only its own (lab tv w6-f10d-pooc-pair, w6-f10e-pooc-coof-
22899 // pair MM-SF). A buy-first pair keeps the source order below.
22900 const bool buy_first = candidates.size() == 2 && !candidates[0].snapshot.is_long
22901 && candidates[1].snapshot.is_long
22902 && detail::run_position(require_host()).signed_units == 0.0;
22903 if (buy_first) std::swap(candidates[0], candidates[1]);
22904 int simulated_sign = 0;
22905 for (auto& candidate : candidates) {
22906 native_order::Request request;
22907 const int requested_sign = candidate.snapshot.is_long ? 1 : -1;
22908 const double signed_units = requested_sign * candidate.snapshot.requested_qty;
22909 if (buy_first) {
22910 const double pending_sell = candidate.snapshot.is_long
22911 ? candidates[1].snapshot.requested_qty : 0.0;
22912 request.intent = native_order::Transact{signed_units + pending_sell};
22913 candidate.snapshot.reverse_to = false;
22914 } else if (simulated_sign != 0 && simulated_sign != requested_sign) {
22915 request.intent = native_order::ReverseTo{signed_units};
22916 candidate.snapshot.reverse_to = true;
22917 } else {
22918 request.intent = native_order::Transact{signed_units};
22919 candidate.snapshot.reverse_to = false;
22920 }
22921 request.label = candidate.snapshot.source_id;
22922 request.comment = candidate.snapshot.comment;
22923 const SourceId replacement_key = candidate.snapshot.source_id;
22924 candidate.snapshot.market_admission = {};
22925 const auto accepted = submit_or_replace(
22926 std::move(request), std::move(candidate.snapshot), true,
22927 replacement_key);
22928 if (accepted) {
22929 simulated_sign = requested_sign;
22930 if (execute_now) {
22931 (void)require_host().execute_current(
22932 {*accepted, NativeCurrentPriceRule::NearestTick});
22933 }
22934 }
22935 }
22936}
22937
22938namespace {
22939// A priced request the Pine layer only ever places as a plain stop, which a
22940// COOF recalculation may rest from its leg's extreme as a trail pinned there
22941// (entry(), exit()): an entry stop, or an exit stop that names no trail.
22942bool rested_stop_family(const PlacementSnapshot& row) noexcept {
22943 if (!finite_positive(row.exit_levels.stop)) return false;
22944 if (row.family == PineOrderFamily::Entry) return true;
22945 return row.family == PineOrderFamily::ExitStop
22946 && std::isnan(row.exit_levels.trail_points)
22947 && std::isnan(row.exit_levels.trail_price)
22948 && std::isnan(row.exit_levels.trail_offset);
22949}
22950} // namespace
22951
22953 // The script bar this opening follows, before it is replaced below: a
22954 // short process_orders_on_close filled at its close is margined at that
22955 // close (the opening slice below).
22956 const Bar prior_policy_bar = policy_script_bar_;
22957 const bool prior_policy_bar_valid = policy_script_bar_valid_;
22958 // Terminal entry refusals have no Applied notification. Consume their
22959 // generic receipt before the next matching point so their deferred
22960 // per-origin bracket legs cannot close a different cohort member.
22962 erase_retired_rows(context);
22963 release_closed_cohort_origins();
22964 retire_origin_prefixes();
22965 restore_carried_trail_retunes();
22966 trail_state_at_open_.clear();
22967 unrepeated_margin_calls_.clear();
22968 // An opening checkpoint after an open's exits is due only there (on_applied).
22969 if (PF_RARE(open_exit_first_bar_ != std::numeric_limits<std::int64_t>::min())) {
22970 open_exit_first_bar_ = std::numeric_limits<std::int64_t>::min();
22971 open_exit_first_origin_ = 0;
22972 open_exit_first_id_.clear();
22973 open_exit_first_fill_ = 0.0;
22974 }
22975 bool rested_entry_stop = false;
22976 if (has_live_trail_policy()) for (const auto& handle : live_handles_) {
22977 const auto placement = placement_.find(handle.incarnation);
22978 if (placement == placement_.end()) continue;
22979 if (rested_stop_family(placement->second)) {
22980 rested_entry_stop = rested_entry_stop
22981 || (finite_positive(placement->second.forced_execution_price)
22982 && require_host().trail_state(handle).has_value());
22983 continue;
22984 }
22985 if (placement->second.family != PineOrderFamily::ExitTrail) continue;
22986 if (const auto state = require_host().trail_state(handle))
22987 trail_state_at_open_.emplace(handle.incarnation, *state);
22988 }
22989 // A stop entry() or exit() rested from its leg's extreme rode a trail
22990 // pinned at that extreme for the rest of its bar; from this opening on it
22991 // is the plain stop at its own level again, gap-filled at an open beyond
22992 // it.
22993 if (rested_entry_stop) {
22994 auto& host = require_host();
22996 keep.keep_handle = true;
22997 for (const auto& working : host.native_working_requests()) {
22998 const auto& definition = *working.definition;
22999 if (!std::holds_alternative<native_order::Trail>(definition.request.trigger))
23000 continue;
23001 const auto found = placement_.find(definition.handle.incarnation);
23002 if (found == placement_.end() || !rested_stop_family(found->second)) continue;
23003 native_order::Request plain = definition.request;
23004 plain.trigger = native_order::Stop{found->second.exit_levels.stop};
23005 const auto result = host.replace(definition.handle, plain, keep);
23006 if (result.status == native_order::ReplaceStatus::Replaced)
23007 found->second.forced_execution_price = kNaN;
23008 }
23009 }
23010 // The preceding source broker batch is complete at this next opening.
23011 // This is deliberately after any POOC after-calculation matching of the
23012 // prior script bar, so a same-batch cap transfer remains available to its
23013 // designated sibling.
23015 if (config_.process_orders_on_close && !config_.calc_on_order_fills
23016 && config_.pyramiding > 0 && margins_disabled()
23017 && detail::exit_binding_rule_switches().priced_add_at_cap_not_placed) {
23018 withdraw_unplaced_cap_adds(context);
23019 }
23020 if (context.coordinate.interval_index != entry_openings_interval_index_) {
23021 entry_openings_interval_index_ = context.coordinate.interval_index;
23022 entry_openings_this_interval_ = 0;
23023 }
23024 if (context.script_bar_open_ms != last_broker_open_ms_) {
23025 last_broker_open_ms_ = context.script_bar_open_ms;
23026 ++broker_open_epoch_;
23027 }
23028 // Quiet-bar gates (pine_quiet_bar.hpp): a policy hook below whose body
23029 // has nothing to act on at this opening is not called at all. Each
23030 // condition is that hook's own no-op precondition.
23031 using detail::QuietHook;
23032 using detail::skip_quiet;
23033 // ab9714be prearmed pending-entry legs become executable at the next
23034 // broker opening, before that bar's path is matched. Releasing them only
23035 // from the later source close callback misses the intended bar.
23036 // Quiet: no order is delayed.
23037 if (!skip_quiet(QuietHook::DelayedRelease, delayed_market_orders_.empty()))
23038 release_delayed_orders(/*explicit_brackets_only=*/true, bar.open);
23039 activate_short_seed_plan_at_open(context);
23040 // Quiet: OrderPriority::select ranks only a pair of live candidates, and
23041 // only for an attached retained-parent rule on a close-order run outside
23042 // fill recalculation; otherwise it answers nothing and the book keeps
23043 // its order.
23044 if (!skip_quiet(QuietHook::L4cPriority,
23045 !priority.attached() || !priority.retained_parent_first()
23046 || !config_.process_orders_on_close || config_.calc_on_order_fills
23047 || coof_recalc_active_ || live_handles_.size() < 2)) {
23048 update_l4c_priority();
23049 }
23050 // Quiet: every admission candidate is a live entry the previous source
23051 // bar placed; without one no pair or pyramiding rule applies, nothing is
23052 // cancelled and both reviews see no handle.
23053 const int source_bar = context.coordinate.interval_index - 1;
23054 if (!skip_quiet(QuietHook::OpenMarketAdmission, !has_live_family(PineOrderFamily::Entry) ||
23055 !any_live_row(live_handles_, placement_, [&](const PlacementSnapshot& row) {
23056 return row.family == PineOrderFamily::Entry
23057 && row.projection_created_bar == source_bar;
23058 }))) {
23059 apply_open_market_admission(context);
23060 }
23061 // Quiet: a sell is deferred only when both a live pure-stop buy entry and
23062 // a live pure-stop sell entry are marketable at the fill point (the close
23063 // under process_orders_on_close, else the open).
23064 const double sell_fill_point = config_.process_orders_on_close ? bar.close : bar.open;
23065 const auto marketable_stop_entry = [&](bool is_long) {
23066 return has_live_family(PineOrderFamily::Entry) && any_live_row(live_handles_, placement_, [&](const PlacementSnapshot& row) {
23067 return row.is_long == is_long
23068 && pure_stop_entry_marketable_at(row, sell_fill_point);
23069 });
23070 };
23071 if (!skip_quiet(QuietHook::OpenMarketableSells,
23072 config_.calc_on_order_fills || !modeled_input()
23073 || !marketable_stop_entry(true) || !marketable_stop_entry(false))) {
23074 defer_open_marketable_sells(bar);
23075 }
23076 // Quiet: a gapped leg is re-accepted only behind a live pure market add
23077 // of its own id (of any id for a from_entry "" exit), which is an
23078 // opening entry or order.
23079 if (!skip_quiet(QuietHook::GappedBracketReaccept,
23080 !any_live_row(live_handles_, placement_, [](const PlacementSnapshot& row) {
23081 return row.opening && (row.family == PineOrderFamily::Entry
23082 || row.family == PineOrderFamily::Order);
23083 }))) {
23084 reaccept_gapped_bracket_behind_same_id_add(bar, context);
23085 }
23086 order_open_marketable_limit_entries(bar, context);
23087 pending_write(source_shadow_pending_).clear();
23088 coof_script_bar_ = bar;
23089 coof_script_bar_valid_ = true;
23090 policy_script_bar_ = bar;
23091 policy_script_bar_valid_ = true;
23092 if (modeled_input() && !context.driver_statistics.intrabar_path_enabled
23093 && !trail_state_at_open_.empty()) {
23094 const double open_tick = source_bar_fill_tick(bar.open, staged_.syminfo.mintick);
23096 keep.keep_handle = true;
23097 for (const auto& working : require_host().native_working_requests()) {
23098 const auto& definition = *working.definition;
23099 const auto carried = trail_state_at_open_.find(definition.handle.incarnation);
23100 const auto found = placement_.find(definition.handle.incarnation);
23101 if (carried == trail_state_at_open_.end() || !carried->second.activated
23102 || found == placement_.end()
23103 || !std::holds_alternative<native_order::Trail>(definition.request.trigger)
23104 || !(found->second.exit_levels.trail_offset >= 1.0)) {
23105 continue;
23106 }
23107 const double held_units = detail::run_position(require_host()).signed_units;
23108 if (held_units == 0.0) continue;
23109 const bool is_buy = held_units < 0.0;
23110 const double level = carried->second.current_level;
23111 const bool raw_reached = is_buy ? bar.open >= level : bar.open <= level;
23112 const bool print_reached = is_buy ? open_tick >= level : open_tick <= level;
23113 if (raw_reached || !print_reached) continue;
23114 native_order::Request request = definition.request;
23115 request.trigger = native_order::Market{};
23116 const auto replaced = require_host().replace(definition.handle, request, keep);
23117 if (replaced.status != native_order::ReplaceStatus::Replaced) continue;
23118 found->second.forced_execution_price = open_tick
23119 + (is_buy ? 1.0 : -1.0) * config_.slippage * staged_.syminfo.mintick;
23120 (void)require_host().execute_current(
23121 {definition.handle, NativeCurrentPriceRule::NearestTick});
23122 }
23123 }
23124 retune_carried_trails_for_tick_reach(bar, context);
23125 // The C observer snapshots the ordinary flat two-stop arbitration at the
23126 // bar boundary, before either native request can fill or be declined.
23127 // COOF has its own callback scheduling and deliberately leaves this
23128 // ordinary-path projection untouched, matching the legacy contract.
23129 last_bar_dual_entry_path_ = 0;
23130 last_bar_dual_entry_script_open_ms_ = context.script_bar_open_ms;
23131 // Quiet: a path is recorded only when this bar touches two live flat
23132 // stop entries, each by its own side's test.
23133 const auto touched_stop_entry = [&](const PlacementSnapshot& row) {
23134 return row.family == PineOrderFamily::Entry
23135 && finite_positive(row.exit_levels.stop)
23136 && !std::isfinite(row.exit_levels.limit)
23137 && !finite_positive(row.exit_levels.trail_offset)
23138 && (row.is_long ? bar.high >= row.exit_levels.stop
23139 : bar.low <= row.exit_levels.stop);
23140 };
23141 std::size_t touched_stop_entries = 0;
23142 if (has_live_family(PineOrderFamily::Entry)) for (const auto& handle : live_handles_) {
23143 const auto found = placement_.find(handle.incarnation);
23144 if (found != placement_.end() && touched_stop_entry(found->second))
23145 ++touched_stop_entries;
23146 }
23147 if (!skip_quiet(QuietHook::DualEntryPath, touched_stop_entries < 2)
23148 && !config_.calc_on_order_fills && detail::run_position(require_host()).signed_units == 0.0) {
23149 std::vector<const PlacementSnapshot*> stops;
23150 for (const auto& handle : live_handles_) {
23151 const auto found = placement_.find(handle.incarnation);
23152 if (found == placement_.end()) continue;
23153 const auto& snapshot = found->second;
23154 if (snapshot.family != PineOrderFamily::Entry
23155 || !finite_positive(snapshot.exit_levels.stop)
23156 || std::isfinite(snapshot.exit_levels.limit)
23157 || finite_positive(snapshot.exit_levels.trail_offset)) {
23158 continue;
23159 }
23160 stops.push_back(&snapshot);
23161 }
23162 if (stops.size() == 2 && stops[0]->is_long != stops[1]->is_long) {
23163 const auto* long_stop = stops[0]->is_long ? stops[0] : stops[1];
23164 const auto* short_stop = stops[0]->is_long ? stops[1] : stops[0];
23165 const bool long_touched = bar.high >= long_stop->exit_levels.stop;
23166 const bool short_touched = bar.low <= short_stop->exit_levels.stop;
23167 if (long_touched && short_touched) {
23168 // The leg the run walks first, asked of the kernel: under Auto
23169 // a tie bar walks the low leg first and fills the short entry.
23170 last_bar_dual_entry_path_ = source_path_uses_high_first(bar) ? 1 : 2;
23171 }
23172 }
23173 }
23174 // Quiet: no fill recalculation left a request staged.
23175 if (!skip_quiet(QuietHook::CoofTail, pending_coof_requests_.empty()))
23176 flush_coof_tail(/*openings_only=*/false, /*include_next_open=*/true);
23177 // Quiet: brackets are suspended only for a live opening reversal entry.
23178 const auto opening_reversal_entry = [](const PlacementSnapshot& row) {
23179 return row.opening && row.family == PineOrderFamily::Entry && row.reverse_to;
23180 };
23181 if (!skip_quiet(QuietHook::CoofDeclinedReversal, !has_live_family(PineOrderFamily::Entry) ||
23182 !any_live_row(live_handles_, placement_, opening_reversal_entry))) {
23183 suspend_coof_declined_reversal_at_open(bar, context);
23184 }
23185 if (close_all_pending_script_bar_ != context.script_bar_open_ms)
23186 close_all_pending_script_bar_ = std::numeric_limits<std::int64_t>::min();
23187 pooc_open_script_bar_ = context.script_bar_open_ms;
23188 pooc_open_basis_ = std::abs(detail::run_position(require_host()).signed_units);
23189 day_ledger_.current_day = chart_day_key(context.sub_bar_open_ms);
23190 if (day_ledger_.intraday_loss_day != day_ledger_.current_day) {
23191 day_ledger_.intraday_loss_day = day_ledger_.current_day;
23192 day_ledger_.intraday_start_equity = require_host().native_marked_equity(bar.open);
23193 day_ledger_.intraday_start_script_bar = context.script_bar_open_ms;
23194 day_ledger_.intraday_realized = 0.0;
23195 }
23196 // Quiet: no filled-orders cap close is due.
23197 if (!skip_quiet(QuietHook::DueCapClose, !cap.due_cause().has_value()))
23198 execute_due_cap_close(context);
23199 apply_fx_open_margin_slice(bar, context);
23200 (void)submit_slipped_pooc_opening_money_call(bar, context);
23201 const bool close_call_queued = prior_policy_bar_valid
23202 && close_point_margin_call_at_open(bar, prior_policy_bar, context);
23203 // The previous close's after-script call is read only by the orders that
23204 // close placed, which fill at this open (apply_frozen_reversal_close).
23205 if (PF_RARE(close_call_after_script_bar_ != std::numeric_limits<std::int64_t>::min())
23206 && (!prior_policy_bar_valid || close_call_after_script_bar_ != prior_policy_bar.timestamp)) {
23207 close_call_after_script_bar_ = std::numeric_limits<std::int64_t>::min();
23208 close_call_after_script_book_ = 0.0;
23209 close_call_after_script_units_ = 0.0;
23210 }
23211 // A follow-up the previous close owes this open (walk_short_path_points),
23212 // booked ahead of the open's own check.
23213 if (PF_RARE(owed_follow_up_bar_ != std::numeric_limits<std::int64_t>::min())) {
23214 if (prior_policy_bar_valid && owed_follow_up_bar_ == prior_policy_bar.timestamp
23215 && owed_follow_up_point_ == 4 && short_path_points_scope(context)) {
23216 owed_follow_up_bar_ = context.script_bar_open_ms;
23217 owed_follow_up_point_ = 0;
23218 execute_owed_short_follow_up(0, bar.open, context);
23219 } else if (owed_follow_up_bar_ != context.script_bar_open_ms) {
23220 owed_follow_up_bar_ = std::numeric_limits<std::int64_t>::min();
23221 owed_follow_up_point_ = -1;
23222 owed_follow_up_units_ = 0.0;
23223 }
23224 }
23225 const auto opening_position = detail::run_position(require_host());
23226 const bool long_full_margin = opening_position.signed_units > 0.0
23227 && std::abs(config_.margin_long - 100.0) < 1e-12;
23228 // A full-margin long that a commissioned process_orders_on_close fill
23229 // opened at the previous bar's close is called at this open, not at that
23230 // fill (on_applied, called_at_next_open): the units the fill's checkpoint
23231 // would have taken, executed here (lab tv tapes tests/fixtures/margin_v6
23232 // w5-c2-pooc-rev-long and -rev-long-noslip; lane W5-ENG-MARGIN-V6). So is
23233 // a long two or more of that bar's flat entries opened there, sized at
23234 // that close as a short's is below (lab tv tapes
23235 // tests/fixtures/margin_residual w5b-sb-pooc-60x2 and -pooc-default-x3;
23236 // lane W5B-ENG-MARGIN-RESIDUAL).
23237 const bool pooc_opened_at_prior_close = config_.process_orders_on_close
23238 && !config_.calc_on_order_fills
23239 && !context.driver_statistics.intrabar_path_enabled && context.sub_index == 0
23240 && prior_policy_bar_valid && position_open_phase_ == NativePathPhase::Close
23241 && position_open_script_bar_ == prior_policy_bar.timestamp;
23242 // A book two or more adds of the previous bar grew at its close is called
23243 // at this open too, sized at that close, fees or none, long or short (lab
23244 // tv tapes tests/fixtures/add_sibling_margin tailb-adds-pooc-*; lane
23245 // TAIL-B): on_applied took no call at their fills.
23246 const bool pooc_adds_at_prior_close = config_.process_orders_on_close
23247 && !config_.calc_on_order_fills
23248 && !context.driver_statistics.intrabar_path_enabled && context.sub_index == 0
23249 && prior_policy_bar_valid && !pooc_opened_at_prior_close
23250 && finite_positive(prior_policy_bar.close)
23251 && add_sibling_lots(prior_policy_bar.timestamp) >= 2;
23252 if (long_full_margin && (pooc_opened_at_prior_close || pooc_adds_at_prior_close)) {
23253 const double opening_mark = nearest_tick(bar.open, staged_.syminfo.mintick);
23254 // In the opening-call schedule's shape a long's call is sized at the
23255 // source close too, and the open's own calls follow it at its print
23256 // (close_sized_open_units, run_close_sized_open_calls; switch
23257 // close_sized_long_call). With none sized there, the open's own check.
23258 const double close_sized = config_.commission_value != 0.0 && pooc_opened_at_prior_close
23259 && PF_RARE(detail::margin_opening_switches().close_sized_long_call)
23260 && !pooc_adds_at_prior_close && finite_positive(prior_policy_bar.close)
23261 && !source_margin_rounded_tie_veto() && close_sized_open_call_scope(false, true)
23262 ? close_sized_open_units(nearest_tick(prior_policy_bar.close, staged_.syminfo.mintick),
23263 source_margin_fill_price(opening_mark, false),
23264 context.sub_bar_open_ms)
23265 : 0.0;
23266 if (close_sized > 0.0) {
23267 (void)run_close_sized_open_calls(close_sized, opening_mark, context);
23268 } else if (config_.commission_value != 0.0 && pooc_opened_at_prior_close) {
23269 (void)submit_margin_call_slice(opening_mark, context);
23270 } else if ((pooc_adds_at_prior_close || flat_sibling_placement(2))
23271 && finite_positive(prior_policy_bar.close)) {
23272 const double at_close_units = source_margin_units(
23273 source_margin_money(prior_policy_bar.close, context.sub_bar_open_ms), false);
23274 const auto at_open = source_margin_money(opening_mark, context.sub_bar_open_ms);
23275 const double open_shortfall = at_open.valid && at_open.required > at_open.equity
23276 ? (at_open.required - at_open.equity) / at_open.unit_margin : 0.0;
23277 const bool at_close = at_close_units > 0.0 && at_close_units >= open_shortfall;
23278 const bool follow_up = whole_unit_follow_up_due(
23279 at_close ? at_close_units : source_margin_units(at_open, false), opening_mark);
23280 const double held_before = opening_position.signed_units;
23281 const auto lots_before = pooc_adds_at_prior_close
23282 ? require_host().native_open_lots(opening_mark) : std::vector<NativeOpenLot>{};
23283 const bool called = at_close
23284 ? submit_margin_call_units(opening_mark, context, at_close_units)
23285 : submit_margin_call_slice(opening_mark, context);
23286 if (called && follow_up) (void)submit_margin_call_units(opening_mark, context, 1.0);
23287 // The adds' book takes the broker's follow-up at this same open
23288 // (lagged_margin_follow_up_units).
23289 if (called && pooc_adds_at_prior_close) {
23290 const double taken = held_before
23291 - detail::run_position(require_host()).signed_units;
23292 const double more = lagged_margin_follow_up_units(
23293 lots_before, at_open, taken, source_margin_fill_price(opening_mark, false),
23294 context.sub_bar_open_ms);
23295 if (more > 0.0) (void)submit_margin_call_units(opening_mark, context, more);
23296 }
23297 }
23298 }
23299 // Margin rule 3: a commission-free full-margin long with slippage is
23300 // checked at every bar's opening mark, as a short is
23301 // (slipped_long_margin_units).
23302 if (PF_RARE(long_full_margin && slipped_long_margin_scope())) {
23303 const double opening_mark = nearest_tick(bar.open, staged_.syminfo.mintick);
23304 const double called = slipped_long_margin_units(opening_mark);
23305 if (called > 0.0) (void)submit_margin_call_units(opening_mark, context, called);
23306 }
23307 bool marketable_limit_at_open = false;
23308 if (long_full_margin) {
23309 for (const auto& handle : live_handles_) {
23310 const auto found = placement_.find(handle.incarnation);
23311 if (found == placement_.end()) continue;
23312 const auto& candidate = found->second;
23313 const bool exit = candidate.family == PineOrderFamily::ExitLimit
23314 || candidate.family == PineOrderFamily::ExitStop
23315 || candidate.family == PineOrderFamily::ExitTrail;
23316 if (exit && std::isfinite(candidate.exit_levels.limit)
23317 && bar.open >= candidate.exit_levels.limit) {
23318 marketable_limit_at_open = true;
23319 break;
23320 }
23321 }
23322 }
23323 if (long_full_margin && !marketable_limit_at_open
23324 && position_open_script_bar_ != context.script_bar_open_ms) {
23325 const Bar open_only{bar.open, bar.open, bar.open, bar.open, 0.0,
23326 bar.timestamp};
23327 (void)submit_tv_money_long_margin_call(open_only, context);
23328 if (last_margin_call_script_bar_ != context.script_bar_open_ms)
23329 (void)schedule_tv_money_long_margin_before_trail(bar, context);
23330 }
23331 // Quiet: a flat book takes no open or path slice -- its margin money is
23332 // invalid, it declines no reversal and schedules no path check -- so the
23333 // block below changes nothing.
23334 if (!long_full_margin && staged_.account_fx_effective_from_ms.empty()
23335 && !skip_quiet(QuietHook::OpenMarginCheckpoints, opening_position.signed_units == 0.0)) {
23336 const double held_at_open = std::abs(opening_position.signed_units);
23337 const bool held_long_at_open = opening_position.signed_units > 0.0;
23338 // The point order (switch point_fills_before_margin): at the open the
23339 // queued orders execute first, then the exits the open reaches, and
23340 // the margin check runs on what is left. A close the script placed
23341 // ahead of its opposite entry (close-first) is unconditional, and a
23342 // protective strategy.exit stop or limit the opening print reaches
23343 // fills there (tests/fixtures/admission_rules c1, c2, c4 and the
23344 // stop-priority tapes; c3b and the coupled close + reversal tapes),
23345 // a leg of an exit that also trails included (switch
23346 // point_order_trailing_exits; gapstop-percent-trail-points and
23347 // -trail-offset: c2 with trail_points, and with a trail_offset too,
23348 // book c2's rows).
23350 && pinned_lot_grid();
23351 // The opening print, which only protective_exit reads, under the
23352 // point order.
23353 const double open_print = point_order ? nearest_tick(bar.open, staged_.syminfo.mintick)
23354 : 0.0;
23355 bool opposite_entry_waits = false;
23356 bool whole_market_close_waits = false;
23357 // Read only by protective_exit, which the point order gates.
23358 const bool opposite_entry_pending = point_order && any_live_row(live_handles_, placement_,
23359 [&](const PlacementSnapshot& row) {
23360 return row.opening && (row.family == PineOrderFamily::Entry
23363 && row.is_long != held_long_at_open && !row.close_first_entry;
23364 });
23365 // The protective exits the opening print reaches: a whole one, and the
23366 // units the partial ones close.
23367 bool protective_whole_waits = false;
23368 double protective_partial_closing = 0.0;
23369 for (const auto& handle : live_handles_) {
23370 const auto found = placement_.find(handle.incarnation);
23371 if (found == placement_.end()) continue;
23372 const auto& pending = found->second;
23373 if (pending.opening
23374 && (pending.family == PineOrderFamily::Entry
23375 || pending.family == PineOrderFamily::Order)
23376 && pending.projection_created_bar < projection_bar_index(context)
23377 && pending.is_long != held_long_at_open
23378 && !(point_order && pending.close_first_entry)) {
23379 opposite_entry_waits = true;
23380 }
23381 const bool market_close = pending.family == PineOrderFamily::Close
23382 || pending.family == PineOrderFamily::CloseAll;
23383 // An opposite opening waiting at this open may be declined there,
23384 // which kills the position's brackets (dormant until a margin
23385 // call revives them: test_bracket_lifecycle_declined_reversal_l4c
23386 // CASCADE); with one waiting no exit is taken to fill first.
23387 const bool protective_exit = point_order && !opposite_entry_pending
23388 && (pending.family == PineOrderFamily::ExitStop
23389 || pending.family == PineOrderFamily::ExitLimit)
23390 && !pending.legs.dormant() && !pending.void_issue
23391 && pending.projection_created_bar < projection_bar_index(context)
23393 || (!finite_positive(pending.exit_levels.trail_offset)
23394 && !finite_positive(pending.exit_levels.trail_points)
23395 && !finite_positive(pending.exit_levels.trail_price)))
23396 && finite_positive(open_print)
23397 && ((pending.family == PineOrderFamily::ExitStop
23398 && finite_positive(pending.exit_levels.stop)
23399 && (held_long_at_open ? open_print <= pending.exit_levels.stop
23400 : open_print >= pending.exit_levels.stop))
23401 || (pending.family == PineOrderFamily::ExitLimit
23402 && finite_positive(pending.exit_levels.limit)
23403 && (held_long_at_open ? open_print >= pending.exit_levels.limit
23404 : open_print <= pending.exit_levels.limit)));
23405 // An exit whose entry holds none of the position at the open fills
23406 // nothing there, so the margin check keeps its place: a bracket
23407 // still armed for an entry of the other side that never filled.
23408 const double exit_owned = !protective_exit ? 0.0
23409 : from_entry_units(pending.from_entry).value_or(held_at_open);
23410 if (PF_RARE(protective_exit && exit_owned > internal::kQtyEpsilon)) {
23411 const double closing = std::isfinite(pending.projection_remaining_qty)
23412 ? pending.projection_remaining_qty
23413 : (std::isfinite(pending.requested_qty)
23414 ? std::abs(pending.requested_qty)
23415 : (!std::isfinite(pending.qty_percent)
23416 || pending.qty_percent >= 100.0 - 1e-9
23417 ? held_at_open : 0.0));
23418 if (exit_owned >= held_at_open - 1e-10 && closing >= held_at_open - 1e-10)
23419 protective_whole_waits = true;
23420 else
23421 protective_partial_closing += std::min(closing, exit_owned);
23422 continue;
23423 }
23424 if (!market_close
23425 || pending.projection_created_bar >= projection_bar_index(context)
23426 || finite_positive(pending.exit_levels.limit)
23427 || finite_positive(pending.exit_levels.stop)
23428 || finite_positive(pending.exit_levels.trail_offset)) {
23429 continue;
23430 }
23431 const double closing = std::isfinite(pending.projection_remaining_qty)
23432 ? pending.projection_remaining_qty
23433 : (std::isfinite(pending.requested_qty)
23434 ? std::abs(pending.requested_qty)
23435 : (std::isfinite(pending.qty_percent)
23436 && pending.qty_percent >= 100.0 - 1e-9
23437 ? held_at_open : 0.0));
23438 const double owned = pending.from_entry.empty()
23439 ? held_at_open : cohort_exposure_for(pending.from_entry);
23440 if (pending.family == PineOrderFamily::CloseAll
23441 || (owned >= held_at_open - 1e-10
23442 && closing >= held_at_open - 1e-10)) {
23443 whole_market_close_waits = true;
23444 }
23445 }
23446 // ab9714be pine_fills.cpp:2462-2523: an unconditional whole close
23447 // resting for this opening fills before the open margin checkpoint.
23448 // A close paired with an opposite entry is conditional on that entry's
23449 // admission and therefore does not suppress the slice.
23450 whole_market_close_waits = (whole_market_close_waits
23451 && !opposite_entry_waits) || close_call_queued;
23452 const double opening_mark = nearest_tick(bar.open, staged_.syminfo.mintick);
23453 // A short process_orders_on_close filled at the previous bar's close
23454 // is margined at that close: TradingView sizes the call there (the
23455 // slipped fill and the entry's commission leave it short of margin)
23456 // and executes it at this open, re-sized at the open only when it no
23457 // longer covers the open's own shortfall. Every first opening call of
23458 // the 258 such shorts in lab tv tape
23459 // the opening-margin probe (ETHUSDT.P 15,
23460 // commission 0.05 %, slippage 1) follows this; marking at the open
23461 // alone books 118 (lab tv tapes tests/fixtures/margin_v6
23462 // w5-m2-pooc-short-comm; lane W5-ENG-MARGIN-V6).
23463 double close_margined_units = 0.0;
23464 // The full-margin one-lot shape the opening-call schedule pins
23465 // (close_sized_open_call_scope): the call sized at the close stands
23466 // unless its gate or one-unit band, priced at this open's print,
23467 // vetoes it, and the open's own calls follow it at that print
23468 // (run_close_sized_open_calls). A vetoed one leaves the open's own
23469 // check.
23470 bool close_sized_calls = false;
23471 if (config_.process_orders_on_close && !config_.calc_on_order_fills
23472 && opening_position.signed_units < 0.0 && prior_policy_bar_valid
23473 && context.sub_index == 0 && !context.driver_statistics.intrabar_path_enabled
23474 && ((position_open_phase_ == NativePathPhase::Close
23475 && position_open_script_bar_ == prior_policy_bar.timestamp)
23476 || pooc_adds_at_prior_close)
23477 && finite_positive(prior_policy_bar.close)
23479 const auto& opening_rules = detail::margin_opening_switches();
23480 const bool schedule = !pooc_adds_at_prior_close
23481 && PF_RARE(close_sized_open_call_scope(true, true));
23482 const double at_close_units = schedule
23483 ? close_sized_open_units(nearest_tick(prior_policy_bar.close, staged_.syminfo.mintick),
23484 source_margin_fill_price(opening_mark, true),
23485 context.sub_bar_open_ms)
23487 source_margin_money(prior_policy_bar.close, context.sub_bar_open_ms), false);
23488 const auto at_open = source_margin_money(opening_mark, context.sub_bar_open_ms);
23489 const double open_shortfall = at_open.valid && at_open.required > at_open.equity
23490 ? (at_open.required - at_open.equity) / at_open.unit_margin : 0.0;
23491 if (at_close_units > 0.0
23492 && ((schedule && opening_rules.close_call_gate_at_print)
23493 || at_close_units >= open_shortfall)) {
23494 close_margined_units = at_close_units;
23495 close_sized_calls = schedule
23496 && (opening_rules.booked_open_recheck || opening_rules.open_print_follow_up);
23497 }
23498 }
23499 // The exits the open reaches fill before the open's own margin check,
23500 // with two exceptions that keep the earlier order. A call sized at the
23501 // previous close that the open does not re-size (close_margined_units:
23502 // it still covers the open's own shortfall) is a queued order: it
23503 // executes first and the exits after it (TradingView's tape of
23504 // a third-party strategy on OANDA:XAUUSD 15m; synthetic illustration:
23505 // a call of 1 unit, then the gapped stop's remaining 2 units;
23506 // tests/test_point_order_open_call.cpp). A call the open re-sizes is
23507 // the open's own check, after the exits; no tape decides that shape
23508 // yet. And a partial exit fills first only when what it leaves is
23509 // margined (stop_priority stop-09 and -10: no call at the gapped
23510 // open); when the remainder is still short the call at the open comes
23511 // first, as before, which no tape decides yet.
23512 bool open_exit_fills_first = false;
23513 if (!(close_margined_units > 0.0)) {
23514 if (protective_whole_waits && !opposite_entry_waits) whole_market_close_waits = true;
23515 if (PF_RARE(protective_partial_closing > internal::kQtyEpsilon
23516 && !whole_market_close_waits)) {
23517 const auto at_open = source_margin_money(opening_mark, context.sub_bar_open_ms);
23518 const double remainder = held_at_open - protective_partial_closing;
23519 open_exit_fills_first = !(at_open.valid && remainder > internal::kQtyEpsilon
23520 && remainder * at_open.unit_margin > at_open.equity);
23521 }
23522 }
23523 // So is a short two or more of that bar's flat entries opened at the
23524 // close: a call there can take a one-unit follow-up at this open
23525 // (whole_unit_follow_up_due).
23526 const bool opening_follow_up = !whole_market_close_waits && close_margined_units > 0.0
23527 && flat_sibling_placement(2)
23528 && whole_unit_follow_up_due(close_margined_units, opening_mark);
23529 const bool opening_margin_applied =
23530 !whole_market_close_waits && !open_exit_fills_first
23531 && (close_margined_units > 0.0
23532 ? (close_sized_calls
23533 ? run_close_sized_open_calls(close_margined_units, opening_mark, context)
23534 : submit_margin_call_units(opening_mark, context, close_margined_units))
23535 : open_call_owing_follow_up(opening_mark, context));
23536 if (opening_margin_applied && opening_follow_up) {
23537 (void)submit_margin_call_units(opening_mark, context, 1.0);
23538 } else if (opening_margin_applied && close_margined_units > 0.0 && !close_sized_calls
23539 && PF_RARE(detail::margin_schedule_switches().close_call_follow_up_at_open)) {
23540 // The call sized at the previous close and executed here is
23541 // followed at this same open, at its own fill, while the book it
23542 // left, re-marked at that fill, is still short
23543 // (margin_follow_up_units): TradingView books the follow-up at
23544 // the first slice's price (switch close_call_follow_up_at_open).
23545 // Synthetic check snapshots illustrate the margin_call_rules fixtures
23546 // (NYSE:F 15m, commission 0.05 %, slippage 2): an initial call
23547 // at a 25.02 open fills at 25.04; if still short at that fill,
23548 // its follow-up also fills at 25.04, before any high-point check.
23549 // Waiting for the high instead changes that check's book. One
23550 // follow-up restores the book at that fill -- four times its
23551 // lot-floored shortfall, or one unit under one lot of it -- so
23552 // none follows it.
23553 const double fill = source_margin_fill_price(opening_mark, true);
23554 if (margin_follow_up_scope()
23555 && detail::run_position(require_host()).signed_units != 0.0) {
23556 const double follow_up = margin_follow_up_units(
23557 close_margined_units, fill, context.sub_bar_open_ms);
23558 if (follow_up > 0.0)
23559 (void)submit_margin_call_units(opening_mark, context, follow_up, true, fill);
23560 }
23561 }
23562 // pine_fills.cpp:2525-2678 gives an opening slice priority over the
23563 // remaining path. The surviving book is then evaluated over the
23564 // suffix: a restored bracket at an earlier level wins naturally, while
23565 // an unprotected position can take a second slice at the adverse
23566 // extreme on the same bar.
23567 // Quiet: only a live opening reversal entry can be declined.
23568 const bool declined_reversal = !skip_quiet(QuietHook::DeclinedReversalAtOpen,
23569 !any_live_row(live_handles_, placement_, opening_reversal_entry))
23570 && declined_reversal_at_open(bar);
23571 // A vetoed deficit pending at the open is checked at the bar's first
23572 // path point before an exit touched on the way (pending_veto_point_first).
23573 if (!opening_margin_applied && !whole_market_close_waits && !open_exit_fills_first
23574 && !declined_reversal && PF_RARE(!held_long_at_open
23575 && detail::margin_schedule_switches().pending_veto_first)) {
23576 (void)pending_veto_point_first(bar, context, opening_mark);
23577 }
23578 bool margin_scheduled = false;
23579 if (!opening_margin_applied
23580 && (!whole_market_close_waits || declined_reversal)) {
23581 margin_scheduled = schedule_margin_call_path(bar, context);
23582 }
23583 if (declined_reversal && !opening_margin_applied) {
23584 defer_declined_reversal_exits_at_adverse(
23585 bar, context, margin_scheduled);
23586 }
23587 }
23588 // Quiet: without an intraday loss rule no loss is ever breached, so no
23589 // close is executed now or scheduled on the path.
23590 const bool no_intraday_loss_rule = !(risk_.max_intraday_loss > 0.0);
23591 if (!skip_quiet(QuietHook::IntradayLossClose, no_intraday_loss_rule))
23592 (void)submit_intraday_loss_close(bar.open, context, true);
23593 if (!skip_quiet(QuietHook::IntradayLossPath, no_intraday_loss_rule))
23594 schedule_intraday_loss_path(bar, context);
23595 // Quiet: margin calls off, a non-percent default quantity or no live
23596 // opening entry, which the slice refuses before it reads the book.
23597 if (!skip_quiet(QuietHook::PreopenMarginSlice,
23598 !source_margin_call_enabled_
23599 || config_.default_qty_type != static_cast<int>(QtyType::PERCENT_OF_EQUITY)
23600 || !any_live_row(live_handles_, placement_, [](const PlacementSnapshot& row) {
23601 return row.opening && row.family == PineOrderFamily::Entry;
23602 }))) {
23603 schedule_preopen_margin_slice(bar, context);
23604 }
23605 if (context.sub_index == 0)
23606 cap.ordinary_open(context.coordinate.interval_index);
23607}
23608
23610 const Bar& tick, const NativeTickContext& context) {
23611 // A realtime print is a current generic decision point. The source
23612 // policy owns the financial threshold; the native request core still
23613 // owns request acceptance, settlement, receipts and any later matching.
23614 (void)submit_margin_call_slice(tick.close, context.decision);
23615}
23616
23617void PineExecutionAdapter::rearm_throttled_reopens() {
23618 auto queued = std::move(throttled_reopen_rearm_);
23619 throttled_reopen_rearm_.clear();
23620 const auto physical = detail::run_position(require_host());
23621 for (auto& snapshot : queued) {
23622 native_order::Request request;
23623 if (snapshot.deferred_cohort || !std::isfinite(snapshot.requested_qty)) {
23624 request.intent = native_order::HostSized{
23625 native_order::HostSizedKind::Open,
23626 snapshot.is_long ? native_order::Side::Long : native_order::Side::Short};
23627 } else {
23628 const double units = std::abs(snapshot.requested_qty);
23629 request.intent = native_order::Transact{snapshot.is_long ? units : -units};
23630 }
23631 request.label = snapshot.source_id;
23632 request.comment = snapshot.comment;
23633 request.trigger = native_order::Stop{snapshot.exit_levels.stop};
23634 request.group = group_for(snapshot.oca_name, snapshot.oca_type);
23635 const bool same_dir = physical.signed_units != 0.0
23636 && ((physical.signed_units > 0.0) == snapshot.is_long);
23637 const bool already_touched = policy_script_bar_valid_
23638 && finite_positive(snapshot.exit_levels.stop)
23639 && (snapshot.is_long
23640 ? policy_script_bar_.high >= snapshot.exit_levels.stop
23641 : policy_script_bar_.low <= snapshot.exit_levels.stop);
23642 // The kernel already walked past the nearer stop. Keep the owner's
23643 // fill price (the stop level) instead of the current path quote.
23644 // ab9714be pine_fills.cpp:8008-8018: round_to_mintick_directional rounds touched stop execution price to tick
23645 snapshot.forced_execution_price = (same_dir && already_touched)
23646 ? (finite_positive(staged_.syminfo.mintick)
23647 ? directional_tick(snapshot.exit_levels.stop, staged_.syminfo.mintick, snapshot.is_long)
23648 : snapshot.exit_levels.stop)
23649 : kNaN;
23650 snapshot.projection_after_close = false;
23651 snapshot.cancellation = {};
23652 snapshot.market_admission = {};
23653 const SourceId key = snapshot.source_id;
23654 const auto accepted = submit_or_replace(
23655 std::move(request), std::move(snapshot), true, key);
23656 if (accepted && same_dir && already_touched) {
23657 (void)require_host().execute_current(
23658 {*accepted, NativeCurrentPriceRule::NearestTick});
23659 }
23660 }
23661}
23662
23663void PineExecutionAdapter::flush_pooc_marketable_limit_entry_fills(
23664 const Bar& bar, const NativeDecisionContext& context) {
23665 // ab9714be pine_fills.cpp:7604-7646 (classify_order_eligibility) +
23666 // pine_fills.cpp:8022-8043 (evaluate_fill_price): under
23667 // process_orders_on_close, a pure LIMIT entry (no stop, no trail) placed by
23668 // this bar's source calc is evaluated in the post-calculation fill pass
23669 // (pine_scheduler.cpp:259 step 4) against THIS bar's close. When the
23670 // close's tick is on the marketable side of the limit, or on it, it fills
23671 // there, limit-or-better, at bar_fill_price(bar.close) with no slippage;
23672 // otherwise it rests and gets the ordinary touch evaluation from the next
23673 // bar on, however far the bar's range reached. TradingView reads the
23674 // close's tick, not the raw close (a sell limit at 11.76 over a raw 11.755
23675 // close fills at that close; a buy limit at 11.65 over 11.655, whose tick
23676 // is 11.66, rests), with or without calc_on_order_fills (lab tv
23677 // pa3-f3-pooc-limit-{long,short}{,-coof}; the extreme-point fill correction). Scoped
23678 // to the non-stream route and a flat book.
23679 // A pure STOP entry the close already reached fills there too, with or
23680 // without calc_on_order_fills (the order-schedule correction): TradingView books it
23681 // at that bar's close, the close's tick slipped like any stop fill, on
23682 // every path -- the five such entries of each of H-MEASURE's
23683 // hm-{chart,mag}-diff-v1 / -v3 / -v5 / -v7 tapes, which the resting stop
23684 // filled a bar late at the next open. The stop is reached when the close's
23685 // tick is at or through it (a raw close half a tick short prints it). Only
23686 // a lone marketable entry: when a second entry of this bar is marketable at
23687 // the same close, which fills first is unmeasured, and the stops keep the
23688 // next open's both-marketable arbitration (lane L9b's P0-B shapes).
23689 // A pure stop entry placed while the book held the other side fills at
23690 // that close as well, sized as its row is: a plain reversal here, and one
23691 // a same-bar strategy.close or close_all of the held side precedes right
23692 // after that close fills (fill_pooc_close_entries from on_applied), as
23693 // TradingView books both (the recalculation fill-order correction: lab tv
23694 // pa2-i3-rev-s-{chart,mag} and pa2-i3-flip-{s,l}-{chart,mag}, corpus probe
23695 // 96, which filled it at the next open; pa2-i3-flip-unreached-* keep an
23696 // unreached one resting). Only without calc_on_order_fills, the route the
23697 // tapes cover.
23698 if (!config_.process_orders_on_close || !modeled_input() || coof_recalc_active_) return;
23699 if (fill_pooc_close_pair(bar.close, context)) return;
23700 fill_pooc_close_entries(bar.close, context, /*after_close=*/false);
23701}
23702
23703void PineExecutionAdapter::fill_pooc_close_entries(
23704 double raw_close, const NativeDecisionContext& context, bool after_close) {
23705 const double book_before = detail::run_position(require_host()).signed_units;
23706 if (!finite_positive(raw_close)) return;
23707 // A same-bar close of the held side fills at this close after this pass:
23708 // the reversing stops it precedes wait for its fill.
23709 const bool close_follows = !after_close && std::any_of(
23710 live_handles_.begin(), live_handles_.end(),
23711 [&](const native_order::RequestHandle& handle) {
23712 const auto found = placement_.find(handle.incarnation);
23713 return found != placement_.end()
23714 && (found->second.family == PineOrderFamily::Close
23715 || found->second.family == PineOrderFamily::CloseAll)
23716 && found->second.projection_created_bar == projection_bar_index(context);
23717 });
23718 const double tick = staged_.syminfo.mintick;
23719 const double close_tick = source_bar_fill_tick(raw_close, tick);
23720 std::vector<std::pair<native_order::RequestHandle, PlacementSnapshot>> marketable;
23721 for (const auto& handle : live_handles_) {
23722 const auto found = placement_.find(handle.incarnation);
23723 if (found == placement_.end()) continue;
23724 const auto& row = found->second;
23725 if (row.family != PineOrderFamily::Entry || !row.opening) continue;
23726 if (row.birth.from_fill()) continue;
23727 if (row.projection_created_bar != projection_bar_index(context)) continue;
23728 const bool pure_limit = finite_positive(row.exit_levels.limit)
23729 && !std::isfinite(row.exit_levels.stop);
23730 const bool pure_stop = finite_positive(row.exit_levels.stop)
23731 && !std::isfinite(row.exit_levels.limit);
23732 const bool stop_limit = finite_positive(row.exit_levels.stop)
23733 && finite_positive(row.exit_levels.limit);
23734 if (!pure_limit && !pure_stop && !stop_limit) continue;
23735 const bool placed_flat = row.projection_position_side
23736 == static_cast<std::int32_t>(PositionSide::FLAT);
23737 const bool placed_against = row.projection_position_side
23738 == static_cast<std::int32_t>(row.is_long ? PositionSide::SHORT
23740 const bool reversing_stop = placed_against && pure_stop
23741 && !config_.calc_on_order_fills && (after_close || !close_follows)
23742 && (book_before == 0.0 || (book_before > 0.0) != row.is_long);
23743 // So does a limit placed against the held side, reversing it at this
23744 // close (lab tv w5r-pooc-add-close cell J), and a limit or stop ADD
23745 // placed while its own side is held (cells A, B, C, E, H; the
23746 // unreached add D rests; the recalculation scheduling diag-w5r, the placement and recalculation correction).
23747 const bool reversing_limit = placed_against && pure_limit
23748 && !config_.calc_on_order_fills && (after_close || !close_follows)
23749 && (book_before == 0.0 || (book_before > 0.0) != row.is_long);
23750 const bool placed_with = row.projection_position_side
23751 == static_cast<std::int32_t>(row.is_long ? PositionSide::LONG
23753 const bool pyramid_add = placed_with && !after_close && book_before != 0.0
23754 && (book_before > 0.0) == row.is_long;
23755 if (placed_flat ? (after_close || book_before != 0.0)
23756 : !(reversing_stop || reversing_limit || pyramid_add)) {
23757 continue;
23758 }
23759 if (row.direction_gate || row.paired_flat_market_candidate) continue;
23760 if (row.terms_priced_reverse && !(reversing_stop || reversing_limit)) continue;
23761 const bool at_close = finite_positive(close_tick) && (pure_limit
23762 ? (row.is_long ? close_tick <= row.exit_levels.limit
23763 : close_tick >= row.exit_levels.limit)
23764 : (row.is_long ? close_tick >= row.exit_levels.stop
23765 : close_tick <= row.exit_levels.stop));
23766 if (stop_limit && !(row.is_long ? close_tick <= row.exit_levels.limit
23767 : close_tick >= row.exit_levels.limit)) {
23768 if (at_close) (void)require_host().activate_current_stop_limit(handle);
23769 continue;
23770 }
23771 if (at_close) marketable.emplace_back(handle, row);
23772 }
23773 if (marketable.size() > 1) {
23774 marketable.erase(std::remove_if(marketable.begin(), marketable.end(),
23775 [](const std::pair<native_order::RequestHandle, PlacementSnapshot>& candidate) {
23776 return finite_positive(candidate.second.exit_levels.stop);
23777 }), marketable.end());
23778 }
23779 for (auto& candidate : marketable) {
23780 // An earlier fill of this pass moves the book; the owner's later rows
23781 // are then ordinary pending orders again.
23782 if (detail::run_position(require_host()).signed_units != book_before) break;
23783 const auto& row = candidate.second;
23784 // A reversal's frozen transaction is resolved by the host, as at the
23785 // next open (resolve_execution_terms reads the copied row).
23786 const bool host_sized = row.deferred_cohort || row.terms_priced_reverse
23787 || row.qty_type == static_cast<int>(QtyType::CASH)
23788 || row.qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY);
23789 if (!host_sized && !finite_positive(row.requested_qty)) continue;
23790 native_order::Request request;
23791 request.intent = host_sized
23792 ? native_order::OrderIntent{native_order::HostSized{
23793 native_order::HostSizedKind::Open,
23794 row.is_long ? native_order::Side::Long : native_order::Side::Short}}
23796 row.is_long ? row.requested_qty : -row.requested_qty}};
23797 request.label = row.source_id;
23798 request.comment = row.comment;
23799 request.trigger = native_order::Market{};
23800 request.group = group_for(row.oca_name, row.oca_type);
23801 PlacementSnapshot immediate = row;
23802 immediate.cancellation = {};
23803 immediate.market_admission = {};
23804 // bar_fill_price(bar.close): the raw close, nearest-tick rounded
23805 // (pine_fills.cpp:8038-8041); a limit fill is never slipped, a stop
23806 // fill is, after the rounding.
23807 const bool stop_entry = finite_positive(row.exit_levels.stop)
23808 && !std::isfinite(row.exit_levels.limit);
23809 immediate.forced_execution_price = stop_entry && config_.slippage != 0
23810 ? nearest_tick(close_tick + (row.is_long ? 1.0 : -1.0)
23811 * config_.slippage * tick, tick)
23812 : close_tick;
23813 const auto accepted = submit_or_replace(
23814 std::move(request), std::move(immediate), true, row.source_id);
23815 if (accepted) {
23816 // The replacement is the row's own order executed at this close:
23817 // it keeps the side and carried units it was placed against,
23818 // which its reversal sizing reads, when a same-bar close has
23819 // flattened the book since.
23820 auto& placed = placement_.at(accepted->incarnation);
23821 placed.projection_position_side = row.projection_position_side; invalidate_retirement();
23822 placed.projection_tv_carry_qty = row.projection_tv_carry_qty;
23823 (void)require_host().execute_current(
23824 {*accepted, NativeCurrentPriceRule::NearestTick});
23825 }
23826 }
23827}
23828
23829void PineExecutionAdapter::flush_pooc_marketable_exit_fills(
23830 const Bar& bar, const NativeDecisionContext& context) {
23831 // Under process_orders_on_close the close pass decides the exits this
23832 // calculation placed, after the calculation's orders that fill at the
23833 // close whatever its price (fill_pooc_close_exits), with or without
23834 // calc_on_order_fills. With one of those still to fill, the exits wait for
23835 // it: on_applied runs the pass after that fill.
23836 if (!config_.process_orders_on_close || !modeled_input() || coof_recalc_active_) return;
23837 if (pooc_close_market_pending(context)) return;
23838 if (pooc_close_entry_immediate_pending(context, nullptr)) return;
23839 fill_pooc_close_exits(bar.close, context);
23840}
23841
23842// A priced entry this calculation placed that fill_pooc_close_entries
23843// executes at this close (its immediate: an opening Entry row born this bar,
23844// not from a fill, with the close forced as its price) and that has not
23845// filled yet. The close pass decides the calculation's exits after it, as
23846// TradingView does (lab tv w5r-pooc-add-close cells A, B, E, H: the add's
23847// exit at that same close; the recalculation scheduling diag-w5r, the placement and recalculation correction).
23848bool PineExecutionAdapter::pooc_close_entry_immediate_pending(
23849 const NativeDecisionContext& context,
23850 const native_order::RequestHandle* except) const {
23851 const int bar_index = projection_bar_index(context);
23852 for (const auto& handle : live_handles_) {
23853 if (except && handle == *except) continue;
23854 const auto found = placement_.find(handle.incarnation);
23855 if (found == placement_.end()) continue;
23856 const auto& row = found->second;
23857 if (row.family == PineOrderFamily::Entry && row.opening && !row.birth.from_fill()
23858 && row.projection_created_bar == bar_index
23859 && std::isfinite(row.forced_execution_price)
23860 && (std::isfinite(row.exit_levels.limit) || std::isfinite(row.exit_levels.stop))) {
23861 return true;
23862 }
23863 }
23864 return false;
23865}
23866
23867bool PineExecutionAdapter::pooc_close_market_pending(
23868 const NativeDecisionContext& context) const {
23869 // A strategy.close / close_all, or a market entry or order, that this
23870 // calculation placed and the close point has not filled yet.
23871 const int bar_index = projection_bar_index(context);
23872 return any_live_row(live_handles_, placement_, [&](const PlacementSnapshot& row) {
23873 if (row.projection_created_bar != bar_index || row.birth.from_fill()) return false;
23874 if (row.family == PineOrderFamily::Close || row.family == PineOrderFamily::CloseAll)
23875 return true;
23876 return (row.family == PineOrderFamily::Entry || row.family == PineOrderFamily::Order)
23877 && !std::isfinite(row.exit_levels.limit) && !std::isfinite(row.exit_levels.stop);
23878 });
23879}
23880
23881bool PineExecutionAdapter::fill_time_global_exit(const PlacementSnapshot& source) const {
23882 // TradingView sizes a from_entry "" exit that names no quantity when it
23883 // fills: its percentage of the whole position then, the lots a market add
23884 // opened after it was placed included -- at the same open it fills at, on
23885 // a later bar, or at a process_orders_on_close close -- and whether it was
23886 // placed before or after that add (lab tv w4-f19a-*, lane
23887 // W4-ENG-POOC-SAMEPASS; job-2388's DCA take-profit). An exit that names its
23888 // entry closes that entry's lots only, which the kernel scope already
23889 // selects. Scoped to the book's only exit: beside another exit order the
23890 // placement-time reservation still splits the book.
23891 if (!source.from_entry.empty() || std::isfinite(source.requested_qty)) return false;
23892 if (source.family != PineOrderFamily::ExitLimit && source.family != PineOrderFamily::ExitStop
23893 && source.family != PineOrderFamily::ExitTrail) {
23894 return false;
23895 }
23896 if (!(std::isfinite(source.projection_remaining_qty) && source.projection_remaining_qty > 0.0))
23897 return false;
23898 const auto other_exit = [&](const PlacementSnapshot& row) {
23899 return (row.family == PineOrderFamily::ExitLimit || row.family == PineOrderFamily::ExitStop
23900 || row.family == PineOrderFamily::ExitTrail)
23901 && (row.source_id != source.source_id || !row.from_entry.empty());
23902 };
23903 if (any_live_row(live_handles_, placement_, other_exit)) return false;
23904 for (const auto& leg : pending_bracket_legs_)
23905 if (other_exit(leg.snapshot)) return false;
23906 return true;
23907}
23908
23909// TradingView's from_entry "" exit with relative legs over a pyramided
23910// position (ExitBindingRuleSwitches::global_exit_per_entry_levels). It gives
23911// each entry its own bracket, at levels from that entry's own fill price
23912// (profit: fill + / - the profit ticks, loss: fill - / + the loss ticks); the
23913// two legs of one entry are one bracket. An entry that fills after the call
23914// gets its bracket at its fill, live for the rest of that bar. Each bracket
23915// fills at most once, and a re-issued exit does not give a consumed entry
23916// another. Every fill closes the oldest open trade, whichever entry's bracket
23917// filled. A book that goes flat ends the brackets left, so an entry filling
23918// later in that bar waits for the next call. 60 lab tv tapes of synthetic
23919// scripts (NYSE:F 15m: a held lot, a same-side add reached later by a limit,
23920// a stop or a market order, and the exit called on every held bar;
23921// tests/fixtures/per_entry_exit), each reproduced row for row by a forward
23922// predictor of these rules. A position of one entry keeps its one exit at
23923// the average price, which is that entry's own bracket; the second entry's
23924// fill turns it into the first entry's bracket (per_entry_exits_at_entry_fill).
23925// The tapes have neither calc_on_order_fills nor process_orders_on_close, run
23926// on the chart's own bars and close FIFO; every other configuration keeps the
23927// one exit over the whole position.
23928bool PineExecutionAdapter::per_entry_exit_scope() const {
23929 if (!detail::exit_binding_rule_switches().global_exit_per_entry_levels
23930 || config_.calc_on_order_fills || config_.process_orders_on_close
23931 || config_.close_entries_rule_any || !finite_positive(staged_.syminfo.mintick)
23932 || !modeled_input()) {
23933 return false;
23934 }
23935 const auto native = detail::run_state(require_host());
23936 return !native.spec || native.spec->intrabar.is_none();
23937}
23938
23939// The held position's entries in fill order: one per open lot on the held
23940// side, each with its own fill price and the units it still holds. Empty
23941// when a lot was not opened by a strategy.entry order (the tapes reach
23942// nothing else).
23944PineExecutionAdapter::per_entry_open_entries(bool long_side) const {
23945 detail::ExitScratchRows<PerEntryExitChild> entries(*this);
23946 detail::exit_scratch_lots(entries.context(), require_host(), kNaN,
23947 [&](const NativeOpenLot& lot) {
23948 const auto row = placement_.find(lot.entry_incarnation);
23949 if (row == placement_.end() || !row->second.opening
23950 || row->second.family != PineOrderFamily::Entry
23951 || (lot.signed_units > 0.0) != long_side || !finite_positive(lot.entry_price)
23952 || !(std::abs(lot.signed_units) > 0.0)) {
23953 entries.clear();
23954 return false;
23955 }
23956 entries.push_back({lot.entry_incarnation, lot.entry_price, std::abs(lot.signed_units),
23957 false});
23958 return true;
23959 });
23960 return entries;
23961}
23962
23963detail::ExitScratchRows<native_order::RequestHandle> PineExecutionAdapter::per_entry_child_legs(
23964 const SourceId& exit_id, std::uint64_t origin) const {
23965 detail::ExitScratchRows<native_order::RequestHandle> legs(*this);
23966 for (const auto& handle : live_handles_) {
23967 const auto row = placement_.find(handle.incarnation);
23968 if (row != placement_.end() && row->second.per_entry_origin != 0
23969 && row->second.source_id == exit_id
23970 && (origin == 0 || row->second.per_entry_origin == origin)) {
23971 legs.push_back(handle);
23972 }
23973 }
23974 return legs;
23975}
23976
23977// Cancels and retires those of `legs` still working. One the kernel already
23978// ended (an OCA sibling of a fill) waits for its terminal receipt, and a
23979// cancel of it would record a NotWorking event.
23980void PineExecutionAdapter::cancel_working_legs(
23982 if (legs.empty()) return;
23983 detail::ExitScratchIndex working(legs.context());
23984 detail::exit_scratch_working(legs.context(), require_host(),
23985 [&](const NativeWorkingRequest& request) {
23986 working.put(request.definition->handle.incarnation, 0);
23987 return true;
23988 });
23989 for (const auto& handle : legs) {
23990 const bool still_working =
23991 working.find(handle.incarnation) != detail::ExitScratchIndex::absent;
23992 if (!still_working) continue;
23993 const auto result = require_host().cancel(handle);
23994 if (result.status == native_order::CancelStatus::Cancelled) retire(handle);
23995 }
23996}
23997
23998// Serves a strategy.exit call per entry when the rule covers it; false leaves
23999// the call to exit()'s own course.
24000bool PineExecutionAdapter::per_entry_exit_call(
24001 const SourceId& exit_id, const SourceId& from_entry, double limit, double stop,
24002 double trail_points, double trail_offset, double trail_price, double qty_percent,
24003 const std::string& comment, double qty, const std::string& oca_name,
24004 double profit_ticks, double loss_ticks, std::uint64_t command_sequence) {
24005 const auto tracked = [&] {
24006 return std::find_if(per_entry_exits_.begin(), per_entry_exits_.end(),
24007 [&](const PerEntryExit& row) { return row.exit_id == exit_id; });
24008 };
24009 const auto absent_or_positive = [](double value) {
24010 return std::isnan(value) || finite_positive(value);
24011 };
24012 const bool relative_only = from_entry.empty() && std::isnan(limit) && std::isnan(stop)
24013 && std::isnan(trail_points) && std::isnan(trail_offset) && std::isnan(trail_price)
24014 && std::isnan(qty)
24015 && (std::isnan(qty_percent) || qty_percent >= 100.0 - internal::kFullPercentEps)
24016 && oca_name.empty() && absent_or_positive(profit_ticks)
24017 && absent_or_positive(loss_ticks)
24018 && (finite_positive(profit_ticks) || finite_positive(loss_ticks));
24019 auto record = tracked();
24020 if (record == per_entry_exits_.end() && !relative_only) return false;
24021 const auto physical = detail::run_position(require_host());
24022 const bool long_side = physical.signed_units > 0.0;
24023 const bool in_scope = relative_only && physical.signed_units != 0.0
24024 && per_entry_exit_scope();
24025 if (record != per_entry_exits_.end()
24026 && (!in_scope || record->cycle != current_position_cycle_
24027 || record->long_side != long_side)) {
24028 // Another definition, or a position this record no longer stands on:
24029 // the call takes exit()'s own course over what the position holds.
24030 end_per_entry_exit(static_cast<std::size_t>(record - per_entry_exits_.begin()));
24031 record = per_entry_exits_.end();
24032 }
24033 if (!in_scope) return false;
24034 const auto same = [](double left, double right) {
24035 return same_double_bits(left, right) || (std::isnan(left) && std::isnan(right));
24036 };
24037 if (record == per_entry_exits_.end()) {
24038 auto entries = per_entry_open_entries(long_side);
24039 if (entries.size() < 2) return false;
24040 // Called over two entries already: each takes its own bracket, and a
24041 // leg this exit holds over the whole position gives way to them.
24042 detail::ExitScratchRows<native_order::RequestHandle> whole(*this);
24043 for (const auto& handle : live_handles_) {
24044 const auto row = placement_.find(handle.incarnation);
24045 if (row == placement_.end()) continue;
24046 const auto& leg = row->second;
24047 if ((leg.family == PineOrderFamily::ExitLimit
24048 || leg.family == PineOrderFamily::ExitStop
24049 || leg.family == PineOrderFamily::ExitTrail)
24050 && leg.source_id == exit_id && leg.from_entry.empty()) {
24051 whole.push_back(handle);
24052 }
24053 }
24054 cancel_working_legs(whole);
24055 if (const auto family = bracket_families_.find(key_for(exit_id, from_entry));
24056 family != bracket_families_.end()) {
24057 family->second.remove_selected(whole);
24058 if (family->second.empty()) bracket_families_.erase(family);
24059 }
24060 PerEntryExit added;
24061 added.exit_id = exit_id;
24062 added.cycle = current_position_cycle_;
24063 added.long_side = long_side;
24064 added.profit_ticks = profit_ticks;
24065 added.loss_ticks = loss_ticks;
24066 added.comment = comment;
24067 for (const auto& entry : entries) added.children.push_back(entry);
24068 per_entry_exits_.push_back(std::move(added));
24069 const PerEntryExit& exit = per_entry_exits_.back();
24070 for (const auto& child : exit.children)
24071 submit_per_entry_child(exit, child, command_sequence);
24072 return true;
24073 }
24074 // Re-issued: every bracket stands as it was placed, and an entry whose
24075 // bracket filled gets none again. A changed definition re-prices the
24076 // brackets still live.
24077 const bool same_definition = same(record->profit_ticks, profit_ticks)
24078 && same(record->loss_ticks, loss_ticks) && record->comment == comment;
24079 record->profit_ticks = profit_ticks;
24080 record->loss_ticks = loss_ticks;
24081 record->comment = comment;
24082 for (const auto& entry : per_entry_open_entries(long_side)) {
24083 const bool known = std::any_of(record->children.begin(), record->children.end(),
24084 [&](const PerEntryExitChild& child) { return child.origin == entry.origin; });
24085 if (!known) record->children.push_back(entry);
24086 }
24087 const std::size_t index = static_cast<std::size_t>(record - per_entry_exits_.begin());
24088 for (std::size_t position = 0; position < per_entry_exits_[index].children.size();
24089 ++position) {
24090 const PerEntryExitChild child = per_entry_exits_[index].children[position];
24091 if (child.consumed) continue;
24092 const auto legs = per_entry_child_legs(exit_id, child.origin);
24093 if (!legs.empty() && same_definition) continue;
24094 cancel_working_legs(legs);
24095 submit_per_entry_child(per_entry_exits_[index], child, command_sequence);
24096 }
24097 return true;
24098}
24099
24100// One entry's bracket: a limit at its fill price + / - the profit ticks and a
24101// stop at - / + the loss ticks, each the trigger exit() gives such a level,
24102// in one cancel group of the entry's own, each closing the entry's units.
24103void PineExecutionAdapter::submit_per_entry_child(const PerEntryExit& exit,
24104 const PerEntryExitChild& child,
24105 std::uint64_t command_sequence) {
24106 const double tick = staged_.syminfo.mintick;
24107 const double side = exit.long_side ? 1.0 : -1.0;
24108 const bool exit_is_buy = !exit.long_side;
24109 const double limit = finite_positive(exit.profit_ticks)
24110 ? source_level_on_price_grid(child.fill_price + side * exit.profit_ticks * tick, tick)
24111 : kNaN;
24112 const double stop = finite_positive(exit.loss_ticks)
24113 ? source_level_on_price_grid(child.fill_price - side * exit.loss_ticks * tick, tick)
24114 : kNaN;
24115 const std::string origin_key = std::to_string(child.origin);
24116 const SourceId group_name = exit.exit_id + "\x1f\x1f" + origin_key;
24117 const OrderBirth birth = capture_order_birth();
24118 const PineSizingSnapshot sizing = sizing_snapshot();
24119 const auto family_key = key_for(exit.exit_id, SourceId{});
24120 const auto submit = [&](PineOrderFamily family, native_order::Trigger trigger) {
24121 native_order::Request request;
24122 request.intent = native_order::HostSized{native_order::HostSizedKind::Close, std::nullopt};
24123 request.label = exit.exit_id;
24124 request.comment = exit.comment;
24125 request.trigger = trigger;
24126 request.owner = native_order::Independent{};
24127 request.group = group_for(group_name, 1, static_cast<std::int64_t>(family));
24128 PlacementSnapshot snapshot;
24129 snapshot.family = family;
24130 snapshot.source_id = exit.exit_id;
24131 snapshot.comment = exit.comment;
24132 snapshot.qty_percent = 100.0;
24133 snapshot.deferred_cohort = true;
24134 snapshot.is_long = false;
24135 snapshot.command_sequence = command_sequence;
24136 snapshot.exit_levels = {limit, stop, kNaN, kNaN, kNaN, exit.profit_ticks, exit.loss_ticks};
24137 snapshot.birth = birth;
24138 snapshot.birth_reach = compat::pine::select_historical_birth_reach(birth, false);
24139 snapshot.sizing = sizing;
24140 snapshot.placement_cycle = current_position_cycle_;
24141 snapshot.projection_remaining_qty = child.units;
24142 snapshot.per_entry_origin = child.origin;
24143 snapshot.per_entry_units = child.units;
24144 initialize_l4c_policy(snapshot, {});
24145 const SourceId replacement_key = exit.exit_id + "\x1f\x1f"
24146 + std::to_string(static_cast<int>(family)) + "\x1f" + origin_key;
24147 const auto accepted = submit_or_replace(std::move(request), std::move(snapshot), false,
24148 replacement_key);
24149 if (accepted) {
24150 auto& roster = bracket_family(family_key);
24151 if (!roster.holds(*accepted)) roster.push_back(*accepted);
24152 }
24153 };
24154 if (std::isfinite(limit)) {
24156 native_order::Limit{exit_limit_trigger(limit, tick, exit_is_buy)});
24157 }
24158 if (std::isfinite(stop)) {
24160 native_order::Stop{source_trigger_threshold(stop, tick, exit_is_buy, false)});
24161 }
24162}
24163
24164// An entry that fills while a global exit with relative legs stands on the
24165// position gets its own bracket there, born at the fill (live for the rest of
24166// the bar). The exit's legs over the one entry held before stay that entry's
24167// bracket, closing its units from now on.
24168void PineExecutionAdapter::per_entry_exits_at_entry_fill(
24169 const PlacementSnapshot& opening, const native_order::ExecutionAppliedEvent& event) {
24170 if (!opening.opening || opening.family != PineOrderFamily::Entry
24171 || !(std::abs(event.opened_units) > 0.0) || event.closed_units > 0.0) {
24172 return;
24173 }
24174 const auto physical = detail::run_position(require_host());
24175 const bool long_side = physical.signed_units > 0.0;
24176 if (physical.signed_units == 0.0 || opening.is_long != long_side) return;
24177 const auto tracked = [&](const SourceId& exit_id) {
24178 return std::any_of(per_entry_exits_.begin(), per_entry_exits_.end(),
24179 [&](const PerEntryExit& row) { return row.exit_id == exit_id; });
24180 };
24181 // The exits standing over the whole position: a from_entry "" stop or
24182 // limit leg with a relative operand, on the side held.
24183 detail::ExitScratchRows<SourceId> standing(*this);
24184 for (const auto& handle : live_handles_) {
24185 const auto row = placement_.find(handle.incarnation);
24186 if (row == placement_.end()) continue;
24187 const auto& leg = row->second;
24188 if ((leg.family != PineOrderFamily::ExitLimit && leg.family != PineOrderFamily::ExitStop)
24189 || !leg.from_entry.empty() || leg.per_entry_origin != 0
24190 || !(finite_positive(leg.exit_levels.profit_ticks)
24191 || finite_positive(leg.exit_levels.loss_ticks))
24192 || tracked(leg.source_id)
24193 || standing.contains(leg.source_id)) {
24194 continue;
24195 }
24196 standing.push_back(leg.source_id);
24197 }
24198 if (per_entry_exits_.empty() && standing.empty()) return;
24199 if (!per_entry_exit_scope()) return;
24200 const PerEntryExitChild added{event.handle().incarnation, event.resolved_price,
24201 std::abs(event.opened_units), false};
24202 for (std::size_t index = 0; index < per_entry_exits_.size(); ++index) {
24203 auto& exit = per_entry_exits_[index];
24204 if (exit.cycle != current_position_cycle_ || exit.long_side != long_side
24205 || std::any_of(exit.children.begin(), exit.children.end(),
24206 [&](const PerEntryExitChild& child) { return child.origin == added.origin; })) {
24207 continue;
24208 }
24209 exit.children.push_back(added);
24210 submit_per_entry_child(per_entry_exits_[index], added, 0);
24211 }
24212 if (standing.empty()) return;
24213 const auto held_side = static_cast<std::int32_t>(
24215 const double tick = staged_.syminfo.mintick;
24216 const double side = long_side ? 1.0 : -1.0;
24217 for (const auto& exit_id : standing) {
24218 const auto staged = [&](const PlacementSnapshot& row) {
24219 return row.source_id == exit_id && row.from_entry.empty();
24220 };
24221 bool uniform = std::none_of(pending_bracket_legs_.begin(), pending_bracket_legs_.end(),
24222 [&](const PendingBracketLeg& row) { return staged(row.snapshot); })
24223 && std::none_of(delayed_market_orders_.begin(), delayed_market_orders_.end(),
24224 [&](const DelayedMarketOrder& row) { return staged(row.snapshot); })
24225 && std::none_of(pending_coof_requests_.begin(), pending_coof_requests_.end(),
24226 [&](const PendingCoofRequest& row) { return staged(row.snapshot); });
24227 detail::ExitScratchRows<native_order::RequestHandle> legs(*this);
24228 const PlacementSnapshot* first = nullptr;
24229 for (const auto& handle : live_handles_) {
24230 const auto row = placement_.find(handle.incarnation);
24231 if (row == placement_.end() || !staged(row->second)) continue;
24232 const auto& leg = row->second;
24233 const bool exit_leg = leg.family == PineOrderFamily::ExitLimit
24234 || leg.family == PineOrderFamily::ExitStop
24235 || leg.family == PineOrderFamily::ExitTrail;
24236 if (!exit_leg) continue;
24237 if (!first) first = &leg;
24238 uniform = uniform && leg.family != PineOrderFamily::ExitTrail
24239 && leg.deferred_cohort && std::isnan(leg.requested_qty)
24240 && (std::isnan(leg.qty_percent)
24241 || leg.qty_percent >= 100.0 - internal::kFullPercentEps)
24242 && leg.oca_name.empty() && std::isnan(leg.exit_levels.trail_points)
24243 && std::isnan(leg.exit_levels.trail_offset)
24244 && std::isnan(leg.exit_levels.trail_price)
24245 && leg.placement_cycle == current_position_cycle_
24246 && leg.projection_position_side == held_side
24247 && same_double_bits(leg.exit_levels.profit_ticks, first->exit_levels.profit_ticks)
24248 && same_double_bits(leg.exit_levels.loss_ticks, first->exit_levels.loss_ticks)
24249 && leg.comment == first->comment;
24250 legs.push_back(handle);
24251 }
24252 if (!uniform || !first) continue;
24253 // The position before this fill: the one entry these legs were placed
24254 // over, at its own levels.
24255 const auto entries = per_entry_open_entries(long_side);
24256 PerEntryExitChild prior{};
24257 std::size_t prior_count = 0;
24258 for (const auto& entry : entries) {
24259 if (entry.origin == added.origin) continue;
24260 prior = entry;
24261 ++prior_count;
24262 }
24263 if (prior_count != 1) continue;
24264 const double profit_ticks = first->exit_levels.profit_ticks;
24265 const double loss_ticks = first->exit_levels.loss_ticks;
24266 const auto own_level = [&](const PlacementSnapshot& leg) {
24267 if (leg.family == PineOrderFamily::ExitLimit) {
24268 return finite_positive(profit_ticks) && same_double_bits(leg.exit_levels.limit,
24269 source_level_on_price_grid(prior.fill_price + side * profit_ticks * tick, tick));
24270 }
24271 return finite_positive(loss_ticks) && same_double_bits(leg.exit_levels.stop,
24272 source_level_on_price_grid(prior.fill_price - side * loss_ticks * tick, tick));
24273 };
24274 bool own = true;
24275 for (const auto& handle : legs) {
24276 const auto row = placement_.find(handle.incarnation);
24277 own = own && row != placement_.end() && own_level(row->second);
24278 }
24279 if (!own) continue;
24280 PerEntryExit exit;
24281 exit.exit_id = first->source_id;
24282 exit.cycle = current_position_cycle_;
24283 exit.long_side = long_side;
24284 exit.profit_ticks = profit_ticks;
24285 exit.loss_ticks = loss_ticks;
24286 exit.comment = first->comment;
24287 exit.children = {prior, added};
24288 for (const auto& handle : legs) {
24289 if (const auto row = placement_.find(handle.incarnation); row != placement_.end()) {
24290 row->second.per_entry_origin = prior.origin; invalidate_retirement();
24291 row->second.per_entry_units = prior.units;
24292 }
24293 }
24294 per_entry_exits_.push_back(std::move(exit));
24295 submit_per_entry_child(per_entry_exits_.back(), added, 0);
24296 }
24297}
24298
24299// A leg of an entry's bracket that filled spends that bracket; a book left
24300// flat (or on another position) ends every bracket left.
24301void PineExecutionAdapter::per_entry_exits_after_fill(
24302 const std::optional<PlacementSnapshot>& placement,
24303 const native_order::ExecutionAppliedEvent& event) {
24304 if (per_entry_exits_.empty()) return;
24305 if (placement && placement->per_entry_origin != 0 && event.closed_units > 0.0) {
24306 for (auto& exit : per_entry_exits_) {
24307 if (exit.exit_id != placement->source_id) continue;
24308 for (auto& child : exit.children)
24309 if (child.origin == placement->per_entry_origin) child.consumed = true;
24310 }
24311 }
24312 const auto physical = detail::run_position(require_host());
24313 for (std::size_t index = per_entry_exits_.size(); index-- > 0;) {
24314 const auto& exit = per_entry_exits_[index];
24315 if (physical.signed_units == 0.0 || exit.cycle != current_position_cycle_
24316 || (physical.signed_units > 0.0) != exit.long_side) {
24317 end_per_entry_exit(index);
24318 }
24319 }
24320}
24321
24322// TradingView's order at one point of the path, among the orders a held
24323// global exit with relative legs meets there beside an add of the held side
24324// (global_exit_per_entry_levels): stop and market orders before limit orders,
24325// and among the stop and market orders the buys before the sells; two orders
24326// on one tick meet at one point. A long's limit add that a gapped open passes
24327// together with the exit's stop fills after that stop closed the position,
24328// and waits for the next call for its own bracket (tests/fixtures/
24329// per_entry_exit). The native matcher breaks a same-point tie by queue order,
24330// so after the script those orders are re-priced in place
24331// (ReplaceOptions::keep_handle) into that order, as
24332// order_open_marketable_limit_entries does at an open. An add sized by the
24333// core (an unpriced default quantity, which a re-price would size again) or
24334// another trigger keeps the queue as it is.
24335void PineExecutionAdapter::order_per_entry_point_ties() {
24336 if (!detail::exit_binding_rule_switches().global_exit_per_entry_levels) return;
24337 const auto physical = detail::run_position(require_host());
24338 if (physical.signed_units == 0.0) return;
24339 const bool long_side = physical.signed_units > 0.0;
24340 const auto exit_leg = [&](const PlacementSnapshot& row) {
24341 return (row.family == PineOrderFamily::ExitLimit
24342 || row.family == PineOrderFamily::ExitStop)
24343 && row.from_entry.empty() && std::isnan(row.requested_qty) && row.oca_name.empty()
24344 && (finite_positive(row.exit_levels.profit_ticks)
24345 || finite_positive(row.exit_levels.loss_ticks))
24346 && std::isnan(row.exit_levels.trail_points)
24347 && std::isnan(row.exit_levels.trail_offset)
24348 && std::isnan(row.exit_levels.trail_price);
24349 };
24350 const auto add = [&](const PlacementSnapshot& row) {
24351 return row.opening && row.family == PineOrderFamily::Entry && row.is_long == long_side;
24352 };
24353 if (!any_live_row(live_handles_, placement_, exit_leg)
24354 || !any_live_row(live_handles_, placement_, add) || !per_entry_exit_scope()) {
24355 return;
24356 }
24357 auto& host = require_host();
24358 struct Ranked {
24359 std::uint64_t incarnation = 0;
24360 std::uint64_t request = 0;
24361 int rank = 0;
24362 std::uint64_t priority = 0;
24363 };
24364 detail::ExitScratchRows<Ranked> rows(*this);
24365 auto& versions = detail::exit_scratch_versions(rows.context());
24366 bool supported = true;
24367 detail::exit_scratch_working(rows.context(), host, [&](const NativeWorkingRequest& working) {
24368 const auto& definition = *working.definition;
24369 const auto found = placement_.find(definition.handle.incarnation);
24370 if (found == placement_.end()) return true;
24371 const bool leg = exit_leg(found->second);
24372 if (!leg && !add(found->second)) return true;
24373 const auto& trigger = definition.request.trigger;
24374 int order_class = 0;
24375 if (std::holds_alternative<native_order::Limit>(trigger)) {
24376 order_class = 1;
24377 } else if (!std::holds_alternative<native_order::Market>(trigger)
24378 && !std::holds_alternative<native_order::Stop>(trigger)) {
24379 supported = false;
24380 return false;
24381 }
24382 if (std::holds_alternative<native_order::Sized>(definition.request.intent)) {
24383 supported = false;
24384 return false;
24385 }
24386 const bool buy = leg ? !long_side : long_side;
24387 rows.push_back({definition.handle.incarnation,
24388 versions.request(rows.scope(), definition.request),
24389 order_class * 2 + (buy ? 0 : 1),
24390 definition.priority != 0 ? definition.priority
24391 : definition.handle.incarnation});
24392 return true;
24393 });
24394 if (!supported) return;
24395 detail::ExitScratchRows<Ranked> by_priority(*this);
24396 rows.ordered(
24397 [](const Ranked& row) { return row.priority; },
24398 [&](const Ranked& row, std::size_t) { by_priority.push_back(row); return true; });
24399 native_order::ReplaceOptions reprice;
24400 reprice.keep_handle = true;
24401 std::size_t index = 0;
24402 bool unchanged_prefix = true;
24403 by_priority.ordered(
24404 [](const Ranked& row) { return static_cast<std::uint64_t>(row.rank); },
24405 [&](const Ranked& row, std::size_t) {
24406 unchanged_prefix = unchanged_prefix
24407 && by_priority[index].incarnation == row.incarnation;
24408 ++index;
24409 if (!unchanged_prefix) {
24410 const native_order::RequestHandle handle{rows.context().run, row.incarnation};
24411 (void)host.replace(handle, versions.request(row.request), reprice);
24412 }
24413 return true;
24414 });
24415}
24416
24417void PineExecutionAdapter::end_per_entry_exit(std::size_t index) {
24418 const SourceId exit_id = per_entry_exits_[index].exit_id;
24419 per_entry_exits_.erase(per_entry_exits_.begin() + static_cast<std::ptrdiff_t>(index));
24420 const auto legs = per_entry_child_legs(exit_id, 0);
24421 if (legs.empty()) return;
24422 cancel_working_legs(legs);
24423 if (const auto family = bracket_families_.find(key_for(exit_id, SourceId{}));
24424 family != bracket_families_.end()) {
24425 family->second.remove_selected(legs);
24426 if (family->second.empty()) bracket_families_.erase(family);
24427 }
24428}
24429
24430void PineExecutionAdapter::fill_pooc_close_exits(
24431 double raw_close, const NativeDecisionContext& context) {
24432 // TradingView's process_orders_on_close pass (lab tv w4-f08*, lane
24433 // W4-ENG-POOC-SAMEPASS): the calculation's market orders fill at the
24434 // close first -- a strategy.close, or an entry that reverses the held
24435 // side, voids that side's exits -- and then every exit leg the
24436 // calculation placed is decided against the same close: it fills there
24437 // when the close's tick reaches its level on the closing side (a sell
24438 // stop at or above it, a sell limit at or below it, and the mirror for a
24439 // buy), the stop leg first and at most one leg per exit order; otherwise
24440 // it rests for the next bar. That holds for a position held from an
24441 // earlier bar, one this close pass opened or reversed into (the exit
24442 // placed with its entry), and a from_entry "" exit, with or without
24443 // calc_on_order_fills. The close's tick, not the raw close, decides
24444 // (synthetic NYSE:F half-cent closes: a sell limit 25.047 over a raw
24445 // 25.045 close fills there, a sell stop 25.027 over a raw 25.025 close
24446 // rests, on either side and in either scope). The fill books the close,
24447 // a stop slipped like any stop fill (w4-f08s-pooc-slip).
24448 const auto physical = detail::run_position(require_host());
24449 if (physical.signed_units == 0.0 || !finite_positive(raw_close)) return;
24450 const bool closing_long = physical.signed_units > 0.0;
24451 const auto held_side = static_cast<std::int32_t>(
24452 closing_long ? PositionSide::LONG : PositionSide::SHORT);
24453 const double tick = staged_.syminfo.mintick;
24454 const double close_tick = source_bar_fill_tick(raw_close, tick);
24455 struct Group {
24456 std::uint64_t stop = 0;
24457 std::uint64_t stop_snapshot = 0;
24458 std::uint64_t limit = 0;
24459 std::uint64_t limit_snapshot = 0;
24460 };
24461 detail::ExitScratchRows<Group> groups(*this);
24462 detail::ExitScratchIndex keys(groups.context());
24463 auto& versions = detail::exit_scratch_versions(groups.context());
24464 for (const auto& handle : live_handles_) {
24465 const auto found = placement_.find(handle.incarnation);
24466 if (found == placement_.end()) continue;
24467 const auto& row = found->second;
24468 if (row.family != PineOrderFamily::ExitLimit
24469 && row.family != PineOrderFamily::ExitStop) continue;
24470 if (row.birth.from_fill()) continue;
24471 if (row.projection_created_bar != projection_bar_index(context)) continue;
24472 // The leg is the held position's: a named entry's own lots, or a
24473 // from_entry "" exit placed under this position or while flat
24474 // (TVDEF-DROPS R3: an exit belongs to the position it was placed
24475 // under).
24476 if (!row.from_entry.empty()
24477 ? !(cohort_exposure_for(row.from_entry) > 0.0)
24478 : (row.projection_position_side != held_side
24479 && row.projection_position_side
24480 != static_cast<std::int32_t>(PositionSide::FLAT))) {
24481 continue;
24482 }
24483 // A leg the calculation placed before the entry it names is never
24484 // applied by TradingView (w4-f08-* cell I); the pass leaves it be.
24485 if (!row.from_entry.empty()) {
24486 const auto cohort = cohorts_by_id_.find(row.from_entry);
24487 if (cohort != cohorts_by_id_.end() && !cohort->second.opened.empty()) {
24488 const auto parent = placement_.find(cohort->second.opened.back().incarnation);
24489 if (parent != placement_.end()
24490 && parent->second.projection_created_bar == row.projection_created_bar
24491 && parent->second.command_sequence > row.command_sequence) {
24492 continue;
24493 }
24494 }
24495 }
24496 const bool stop_leg = row.family == PineOrderFamily::ExitStop;
24497 const double level = stop_leg ? row.exit_levels.stop : row.exit_levels.limit;
24498 if (!finite_positive(level)) continue;
24499 const detail::ExitScratchPairText key(row.source_id, row.from_entry);
24500 auto index = keys.find(key);
24501 if (index == detail::ExitScratchIndex::absent) {
24502 index = groups.size();
24503 keys.put(key, index);
24504 groups.push_back(Group{});
24505 }
24506 Group group = groups[index];
24507 if (stop_leg) {
24508 if (group.stop == 0) {
24509 group.stop = handle.incarnation;
24510 group.stop_snapshot = versions.placement(groups.scope(), row);
24511 }
24512 } else if (group.limit == 0) {
24513 group.limit = handle.incarnation;
24514 group.limit_snapshot = versions.placement(groups.scope(), row);
24515 }
24516 groups.set(index, group);
24517 }
24518 for (const auto& group : groups) {
24519 // An earlier exit of this pass may have closed the position.
24520 const double live_signed = detail::run_position(require_host()).signed_units;
24521 if (live_signed == 0.0 || (live_signed > 0.0) != closing_long) break;
24522 const double stop_level = group.stop != 0
24523 ? versions.placement(group.stop_snapshot).exit_levels.stop : kNaN;
24524 const double limit_level = group.limit != 0
24525 ? versions.placement(group.limit_snapshot).exit_levels.limit : kNaN;
24526 const bool fill_stop = group.stop != 0
24527 && (closing_long ? close_tick <= stop_level : close_tick >= stop_level);
24528 const bool fill_limit = group.limit != 0
24529 && (closing_long ? close_tick >= limit_level : close_tick <= limit_level);
24530 if (!fill_stop && !fill_limit) continue;
24531 const native_order::RequestHandle selected{
24532 groups.context().run, fill_stop ? group.stop : group.limit};
24533 if (!selected.incarnation) continue;
24534 const auto row = versions.placement(
24535 fill_stop ? group.stop_snapshot : group.limit_snapshot);
24536 const double live_held_units = std::abs(live_signed);
24537 double units = std::isfinite(row.projection_remaining_qty)
24538 ? std::max(0.0, row.projection_remaining_qty)
24539 : (std::isfinite(row.requested_qty) ? std::abs(row.requested_qty) : 0.0);
24540 if ((!std::isfinite(row.projection_remaining_qty) && !std::isfinite(row.requested_qty))
24541 || fill_time_global_exit(row)) {
24542 // A host-sized leg placed while flat carries no reservation yet,
24543 // and a from_entry "" exit that is the book's only exit sizes on
24544 // the position it fills against: it closes its share of what it
24545 // exits now (w4-f08-* cell J, w4-f19a-pooc cells A and B).
24546 units = quantize_close_units(
24547 row.from_entry.empty() ? live_held_units : cohort_exposure_for(row.from_entry),
24548 std::isfinite(row.qty_percent) ? row.qty_percent : 100.0);
24549 // A partial one keeps its placement reservation (resolve_terms).
24550 if (std::isfinite(row.qty_percent)
24551 && row.qty_percent < 100.0 - internal::kFullPercentEps
24552 && finite_positive(row.projection_remaining_qty)) {
24553 units = std::max(units, row.projection_remaining_qty);
24554 }
24555 }
24556 if (!(units > 0.0)) continue;
24557 // ab9714be pine_fills.cpp:1618 and pine_fills.cpp:1708-1723 spell
24558 // full-position coverage as `qty - kQtyEpsilon`, cap the quantity at
24559 // the whole position, and then book it through the WHOLE-position exit
24560 // instead of a sized reduction. Earlier same-bar reissues in this pass
24561 // have already reduced the book, so the test reads the position fresh.
24562 // Submitting the reservation's stale binary64 residual as a sized
24563 // reduction instead is refused off-grid, and the sub-lot remainder it
24564 // strands is what the 1x-margin path later fragments.
24565 const bool covers_live_book = live_held_units > 0.0
24566 && units >= live_held_units - internal::kQtyEpsilon;
24567 cancel_bracket_siblings(selected);
24568 native_order::Request request;
24569 request.intent = covers_live_book
24572 native_order::Reduce{native_order::ExplicitUnits{units}}};
24573 request.label = row.source_id;
24574 request.comment = row.comment;
24575 request.trigger = native_order::Market{};
24576 PlacementSnapshot immediate = row;
24577 const bool stop_close = row.family == PineOrderFamily::ExitStop;
24578 // A stop leg books on the closing side's own path
24579 // (apply_fill_slippage(price, is_buy)): closing a SHORT is a BUY, so
24580 // the slip is ADDED to the close; closing a LONG is a SELL, so it is
24581 // subtracted. A limit leg is never slipped.
24582 immediate.forced_execution_price = nearest_tick(
24583 raw_close + (stop_close ? (closing_long ? -1.0 : 1.0) : 0.0)
24584 * config_.slippage * tick,
24585 tick);
24586 immediate.projection_predecessor = selected.incarnation;
24587 immediate.projection_predecessor_exit = true;
24588 const auto accepted = submit_or_replace(
24589 std::move(request), std::move(immediate), false,
24590 row.source_id + "\x1f" + row.from_entry
24591 + std::to_string(static_cast<int>(row.family)));
24592 if (accepted) execute_or_withdraw_close(*accepted, row.void_issue);
24593 }
24594}
24595
24596// A carried trailing stop is reached where the bar's tick-built print reaches
24597// it, the level itself staying the raw running best +/- the offset: the
24598// static stop's rule (source_trigger_threshold), now for the stop a trail
24599// rides. TradingView, lab tv tape tests/fixtures/callback_lifecycle
24600// (NYSE:F 15m, calc_on_order_fills off): a synthetic short trailing
24601// 14 ticks behind a best of 20.385 rests at 20.525000000000002; a
24602// high of decimal 20.525 lies one ULP below that computed
24603// level, prints 20.53 and exits there; the raw compare would wait.
24604// The kernel's generic Trail compares the raw path with the raw level, so
24605// for one bar the adapter walks the bar's path as the kernel will (the best
24606// is tested on each leg and then moved by its end), finds the point the
24607// tick-built path first reaches the stop, and, only where that point comes
24608// before the one the raw compare would reach, re-prices the trail's distance
24609// for that bar so the kernel's own compare fires there. That is the direction
24610// the tape shows; a raw reach the tick-built path would not make (a print
24611// whose tick falls back inside the stop) is left to the kernel, as no tape
24612// decides it. The ride (retain_trigger_state) and the handle are kept; the
24613// source fill still books the level from the carried best and the source
24614// offset (resolve_terms). The opening print is the block above's;
24615// calc_on_order_fills and an intrabar path are left out, as no tape covers
24616// them.
24617void PineExecutionAdapter::retune_carried_trails_for_tick_reach(
24618 const Bar& bar, const NativeDecisionContext& context) {
24620 || trail_state_at_open_.empty() || !modeled_input() || config_.calc_on_order_fills
24621 || context.driver_statistics.intrabar_path_enabled) {
24622 return;
24623 }
24624 const double tick = staged_.syminfo.mintick;
24625 if (!finite_positive(tick) || !finite_positive(bar.open) || !finite_positive(bar.high)
24626 || !finite_positive(bar.low) || !finite_positive(bar.close)) {
24627 return;
24628 }
24629 const double held_units = detail::run_position(require_host()).signed_units;
24630 if (held_units == 0.0) return;
24631 const bool is_buy = held_units < 0.0;
24632 const bool high_first = source_path_uses_high_first(bar);
24633 const double path[3] = {high_first ? bar.high : bar.low,
24634 high_first ? bar.low : bar.high, bar.close};
24635 const auto improves = [&](double best, double price) {
24636 return is_buy ? price < best : price > best;
24637 };
24638 for (const auto& working : require_host().native_working_requests()) {
24639 const auto& definition = *working.definition;
24640 const auto* trail = std::get_if<native_order::Trail>(&definition.request.trigger);
24641 const auto carried = trail_state_at_open_.find(definition.handle.incarnation);
24642 const auto found = placement_.find(definition.handle.incarnation);
24643 if (!trail || carried == trail_state_at_open_.end() || !carried->second.activated
24644 || found == placement_.end() || found->second.family != PineOrderFamily::ExitTrail
24645 || !(found->second.exit_levels.trail_offset >= 1.0)
24646 || !std::isfinite(found->second.retained_trail_best)
24647 || !std::holds_alternative<native_order::TrailTrack>(working.trigger_state)) {
24648 continue;
24649 }
24650 const double offset = trail->offset;
24651 double best = carried->second.best_price;
24652 // The replica must name the kernel's own level for the carried best,
24653 // or the walk below is not the kernel's.
24654 if (!(compat::pine::kernel_trail_stop_level(best, offset, is_buy, tick)
24655 == carried->second.current_level)) {
24656 continue;
24657 }
24658 const auto reached = [&](double price, double level, bool on_tick) {
24659 const double threshold = on_tick
24660 ? source_trigger_threshold(level, tick, is_buy, false) : level;
24661 return is_buy ? price >= threshold : price <= threshold;
24662 };
24663 // The opening print: reached on its tick, the block above took it;
24664 // reached raw, the kernel fills it there. Either way not this walk's.
24665 const double carried_level = compat::pine::kernel_trail_stop_level(best, offset, is_buy, tick);
24666 if (reached(bar.open, carried_level, true) || reached(bar.open, carried_level, false))
24667 continue;
24668 if (improves(best, bar.open)) best = bar.open;
24669 double bests[3] = {kNaN, kNaN, kNaN};
24670 int raw_point = -1;
24671 int tick_point = -1;
24672 for (int i = 0; i < 3; ++i) {
24673 bests[i] = best;
24674 const double level = compat::pine::kernel_trail_stop_level(best, offset, is_buy, tick);
24675 if (raw_point < 0 && reached(path[i], level, false)) raw_point = i;
24676 if (tick_point < 0 && reached(path[i], level, true)) tick_point = i;
24677 if (improves(best, path[i])) best = path[i];
24678 }
24679 // Only the tick-built point coming first is re-priced: none reached, or
24680 // the raw compare reaching at that point or before it, stays the kernel's.
24681 if (tick_point < 0 || (raw_point >= 0 && raw_point <= tick_point)) continue;
24682 // The kernel's compare at point i under a trailing distance d.
24683 const auto hits = [&](double distance, int i) {
24684 return reached(path[i], compat::pine::kernel_trail_stop_level(bests[i], distance, is_buy, tick), false);
24685 };
24686 const auto fires_first_at = [&](double distance) {
24687 for (int i = 0; i < 3; ++i)
24688 if (hits(distance, i)) return i;
24689 return -1;
24690 };
24691 // The widest distance the tick-built point still reaches.
24692 double retuned = is_buy ? path[tick_point] - bests[tick_point]
24693 : bests[tick_point] - path[tick_point];
24694 for (int step = 0; step < 64 && retuned > 0.0 && !hits(retuned, tick_point); ++step)
24695 retuned = std::nextafter(retuned, 0.0);
24696 for (int step = 0; step < 64; ++step) {
24697 const double wider = std::nextafter(retuned, std::numeric_limits<double>::infinity());
24698 if (!hits(wider, tick_point)) break;
24699 retuned = wider;
24700 }
24701 if (!(retuned > 0.0) || fires_first_at(retuned) != tick_point) continue;
24702 native_order::Request request = definition.request;
24703 auto retuned_trail = *trail;
24704 retuned_trail.offset = retuned;
24705 retuned_trail.ticks.reset();
24706 request.trigger = retuned_trail;
24707 native_order::ReplaceOptions keep;
24708 keep.retain_trigger_state = true;
24709 keep.keep_handle = true;
24710 keep.keep_binding = detail::carry_reissue_bindings();
24711 const auto replaced = require_host().replace(definition.handle, request, keep);
24712 if (replaced.status != native_order::ReplaceStatus::Replaced) continue;
24713 carried_trail_retunes_.push_back({definition.handle, offset});
24714 }
24715}
24716
24717// The bar is over: a trail the walk re-priced and that is still live rides
24718// its own distance again from the next bar on (retune_carried_trails_for_tick_reach).
24719void PineExecutionAdapter::restore_carried_trail_retunes() {
24720 if (carried_trail_retunes_.empty()) return;
24721 auto retunes = std::move(carried_trail_retunes_);
24722 carried_trail_retunes_.clear();
24723 for (const auto& [handle, offset] : retunes) {
24724 for (const auto& working : require_host().native_working_requests()) {
24725 const auto& definition = *working.definition;
24726 if (!(definition.handle == handle)) continue;
24727 const auto* trail = std::get_if<native_order::Trail>(&definition.request.trigger);
24728 if (!trail || trail->offset == offset) break;
24729 native_order::Request request = definition.request;
24730 auto restored = *trail;
24731 restored.offset = offset;
24732 restored.ticks.reset();
24733 request.trigger = restored;
24734 native_order::ReplaceOptions keep;
24735 keep.retain_trigger_state = true;
24736 keep.keep_handle = true;
24737 keep.keep_binding = detail::carry_reissue_bindings();
24738 (void)require_host().replace(definition.handle, request, keep);
24739 break;
24740 }
24741 }
24742}
24743
24745 const Bar& bar, const NativeDecisionContext& context) {
24746 restore_carried_trail_retunes();
24747 // TradingView calls a short carried under process_orders_on_close at the
24748 // bar's high before the script runs at its close: the script reads the
24749 // called book, and its own close fills meet what the call left (lab tv
24750 // tapes tests/fixtures/margin_v6 w5-c1-seen-size: a reversal by the size
24751 // the script reads is 53.645, not the 55.1762 held before the call;
24752 // w5-m2-pooc-carried-close / -reverse / -none: 1.5312 @1802.01, then the
24753 // close's own fill; lane W5-ENG-MARGIN-V6). A slice at this bar's open
24754 // leaves the high still to be checked, on the book that slice left
24755 // (w5-m2-pooc-short-comm: a call at the open and one at the high of the
24756 // same bar, three times). This is the commissioned or slipped short that
24757 // schedule_margin_call_path takes no path check for; a fee-free one keeps
24758 // the kernel's path check and the checkpoint after the script.
24759 if (host_ == nullptr || !modeled_input() || !config_.process_orders_on_close
24760 || config_.calc_on_order_fills
24761 || (config_.commission_value == 0.0 && config_.slippage == 0)
24762 || !finite_positive(bar.high)) {
24763 return;
24764 }
24765 if (PF_RARE(short_path_points_scope(context))) {
24766 walk_short_path_points(bar, context);
24767 return;
24768 }
24769 if (detail::run_position(require_host()).signed_units < 0.0
24770 && position_open_script_bar_ != std::numeric_limits<std::int64_t>::min()
24771 && position_open_script_bar_ != context.script_bar_open_ms) {
24772 (void)submit_margin_call_slice(bar.high, context);
24773 }
24774}
24775
24777 const Bar& bar, const NativeDecisionContext& context) {
24778 on_bar_close_policy(bar, context);
24779 if (context.is_terminal_sub_bar) {
24780 // Normal policy returns (quiet bars and margin checkpoints included)
24781 // all reach this boundary. Exceptions leave unfinished events intact.
24782 finish_admission_bar();
24783 }
24784}
24785
24786void PineExecutionAdapter::finish_admission_bar() {
24787 fold_admission_events();
24788 admission_journal.retire_prefix(admission_events_folded_,
24789 admission_events_digest_, admission_events_last_);
24790}
24791
24792void PineExecutionAdapter::on_bar_close_policy(
24793 const Bar& bar, const NativeDecisionContext& context) {
24794 // Quiet-bar gates (pine_quiet_bar.hpp), as at the opening. None fires on
24795 // an unbound adapter: its hooks still refuse through require_host().
24796 using detail::QuietHook;
24797 using detail::skip_quiet;
24798 const bool bound = host_ != nullptr;
24799 if (bound) fold_source_money();
24800 // The close-first fact lives for the script calculation that recorded
24801 // it: every entry() that reads it has run by now.
24802 close_first_script_bar_ = std::numeric_limits<std::int64_t>::min();
24803 close_first_side_ = 0;
24804 // A carried process_orders_on_close short's follow-up owed to this close
24805 // is booked now, after the script and ahead of the close's fills
24806 // (book_owed_close_follow_up).
24807 if (PF_RARE(owed_follow_up_bar_ != std::numeric_limits<std::int64_t>::min()) && bound
24808 && config_.process_orders_on_close) {
24809 book_owed_close_follow_up(bar, context);
24810 }
24811 // Quiet: the close fills are a process_orders_on_close pass outside fill
24812 // recalculation and streams, and fill only what this bar placed -- an
24813 // opening entry, or a limit or stop exit leg. "This bar" is the index
24814 // both passes test, projection_bar_index (the named input slot): on an
24815 // aggregated or magnified run it is not the script-bar interval_index,
24816 // and a gate reading that one skipped both passes there (the
24817 // order-schedule correction).
24818 const bool close_fill_pass = config_.process_orders_on_close
24819 && modeled_input() && !coof_recalc_active_;
24820 const bool entry_close_fill_pass = config_.process_orders_on_close
24821 && modeled_input() && !coof_recalc_active_;
24822 const int bar_index = projection_bar_index(context);
24823 if (!skip_quiet(QuietHook::PoocLimitEntryFills, bound && (!entry_close_fill_pass
24824 || !any_live_row(live_handles_, placement_, [&](const PlacementSnapshot& row) {
24825 return row.family == PineOrderFamily::Entry && row.opening
24826 && row.projection_created_bar == bar_index;
24827 })))) {
24828 flush_pooc_marketable_limit_entry_fills(bar, context);
24829 }
24830 if (!skip_quiet(QuietHook::PoocExitFills, bound && (!close_fill_pass
24831 || !any_live_row(live_handles_, placement_, [&](const PlacementSnapshot& row) {
24832 return (row.family == PineOrderFamily::ExitLimit
24833 || row.family == PineOrderFamily::ExitStop)
24834 && row.projection_created_bar == bar_index;
24835 })))) {
24836 flush_pooc_marketable_exit_fills(bar, context);
24837 }
24838 // Quiet: no sell was deferred at this bar's open.
24839 if (!skip_quiet(QuietHook::DeferredSellAdmission,
24840 bound && deferred_open_marketable_sells_.empty())) {
24841 admit_deferred_open_marketable_sells();
24842 }
24843 // Quiet: no flat stop entry was throttled.
24844 if (!skip_quiet(QuietHook::ThrottledReopens, bound && throttled_reopen_rearm_.empty()))
24845 rearm_throttled_reopens();
24846 // A tolerant stream can synthesize a pair-less script callback without a
24847 // separate open hook. Batch bars always pass through on_bar_open and keep
24848 // their completed arbitration observable after the run.
24849 if (last_bar_dual_entry_script_open_ms_ != context.script_bar_open_ms) {
24850 last_bar_dual_entry_path_ = 0;
24851 last_bar_dual_entry_script_open_ms_ = context.script_bar_open_ms;
24852 }
24853 // The script's orders are placed: TradingView's order at one point among
24854 // a held global exit's legs and an add beside it (order_per_entry_point_ties).
24855 if (bound) order_per_entry_point_ties();
24856 // Quiet: the terminal policy is a process_orders_on_close pass at
24857 // pyramiding 0 outside streams over two or more explicit-quantity
24858 // opening entries this bar placed.
24859 std::size_t explicit_entries = 0;
24860 if (config_.process_orders_on_close && config_.pyramiding == 0 && modeled_input()) {
24861 for (const auto& handle : live_handles_) {
24862 const auto found = placement_.find(handle.incarnation);
24863 if (found != placement_.end() && found->second.opening
24864 && found->second.family == PineOrderFamily::Entry
24865 && found->second.projection_created_bar == bar_index
24866 && finite_positive(found->second.requested_qty)) {
24867 ++explicit_entries;
24868 }
24869 }
24870 }
24871 if (!skip_quiet(QuietHook::TerminalExplicitMarket, bound && explicit_entries < 2))
24872 apply_terminal_explicit_market_policy(context);
24873 update_risk_state(bar.close);
24874 if (!modeled_input()) return;
24875 withdraw_waypoint_margin_calls();
24876 waypoint_chain_bar_ = std::numeric_limits<std::int64_t>::min();
24877 {
24878 const bool cancelled = close_margin_cancelled_bar_ == context.script_bar_open_ms;
24879 close_margin_cancelled_bar_ = std::numeric_limits<std::int64_t>::min();
24880 if (close_point_margin_scope() && close_point_margin_call(bar, context, cancelled))
24881 return;
24882 }
24883 // The native callback frame remains current after the source script
24884 // returns. Reproduce the legacy once-per-script-bar margin checkpoint at
24885 // the adverse path extreme, unless the earlier open/path policy already
24886 // applied a margin slice on this script bar.
24887 if (last_margin_call_script_bar_ == context.script_bar_open_ms) return;
24888 {
24889 auto closes = same_bar_close_alls(context);
24890 const double placed = detail::run_position(require_host()).signed_units;
24891 int fired = -1;
24892 if (submit_tv_money_long_margin_call(bar, context, &fired)) {
24893 if (fired == 3 && (!config_.process_orders_on_close
24895 size_close_alls_at_placement(closes, placed);
24896 if ((fired == 1 || fired == 2) && !closes.empty())
24897 (void)close_all_at_next_point(bar, fired, closes, context);
24898 return;
24899 }
24900 }
24901 // Quiet: the checkpoint below belongs to a commissioned full-margin short
24902 // outside process_orders_on_close, and does not apply to this run. A
24903 // carried short under process_orders_on_close was checked over the bar's
24904 // path before the script (schedule_margin_call_path), or at its high
24905 // there when commissioned or slipped (on_bar_close_before_script).
24906 if (skip_quiet(QuietHook::CloseMarginCheckpoints,
24907 bound
24908 && !(!config_.process_orders_on_close && config_.margin_short == 100.0
24909 && config_.commission_type == static_cast<int>(CommissionType::PERCENT)
24910 && config_.commission_value > 0.0))) {
24911 return;
24912 }
24913 const auto position = detail::run_position(require_host());
24914 // ab9714be pine_scheduler.cpp:262-283: in the non-POOC pass, process_margin_call
24915 // runs after invoke_chart_on_bar: a commissioned 1x short no check of the
24916 // bar called is checked once more at the bar's high here. An explicit-qty
24917 // MARKET short's entry bar is no longer deferred to this checkpoint (a
24918 // trim at the fill, then the high): TradingView checks it on the bar's
24919 // path from its opening print, as every slipped market short
24920 // (on_applied; lab tv tapes tests/fixtures/margin_open_print; lane TAIL-I).
24921 const bool non_pooc_commissioned_short = !config_.process_orders_on_close
24922 && position.signed_units < 0.0
24923 && config_.margin_short == 100.0
24924 && config_.commission_type == static_cast<int>(CommissionType::PERCENT)
24925 && config_.commission_value > 0.0;
24926 if (non_pooc_commissioned_short && finite_positive(bar.high)) {
24927 const double adverse = nearest_tick(bar.high, staged_.syminfo.mintick);
24928 (void)submit_margin_call_slice(adverse, context);
24929 }
24930 // Ordinary price-path slices are born at the native open/applied points
24931 // and matched by the generic driver at their actual waypoint. This
24932 // post-calculation checkpoint owns the source-only rounded-money policy;
24933 // replaying the full bar's adverse quote here would incorrectly give a
24934 // close-time position access to prices it did not yet exist through.
24935}
24936
24937namespace {
24938// A strategy.exit leg already marketable at the open's tick: a short's stop
24939// at or below it or its limit at or above it, a long's mirror.
24940bool exit_leg_marketable_at(const PineExitLevels& legs, bool short_book, double open_tick) noexcept {
24941 if (std::isfinite(legs.stop) && (short_book ? open_tick >= legs.stop : open_tick <= legs.stop))
24942 return true;
24943 return std::isfinite(legs.limit)
24944 && (short_book ? open_tick <= legs.limit : open_tick >= legs.limit);
24945}
24946} // namespace
24947
24948// An exit leg (request `leg`) the fill of the entry request `opening` (id
24949// `id`) released onto its new book: bound to that request, bound to the id
24950// and materialized at that fill on the book's side, or a strategy.exit
24951// without from_entry placed while the book was flat after that entry, which
24952// the kernel reaches after the entry's fill at one point and so closes the
24953// book it opens. TradingView orders such a global exit at the open as a bound
24954// one: a short's call before its stop on the path, a long's open-marketable
24955// stop before any call (lab tv tapes tests/fixtures/margin_open_rules
24956// exit-order/global-*). A leg left from an earlier lot of the id -- another
24957// side's, or one placed before this fill's bar -- is not, nor is a global
24958// exit placed before the entry, which the open reaches while still flat.
24959bool PineExecutionAdapter::opening_released_leg(
24960 const PlacementSnapshot& row, std::uint64_t leg, std::uint64_t opening,
24961 const SourceId& id, bool short_book, const NativeDecisionContext& context) const {
24962 if (row.bracket_origin.incarnation == opening) return true;
24963 if (row.bracket_origin.incarnation != 0) return false;
24964 if (row.from_entry.empty())
24965 return leg > opening
24966 && row.projection_position_side == static_cast<std::int32_t>(PositionSide::FLAT);
24967 return row.from_entry == id
24968 && row.projection_created_bar == projection_bar_index(context)
24969 && row.projection_position_side
24970 == static_cast<std::int32_t>(short_book ? PositionSide::SHORT : PositionSide::LONG);
24971}
24972
24973// Whether an exit leg of the book fills ahead of the opening checkpoint
24974// (on_applied): a strategy.exit stop or limit placed by this bar whose
24975// from_entry is in the live cycle. ab9714be pine_fills.cpp:2150-2169: a 1x
24976// short gives way to any leg its bar touches. The legs a market entry's fill
24977// at the bar's open releases (opening_released_leg), a long's as a short's,
24978// precede the check only when they are already marketable at that open -- a
24979// short's stop at or below the open's tick or its limit at or above it, a
24980// long's mirror -- and the check's call is booked at the fill ahead of a leg
24981// the bar reaches only after the open, one tick after it included (lab tv
24982// tapes tests/fixtures/margin_open_rules exit-order/: a short of 3.5 ETH
24983// filled off the 2483.19 open calls 0.0048 there before its 2485.69 stop
24984// fills 3.4952, and calls nothing before a stop 3 ticks below the open; lane
24985// PX-PIN-16B); `at_open` says such a leg precedes it. The book's other legs,
24986// which may be bound to an earlier lot of the id, keep ab9714be's rule.
24987// `filled`, an exit leg filling now, does not count, nor do the other legs of
24988// its strategy.exit call, and with it only the released legs are asked.
24989bool PineExecutionAdapter::priced_exit_precedes_opening(
24990 bool short_book, std::uint64_t opening, const SourceId& opening_id,
24991 const NativeDecisionContext& context, const PlacementSnapshot* filled,
24992 bool* at_open) const {
24993 const double open_tick = opening != 0
24994 ? nearest_tick(policy_script_bar_.open, staged_.syminfo.mintick) : kNaN;
24995 bool marketable_at_open = false;
24996 for (const auto& handle : live_handles_) {
24997 const auto found = placement_.find(handle.incarnation);
24998 if (found == placement_.end()) continue;
24999 const auto& row = found->second;
25000 if (row.family != PineOrderFamily::ExitLimit
25001 && row.family != PineOrderFamily::ExitStop) {
25002 continue;
25003 }
25004 if (filled && row.command_sequence == filled->command_sequence
25005 && row.bracket_origin == filled->bracket_origin) {
25006 continue;
25007 }
25008 // A void global exit's leg is no leg for TradingView: it never
25009 // precedes the check, released or touched (void_global_issue).
25010 if (row.void_global_issue) continue;
25011 if (row.projection_created_bar < 0
25012 || row.projection_created_bar > projection_bar_index(context)) {
25013 continue;
25014 }
25015 // ab9714be pine_fills.cpp:7671-7674 removes an exit
25016 // whose from_entry has not filled in the live cycle
25017 // (the long a direct reversal just closed), so it
25018 // cannot fill and must not preempt the opening
25019 // checkpoint (pine_fills.cpp:6069-6102, 1386-1482).
25020 if (!row.from_entry.empty()
25021 && !from_entry_filled_this_cycle(row.from_entry)) {
25022 continue;
25023 }
25024 if (opening != 0
25025 && opening_released_leg(row, handle.incarnation, opening, opening_id, short_book,
25026 context)) {
25027 marketable_at_open = marketable_at_open
25028 || exit_leg_marketable_at(row.exit_levels, short_book, open_tick);
25029 continue;
25030 }
25031 if (filled || !short_book) continue;
25032 if ((std::isfinite(row.exit_levels.stop)
25033 && policy_script_bar_.high >= row.exit_levels.stop)
25034 || (std::isfinite(row.exit_levels.limit)
25035 && policy_script_bar_.low <= row.exit_levels.limit)) {
25036 if (at_open) *at_open = false;
25037 return true;
25038 }
25039 }
25040 if (at_open) *at_open = marketable_at_open;
25041 return marketable_at_open;
25042}
25043
25044// The opening checkpoint at the fill that opened or grew a full-margin book
25045// (on_applied), at `fill`, the entry's own fill price: the call it takes at
25046// the fill's print. A market entry filling at a bar's open runs it after the
25047// exit legs marketable at that open have filled there (open_exit_first_bar_),
25048// on the book they leave.
25049void PineExecutionAdapter::run_opening_checkpoint(
25050 double fill, bool market, bool sibling_book, bool adds_book_last,
25051 const NativeDecisionContext& context) {
25052 const auto opened_position = detail::run_position(require_host());
25053 const double opening_exact_required = std::abs(opened_position.signed_units) * fill
25054 * staged_.syminfo.pointvalue * active_staged_fx(context.sub_bar_open_ms);
25055 const double opening_equity = require_host().native_marked_equity(fill);
25056 const bool defer_slipped_pooc_rounding =
25057 slipped_pooc_opening_money_scope(policy_script_bar_, context)
25058 && std::isfinite(opening_exact_required)
25059 && std::isfinite(opening_equity)
25060 && opening_equity >= opening_exact_required
25061 && opening_equity < source_money_round(opening_exact_required);
25062 // pine_fills.cpp:1441-1450 at ab9714be leaves this exact-funded
25063 // terminal POOC residual intact until the next opening print.
25064 // Genuine opening deficits retain the immediate checkpoint.
25065 // TradingView sizes a slipped market short's opening call at
25066 // the print its fill slipped from, and executes it there
25067 // with the call's own slippage (lab tv tapes
25068 // tests/fixtures/margin_v6 w5-qp-mkt-once and -reissue:
25069 // 0.0028 @1611.08 off the 1611.07 open that a 1611.06 fill
25070 // slipped from, none at the fill; lane W5-ENG-MARGIN-V6).
25071 const bool slipped_market_short = opened_position.signed_units < 0.0
25072 && config_.slippage != 0 && !config_.process_orders_on_close && market;
25073 const bool cash_contract_market_opening = config_.commission_value > 0.0
25074 && config_.commission_type == static_cast<int>(CommissionType::CASH_PER_CONTRACT)
25075 && !config_.process_orders_on_close && market;
25076 if (!defer_slipped_pooc_rounding && cash_contract_market_opening) {
25077 const double print = source_bar_fill_tick(
25078 fill - (opened_position.signed_units > 0.0 ? 1.0 : -1.0)
25079 * config_.slippage * staged_.syminfo.mintick,
25080 staged_.syminfo.mintick);
25081 const double units = source_margin_units(
25082 source_margin_money(print, context.sub_bar_open_ms), false);
25083 (void)submit_margin_call_units(print, context, units, true);
25084 } else if (!defer_slipped_pooc_rounding && slipped_market_short) {
25085 const double print = source_bar_fill_tick(
25086 fill + config_.slippage * staged_.syminfo.mintick,
25087 staged_.syminfo.mintick);
25088 const double held = std::abs(detail::run_position(require_host()).signed_units);
25089 if (submit_margin_call_slice(print, context)) {
25090 follow_margin_call(
25091 held - std::abs(detail::run_position(require_host()).signed_units),
25092 source_margin_fill_price(print, true), fill, context);
25093 }
25094 } else if (!defer_slipped_pooc_rounding) {
25095 // A sibling book's call can take a one-unit follow-up at
25096 // the bar's next path point (whole_unit_follow_up_due).
25097 const bool follow_up = sibling_book
25098 && whole_unit_follow_up_due(source_margin_units(source_margin_money(
25099 fill, context.sub_bar_open_ms), true), fill);
25100 // The last of a bar's adds on a long: the call, and the
25101 // broker's follow-up after it (lagged_margin_follow_up_units),
25102 // are sized on the print the adds' fill slipped from, the
25103 // call executed there with its own slippage and the
25104 // follow-up taken at the bar's next path point. At margins
25105 // of 100 the book's shortfall is the same at any mark, so
25106 // the print decides the call only through its divisor: a
25107 // synthetic shortfall of 30.005 is 1.2002 lots of ETH over the
25108 // 25.00 print and 1.1992406... over the 25.02 fill slipped
25109 // from it. On a 0.0001 grid, the call is 4.8008, four times the
25110 // first lot floor, rather than 4.7968 from the slipped divisor
25111 // (int30-c4-grid-pk-mkt; lot floor before multiplying by four).
25112 const bool adds_book = adds_book_last && opened_position.signed_units > 0.0;
25113 const double print = source_bar_fill_tick(
25114 fill - config_.slippage * staged_.syminfo.mintick,
25115 staged_.syminfo.mintick);
25116 const auto print_money = adds_book
25117 ? source_margin_money(print, context.sub_bar_open_ms) : SourceMarginMoney{};
25118 const auto lots_before = adds_book
25119 ? require_host().native_open_lots(print) : std::vector<NativeOpenLot>{};
25120 const bool called = adds_book && config_.slippage != 0
25121 ? submit_margin_call_slice(print, context)
25122 : submit_margin_call_slice(fill, context, true);
25123 const double next = next_source_path_waypoint(
25124 policy_script_bar_, context.coordinate.path_phase, fill,
25125 source_path_uses_high_first(policy_script_bar_));
25126 if (called && follow_up && finite_positive(next))
25127 (void)submit_margin_call_units(next, context, 1.0, true);
25128 if (called && adds_book && finite_positive(next)) {
25129 const double taken = opened_position.signed_units
25130 - detail::run_position(require_host()).signed_units;
25131 const double more = lagged_margin_follow_up_units(
25132 lots_before, print_money, taken, source_margin_fill_price(print, false),
25133 context.sub_bar_open_ms);
25134 if (more > 0.0) (void)submit_margin_call_units(next, context, more, true);
25135 }
25136 }
25137}
25138
25140 const NativeDecisionContext& context) {
25141 retired_row_scratch_.roster_pass_due = true;
25142 fold_source_money();
25143 // Nested reissues see new rows now, before the live maps' ordinary debit.
25144 if constexpr (PINEFORGE_POSITION_TAIL_DIAGNOSTICS::enabled) {
25145 if (position_tail_summary_) {
25146 auto& work = position_tail_summary_->work;
25147 const auto before = work.update_rows;
25148 ++work.reissue_updates;
25149 observe_position_tail_rows();
25150 work.reissue_rows += work.update_rows - before;
25151 } else {
25152 observe_position_tail_rows();
25153 }
25154 } else {
25155 observe_position_tail_rows();
25156 }
25157 // materialize_relative_exits can submit new legs. A submission inserts
25158 // into placement_ and may rehash it, so no reference or iterator into
25159 // placement_ may survive that call.
25160 std::optional<PlacementSnapshot> placement_snapshot;
25161 if (const auto placement = placement_.find(event.handle().incarnation);
25162 placement != placement_.end()) {
25163 placement_snapshot = placement->second;
25164 } else if (event.definition
25165 && event.definition->origin
25166 == native_order::RequestOrigin::KernelLiquidation) {
25167 // R5: the kernel originates its own liquidation, so there is no
25168 // submit through submit_or_replace to record one. Adopt it into the
25169 // source placement table on arrival, with the same family the
25170 // adapter's own slice carried, so every Margin-family branch below --
25171 // the pending-sizing refresh, the bracket revival -- sees it exactly
25172 // as before.
25173 PlacementSnapshot adopted;
25175 adopted.source_id = "__margin_call__";
25176 adopted.requested_qty = event.closed_units;
25177 adopted.forced_execution_price = event.resolved_price;
25178 adopted.sizing = sizing_snapshot();
25179 placement_.try_emplace(event.handle().incarnation, adopted); invalidate_retirement();
25180 placement_snapshot = adopted;
25181 }
25182 if (placement_snapshot && event.opened_units != 0.0
25183 && (placement_snapshot->family == PineOrderFamily::Entry
25184 || placement_snapshot->family == PineOrderFamily::Order)) {
25185 note_exit_rank_key(placement_snapshot->source_id);
25186 }
25187 if (placement_snapshot && event.closed_trade_count > 0) {
25188 for (std::size_t i = 0; i < event.closed_trade_count; ++i) {
25189 const std::size_t index = event.first_trade_index + i;
25190 set_exit_phase(index, static_cast<std::uint8_t>(context.coordinate.path_phase));
25191 }
25192 const bool from_bracket =
25193 placement_snapshot->family == PineOrderFamily::ExitLimit
25194 || placement_snapshot->family == PineOrderFamily::ExitStop
25195 || placement_snapshot->family == PineOrderFamily::ExitTrail;
25196 if (auto* pine_host = pine_view_of(&require_host())) {
25197 pine_host->adapter_label_bracket_trades(event, from_bracket);
25198 }
25199 }
25200 if (placement_snapshot && placement_snapshot->opening) {
25202 for (const auto& handle : live_handles_) {
25203 if (handle == event.handle()) continue;
25204 const auto pending = placement_.find(handle.incarnation);
25205 if (pending != placement_.end()
25206 && pending->second.paired_reversal_parent == event.handle()) {
25207 paired_closes.push_back(handle);
25208 }
25209 }
25210 for (const auto& handle : paired_closes) {
25211 const auto result = require_host().cancel(handle);
25212 if (result.status != native_order::CancelStatus::Cancelled) continue;
25213 if (const auto pending = placement_.find(handle.incarnation);
25214 pending != placement_.end()) {
25215 pending->second.cancellation = {
25217 event.handle().incarnation,
25218 static_cast<std::int64_t>(placement_snapshot->source_sequence),
25219 handle.incarnation, pending->second.placement_cycle,
25220 pending->second.legs.revision(),
25221 pending->second.requested_qty, kNaN};
25222 }
25223 retire(handle);
25224 }
25225 }
25226 if (placement_snapshot && event.closed_units > 0.0
25227 && detail::run_position(require_host()).signed_units == 0.0
25228 && (placement_snapshot->family == PineOrderFamily::Close
25229 || placement_snapshot->family == PineOrderFamily::CloseAll
25230 || placement_snapshot->family == PineOrderFamily::Order)) {
25231 const auto closed_side = static_cast<PositionSide>(
25232 placement_snapshot->projection_position_side);
25234 for (const auto& handle : live_handles_) {
25235 if (handle == event.handle()) continue;
25236 const auto found = placement_.find(handle.incarnation);
25237 if (found == placement_.end()) continue;
25238 const auto& pending = found->second;
25239 if (!pending.opening || pending.family != PineOrderFamily::Entry
25240 || pending.is_long != (closed_side == PositionSide::LONG)
25241 || pending.projection_position_side
25242 != static_cast<std::int32_t>(closed_side)) {
25243 continue;
25244 }
25245 const bool resting_limit =
25246 pending.projection_created_bar < projection_bar_index(context)
25247 && finite_positive(pending.exit_levels.limit)
25248 && !finite_positive(pending.exit_levels.stop);
25249 // Under process_orders_on_close a stop entry resting from an
25250 // earlier bar survives the flat as a limit does: TradingView
25251 // fills it later and binds its exit (tests/fixtures/exit_binding
25252 // stop-entry-parent).
25253 const bool resting_stop =
25255 && config_.process_orders_on_close && !config_.calc_on_order_fills
25256 && margins_disabled()
25257 && pending.projection_created_bar < projection_bar_index(context)
25258 && finite_positive(pending.exit_levels.stop);
25259 const bool coqueued_within_cap =
25260 pending.projection_created_bar
25261 == placement_snapshot->projection_created_bar
25262 && (!pending.projection_over_pyramiding
25263 || (placement_snapshot->close_batch_calls != 0
25264 && (finite_positive(pending.exit_levels.limit)
25265 || finite_positive(pending.exit_levels.stop))));
25266 const bool preserved_stop =
25267 placement_snapshot->family == PineOrderFamily::CloseAll
25268 && pending.preserved_by_close_all == event.handle()
25269 && pending.preserved_close_all_bar
25270 == placement_snapshot->projection_created_bar;
25271 const bool frozen_over_cap_transaction = pending.frozen_market_instruction
25272 && pending.projection_over_pyramiding;
25273 if (!resting_limit && !resting_stop && !coqueued_within_cap && !preserved_stop
25274 && !frozen_over_cap_transaction) {
25275 stale_entries.push_back(handle);
25276 }
25277 }
25278 for (const auto& handle : stale_entries) {
25279 const auto result = require_host().cancel(handle);
25280 if (result.status != native_order::CancelStatus::Cancelled) continue;
25281 if (const auto found = placement_.find(handle.incarnation);
25282 found != placement_.end()) {
25283 found->second.cancellation = {
25285 event.handle().incarnation,
25286 static_cast<std::int64_t>(placement_snapshot->source_sequence),
25287 handle.incarnation, found->second.placement_cycle,
25288 found->second.legs.revision(), found->second.requested_qty, kNaN};
25289 }
25290 retire(handle);
25291 }
25292 }
25293 if (placement_snapshot
25294 && (placement_snapshot->family == PineOrderFamily::Close
25295 || placement_snapshot->family == PineOrderFamily::CloseAll)
25296 && event.closed_units > 0.0) {
25297 // ab9714be pine_fills.cpp:7461-7468: the close that flattened the book
25298 // is bound to the position cycle it was issued in. Every exit created
25299 // while that position was open is Removed at the flat, before the
25300 // paired reversal entry of the NEXT cycle is released below, so the
25301 // stale bracket can neither fill against the fresh lot on this bar nor
25302 // be revived against a later reuse of the entry id
25303 // (pine_fills.cpp:7667-7675). Only a bracket the script placed behind
25304 // this same close, for the close's own paired reversal, is reached by
25305 // the legacy queue walk after that entry filled and stays armed.
25306 if (detail::run_position(require_host()).signed_units == 0.0)
25307 retire_in_position_exits_at_flat(/*preserve_pending_parents=*/true,
25308 /*dormant_rows_only=*/false,
25309 &*placement_snapshot);
25310 detail::ExitScratchRows<std::uint64_t> after_close(*this);
25311 auto& versions = detail::exit_scratch_versions(after_close.context());
25312 std::size_t remaining = 0;
25313 for (std::size_t index = 0; index < pending_entries_.size(); ++index) {
25314 auto& entry = pending_entries_[index];
25315 if (entry.snapshot.paired_reversal_parent == event.handle()) {
25316 after_close.push_back(versions.pending(after_close.scope(), std::as_const(entry.request),
25317 std::as_const(entry.snapshot),
25318 std::as_const(entry.replacement_key)));
25319 } else {
25320 if (remaining != index) pending_write(pending_entries_[remaining]) = std::move(entry);
25321 ++remaining;
25322 }
25323 }
25324 pending_write(pending_entries_).resize(remaining);
25325 for (const auto version : after_close) {
25326 PendingEntry entry;
25327 std::as_const(versions).pending(version, entry.request, entry.snapshot, entry.replacement_key);
25328 entry.snapshot.paired_reversal_parent = {};
25329 entry.snapshot.market_admission = {};
25330 entry.request.owner = native_order::Independent{};
25331 // ab9714be pine_fills.cpp:8013-8018: a same-pass close flattens
25332 // first. A following opposite stop that the close's fill has not
25333 // gapped through stays resting and fills at the stop level on
25334 // the remaining path, not at the close's open print.
25335 const bool priced_stop = finite_positive(entry.snapshot.exit_levels.stop)
25336 && !finite_positive(entry.snapshot.exit_levels.limit)
25337 && !finite_positive(entry.snapshot.exit_levels.trail_points)
25338 && !finite_positive(entry.snapshot.exit_levels.trail_price)
25339 && !finite_positive(entry.snapshot.exit_levels.trail_offset);
25340 // A close the bar's own calculation executed (immediately = true)
25341 // fills at the bar's close, where nothing trades after it: the
25342 // entry the pass sent behind it waits for the next bar's open, as
25343 // it does when the close's fill is known before the entry is
25344 // sent (lab tv tapes te-coof-immediate-reentry-*,
25345 // tests/fixtures/coof_immediate_close; lane TAIL-E).
25346 const bool calculation_close = placement_snapshot->immediately
25347 && context.coordinate.provenance == NativePriceProvenance::Calculation;
25348 const bool marketable_now = !calculation_close
25349 && (!priced_stop
25350 || pure_stop_entry_marketable_at(
25351 entry.snapshot, event.resolved_price));
25352 if (marketable_now) {
25353 entry.snapshot.forced_execution_price = event.resolved_price;
25354 // ab9714be pine_fills.cpp:3848-3856 orders the full close
25355 // first, but each market fill still books its own side's
25356 // slippage off the shared open print (engine.hpp:1207-1210).
25357 // A released entry on the closed position's own side is a
25358 // buy after a sell (or the reverse), so it cannot reuse the
25359 // close's slipped price.
25360 const auto closed_side = static_cast<PositionSide>(
25361 placement_snapshot->projection_position_side);
25362 if (!priced_stop && config_.slippage != 0
25363 && closed_side != PositionSide::FLAT
25364 && entry.snapshot.is_long == (closed_side == PositionSide::LONG)) {
25365 entry.snapshot.forced_execution_price =
25366 nearest_tick(event.raw_price, staged_.syminfo.mintick)
25367 + (entry.snapshot.is_long ? 1.0 : -1.0) * config_.slippage
25368 * staged_.syminfo.mintick;
25369 }
25370 }
25371 native_order::RequestHandle prior_flip_stop{};
25372 const bool self_touched = priced_stop && policy_script_bar_valid_
25373 && (entry.snapshot.is_long
25374 ? policy_script_bar_.high >= entry.snapshot.exit_levels.stop
25375 : policy_script_bar_.low <= entry.snapshot.exit_levels.stop);
25376 if (priced_stop && !marketable_now && self_touched) {
25377 std::uint64_t prior_seq = std::numeric_limits<std::uint64_t>::max();
25378 for (const auto& handle : live_handles_) {
25379 const auto found = placement_.find(handle.incarnation);
25380 if (found == placement_.end()) continue;
25381 const auto& prior = found->second;
25382 if (!prior.opening || prior.family != PineOrderFamily::Entry
25383 || prior.is_long != entry.snapshot.is_long
25384 || !finite_positive(prior.exit_levels.stop)
25385 || finite_positive(prior.exit_levels.limit)
25386 || prior.command_sequence >= entry.snapshot.command_sequence) {
25387 continue;
25388 }
25389 const auto created = static_cast<PositionSide>(
25390 prior.projection_position_side);
25391 if (created == PositionSide::FLAT
25392 || (created == PositionSide::LONG) == prior.is_long) {
25393 continue;
25394 }
25395 const bool prior_touched = prior.is_long
25396 ? policy_script_bar_.high >= prior.exit_levels.stop
25397 : policy_script_bar_.low <= prior.exit_levels.stop;
25398 if (!prior_touched || prior.command_sequence >= prior_seq) continue;
25399 prior_seq = prior.command_sequence;
25400 prior_flip_stop = handle;
25401 }
25402 }
25403 if (prior_flip_stop.incarnation != 0) {
25404 const double units = finite_positive(entry.snapshot.requested_qty)
25405 ? std::abs(entry.snapshot.requested_qty)
25406 : (finite_positive(entry.snapshot.sizing.frozen_units)
25407 ? entry.snapshot.sizing.frozen_units
25408 : std::abs(config_.default_qty_value));
25409 entry.request.intent = native_order::Transact{
25410 entry.snapshot.is_long ? units : -units};
25411 entry.request.owner = native_order::WaitForApplied{prior_flip_stop};
25412 }
25413 const auto accepted = submit_or_replace(
25414 std::move(entry.request), std::move(entry.snapshot), true,
25415 entry.replacement_key);
25416 if (accepted && marketable_now && prior_flip_stop.incarnation == 0) {
25417 const auto outcome = require_host().execute_current(
25418 {*accepted, NativeCurrentPriceRule::NearestTick});
25419 (void)outcome;
25420 }
25421 }
25422 }
25423 bool preclose_intraday_loss = false;
25424 // A fill at the open point that captured the day's start equity closes an
25425 // order carried from the previous day: its P&L is already in that equity,
25426 // so TradingView does not count it as the day's loss, market or priced
25427 // (lab tv tape tests/fixtures/intraday_loss_open_fill; lane W8A-SIGSTATE-1
25428 // R4). An intrabar fill keeps the own-fill exclusion
25429 // (test_risk_max_intraday_loss_tv t1).
25430 const bool day_open_fill = context.coordinate.path_phase == NativePathPhase::Open
25431 && context.sub_index == 0
25432 && context.script_bar_open_ms == day_ledger_.intraday_start_script_bar;
25433 if (event.closed_trade_count > 0 && risk_.max_intraday_loss > 0.0
25434 && !intraday_loss_orders_blocked() && !day_open_fill
25435 && std::isfinite(day_ledger_.intraday_start_equity)) {
25436 double closed_pnl = 0.0;
25437 const auto& host = require_host();
25438 for (std::size_t i = 0; i < event.closed_trade_count; ++i) {
25439 const auto index = event.first_trade_index + i;
25440 if (index < static_cast<std::size_t>(host.trade_count()))
25441 closed_pnl += host.get_trade(static_cast<int>(index)).pnl;
25442 }
25443 const double after = host.native_marked_equity(event.resolved_price);
25444 const double before = after - closed_pnl;
25445 const double loss = day_ledger_.intraday_start_equity - before;
25446 const double threshold = risk_.max_intraday_loss_percent
25447 ? day_ledger_.intraday_start_equity * risk_.max_intraday_loss / 100.0
25448 : risk_.max_intraday_loss;
25449 if (std::isfinite(risk_.max_intraday_loss) && !std::isfinite(threshold)) {
25450 engine_command_value_invalid_or_count("risk.threshold",
25451 EngineValueStage::risk, EngineValueReason::numeric_overflow);
25452 }
25453 const double epsilon = 1e-9 * std::max(1.0, std::abs(threshold));
25454 preclose_intraday_loss = std::isfinite(before) && threshold > 0.0
25455 && loss > 0.0 && loss + epsilon >= threshold;
25456 }
25457 last_applied_ordinal_ = event.ordinal;
25458 if (placement_snapshot && placement_snapshot->family == PineOrderFamily::Entry
25459 && std::abs(event.opened_units) > 0.0) {
25460 entry_openings_this_interval_ += 1;
25461 if (!throttled_reopen_rearm_.empty())
25462 rearm_throttled_reopens();
25463 }
25464 const double live_position = detail::run_position(require_host()).signed_units;
25465 const int next_sign = live_position > 0.0 ? 1 : (live_position < 0.0 ? -1 : 0);
25466 const bool flipped_position = current_position_sign_ != 0 && next_sign != 0
25467 && current_position_sign_ != next_sign;
25468 if (flipped_position && event.closed_units > 0.0) {
25469 // A reversal retires the complete prior physical side before opening
25470 // the new one. The generic settlement event owns that fact; mirror
25471 // it into every source cohort so a later reuse of an old entry id
25472 // cannot reserve against already-closed exposure.
25473 std::vector<SourceId> preserved_cohorts;
25474 if (placement_snapshot) preserved_cohorts.push_back(placement_snapshot->source_id);
25475 for (const auto& handle : live_handles_) {
25476 const auto pending = placement_.find(handle.incarnation);
25477 if (pending != placement_.end()
25478 && pending->second.opening
25479 && pending->second.family == PineOrderFamily::Entry
25480 && pending->second.is_long == (live_position > 0.0)) {
25481 preserved_cohorts.push_back(pending->second.source_id);
25482 }
25483 }
25484 std::sort(preserved_cohorts.begin(), preserved_cohorts.end());
25485 // An empty, spilled cohort can still own queued exits. Membership uses
25486 // the logical table, not residency/exposure; the helper keeps the old
25487 // sorted owner order and exact successful scratch allocation.
25488 detail::ExitScratchRows<SourceId> closed_cohorts(*this);
25489 cancel_reversal_exit_owners(preserved_cohorts, closed_cohorts);
25490 // Selection handles are reserved before cancellation, exactly where
25491 // the full text selection used to be built. Its scope remains alive
25492 // through the remaining reversal cleanup below.
25493 //
25494 // The helper scans queued owners once. Debug checks every skipped id
25495 // against the old canceler's per-store predicates; Release never
25496 // walks historical ids here. Scratch failure status is propagated by
25497 // the same transaction/error boundary as the old selected row puts.
25498 // Empty bracket families still receive one cleanup pass per close.
25499 if (position_tail_summary_) position_tail_summary_->forget_origins();
25500 const auto held = cohorts_by_id_.held_rows();
25501 for (auto at = held.begin(); at != held.end();) {
25502 const auto current = at++;
25503 auto& cohort = *current;
25504 assert(cohort_opened_slot_count_ >= cohort.second.opened.size());
25505 cohort_opened_slot_count_ -= cohort.second.opened.size();
25506 ::pineforge::detail::clear_exposure_storage(cohort.second.opened);
25507 ::pineforge::detail::clear_exposure_storage(cohort.second.live_units_by_origin);
25508 cohorts_by_id_.refresh_held(current);
25509 }
25510 // ab9714be pine_strategy_host.cpp:244 / :261
25511 // reset_source_open_position_ledgers_before_book clears the close
25512 // ledgers when a position is opened or reversed, so unclosed units of
25513 // the prior position side cannot survive to admit a later close of that
25514 // side against the new position.
25515 close_logical_units_.clear();
25516 close_ledger_records_.clear();
25517 pyramiding_records_.clear();
25518 exit_tombstones_.clear();
25519 }
25520 if (next_sign != 0 && (current_position_sign_ == 0 || current_position_sign_ != next_sign)) {
25521 ++current_position_cycle_;
25522 position_tail_spill_.reset();
25523 position_open_script_bar_ = context.script_bar_open_ms;
25524 begin_position_tail_summary();
25525 position_open_epoch_ = broker_open_epoch_;
25526 position_open_bar_index_ = context.coordinate.interval_index;
25527 position_open_phase_ = context.coordinate.path_phase;
25528 position_open_priced_ = placement_snapshot
25529 && (finite_positive(placement_snapshot->exit_levels.limit)
25530 || finite_positive(placement_snapshot->exit_levels.stop)
25531 || placement_snapshot->family == PineOrderFamily::Order);
25532 }
25533 current_position_sign_ = next_sign;
25534 per_entry_exits_after_fill(placement_snapshot, event);
25535 if (placement_snapshot
25536 && (placement_snapshot->family == PineOrderFamily::ExitLimit
25537 || placement_snapshot->family == PineOrderFamily::ExitStop
25538 || placement_snapshot->family == PineOrderFamily::ExitTrail)
25539 && event.closed_units > 0.0 && live_position != 0.0
25540 // ab9714be pine_orders.cpp:330-337: execute_partial_exit_qty recognizes explicit requested quantity reductions
25541 && ((std::isfinite(placement_snapshot->requested_qty) && placement_snapshot->requested_qty > 0.0)
25542 || (std::isfinite(placement_snapshot->qty_percent) && placement_snapshot->qty_percent < 100.0 - 1e-9))) {
25543 // The exit tombstones (exit_child_tombstones): a partial strategy.exit
25544 // child that filled is never revived for the entry incarnation it
25545 // filled under -- the newest opening of its from_entry then -- by a
25546 // later call of the same exit or by an older leg of it, under
25547 // process_orders_on_close too, where the re-call at the fill bar's
25548 // close is marketable (tests/fixtures/admission_rules
25549 // c5b-pooc-tp-tombstone-marketable: the engine had re-bound TP1
25550 // for the unreserved two shares). An exit armed for a later opening
25551 // of the id fills as before (test_exit_bracket_pending_entry_leg).
25552 if (detail::margin_rule_switches().exit_child_tombstones
25553 && !placement_snapshot->from_entry.empty() && pinned_lot_grid()) {
25554 const std::uint64_t origin = entry_incarnation_of(placement_snapshot->from_entry);
25555 if (origin != 0)
25556 exit_tombstones_[placement_snapshot->source_id + "\x1f"
25557 + placement_snapshot->from_entry] = origin;
25558 }
25559 // ab9714be pine_fills.cpp:7012-7024: other pending orders with this
25560 // id count as siblings; same-bracket legs are one owner order.
25561 if (!partial_exit_sibling_live(*placement_snapshot, event.handle())) {
25562 consumed_partial_exit_cycles_.insert_or_assign(
25563 placement_snapshot->source_id + "\x1f" + placement_snapshot->from_entry,
25564 current_position_cycle_);
25565 }
25566 }
25567 if (placement_snapshot && placement_snapshot->family == PineOrderFamily::Order
25568 && placement_snapshot->oca_type == 1 && !placement_snapshot->oca_name.empty()) {
25569 const bool fully_filled = !std::isfinite(placement_snapshot->requested_qty)
25570 || event.filled_working >= placement_snapshot->requested_qty;
25571 if (fully_filled) {
25573 for (const auto& handle : live_handles_) {
25574 if (handle == event.handle()) continue;
25575 const auto peer = placement_.find(handle.incarnation);
25576 if (peer != placement_.end()
25577 && (peer->second.family == PineOrderFamily::Order
25578 || peer->second.family == PineOrderFamily::Entry)
25579 && peer->second.oca_type == 1
25580 && peer->second.oca_name == placement_snapshot->oca_name) {
25581 siblings.push_back(handle);
25582 }
25583 }
25584 for (const auto& sibling : siblings) {
25585 const auto result = require_host().cancel(sibling);
25586 if (result.status == native_order::CancelStatus::Cancelled) retire(sibling);
25587 }
25588 }
25589 }
25590 // The over-fill lot a frozen strategy.close(id) or close_all() opened as it
25591 // crossed a carried short to a long (book_owed_close_follow_up) is a lot of
25592 // its own in the close ledger and the pyramiding records, under the close
25593 // order's label and ahead of the entry beside it: the nameless fills that
25594 // take it first in first out -- the next open's margin calls -- book it,
25595 // and a later strategy.close of that entry closes all of the entry's units
25596 // (switch close_follow_up_after_script; synthetic OANDA:EURUSD 15m:
25597 // strategy.close("LONG") closes 40000.00 and the entry "SHORT" opens
25598 // 40100.00). No script id names it, so it has no cohort.
25599 if (PF_RARE(placement_snapshot && placement_snapshot->crosses_zero
25600 && !placement_snapshot->opening)
25601 && (placement_snapshot->family == PineOrderFamily::Close
25602 || placement_snapshot->family == PineOrderFamily::CloseAll)
25603 && std::abs(event.opened_units) > 0.0 && live_position > 0.0
25604 && config_.process_orders_on_close
25605 && detail::margin_schedule_switches().close_follow_up_after_script) {
25606 const SourceId lot = placement_snapshot->family == PineOrderFamily::Close
25607 ? "__close__" + placement_snapshot->source_id : placement_snapshot->source_id;
25608 credit_close_ledger(lot, std::abs(event.opened_units));
25609 credit_pyramiding_record(lot, event.handle().incarnation, std::abs(event.opened_units));
25610 }
25611 if (placement_snapshot && placement_snapshot->opening
25612 && std::abs(event.opened_units) > 0.0) {
25613 withdraw_void_exits(placement_snapshot->source_id);
25614 // A market entry is one the script gave neither a limit nor a stop:
25615 // a limit entry marketable at a process_orders_on_close close fills
25616 // through a market request and is a limit entry here.
25617 settle_void_global_exits(!finite_positive(placement_snapshot->exit_levels.limit)
25618 && !finite_positive(placement_snapshot->exit_levels.stop));
25619 bind_pending_exits_to_fill(placement_snapshot->source_id);
25620 // Explicit brackets armed while their same-id parent was still flat
25621 // use origin zero as a temporary source binding. Once that parent
25622 // applies, bind those live legs to its actual incarnation and move
25623 // their replacement keys with it. A later reissue must replace the
25624 // carried legs before adding legs for another pending instance
25625 // (ab9714be:test_exit_bracket_pending_entry_leg.cpp:10-18).
25626 for (const auto& handle : live_handles_) {
25627 auto child = placement_.find(handle.incarnation);
25628 if (child == placement_.end()) continue;
25629 auto& snapshot = child->second;
25630 const bool exit = snapshot.family == PineOrderFamily::ExitLimit
25631 || snapshot.family == PineOrderFamily::ExitStop
25632 || snapshot.family == PineOrderFamily::ExitTrail;
25633 if (!exit || snapshot.from_entry != placement_snapshot->source_id
25634 || snapshot.bracket_origin.incarnation != 0
25635 || !std::isfinite(snapshot.requested_qty)) {
25636 continue;
25637 }
25638 const auto replacement = [&](std::uint64_t origin) {
25639 return snapshot.source_id + "\x1f" + snapshot.from_entry + "\x1f"
25640 + std::to_string(static_cast<int>(snapshot.family)) + "\x1f"
25641 + std::to_string(origin);
25642 };
25643 const auto old_key = checked_live_key(replacement(0));
25644 const auto old = live_by_source_key_.find(old_key);
25645 if (old != live_by_source_key_.end() && old->second == handle)
25646 live_by_source_key_.erase(old);
25647 snapshot.bracket_origin = event.handle(); invalidate_retirement();
25648 live_by_source_key_[checked_live_key(replacement(event.handle().incarnation))] = live_source_value(handle, replacement(event.handle().incarnation));
25649 }
25650 {
25651 const auto cohort = require_cohort(placement_snapshot->source_id);
25652 auto& facts = cohort->second;
25653 facts.cycle = event.cycle_after;
25654 if (std::find(facts.opened.begin(), facts.opened.end(), event.handle())
25655 == facts.opened.end()) {
25656 facts.opened.push_back(event.handle());
25657 ++cohort_opened_slot_count_;
25658 cohorts_by_id_.refresh_held(cohort);
25659 }
25660 facts.live_units_by_origin[event.handle().incarnation]
25661 += std::abs(event.opened_units);
25662 cohorts_by_id_.refresh_held(cohort);
25663 if (position_tail_summary_)
25664 position_tail_summary_->open_origin(
25665 event.handle().incarnation, event.opened_units > 0.0,
25667 if (position_tail_summary_)
25668 position_tail_summary_->live_cohort(&*cohort, true);
25669 }
25670 credit_close_ledger(placement_snapshot->source_id, std::abs(event.opened_units));
25671 credit_pyramiding_record(placement_snapshot->source_id, event.handle().incarnation,
25672 std::abs(event.opened_units));
25673 record_opening_fee(*placement_snapshot, event);
25674 materialize_pending_bracket_legs(event);
25675 const auto created_side = static_cast<PositionSide>(
25676 placement_snapshot->projection_position_side);
25677 const bool consumed_deferred_carry =
25678 finite_positive(placement_snapshot->projection_tv_carry_qty)
25679 && created_side != PositionSide::FLAT
25680 && ((created_side == PositionSide::LONG) != placement_snapshot->is_long);
25681 if (consumed_deferred_carry) {
25682 for (const auto& handle : live_handles_) {
25683 if (handle == event.handle()) continue;
25684 const auto sibling = placement_.find(handle.incarnation);
25685 if (sibling == placement_.end()) continue;
25686 auto& candidate = sibling->second;
25687 if (!candidate.opening
25688 || candidate.source_id == placement_snapshot->source_id
25689 || candidate.projection_position_side
25690 != placement_snapshot->projection_position_side
25691 || candidate.projection_created_bar
25692 > placement_snapshot->projection_created_bar) {
25693 continue;
25694 }
25695 candidate.projection_tv_carry_qty = 0.0;
25696 }
25697 }
25698 materialize_relative_exits(*placement_snapshot, event);
25699 per_entry_exits_at_entry_fill(*placement_snapshot, event);
25700 const bool true_paired_transaction =
25701 placement_snapshot->paired_flat_market_candidate
25702 && finite_positive(placement_snapshot->paired_flat_market_own_qty)
25703 && finite_positive(placement_snapshot->paired_flat_market_transaction_qty)
25704 && placement_snapshot->paired_flat_market_transaction_qty
25705 > placement_snapshot->paired_flat_market_own_qty + 1e-10;
25706 if (!true_paired_transaction) {
25707 reconcile_deferred_exit_reservations(
25708 placement_snapshot->source_id,
25709 cohort_exposure_for(placement_snapshot->source_id));
25710 }
25711 flush_pending_bracket_legs(event.handle(), false);
25712 // ab9714be pine_fills.cpp:7652-7672 (finding-347): a leg re-issued for
25713 // a carried, already opened origin draws on the live position; it is
25714 // not this parent's pre-armed bracket and never waits for it.
25715 const auto carried_origin_leg = [&](const PlacementSnapshot& row) {
25716 return row.bracket_origin.incarnation != 0
25717 && row.bracket_origin != event.handle();
25718 };
25719 const bool partial_prearmed_parent = std::isfinite(
25720 [&]() {
25721 double smallest = kNaN;
25722 for (const auto& handle : live_handles_) {
25723 const auto pending = placement_.find(handle.incarnation);
25724 if (pending == placement_.end()) continue;
25725 const auto& row = pending->second;
25726 if ((row.family == PineOrderFamily::ExitStop
25727 || row.family == PineOrderFamily::ExitLimit)
25728 && row.from_entry == placement_snapshot->source_id
25729 && row.projection_created_bar
25730 == placement_snapshot->projection_created_bar
25731 && !carried_origin_leg(row)
25732 && std::isfinite(row.requested_qty)) {
25733 smallest = std::isfinite(smallest)
25734 ? std::min(smallest, row.requested_qty)
25735 : row.requested_qty;
25736 }
25737 }
25738 return smallest;
25739 }())
25740 && [&]() {
25741 for (const auto& handle : live_handles_) {
25742 const auto pending = placement_.find(handle.incarnation);
25743 if (pending == placement_.end()) continue;
25744 const auto& row = pending->second;
25745 if ((row.family == PineOrderFamily::ExitStop
25746 || row.family == PineOrderFamily::ExitLimit)
25747 && row.from_entry == placement_snapshot->source_id
25748 && row.projection_created_bar
25749 == placement_snapshot->projection_created_bar
25750 && !carried_origin_leg(row)
25751 && std::isfinite(row.requested_qty)
25752 && row.requested_qty < std::abs(event.opened_units)) {
25753 return true;
25754 }
25755 }
25756 return false;
25757 }();
25758 const bool multiple_prearmed_parents = std::any_of(
25759 live_handles_.begin(), live_handles_.end(), [&](const auto& handle) {
25760 if (handle == event.handle()) return false;
25761 const auto pending = placement_.find(handle.incarnation);
25762 return pending != placement_.end() && pending->second.opening
25763 && pending->second.family == PineOrderFamily::Entry
25764 && pending->second.projection_created_bar
25765 == placement_snapshot->projection_created_bar;
25766 });
25767 // A same-bar leg whose level is already marketable at the parent's
25768 // fill is a wrong-side scratch candidate; legacy scratches it at the
25769 // open for a sole MARKET parent only (test_prearmed_market_parent_gap_exit).
25770 const auto marketable_at_fill = [&](const PlacementSnapshot& row) {
25771 const bool long_position = event.opened_units > 0.0;
25772 const bool stop_marketable = finite_positive(row.exit_levels.stop)
25773 && (long_position ? event.resolved_price <= row.exit_levels.stop
25774 : event.resolved_price >= row.exit_levels.stop);
25775 const bool limit_marketable = finite_positive(row.exit_levels.limit)
25776 && (long_position ? event.resolved_price >= row.exit_levels.limit
25777 : event.resolved_price <= row.exit_levels.limit);
25778 return stop_marketable || limit_marketable;
25779 };
25780 // ab9714be pine_fills.cpp:7679-7700: TradingView evaluates a filled
25781 // parent's priced exits on the entry bar itself. With sibling pre-armed
25782 // parents on the same bar, only the wrong-side (marketable) legs wait
25783 // for the next opening; the right-side legs join the entry bar
25784 // (L9g; delta-3 P0-N4). A partial-quantity bracket waits entirely.
25785 if (partial_prearmed_parent || multiple_prearmed_parents) {
25787 for (const auto& handle : live_handles_) {
25788 const auto pending = placement_.find(handle.incarnation);
25789 if (pending == placement_.end()) continue;
25790 const auto& row = pending->second;
25791 const bool matches_parent = row.from_entry.empty()
25792 || row.from_entry == placement_snapshot->source_id;
25793 if ((row.family == PineOrderFamily::ExitStop
25794 || row.family == PineOrderFamily::ExitLimit)
25795 && matches_parent && row.per_entry_origin == 0
25796 && row.projection_created_bar
25797 == placement_snapshot->projection_created_bar
25798 && !carried_origin_leg(row)
25799 && (partial_prearmed_parent || marketable_at_fill(row))) {
25800 delayed_legs.push_back(handle);
25801 }
25802 }
25803 for (const auto& handle : delayed_legs) {
25804 const auto pending = placement_.find(handle.incarnation);
25805 if (pending == placement_.end()) continue;
25806 const PlacementSnapshot row = pending->second;
25807 native_order::Request request;
25808 request.intent = native_order::HostSized{
25809 native_order::HostSizedKind::Close, std::nullopt};
25810 request.label = row.source_id;
25811 request.comment = row.comment;
25812 request.owner = owner_for_close(row.from_entry, true);
25813 const bool exit_is_buy = event.opened_units < 0.0;
25814 if (row.family == PineOrderFamily::ExitStop) {
25815 request.trigger = native_order::Stop{source_trigger_threshold(
25816 row.exit_levels.stop, staged_.syminfo.mintick,
25817 exit_is_buy, false)};
25818 } else {
25819 request.trigger = native_order::Limit{source_trigger_threshold(
25820 row.exit_levels.limit, staged_.syminfo.mintick,
25821 exit_is_buy, true)};
25822 }
25823 const bool explicit_origin = std::isfinite(row.requested_qty)
25824 && row.bracket_origin.incarnation != 0;
25825 const std::string origin_suffix = explicit_origin
25826 ? "\x1f" + std::to_string(row.bracket_origin.incarnation) : "";
25827 const std::string group_name = row.oca_name.empty()
25828 ? row.source_id + "\x1f" + row.from_entry + origin_suffix
25829 : row.oca_name;
25830 request.group = group_for(group_name, 1);
25831 const std::string replacement_key = row.source_id + "\x1f"
25832 + row.from_entry + "\x1f"
25833 + std::to_string(static_cast<int>(row.family)) + origin_suffix;
25834 const auto cancelled = require_host().cancel(handle);
25835 if (cancelled.status != native_order::CancelStatus::Cancelled)
25836 continue;
25837 retire(handle);
25838 delayed_market_orders_.push_back({
25839 std::move(request), row, replacement_key,
25840 broker_open_epoch_ + 1U});
25841 }
25842 }
25843 if (!partial_prearmed_parent) {
25844 std::optional<PlacementSnapshot> rearm;
25845 for (const auto& handle : live_handles_) {
25846 const auto pending = placement_.find(handle.incarnation);
25847 if (pending == placement_.end()) continue;
25848 const auto& row = pending->second;
25849 const bool matches_parent = row.from_entry.empty()
25850 || row.from_entry == placement_snapshot->source_id;
25851 // An entry's own bracket of a per-entry global exit stands as
25852 // it was placed (per_entry_exits_at_entry_fill).
25853 if ((row.family != PineOrderFamily::ExitStop
25854 && row.family != PineOrderFamily::ExitLimit)
25855 || !matches_parent || row.per_entry_origin != 0
25856 || row.projection_created_bar
25857 != placement_snapshot->projection_created_bar
25858 || carried_origin_leg(row)) {
25859 continue;
25860 }
25861 // A from_entry "" exit placed while the position this fill
25862 // reversed was open belongs to that position (TVDEF-DROPS R3):
25863 // it never joins the new side's entry-bar path (lab tv
25864 // w4-f08b-pooc-coof cell S).
25865 if (row.from_entry.empty() && event.closed_units > 0.0
25866 && row.projection_position_side
25867 == static_cast<std::int32_t>(event.opened_units > 0.0
25869 : PositionSide::LONG)) {
25870 continue;
25871 }
25872 const bool long_position = event.opened_units > 0.0;
25873 const bool stop_marketable = finite_positive(row.exit_levels.stop)
25874 && (long_position ? event.resolved_price <= row.exit_levels.stop
25875 : event.resolved_price >= row.exit_levels.stop);
25876 const bool limit_marketable = finite_positive(row.exit_levels.limit)
25877 && (long_position ? event.resolved_price >= row.exit_levels.limit
25878 : event.resolved_price <= row.exit_levels.limit);
25879 const bool equal_limit = finite_positive(row.exit_levels.limit)
25880 && event.resolved_price == row.exit_levels.limit;
25881 if ((!stop_marketable && !limit_marketable) || equal_limit) {
25882 rearm = row;
25883 break;
25884 }
25885 }
25886 if (rearm) {
25887 // ab9714be pine_fills.cpp:7788-7800: a full prearmed MARKET
25888 // parent bracket that is not an immediate wrong-side scratch
25889 // joins the remaining entry-bar path. Reissuing from the
25890 // parent's Applied callback gives the generic requests that
25891 // exact birth floor and the now-known close direction. With
25892 // sibling parents the wrong-side leg was parked above, so
25893 // only the right-side levels are reissued here (L9g).
25894 const bool long_position = event.opened_units > 0.0;
25895 const bool stop_wrong_side = multiple_prearmed_parents
25896 && finite_positive(rearm->exit_levels.stop)
25897 && (long_position ? event.resolved_price <= rearm->exit_levels.stop
25898 : event.resolved_price >= rearm->exit_levels.stop);
25899 const bool limit_wrong_side = multiple_prearmed_parents
25900 && finite_positive(rearm->exit_levels.limit)
25901 && (long_position ? event.resolved_price >= rearm->exit_levels.limit
25902 : event.resolved_price <= rearm->exit_levels.limit)
25903 && event.resolved_price != rearm->exit_levels.limit;
25904 exit(rearm->source_id, rearm->from_entry,
25905 limit_wrong_side ? kNaN : rearm->exit_levels.limit,
25906 stop_wrong_side ? kNaN : rearm->exit_levels.stop,
25907 rearm->exit_levels.trail_points,
25908 rearm->exit_levels.trail_offset,
25909 rearm->exit_levels.trail_price,
25910 rearm->qty_percent, rearm->comment,
25911 rearm->requested_qty, rearm->oca_name,
25912 rearm->exit_levels.profit_ticks,
25913 rearm->exit_levels.loss_ticks);
25914 }
25915 }
25916 if (event.closed_units > 0.0 && event.opened_units > 0.0
25917 && config_.default_qty_type
25918 == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
25919 && config_.default_qty_value < 100.0) {
25920 // ab9714be pine_fills.cpp:3670-3780, famx NIFTY admit90:
25921 // after the opposite MARKET wins the gapped-open arbitration,
25922 // its newly materialized relative trail does not scratch the
25923 // admitted reversal at that same opening point.
25925 for (const auto& handle : live_handles_) {
25926 const auto pending = placement_.find(handle.incarnation);
25927 if (pending != placement_.end()
25928 && pending->second.family == PineOrderFamily::ExitTrail
25929 && pending->second.from_entry == placement_snapshot->source_id
25930 && pending->second.projection_created_bar
25931 == placement_snapshot->projection_created_bar) {
25932 prearmed_trails.push_back(handle);
25933 }
25934 }
25935 for (const auto& handle : prearmed_trails) {
25936 const auto result = require_host().cancel(handle);
25937 if (result.status == native_order::CancelStatus::Cancelled)
25938 retire(handle);
25939 }
25940 }
25941 // The generic cohort is already the quantity authority. Rebind only
25942 // adapter lifecycle/reservation receipts after the opening becomes a
25943 // live physical fact; no request is resized or resubmitted here.
25945 for (const auto& handle : live_handles_) handles.push_back(handle);
25946 handles.finish();
25947 for (const auto& handle : handles) {
25948 const auto pending = placement_.find(handle.incarnation);
25949 if (pending == placement_.end()) continue;
25950 auto& candidate = pending->second;
25951 const bool exit = candidate.family == PineOrderFamily::ExitLimit
25952 || candidate.family == PineOrderFamily::ExitStop
25953 || candidate.family == PineOrderFamily::ExitTrail;
25954 if (!exit) continue;
25955 if (candidate.from_entry == placement_snapshot->source_id) {
25956 if (candidate.legs.target().incarnation
25957 && candidate.legs.target().owner != current_position_cycle_) {
25958 const exit_legs::Frame cause{event.ordinal,
25959 context.coordinate.interval_index,
25960 context.sub_count > 1 ? exit_legs::Domain::MagnifierFillRecalc
25961 : exit_legs::Domain::FillRecalc,
25962 exit_legs::Phase::Observation};
25963 const exit_legs::Action bind{candidate.legs.target(),
25964 candidate.legs.revision(), cause,
25965 exit_legs::BindOwner{current_position_cycle_}};
25966 (void)candidate.legs.apply(candidate.legs.target(), bind); invalidate_retirement();
25967 }
25968 initialize_l4c_policy(candidate, handle);
25969 }
25970 const bool bound_preexit_add = placement_snapshot
25971 && placement_snapshot->opening && event.opened_units > 0.0
25972 && placement_snapshot->command_sequence < candidate.command_sequence;
25973 const bool selected_growth_source = placement_snapshot
25974 && placement_snapshot->reservation_growth_source.reservation_owner()
25975 && *placement_snapshot->reservation_growth_source.reservation_owner()
25976 == handle.incarnation;
25977 if (candidate.reservation_expansion.capture() && handle != event.handle()
25978 && selected_growth_source
25979 && (candidate.reservation_expansion.population_open() || bound_preexit_add)) {
25980 if (bound_preexit_add && std::isfinite(candidate.projection_remaining_qty)) {
25981 candidate.projection_remaining_qty += std::abs(event.opened_units);
25982 }
25983 if (candidate.reservation_expansion.population_open()) {
25984 candidate.pooc_global_full_exit_dynamic_qty = false;
25985 candidate.pooc_global_full_exit_tracks_bound_adds = false;
25986 candidate.reservation_expansion.close_population(event.handle().incarnation);
25987 candidate.pooc_global_full_exit_bound_add = bound_preexit_add;
25988 }
25989 }
25990 }
25991 }
25992 if (placement_snapshot && placement_snapshot->opening
25993 && placement_snapshot->reverse_to && event.closed_units > 0.0
25994 && event.opened_units > 0.0) {
25995 purge_brackets_after_applied_reversal(*placement_snapshot);
25996 }
25997 // A from_entry="" exit the script placed while the reversed position was
25998 // open belongs to that position, as every exit a flattening close
25999 // outlives does (retire_in_position_exits_at_flat): TradingView never
26000 // applies it to the side the reversal opens, on its entry bar or later,
26001 // in either direction (lane TVDEF-DROPS, tests/fixtures/tvdef_drops R3).
26002 // The kernel binds such a close to the book it first evaluates against;
26003 // one evaluated before this fill was already cancelled with that book
26004 // and waits for its terminal receipt: a cancel of it would record a
26005 // NotWorking event, so only a leg still working is cancelled here.
26006 if (placement_snapshot && placement_snapshot->opening
26007 && placement_snapshot->reverse_to && event.closed_units > 0.0
26008 && event.opened_units != 0.0) {
26009 const auto reversed_side = static_cast<std::int32_t>(
26010 event.opened_units > 0.0 ? PositionSide::SHORT : PositionSide::LONG);
26011 detail::ExitScratchRows<native_order::RequestHandle> reversed_global_exits(*this);
26012 for (const auto& handle : live_handles_) {
26013 const auto found = placement_.find(handle.incarnation);
26014 if (found == placement_.end()) continue;
26015 const auto& row = found->second;
26016 const bool exit = row.family == PineOrderFamily::ExitLimit
26017 || row.family == PineOrderFamily::ExitStop
26018 || row.family == PineOrderFamily::ExitTrail;
26019 // The reversal advanced the cycle above; the reversed position's
26020 // is the one before it.
26021 if (exit && row.from_entry.empty() && row.projection_position_side == reversed_side
26022 && row.placement_cycle == current_position_cycle_ - 1) {
26023 reversed_global_exits.push_back(handle);
26024 }
26025 }
26026 cancel_working_legs(reversed_global_exits);
26027 // The same exit may already wait off the book: a leg this fill found
26028 // marketable beside a second parent of the bar was parked for the next
26029 // open, its request cancelled (lane W3-ENG-EXIT-ALLOC, tape
26030 // w3f05-r1-two-parent-reversal-global-exit). It is void as well, and
26031 // leaving the queue records no kernel event.
26032 const auto reversed_global_leg = [&](const PlacementSnapshot& row) {
26033 return (row.family == PineOrderFamily::ExitLimit
26034 || row.family == PineOrderFamily::ExitStop
26035 || row.family == PineOrderFamily::ExitTrail)
26036 && row.from_entry.empty() && row.projection_position_side == reversed_side
26037 && row.placement_cycle == current_position_cycle_ - 1;
26038 };
26039 delayed_market_orders_.erase(std::remove_if(
26040 delayed_market_orders_.begin(), delayed_market_orders_.end(),
26041 [&](const auto& order) { return reversed_global_leg(order.snapshot); }),
26042 delayed_market_orders_.end());
26043 pending_write(pending_bracket_legs_).erase(std::remove_if(
26044 pending_bracket_legs_.begin(), pending_bracket_legs_.end(),
26045 [&](const PendingBracketLeg& leg) { return reversed_global_leg(leg.snapshot); }),
26046 pending_bracket_legs_.end());
26047 }
26048 if (placement_snapshot && placement_snapshot->family == PineOrderFamily::Margin
26049 && event.closed_units > 0.0) {
26050 refresh_pending_sizing_after_margin(event, context);
26051 // pine_fills.cpp:6399-6434's narrow MC-surplus receipt. It is not
26052 // inferred from an arbitrary requested-minus-live quantity: the
26053 // source entry must have been reduced by this one-unit margin event
26054 // after its close-only placement, while its original cycle remains the
26055 // sole live long lot. Snapshot mutation at an Applied boundary is a
26056 // pinned P5 write boundary.
26057 const auto physical = detail::run_position(require_host());
26058 std::uint64_t sole_live_entry_incarnation = 0;
26059 std::size_t live_entry_count = 0;
26060 for (const auto& cohort : cohorts_by_id_.held_rows()) {
26061 for (const auto& opening : cohort.second.opened) {
26062 const auto units = cohort.second.live_units_by_origin.find(
26063 opening.incarnation);
26064 if (units == cohort.second.live_units_by_origin.end()
26065 || !(units->second > 0.0)) {
26066 continue;
26067 }
26068 sole_live_entry_incarnation = opening.incarnation;
26069 ++live_entry_count;
26070 }
26071 }
26072 const std::uint64_t source_fill_sequence =
26073 static_cast<PineStrategyHost&>(require_host()).broker_fill_event_seq_;
26074 if (live_entry_count == 1U && sole_live_entry_incarnation != 0
26075 && physical.signed_units > 0.0 && physical.lot_count == 1U
26076 && std::abs(event.closed_units - 1.0) < 1e-6
26077 && config_.default_qty_type
26078 == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
26079 && std::abs(config_.default_qty_value - 100.0) < 1e-12
26080 && config_.commission_type == static_cast<int>(CommissionType::PERCENT)
26081 && config_.commission_value == 0.0 && config_.slippage == 0
26082 && source_fill_sequence != 0) {
26083 signal_close_mc_event_bar_ = context.coordinate.interval_index;
26084 signal_close_mc_position_cycle_ = current_position_cycle_;
26085 signal_close_mc_entry_incarnation_ = sole_live_entry_incarnation;
26086 signal_close_mc_fill_seq_ = source_fill_sequence;
26087 signal_close_mc_remaining_qty_ = std::abs(physical.signed_units);
26088 signal_close_mc_before_qty_ = signal_close_mc_remaining_qty_
26089 + std::abs(event.closed_units);
26090 }
26091 std::uint64_t sole_entry_incarnation = 0;
26092 int live_entry_origins = 0;
26093 if (physical.signed_units != 0.0 && physical.lot_count == 1U) {
26094 for (const auto& cohort : cohorts_by_id_.held_rows()) {
26095 for (const auto& origin : cohort.second.opened) {
26096 const auto units = cohort.second.live_units_by_origin.find(
26097 origin.incarnation);
26098 if (units == cohort.second.live_units_by_origin.end()
26099 || !(units->second > 0.0)) {
26100 continue;
26101 }
26102 const auto opening = placement_.find(origin.incarnation);
26103 if (opening == placement_.end()
26104 || !opening->second.opening
26105 || opening->second.is_long != (physical.signed_units > 0.0)) {
26106 continue;
26107 }
26108 sole_entry_incarnation = origin.incarnation;
26109 ++live_entry_origins;
26110 }
26111 }
26112 }
26113 if (live_entry_origins != 1) sole_entry_incarnation = 0;
26114 last_margin_call_script_bar_ = context.script_bar_open_ms;
26115 last_margin_call_event_ordinal_ = event.ordinal;
26116 last_margin_call_entry_incarnation_ = sole_entry_incarnation;
26117 last_margin_call_position_cycle_ = current_position_cycle_;
26118 last_margin_call_at_script_close_ = policy_script_bar_valid_
26119 && policy_script_bar_.timestamp == context.script_bar_open_ms
26120 && nearest_tick(event.resolved_price, staged_.syminfo.mintick)
26121 == nearest_tick(policy_script_bar_.close, staged_.syminfo.mintick);
26122 last_margin_call_closed_units_ = event.closed_units;
26123 last_margin_call_remaining_units_ = std::abs(physical.signed_units);
26124 const auto state = detail::run_state(require_host());
26125 const auto* pine_host = pine_view_of(&require_host());
26126 const bool magnifier = pine_host
26127 && pine_host->scheduler_.bar_magnifier_enabled();
26128 const bool ordinary_margin_receipt = !config_.process_orders_on_close
26129 && !config_.calc_on_order_fills && !coof_recalc_active_
26130 && !magnifier
26131 && last_margin_call_at_script_close_;
26132 for (const auto& handle : live_handles_) {
26133 const auto found = placement_.find(handle.incarnation);
26134 if (found == placement_.end()) continue;
26135 auto& candidate = found->second;
26136 const double close_surplus = candidate.projection_tv_carry_qty
26137 - std::abs(physical.signed_units);
26138 const bool exact_margin_receipt = candidate.family == PineOrderFamily::Entry
26139 && candidate.affordability_close_only && !candidate.is_long
26140 && candidate.rounded_signal_cost_close_only
26141 && candidate.deferred_cohort
26142 && ordinary_margin_receipt
26143 && !std::isfinite(candidate.requested_qty)
26144 && !finite_positive(candidate.exit_levels.limit)
26145 && !finite_positive(candidate.exit_levels.stop)
26146 && !candidate.projection_after_close
26147 && candidate.projection_created_bar
26148 == context.coordinate.interval_index
26149 && candidate.projection_position_side
26150 == static_cast<std::int32_t>(PositionSide::LONG)
26151 && candidate.placement_cycle == current_position_cycle_
26152 && physical.signed_units > 0.0 && physical.lot_count == 1U
26153 && sole_entry_incarnation != 0
26154 && std::isfinite(close_surplus) && std::abs(close_surplus - 1.0) < 1e-6
26155 && std::abs(event.closed_units - 1.0) < 1e-6;
26156 if (exact_margin_receipt) {
26157 candidate.affordability_keep_mc_close_surplus = true;
26158 candidate.signal_close_mc_bar = candidate.projection_created_bar;
26159 candidate.signal_close_mc_entry_incarnation = sole_entry_incarnation;
26160 candidate.signal_close_mc_fill_seq = pine_host
26161 ? pine_host->adapter_broker_fill_event_sequence() : 0;
26162 candidate.signal_close_mc_remaining_qty =
26163 std::abs(physical.signed_units);
26164 if (live_entry_count == 1U && sole_live_entry_incarnation != 0) {
26165 candidate.signal_close_mc_bar = candidate.projection_created_bar;
26166 candidate.signal_close_mc_entry_incarnation =
26167 sole_live_entry_incarnation;
26168 candidate.signal_close_mc_fill_seq = source_fill_sequence;
26169 candidate.signal_close_mc_remaining_qty =
26170 std::abs(physical.signed_units);
26171 }
26172 }
26173 }
26174 revive_brackets_after_margin(event, context);
26175 }
26176 if (event.closed_units > 0.0) {
26177 const SourceId* fee_source = nullptr;
26178 if (placement_snapshot && !placement_snapshot->from_entry.empty())
26179 fee_source = &placement_snapshot->from_entry;
26180 consume_opening_fees(event, fee_source);
26181 }
26182 const bool current_debit_observed =
26183 current_debited_applied_ordinals_.erase(event.ordinal) != 0;
26184 if (!flipped_position && !current_debit_observed && event.closed_trade_count > 0)
26185 consume_closed_trade_rows(event,
26186 placement_snapshot ? &*placement_snapshot : nullptr);
26187 // The margin call's units have left their cohorts: the exits' queue
26188 // reserves the position that remains (rule PK).
26189 if (placement_snapshot && placement_snapshot->family == PineOrderFamily::Margin
26190 && event.closed_units > 0.0) {
26191 reconcile_exit_reservations_after_margin();
26192 }
26193 // ab9714be pine_fills.cpp:5883-5899 / pine_strategy_commands.cpp:
26194 // 2739-2811: once an opening applies, every deferred bracket family for
26195 // a now-live source cohort receives its reservation. This also covers
26196 // the second half of a flat MARKET/MARKET transaction pair; its Applied
26197 // shape is not required to expose closed_units for the source reservation
26198 // boundary to be observable.
26199 if (placement_snapshot && placement_snapshot->opening) {
26200 const double physical_exposure =
26201 std::abs(detail::run_position(require_host()).signed_units);
26202 for (const auto& cohort : cohorts_by_id_.held_rows()) {
26203 const auto& id = cohort.first;
26204 const double exposure = cohort_exposure_for(cohort.second);
26205 const bool paired_source_transaction =
26206 placement_snapshot->paired_flat_market_candidate
26207 && finite_positive(
26208 placement_snapshot->paired_flat_market_own_qty)
26209 && finite_positive(
26210 placement_snapshot->paired_flat_market_transaction_qty)
26211 && placement_snapshot->paired_flat_market_transaction_qty
26212 > placement_snapshot->paired_flat_market_own_qty + 1e-10;
26213 const double source_own = paired_source_transaction
26214 ? placement_snapshot->paired_flat_market_own_qty
26215 : physical_exposure;
26216 const bool intermediate_paired_gross = config_.pyramiding == 2
26217 && source_own > 0.0 && exposure > source_own + 1e-10;
26218 if (exposure > 0.0 && !intermediate_paired_gross)
26219 reconcile_deferred_exit_reservations(id, exposure);
26220 }
26221 // Exits for every entry (from_entry="") armed before this opening
26222 // queue for the position the same way: behind a full one created
26223 // earlier, a later one is left nothing (lane W3-ENG-EXIT-ALLOC, tape
26224 // w3f05-s11-pooc-qty-any-entry): the front full one keeps its dynamic
26225 // reservation and the unreserved ones behind it are cancelled. A full
26226 // one behind partial ones gets what they leave (lane W3B-ENG-GRID,
26227 // w3bf05-g8): the queue then hands the position out as it does for a
26228 // named entry's exits.
26229 if (physical_exposure > 0.0) {
26230 struct GlobalExit {
26231 std::uint64_t key = 0;
26232 std::uint64_t rank = 0;
26233 bool full = true;
26234 bool reserved = false;
26235 double units = 0.0;
26236 };
26238 detail::ExitScratchIndex families(armed.context());
26239 detail::ExitScratchIndex members(armed.context());
26240 std::uint64_t member_ordinal = 0;
26241 for (const auto& handle : live_handles_) {
26242 const auto found = placement_.find(handle.incarnation);
26243 if (found == placement_.end()) continue;
26244 const auto& row = found->second;
26245 const bool exit = row.family == PineOrderFamily::ExitLimit
26246 || row.family == PineOrderFamily::ExitStop
26247 || row.family == PineOrderFamily::ExitTrail;
26248 if (!exit || !row.from_entry.empty()) continue;
26249 const bool reserved = std::isfinite(row.projection_remaining_qty);
26250 const bool full = !std::isfinite(row.requested_qty)
26251 && (!std::isfinite(row.qty_percent)
26252 || row.qty_percent >= 100.0 - internal::kFullPercentEps);
26253 // What a partial exit holds: its reservation, else its
26254 // explicit or percent share of the new position.
26255 const double units = reserved ? std::max(0.0, row.projection_remaining_qty)
26256 : std::isfinite(row.requested_qty) ? std::abs(row.requested_qty)
26257 : physical_exposure * (std::isfinite(row.qty_percent)
26258 ? std::clamp(row.qty_percent, 0.0, 100.0) : 100.0) / 100.0;
26259 const auto key = key_for(row.source_id, row.from_entry);
26260 const auto rank = exit_queue_rank(placement_, row);
26261 auto index = families.find(key);
26262 if (index == detail::ExitScratchIndex::absent) {
26263 index = armed.size();
26264 families.put(key, index);
26265 armed.push_back({key, rank, full, reserved, units});
26266 } else {
26267 GlobalExit family = armed[index];
26268 family.rank = std::min(family.rank, rank);
26269 family.full = family.full && full;
26270 family.reserved = family.reserved || reserved;
26271 family.units = std::max(family.units, units);
26272 armed.set(index, family);
26273 }
26274 members.put(key, handle.incarnation, member_ordinal++);
26275 }
26276 if (armed.size() > 1) {
26278 bool behind_full = false;
26279 bool partial_ahead_of_full = false;
26280 double ahead = 0.0;
26281 armed.ordered(
26282 [](const GlobalExit& family) { return family.rank; },
26283 [&](const GlobalExit& family, std::size_t) {
26284 if (behind_full) {
26285 if (!family.reserved) {
26286 members.each(family.key, [&](std::uint64_t incarnation) {
26287 behind.push_back({behind.context().run, incarnation});
26288 return true;
26289 });
26290 }
26291 return true;
26292 }
26293 if (!family.full) {
26294 ahead += family.units;
26295 return true;
26296 }
26297 // A full exit already holding a reservation is its own
26298 // family's business (reconcile_deferred_exit_reservations).
26299 if (family.reserved) return true;
26300 behind_full = true;
26301 if (ahead > internal::kQtyEpsilon) partial_ahead_of_full = true;
26302 return true;
26303 });
26304 if (partial_ahead_of_full) {
26305 reconcile_deferred_exit_reservations({}, physical_exposure);
26306 } else {
26307 for (const auto& handle : behind) {
26308 const auto result = require_host().cancel(handle);
26309 if (result.status == native_order::CancelStatus::Cancelled)
26310 retire(handle);
26311 }
26312 }
26313 }
26314 }
26315 }
26316 if (placement_snapshot && placement_snapshot->family == PineOrderFamily::CloseAll
26317 && event.closed_units > 0.0) {
26318 const auto held = cohorts_by_id_.held_rows();
26319 for (auto at = held.begin(); at != held.end();) {
26320 const auto current = at++;
26321 ::pineforge::detail::clear_exposure_storage(current->second.live_units_by_origin);
26322 cohorts_by_id_.refresh_held(current);
26323 }
26324 if (position_tail_summary_) position_tail_summary_->forget_origins();
26325 }
26326 if (placement_snapshot && placement_snapshot->family == PineOrderFamily::Margin
26327 && event.closed_units > 0.0
26328 && detail::run_position(require_host()).signed_units != 0.0) {
26329 const auto physical = detail::run_position(require_host());
26330 auto revival = std::find_if(
26331 pending_margin_revivals_.begin(), pending_margin_revivals_.end(),
26332 [&](const PendingMarginRevival& pending) {
26333 const auto& row = pending.snapshot;
26334 const bool same_side = row.projection_position_side
26335 == static_cast<std::int32_t>(PositionSide::LONG)
26336 ? physical.signed_units > 0.0 : physical.signed_units < 0.0;
26337 const bool exposed = !row.from_entry.empty()
26338 && cohort_exposure_for(row.from_entry) > 0.0;
26339 return same_side && exposed;
26340 });
26341 if (revival != pending_margin_revivals_.end()) {
26342 PlacementSnapshot snapshot = std::move(revival->snapshot);
26343 pending_margin_revivals_.erase(revival);
26344 const bool reached = physical.signed_units > 0.0
26345 ? event.resolved_price <= snapshot.exit_levels.stop
26346 : event.resolved_price >= snapshot.exit_levels.stop;
26347 native_order::Request request;
26348 request.intent = native_order::Reduce{
26349 native_order::ExplicitUnits{std::abs(physical.signed_units)}};
26350 request.label = snapshot.source_id;
26351 request.comment = snapshot.comment;
26352 if (!reached) {
26353 const bool exit_is_buy = physical.signed_units < 0.0;
26354 request.trigger = native_order::Stop{source_trigger_threshold(
26355 snapshot.exit_levels.stop, staged_.syminfo.mintick,
26356 exit_is_buy, false)};
26357 }
26359 snapshot.restored_after_margin = true;
26360 snapshot.requested_qty = std::abs(physical.signed_units);
26361 snapshot.qty_percent = 100.0;
26362 snapshot.projection_remaining_qty = kNaN;
26363 snapshot.fixed_exit_reservation = false;
26364 snapshot.forced_execution_price = reached
26365 ? event.resolved_price : kNaN;
26366 snapshot.market_admission = {};
26367 snapshot.cancellation = {};
26368 const auto family_key = key_for(snapshot.source_id, snapshot.from_entry);
26369 const SourceId replacement_key = snapshot.source_id + "\x1f"
26370 + snapshot.from_entry + "\x1fmargin-revival";
26371 const auto accepted = submit_or_replace(
26372 std::move(request), std::move(snapshot), false, replacement_key);
26373 if (accepted) {
26374 bracket_family(family_key).push_back(*accepted);
26375 if (reached) {
26376 (void)require_host().execute_current(
26377 {*accepted, NativeCurrentPriceRule::NearestTick});
26378 }
26379 }
26380 }
26381 }
26382 if (detail::run_position(require_host()).signed_units == 0.0) {
26383 detail::ExitScratchRows<SourceId> exit_owners(*this);
26384 const auto collect_owner = [&](const PlacementSnapshot& candidate) {
26385 const bool exit = candidate.family == PineOrderFamily::ExitLimit
26386 || candidate.family == PineOrderFamily::ExitStop
26387 || candidate.family == PineOrderFamily::ExitTrail;
26388 if (exit) exit_owners.push_back(candidate.from_entry);
26389 };
26390 for (const auto& handle : live_handles_) {
26391 const auto found = placement_.find(handle.incarnation);
26392 if (found != placement_.end()) collect_owner(found->second);
26393 }
26394 for (const auto& pending : pending_bracket_legs_) collect_owner(pending.snapshot);
26395 for (const auto& pending : pending_coof_requests_) collect_owner(pending.snapshot);
26396 for (const auto& shadow : source_shadow_pending_) collect_owner(shadow.snapshot);
26397 const auto pending_parent = [&](const detail::ExitScratchText& owner) {
26398 if (placement_snapshot && placement_snapshot->opening
26399 && placement_snapshot->source_id == owner) {
26400 return true;
26401 }
26402 for (const auto& handle : live_handles_) {
26403 if (event.terminal && handle == event.handle()) continue;
26404 const auto found = placement_.find(handle.incarnation);
26405 if (found != placement_.end() && found->second.opening
26406 && found->second.family == PineOrderFamily::Entry
26407 && found->second.source_id == owner) {
26408 return true;
26409 }
26410 }
26411 return std::any_of(pending_entries_.begin(), pending_entries_.end(),
26412 [&](const PendingEntry& entry) {
26413 return entry.snapshot.opening
26414 && entry.snapshot.source_id == owner;
26415 });
26416 };
26417 exit_owners.ordered_text(
26418 [&](const detail::ExitScratchText& owner) {
26419 // A transient flat between two same-point reversal transactions
26420 // does not end the pending parent's lifecycle. Keep only that
26421 // parent's brackets; stale owners still retire immediately.
26422 if (!pending_parent(owner)) cancel_exit_orders_for_full_close(owner);
26423 return true;
26424 }, true);
26425 // ab9714be pine_fills.cpp:7461-7468 / 7667-7673: the owner scan above
26426 // only sees exits that are live or still pending, so a bracket that
26427 // already went dormant survives the flat in its projection row and is
26428 // revived against the next position that reuses its entry id. The
26429 // legacy book Removes every in-position exit at the flat whatever its
26430 // current lifecycle state.
26431 retire_in_position_exits_at_flat(/*preserve_pending_parents=*/true,
26432 /*dormant_rows_only=*/true,
26433 /*paired_close=*/nullptr);
26434 position_tail_spill_.reset();
26435 if (position_tail_summary_)
26436 position_tail_summary_->invalidate(PositionTailGuard::Flat,
26438 position_open_script_bar_ = std::numeric_limits<std::int64_t>::min();
26439 position_open_bar_index_ = -1;
26440 position_open_phase_ = NativePathPhase::None;
26441 position_open_priced_ = false;
26442 open_entry_fees_.clear();
26443 close_logical_units_.clear();
26444 close_ledger_records_.clear();
26445 pyramiding_records_.clear();
26446 exit_tombstones_.clear();
26447 detail::ExitScratchRows<SourceId> ended_sources(*this);
26448 pending_margin_revivals_.clear();
26449 const auto held = cohorts_by_id_.held_rows();
26450 for (auto at = held.begin(); at != held.end();) {
26451 const auto current = at++;
26452 auto& cohort = *current;
26453 if (!cohort.second.opened.empty()) ended_sources.push_back(cohort.first);
26454 assert(cohort_opened_slot_count_ >= cohort.second.opened.size());
26455 cohort_opened_slot_count_ -= cohort.second.opened.size();
26457 ::pineforge::detail::clear_exposure_storage(cohort.second.live_units_by_origin);
26458 cohorts_by_id_.refresh_held(current);
26459 }
26461 for (const auto& handle : live_handles_) handles.push_back(handle);
26462 handles.finish();
26463 for (const auto& handle : handles) {
26464 const auto found = placement_.find(handle.incarnation);
26465 if (found == placement_.end()) continue;
26466 const auto family = found->second.family;
26467 // ab9714be pine_orders.cpp:530-534: purge_exit_orders retires all exit and close orders when position flattens
26468 if (family == PineOrderFamily::ExitLimit
26469 || family == PineOrderFamily::ExitStop
26470 || family == PineOrderFamily::ExitTrail
26471 || family == PineOrderFamily::Close
26472 || family == PineOrderFamily::CloseAll
26473 // The slice is a full close of its from_entry's cycle: the owner
26474 // clears cycle_filled_entry_ids_ when the book goes flat
26475 // (ab9714be pine_strategy_host.cpp:245-251), so the bracket
26476 // retires with the rest instead of surviving into the next lot
26477 // that reuses the id.
26478 || family == PineOrderFamily::Margin) {
26479 // A margin call placed again for the next open is sized at
26480 // placement, not an exit of the book it was sized on: it fills
26481 // as placed on the book a close order flattened
26482 // (close_point_margin_call; lab tv tape w13-b2-cancel-close-f).
26483 if (family == PineOrderFamily::Margin && found->second.crosses_zero) continue;
26484 if (!found->second.from_entry.empty()
26485 && !ended_sources.contains(found->second.from_entry)) {
26486 continue;
26487 }
26488 (void)require_host().cancel(handle);
26489 retire(handle);
26490 }
26491 }
26492 bracket_shadowed_openings_.clear();
26493 }
26494 if (short_seed_.final_short.incarnation != 0 && event.handle() == short_seed_.final_short
26495 && config_.default_qty_type != static_cast<int>(QtyType::FIXED)) {
26496 short_seed_.report_swap_pending = true;
26497 }
26498 update_l4c_lifecycle(event, context);
26499 if (event.terminal) {
26500 const auto filled = placement_.find(event.handle().incarnation);
26501 // Share K1's named-only guard: global fills stay supersession evidence.
26502 if (filled != placement_.end() && event.closed_units > 0.0
26503 && unbound_limit_without_revival(filled->second))
26504 filled->second.filled_exit = true, invalidate_retirement();
26505 retire(event.handle());
26506 }
26507 if (event.ordinal != day_ledger_.observed_applied_ordinal) {
26508 day_ledger_.observed_applied_ordinal = event.ordinal;
26509 const auto day = chart_day_key(context.sub_bar_open_ms);
26510 if (event.closed_trade_count > 0) {
26511 const auto& host = require_host();
26512 for (std::size_t i = 0; i < event.closed_trade_count; ++i) {
26513 const auto index = event.first_trade_index + i;
26514 if (index >= static_cast<std::size_t>(host.trade_count())) continue;
26515 const double pnl = host.get_trade(static_cast<int>(index)).pnl;
26516 day_ledger_.intraday_realized += pnl;
26517 if (pnl < 0.0 && day != day_ledger_.last_loss_day) {
26518 day_ledger_.last_loss_day = day;
26519 if (day_ledger_.consecutive_loss_days
26520 == std::numeric_limits<int>::max()) {
26521 throw coded<std::overflow_error>(RunFailureCode::engine_invariant, {},
26522 "closed trade counter exhausted");
26523 }
26524 ++day_ledger_.consecutive_loss_days;
26525 } else if (pnl > 0.0) {
26526 day_ledger_.consecutive_loss_days = 0;
26527 }
26528 }
26529 }
26530 }
26531 // A fill that reduces the side it leaves open books its units against
26532 // the close ledger (a reversal or a flat has just cleared it).
26533 if (event.closed_units > 0.0 && !flipped_position
26534 && detail::run_position(require_host()).signed_units != 0.0) {
26535 observe_close_ledger(event, placement_snapshot ? &*placement_snapshot : nullptr);
26536 }
26537 if (placement_snapshot) {
26538 observe_intraday_cap(event, *placement_snapshot, context);
26539 if (placement_snapshot->family == PineOrderFamily::Margin
26540 && event.closed_units > 0.0) {
26541 last_margin_call_script_bar_ = context.script_bar_open_ms;
26542 const auto after_margin = detail::run_position(require_host());
26543 const bool one_x_long = after_margin.signed_units > 0.0
26544 && std::abs(config_.margin_long - 100.0) < 1e-12;
26545 if (context.coordinate.path_phase == NativePathPhase::Open
26546 && policy_script_bar_valid_
26547 && policy_script_bar_.timestamp == context.script_bar_open_ms
26548 && after_margin.signed_units != 0.0 && !one_x_long) {
26549 // A carried POOC short filled at the previous close rechecks
26550 // its already-slipped fill: mark and forced execution both
26551 // reuse event.resolved_price unchanged until margin is covered.
26552 // TradingView repeats at one price (tests/fixtures/margin_residual
26553 // px-f2b-eth-short-0/1, -eth-slip-3). Population confirmations:
26554 // third-party ETHUSDT.P 1D and OANDA:EURUSD strategies
26555 // also repeat their calls at the same slipped fill.
26556 // Not the calls after one sized at the source close
26557 // (run_close_sized_open_calls): those are TradingView's whole
26558 // sequence at that print.
26559 const auto unrepeated = PF_RARE(!unrepeated_margin_calls_.empty())
26560 ? std::find(unrepeated_margin_calls_.begin(), unrepeated_margin_calls_.end(),
26561 event.handle().incarnation)
26562 : unrepeated_margin_calls_.end();
26563 const bool sequence_call = unrepeated != unrepeated_margin_calls_.end();
26564 if (sequence_call) unrepeated_margin_calls_.erase(unrepeated);
26565 const bool recheck_at_fill = config_.process_orders_on_close
26566 && !config_.calc_on_order_fills && config_.slippage != 0
26567 && after_margin.signed_units < 0.0
26568 && position_open_phase_ == NativePathPhase::Close
26569 && position_open_script_bar_ != context.script_bar_open_ms
26570 && finite_positive(event.resolved_price) && !sequence_call;
26571 // ab9714be pine_fills.cpp:2525-2678 then :1266-1751:
26572 // after an opening slice, the survivor is checked over the
26573 // unconsumed bar suffix. Submission from this Applied point
26574 // uses A35 remaining-path eligibility and sizes from the
26575 // already-reduced physical book.
26576 if (!recheck_at_fill || !submit_margin_call_slice(
26577 event.resolved_price, context, false, event.resolved_price))
26578 (void)schedule_margin_call_path(policy_script_bar_, context);
26579 }
26580 // A short's call at a path extreme owes the bar's next point its
26581 // lagged follow-up (schedule_lagged_short_follow_up).
26582 if (PF_RARE(after_margin.signed_units < 0.0
26583 && detail::margin_schedule_switches().lagged_short_follow_up))
26584 schedule_lagged_short_follow_up(event, context);
26585 }
26586 if (placement_snapshot->family == PineOrderFamily::Risk
26587 && event.closed_units > 0.0) {
26588 risk_.intraday_block_day = chart_day_key(context.sub_bar_open_ms);
26589 risk_.intraday_cancel_pending = true;
26590 }
26591 }
26592 if (preclose_intraday_loss) {
26593 risk_.intraday_block_day = chart_day_key(context.sub_bar_open_ms);
26594 risk_.intraday_cancel_pending = true;
26595 if (detail::run_position(require_host()).signed_units != 0.0) {
26596 native_order::Request request;
26597 request.intent = native_order::Flatten{};
26598 request.comment = kIntradayLossComment;
26601 snapshot.source_id = "__intraday_loss__";
26602 snapshot.comment = request.comment;
26603 snapshot.sizing = sizing_snapshot();
26604 const auto accepted = submit_or_replace(
26605 std::move(request), std::move(snapshot), false,
26606 "__intraday_loss_close__");
26607 if (accepted) {
26608 (void)require_host().execute_current(
26609 {*accepted, NativeCurrentPriceRule::NearestTick});
26610 }
26611 }
26612 }
26613 if (risk_.intraday_cancel_pending) {
26614 risk_.intraday_cancel_pending = false;
26615 cancel_all();
26616 }
26617 if (placement_snapshot && placement_snapshot->opening
26618 && std::abs(event.opened_units) > 0.0 && policy_script_bar_valid_) {
26619 // Legacy processes the opening-affordability checkpoint at the
26620 // matched entry price before the remainder of that bar's path. The
26621 // native callback is at exactly that current execution point, so the
26622 // adapter can issue the generic reduction synchronously without a
26623 // second matching loop.
26624 const auto opened_position = detail::run_position(require_host());
26625 const double opening_margin = opened_position.signed_units < 0.0
26626 ? config_.margin_short : config_.margin_long;
26627 const bool full_margin_opening = opened_position.signed_units != 0.0
26628 && std::abs(opening_margin - 100.0) < 1e-12;
26629 const bool preopen_margin_already_scheduled = std::any_of(
26630 live_handles_.begin(), live_handles_.end(), [&](const auto& handle) {
26631 const auto pending = placement_.find(handle.incarnation);
26632 return pending != placement_.end()
26633 && pending->second.family == PineOrderFamily::Margin
26634 && pending->second.from_entry == placement_snapshot->source_id
26635 && pending->second.source_id.rfind("__margin_preopen__", 0) == 0;
26636 });
26637 // As in resolve_terms: a flat stop entry is kept while a live or
26638 // queued entry shares its placement bar (erase_retired_rows, K8).
26639 const auto dual_stop_peer = [&](std::uint64_t incarnation, const PlacementSnapshot& peer) {
26640 return incarnation != event.handle().incarnation && peer.opening
26641 && peer.family == PineOrderFamily::Entry
26642 && peer.projection_position_side
26643 == static_cast<std::int32_t>(PositionSide::FLAT)
26644 && peer.projection_created_bar
26645 == placement_snapshot->projection_created_bar
26646 && peer.is_long != placement_snapshot->is_long
26647 && finite_positive(peer.exit_levels.stop)
26648 && !finite_positive(peer.exit_levels.limit);
26649 };
26650 const bool dual_stop_candidate = last_bar_dual_entry_path_ != 0
26651 && placement_snapshot->projection_position_side
26652 == static_cast<std::int32_t>(PositionSide::FLAT)
26653 && finite_positive(placement_snapshot->exit_levels.stop)
26654 && !finite_positive(placement_snapshot->exit_levels.limit);
26655 if (dual_stop_candidate)
26656 PINEFORGE_AUDIT_PLACEMENT_SCAN(placement_, dual_stop_peer);
26657 const bool flat_dual_stop_member = dual_stop_candidate
26658 && std::any_of(placement_.begin(), placement_.end(),
26659 [&](const auto& row) { return dual_stop_peer(row.first, row.second); });
26660 // Timestamped FX has its own base-equivalent opening checkpoint
26661 // (apply_fx_opening_margin_slice). A generic fill-price retry here
26662 // would replay a rate epoch that was consumed while the host was
26663 // flat, producing a false margin row on the subsequent opening.
26664 const bool stable_opening_fx = staged_.account_fx_effective_from_ms.empty()
26665 || placement_snapshot->sizing.fx
26666 == active_staged_fx(context.sub_bar_open_ms);
26667 // A book two or more of one bar's flat entries of this side opened
26668 // here, this fill included, is margined as one once the last of them
26669 // has filled (flat_sibling_fill_follows): at that fill, or under
26670 // process_orders_on_close at the next open (lab tv tapes
26671 // tests/fixtures/margin_residual w5b-sb-*; lane W5B-ENG-MARGIN-RESIDUAL).
26672 const bool sibling_book = opened_position.lot_count >= 2
26673 && flat_sibling_book(*placement_snapshot, context.coordinate.interval_index,
26674 context.coordinate.path_phase);
26675 const bool sibling_fill_follows = sibling_book
26676 || opened_position.lot_count == 1
26677 ? flat_sibling_fill_follows(*placement_snapshot, event.handle()) : false;
26678 // So is a book the adds one bar placed on it grew: once the last of
26679 // them has filled, at that fill -- or under process_orders_on_close
26680 // at the next open, sized at the close (on_bar_open) -- four times
26681 // the lot-floored shortfall of the whole book, not a call after
26682 // each add (lab tv tapes tests/fixtures/add_sibling_margin; lane
26683 // TAIL-B).
26684 const bool placed_add = !sibling_book && add_entry_row(*placement_snapshot);
26685 const bool add_fill_follows = placed_add
26686 && add_sibling_fill_follows(*placement_snapshot, event.handle());
26687 const std::size_t filled_adds = placed_add && !add_fill_follows
26688 ? add_sibling_lots(placement_snapshot->placement_script_open_ms) : 0;
26689 const bool pooc_add_book = placed_add && config_.process_orders_on_close
26690 && !config_.calc_on_order_fills
26691 && !context.driver_statistics.intrabar_path_enabled
26692 && context.coordinate.path_phase == NativePathPhase::Close
26693 && (add_fill_follows || filled_adds >= 2);
26694 // The last of a bar's market adds: the grown book's call is taken at
26695 // this fill, a commissioned explicit short's too (its print, as a
26696 // slipped market short's), not at a later path point.
26697 const bool adds_book_last = placed_add && !config_.process_orders_on_close
26698 && filled_adds >= 2;
26699 const bool zero_fee_true_flat_default = !sibling_book
26700 && config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
26701 && std::abs(config_.default_qty_value - 100.0) < 1e-12
26702 && config_.commission_value == 0.0
26703 && !std::isfinite(placement_snapshot->requested_qty)
26704 && placement_snapshot->projection_position_side
26705 == static_cast<std::int32_t>(PositionSide::FLAT)
26706 && !placement_snapshot->projection_after_close
26707 && std::holds_alternative<native_order::Market>(event.request().trigger)
26708 && placement_snapshot->sizing.price == event.resolved_price;
26709 if ((full_margin_opening || placement_snapshot->has_full_entry_bracket)
26710 && !preopen_margin_already_scheduled && stable_opening_fx) {
26711 const bool long_full_margin = opened_position.signed_units > 0.0
26712 && std::abs(config_.margin_long - 100.0) < 1e-12;
26713 const bool prearmed_entry_bar_margin = std::any_of(
26714 live_handles_.begin(), live_handles_.end(), [&](const auto& handle) {
26715 const auto pending = placement_.find(handle.incarnation);
26716 return pending != placement_.end()
26717 && pending->second.family == PineOrderFamily::Margin
26718 && pending->second.from_entry == placement_snapshot->source_id;
26719 });
26720 // A market entry opening the book from flat at the bar's open gives
26721 // way to the exit legs it releases only where they are already
26722 // marketable at that open: they fill there first and the check
26723 // runs on the book they leave, after them (open_exit_first_bar_).
26724 // Every other leg and fill -- a reversal, a stop or limit entry,
26725 // an add, a book of several entries, process_orders_on_close and
26726 // calc_on_order_fills -- keeps ab9714be's rule
26727 // (priced_exit_precedes_opening), though TradingView calls a
26728 // short reversal and a short stop entry gapped at the open before
26729 // their stop on the path too (lab tv tapes tests/fixtures/
26730 // margin_open_rules exit-order/reversal-*, stop-entry-*: known
26731 // divergences).
26732 const bool open_market_fill = detail::margin_opening_switches().open_marketable_exit_first
26733 && context.coordinate.path_phase == NativePathPhase::Open
26734 && context.sub_index == 0
26735 && policy_script_bar_.timestamp == context.script_bar_open_ms
26736 && std::holds_alternative<native_order::Market>(event.request().trigger)
26737 && !config_.process_orders_on_close && !config_.calc_on_order_fills
26738 && event.closed_units == 0.0 && opened_position.lot_count == 1
26739 && !sibling_book && !placed_add;
26740 bool exit_first_at_open = false;
26741 const bool short_preempted_by_priced_exit = priced_exit_precedes_opening(
26742 opened_position.signed_units < 0.0,
26743 open_market_fill ? event.handle().incarnation : 0, placement_snapshot->source_id,
26744 context, nullptr, &exit_first_at_open);
26745 // TradingView takes no call at the close fill that opens (or
26746 // reverses into) a commissioned process_orders_on_close position:
26747 // it calls that book at the next open instead (on_bar_open) --
26748 // a long for the units this checkpoint would take here, a short
26749 // at the close's mark (lab tv tapes tests/fixtures/margin_v6
26750 // w5-c2-pooc-rev-long, -rev-long-noslip and -rev-short: every
26751 // reversal's call at the next open, fifteen cells each; lane
26752 // W5-ENG-MARGIN-V6).
26753 const bool called_at_next_open = config_.process_orders_on_close
26754 && !config_.calc_on_order_fills
26755 && (config_.commission_value != 0.0 || sibling_book)
26756 && !context.driver_statistics.intrabar_path_enabled
26757 && context.coordinate.path_phase == NativePathPhase::Close
26758 && position_open_phase_ == NativePathPhase::Close
26759 && position_open_script_bar_ == context.script_bar_open_ms;
26760 const bool stopped_flat_reversal = opened_position.signed_units < 0.0
26761 && std::abs(config_.margin_short - 100.0) < 1e-12 && event.closed_units == 0.0
26762 && placement_snapshot->reverse_to
26763 && std::holds_alternative<native_order::Stop>(event.request().trigger)
26764 && context.coordinate.path_phase != NativePathPhase::Open;
26765 const bool priced_opening_waypoint = (stopped_flat_reversal
26766 || (long_full_margin && !std::isfinite(placement_snapshot->requested_qty)
26767 && context.coordinate.path_phase != NativePathPhase::Open))
26768 && position_open_priced_
26769 && placement_snapshot->family == PineOrderFamily::Entry
26770 && std::holds_alternative<native_order::Stop>(event.request().trigger)
26771 && !sibling_book && !placed_add
26772 && !config_.process_orders_on_close && !config_.calc_on_order_fills
26773 && modeled_input() && !bar_magnifier_;
26774 // The 10-significant-digit long residual is same-currency,
26775 // pointvalue-one policy. A non-unit point value does not inherit
26776 // an exact-money opening slice merely because the generic
26777 // floating ledger rounds its fill cost differently.
26778 // Margin rule 3: nor at the close fill that opens a
26779 // commission-free slipped full-margin long; the next open checks
26780 // it (slipped_long_margin_units).
26781 const bool slipped_long_called_at_next_open = config_.process_orders_on_close
26782 && long_full_margin && slipped_long_margin_scope()
26783 && context.coordinate.path_phase == NativePathPhase::Close
26784 && position_open_script_bar_ == context.script_bar_open_ms
26785 && slipped_long_unit_shortfall(nearest_tick(policy_script_bar_.close,
26786 staged_.syminfo.mintick));
26787 const bool opening_check_due = !zero_fee_true_flat_default && !called_at_next_open
26788 && !priced_opening_waypoint && !slipped_long_called_at_next_open
26789 && !sibling_fill_follows && !add_fill_follows && !pooc_add_book
26790 && !flat_dual_stop_member && !prearmed_entry_bar_margin
26791 && !(long_full_margin && !sibling_book
26792 && std::abs(staged_.syminfo.pointvalue - 1.0) > 1e-12);
26793 if (opening_check_due && !short_preempted_by_priced_exit
26794 && !same_point_pair_fill_follows(*placement_snapshot, event.handle(),
26795 event.resolved_price)) {
26796 run_opening_checkpoint(
26797 event.resolved_price,
26798 std::holds_alternative<native_order::Market>(event.request().trigger),
26799 sibling_book, adds_book_last, context);
26800 } else if (opening_check_due && exit_first_at_open
26801 && !same_point_pair_fill_follows(*placement_snapshot, event.handle(),
26802 event.resolved_price)) {
26803 // The exit legs marketable at this open fill there first: the
26804 // check is owed to the last of them, on the book they leave
26805 // (lab tv tapes tests/fixtures/margin_open_rules exit-order/).
26806 open_exit_first_bar_ = context.script_bar_open_ms;
26807 open_exit_first_origin_ = event.handle().incarnation;
26808 open_exit_first_id_ = placement_snapshot->source_id;
26809 open_exit_first_fill_ = event.resolved_price;
26810 }
26811 const bool terminal_pooc_open = config_.process_orders_on_close
26812 && position_open_phase_ == NativePathPhase::Close;
26813 if (!terminal_pooc_open && !flat_dual_stop_member
26814 && !prearmed_entry_bar_margin) {
26815 if (priced_opening_waypoint) {
26816 (void)schedule_priced_opening_margin(
26817 policy_script_bar_, context, event.resolved_price);
26818 } else if (long_full_margin) {
26819 (void)schedule_tv_money_long_margin_before_trail(
26820 policy_script_bar_, context);
26821 } else {
26822 schedule_margin_call_path(policy_script_bar_, context);
26823 }
26824 }
26825 } else if (!full_margin_opening && !placement_snapshot->has_full_entry_bracket
26826 && !preopen_margin_already_scheduled && stable_opening_fx
26827 && leveraged_entry_bar_checked()
26828 && !preopen_slice_class(*placement_snapshot, context)) {
26829 // A leveraged opening (margin below 100) has no fill-price trim,
26830 // but TradingView still checks its ENTRY bar over the path after
26831 // the fill: four lab tv tapes (tests/fixtures/margin_entry_bar)
26832 // book the margin call at that bar's low, as ab9714be's
26833 // entry-bar pass did (pine_fills.cpp:1025-1063 and :1535-1546,
26834 // the suffix after the opening's fill). The run shapes ab9714be
26835 // took no such suffix on keep their own routes here
26836 // (leveraged_entry_bar_checked), and so does the opening
26837 // schedule_preopen_margin_slice answers for, sliced or not
26838 // (preopen_slice_class). Admit the kernel's post-fill point for
26839 // this driver point: it measures the same suffix and sizes the
26840 // call with TradingView's money and slice
26841 // (resolve_margin_requirement, resolve_margin_call_units). Every
26842 // opening-side fill is admitted on its own bar -- the position's
26843 // first, an add on the same bar, and an add to a book carried in
26844 // from an earlier bar, which TradingView checks over the rest of
26845 // its bar on the combined book too (the intrabar margin correction,
26846 // pm2-m7b-lim-* and pm2-m7b-mkt-*) -- so an add re-sizes a call
26847 // armed on the smaller book.
26848 // The open's own check runs after the market adds that fill at
26849 // that open, on the book holding them and their entry fees
26850 // (switch open_check_after_add_fill): on OANDA:EURUSD at margin
26851 // 1 %, a synthetic short of 20000 adding 20000 at a 2.00000 open is
26852 // short of margin there only with the add and its fee, and
26853 // TradingView checks the combined book at that open before the path's
26854 // high-point check (lab tv tapes tests/fixtures/
26855 // margin_schedule_rules add-fee/, margin_open_rules add-open/).
26856 if (PF_RARE(detail::margin_opening_switches().open_check_after_add_fill)
26857 && placed_add && !add_fill_follows && !config_.process_orders_on_close
26858 && !config_.calc_on_order_fills && config_.slippage == 0
26859 && config_.commission_type == static_cast<int>(CommissionType::PERCENT)
26860 && config_.commission_value > 0.0
26861 && context.coordinate.path_phase == NativePathPhase::Open
26862 && std::holds_alternative<native_order::Market>(event.request().trigger)
26863 && policy_script_bar_.timestamp == context.script_bar_open_ms
26864 && staged_.account_fx_effective_from_ms.empty()
26865 && finite_positive(policy_script_bar_.open)) {
26866 (void)submit_margin_call_slice(
26867 nearest_tick(policy_script_bar_.open, staged_.syminfo.mintick), context);
26868 }
26869 (void)schedule_margin_call_path(policy_script_bar_, context);
26870 }
26871 schedule_intraday_loss_path(policy_script_bar_, context);
26872 }
26873 if (placement_snapshot && placement_snapshot->family == PineOrderFamily::Close
26874 && context.coordinate.path_phase == NativePathPhase::Open
26875 && detail::run_position(require_host()).signed_units != 0.0
26876 && policy_script_bar_valid_ && !config_.process_orders_on_close
26877 && staged_.account_fx_effective_from_ms.empty()) {
26878 // The retired ordinary scheduler settled old MARKET closes before its
26879 // pre-script adverse-margin checkpoint. Recompute the path slice from
26880 // the post-close physical book so a partial close cannot leave behind
26881 // a fixed liquidation request sized on the larger pre-open position.
26882 detail::ExitScratchRows<native_order::RequestHandle> stale_margin_requests(*this);
26883 for (const auto& handle : live_handles_) {
26884 const auto pending = placement_.find(handle.incarnation);
26885 if (pending != placement_.end()
26886 && pending->second.family == PineOrderFamily::Margin) {
26887 stale_margin_requests.push_back(handle);
26888 }
26889 }
26890 for (const auto& handle : stale_margin_requests) {
26891 const auto cancelled = require_host().cancel(handle);
26892 if (cancelled.status == native_order::CancelStatus::Cancelled)
26893 retire(handle);
26894 }
26895 (void)schedule_margin_call_path(policy_script_bar_, context);
26896 }
26897 // The opening checkpoint a market entry's fill at this open owes the exit
26898 // legs it released, marketable there (open_exit_first_bar_): due once the
26899 // last of them has filled -- at the open's price, on the bar's first
26900 // segment -- at the same open on the book they leave: a short of 3.5 ETH
26901 // whose 0.0004 stop fills at the open calls 0.0032 there, not 0.0048
26902 // before the stop nor nothing (lab tv tape tests/fixtures/
26903 // margin_open_rules exit-order/short-partial-stop-at-open). A book they
26904 // closed owes nothing.
26905 if (PF_RARE(open_exit_first_bar_ != std::numeric_limits<std::int64_t>::min())
26906 && placement_snapshot && event.closed_units > 0.0
26907 && (placement_snapshot->family == PineOrderFamily::ExitLimit
26908 || placement_snapshot->family == PineOrderFamily::ExitStop)) {
26909 const auto after_exit = detail::run_position(require_host());
26910 const bool short_book = after_exit.signed_units < 0.0;
26911 const bool owed = after_exit.signed_units != 0.0
26912 && open_exit_first_bar_ == context.script_bar_open_ms
26913 && policy_script_bar_.timestamp == context.script_bar_open_ms;
26914 const bool released_leg_at_open = owed
26915 && opening_released_leg(*placement_snapshot, event.handle().incarnation,
26916 open_exit_first_origin_,
26917 open_exit_first_id_, short_book, context)
26918 && exit_leg_marketable_at(placement_snapshot->exit_levels, short_book,
26919 nearest_tick(policy_script_bar_.open,
26920 staged_.syminfo.mintick));
26921 if (!owed || (released_leg_at_open
26922 && !priced_exit_precedes_opening(short_book, open_exit_first_origin_,
26923 open_exit_first_id_, context,
26924 &*placement_snapshot, nullptr))) {
26925 const double fill = open_exit_first_fill_;
26926 open_exit_first_bar_ = std::numeric_limits<std::int64_t>::min();
26927 open_exit_first_origin_ = 0;
26928 open_exit_first_id_.clear();
26929 open_exit_first_fill_ = 0.0;
26930 if (owed) {
26931 // The check is the open's: its path, and its follow-up, go on from there.
26932 NativeDecisionContext at_open = context;
26933 at_open.coordinate.path_phase = NativePathPhase::Open;
26934 run_opening_checkpoint(fill, true, false, false, at_open);
26935 }
26936 }
26937 }
26938 if (placement_snapshot && event.closed_units > 0.0
26939 && (placement_snapshot->family == PineOrderFamily::ExitLimit
26940 || placement_snapshot->family == PineOrderFamily::ExitStop
26941 || placement_snapshot->family == PineOrderFamily::ExitTrail)
26942 && policy_script_bar_valid_
26943 && policy_script_bar_.timestamp == context.script_bar_open_ms
26944 && !config_.process_orders_on_close && !config_.calc_on_order_fills
26945 && staged_.account_fx_effective_from_ms.empty()) {
26946 // ab9714be pine_scheduler.cpp:267-282: the ordinary margin call runs
26947 // once after every order of the bar, on the post-fill book; only an
26948 // adverse extreme that strictly precedes the priced exit's fill takes
26949 // the slice ahead of it (pine_fills.cpp:2224), and that slice has
26950 // already settled by now, as has a carried open-price slice
26951 // (pine_scheduler.cpp:177-183). A path slice still live here was sized on
26952 // the pre-exit position, so re-size it from the reduced book. The
26953 // 1x-long TV-money slice keeps its own scheduler.
26954 const auto after_exit = detail::run_position(require_host());
26955 const bool one_x_long = after_exit.signed_units > 0.0
26956 && std::abs(config_.margin_long - 100.0) < 1e-12;
26957 detail::ExitScratchRows<native_order::RequestHandle> stale_margin_requests(*this);
26958 for (const auto& handle : live_handles_) {
26959 const auto pending = placement_.find(handle.incarnation);
26960 if (pending != placement_.end()
26961 && pending->second.family == PineOrderFamily::Margin) {
26962 stale_margin_requests.push_back(handle);
26963 }
26964 }
26965 if (!one_x_long && !stale_margin_requests.empty()) {
26966 for (const auto& handle : stale_margin_requests) {
26967 const auto cancelled = require_host().cancel(handle);
26968 if (cancelled.status == native_order::CancelStatus::Cancelled)
26969 retire(handle);
26970 }
26971 if (after_exit.signed_units != 0.0)
26972 (void)schedule_margin_call_path(policy_script_bar_, context);
26973 } else if (!one_x_long) {
26974 // R5: under the kernel margin model the live path slice is the
26975 // kernel's, so there is no source row here to cancel and the
26976 // scan above finds nothing. The re-size still has to happen --
26977 // a slice sized on the pre-exit book must not survive the exit
26978 // and execute against the remainder -- so admit the kernel's own
26979 // check point for this driver point instead. Gated on a resting
26980 // slice in margin_check_allowed, so a point where the legacy
26981 // broker did nothing stays a point where nothing is done.
26982 kernel_margin_resize_point_ = context.coordinate.ordinal;
26983 if (after_exit.signed_units != 0.0)
26984 (void)schedule_margin_call_path(policy_script_bar_, context);
26985 }
26986 }
26987 apply_fx_opening_margin_slice(event, context);
26988 refresh_pending_view();
26989 // ab9714be pine_fills.cpp:7026-7033 and pine_orders.cpp:444-480:
26990 // after a priced from_entry bracket leg fills, every same-id MARKET
26991 // pyramid add opened on this bar is still open behind the leg's own
26992 // (FIFO) reduction; the owner covers it at the leg's booked price as a
26993 // second fill of the same order.
26994 if (placement_snapshot && event.closed_units > 0.0
26995 && (placement_snapshot->family == PineOrderFamily::ExitLimit
26996 || placement_snapshot->family == PineOrderFamily::ExitStop
26997 || placement_snapshot->family == PineOrderFamily::ExitTrail)
26998 && !placement_snapshot->from_entry.empty()
26999 && detail::run_position(require_host()).signed_units != 0.0
27000 && current_position_cycle_ > 0) {
27001 auto* pine = pine_view_of(&require_host());
27002 const auto cohort = cohorts_by_id_.find(placement_snapshot->from_entry);
27003 if (pine && cohort != cohorts_by_id_.end()) {
27004 // The kernel books lot rows in script-bar space. This owner-side
27005 // cohort rule intentionally compares the explicit chart source
27006 // index to keep Pine's sampling cadence separate from that book.
27007 const auto state = detail::run_state(*pine);
27008 const int ratio = state.spec && !state.spec->timeframe_undetected
27009 ? tf_ratio(state.spec->input_tf, state.spec->script_tf) : 1;
27010 const int bar = pine->scheduler_.bar_magnifier_enabled() || ratio > 1 || ratio == -1
27011 ? pine->scheduler_.source_bar_index_for(context)
27012 : context.coordinate.interval_index;
27014 detail::ExitScratchIndex opened(adds.context());
27015 detail::ExitScratchIndex selected_adds(adds.context());
27016 for (const auto& opening : cohort->second.opened)
27017 opened.put(opening.incarnation, 0);
27018 double units = 0.0;
27019 for (const auto& lot : pine->pyramid_entries_) {
27020 if (!market_pyramid_add(lot.entry_incarnation)
27021 || lot.entry_bar_index != bar
27022 || lot.entry_id != placement_snapshot->from_entry
27023 || !(lot.qty > internal::kQtyEpsilon)) {
27024 continue;
27025 }
27026 if (opened.find(lot.entry_incarnation) == detail::ExitScratchIndex::absent)
27027 continue;
27028 adds.push_back({adds.context().run, lot.entry_incarnation});
27029 selected_adds.put(lot.entry_incarnation, 0);
27030 units += lot.qty;
27031 }
27032 // The cover stands behind the leg's own reduction of an OLDER lot
27033 // of the id: a leg whose fill reduced only the add, or (FIFO) a
27034 // lot of another id, keeps to its own quantity
27035 // (tests/fixtures/ki62_same_id_cover).
27036 bool drained_older = false;
27037 for (std::size_t i = 0; i < event.closed_trade_count; ++i) {
27038 const std::size_t row = event.first_trade_index + i;
27039 drained_older = drained_older || (row < pine->trades_.size()
27040 && pine->trades_[row].entry_id == placement_snapshot->from_entry
27041 && selected_adds.find(pine->trades_[row].entry_incarnation)
27043 }
27044 if (!adds.empty() && units > 0.0 && drained_older) {
27045 native_order::Request request;
27046 request.intent = native_order::Reduce{native_order::ExplicitUnits{units}};
27047 request.label = placement_snapshot->source_id;
27048 request.comment = placement_snapshot->comment;
27049 request.trigger = native_order::Market{};
27051 adds.context().run, current_position_cycle_, adds.size(), &adds,
27052 [](void* opaque, std::uint64_t index, native_order::RequestHandle& result) {
27053 const auto& rows = *static_cast<const detail::ExitScratchRows<
27054 native_order::RequestHandle>*>(opaque);
27055 if (index >= rows.size()) return detail::ScratchStatus::not_found;
27056 try {
27057 result = rows[index];
27058 return detail::ScratchStatus::ok;
27059 } catch (const detail::ExitScratchError& error) {
27060 return error.status();
27061 }
27062 });
27063 request.group = native_order::NoGroup{};
27064 PlacementSnapshot cover = *placement_snapshot;
27065 cover.requested_qty = units;
27066 cover.immediately = true;
27067 cover.deferred_cohort = false;
27068 cover.forced_execution_price = event.resolved_price;
27069 const auto accepted = submit_or_replace(
27070 std::move(request), std::move(cover), false,
27071 placement_snapshot->source_id + "\x1f" + placement_snapshot->from_entry
27072 + "\x1f" + "cover");
27073 if (accepted) {
27074 (void)require_host().execute_current(
27075 {*accepted, NativeCurrentPriceRule::NearestTick});
27076 }
27077 }
27078 }
27079 }
27080 // A same-bar strategy.close or close_all that fills at a
27081 // process_orders_on_close close is followed there by the reversing stop
27082 // entries the close pass left for it (the recalculation fill-order correction).
27083 if (placement_snapshot && event.closed_units > 0.0
27084 && (placement_snapshot->family == PineOrderFamily::Close
27085 || placement_snapshot->family == PineOrderFamily::CloseAll)
27086 && placement_snapshot->projection_created_bar == projection_bar_index(context)
27087 && config_.process_orders_on_close && !config_.calc_on_order_fills
27088 && modeled_input() && !coof_recalc_active_
27089 && event.cursor.point.path_phase == NativePathPhase::Close) {
27090 fill_pooc_close_entries(event.raw_price, context, /*after_close=*/true);
27091 }
27092 // Once the calculation's last order that fills at a process_orders_on_close
27093 // close has filled there, the pass decides the calculation's exits,
27094 // including those placed with the entry that just opened or reversed the
27095 // position (fill_pooc_close_exits; lane W4-ENG-POOC-SAMEPASS).
27096 // The close-pass immediate of a priced entry fills with the calculation's
27097 // own provenance; its exits are decided once the last one has filled.
27098 const bool close_pass_entry_filled = placement_snapshot
27099 && placement_snapshot->family == PineOrderFamily::Entry && placement_snapshot->opening
27100 && context.coordinate.provenance == NativePriceProvenance::Calculation
27101 && std::isfinite(placement_snapshot->forced_execution_price)
27102 && (std::isfinite(placement_snapshot->exit_levels.limit)
27103 || std::isfinite(placement_snapshot->exit_levels.stop))
27104 && !pooc_close_entry_immediate_pending(context, &event.handle());
27105 if (placement_snapshot && config_.process_orders_on_close
27106 && modeled_input() && !coof_recalc_active_
27107 && (context.coordinate.provenance == NativePriceProvenance::AfterCalculationClose
27108 || close_pass_entry_filled)
27109 && placement_snapshot->projection_created_bar == projection_bar_index(context)
27110 && (placement_snapshot->opening
27111 || placement_snapshot->family == PineOrderFamily::Close
27112 || placement_snapshot->family == PineOrderFamily::CloseAll
27113 || placement_snapshot->family == PineOrderFamily::Order)
27114 && !pooc_close_market_pending(context)) {
27115 fill_pooc_close_exits(event.raw_price, context);
27116 }
27117 // A path point's call leaves a book the rest of the path checks again
27118 // (schedule_margin_call_path).
27119 if (placement_snapshot && placement_snapshot->waypoint_margin_call
27120 && event.closed_units > 0.0 && policy_script_bar_valid_
27121 && waypoint_chain_bar_ == context.script_bar_open_ms
27122 && detail::run_position(require_host()).signed_units != 0.0) {
27123 const bool high_first = source_path_uses_high_first(policy_script_bar_);
27124 const auto phase = context.coordinate.path_phase;
27125 const int filled_at = phase == NativePathPhase::Open ? 0
27126 : phase == NativePathPhase::Close ? 3
27127 : ((phase == NativePathPhase::High) == high_first ? 1 : 2);
27128 const double print = filled_at == 0 ? policy_script_bar_.open
27129 : filled_at == 3 ? policy_script_bar_.close
27130 : phase == NativePathPhase::High ? policy_script_bar_.high : policy_script_bar_.low;
27131 withdraw_waypoint_margin_calls();
27132 // A call so taken is followed as one the kernel's check takes.
27133 follow_margin_call(event.closed_units, event.resolved_price, print, context);
27134 if (detail::run_position(require_host()).signed_units != 0.0)
27135 (void)rest_waypoint_margin_call(policy_script_bar_, filled_at + 1, 3, context);
27136 }
27137 // The kernel's path check calls as the opening print does, and is
27138 // followed alike (follow_margin_call).
27139 if (event.definition
27140 && event.definition->origin == native_order::RequestOrigin::KernelLiquidation
27141 && event.closed_units > 0.0) {
27142 follow_margin_call(event.closed_units, event.resolved_price, event.raw_price, context);
27143 }
27144}
27145
27147 // Only the plan actually selected at the qualifying broker open projects
27148 // executable roles. Retained immutable handles are lifecycle/hash facts,
27149 // not historical role authority.
27150 if (!short_seed_.active) return 0;
27151 if (handle == short_seed_.long_entry) return 1;
27152 if (handle == short_seed_.materialize_long) return 2;
27153 if (handle == short_seed_.final_short) return 3;
27154 return 0;
27155}
27156
27157bool PineExecutionAdapter::take_intraday_loss_relabel(std::uint64_t ordinal) noexcept {
27158 const bool relabel = intraday_loss_relabel_ordinals_.erase(ordinal) != 0;
27159 // The host clears exit_id immediately after this receipt is taken.
27160 if (relabel && position_tail_summary_)
27161 position_tail_summary_->invalidate(PositionTailGuard::Relabel,
27163 return relabel;
27164}
27165
27166std::vector<PineExecutionAdapter::FixturePendingSnapshot>
27168 std::vector<FixturePendingSnapshot> rows;
27169 rows.reserve(live_handles_.size() + pending_entries_.size()
27170 + pending_bracket_legs_.size()
27171 + pending_same_bar_commands_.size()
27172 + pending_coof_requests_.size()
27173 + source_shadow_pending_.size());
27174 // One above every incarnation a row was ever remembered under, erased
27175 // rows included (the retired-row compaction).
27176 std::uint64_t next_incarnation = std::max<std::uint64_t>(1, placement_.high_water() + 1);
27177 struct StagedParentProjection {
27178 SourceId id;
27179 std::uint64_t command_sequence = 0;
27180 std::uint64_t incarnation = 0;
27181 };
27182 std::vector<StagedParentProjection> staged_parents;
27183 for (const auto& handle : live_handles_) {
27184 const auto found = placement_.find(handle.incarnation);
27185 if (found != placement_.end()) rows.push_back({handle.incarnation, found->second, false});
27186 }
27187 for (const auto& pending : pending_entries_) {
27188 const std::uint64_t incarnation = next_incarnation++;
27189 rows.push_back({incarnation, pending.snapshot, true});
27190 staged_parents.push_back({pending.snapshot.source_id,
27191 pending.snapshot.command_sequence,
27192 incarnation});
27193 }
27194 std::vector<std::uint64_t> pending_bracket_families;
27195 for (const auto& pending : pending_bracket_legs_) {
27196 if (std::find(pending_bracket_families.begin(), pending_bracket_families.end(),
27197 pending.family_key) != pending_bracket_families.end()) {
27198 continue;
27199 }
27200 pending_bracket_families.push_back(pending.family_key);
27201 std::uint64_t incarnation = 0;
27202 if (pending.snapshot.defer_until_post_parent_calculation
27203 && pending.snapshot.legs.target().incarnation != 0) {
27204 incarnation = pending.snapshot.legs.target().incarnation;
27205 } else {
27206 const auto parent = std::find_if(
27207 staged_parents.begin(), staged_parents.end(),
27208 [&](const StagedParentProjection& row) {
27209 return row.id == pending.snapshot.from_entry
27210 && pending.snapshot.command_sequence >= row.command_sequence;
27211 });
27212 if (parent != staged_parents.end()) {
27213 incarnation = parent->incarnation
27214 + (pending.snapshot.command_sequence - parent->command_sequence);
27215 next_incarnation = std::max(next_incarnation, incarnation + 1);
27216 } else {
27217 incarnation = next_incarnation++;
27218 }
27219 }
27220 rows.push_back({incarnation, pending.snapshot, true});
27221 }
27222 for (const auto& pending : pending_same_bar_commands_)
27223 rows.push_back({0, pending.snapshot, true});
27224 for (const auto& pending : pending_coof_requests_)
27225 rows.push_back({0, pending.snapshot, true});
27226 for (const auto& shadow : source_shadow_pending_)
27227 rows.push_back({0, shadow.snapshot, true});
27228 return rows;
27229}
27230
27232 const SourceId& id) const noexcept {
27233 const auto found = close_logical_units_.find(id);
27234 return found == close_logical_units_.end() ? 0.0 : found->second;
27235}
27236
27237std::vector<PineExecutionAdapter::FixtureCloseCallsite>
27239 std::vector<FixtureCloseCallsite> result;
27240 result.reserve(close_batch_callsites_.size());
27241 for (const auto& row : close_batch_callsites_) {
27242 const auto& site = row.second;
27243 result.push_back({site.token, site.active, site.target, site.calls,
27244 site.id, site.comment, site.queue_sequence});
27245 }
27246 return result;
27247}
27248
27251 if (index < 0 || index >= static_cast<int>(live_handles_.size())) return nullptr;
27252 const auto handle = live_handles_[static_cast<std::size_t>(index)];
27253 const auto found = placement_.find(handle.incarnation);
27254 return found == placement_.end() ? nullptr : &found->second.cancellation;
27255}
27256
27258 const SourceId& id) {
27259 pending_write(pending_entries_).erase(std::remove_if(
27260 pending_entries_.begin(), pending_entries_.end(),
27261 [&](const PendingEntry& pending) {
27262 return pending.snapshot.family == PineOrderFamily::Entry
27263 && pending.snapshot.source_id == id;
27264 }), pending_entries_.end());
27265 std::vector<native_order::RequestHandle> matches;
27266 for (const auto& handle : live_handles_) {
27267 const auto found = placement_.find(handle.incarnation);
27268 if (found != placement_.end()
27269 && found->second.family == PineOrderFamily::Entry
27270 && found->second.source_id == id) {
27271 matches.push_back(handle);
27272 }
27273 }
27274 for (const auto& handle : matches) {
27275 const auto result = require_host().cancel(handle);
27276 if (result.status == native_order::CancelStatus::Cancelled) retire(handle);
27277 }
27278 named_entry_cancel_tokens_.erase(id);
27279}
27280
27281void PineExecutionAdapter::set_risk_direction(int direction) noexcept { risk_.direction = direction; }
27282void PineExecutionAdapter::set_risk_max_cons_loss_days(int value) noexcept { risk_.max_cons_loss_days = value; }
27283void PineExecutionAdapter::set_risk_max_drawdown(double value, bool percent) noexcept {
27284 risk_.max_drawdown = value;
27285 if (percent) risk_.max_drawdown_percent = true;
27286}
27287void PineExecutionAdapter::set_risk_max_intraday_loss(double value, bool percent) noexcept {
27288 risk_.max_intraday_loss = value;
27289 if (percent) risk_.max_intraday_loss_percent = true;
27290}
27291void PineExecutionAdapter::set_risk_max_position_size(double value) noexcept { risk_.max_position_size = value; }
27293 PineReplayRiskConfig snapshot;
27294 snapshot.direction = risk_.direction;
27295 snapshot.max_cons_loss_days = risk_.max_cons_loss_days;
27296 snapshot.max_drawdown = risk_.max_drawdown;
27297 snapshot.max_drawdown_percent = risk_.max_drawdown_percent;
27298 snapshot.max_intraday_loss = risk_.max_intraday_loss;
27299 snapshot.max_intraday_loss_percent = risk_.max_intraday_loss_percent;
27300 snapshot.max_position_size = risk_.max_position_size;
27301 snapshot.cap_attachment = cap.attachment();
27302 snapshot.cap_configuration = cap.configuration();
27303 snapshot.cap_declared = cap.declared();
27304 return snapshot;
27305}
27307 const PineReplayRiskConfig& snapshot) noexcept {
27308 risk_.direction = snapshot.direction;
27309 risk_.max_cons_loss_days = snapshot.max_cons_loss_days;
27310 risk_.max_drawdown = snapshot.max_drawdown;
27311 risk_.max_drawdown_percent = snapshot.max_drawdown_percent;
27312 risk_.max_intraday_loss = snapshot.max_intraday_loss;
27313 risk_.max_intraday_loss_percent = snapshot.max_intraday_loss_percent;
27314 risk_.max_position_size = snapshot.max_position_size;
27315 cap.restore_replay_configuration(snapshot.cap_attachment, snapshot.cap_configuration,
27316 snapshot.cap_declared);
27317}
27319 source_margin_call_enabled_ = enabled;
27320}
27321void PineExecutionAdapter::mark_market_pyramid_add(std::uint64_t incarnation) {
27322 market_pyramid_adds_.insert(incarnation);
27323}
27326
27327std::vector<native_order::RequestHandle> PineExecutionAdapter::take_first_open_newborns() {
27328 auto result = std::move(first_open_newborns_);
27329 first_open_newborns_.clear();
27330 return result;
27331}
27332
27333void PineExecutionAdapter::refresh_pending_view() noexcept {
27334 ++pending_projection_version_;
27335 projected_pending_valid_ = false;
27336 std::vector<int>{}.swap(projected_pending_indexes_);
27337 std::vector<int>{}.swap(projected_pending_next_);
27338}
27339
27340int PineExecutionAdapter::projected_raw_pending_size() const noexcept {
27341 const std::size_t total = live_handles_.size()
27342 + pending_same_bar_commands_.size() + pending_entries_.size()
27343 + pending_bracket_legs_.size() + pending_coof_requests_.size()
27344 + source_shadow_pending_.size();
27345 return total > static_cast<std::size_t>(std::numeric_limits<int>::max())
27346 ? std::numeric_limits<int>::max() : static_cast<int>(total);
27347}
27348
27349bool PineExecutionAdapter::projected_raw_pending_at(
27350 int index, const PlacementSnapshot*& snapshot,
27351 native_order::RequestHandle& handle) const noexcept {
27352 ++projected_raw_pending_calls_; snapshot = nullptr;
27353 handle = {};
27354 if (index < 0) return false;
27355 std::size_t offset = static_cast<std::size_t>(index);
27356 if (offset < live_handles_.size()) {
27357 handle = live_handles_[offset];
27358 const auto found = placement_.find(handle.incarnation);
27359 if (found == placement_.end()) return false;
27360 snapshot = &found->second;
27361 return true;
27362 }
27363 offset -= live_handles_.size();
27364 const auto locate = [&](const auto& rows, auto read) {
27365 if (offset >= rows.size()) {
27366 offset -= rows.size();
27367 return false;
27368 }
27369 snapshot = &read(rows[offset]);
27370 return true;
27371 };
27372 if (locate(pending_same_bar_commands_,
27373 [](const PendingSameBarCommand& row) -> const PlacementSnapshot& {
27374 return row.snapshot;
27375 })) return true;
27376 if (locate(pending_entries_,
27377 [](const PendingEntry& row) -> const PlacementSnapshot& {
27378 return row.snapshot;
27379 })) return true;
27380 if (locate(pending_bracket_legs_,
27381 [](const PendingBracketLeg& row) -> const PlacementSnapshot& {
27382 return row.snapshot;
27383 })) return true;
27384 if (locate(pending_coof_requests_,
27385 [](const PendingCoofRequest& row) -> const PlacementSnapshot& {
27386 return row.snapshot;
27387 })) return true;
27388 return locate(source_shadow_pending_,
27389 [](const SourceShadowPending& row) -> const PlacementSnapshot& {
27390 return row.snapshot;
27391 });
27392}
27393
27394bool PineExecutionAdapter::same_projected_order(
27395 const PlacementSnapshot& left,
27396 const PlacementSnapshot& right) noexcept {
27397 const auto is_exit = [](PineOrderFamily family) {
27398 return family == PineOrderFamily::ExitLimit
27399 || family == PineOrderFamily::ExitStop
27400 || family == PineOrderFamily::ExitTrail;
27401 };
27402 if (!is_exit(left.family) || !is_exit(right.family)) return false;
27403 return left.source_id == right.source_id
27404 && left.from_entry == right.from_entry
27405 && left.bracket_origin == right.bracket_origin;
27406}
27407
27408bool PineExecutionAdapter::ensure_projected_pending_index() const try {
27409 const auto placements_version = placement_.pending_observer_version_;
27410 if (projected_pending_valid_
27411 && projected_pending_version_ == pending_projection_version_
27412 && projected_placement_version_ == placements_version) return true;
27413 struct Key {
27414 std::string_view source, from, session;
27415 std::uint64_t run, origin;
27416 bool operator==(const Key& other) const noexcept {
27417 return source == other.source && from == other.from
27418 && session == other.session && run == other.run
27419 && origin == other.origin;
27420 }
27421 };
27422 struct Hash {
27423 std::size_t operator()(const Key& key) const noexcept {
27424 std::size_t hash = 0;
27425 const auto add = [&](std::size_t value) {
27426 hash ^= value + 0x9e3779b9U + (hash << 6U) + (hash >> 2U);
27427 };
27428 const std::hash<std::string_view> text;
27429 add(text(key.source)); add(text(key.from)); add(text(key.session));
27430 add(std::hash<std::uint64_t>{}(key.run));
27431 add(std::hash<std::uint64_t>{}(key.origin));
27432 return hash;
27433 }
27434 };
27435 const int raw_count = projected_raw_pending_size();
27436 std::vector<int> indexes;
27437 std::vector<int> next(static_cast<std::size_t>(raw_count), -1);
27438 std::unordered_map<Key, int, Hash> exits;
27439 indexes.reserve(static_cast<std::size_t>(raw_count));
27440 exits.reserve(static_cast<std::size_t>(raw_count));
27441 // A group's first raw slot survives; later slots form an ordered chain.
27442 for (int raw = 0; raw < raw_count; ++raw) {
27443 const PlacementSnapshot* row = nullptr;
27444 native_order::RequestHandle handle;
27445 if (!projected_raw_pending_at(raw, row, handle) || !row) continue;
27446 const bool exit = row->family == PineOrderFamily::ExitLimit
27447 || row->family == PineOrderFamily::ExitStop
27448 || row->family == PineOrderFamily::ExitTrail;
27449 if (exit) {
27450 const auto& origin = row->bracket_origin;
27451 const Key key{row->source_id, row->from_entry, std::string_view(origin.run.session_key.string()),
27452 origin.run.run_number, origin.incarnation};
27453 auto inserted = exits.emplace(key, raw);
27454 if (!inserted.second) {
27455 next[static_cast<std::size_t>(inserted.first->second)] = raw;
27456 inserted.first->second = raw;
27457 continue;
27458 }
27459 }
27460 indexes.push_back(raw);
27461 }
27462 // Publish complete integer indexes only; failed allocation leaves exact scans in charge.
27463 projected_pending_indexes_.swap(indexes);
27464 projected_pending_next_.swap(next);
27465 projected_pending_version_ = pending_projection_version_;
27466 projected_placement_version_ = placements_version;
27467 projected_pending_valid_ = true; return true;
27468} catch (const std::bad_alloc&) { projected_pending_valid_ = false; return false; }
27469
27470int PendingIntentView::size() const noexcept {
27471 return owner_ ? owner_->projected_pending_size() : 0;
27472}
27473
27474int PendingIntentView::probe_fill_qty(int index, double fill_price, double* qty,
27475 int* close_only, int* partition) const noexcept {
27476 if (!owner_ || !qty || !close_only || !partition) return -1;
27477 const PlacementSnapshot* row = nullptr;
27479 if (!owner_->projected_pending_at(index, row, handle) || !row) return -1;
27480 const auto& snapshot = *row;
27481 *qty = kNaN;
27482 *close_only = 0;
27483 *partition = -1;
27484 if (!snapshot.opening) return 1;
27485
27486 const auto physical = detail::run_position(owner_->require_host());
27487 const bool opposite = physical.signed_units != 0.0
27488 && ((physical.signed_units > 0.0) != snapshot.is_long);
27489 bool kernel_close_only = false;
27490 bool sized = false;
27491 if (snapshot.frozen_market_instruction
27492 && finite_positive(snapshot.frozen_market_transaction_units)) {
27493 if (opposite) {
27494 *qty = std::max(0.0, snapshot.frozen_market_transaction_units
27495 - std::abs(physical.signed_units));
27496 *partition = 1;
27497 kernel_close_only = !(*qty > 1e-10);
27498 sized = true;
27499 } else if (owner_->fixed_unmargined_market_batch_scope()) {
27500 // Match precommit's batch boundary without an unpersisted scope flag.
27501 *qty = snapshot.frozen_market_transaction_units;
27502 *partition = 1;
27503 sized = true;
27504 } else if (physical.signed_units != 0.0
27505 && ((physical.signed_units > 0.0) == snapshot.is_long)
27506 && snapshot.projection_over_pyramiding) {
27507 *qty = snapshot.frozen_market_transaction_units;
27508 *partition = 1;
27509 sized = true;
27510 } else if (physical.signed_units == 0.0
27511 && finite_positive(snapshot.frozen_market_own_units)
27512 && snapshot.frozen_market_transaction_units
27513 > snapshot.frozen_market_own_units + 1e-10) {
27514 *qty = snapshot.frozen_market_transaction_units;
27515 *partition = 1;
27516 sized = true;
27517 }
27518 }
27519 const bool default_stop_shape = snapshot.family == PineOrderFamily::Entry
27520 && !std::isnan(snapshot.exit_levels.stop)
27521 && std::isnan(snapshot.exit_levels.limit)
27522 && std::isnan(snapshot.requested_qty)
27523 && owner_->config_.default_qty_type
27524 == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
27525 && finite_positive(snapshot.sizing.frozen_units);
27526 const bool default_stop = default_stop_shape
27527 && finite_positive(fill_price)
27528 && snapshot.projection_position_side
27529 == static_cast<std::int32_t>(PositionSide::FLAT);
27530 const bool unpriced_market = std::isnan(snapshot.exit_levels.limit)
27531 && std::isnan(snapshot.exit_levels.stop)
27532 && std::isnan(snapshot.exit_levels.trail_offset)
27533 && std::isnan(snapshot.exit_levels.trail_price);
27534 const bool limit_route = (snapshot.family == PineOrderFamily::Entry
27535 || snapshot.family == PineOrderFamily::Order)
27536 && !std::isnan(snapshot.exit_levels.limit);
27537 double sized_price = fill_price;
27538 if (!limit_route && std::isfinite(sized_price)) {
27539 sized_price += (snapshot.is_long ? 1.0 : -1.0)
27540 * owner_->config_.slippage * owner_->staged_.syminfo.mintick;
27541 }
27542 const bool typed_entry = snapshot.family == PineOrderFamily::Entry
27543 && (snapshot.qty_type == static_cast<int>(QtyType::CASH)
27544 || snapshot.qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY));
27545 if (!sized && finite_positive(snapshot.requested_qty)) {
27546 // A typed quantity is money: the units its fill books at this price
27547 // (resolve_terms), not the cash or percentage number itself.
27548 *qty = typed_entry
27549 ? owner_->typed_entry_units(snapshot, sized_price,
27550 owner_->sizing_snapshot().fx, handle).value_or(0.0)
27551 : snapshot.requested_qty;
27552 *partition = 0;
27553 sized = true;
27554 } else if (!sized && default_stop) {
27555 *qty = snapshot.sizing.frozen_units;
27556 *partition = 1;
27557 sized = true;
27558 } else if (!sized && !default_stop_shape && unpriced_market
27559 && finite_positive(snapshot.sizing.frozen_units)) {
27560 *qty = snapshot.sizing.frozen_units;
27561 *partition = 1;
27562 sized = true;
27563 }
27564 if (!sized) {
27565 const int type = owner_->config_.default_qty_type;
27566 if (type == static_cast<int>(QtyType::CASH)
27567 || type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)) {
27568 // The quantity resolve_terms books at this fill (its at-fill
27569 // branch): the source's money on its own sizing equity, converted
27570 // by the kernel with the percentage fee reserve and floored by the
27571 // source -- default_sizing_units, the one conversion.
27572 PineSizingSnapshot sizing = owner_->sizing_snapshot();
27573 sizing.price = sized_price;
27574 sizing.mark = sized_price;
27575 if (finite_positive(sized_price)) {
27576 owner_->mark_sizing_equity(sizing, sized_price);
27577 } else {
27578 sizing.equity = kNaN;
27579 sizing.strategy_equity = kNaN;
27580 }
27581 *qty = owner_->default_sizing_units(sizing);
27582 } else {
27583 *qty = floor_quantity_grid(owner_->config_.default_qty_value,
27584 owner_->staged_.quantity_grid);
27585 }
27586 *partition = 3;
27587 }
27588
27589 const bool prior_cycle_close_only = snapshot.family == PineOrderFamily::Entry
27590 && opposite
27591 && snapshot.projection_position_side
27592 != (physical.signed_units > 0.0
27593 ? static_cast<std::int32_t>(PositionSide::LONG)
27594 : static_cast<std::int32_t>(PositionSide::SHORT))
27595 && !snapshot.projection_predecessor_market;
27596 *close_only = (snapshot.affordability_close_only || prior_cycle_close_only
27597 || kernel_close_only) ? 1 : 0;
27598 return 0;
27599}
27600
27601int PendingIntentView::level_resolved(int index) const noexcept {
27602 if (!owner_) return -1;
27603 const PlacementSnapshot* snapshot = nullptr;
27605 if (!owner_->projected_pending_at(index, snapshot, handle) || !snapshot) return -1;
27606 // Share the row lookup with effective_levels, which already has it.
27607 // Cohort state remains a current read, outside the position index.
27608 return owner_->projected_level_resolved(*snapshot);
27609}
27610
27611int PendingIntentView::effective_levels(int index, double* stop, double* limit,
27612 double* trail_activation) const noexcept {
27613 if (!owner_ || !stop || !limit || !trail_activation) return -1;
27614 const PlacementSnapshot* row = nullptr;
27616 if (!owner_->projected_pending_at(index, row, handle) || !row) return -1;
27617 const auto& snapshot = *row;
27618 const double tick = owner_->staged_.syminfo.mintick;
27619 const auto physical = detail::run_position(owner_->require_host());
27620 const bool long_side = physical.signed_units > 0.0;
27621 const double entry = owner_->require_host().position_avg_price();
27622 *stop = snapshot.exit_levels.stop;
27623 *limit = snapshot.exit_levels.limit;
27624 double trail_points = snapshot.exit_levels.trail_points;
27625 double trail_price = snapshot.exit_levels.trail_price;
27626 double profit_ticks = snapshot.exit_levels.profit_ticks;
27627 double loss_ticks = snapshot.exit_levels.loss_ticks;
27628 if (snapshot.family == PineOrderFamily::ExitLimit
27629 || snapshot.family == PineOrderFamily::ExitStop
27630 || snapshot.family == PineOrderFamily::ExitTrail) {
27631 for (int raw = owner_->projected_pending_next(index); raw >= 0; raw = owner_->projected_raw_pending_next(raw)) {
27632 const PlacementSnapshot* sibling = nullptr;
27633 native_order::RequestHandle sibling_handle;
27634 if (!owner_->projected_raw_pending_at(raw, sibling, sibling_handle)
27635 || !sibling || !PineExecutionAdapter::same_projected_order(snapshot, *sibling)) {
27636 continue;
27637 }
27638 if (!std::isnan(sibling->exit_levels.stop)) *stop = sibling->exit_levels.stop;
27639 if (!std::isnan(sibling->exit_levels.limit)) *limit = sibling->exit_levels.limit;
27640 if (!std::isnan(sibling->exit_levels.trail_points))
27641 trail_points = sibling->exit_levels.trail_points;
27642 if (!std::isnan(sibling->exit_levels.trail_price))
27643 trail_price = sibling->exit_levels.trail_price;
27644 if (!std::isnan(sibling->exit_levels.profit_ticks))
27645 profit_ticks = sibling->exit_levels.profit_ticks;
27646 if (!std::isnan(sibling->exit_levels.loss_ticks))
27647 loss_ticks = sibling->exit_levels.loss_ticks;
27648 }
27649 }
27650 // The legacy C observer reports the executable levels, not merely the
27651 // raw tick offsets retained at the command. Keep the source tick
27652 // derivation at the projection boundary where it is observable.
27653 const bool position_live = physical.signed_units != 0.0 && std::isfinite(entry);
27654 const bool resolved = position_live && owner_->projected_level_resolved(snapshot) == 1;
27655 const double direction = long_side ? 1.0 : -1.0;
27656 if ((snapshot.family == PineOrderFamily::ExitLimit
27657 || snapshot.family == PineOrderFamily::ExitStop
27658 || snapshot.family == PineOrderFamily::ExitTrail)
27659 && resolved) {
27660 if (std::isnan(*limit) && !std::isnan(profit_ticks)) {
27661 *limit = source_level_on_price_grid(
27662 entry + direction * profit_ticks * tick, tick);
27663 }
27664 if (std::isnan(*stop) && !std::isnan(loss_ticks)) {
27665 *stop = source_level_on_price_grid(
27666 entry - direction * loss_ticks * tick, tick);
27667 }
27668 }
27669 *trail_activation = kNaN;
27670 if (!std::isnan(trail_points)) {
27671 if (resolved) {
27672 const double ticks = source_trail_points_ticks(trail_points, tick);
27674 entry + direction * ticks * tick, tick);
27675 }
27676 } else {
27677 *trail_activation = trail_price;
27678 }
27679 return 0;
27680}
27681
27682int PendingIntentView::copy_v1(int index, pf_pending_order_v1_t* out) const noexcept {
27683 if (!owner_ || !out) return -1;
27684 const PlacementSnapshot* row = nullptr;
27686 if (!owner_->projected_pending_at(index, row, handle) || !row) return -1;
27687 const PlacementSnapshot& snapshot = *row;
27688
27689 std::memset(out, 0, sizeof(*out));
27690 out->struct_version = 1;
27691 out->size = static_cast<std::uint32_t>(sizeof(*out));
27692 if (snapshot.family == PineOrderFamily::Close) {
27693 copy_pending_prefixed_string("__close__", snapshot.source_id,
27694 out->id, &out->id_truncated,
27695 &out->id_hash64);
27696 } else {
27697 copy_pending_string(snapshot.source_id, out->id,
27698 &out->id_truncated, &out->id_hash64);
27699 }
27700 copy_pending_string(snapshot.from_entry, out->from_entry, &out->from_entry_truncated,
27701 &out->from_entry_hash64);
27702 copy_pending_string(snapshot.oca_name, out->oca_name, &out->oca_name_truncated,
27703 &out->oca_name_hash64);
27704 copy_pending_string(snapshot.comment, out->comment, &out->comment_truncated,
27705 &out->comment_hash64);
27706 out->type = mirror_order_type(snapshot.family);
27707 out->is_long = snapshot.is_long ? 1U : 0U;
27708 out->limit_price = snapshot.exit_levels.limit;
27709 out->stop_price = snapshot.exit_levels.stop;
27710 out->trail_points = snapshot.exit_levels.trail_points;
27711 out->trail_price = snapshot.exit_levels.trail_price;
27712 out->trail_offset = snapshot.exit_levels.trail_offset;
27713 out->profit_ticks = snapshot.exit_levels.profit_ticks;
27714 out->loss_ticks = snapshot.exit_levels.loss_ticks;
27715 if (snapshot.family == PineOrderFamily::ExitLimit
27716 || snapshot.family == PineOrderFamily::ExitStop
27717 || snapshot.family == PineOrderFamily::ExitTrail) {
27718 for (int raw = owner_->projected_pending_next(index); raw >= 0; raw = owner_->projected_raw_pending_next(raw)) {
27719 const PlacementSnapshot* sibling = nullptr;
27720 native_order::RequestHandle sibling_handle;
27721 if (!owner_->projected_raw_pending_at(raw, sibling, sibling_handle)
27722 || !sibling || !PineExecutionAdapter::same_projected_order(snapshot, *sibling)) {
27723 continue;
27724 }
27725 if (!std::isnan(sibling->exit_levels.limit))
27726 out->limit_price = sibling->exit_levels.limit;
27727 if (!std::isnan(sibling->exit_levels.stop))
27728 out->stop_price = sibling->exit_levels.stop;
27729 if (!std::isnan(sibling->exit_levels.trail_points))
27730 out->trail_points = sibling->exit_levels.trail_points;
27731 if (!std::isnan(sibling->exit_levels.trail_price))
27732 out->trail_price = sibling->exit_levels.trail_price;
27733 if (!std::isnan(sibling->exit_levels.trail_offset))
27734 out->trail_offset = sibling->exit_levels.trail_offset;
27735 if (!std::isnan(sibling->exit_levels.profit_ticks))
27736 out->profit_ticks = sibling->exit_levels.profit_ticks;
27737 if (!std::isnan(sibling->exit_levels.loss_ticks))
27738 out->loss_ticks = sibling->exit_levels.loss_ticks;
27739 }
27740 }
27741 out->qty = snapshot.family == PineOrderFamily::Close
27742 ? snapshot.requested_qty
27743 : (std::isfinite(snapshot.projection_remaining_qty)
27744 ? snapshot.projection_remaining_qty : snapshot.requested_qty);
27745 out->qty_type = snapshot.qty_type;
27746 out->qty_percent = snapshot.qty_percent;
27747 out->oca_type = snapshot.oca_type;
27748 out->created_bar = snapshot.projection_created_bar;
27749 out->created_seq = static_cast<std::int64_t>(snapshot.source_sequence);
27750 out->incarnation = handle.incarnation;
27751 out->created_by_same_id_replacement = snapshot.projection_predecessor != 0
27752 && snapshot.family != PineOrderFamily::Order ? 1U : 0U;
27753 out->replaced_default_market_incarnation = snapshot.projection_predecessor_market
27754 ? snapshot.projection_predecessor : 0;
27755 out->declined_by_replaced_short_market = snapshot.cancellation.cause
27757 out->replaced_exit_order_incarnation = snapshot.projection_predecessor_exit
27758 ? snapshot.projection_predecessor : 0;
27759 out->recreated_after_named_cancelled_entry_incarnation =
27760 snapshot.recreated_after_named_cancelled_entry_incarnation;
27761 out->named_cancel_surviving_exit_incarnation = snapshot.named_cancel_surviving_exit_incarnation;
27762 out->stop_limit_activated = snapshot.stop_limit_activated ? 1U : 0U;
27763 out->coof_suppress_stop_on_entry_bar = snapshot.exit_activation.holds_stop() ? 1U : 0U;
27764 out->coof_suppress_limit_on_entry_bar = snapshot.exit_activation.holds_limit() ? 1U : 0U;
27765 out->created_during_coof_recalc = snapshot.birth.from_fill() ? 1U : 0U;
27766 out->coof_born_at_close_recalc = snapshot.birth.at_terminal_fill() ? 1U : 0U;
27767 out->coof_born_mid_bar = compat::pine::historical_cascade_reach(snapshot.birth_reach) ? 1U : 0U;
27768 out->coof_cascade_seg_i = snapshot.coof_cascade_seg_i;
27769 out->coof_cascade_inflight_fires = snapshot.coof_cascade_inflight_fires ? 1U : 0U;
27770 out->created_position_side = snapshot.projection_position_side;
27771 out->created_position_cycle_seq = snapshot.placement_cycle;
27772 out->created_after_position_close_in_bar = snapshot.projection_after_close ? 1U : 0U;
27773 out->over_pyramiding_cap_at_placement = snapshot.projection_over_pyramiding ? 1U : 0U;
27774 out->same_id_stop_deferred_close_all_bar = snapshot.cancellation.cause
27775 == PineCancellationCause::Dependency ? snapshot.projection_created_bar : -1;
27776 out->same_id_stop_deferred_close_all_incarnation = snapshot.cancellation.cause
27777 == PineCancellationCause::Dependency ? snapshot.cancellation.target_incarnation : 0;
27778 out->reverses_same_bar_market_from_flat =
27779 snapshot.projection_opposite_market_predecessor ? 1U : 0U;
27780 out->paired_flat_market_candidate = snapshot.paired_flat_market_candidate ? 1U : 0U;
27781 out->paired_flat_market_own_qty = snapshot.paired_flat_market_own_qty;
27782 out->paired_flat_market_signal_close = snapshot.paired_flat_market_signal_close;
27783 out->paired_flat_market_signal_equity = snapshot.paired_flat_market_signal_equity;
27784 out->paired_flat_market_signal_margin_pct = snapshot.paired_flat_market_signal_margin_pct;
27785 out->paired_flat_market_signal_pointvalue = snapshot.paired_flat_market_signal_pointvalue;
27786 out->paired_flat_market_signal_fx = snapshot.paired_flat_market_signal_fx;
27787 out->paired_flat_market_peer_seq = snapshot.paired_flat_market_peer_seq;
27788 out->paired_flat_market_transaction_qty = snapshot.paired_flat_market_transaction_qty;
27789 out->default_flat_market_gross_candidate = snapshot.paired_flat_market_candidate
27790 && !std::isfinite(snapshot.requested_qty) ? 1U : 0U;
27791 out->tv_carry_qty = snapshot.projection_tv_carry_qty;
27792 out->frozen_default_qty = snapshot.sizing.frozen_units;
27793 out->default_stop_placement_qty = snapshot.sizing.frozen_units;
27794 out->default_stop_placement_equity = snapshot.projection_default_stop_equity;
27795 out->default_stop_placement_signal_close = snapshot.projection_default_stop_signal_close;
27796 out->default_stop_sizing_price = snapshot.sizing.price;
27797 out->sizing_equity = snapshot.sizing.equity;
27798 out->sizing_price = snapshot.sizing.price;
27799 out->sizing_fx = snapshot.sizing.fx;
27800 out->sizing_mark = snapshot.sizing.mark;
27801 out->opening_affordability_exemption_candidate = snapshot.opening
27802 && !std::isfinite(snapshot.requested_qty)
27803 && snapshot.projection_position_side == static_cast<std::int32_t>(PositionSide::FLAT) ? 1U : 0U;
27804 out->explicit_flat_admission_candidate = snapshot.opening
27805 && std::isfinite(snapshot.requested_qty)
27806 && snapshot.projection_position_side == static_cast<std::int32_t>(PositionSide::FLAT) ? 1U : 0U;
27807 out->explicit_placement_equity = snapshot.projection_explicit_equity;
27808 out->explicit_slipped_signal_close = snapshot.projection_explicit_signal_close;
27809 out->affordability_placement_equity = snapshot.projection_affordability_equity;
27810 out->affordability_signal_price = snapshot.projection_affordability_signal_price;
27811 out->affordability_held_qty = snapshot.projection_affordability_held_qty;
27812 out->affordability_close_only = snapshot.affordability_close_only ? 1U : 0U;
27813 out->rounded_signal_cost_close_only = snapshot.rounded_signal_cost_close_only ? 1U : 0U;
27814 // The source command boundary itself is the truthful placement
27815 // observation after the legacy admission draft owner was retired. Its
27816 // original sizing tuple is the immutable adapter snapshot captured by
27817 // entry()/order(); no allocation or reconstructed executable book is
27818 // involved in this projection.
27819 out->market_admission_observation_present = snapshot.opening ? 1U : 0U;
27820 out->market_admission_observation_original_sizing_present =
27821 snapshot.opening ? 1U : 0U;
27822 out->market_admission_observation_requested_quantity = snapshot.requested_qty;
27823 out->market_admission_observation_quantity_type = snapshot.qty_type;
27824 out->market_admission_observation_buy = snapshot.is_long ? 1U : 0U;
27825 out->market_admission_observation_prices_limit = snapshot.exit_levels.limit;
27826 out->market_admission_observation_prices_stop = snapshot.exit_levels.stop;
27827 copy_pending_string(snapshot.source_id,
27828 out->market_admission_observation_id,
27829 &out->market_admission_observation_id_truncated,
27830 &out->market_admission_observation_id_hash64);
27831 copy_pending_string(snapshot.oca_name,
27832 out->market_admission_observation_oca_name,
27833 &out->market_admission_observation_oca_name_truncated,
27834 &out->market_admission_observation_oca_name_hash64);
27835 out->market_admission_observation_placement_equity = snapshot.sizing.equity;
27836 out->market_admission_observation_signal_close = snapshot.sizing.price;
27837 out->market_admission_observation_original_sizing_quantity =
27838 snapshot.requested_qty;
27839 out->market_admission_observation_original_sizing_equity = snapshot.sizing.equity;
27840 out->market_admission_observation_original_sizing_price = snapshot.sizing.price;
27841 out->market_admission_observation_original_sizing_mark = snapshot.sizing.mark;
27842 out->market_admission_observation_original_sizing_fx = snapshot.sizing.fx;
27843 out->signal_close_mc_bar = snapshot.signal_close_mc_bar;
27844 out->signal_close_mc_entry_incarnation = snapshot.signal_close_mc_entry_incarnation;
27845 out->signal_close_mc_fill_seq = snapshot.signal_close_mc_fill_seq;
27846 out->signal_close_mc_remaining_qty = snapshot.signal_close_mc_remaining_qty;
27847 out->requested_partial = (!snapshot.opening && std::isfinite(snapshot.qty_percent)
27848 && snapshot.qty_percent < 100.0) || (!snapshot.opening
27849 && std::isfinite(snapshot.requested_qty)) ? 1U : 0U;
27850 out->full_percent_exit_request = !snapshot.opening && !std::isfinite(snapshot.requested_qty)
27851 && (!std::isfinite(snapshot.qty_percent) || snapshot.qty_percent == 100.0) ? 1U : 0U;
27852 out->pooc_global_full_exit_dynamic_qty = snapshot.pooc_global_full_exit_dynamic_qty ? 1U : 0U;
27853 out->pooc_global_full_exit_tracks_bound_adds = snapshot.pooc_global_full_exit_tracks_bound_adds ? 1U : 0U;
27854 out->pooc_global_full_exit_bound_add = snapshot.pooc_global_full_exit_bound_add ? 1U : 0U;
27855 out->created_while_in_position = !snapshot.opening
27856 && snapshot.projection_position_side != static_cast<std::int32_t>(PositionSide::FLAT)
27857 ? 1U : 0U;
27858 out->sbmt_member = snapshot.frozen_market_instruction ? 1U : 0U;
27859 out->sbmt_own_qty = snapshot.frozen_market_instruction
27860 ? snapshot.frozen_market_own_units : kNaN;
27861 out->sbmt_tx_qty = snapshot.frozen_market_instruction
27862 ? snapshot.frozen_market_transaction_units : kNaN;
27863 out->sbmt_kept_over_cap = snapshot.frozen_market_instruction
27864 && snapshot.projection_over_pyramiding ? 1U : 0U;
27865 out->sbmt_close_qty = snapshot.frozen_market_targeted_close
27866 ? snapshot.requested_qty : kNaN;
27867 out->sbmt_close_buy = snapshot.frozen_market_targeted_close
27868 && snapshot.projection_position_side == static_cast<std::int32_t>(PositionSide::SHORT)
27869 ? 1U : 0U;
27870 out->suppress_as_declined_reversal_close = snapshot.cancellation.cause
27872 out->dormant_bracket = snapshot.legs.dormant() ? 1U : 0U;
27873 out->dormant_reissue_pending = snapshot.legs.pending_replacement() ? 1U : 0U;
27874 out->dormant_original_stop_price = snapshot.legs.original_stop();
27875 out->dormant_hold_bar = snapshot.legs.hold_bar();
27876 out->dormant_reversal_kill_bar = snapshot.legs.excluded_bar();
27877 out->dormant_trail_best = snapshot.legs.trail_best();
27878 out->dormant_trail_best_start = snapshot.legs.trail_prefix();
27879 out->dormant_trail_leg_dead = snapshot.legs.retired(exit_legs::Leg::Trail) ? 1U : 0U;
27880 out->suppressed_close_consumed_ledger_qty = snapshot.cancellation.close_claim_consumed;
27881 out->suppressed_close_retired_ledger_qty = snapshot.cancellation.close_claim_retired;
27882 out->short_seed_collision_role = owner_->short_seed_.active ? owner_->short_seed_collision_role_v1(handle) : 0;
27883 out->replaced_order_incarnation = snapshot.projection_predecessor;
27884 out->birth_timestamp = snapshot.birth.timestamp();
27885 out->birth_cause = static_cast<std::int32_t>(snapshot.birth.cause());
27886 out->birth_bar = snapshot.birth.bar();
27887 out->birth_cursor_domain = static_cast<std::int32_t>(snapshot.birth.cursor().domain());
27888 out->birth_cursor_position = static_cast<std::int32_t>(snapshot.birth.cursor().position());
27889 out->birth_cursor_index = snapshot.birth.cursor().index();
27890 out->birth_cursor_count = snapshot.birth.cursor().count();
27891 out->birth_cursor_price = snapshot.birth.cursor_price();
27892 out->birth_first_fill = snapshot.birth.first_fill();
27893 out->birth_last_fill = snapshot.birth.last_fill();
27894 out->birth_evaluation_ordinal = snapshot.birth.evaluation_ordinal();
27895 out->pine_birth_reach = static_cast<std::int32_t>(snapshot.birth_reach);
27896 out->pine_frozen_market_instruction_kind = snapshot.frozen_market_instruction ? 1U : 0U;
27897 out->pine_frozen_market_instruction_own_units = snapshot.frozen_market_own_units;
27898 out->pine_frozen_market_instruction_transaction_units =
27899 snapshot.frozen_market_transaction_units;
27900 copy_pending_string(snapshot.frozen_market_targeted_close
27901 ? std::string_view(snapshot.source_id) : std::string_view{},
27902 out->pine_frozen_market_instruction_target_id,
27903 &out->pine_frozen_market_instruction_target_id_truncated,
27904 &out->pine_frozen_market_instruction_target_id_hash64);
27905 const bool explicit_units = std::isfinite(snapshot.requested_qty);
27906 const bool percentage = !explicit_units && std::isfinite(snapshot.qty_percent);
27907 out->quantity_intent_kind = explicit_units ? 2U : (percentage ? 3U : 1U);
27908 out->quantity_intent_units = explicit_units ? snapshot.requested_qty : 0.0;
27909 out->quantity_intent_numerator = percentage ? snapshot.qty_percent : 0.0;
27910 out->quantity_intent_denominator = percentage ? 100.0 : 0.0;
27911 const double exposure = snapshot.from_entry.empty() ? 0.0
27912 : owner_->cohort_exposure_for(snapshot.from_entry);
27913 out->quantity_reservation_present = snapshot.deferred_cohort && exposure > 0.0 ? 1U : 0U;
27914 out->quantity_reservation_units = out->quantity_reservation_present ? exposure : 0.0;
27915 out->quantity_reservation_basis_units = out->quantity_reservation_present ? exposure : 0.0;
27916 out->leg_activation_present = snapshot.leg_activation.bounds() ? 1U : 0U;
27917 out->leg_activation_owner_cycle = snapshot.leg_activation.bounds()
27918 ? snapshot.leg_activation.bounds()->position_cycle : 0;
27919 out->leg_activation_stop_first_bar = snapshot.leg_activation.bounds()
27920 ? snapshot.leg_activation.bounds()->stop_first_bar : 0;
27921 out->leg_activation_limit_first_bar = snapshot.leg_activation.bounds()
27922 ? snapshot.leg_activation.bounds()->limit_first_bar : 0;
27923 const auto& activation = snapshot.exit_activation.evidence();
27924 out->pine_exit_activation_present = activation ? 1U : 0U;
27925 out->pine_exit_activation_owner_cycle_at_birth = activation ? activation->position_cycle : 0;
27926 out->pine_exit_activation_entry_bar_at_birth = activation ? activation->entry_bar : 0;
27927 out->pine_exit_activation_direction_at_birth = activation ? activation->direction : 0;
27928 out->pine_exit_activation_cursor_price_at_birth = activation ? activation->cursor_price : 0.0;
27929 out->pine_exit_activation_stop_level_at_birth = activation ? activation->stop_level : 0.0;
27930 out->pine_exit_activation_limit_level_at_birth = activation ? activation->limit_level : 0.0;
27931 out->pine_exit_activation_limit_continuation_present = activation
27932 && activation->limit_continuation ? 1U : 0U;
27933 out->pine_exit_activation_limit_continuation_cause = activation
27934 && activation->limit_continuation
27935 ? static_cast<std::int32_t>(activation->limit_continuation->cause) : 0;
27936 out->pine_exit_activation_limit_continuation_fill = activation
27937 && activation->limit_continuation
27938 ? activation->limit_continuation->observed_fill_sequence : 0;
27939 const auto& expansion = snapshot.reservation_expansion.capture();
27940 out->reservation_expansion_present = expansion ? 1U : 0U;
27941 out->reservation_expansion_position_cycle = expansion ? expansion->position_cycle : 0;
27942 out->reservation_expansion_side = expansion ? static_cast<std::int32_t>(expansion->side) : 0;
27943 out->reservation_expansion_first_later_admission_present = expansion
27944 && expansion->first_later_admission ? 1U : 0U;
27945 out->reservation_expansion_first_later_admission = expansion
27946 && expansion->first_later_admission ? *expansion->first_later_admission : 0;
27947 out->reservation_growth_source_present = snapshot.reservation_growth_source.reservation_owner()
27948 ? 1U : 0U;
27949 out->reservation_growth_source_reservation_owner = snapshot.reservation_growth_source.reservation_owner()
27950 ? *snapshot.reservation_growth_source.reservation_owner() : 0;
27951
27952 const auto target = snapshot.legs.target();
27953 const auto& definition = snapshot.legs.current_definition();
27954 out->legs_target_incarnation = target.incarnation;
27955 out->legs_target_owner = target.owner;
27956 out->legs_revision = snapshot.legs.revision();
27957 out->legs_definition_incarnation = definition.incarnation();
27958 out->legs_definition_revision = definition.revision();
27959 out->legs_definition_value_present = definition.has_value() ? 1U : 0U;
27960 out->legs_definition_limit_price = definition.has_value() ? definition.prices().limit_price : kNaN;
27961 out->legs_definition_stop_price = definition.has_value() ? definition.prices().stop_price : kNaN;
27962 out->legs_definition_trail_points = definition.has_value() ? definition.prices().trail_points : kNaN;
27963 out->legs_definition_trail_price = definition.has_value() ? definition.prices().trail_price : kNaN;
27964 out->legs_definition_trail_offset = definition.has_value() ? definition.prices().trail_offset : kNaN;
27965 out->legs_definition_profit_ticks = definition.has_value() ? definition.prices().profit_ticks : kNaN;
27966 out->legs_definition_loss_ticks = definition.has_value() ? definition.prices().loss_ticks : kNaN;
27967 const auto& retirements = snapshot.legs.retirements();
27968 const auto copy_retirement = [&](std::size_t number, std::uint64_t& generation,
27969 std::uint8_t& present, std::uint64_t& receipt_generation,
27970 std::uint64_t& event, std::int64_t& bar,
27971 std::uint32_t& domain, std::uint32_t& phase) {
27972 const auto leg = static_cast<exit_legs::Leg>(number);
27973 generation = snapshot.legs.generation(leg);
27974 const auto& receipt = retirements[number];
27975 present = receipt ? 1U : 0U;
27976 receipt_generation = receipt ? receipt->generation : 0;
27977 event = receipt ? receipt->cause.event : 0;
27978 bar = receipt ? receipt->cause.bar : 0;
27979 domain = receipt ? static_cast<std::uint32_t>(receipt->cause.domain) : 0U;
27980 phase = receipt ? static_cast<std::uint32_t>(receipt->cause.phase) : 0U;
27981 };
27982 copy_retirement(0, out->legs_generation0, out->legs_retirement0_present,
27983 out->legs_retirement0_generation, out->legs_retirement0_cause_event,
27984 out->legs_retirement0_cause_bar, out->legs_retirement0_cause_domain,
27985 out->legs_retirement0_cause_phase);
27986 copy_retirement(1, out->legs_generation1, out->legs_retirement1_present,
27987 out->legs_retirement1_generation, out->legs_retirement1_cause_event,
27988 out->legs_retirement1_cause_bar, out->legs_retirement1_cause_domain,
27989 out->legs_retirement1_cause_phase);
27990 copy_retirement(2, out->legs_generation2, out->legs_retirement2_present,
27991 out->legs_retirement2_generation, out->legs_retirement2_cause_event,
27992 out->legs_retirement2_cause_bar, out->legs_retirement2_cause_domain,
27993 out->legs_retirement2_cause_phase);
27994 const auto& suspension = snapshot.legs.suspension();
27995 out->legs_suspension_present = suspension ? 1U : 0U;
27996 out->legs_suspension_cause_event = suspension ? suspension->cause.event : 0;
27997 out->legs_suspension_cause_bar = suspension ? suspension->cause.bar : 0;
27998 out->legs_suspension_cause_domain = suspension
27999 ? static_cast<std::uint32_t>(suspension->cause.domain) : 0U;
28000 out->legs_suspension_cause_phase = suspension
28001 ? static_cast<std::uint32_t>(suspension->cause.phase) : 0U;
28002 out->legs_suspension_legs_count = suspension
28003 ? static_cast<std::uint32_t>(suspension->legs.size()) : 0U;
28004 const auto suspended_leg = [&](std::size_t number) -> std::uint32_t {
28005 return suspension && suspension->legs.size() > number
28006 ? static_cast<std::uint32_t>(suspension->legs[number]) : UINT32_MAX;
28007 };
28008 out->legs_suspension_legs_item0 = suspended_leg(0);
28009 out->legs_suspension_legs_item1 = suspended_leg(1);
28010 out->legs_suspension_legs_item2 = suspended_leg(2);
28011 out->legs_suspension_hold_present = suspension && suspension->hold ? 1U : 0U;
28012 if (suspension && suspension->hold) {
28013 const auto& hold = *suspension->hold;
28014 out->legs_suspension_hold_requested_event = hold.requested.event;
28015 out->legs_suspension_hold_requested_bar = hold.requested.bar;
28016 out->legs_suspension_hold_requested_domain = static_cast<std::uint32_t>(hold.requested.domain);
28017 out->legs_suspension_hold_requested_phase = static_cast<std::uint32_t>(hold.requested.phase);
28018 out->legs_suspension_hold_target_incarnation = hold.target.incarnation;
28019 out->legs_suspension_hold_target_owner = hold.target.owner;
28020 out->legs_suspension_hold_revision = hold.revision;
28021 }
28022 out->legs_suspension_revival_definition_limit_price = kNaN;
28023 out->legs_suspension_revival_definition_stop_price = kNaN;
28024 out->legs_suspension_revival_definition_trail_points = kNaN;
28025 out->legs_suspension_revival_definition_trail_price = kNaN;
28026 out->legs_suspension_revival_definition_trail_offset = kNaN;
28027 out->legs_suspension_revival_definition_profit_ticks = kNaN;
28028 out->legs_suspension_revival_definition_loss_ticks = kNaN;
28029 const auto* revival = suspension && suspension->revival_definition
28030 ? &*suspension->revival_definition : nullptr;
28031 out->legs_suspension_revival_definition_present = revival ? 1U : 0U;
28032 if (revival) {
28033 out->legs_suspension_revival_definition_incarnation = revival->incarnation();
28034 out->legs_suspension_revival_definition_revision = revival->revision();
28035 out->legs_suspension_revival_definition_value_present = revival->has_value() ? 1U : 0U;
28036 if (revival->has_value()) {
28037 const auto& prices = revival->prices();
28038 out->legs_suspension_revival_definition_limit_price = prices.limit_price;
28039 out->legs_suspension_revival_definition_stop_price = prices.stop_price;
28040 out->legs_suspension_revival_definition_trail_points = prices.trail_points;
28041 out->legs_suspension_revival_definition_trail_price = prices.trail_price;
28042 out->legs_suspension_revival_definition_trail_offset = prices.trail_offset;
28043 out->legs_suspension_revival_definition_profit_ticks = prices.profit_ticks;
28044 out->legs_suspension_revival_definition_loss_ticks = prices.loss_ticks;
28045 }
28046 }
28047 out->legs_suspension_replacement_revival_definition_limit_price = kNaN;
28048 out->legs_suspension_replacement_revival_definition_stop_price = kNaN;
28049 out->legs_suspension_replacement_revival_definition_trail_points = kNaN;
28050 out->legs_suspension_replacement_revival_definition_trail_price = kNaN;
28051 out->legs_suspension_replacement_revival_definition_trail_offset = kNaN;
28052 out->legs_suspension_replacement_revival_definition_profit_ticks = kNaN;
28053 out->legs_suspension_replacement_revival_definition_loss_ticks = kNaN;
28054 const auto* replacement = suspension && suspension->replacement
28055 ? &*suspension->replacement : nullptr;
28056 out->legs_suspension_replacement_present = replacement ? 1U : 0U;
28057 if (replacement) {
28058 out->legs_suspension_replacement_queue_predecessor = replacement->queue_predecessor;
28059 const auto& definition = replacement->revival_definition;
28060 out->legs_suspension_replacement_revival_definition_incarnation = definition.incarnation();
28061 out->legs_suspension_replacement_revival_definition_revision = definition.revision();
28062 out->legs_suspension_replacement_revival_definition_value_present =
28063 definition.has_value() ? 1U : 0U;
28064 if (definition.has_value()) {
28065 const auto& prices = definition.prices();
28066 out->legs_suspension_replacement_revival_definition_limit_price = prices.limit_price;
28067 out->legs_suspension_replacement_revival_definition_stop_price = prices.stop_price;
28068 out->legs_suspension_replacement_revival_definition_trail_points = prices.trail_points;
28069 out->legs_suspension_replacement_revival_definition_trail_price = prices.trail_price;
28070 out->legs_suspension_replacement_revival_definition_trail_offset = prices.trail_offset;
28071 out->legs_suspension_replacement_revival_definition_profit_ticks = prices.profit_ticks;
28072 out->legs_suspension_replacement_revival_definition_loss_ticks = prices.loss_ticks;
28073 }
28074 const auto& release = replacement->release;
28075 out->legs_suspension_replacement_release_requested_event = release.requested.event;
28076 out->legs_suspension_replacement_release_requested_bar = release.requested.bar;
28077 out->legs_suspension_replacement_release_requested_domain =
28078 static_cast<std::uint32_t>(release.requested.domain);
28079 out->legs_suspension_replacement_release_requested_phase =
28080 static_cast<std::uint32_t>(release.requested.phase);
28081 out->legs_suspension_replacement_release_target_incarnation = release.target.incarnation;
28082 out->legs_suspension_replacement_release_target_owner = release.target.owner;
28083 out->legs_suspension_replacement_release_revision = release.revision;
28084 }
28085 out->legs_suspension_window_present = suspension && suspension->window ? 1U : 0U;
28086 out->legs_suspension_window_best = kNaN;
28087 out->legs_suspension_window_prefix = kNaN;
28088 if (suspension && suspension->window) {
28089 const auto& window = *suspension->window;
28090 out->legs_suspension_window_excluded_event = window.excluded.event;
28091 out->legs_suspension_window_excluded_bar = window.excluded.bar;
28092 out->legs_suspension_window_excluded_domain = static_cast<std::uint32_t>(window.excluded.domain);
28093 out->legs_suspension_window_excluded_phase = static_cast<std::uint32_t>(window.excluded.phase);
28094 out->legs_suspension_window_best = window.best;
28095 out->legs_suspension_window_prefix = window.prefix;
28096 }
28097 const auto& last = snapshot.legs.last_action();
28098 out->legs_last_present = last ? 1U : 0U;
28099 out->legs_last_suspend_legs_item0 = UINT32_MAX;
28100 out->legs_last_suspend_legs_item1 = UINT32_MAX;
28101 out->legs_last_suspend_legs_item2 = UINT32_MAX;
28102 out->legs_last_suspend_window_best = kNaN;
28103 out->legs_last_suspend_window_prefix = kNaN;
28104 out->legs_last_suspend_retire_item0 = UINT32_MAX;
28105 out->legs_last_suspend_retire_item1 = UINT32_MAX;
28106 out->legs_last_suspend_retire_item2 = UINT32_MAX;
28107 out->legs_last_stage_revival_definition_limit_price = kNaN;
28108 out->legs_last_stage_revival_definition_stop_price = kNaN;
28109 out->legs_last_stage_revival_definition_trail_points = kNaN;
28110 out->legs_last_stage_revival_definition_trail_price = kNaN;
28111 out->legs_last_stage_revival_definition_trail_offset = kNaN;
28112 out->legs_last_stage_revival_definition_profit_ticks = kNaN;
28113 out->legs_last_stage_revival_definition_loss_ticks = kNaN;
28114 out->legs_last_restore_legs_item0 = UINT32_MAX;
28115 out->legs_last_restore_legs_item1 = UINT32_MAX;
28116 out->legs_last_restore_legs_item2 = UINT32_MAX;
28117 out->legs_last_observe_high = kNaN;
28118 out->legs_last_observe_low = kNaN;
28119 out->legs_last_cancel_legs_item0 = UINT32_MAX;
28120 out->legs_last_cancel_legs_item1 = UINT32_MAX;
28121 out->legs_last_cancel_legs_item2 = UINT32_MAX;
28122 if (last) {
28123 out->legs_last_target_incarnation = last->target.incarnation;
28124 out->legs_last_target_owner = last->target.owner;
28125 out->legs_last_expected_revision = last->expected_revision;
28126 out->legs_last_cause_event = last->cause.event;
28127 out->legs_last_cause_bar = last->cause.bar;
28128 out->legs_last_cause_domain = static_cast<std::uint32_t>(last->cause.domain);
28129 out->legs_last_cause_phase = static_cast<std::uint32_t>(last->cause.phase);
28130 out->legs_last_operation = static_cast<std::uint32_t>(last->operation.index());
28131 if (const auto* bind = std::get_if<exit_legs::BindOwner>(&last->operation)) {
28132 out->legs_last_bind_owner = bind->owner;
28133 } else if (const auto* suspended = std::get_if<exit_legs::Suspend>(&last->operation)) {
28134 const auto copy_leg = [](const std::vector<exit_legs::Leg>& legs,
28135 std::size_t index) -> std::uint32_t {
28136 return index < legs.size() ? static_cast<std::uint32_t>(legs[index])
28137 : UINT32_MAX;
28138 };
28139 out->legs_last_suspend_legs_count =
28140 static_cast<std::uint32_t>(suspended->legs.size());
28141 out->legs_last_suspend_legs_item0 = copy_leg(suspended->legs, 0);
28142 out->legs_last_suspend_legs_item1 = copy_leg(suspended->legs, 1);
28143 out->legs_last_suspend_legs_item2 = copy_leg(suspended->legs, 2);
28144 out->legs_last_suspend_hold_present = suspended->hold ? 1U : 0U;
28145 if (suspended->hold) {
28146 const auto& hold = *suspended->hold;
28147 out->legs_last_suspend_hold_requested_event = hold.requested.event;
28148 out->legs_last_suspend_hold_requested_bar = hold.requested.bar;
28149 out->legs_last_suspend_hold_requested_domain =
28150 static_cast<std::uint32_t>(hold.requested.domain);
28151 out->legs_last_suspend_hold_requested_phase =
28152 static_cast<std::uint32_t>(hold.requested.phase);
28153 out->legs_last_suspend_hold_target_incarnation = hold.target.incarnation;
28154 out->legs_last_suspend_hold_target_owner = hold.target.owner;
28155 out->legs_last_suspend_hold_revision = hold.revision;
28156 }
28157 out->legs_last_suspend_window_present = suspended->window ? 1U : 0U;
28158 if (suspended->window) {
28159 const auto& window = *suspended->window;
28160 out->legs_last_suspend_window_excluded_event = window.excluded.event;
28161 out->legs_last_suspend_window_excluded_bar = window.excluded.bar;
28162 out->legs_last_suspend_window_excluded_domain =
28163 static_cast<std::uint32_t>(window.excluded.domain);
28164 out->legs_last_suspend_window_excluded_phase =
28165 static_cast<std::uint32_t>(window.excluded.phase);
28166 out->legs_last_suspend_window_best = window.best;
28167 out->legs_last_suspend_window_prefix = window.prefix;
28168 }
28169 out->legs_last_suspend_retire_count =
28170 static_cast<std::uint32_t>(suspended->retire.size());
28171 out->legs_last_suspend_retire_item0 = copy_leg(suspended->retire, 0);
28172 out->legs_last_suspend_retire_item1 = copy_leg(suspended->retire, 1);
28173 out->legs_last_suspend_retire_item2 = copy_leg(suspended->retire, 2);
28174 } else if (const auto* stage =
28175 std::get_if<exit_legs::StageReplacement>(&last->operation)) {
28176 const auto& relation = stage->relation;
28177 out->legs_last_stage_queue_predecessor = relation.queue_predecessor;
28178 const auto& definition = relation.revival_definition;
28179 out->legs_last_stage_revival_definition_incarnation = definition.incarnation();
28180 out->legs_last_stage_revival_definition_revision = definition.revision();
28181 out->legs_last_stage_revival_definition_value_present =
28182 definition.has_value() ? 1U : 0U;
28183 if (definition.has_value()) {
28184 const auto& prices = definition.prices();
28185 out->legs_last_stage_revival_definition_limit_price = prices.limit_price;
28186 out->legs_last_stage_revival_definition_stop_price = prices.stop_price;
28187 out->legs_last_stage_revival_definition_trail_points = prices.trail_points;
28188 out->legs_last_stage_revival_definition_trail_price = prices.trail_price;
28189 out->legs_last_stage_revival_definition_trail_offset = prices.trail_offset;
28190 out->legs_last_stage_revival_definition_profit_ticks = prices.profit_ticks;
28191 out->legs_last_stage_revival_definition_loss_ticks = prices.loss_ticks;
28192 }
28193 const auto& release = relation.release;
28194 out->legs_last_stage_release_requested_event = release.requested.event;
28195 out->legs_last_stage_release_requested_bar = release.requested.bar;
28196 out->legs_last_stage_release_requested_domain =
28197 static_cast<std::uint32_t>(release.requested.domain);
28198 out->legs_last_stage_release_requested_phase =
28199 static_cast<std::uint32_t>(release.requested.phase);
28200 out->legs_last_stage_release_target_incarnation = release.target.incarnation;
28201 out->legs_last_stage_release_target_owner = release.target.owner;
28202 out->legs_last_stage_release_revision = release.revision;
28203 } else if (const auto* restore = std::get_if<exit_legs::Restore>(&last->operation)) {
28204 out->legs_last_restore_legs_count =
28205 static_cast<std::uint32_t>(restore->legs.size());
28206 const auto copy_leg = [&](std::size_t index) -> std::uint32_t {
28207 return index < restore->legs.size()
28208 ? static_cast<std::uint32_t>(restore->legs[index]) : UINT32_MAX;
28209 };
28210 out->legs_last_restore_legs_item0 = copy_leg(0);
28211 out->legs_last_restore_legs_item1 = copy_leg(1);
28212 out->legs_last_restore_legs_item2 = copy_leg(2);
28213 } else if (const auto* complete =
28214 std::get_if<exit_legs::CompleteBarrier>(&last->operation)) {
28215 out->legs_last_complete_completed_event = complete->completed.event;
28216 out->legs_last_complete_completed_bar = complete->completed.bar;
28217 out->legs_last_complete_completed_domain =
28218 static_cast<std::uint32_t>(complete->completed.domain);
28219 out->legs_last_complete_completed_phase =
28220 static_cast<std::uint32_t>(complete->completed.phase);
28221 out->legs_last_complete_requested_present = complete->requested ? 1U : 0U;
28222 if (complete->requested) {
28223 const auto& requested = *complete->requested;
28224 out->legs_last_complete_requested_requested_event = requested.requested.event;
28225 out->legs_last_complete_requested_requested_bar = requested.requested.bar;
28226 out->legs_last_complete_requested_requested_domain =
28227 static_cast<std::uint32_t>(requested.requested.domain);
28228 out->legs_last_complete_requested_requested_phase =
28229 static_cast<std::uint32_t>(requested.requested.phase);
28230 out->legs_last_complete_requested_target_incarnation =
28231 requested.target.incarnation;
28232 out->legs_last_complete_requested_target_owner = requested.target.owner;
28233 out->legs_last_complete_requested_revision = requested.revision;
28234 }
28235 } else if (const auto* observe = std::get_if<exit_legs::Observe>(&last->operation)) {
28236 out->legs_last_observe_high = observe->high;
28237 out->legs_last_observe_low = observe->low;
28238 out->legs_last_observe_direction = observe->direction;
28239 out->legs_last_observe_fold = static_cast<std::uint32_t>(observe->fold);
28240 } else if (const auto* cancel = std::get_if<exit_legs::Cancel>(&last->operation)) {
28241 out->legs_last_cancel_legs_count =
28242 static_cast<std::uint32_t>(cancel->legs.size());
28243 const auto copy_leg = [&](std::size_t index) -> std::uint32_t {
28244 return index < cancel->legs.size()
28245 ? static_cast<std::uint32_t>(cancel->legs[index]) : UINT32_MAX;
28246 };
28247 out->legs_last_cancel_legs_item0 = copy_leg(0);
28248 out->legs_last_cancel_legs_item1 = copy_leg(1);
28249 out->legs_last_cancel_legs_item2 = copy_leg(2);
28250 }
28251 }
28252 const auto& admission = snapshot.market_admission;
28253 const auto& observation = admission.observation();
28254 if (observation) out->market_admission_observation_present = 1U;
28255 if (observation) {
28256 out->market_admission_observation_command = observation->command;
28257 out->market_admission_observation_kind = static_cast<std::int64_t>(observation->kind);
28258 out->market_admission_observation_birth_cause =
28259 static_cast<std::int64_t>(observation->birth.cause());
28260 out->market_admission_observation_birth_bar = observation->birth.bar();
28261 out->market_admission_observation_birth_timestamp = observation->birth.timestamp();
28262 out->market_admission_observation_birth_cursor_domain =
28263 static_cast<std::int64_t>(observation->birth.cursor().domain());
28264 out->market_admission_observation_birth_cursor_position =
28265 static_cast<std::int64_t>(observation->birth.cursor().position());
28266 out->market_admission_observation_birth_cursor_index = observation->birth.cursor().index();
28267 out->market_admission_observation_birth_cursor_count = observation->birth.cursor().count();
28268 out->market_admission_observation_birth_cursor_price = observation->birth.cursor_price();
28269 out->market_admission_observation_birth_first_fill = observation->birth.first_fill();
28270 out->market_admission_observation_birth_last_fill = observation->birth.last_fill();
28271 out->market_admission_observation_birth_evaluation_ordinal =
28272 observation->birth.evaluation_ordinal();
28273 copy_pending_string(observation->id, out->market_admission_observation_id,
28274 &out->market_admission_observation_id_truncated,
28275 &out->market_admission_observation_id_hash64);
28276 out->market_admission_observation_requested_quantity = observation->requested_quantity;
28277 out->market_admission_observation_quantity_type = observation->quantity_type;
28278 out->market_admission_observation_buy = observation->buy ? 1U : 0U;
28279 out->market_admission_observation_prices_limit = observation->prices.limit;
28280 out->market_admission_observation_prices_stop = observation->prices.stop;
28281 copy_pending_string(observation->oca_name, out->market_admission_observation_oca_name,
28282 &out->market_admission_observation_oca_name_truncated,
28283 &out->market_admission_observation_oca_name_hash64);
28284 out->market_admission_observation_oca_type = observation->oca_type;
28285 const auto& configuration = observation->configuration;
28286 out->market_admission_observation_configuration_process_on_close =
28287 configuration.process_on_close ? 1U : 0U;
28288 out->market_admission_observation_configuration_calc_on_fills =
28289 configuration.calc_on_fills ? 1U : 0U;
28290 out->market_admission_observation_configuration_magnifier =
28291 configuration.magnifier ? 1U : 0U;
28292 out->market_admission_observation_configuration_fill_recalculation =
28293 configuration.fill_recalculation ? 1U : 0U;
28294 out->market_admission_observation_configuration_scheduler =
28295 configuration.scheduler ? 1U : 0U;
28296 out->market_admission_observation_configuration_slippage = configuration.slippage;
28297 out->market_admission_observation_configuration_pyramiding = configuration.pyramiding;
28298 out->market_admission_observation_configuration_default_quantity_type =
28299 configuration.default_quantity_type;
28300 out->market_admission_observation_configuration_default_quantity_value =
28301 configuration.default_quantity_value;
28302 out->market_admission_observation_configuration_long_margin = configuration.long_margin;
28303 out->market_admission_observation_configuration_short_margin = configuration.short_margin;
28304 out->market_admission_observation_configuration_commission_value =
28305 configuration.commission_value;
28306 out->market_admission_observation_configuration_commission_type =
28307 configuration.commission_type;
28308 out->market_admission_observation_configuration_pointvalue = configuration.pointvalue;
28309 out->market_admission_observation_configuration_fx = configuration.fx;
28310 out->market_admission_observation_configuration_quantity_step =
28311 configuration.quantity_step;
28312 out->market_admission_observation_configuration_mintick = configuration.mintick;
28313 out->market_admission_observation_configuration_risk_direction =
28314 configuration.risk_direction;
28315 out->market_admission_observation_configuration_loss_days_limit =
28316 configuration.loss_days_limit;
28317 out->market_admission_observation_configuration_drawdown_limit =
28318 configuration.drawdown_limit;
28319 out->market_admission_observation_configuration_intraday_loss_limit =
28320 configuration.intraday_loss_limit;
28321 out->market_admission_observation_configuration_position_limit =
28322 configuration.position_limit;
28323 out->market_admission_observation_configuration_fill_cap_active =
28324 configuration.fill_cap_active ? 1U : 0U;
28325 out->market_admission_observation_configuration_risk_halted =
28326 configuration.risk_halted ? 1U : 0U;
28327 out->market_admission_observation_bar = observation->bar;
28328 out->market_admission_observation_placement_side = observation->placement_side;
28329 out->market_admission_observation_placement_cycle = observation->placement_cycle;
28330 out->market_admission_observation_prior_close_quantity =
28331 observation->prior_close_quantity;
28332 out->market_admission_observation_held_quantity = observation->held_quantity;
28333 out->market_admission_observation_held_entries = observation->held_entries;
28334 out->market_admission_observation_realized_equity = observation->realized_equity;
28335 out->market_admission_observation_placement_equity = observation->placement_equity;
28336 out->market_admission_observation_signal_close = observation->signal_close;
28337 out->market_admission_observation_quantized_fixed_quantity =
28338 observation->quantized_fixed_quantity;
28339 out->market_admission_observation_original_sizing_present =
28340 observation->original_sizing ? 1U : 0U;
28341 if (observation->original_sizing) {
28342 out->market_admission_observation_original_sizing_quantity =
28343 observation->original_sizing->quantity;
28344 out->market_admission_observation_original_sizing_equity =
28345 observation->original_sizing->equity;
28346 out->market_admission_observation_original_sizing_price =
28347 observation->original_sizing->price;
28348 out->market_admission_observation_original_sizing_mark =
28349 observation->original_sizing->mark;
28350 out->market_admission_observation_original_sizing_fx =
28351 observation->original_sizing->fx;
28352 }
28353 out->market_admission_observation_explicit_equity = observation->explicit_equity;
28354 out->market_admission_observation_explicit_price = observation->explicit_price;
28355 }
28356 out->market_admission_review_present = admission.review() ? 1U : 0U;
28357 if (admission.review()) {
28358 out->market_admission_review_sequence = admission.review()->sequence;
28359 out->market_admission_review_checkpoint =
28360 static_cast<std::int64_t>(admission.review()->checkpoint);
28361 out->market_admission_review_bar = admission.review()->bar;
28362 out->market_admission_review_target_command = admission.review()->target_command;
28363 }
28364 out->market_admission_sizing_revision_present = admission.sizing_revision() ? 1U : 0U;
28365 if (admission.sizing_revision()) {
28366 out->market_admission_sizing_revision_sequence = admission.sizing_revision()->sequence;
28367 out->market_admission_sizing_revision_cause_fill =
28368 admission.sizing_revision()->cause_fill;
28369 out->market_admission_sizing_revision_bar = admission.sizing_revision()->bar;
28370 out->market_admission_sizing_revision_target_command =
28371 admission.sizing_revision()->target_command;
28372 }
28373 out->cancellation_cause = static_cast<std::int32_t>(snapshot.cancellation.cause);
28374 out->cancellation_state = snapshot.cancellation.state;
28375 out->cancellation_close_claim_release = snapshot.cancellation.close_claim_release;
28376 out->cancellation_source_incarnation = snapshot.cancellation.source_incarnation;
28377 out->cancellation_source_sequence = snapshot.cancellation.source_sequence;
28378 out->cancellation_target_incarnation = snapshot.cancellation.target_incarnation;
28379 out->cancellation_target_owner = snapshot.cancellation.target_owner;
28380 out->cancellation_target_revision = snapshot.cancellation.target_revision;
28381 out->cancellation_close_claim_consumed = snapshot.cancellation.close_claim_consumed;
28382 out->cancellation_close_claim_retired = snapshot.cancellation.close_claim_retired;
28383 return 0;
28384}
28385
28387 if (!owner_) return -1;
28388 const PlacementSnapshot* snapshot = nullptr;
28390 if (!owner_->projected_pending_at(index, snapshot, handle) || !snapshot) return -1;
28391 if (!owner_->short_seed_.active) return 0;
28392 return owner_->short_seed_collision_role_v1(handle);
28393}
28394int PendingIntentView::last_bar_dual_entry_path() const noexcept { return owner_ ? owner_->last_bar_dual_entry_path_ : 0; }
28396 return owner_ && owner_->host_ ? owner_->host_->trail_best_price() : kNaN;
28397}
28398
28399// The entry/reversal fill ordering, family F10: the own units of the opposite MARKET
28400// entries this source bar placed that are still pending (batched or live), or
28401// NaN when one of them carries no explicit quantity. TradingView adds them to
28402// the later call's transaction. A call of the caller's own id is the one the
28403// caller replaces, not a pending one, and a typed, OCA-named or
28404// fill-recalculation market is a book the tapes do not measure (NaN, lane
28405// W6B-ENG-PAIRS).
28406double PineExecutionAdapter::pending_opposite_market_units(
28407 bool is_long, std::int64_t script_open_ms, const SourceId& id) const {
28408 double units = 0.0;
28409 bool unsized = false;
28410 const auto add = [&](const PlacementSnapshot& row, double own) {
28411 if (!row.opening || row.family != PineOrderFamily::Entry || row.is_long == is_long
28412 || row.placement_script_open_ms != script_open_ms || row.source_id == id
28413 || finite_positive(row.exit_levels.limit)
28414 || finite_positive(row.exit_levels.stop)
28415 || finite_positive(row.exit_levels.trail_points)
28416 || finite_positive(row.exit_levels.trail_price)
28417 || finite_positive(row.exit_levels.trail_offset)) {
28418 return;
28419 }
28420 const bool measured = row.oca_name.empty() && !row.birth.from_fill()
28421 && (row.qty_type < 0 || row.qty_type == static_cast<int>(QtyType::FIXED));
28422 if (measured && finite_positive(own)) units += own;
28423 else unsized = true;
28424 };
28425 for (const auto& prior : pending_same_bar_commands_) {
28426 add(prior.snapshot, finite_positive(prior.snapshot.frozen_market_own_units)
28427 ? prior.snapshot.frozen_market_own_units : prior.snapshot.requested_qty);
28428 }
28429 for (const auto& prior : pending_entries_) add(prior.snapshot, kNaN);
28430 for (const auto& handle : live_handles_) {
28431 const auto prior = placement_.find(handle.incarnation);
28432 if (prior != placement_.end()) add(prior->second, prior->second.requested_qty);
28433 }
28434 return unsized ? kNaN : units;
28435}
28436
28437// The flat-entry pair handling: whether the opening fill of a flat entry is the
28438// first of a same-point pair whose opposite member, placed on the same bar,
28439// fills at this same price next. TradingView checks the margin of such a pair
28440// once both have filled: a commissioned pair whose one transaction the
28441// commission leaves just short of the equity is booked with no margin call
28442// (lab tv w6b-p1c-pair-gross-percent MM-SF-U and MS-SF-U).
28443bool PineExecutionAdapter::same_point_pair_fill_follows(
28444 const PlacementSnapshot& filled, const native_order::RequestHandle& filled_handle,
28445 double price) const {
28446 if (!filled.opening || filled.family != PineOrderFamily::Entry
28447 || filled.projection_position_side != static_cast<std::int32_t>(PositionSide::FLAT)
28448 || !finite_positive(filled.requested_qty) || !finite_positive(price)
28449 || !same_point_pair_scope()) {
28450 return false;
28451 }
28452 return std::any_of(live_handles_.begin(), live_handles_.end(),
28453 [&](const native_order::RequestHandle& handle) {
28454 if (handle == filled_handle) return false;
28455 const auto found = placement_.find(handle.incarnation);
28456 if (found == placement_.end()) return false;
28457 const auto& row = found->second;
28458 if (!row.opening || row.family != PineOrderFamily::Entry
28459 || row.is_long == filled.is_long
28460 || row.projection_created_bar != filled.projection_created_bar
28461 || row.projection_position_side
28462 != static_cast<std::int32_t>(PositionSide::FLAT)
28463 || !finite_positive(row.requested_qty) || !row.oca_name.empty()
28464 || row.birth.from_fill()
28465 || !(row.qty_type < 0 || row.qty_type == static_cast<int>(QtyType::FIXED))) {
28466 return false;
28467 }
28468 const bool stop = finite_positive(row.exit_levels.stop);
28469 const bool limit = finite_positive(row.exit_levels.limit);
28470 if (stop && limit) return false;
28471 if (stop) return row.is_long ? row.exit_levels.stop <= price
28472 : row.exit_levels.stop >= price;
28473 if (limit) return row.is_long ? row.exit_levels.limit >= price
28474 : row.exit_levels.limit <= price;
28475 return true;
28476 });
28477}
28478
28479// The flat-entry pair handling, item 3. Under process_orders_on_close a flat pair
28480// of one market call and one priced opposite call, both of this bar and the
28481// priced one already marketable at the close, fills at that close in the
28482// order and with the quantities of W6's rules: the rank of each leg (buy
28483// market, buy stop, sell market, sell stop, buy limit, sell limit), the later
28484// call as one transaction of its own quantity plus the earlier call's when
28485// that one is the market, the earlier call its own (lab tv w6-f10d-pooc-pair
28486// and w6-f10e-pooc-coof-pair MS-SF, SM-SF, LM-SF, LM-LF, ML-SF). The close
28487// pass had filled the priced leg first, before the market leg of
28488// apply_terminal_explicit_market_policy, each with its own quantity. A
28489// market pair keeps that policy's own route. True when the pair filled here.
28490bool PineExecutionAdapter::fill_pooc_close_pair(
28491 double raw_close, const NativeDecisionContext& context) {
28492 const double tick = staged_.syminfo.mintick;
28493 const double close_tick = source_bar_fill_tick(raw_close, tick);
28494 if (config_.pyramiding != 0 || config_.slippage != 0 || config_.close_entries_rule_any
28495 || config_.commission_value != 0.0 || config_.margin_long != 100.0
28496 || config_.margin_short != 100.0 || bar_magnifier_
28497 || config_.default_qty_type != static_cast<int>(QtyType::FIXED)
28498 || risk_.direction != 0 || risk_.max_cons_loss_days != 0
28499 || risk_.max_drawdown > 0.0 || risk_.max_intraday_loss > 0.0
28500 || risk_.max_position_size > 0.0 || risk_.halted || cap.active()
28501 || !finite_positive(close_tick)
28502 || detail::run_position(require_host()).signed_units != 0.0) {
28503 return false;
28504 }
28505 struct Leg {
28506 native_order::RequestHandle handle;
28507 PlacementSnapshot row;
28508 bool priced = false;
28509 int rank = 0;
28510 };
28511 std::vector<Leg> legs;
28512 const int bar_index = projection_bar_index(context);
28513 for (const auto& handle : live_handles_) {
28514 const auto found = placement_.find(handle.incarnation);
28515 if (found == placement_.end()) continue;
28516 const auto& row = found->second;
28517 if (!row.opening || (row.family != PineOrderFamily::Entry
28518 && row.family != PineOrderFamily::Order)) {
28519 continue;
28520 }
28521 if (row.projection_created_bar != bar_index) return false; // a resting entry
28522 const bool stop = finite_positive(row.exit_levels.stop);
28523 const bool limit = finite_positive(row.exit_levels.limit);
28525 || row.projection_position_side != static_cast<std::int32_t>(PositionSide::FLAT)
28526 || !finite_positive(row.requested_qty)
28527 || !(row.qty_type < 0 || row.qty_type == static_cast<int>(QtyType::FIXED))
28528 || !row.oca_name.empty() || row.birth.from_fill() || row.birth.at_terminal_fill()
28529 || (stop && limit) || row.direction_gate
28530 || finite_positive(row.exit_levels.trail_points)
28531 || finite_positive(row.exit_levels.trail_price)
28532 || finite_positive(row.exit_levels.trail_offset)) {
28533 return false;
28534 }
28535 if (stop && (row.is_long ? close_tick < row.exit_levels.stop
28536 : close_tick > row.exit_levels.stop)) {
28537 return false;
28538 }
28539 if (limit && (row.is_long ? close_tick > row.exit_levels.limit
28540 : close_tick < row.exit_levels.limit)) {
28541 return false;
28542 }
28543 legs.push_back({handle, row, stop || limit,
28544 limit ? (row.is_long ? 4 : 5)
28545 : (stop ? (row.is_long ? 1 : 3) : (row.is_long ? 0 : 2))});
28546 }
28547 if (legs.size() != 2 || legs[0].row.is_long == legs[1].row.is_long
28548 || legs[0].priced == legs[1].priced) {
28549 return false;
28550 }
28551 if (legs[1].row.command_sequence < legs[0].row.command_sequence) std::swap(legs[0], legs[1]);
28552 // legs[0] is the earlier call; the later one adds its quantity when it
28553 // is the market.
28554 const double pending = legs[0].priced ? 0.0 : legs[0].row.requested_qty;
28555 std::vector<std::pair<Leg*, double>> order = {
28556 {&legs[0], legs[0].row.requested_qty},
28557 {&legs[1], legs[1].row.requested_qty + pending},
28558 };
28559 if (legs[1].rank < legs[0].rank) std::swap(order[0], order[1]);
28560 // TradingView's admission of that transaction is unmeasured here: one
28561 // over the equity at the close keeps the legacy route.
28562 const double equity = require_host().native_marked_equity(close_tick);
28563 const double required = (legs[1].row.requested_qty + pending) * close_tick
28564 * staged_.syminfo.pointvalue * active_staged_fx(context.sub_bar_open_ms);
28565 if (!std::isfinite(required) || !std::isfinite(equity) || required > equity) return false;
28566 bool filled = false;
28567 for (const auto& [leg, units] : order) {
28568 native_order::Request request;
28569 request.intent = native_order::Transact{leg->row.is_long ? units : -units};
28570 request.label = leg->row.source_id;
28571 request.comment = leg->row.comment;
28572 request.trigger = native_order::Market{};
28573 PlacementSnapshot immediate = leg->row;
28574 immediate.cancellation = {};
28575 immediate.market_admission = {};
28576 immediate.reverse_to = false;
28577 immediate.terms_priced_reverse = false;
28578 immediate.forced_execution_price = close_tick;
28579 const auto accepted = submit_or_replace(
28580 std::move(request), std::move(immediate), true, leg->row.source_id);
28581 if (!accepted) return filled;
28582 auto& placed = placement_.at(accepted->incarnation);
28583 placed.projection_position_side = leg->row.projection_position_side; invalidate_retirement();
28584 (void)require_host().execute_current(
28585 {*accepted, NativeCurrentPriceRule::NearestTick});
28586 filled = true;
28587 }
28588 return filled;
28589}
28590
28591// The flat-entry pair handling, item 2: under pyramiding above 1 the queued
28592// market's quantity joins the later call's transaction on the book the tapes
28593// measure only (w6b-p2a, w6b-p2b): the call and one other entry-like order,
28594// both of this script bar, nothing resting from an earlier one. A larger book
28595// keeps its legacy route (test_dual_entry_placement_sizing_l4b MM cases).
28596bool PineExecutionAdapter::same_point_two_leg_book(
28597 const SourceId& id, std::int64_t script_open_ms) const {
28598 std::size_t others = 0;
28599 bool foreign = false;
28600 const auto count = [&](const PlacementSnapshot& row) {
28601 if (!row.opening || (row.family != PineOrderFamily::Entry
28603 || row.source_id == id) {
28604 return;
28605 }
28606 if (row.placement_script_open_ms == script_open_ms) ++others;
28607 else foreign = true;
28608 };
28609 for (const auto& prior : pending_same_bar_commands_) count(prior.snapshot);
28610 for (const auto& prior : pending_entries_) count(prior.snapshot);
28611 for (const auto& handle : live_handles_) {
28612 const auto found = placement_.find(handle.incarnation);
28613 if (found != placement_.end()) count(found->second);
28614 }
28615 return !foreign && others == 1;
28616}
28617
28618// The entry/reversal fill ordering, family F10. TradingView fills the entries that
28619// reach one fill point in a fixed order of side and kind -- buy market, buy
28620// stop, sell market, sell stop, buy limit, sell limit -- and entries of one
28621// rank in the order they were placed (lab tv w6-f10a-open-pair,
28622// w6-f10b-limit-class and w6-f10c-same-side-class, tests/fixtures/
28623// same_point_entries). The kernel breaks a tie at one cursor oldest request
28624// first, so after a source evaluation this queues the flat entries it placed
28625// in that order: every request from the first one out of order on is
28626// re-priced in place (ReplaceOptions::keep_handle), which keeps its handle
28627// and ranks it newest. Only entries already marketable at the evaluation's
28628// price can share the next fill point, and a pure-stop book is
28629// defer_open_marketable_sells' at that open. The scope is the flat
28630// explicit-quantity family same_bar_market_tx_scope() settles as one broker
28631// batch of frozen transactions, which is what the tapes measure, widened by
28632// same_point_pair_scope() to the configurations lane W6B-ENG-PAIRS measured.
28633void PineExecutionAdapter::order_same_point_entries() {
28634 if (!has_live_family(PineOrderFamily::Entry) || !host_ || live_handles_.size() < 2 || !same_point_pair_scope()) return;
28635 auto& host = require_host();
28636 if (detail::run_position(host).signed_units != 0.0) return;
28637 const auto point = detail::callback_point(host);
28638 if (!point || !finite_positive(point->price)) return;
28639 const int bar = projection_bar_index(point->decision);
28640 const double signal = point->price;
28641 struct Candidate {
28642 native_order::RequestHandle handle;
28643 int rank = 0;
28644 std::uint64_t queue = 0;
28645 };
28646 std::vector<Candidate> candidates;
28647 bool pure_stops = true;
28648 for (const auto& handle : live_handles_) {
28649 const auto found = placement_.find(handle.incarnation);
28650 if (found == placement_.end()) continue;
28651 const auto& row = found->second;
28652 const bool stop = finite_positive(row.exit_levels.stop);
28653 const bool limit = finite_positive(row.exit_levels.limit);
28654 if (row.family != PineOrderFamily::Entry || !row.opening
28655 || row.projection_created_bar != bar
28656 || row.projection_position_side != static_cast<std::int32_t>(PositionSide::FLAT)
28657 || !finite_positive(row.requested_qty)
28658 || !(row.qty_type < 0 || row.qty_type == static_cast<int>(QtyType::FIXED))
28659 || !row.oca_name.empty() || row.birth.from_fill() || (stop && limit)
28660 || finite_positive(row.exit_levels.trail_points)
28661 || finite_positive(row.exit_levels.trail_price)
28662 || finite_positive(row.exit_levels.trail_offset)
28663 || (stop && (row.is_long ? row.exit_levels.stop > signal
28664 : row.exit_levels.stop < signal))
28665 || (limit && (row.is_long ? row.exit_levels.limit < signal
28666 : row.exit_levels.limit > signal))) {
28667 continue;
28668 }
28669 pure_stops = pure_stops && stop;
28670 candidates.push_back({handle, limit ? (row.is_long ? 4 : 5)
28671 : (stop ? (row.is_long ? 1 : 3)
28672 : (row.is_long ? 0 : 2)), 0});
28673 }
28674 if (candidates.size() < 2 || pure_stops) return;
28675 // Under pyramiding above 1 the rank covers the books lane W6B-ENG-PAIRS
28676 // measured -- a two-leg pair, or legs of one side -- and nothing else:
28677 // a third entry-like order of either side, one resting from an earlier
28678 // bar, or a replaced or cancelled call keeps the legacy source order its
28679 // pinned cases assert (test_dual_entry_placement_sizing_l4b MM cases).
28680 if (config_.pyramiding > 1) {
28681 if (source_batch_mutated_) return;
28682 for (const auto& candidate : candidates) {
28683 const auto found = placement_.find(candidate.handle.incarnation);
28684 if (found != placement_.end() && found->second.replaced_opening) return;
28685 }
28686 const auto buys = [](const Candidate& c) { return c.rank == 0 || c.rank == 1 || c.rank == 4; };
28687 const auto buy_count = std::count_if(candidates.begin(), candidates.end(), buys);
28688 const bool both_sides = buy_count != 0
28689 && buy_count != static_cast<std::ptrdiff_t>(candidates.size());
28690 if (both_sides && candidates.size() > 2) return;
28691 for (const auto& handle : live_handles_) {
28692 const auto found = placement_.find(handle.incarnation);
28693 if (found == placement_.end() || !found->second.opening
28694 || (found->second.family != PineOrderFamily::Entry
28695 && found->second.family != PineOrderFamily::Order)) {
28696 continue;
28697 }
28698 const bool candidate = std::any_of(candidates.begin(), candidates.end(),
28699 [&](const Candidate& c) { return c.handle == handle; });
28700 if (!candidate) return;
28701 }
28702 }
28703 const auto working = host.native_working_requests();
28704 const auto definition = [&](const native_order::RequestHandle& handle) {
28705 const auto row = std::find_if(working.begin(), working.end(),
28706 [&](const NativeWorkingRequest& request) {
28707 return request.definition->handle == handle;
28708 });
28709 return row == working.end() ? nullptr : row->definition.get();
28710 };
28711 for (auto& candidate : candidates) {
28712 const auto* live = definition(candidate.handle);
28713 if (!live) return;
28714 candidate.queue = live->priority != 0 ? live->priority : live->handle.incarnation;
28715 }
28716 std::stable_sort(candidates.begin(), candidates.end(),
28717 [](const Candidate& left, const Candidate& right) {
28718 if (left.rank != right.rank) return left.rank < right.rank;
28719 return left.queue < right.queue;
28720 });
28721 std::size_t first = candidates.size();
28722 std::uint64_t highest = 0;
28723 for (std::size_t index = 0; index < candidates.size(); ++index) {
28724 if (candidates[index].queue < highest) {
28725 first = index;
28726 break;
28727 }
28728 highest = candidates[index].queue;
28729 }
28730 native_order::ReplaceOptions in_place;
28731 in_place.keep_handle = true;
28732 for (std::size_t index = first; index < candidates.size(); ++index) {
28733 const auto& handle = candidates[index].handle;
28734 const auto* live = definition(handle);
28735 if (!live) return;
28736 const native_order::Request request = live->request;
28737 const auto result = host.replace(handle, request, in_place);
28738 if (result.status != native_order::ReplaceStatus::Replaced) return;
28739 // This changes order only, through the same live bookkeeping.
28740 // Preserve the existing survivor order and append the moved handle.
28741 requeue_live(handle);
28742 }
28743}
28744
28745// L18 dependency adapter. L8c supplies the approved global prefix end and
28746// L6b-4 supplies the bound scratch directory. No sink text/curve API is
28747// involved: DESIGN 3.8 permits a second private unlinked scratch file.
28748void PineExecutionAdapter::preserve_position_tail_before_seal(
28749 std::size_t global_end, const std::string& scratch_directory) {
28750 auto* pine = pine_view_of(host_);
28751 if (!pine || global_end < pine->trades_.base() || global_end > pine->trades_.size()) {
28752 throw coded<std::logic_error>(RunFailureCode::engine_invariant, {},
28753 "invalid Pine position-tail seal prefix");
28754 }
28755 observe_position_tail_rows(); // never miss a row retired by the row source
28756 detail::PositionTailBinding::preserve(position_tail_spill_, pine->trades_, global_end,
28757 position_open_script_bar_, scratch_directory,
28758 pine->cache_policy_for_run().position_tail_pages);
28759}
28760
28763 return position_tail_summary_ ? position_tail_summary_->work : PositionTailWork{};
28764}
28765
28766void PineExecutionAdapter::observe_position_tail_rows() {
28767 if (!position_tail_summary_) return;
28768 auto* pine = pine_view_of(host_);
28769 // Short-seed presentation swaps incarnations after Applied returns.
28770 // Non-Pine hosts can amend rows without the Pine permutation seam.
28771 if (!pine || short_seed_.final_short.incarnation != 0) {
28772 position_tail_summary_->invalidate(
28775 return;
28776 }
28777 const auto& host = require_host();
28778 position_tail_summary_->observe(pine->trades_.base(), host.closed_trade_count(),
28779 [&](std::size_t index) -> Trade { return host.get_trade(static_cast<int>(index)); },
28781}
28782
28783void PineExecutionAdapter::begin_position_tail_summary() {
28784 if (!position_tail_summary_)
28785 position_tail_summary_ = std::make_shared<detail::AdapterPositionTailSummary>();
28786 auto* pine = pine_view_of(host_);
28787 if (!pine || short_seed_.final_short.incarnation != 0) {
28788 position_tail_summary_->invalidate(
28791 return;
28792 }
28793 // L18 has just reset the old spill. Replay the resident reachable suffix
28794 // once, retaining equality at the opening mark, including old margin ids.
28795 const auto& host = require_host();
28796 position_tail_summary_->begin_position(position_open_script_bar_, pine->trades_.base(),
28797 host.closed_trade_count(),
28798 [&](std::size_t index) -> Trade { return host.get_trade(static_cast<int>(index)); },
28800 const bool is_long = detail::run_position(require_host()).signed_units > 0.0;
28801 for (const auto& cohort : cohorts_by_id_.held_rows()) {
28802 if (cohort.second.live_units_by_origin.empty()) continue;
28803 position_tail_summary_->live_cohort(&cohort, true);
28804 for (const auto& origin : cohort.second.live_units_by_origin)
28805 position_tail_summary_->open_origin(origin.first, is_long,
28807 }
28808}
28809
28810double PineStrategyHost::default_entry_qty(double fill_price) const {
28811 // TradingView does not validate this query's hypothetical price before
28812 // applying the declared sizing mode: fixed sizing ignores it, while
28813 // money sizing returns zero for NaN/zero and a signed grid quantity for
28814 // a negative price. Observation only: no order or driver state changes.
28815 if (adapter_.config_.default_qty_type == static_cast<int>(QtyType::FIXED)) {
28816 return floor_quantity_grid(adapter_.config_.default_qty_value,
28817 adapter_.staged_.quantity_grid);
28818 }
28819 auto sizing = adapter_.sizing_snapshot();
28820 sizing.price = fill_price;
28821 if (!std::isfinite(fill_price) || fill_price == 0.0) return 0.0;
28822 if (fill_price > 0.0) return adapter_.default_sizing_units(sizing);
28823 sizing.price = -fill_price;
28824 const auto sized = adapter_.default_sizing_shape(sizing);
28825 if (!sized) return 0.0;
28826 const auto units = native_sized_units(
28827 *sized, -fill_price, sizing.equity, sizing.fx);
28828 // The core computes the positive-magnitude quotient once; apply the source
28829 // lot floor before restoring the negative hypothetical price's sign.
28830 return units ? -adapter_.default_sizing_lot_floor(*units) : 0.0;
28831}
28832
28833} // namespace pineforge::source
28834
28835#include "exit_payload_codec.inc"
28836
28837namespace pineforge::source {
28838
28839// These definitions are appended so existing source citations
28840// keep their line numbers. Only the retained window is addressable; the
28841// original count and append digest remain the source hash's logical words.
28843 if (global < base_ || global >= size()) {
28845 "origin roster read outside retained window");
28846 }
28847 return items_[global - base_];
28848}
28849
28851 std::vector<std::size_t>& out) const {
28852 const auto found = positions_.find(handle.incarnation);
28853 if (found == positions_.end()) return;
28854 for (const auto position : found->second)
28855 if ((*this)[position] == handle) out.push_back(position);
28856}
28857
28858void OriginRoster::retire_before(std::size_t global) {
28859 if (global < base_ || global > size()) {
28861 "origin roster retirement outside retained window");
28862 }
28863 if (global == base_) return;
28864 // Commit the non-allocating edits first. A compaction allocation failure
28865 // may abort the run, but cannot leave the index using the previous base.
28866 for (auto at = positions_.begin(); at != positions_.end();) {
28867 auto& positions = at->second;
28868 const auto kept = std::lower_bound(positions.begin(), positions.end(), global);
28869 positions.erase(positions.begin(), kept);
28870 if (positions.empty()) at = positions_.erase(at);
28871 else ++at;
28872 }
28873 items_.erase(items_.begin(), items_.begin() + static_cast<std::ptrdiff_t>(global - base_));
28874 base_ = global;
28875 if (items_.empty()) {
28876 decltype(items_){}.swap(items_);
28877 decltype(positions_){}.swap(positions_);
28878 return;
28879 }
28880 // An old peak must not leave a historical vector, inner vector or bucket
28881 // allocation resident. Slack is bounded by the current retained window.
28882 if (items_.capacity() / 4 > items_.size())
28883 decltype(items_)(items_).swap(items_);
28884 for (auto& row : positions_) {
28885 auto& positions = row.second;
28886 if (positions.capacity() / 4 > positions.size())
28887 std::vector<std::size_t>(positions).swap(positions);
28888 }
28889 if (positions_.bucket_count() / 4 > positions_.size())
28890 positions_.rehash(positions_.size());
28891}
28892
28893// Called at an outermost bar open, after terminal receipts, placement-row
28894// erasure and cohort_remove. Keeping an origin's own retained row is more
28895// conservative than keeping only live placements, and preserves every old
28896// row reader. Explicit names in live/queued placements also pin an origin
28897// whose own row has gone. No persistent release bitmap or lookup is added.
28898void PineExecutionAdapter::retire_origin_prefixes() {
28899 RetirementObservation observation(*this);
28900 auto& work = retirement_work_;
28901 ++work.prefix_sweeps;
28902 cohorts_by_id_.spill_implicitly();
28903 if (detail::retain_retired_rows()) return;
28904 // Skip prefix retirement while every nonempty front still has a placement;
28905 // later name and ownership checks cannot release such a front. Erasure
28906 // invalidates this fact; new/restored cohorts are empty and origins append
28907 // after remember. Without that guarantee, run the full retirement walk.
28908 if (retired_row_scratch_.origin_fronts_pinned) return;
28909 // The front row alone can pin each retained window. Check before building
28910 // named, so a resting bracket/stop pair allocates no retirement scratch.
28911 if (std::all_of(cohorts_by_id_.begin(), cohorts_by_id_.end(), [this](const auto& row) {
28912 auto& counters = retirement_work_;
28913 ++counters.cohort_rows;
28914 ++counters.certificate_rows;
28915 const auto& members = row.second.origins.members();
28916 if (members.empty()) return true;
28917 ++counters.fronts_tested;
28918 const bool present = placement_.contains(members.front().incarnation);
28919 if (!present) ++counters.missing_fronts;
28920 return present;
28921 })) { retired_row_scratch_.origin_fronts_pinned = true; return; }
28922 IExecutionConsumer* consumer = bound_consumer();
28923 if (!consumer) return;
28924 const auto& core = as_native_consumer(*consumer).request_core();
28925 std::vector<std::uint64_t> named;
28926 const auto name = [&](std::uint64_t incarnation) {
28927 if (incarnation != 0) named.push_back(incarnation);
28928 };
28929 const auto snapshot_names = [&](const PlacementSnapshot& row) {
28930 name(row.bracket_origin.incarnation);
28933 name(row.projection_predecessor);
28934 name(row.legs.target().incarnation);
28936 };
28937 const auto request_names = [&](const native_order::Request& request) {
28938 if (const auto* wait = std::get_if<native_order::WaitForApplied>(&request.owner)) {
28939 name(wait->parent.incarnation);
28940 } else if (const auto* opening = std::get_if<native_order::BindOpening>(&request.owner)) {
28941 name(opening->opening.incarnation);
28942 } else if (const auto* openings = std::get_if<native_order::BindOpenings>(&request.owner)) {
28943 for (const auto& opening : openings->openings) name(opening.incarnation);
28944 }
28945 };
28946 const auto live_names = [&](const native_order::RequestHandle& handle) {
28947 name(handle.incarnation);
28948 const auto at = placement_.find(handle.incarnation);
28949 if (at != placement_.end()) snapshot_names(at->second);
28950 };
28951 for (const auto& handle : live_handles_) live_names(handle);
28952 for (const auto& live : core.live()) {
28953 live_names(live.handle());
28954 request_names(live.definition->request);
28955 if (live.definition->root) name(live.definition->root->incarnation);
28956 }
28957 for (const auto& leg : pending_bracket_legs_) {
28958 snapshot_names(leg.snapshot); request_names(leg.request);
28959 }
28960 for (const auto& entry : pending_entries_) {
28961 snapshot_names(entry.snapshot); request_names(entry.request);
28962 }
28963 for (const auto& order : delayed_market_orders_) {
28964 snapshot_names(order.snapshot); request_names(order.request);
28965 }
28966 for (const auto& sell : deferred_open_marketable_sells_) snapshot_names(sell.snapshot);
28967 for (const auto& row : throttled_reopen_rearm_) snapshot_names(row);
28968 for (const auto& command : pending_same_bar_commands_) {
28969 snapshot_names(command.snapshot); request_names(command.request);
28970 }
28971 for (const auto& shadow : source_shadow_pending_) snapshot_names(shadow.snapshot);
28972 for (const auto& pending : pending_coof_requests_) {
28973 snapshot_names(pending.snapshot); request_names(pending.request);
28974 }
28975 for (const auto& revival : pending_margin_revivals_) snapshot_names(revival.snapshot);
28976 for (const auto& leg : anchored_relative_legs_) {
28977 name(leg.handle.incarnation); name(leg.parent.incarnation); request_names(leg.request);
28978 }
28979 std::sort(named.begin(), named.end());
28980 named.erase(std::unique(named.begin(), named.end()), named.end());
28981 for (auto& row : cohorts_by_id_) {
28982 ++work.cohort_rows;
28983 auto& facts = row.second;
28984 auto& origins = facts.origins;
28985 std::size_t through = origins.base();
28986 for (const auto& origin : origins) {
28987 ++work.fronts_tested;
28988 const bool present = placement_.contains(origin.incarnation);
28989 if (present
28990 || std::binary_search(named.begin(), named.end(), origin.incarnation)
28991 || facts.live_units_by_origin.find(origin.incarnation)
28992 != facts.live_units_by_origin.end()
28993 || std::find(facts.opened.begin(), facts.opened.end(), origin) != facts.opened.end()
28994 || core.cohort_contains(facts.handle, origin)) {
28995 if (!present && through == origins.base()) ++work.missing_fronts_pinned;
28996 break;
28997 }
28998 ++through;
28999 }
29000 const auto old_base = origins.base();
29001 origins.retire_before(through);
29002 work.origins_retired += through - old_base;
29003 if (origins.members().empty()) {
29004 if (auto* index = adapter_lookup_index(consumer, this, run_counter_, false))
29005 index->cohort_sides.erase(facts.handle.value);
29006 }
29007 }
29008 cohorts_by_id_.spill_implicitly();
29009 if (auto* index = adapter_lookup_index(consumer, this, run_counter_, false)) {
29010 auto& sides = index->cohort_sides;
29011 if (sides.bucket_count() / 4 > sides.size()) sides.rehash(sides.size());
29012 }
29013}
29014
29017 RetirementPopulation result;
29018 result.live = live_handles_.size();
29019 result.pending = pending_bracket_legs_.size() + pending_entries_.size()
29020 + delayed_market_orders_.size() + deferred_open_marketable_sells_.size()
29021 + throttled_reopen_rearm_.size() + pending_same_bar_commands_.size()
29022 + source_shadow_pending_.size() + pending_coof_requests_.size()
29023 + pending_margin_revivals_.size() + anchored_relative_legs_.size();
29024 const auto owners = [&](const native_order::Request& request) {
29025 if (const auto* wait = std::get_if<native_order::WaitForApplied>(&request.owner))
29026 result.owners += wait->parent.incarnation != 0;
29027 else if (const auto* opening = std::get_if<native_order::BindOpening>(&request.owner))
29028 result.owners += opening->opening.incarnation != 0;
29029 else if (const auto* openings = std::get_if<native_order::BindOpenings>(&request.owner))
29030 for (const auto& opening : openings->openings)
29031 result.owners += opening.incarnation != 0;
29032 };
29033 for (const auto& leg : pending_bracket_legs_) owners(leg.request);
29034 for (const auto& entry : pending_entries_) owners(entry.request);
29035 for (const auto& order : delayed_market_orders_) owners(order.request);
29036 for (const auto& command : pending_same_bar_commands_) owners(command.request);
29037 for (const auto& request : pending_coof_requests_) owners(request.request);
29038 for (const auto& leg : anchored_relative_legs_) owners(leg.request);
29039 result.opened = cohort_opened_slot_count_;
29040 result.m = result.live + result.pending + result.owners + result.opened;
29041 result.R = placement_.size() - result.live;
29042 result.C = cohorts_by_id_.size();
29043 placement_.visit_rows([&](std::uint64_t incarnation, const PlacementSnapshot& row) {
29046 const bool live = std::any_of(live_handles_.begin(), live_handles_.end(),
29047 [&](const auto& handle) { return handle.incarnation == incarnation; });
29048 if (!live) ++result.retired_exit_rows;
29049 }
29050 return false;
29051 });
29052 return result;
29053}
29054
29055} // namespace pineforge::source
29056
29057namespace pineforge::source {
29058
29059OriginRoster OriginRoster::from_retired(std::size_t count, std::uint64_t digest) {
29060 OriginRoster result;
29061 result.base_ = count;
29062 result.digest_ = digest;
29063 return result;
29064}
29065
29067PineExecutionAdapter::CohortFacts::retired_record() const noexcept {
29068 static_assert(sizeof(Record) == 40, "retired cohort scratch record");
29069 return {handle.value, cycle, origins.size(), origins.digest(), erased_opening_sides};
29070}
29071
29072PineExecutionAdapter::CohortFacts
29073PineExecutionAdapter::CohortFacts::restore_record(const Record& row, bool /*arena_backed*/) {
29074 if (row.handle == 0 || static_cast<std::uint64_t>(
29075 static_cast<std::size_t>(row.origin_count)) != row.origin_count
29076 || row.erased_sides > 3) {
29077 ::pineforge::detail::raise_scratch_io_error(EIO, "scratch cohort invalid record");
29078 }
29079 CohortFacts facts;
29080 facts.handle.value = row.handle;
29081 facts.cycle = row.cycle;
29082 facts.origins = OriginRoster::from_retired(
29083 static_cast<std::size_t>(row.origin_count), row.origin_digest);
29084 facts.erased_opening_sides = static_cast<std::uint8_t>(row.erased_sides);
29085 return facts;
29086}
29087
29088bool PineExecutionAdapter::CohortFacts::spillable() const noexcept {
29089 return origins.members().empty() && opened.empty() && live_units_by_origin.empty();
29090}
29091
29092bool PineExecutionAdapter::CohortFacts::held() const noexcept {
29093 // Predicate and held-walk design credited to the contributor of external PR #458.
29094 return !opened.empty() || !live_units_by_origin.empty();
29095}
29096
29097// The one place that binds the scratch
29098// directory of every adapter-owned L19l table -- part A's cohort and
29099// entry-rank tables, part C's placement-minima and consumed-cycle tables --
29100// the way set_successor_roots_directory binds every table of the kernel core.
29101// ScratchFile and IdPairStore accept the directory they already hold, so
29102// calling both public names (this and set_auxiliary_tables_directory) with
29103// one directory is harmless. A different directory once a table's file is
29104// open throws std::logic_error from that table: bind after reset_for_run and
29105// before the first id, as the declaration says.
29106void PineExecutionAdapter::set_id_tables_directory(const std::string& directory) {
29107 cohorts_by_id_.set_directory(directory);
29108 exit_rank_keys_.set_directory(directory);
29109 placement_.set_auxiliary_tables_directory(directory);
29110 consumed_partial_exit_cycles_.set_directory(directory);
29111 if (!position_tail_summary_)
29112 position_tail_summary_ = std::make_shared<detail::AdapterPositionTailSummary>();
29113 position_tail_summary_->set_directory(directory);
29114}
29115
29117 cohorts_by_id_.set_cache_capacity(policy.cohort_id_pages);
29118 exit_rank_keys_.set_cache_capacity(policy.exit_rank_pages);
29119 placement_.set_cache_capacity(policy.erased_minima_pages, policy.retained_sequence_pages);
29120 consumed_partial_exit_cycles_.set_cache_capacity(policy.consumed_cycle_pages);
29121}
29122
29124PineExecutionAdapter::require_cohort(const SourceId& id) {
29125 auto found = cohorts_by_id_.find(id);
29126 if (found == cohorts_by_id_.end()) {
29128 "accepted opening has no source cohort");
29129 }
29130 return found;
29131}
29132
29133} // namespace pineforge::source
29134
29135// Keep the new storage wiring together, away from part A's
29136// cohort / entry-id changes in this translation unit. These paths and file
29137// offsets describe storage of existing state, not additional broker state.
29138namespace pineforge::source {
29139
29140void PlacementTable::set_auxiliary_tables_directory(const std::string& directory) {
29141 erased_minima_.set_directory(directory); sequence_index_.reset();
29142}
29143
29144void PlacementTable::bind_auxiliary_scratch_tables(const std::shared_ptr<::pineforge::detail::ScratchSlab>& slab) {
29145 erased_minima_.bind_scratch_tables(slab, ::pineforge::detail::ScratchResource::erased_data,
29147 sequence_index_.reset();
29148}
29149
29151 if (high_water_ != 0)
29154 "placement auxiliary fixture backend changed after use");
29155 erased_minima_.fixture_use_memory_reference(); sequence_index_.reset();
29156}
29157
29158std::uint64_t PlacementTable::auxiliary_disk_bytes() const noexcept {
29159 return erased_minima_.disk_bytes();
29160}
29161
29162// Part C's name for the merged binding: set_id_tables_directory above binds
29163// the placement minima and consumed-cycle tables together with part A's tables.
29165 set_id_tables_directory(directory);
29166}
29167
29169 placement_.fixture_use_memory_auxiliary_tables();
29170 consumed_partial_exit_cycles_.fixture_use_memory_reference();
29171}
29172
29174 return placement_.auxiliary_disk_bytes() + consumed_partial_exit_cycles_.disk_bytes();
29175}
29176
29177} // namespace pineforge::source
29178
29179#include "../scratch_table_binders.hpp"
29181
29182namespace pineforge::detail {
29183
29184// The tree and IdPair lanes fill their respective backend-owned seams;
29185// the foundation's begin calls and binder declarations remain unchanged.
29187 const std::shared_ptr<ScratchSlab>& slab) {
29188 // Refuse a late rebind before either table can change backing/bytes.
29189 if (adapter.cohorts_by_id_.size() != 0 || adapter.exit_rank_keys_.size() != 0)
29191 "tree scratch backend changed after use");
29192 adapter.cohorts_by_id_.bind_scratch(slab, ScratchResource::cohort);
29193 adapter.exit_rank_keys_.bind_scratch(slab, ScratchResource::exit_rank);
29194}
29195
29197 const std::shared_ptr<ScratchSlab>& slab) {
29198 adapter.placement_.bind_auxiliary_scratch_tables(slab);
29199 adapter.consumed_partial_exit_cycles_.bind_scratch_tables(
29201}
29202
29203} // namespace pineforge::detail
29204
29205namespace pineforge::source {
29206
29207void PineExecutionAdapter::require_cohort_ticket(const SourceId* prospective) const {
29208 cohorts_by_id_.require_inserts([&](auto&& visit) {
29209 if (prospective) visit(*prospective);
29210 for (const auto& row : pending_same_bar_commands_)
29211 if (row.opening) visit(row.snapshot.source_id);
29212 for (const auto& row : pending_entries_) visit(row.snapshot.source_id);
29213 for (const auto& row : pending_coof_requests_)
29214 if (row.opening) visit(row.snapshot.source_id);
29215 for (const auto& row : delayed_market_orders_)
29216 if (row.snapshot.family == PineOrderFamily::Entry
29217 || row.snapshot.family == PineOrderFamily::Order)
29218 visit(row.snapshot.source_id);
29219 for (const auto& row : deferred_open_marketable_sells_) visit(row.snapshot.source_id);
29220 });
29221}
29222
29223void PineExecutionAdapter::require_exit_rank_key(const SourceId& id) const {
29224 // Preserve the 27/28 cutoff: an ignored 28th key needs no ticket, even
29225 // when its string would exceed the admitted aggregate byte allowance.
29226 if (exit_rank_keys_.size() > 26) return;
29227 exit_rank_keys_.require_insert(id);
29228}
29229
29230void PineExecutionAdapter::require_tree_precommit(const NativePrecommitView& view) const {
29231 // The kernel calls PRECOMMIT only for an Applied-ready candidate, before
29232 // financial effects. Capacity is a coded infrastructure exception, never
29233 // a trading Refuse. This mirrors note_exit_rank_key's on_applied predicate.
29234 if (!view.account.would_open || view.inspected_opened_units == 0.0) return;
29235 const auto row = placement_.find(view.target.incarnation);
29236 if (row != placement_.end()
29237 && (row->second.family == PineOrderFamily::Entry
29238 || row->second.family == PineOrderFamily::Order))
29239 require_exit_rank_key(row->second.source_id);
29240}
29241
29242} // namespace pineforge::source
29243
29244namespace pineforge::source {
29245
29246std::uint64_t PineExecutionAdapter::checked_live_key(const SourceId& spelling) const {
29247 const auto key = key_for(spelling);
29248 const auto existing = live_by_source_key_.find(key);
29249 if (existing == live_by_source_key_.end()) return key;
29250 // Unrepresented keys retain the original hash-only insert/replace/read/erase.
29251 if (existing->second.identity.shape == detail::LiveSourceIdentity::Shape::Unverified)
29252 return key;
29253 const auto previous = placement_.find(existing->second.incarnation);
29254 if (previous == placement_.end()) {
29255 throw coded<std::logic_error>(RunFailureCode::engine_invariant, {},
29256 "Pine live source key has no placement");
29257 }
29258 if (!existing->second.identity.matches(spelling, previous->second.source_id,
29259 previous->second.from_entry)) {
29260 throw coded<std::logic_error>(RunFailureCode::engine_invariant,
29261 {{"reason", "order_key_hash_collision"}},
29262 "Pine live source key collision");
29263 }
29264 return key;
29265}
29266
29267detail::LiveSourceHandle PineExecutionAdapter::live_source_value(
29268 native_order::RequestHandle handle, const SourceId& spelling) const {
29269 const auto row = placement_.find(handle.incarnation);
29270 if (row == placement_.end()) {
29271 throw coded<std::logic_error>(RunFailureCode::engine_invariant, {},
29272 "Pine live source key has no placement");
29273 }
29274 const auto& snapshot = row->second;
29275 const detail::LiveSourceHandle value{handle, spelling, snapshot.source_id, snapshot.from_entry};
29276#ifdef PINEFORGE_TEST_SOURCE_KEY_MASK
29277 // Only the test adapter objects define this seam; the shipped object has
29278 // no missing-spelling throw path, runtime switch, diagnostic state or log.
29279 if (value.identity.shape == detail::LiveSourceIdentity::Shape::Unverified) {
29280 throw coded<std::logic_error>(RunFailureCode::engine_invariant, {},
29281 "Pine live source key spelling is not represented");
29282 }
29283#endif
29284 return value;
29285}
29286
29287} // namespace pineforge::source
29288
29289namespace pineforge::source {
29290
29291#ifndef PINEFORGE_FULL_CLOSE_EXIT_ORACLE
29292#define PINEFORGE_FULL_CLOSE_EXIT_ORACLE() false
29293#endif
29294#ifndef PINEFORGE_FULL_CLOSE_EXIT_CALL
29295#define PINEFORGE_FULL_CLOSE_EXIT_CALL() ((void)0)
29296#endif
29297
29298void PineExecutionAdapter::cancel_reversal_exit_owners(
29299 const std::vector<SourceId>& preserved,
29300 detail::ExitScratchRows<SourceId>& selection) {
29301 const auto exempt = [&](const SourceId& id) {
29302 return std::binary_search(preserved.begin(), preserved.end(), id);
29303 };
29304 // The public-run fixture executes the old selection on a separate run.
29305 // Shipped objects have no runtime switch or additional adapter state.
29307 cohorts_by_id_.for_each_sorted_id([&](const SourceId& id) {
29308 if (!exempt(id)) selection.push_back(id);
29309 });
29310 selection.ordered_text([&](const detail::ExitScratchText& id) {
29311 cancel_exit_orders_for_full_close(id);
29312 return true;
29313 });
29314 return;
29315 }
29316
29317 // Logical size is stored in the disk index; neither size nor these few
29318 // exemption lookups walk historical ids. Repeated exemptions count once.
29319 std::uint64_t count = cohorts_by_id_.size();
29320 const SourceId* previous = nullptr;
29321 for (const auto& id : preserved) {
29322 if (previous && *previous == id) continue;
29323 previous = &id;
29324 if (cohorts_by_id_.contains(id)) --count;
29325 }
29326 if (count == 0) return;
29327
29328 // Snapshot only current queues: cancellation erases from these stores.
29329 // Non-exit shadows, pending-bound rows and COOF-reachable legs are safe
29330 // supersets; the original canceler retains their exact policy decisions.
29331 std::vector<SourceId> owners;
29332 for (const auto& row : pending_bracket_legs_) owners.push_back(row.snapshot.from_entry);
29333 for (const auto& row : pending_coof_requests_) owners.push_back(row.snapshot.from_entry);
29334 for (const auto& row : source_shadow_pending_) owners.push_back(row.snapshot.from_entry);
29335 for (const auto& row : pending_relative_exits_) owners.push_back(row.from_entry);
29336 for (const auto& row : anchored_relative_legs_) owners.push_back(row.from_entry);
29337 for (const auto& handle : live_handles_) {
29338 const auto row = placement_.find(handle.incarnation);
29339 if (row == placement_.end()) continue;
29340 const auto family = row->second.family;
29341 if (family == PineOrderFamily::ExitLimit || family == PineOrderFamily::ExitStop
29342 || family == PineOrderFamily::ExitTrail) owners.push_back(row->second.from_entry);
29343 }
29344 std::sort(owners.begin(), owners.end());
29345 owners.erase(std::unique(owners.begin(), owners.end()), owners.end());
29346 owners.erase(std::remove_if(owners.begin(), owners.end(), [&](const SourceId& id) {
29347 return exempt(id) || !cohorts_by_id_.contains(id);
29348 }), owners.end());
29349 assert(owners.size() <= count);
29350
29351 // Every old text append allocated an ordinal/text pair before ANY cancel.
29352 // Reserve that entire prefix, including the candidates' pairs, once.
29353 // Returning the status through exit_scratch_edit preserves its exact
29354 // carried-I/O / rollback / ExitScratchError mapping on every failure.
29355 auto& context = selection.context();
29356 if (count > std::numeric_limits<std::uint64_t>::max() / 2)
29357 detail::exit_scratch_failure(detail::ScratchStatus::programming_error);
29358 detail::exit_scratch_edit(context, [&] {
29359 return context.store.advance_handles(2 * count);
29360 });
29361
29362 // Even a no-owner cancel erased empty families. Preserve that effect once,
29363 // in the original family iteration order, independent of historical ids.
29364 for (auto family = bracket_families_.begin(); family != bracket_families_.end();) {
29365 if (family->second.empty()) family = bracket_families_.erase(family);
29366 else ++family;
29367 }
29368
29369#ifndef NDEBUG
29370 // Independent skip oracle: recompute each actual old per-store predicate,
29371 // without executing cancels, copying an adapter or trusting the owner scan.
29372 const bool keep_pending_bound =
29373 detail::exit_binding_rule_switches().pending_bound_exit_survives_flat;
29374 cohorts_by_id_.for_each_sorted_id([&](const SourceId& id) {
29375 if (exempt(id) || std::binary_search(owners.begin(), owners.end(), id)) return;
29376 const auto matches = [&](const PlacementSnapshot& row) {
29377 const bool exit = row.family == PineOrderFamily::ExitLimit
29378 || row.family == PineOrderFamily::ExitStop
29379 || row.family == PineOrderFamily::ExitTrail;
29380 return exit && !(keep_pending_bound && row.pending_bound_exit)
29381 && row.from_entry == id;
29382 };
29383 assert(std::none_of(pending_bracket_legs_.begin(), pending_bracket_legs_.end(),
29384 [&](const PendingBracketLeg& row) { return matches(row.snapshot); }));
29385 assert(std::none_of(pending_coof_requests_.begin(), pending_coof_requests_.end(),
29386 [&](const PendingCoofRequest& row) { return matches(row.snapshot); }));
29387 assert(std::none_of(source_shadow_pending_.begin(), source_shadow_pending_.end(),
29388 [&](const SourceShadowPending& row) { return matches(row.snapshot); }));
29389 assert(std::none_of(pending_relative_exits_.begin(), pending_relative_exits_.end(),
29390 [&](const PendingRelativeExit& row) { return row.from_entry == id; }));
29391 if (!id.empty())
29392 assert(std::none_of(anchored_relative_legs_.begin(), anchored_relative_legs_.end(),
29393 [&](const AnchoredRelativeLeg& row) { return row.from_entry == id; }));
29394 for (const auto& handle : live_handles_) {
29395 const auto row = placement_.find(handle.incarnation);
29396 assert(row == placement_.end() || !matches(row->second)
29397 || coof_leg_reaches_exit(row->second));
29398 }
29399 assert(std::none_of(bracket_families_.begin(), bracket_families_.end(),
29400 [](const auto& family) { return family.second.empty(); }));
29401 });
29402#endif
29403
29404 for (const auto& id : owners) {
29406 cancel_exit_orders_for_full_close(id);
29407 }
29408 // A skipped cancel creates empty handles/removed scopes, but no records.
29409 // Their ordinals are not folded by hash_exit_scratch; preserve the final
29410 // counter too, so scopes created after this helper keep their old keys.
29411 // Real cancels cannot publish a hash: cancel / parent-terminal draining
29412 // only retire requests, without an Applied or source callback. Their own
29413 // transient rows have been erased before this counter is advanced.
29414 const auto skipped = count - static_cast<std::uint64_t>(owners.size());
29415 if (skipped > (std::numeric_limits<std::uint64_t>::max() - context.next_scope) / 2)
29416 detail::exit_scratch_failure(detail::ScratchStatus::programming_error);
29417 context.next_scope += 2 * skipped;
29418}
29419
29420// Keep live-family counters in step with live membership at this shared
29421// writer, so policy readers need no independent scans. Placement rows stay
29422// present while their handles are live (erase_retired_rows roots them).
29423void PineExecutionAdapter::remember_live(const native_order::RequestHandle& handle,
29424 PlacementSnapshot&& snapshot) {
29425 invalidate_retirement();
29426 const auto previous = placement_.find(handle.incarnation);
29427 const bool already_live = previous != placement_.end()
29428 && std::find(live_handles_.begin(), live_handles_.end(), handle) != live_handles_.end();
29429 if (already_live) count_live(previous->second, false);
29430 if (previous != placement_.end() || snapshot.projection_predecessor != 0)
29431 retired_row_scratch_.roster_pass_due = true;
29432 const auto inserted = placement_.try_emplace(handle.incarnation, std::move(snapshot));
29433 if (!inserted.second) placement_.replace(handle.incarnation, std::move(snapshot));
29434 if (!already_live) pending_write(live_handles_).push_back(handle);
29435 count_live(placement_.at(handle.incarnation), true);
29436 assert_live_counts();
29437}
29438
29439void PineExecutionAdapter::forget_live(const native_order::RequestHandle& handle) noexcept {
29440 const auto removed = std::remove(pending_write(live_handles_).begin(), live_handles_.end(), handle);
29441 if (removed != live_handles_.end()) {
29442 assert(std::distance(removed, live_handles_.end()) == 1);
29443 const auto row = placement_.find(handle.incarnation);
29444 assert(row != placement_.end());
29445 if (row != placement_.end()) count_live(row->second, false);
29446 pending_write(live_handles_).erase(removed, live_handles_.end());
29447 }
29448 assert_live_counts();
29449}
29450
29451void PineExecutionAdapter::requeue_live(const native_order::RequestHandle& handle) {
29452 // order_same_point_entries moves an existing row, never a new admission.
29453 assert(std::find(live_handles_.begin(), live_handles_.end(), handle) != live_handles_.end());
29454 forget_live(handle);
29455 pending_write(live_handles_).push_back(handle);
29456 count_live(placement_.at(handle.incarnation), true);
29457 assert_live_counts();
29458}
29459
29460void PineExecutionAdapter::clear_live() noexcept {
29461 pending_write(live_handles_).clear();
29462 for (auto& count : retired_row_scratch_.live_families) count = 0;
29463 retired_row_scratch_.origin_fronts_pinned = true;
29464}
29465
29466void PineExecutionAdapter::erase_placement(std::uint64_t incarnation) {
29467 assert(std::none_of(live_handles_.begin(), live_handles_.end(),
29468 [&](const auto& handle) { return handle.incarnation == incarnation; }));
29469 // A retained front might now be unpinned. Until a complete all-of proves
29470 // otherwise, run the original retirement algorithm on every call: queued
29471 // references, owners, opened lots and kernel membership can all change.
29472 retired_row_scratch_.origin_fronts_pinned = false;
29473 invalidate_retirement();
29474 if (placement_.erase(incarnation)) ++retirement_work_.rows_erased;
29475}
29476
29477void PineExecutionAdapter::count_live(const PlacementSnapshot& row, bool adding) noexcept {
29478 const auto change = [&](std::size_t slot) {
29479 auto& count = retired_row_scratch_.live_families[slot];
29480 if (adding) ++count;
29481 else { assert(count != 0); --count; }
29482 };
29483 change(static_cast<std::size_t>(row.family));
29484 if (row.family == PineOrderFamily::Order
29485 && !finite_positive(row.exit_levels.limit) && !finite_positive(row.exit_levels.stop))
29486 change(RetiredRowScratch::market_order_slot);
29487 // submit_or_replace initializes dynamic quantity only for !opening;
29488 // initialize_l4c_policy captures expansion only for an exit family.
29489 // A replacement may carry an immutable capture into any family. Include
29490 // it too: close_population never erases a capture; any later creation is
29491 // on an exit-family row, which already belongs to this policy population.
29492 if (!row.opening || row.family == PineOrderFamily::ExitLimit
29493 || row.family == PineOrderFamily::ExitStop || row.family == PineOrderFamily::ExitTrail
29494 || row.reservation_expansion.capture().has_value())
29495 change(RetiredRowScratch::exit_policy_slot);
29496}
29497
29498void PineExecutionAdapter::assert_live_counts() const noexcept {
29499#ifndef NDEBUG
29500 std::size_t actual[RetiredRowScratch::family_count + 2]{};
29501 for (const auto& handle : live_handles_) {
29502 const auto found = placement_.find(handle.incarnation);
29503 assert(found != placement_.end());
29504 if (found == placement_.end()) continue;
29505 const auto& row = found->second;
29506 ++actual[static_cast<std::size_t>(row.family)];
29507 if (row.family == PineOrderFamily::Order
29508 && !finite_positive(row.exit_levels.limit)
29509 && !finite_positive(row.exit_levels.stop))
29510 ++actual[RetiredRowScratch::market_order_slot];
29511 if (!row.opening || row.family == PineOrderFamily::ExitLimit
29512 || row.family == PineOrderFamily::ExitStop || row.family == PineOrderFamily::ExitTrail
29513 || row.reservation_expansion.capture().has_value())
29514 ++actual[RetiredRowScratch::exit_policy_slot];
29515 }
29516 for (std::size_t i = 0; i < RetiredRowScratch::family_count + 2; ++i)
29517 assert(actual[i] == retired_row_scratch_.live_families[i]);
29518#endif
29519}
29520
29521bool PineExecutionAdapter::has_live_family(PineOrderFamily family) const noexcept {
29522 assert_live_counts();
29523 return retired_row_scratch_.live_families[static_cast<std::size_t>(family)] != 0;
29524}
29525
29526bool PineExecutionAdapter::has_live_exit_policy() const noexcept {
29527 assert_live_counts();
29528 return retired_row_scratch_.live_families[RetiredRowScratch::exit_policy_slot] != 0;
29529}
29530
29531bool PineExecutionAdapter::has_live_trail_policy() const noexcept {
29532 assert_live_counts();
29533 const auto& counts = retired_row_scratch_.live_families;
29534 return counts[static_cast<std::size_t>(PineOrderFamily::Entry)] != 0
29535 || counts[static_cast<std::size_t>(PineOrderFamily::ExitStop)] != 0
29536 || counts[static_cast<std::size_t>(PineOrderFamily::ExitTrail)] != 0;
29537}
29538
29539// Original exact scans serve allocation failures and independent Debug checks.
29540int PineExecutionAdapter::scan_projected_pending_size(
29541 const PineExecutionAdapter& owner) noexcept {
29542 const int raw_count = owner.projected_raw_pending_size();
29543 int count = 0;
29544 for (int index = 0; index < raw_count; ++index) {
29545 const PlacementSnapshot* candidate = nullptr;
29546 native_order::RequestHandle handle;
29547 if (!owner.projected_raw_pending_at(index, candidate, handle) || !candidate) continue;
29548 bool duplicate = false;
29549 if (candidate->family == PineOrderFamily::ExitLimit
29550 || candidate->family == PineOrderFamily::ExitStop
29551 || candidate->family == PineOrderFamily::ExitTrail) {
29552 for (int prior = 0; prior < index; ++prior) {
29553 const PlacementSnapshot* earlier = nullptr;
29554 native_order::RequestHandle earlier_handle;
29555 if (owner.projected_raw_pending_at(prior, earlier, earlier_handle) && earlier
29556 && same_projected_order(*candidate, *earlier)) {
29557 duplicate = true;
29558 break;
29559 }
29560 }
29561 }
29562 if (!duplicate && count != std::numeric_limits<int>::max()) ++count;
29563 }
29564 return count;
29565}
29566
29567bool PineExecutionAdapter::scan_projected_pending_at(
29568 const PineExecutionAdapter& owner, int index, const PlacementSnapshot*& snapshot,
29569 native_order::RequestHandle& handle, int* raw_index) noexcept {
29570 snapshot = nullptr;
29571 handle = {};
29572 if (raw_index) *raw_index = -1;
29573 if (index < 0) return false;
29574 const int raw_count = owner.projected_raw_pending_size();
29575 int projected = 0;
29576 for (int raw = 0; raw < raw_count; ++raw) {
29577 const PlacementSnapshot* candidate = nullptr;
29578 native_order::RequestHandle candidate_handle;
29579 if (!owner.projected_raw_pending_at(raw, candidate, candidate_handle) || !candidate) continue;
29580 bool duplicate = false;
29581 if (candidate->family == PineOrderFamily::ExitLimit
29582 || candidate->family == PineOrderFamily::ExitStop
29583 || candidate->family == PineOrderFamily::ExitTrail) {
29584 for (int prior = 0; prior < raw; ++prior) {
29585 const PlacementSnapshot* earlier = nullptr;
29586 native_order::RequestHandle earlier_handle;
29587 if (owner.projected_raw_pending_at(prior, earlier, earlier_handle) && earlier
29588 && same_projected_order(*candidate, *earlier)) {
29589 duplicate = true;
29590 break;
29591 }
29592 }
29593 }
29594 if (duplicate) continue;
29595 if (projected++ == index) {
29596 snapshot = candidate;
29597 handle = candidate_handle;
29598 if (raw_index) *raw_index = raw;
29599 return true;
29600 }
29601 }
29602 return false;
29603}
29604
29605int PineExecutionAdapter::projected_pending_size() const noexcept {
29606 if (!ensure_projected_pending_index()) return scan_projected_pending_size(*this);
29607 const int count = static_cast<int>(projected_pending_indexes_.size());
29608#ifndef NDEBUG
29609 if (projected_raw_pending_size() <= 256)
29610 assert(count == scan_projected_pending_size(*this));
29611#endif
29612 return count;
29613}
29614
29615bool PineExecutionAdapter::projected_pending_at(
29616 int index, const PlacementSnapshot*& snapshot,
29617 native_order::RequestHandle& handle) const noexcept {
29618 snapshot = nullptr;
29619 handle = {};
29620 bool found = false;
29621 if (index >= 0) {
29622 if (!ensure_projected_pending_index())
29623 return scan_projected_pending_at(*this, index, snapshot, handle);
29624 if (static_cast<std::size_t>(index) < projected_pending_indexes_.size())
29625 found = projected_raw_pending_at(
29626 projected_pending_indexes_[static_cast<std::size_t>(index)], snapshot, handle);
29627 }
29628#ifndef NDEBUG
29629 if (projected_raw_pending_size() <= 256) {
29630 const PlacementSnapshot* expected_snapshot = nullptr;
29631 native_order::RequestHandle expected_handle;
29632 const bool expected_found = scan_projected_pending_at(
29633 *this, index, expected_snapshot, expected_handle);
29634 assert(found == expected_found);
29635 assert(snapshot == expected_snapshot);
29636 assert(handle == expected_handle);
29637 }
29638#endif
29639 return found;
29640}
29641
29642int PineExecutionAdapter::projected_pending_next(int index) const noexcept {
29643 if (!projected_pending_valid_) {
29644 const PlacementSnapshot* snapshot = nullptr;
29645 native_order::RequestHandle handle;
29646 int raw = -1;
29647 if (!scan_projected_pending_at(*this, index, snapshot, handle, &raw)) return -1;
29648 return projected_raw_pending_next(raw);
29649 }
29650 return projected_raw_pending_next(
29651 projected_pending_indexes_[static_cast<std::size_t>(index)]);
29652}
29653
29654int PineExecutionAdapter::projected_raw_pending_next(int raw) const noexcept {
29655 // Stay on the scan for the whole observer call, including sibling merges.
29656 // The next independent accessor retries the index build.
29657 if (raw < 0) return -1;
29658 if (!projected_pending_valid_)
29659 return raw < projected_raw_pending_size() - 1 ? raw + 1 : -1;
29660 return projected_pending_next_[static_cast<std::size_t>(raw)];
29661}
29662
29663int PineExecutionAdapter::projected_level_resolved(
29664 const PlacementSnapshot& snapshot) const noexcept {
29665 if (snapshot.from_entry.empty()) return 1;
29666 const auto cohort = cohorts_by_id_.find_resident(snapshot.from_entry);
29667 return cohort != cohorts_by_id_.end() && !cohort->second.opened.empty() ? 1 : 0;
29668}
29669
29670} // namespace pineforge::source
29671
29672namespace pineforge::source {
29673
29674// Retained identities and the current tail's dependencies use an independent
29675// store. Neither its handles nor its physical layout enter the broker hash.
29676// The comparison index has logarithmic population cost and fixed resident
29677// workspace; it is not a constant-time radix directory.
29678struct PlacementTable::SequenceIndex {
29681 using Buffer = ::pineforge::detail::ScratchBuffer;
29682 using Bytes = ::pineforge::detail::ScratchBytes;
29684 static constexpr auto absent = std::numeric_limits<std::uint64_t>::max();
29685
29686 // A length-delimited exact identity, followed by big-endian sequence and
29687 // incarnation words. Domains 0 and 1 index retained singles and pairs.
29688 // Domain 3 watches query answers. Domain 2 orders fallback dependencies
29689 // by their answer before their from-entry bytes, for bounded range checks.
29690 class Key final : public ::pineforge::detail::ScratchByteSource {
29691 public:
29692 Key(unsigned domain, std::string_view id,
29693 std::optional<std::string_view> from = std::nullopt,
29694 std::uint64_t sequence = 0, std::uint64_t incarnation = 0)
29695 : domain_(domain), id_(id), from_(from),
29696 sequence_(sequence), incarnation_(incarnation) {
29697 if (id.size() > absent - 33
29698 || (from && from->size() > absent - 33 - id.size()))
29699 throw std::length_error("retained identity key is too long");
29700 }
29701 std::uint64_t prefix_size() const noexcept {
29702 return 9 + id_.size() + (from_ ? 8 + from_->size() : 0);
29703 }
29704 std::uint64_t id_prefix_size() const noexcept { return 9 + id_.size(); }
29705 std::uint64_t size() const noexcept override { return prefix_size() + 16; }
29706 Status read(std::uint64_t offset, Buffer destination) const noexcept override {
29707 if (offset > size() || destination.size > size() - offset
29708 || (destination.size && !destination.data)) return Status::programming_error;
29709 for (std::size_t index = 0; index < destination.size; ++index)
29710 destination.data[index] = static_cast<std::byte>(byte(offset + index));
29711 return Status::ok;
29712 }
29713 private:
29714 static unsigned char word(std::uint64_t value, std::uint64_t at) noexcept {
29715 return static_cast<unsigned char>(value >> (8 * (7 - at)));
29716 }
29717 unsigned char byte(std::uint64_t at) const noexcept {
29718 if (at == 0) return static_cast<unsigned char>(domain_);
29719 --at;
29720 if (at < 8) return word(id_.size(), at);
29721 at -= 8;
29722 if (at < id_.size()) return static_cast<unsigned char>(id_[at]);
29723 at -= id_.size();
29724 if (domain_ == 2) {
29725 if (at < 8) return word(sequence_, at);
29726 at -= 8;
29727 }
29728 if (from_) {
29729 if (at < 8) return word(from_->size(), at);
29730 at -= 8;
29731 if (at < from_->size()) return static_cast<unsigned char>((*from_)[at]);
29732 at -= from_->size();
29733 }
29734 if (domain_ == 2) return word(incarnation_, at);
29735 return at < 8 ? word(sequence_, at) : word(incarnation_, at - 8);
29736 }
29737 unsigned domain_;
29738 std::string_view id_;
29739 std::optional<std::string_view> from_;
29740 std::uint64_t sequence_, incarnation_;
29741 };
29742
29743 static Status compare(void* context, const ::pineforge::detail::ScratchByteSource& a,
29744 const ::pineforge::detail::ScratchByteSource& b, int& order) noexcept {
29745 ++static_cast<SequenceIndex*>(context)->operations;
29746 std::array<std::byte, 128> left{}, right{};
29747 const auto common = std::min(a.size(), b.size());
29748 for (std::uint64_t offset = 0; offset < common;) {
29749 const auto count = static_cast<std::size_t>(
29750 std::min<std::uint64_t>(left.size(), common - offset));
29751 auto status = a.read(offset, {left.data(), count});
29752 if (status != Status::ok) return status;
29753 status = b.read(offset, {right.data(), count});
29754 if (status != Status::ok) return status;
29755 const int compared = std::memcmp(left.data(), right.data(), count);
29756 if (compared != 0) { order = compared; return Status::ok; }
29757 offset += count;
29758 }
29759 order = a.size() < b.size() ? -1 : b.size() < a.size() ? 1 : 0;
29760 return Status::ok;
29761 }
29762
29763 static std::unique_ptr<::pineforge::detail::ScratchBacking> make_backing(
29764 CacheCapacity capacity) {
29765 // Derived identities are not command scopes. The factory snapshots
29766 // the observer, so isolate their accounting without changing the
29767 // backing selection, shared byte budget or failure propagation.
29768 struct RestoreObserver {
29769 ::pineforge::detail::ScratchIoObserver previous =
29770 detail::set_command_scratch_io_observer({});
29771 ~RestoreObserver() { detail::set_command_scratch_io_observer(previous); }
29772 } restore;
29773 // One allowance sizes the shared slab in both prefix and read phases.
29774 auto backing = ::pineforge::detail::make_command_scratch_backing(512, capacity);
29775 const auto observer = detail::sequence_cache_observer;
29776 if (observer.created) observer.created(observer.context, *backing);
29777 return backing;
29778 }
29779
29780 mutable std::uint64_t operations = 0;
29781 std::unique_ptr<::pineforge::detail::ScratchBacking> backing;
29782 ::pineforge::detail::CommandScratchStore store{
29783 {2, 512, 1, 1, {0x6578697473657131ULL, this, compare},
29785 bool certified = false;
29786
29787 explicit SequenceIndex(CacheCapacity capacity)
29788 : backing(make_backing(capacity)) { require(store.status()); }
29789 void require(Status status) const {
29790 if (status == Status::ok) return;
29791 if (status == Status::io_error)
29793 throw coded<std::logic_error>(
29794 status == Status::io_error ? RunFailureCode::scratch_io_failed
29795 : RunFailureCode::engine_invariant,
29796 {}, "retained sequence index operation failed");
29797 }
29798 struct Transaction {
29799 SequenceIndex& index;
29800 explicit Transaction(SequenceIndex& value) : index(value) {
29801 ++index.operations;
29802 index.require(index.store.begin());
29803 }
29804 ~Transaction() {
29805 if (index.store.transaction_active()) (void)index.store.rollback();
29806 }
29807 void commit() {
29808 ++index.operations;
29809 index.require(index.store.commit(::pineforge::detail::ScratchDurability::buffered));
29810 }
29811 };
29812 Handle seek(const Key& key) const {
29813 ++operations;
29815 const auto status = store.lower_bound(key, handle);
29816 if (status != Status::end) require(status);
29817 return handle;
29818 }
29819 bool matches(Handle handle, const Key& key, std::uint64_t bytes) const {
29820 if (handle == ::pineforge::detail::scratch_no_handle) return false;
29821 ::pineforge::detail::ScratchRecordInfo info{};
29822 ++operations;
29823 require(store.point(handle, info));
29824 if (info.key_bytes < bytes || (bytes == key.size() && info.key_bytes != bytes))
29825 return false;
29826 std::array<std::byte, 128> left{}, right{};
29827 for (std::uint64_t offset = 0; offset < bytes;) {
29828 const auto count = static_cast<std::size_t>(
29829 std::min<std::uint64_t>(left.size(), bytes - offset));
29830 ++operations;
29831 require(store.read(handle, Field::key, offset, {left.data(), count}));
29832 require(key.read(offset, {right.data(), count}));
29833 if (std::memcmp(left.data(), right.data(), count) != 0) return false;
29834 offset += count;
29835 }
29836 return true;
29837 }
29838 Handle find(const Key& key) const {
29839 const auto handle = seek(key);
29840 return matches(handle, key, key.size()) ? handle : ::pineforge::detail::scratch_no_handle;
29841 }
29842 std::uint64_t minimum(unsigned domain, std::string_view id,
29843 std::optional<std::string_view> from = std::nullopt) const {
29844 const Key probe(domain, id, from);
29845 const auto handle = seek(probe);
29846 if (!matches(handle, probe, probe.prefix_size())) return absent;
29847 std::array<std::byte, 8> bytes{};
29848 ++operations;
29849 require(store.read(handle, Field::key, probe.prefix_size(), {bytes.data(), bytes.size()}));
29850 std::uint64_t result = 0;
29851 for (const auto byte : bytes)
29852 result = (result << 8) | std::to_integer<unsigned char>(byte);
29853 return result;
29854 }
29855 void put(const Key& key, Bytes bytes = {}) {
29856 ::pineforge::detail::ScratchSpanSource value(bytes);
29858 ++operations;
29859 require(store.put(key, value, handle));
29860 }
29861 void erase(const Key& key) {
29862 const auto handle = find(key);
29864 throw coded<std::logic_error>(RunFailureCode::engine_invariant, {},
29865 "retained sequence identity is absent");
29866 ++operations;
29867 require(store.erase(handle));
29868 }
29869 void insert(std::uint64_t incarnation, const PlacementSnapshot& row) {
29870 put(Key(0, row.source_id, std::nullopt, row.command_sequence, incarnation));
29871 put(Key(1, row.source_id, row.from_entry, row.command_sequence, incarnation));
29872 }
29873 void remove(std::uint64_t incarnation, const PlacementSnapshot& row) {
29874 erase(Key(0, row.source_id, std::nullopt, row.command_sequence, incarnation));
29875 erase(Key(1, row.source_id, row.from_entry, row.command_sequence, incarnation));
29876 }
29877 void clear_dependencies() {
29878 certified = false;
29879 Transaction transaction(*this);
29880 const Key first(2, {});
29881 for (auto handle = seek(first); handle != ::pineforge::detail::scratch_no_handle;
29882 handle = seek(first)) {
29883 ++operations;
29884 require(store.erase(handle));
29885 }
29886 transaction.commit();
29887 }
29888 void watch(unsigned domain, std::string_view id,
29889 std::optional<std::string_view> from, std::uint64_t minimum) {
29890 const Key key(domain, id, from, domain == 2 ? minimum : 0);
29891 if (find(key) != ::pineforge::detail::scratch_no_handle) return;
29892 Transaction transaction(*this);
29893 put(key, {reinterpret_cast<const std::byte*>(&minimum), sizeof minimum});
29894 transaction.commit();
29895 }
29896 template<class Erased>
29897 std::uint64_t combined_minimum(unsigned domain, std::string_view id,
29898 std::optional<std::string_view> from,
29899 const Erased& erased) const {
29900 auto current = minimum(domain, id, from);
29901 const auto historical = from ? erased.lookup(id, *from) : erased.lookup(id);
29902 return historical ? std::min(current, *historical) : current;
29903 }
29904 template<class Erased>
29905 void check_pair(std::string_view id, std::string_view from, const Erased& erased) {
29906 if (!certified) return;
29907 const auto handle = find(Key(3, id, from));
29908 if (handle == ::pineforge::detail::scratch_no_handle) return;
29909 std::uint64_t previous = absent;
29910 ++operations;
29911 require(store.read(handle, Field::value, 0,
29912 {reinterpret_cast<std::byte*>(&previous), sizeof previous}));
29913 const auto paired = combined_minimum(1, id, from, erased);
29914 const auto current = paired != absent
29915 ? paired : combined_minimum(0, id, std::nullopt, erased);
29916 if (previous != current) { certified = false; return; }
29917 // A pair can begin/end using the fallback while its final answer
29918 // stays identical. Maintain that dependency without dirtying the tail.
29919 const Key fallback(2, id, from, previous);
29920 const auto dependency = find(fallback);
29921 if (paired == absent && dependency == ::pineforge::detail::scratch_no_handle)
29922 put(fallback);
29923 else if (paired != absent && dependency != ::pineforge::detail::scratch_no_handle) {
29924 ++operations;
29925 require(store.erase(dependency));
29926 }
29927 }
29928 template<class Erased>
29929 void check_fallbacks(std::string_view id, const Erased& erased) {
29930 if (!certified) return;
29931 const Key first(2, id);
29932 const auto handle = seek(first);
29933 if (!matches(handle, first, first.id_prefix_size())) return;
29934 const auto current = combined_minimum(0, id, std::nullopt, erased);
29935 std::array<std::byte, 8> bytes{};
29936 ++operations;
29937 require(store.read(handle, Field::key, first.id_prefix_size(), {bytes.data(), bytes.size()}));
29938 std::uint64_t minimum = 0;
29939 for (const auto byte : bytes)
29940 minimum = (minimum << 8) | std::to_integer<unsigned char>(byte);
29941 if (minimum != current) { certified = false; return; }
29942 if (current == absent) return;
29943 // Every remaining fallback must have this exact answer. One lower
29944 // bound rules out the whole greater-answer range, without a tail walk.
29945 const Key greater(2, id, std::nullopt, current + 1);
29946 if (matches(seek(greater), greater, greater.id_prefix_size())) certified = false;
29947 }
29948};
29949
29950PlacementTable::SequenceIndex& PlacementTable::sequence_index() const {
29951 if (!sequence_index_) {
29952 auto next = std::make_shared<SequenceIndex>(sequence_cache_capacity_);
29953 SequenceIndex::Transaction transaction(*next);
29954 // A deferred first insertion leaves at most one row to build.
29955 // Failed derived writes or a backing rebind can need a recovery walk.
29956 visit_rows([&](std::uint64_t incarnation, const PlacementSnapshot& row) {
29957 ++sequence_rows_;
29958 next->insert(incarnation, row);
29959 return false;
29960 });
29961 transaction.commit();
29962 sequence_index_ = std::move(next);
29963 }
29964 return *sequence_index_;
29965}
29966
29967void PlacementTable::prepare_place(std::uint64_t incarnation) {
29968 // All allocation precedes the backed update. place() then only moves an
29969 // owning pointer into reserved storage, so it cannot fail after commit.
29970 const auto reserve = [](auto& rows, std::size_t required) {
29971 if (required <= rows.capacity()) return;
29972 const auto grown = rows.capacity()
29973 + std::min(rows.capacity(), rows.max_size() - rows.capacity());
29974 rows.reserve(std::max(required, grown));
29975 };
29976 if (old_only_) {
29977 reserve(old_, old_.size() + 1U);
29978 return;
29979 }
29980 if (window_rows_ == 0 && (old_.empty() || incarnation > old_.back().first)) {
29981 reserve(window_, 1);
29982 } else if (incarnation < base_) {
29983 reserve(old_, old_.size() + 1);
29984 } else {
29985 const auto index = head_ + static_cast<std::size_t>(incarnation - base_);
29986 constexpr std::size_t kDenseFloor = 64;
29987 const auto span = std::max(window_.size() - head_,
29988 static_cast<std::size_t>(incarnation - base_) + 1U);
29989 const bool dense = span <= kDenseFloor || span <= 4U * window_rows_;
29990 if (window_rows_ != 0 && !dense) {
29991 reserve(old_, old_.size() + window_rows_ + 1U);
29992 for (std::size_t slot = head_; slot < window_.size(); ++slot) {
29993 if (!window_[slot]) continue;
29994 old_.emplace_back(
29995 base_ + static_cast<std::uint64_t>(slot - head_),
29996 std::move(window_[slot]));
29997 ++placement_work_;
29998 }
29999 window_.clear();
30000 head_ = 0;
30001 window_rows_ = 0;
30002 base_ = std::numeric_limits<std::uint64_t>::max();
30003 old_only_ = true;
30004 return;
30005 }
30006 if (index >= window_.size()) reserve(window_, index + 1);
30007 }
30008}
30009
30010void PlacementTable::change_sequences(std::uint64_t incarnation,
30011 const PlacementSnapshot* before,
30012 const PlacementSnapshot* after) {
30013 static_assert(std::is_nothrow_move_assignable_v<PlacementSnapshot>,
30014 "placement publication must not fail after its identity update");
30015 // The first insertion has no index or certified answer to maintain.
30016 // Avoid claiming an arena region before the authoritative cohort/rank
30017 // stores admit it. The next mutation or query builds from retained rows.
30018 if (!before && size_ == 0 && !sequence_index_) return;
30019 if (before && after && before->source_id == after->source_id
30020 && before->from_entry == after->from_entry
30021 && before->command_sequence == after->command_sequence) return;
30022 try {
30023 auto& index = sequence_index();
30024 SequenceIndex::Transaction transaction(index);
30025 if (before) index.remove(incarnation, *before);
30026 if (after) index.insert(incarnation, *after);
30027 // Erasure transfers the identical minima to erased_minima_; its
30028 // combined query answers therefore do not change at all.
30029 if (after) {
30030 if (before) index.check_pair(before->source_id, before->from_entry, erased_minima_);
30031 index.check_pair(after->source_id, after->from_entry, erased_minima_);
30032 if (before) index.check_fallbacks(before->source_id, erased_minima_);
30033 index.check_fallbacks(after->source_id, erased_minima_);
30034 }
30035 transaction.commit();
30036 } catch (...) {
30037 // Placements are still unchanged. Discard all possibly stale derived
30038 // handles; the next use reconstructs from the authoritative rows.
30039 sequence_index_.reset();
30040 throw;
30041 }
30042}
30043
30044std::array<std::uint64_t, 2> PlacementTable::retained_sequences(
30045 const SourceId& id, const SourceId& from) const {
30046 constexpr auto absent = SequenceIndex::absent;
30047 if (size_ == 0 && !sequence_index_) return {absent, absent};
30048 const auto& index = sequence_index();
30049 return {index.minimum(0, id), index.minimum(1, id, from)};
30050}
30051
30052std::array<std::uint64_t, 5> PlacementTable::sequence_work() const noexcept {
30053 if (!sequence_index_) return {sequence_queries_, sequence_rows_, 0, 0, 0};
30054 const auto stats = sequence_index_->store.stats();
30055 return {sequence_queries_, sequence_rows_, sequence_index_->operations,
30056 stats.page_reads + stats.page_writes + stats.syncs, stats.resident_bytes};
30057}
30058
30059bool PlacementTable::exit_sequences_certified() const noexcept {
30060 return sequence_index_ && sequence_index_->certified;
30061}
30062
30063void PlacementTable::begin_exit_sequence_certificate() {
30064 sequence_index().clear_dependencies();
30065}
30066
30067void PlacementTable::watch_exit_sequence(const SourceId& id, const SourceId& from) {
30068 auto& index = sequence_index();
30069 const auto paired = index.combined_minimum(1, id, from, erased_minima_);
30070 const auto answer = paired != SequenceIndex::absent
30071 ? paired : index.combined_minimum(0, id, std::nullopt, erased_minima_);
30072 index.watch(3, id, from, answer);
30073 // A finite paired minimum masks the any-entry fallback completely.
30074 if (paired != SequenceIndex::absent) return;
30075 index.watch(2, id, from, answer);
30076}
30077
30078void PlacementTable::certify_exit_sequences() noexcept {
30079 if (sequence_index_) sequence_index_->certified = true;
30080}
30081
30083 auto result = exit_group_work_;
30084 const auto sequence = placement_.sequence_work();
30085 result.sequence_queries = sequence[0];
30086 result.sequence_rows = sequence[1];
30087 result.sequence_index_operations = sequence[2];
30088 result.sequence_index_pages = sequence[3];
30089 result.sequence_index_resident_bytes = sequence[4];
30090 result.retained_placements = placement_.size();
30091 return result;
30092}
30093
30094bool PineExecutionAdapter::exit_group_certified(const TradeLedger& trades) const noexcept {
30095 const auto& certificate = exit_group_certificate_;
30096 return certificate.rows_valid && certificate.ledger == &trades
30097 && certificate.base == trades.base() && certificate.end == trades.size()
30098 && placement_.exit_sequences_certified();
30099}
30100
30101void PineExecutionAdapter::invalidate_exit_group(std::size_t first, std::size_t count) noexcept {
30102 auto& certificate = exit_group_certificate_;
30103 // The preceding row is a dependency too: a key rewrite can merge it
30104 // into the formerly certified group without changing the ledger size.
30105 const auto boundary = certificate.start == 0 ? 0 : certificate.start - 1;
30106 if (count && first < certificate.end
30107 && (first >= boundary || count > boundary - first))
30108 certificate.rows_valid = false;
30109}
30110
30111void PineExecutionAdapter::certify_exit_group(const TradeLedger& trades, std::size_t start, bool ordered) {
30112 exit_group_certificate_.rows_valid = false;
30113 placement_.begin_exit_sequence_certificate();
30114 for (std::size_t index = start; index < trades.size(); ++index) {
30115 ++exit_group_work_.tail_rows;
30116 const Trade& row = trades[index];
30117 placement_.watch_exit_sequence(row.exit_id, row.entry_id);
30118 }
30119 placement_.certify_exit_sequences();
30120 exit_group_certificate_ = {&trades, trades.base(), start, trades.size(), ordered};
30121}
30122
30123void PineExecutionAdapter::finish_exit_group_sort(
30124 ::pineforge::detail::CommandScratchStore& scratch,
30125 const ::pineforge::detail::ScratchStoreStats& before) noexcept {
30126 const auto after = scratch.stats();
30127 exit_group_work_.scratch_operations += (after.page_reads - before.page_reads)
30128 + (after.page_writes - before.page_writes) + (after.syncs - before.syncs);
30129 // The permutation changes positions, preserving the watched key set.
30130 // Publish only after all replacements and scratch rollback succeeded.
30131 exit_group_certificate_.rows_valid = true;
30132}
30133
30134} // namespace pineforge::source
30135
30136namespace pineforge::source {
30137namespace {
30138template<class T>
30139bool retirement_capacity_excessive(const std::vector<T>& values) {
30140 // Express size * 4 < capacity without overflowing the size type.
30141 return values.capacity() != 0 && values.size() <= (values.capacity() - 1) / 4;
30142}
30143
30144template<class T>
30145void compact_retirement_set(std::vector<T>& values) {
30146 if (retirement_capacity_excessive(values)) std::vector<T>(values).swap(values);
30147}
30148
30149template<class T>
30150void canonical_retirement_set(std::vector<T>& values, bool compact) {
30151 // Refill includes duplicate references. Keep room for them after the
30152 // distinct set is formed, so an unchanged refill never has to regrow.
30153 if (compact) compact_retirement_set(values);
30154 std::sort(values.begin(), values.end());
30155 values.erase(std::unique(values.begin(), values.end()), values.end());
30156}
30157} // namespace
30158
30159bool PineExecutionAdapter::capture_retirement_inputs(RetirementInputs& inputs) const {
30160 inputs.roots.clear();
30161 inputs.askable.clear();
30162 inputs.groups.clear();
30163 inputs.bar_numbers.clear();
30164 inputs.marks.clear();
30165 const auto root = [&](std::uint64_t value) {
30166 if (value != 0) inputs.roots.push_back(value);
30167 };
30168 const auto snapshot = [&](const PlacementSnapshot& value) {
30169 root(value.bracket_origin.incarnation);
30170 root(value.paired_reversal_parent.incarnation);
30171 root(value.preserved_by_close_all.incarnation);
30172 root(value.projection_predecessor);
30173 root(value.legs.target().incarnation);
30174 root(value.reservation_growth_owner_incarnation);
30175 if (value.sequential_group != 0) inputs.groups.push_back(value.sequential_group);
30176 if (value.projection_created_bar_pinned)
30177 inputs.bar_numbers.push_back(value.projection_created_bar);
30178 };
30179 const auto request = [&](const native_order::Request& value) {
30180 if (const auto* wait = std::get_if<native_order::WaitForApplied>(&value.owner)) {
30181 root(wait->parent.incarnation);
30182 } else if (const auto* opening = std::get_if<native_order::BindOpening>(&value.owner)) {
30183 root(opening->opening.incarnation);
30184 } else if (const auto* openings = std::get_if<native_order::BindOpenings>(&value.owner)) {
30185 for (const auto& opening : openings->openings) root(opening.incarnation);
30186 }
30187 };
30188 for (const auto& handle : live_handles_) {
30189 root(handle.incarnation);
30190 inputs.askable.push_back(handle.incarnation);
30191 const auto row = placement_.find(handle.incarnation);
30192 if (row == placement_.end()) continue;
30193 if (row->second.sequential_group != 0)
30194 inputs.groups.push_back(row->second.sequential_group);
30195 if (row->second.family == PineOrderFamily::Entry)
30196 inputs.bar_numbers.push_back(row->second.projection_created_bar);
30197 }
30198 for (const auto& cohort : cohorts_by_id_.held_rows()) {
30199 for (const auto& opening : cohort.second.opened) {
30200 root(opening.incarnation);
30201 inputs.askable.push_back(opening.incarnation);
30202 }
30203 }
30204 if (auto* pine = pine_view_of(host_))
30205 for (const auto& lot : pine->pyramid_entries_) root(lot.entry_incarnation);
30206 for (const auto& handle : first_open_newborns_) root(handle.incarnation);
30207 for (const auto& entry : live_by_source_key_) root(entry.second.incarnation);
30208 for (const auto& leg : pending_bracket_legs_) { snapshot(leg.snapshot); request(leg.request); }
30209 for (const auto& entry : pending_entries_) { snapshot(entry.snapshot); request(entry.request); }
30210 for (const auto& order : delayed_market_orders_) { snapshot(order.snapshot); request(order.request); }
30211 for (const auto& sell : deferred_open_marketable_sells_) snapshot(sell.snapshot);
30212 for (const auto& value : throttled_reopen_rearm_) snapshot(value);
30213 for (const auto& command : pending_same_bar_commands_) { snapshot(command.snapshot); request(command.request); }
30214 for (const auto& shadow : source_shadow_pending_) snapshot(shadow.snapshot);
30215 for (const auto& pending : pending_coof_requests_) { snapshot(pending.snapshot); request(pending.request); }
30216 for (const auto& revival : pending_margin_revivals_) snapshot(revival.snapshot);
30217 for (const auto& leg : anchored_relative_legs_) {
30218 root(leg.handle.incarnation); root(leg.parent.incarnation); request(leg.request);
30219 }
30220 root(materializing_parent_.incarnation);
30221 for (const auto* plan : {&short_seed_, &pending_short_seed_.plan}) {
30222 root(plan->long_entry.incarnation);
30223 root(plan->materialize_long.incarnation);
30224 root(plan->final_short.incarnation);
30225 }
30226 root(short_seed_long_candidate_.incarnation);
30227 for (const auto mark : market_pyramid_adds_) inputs.marks.push_back(mark);
30228 if (auto* consumer = bound_consumer()) {
30229 const auto& native = as_native_consumer(*consumer);
30230 for (const auto& working : native.request_core().live())
30231 root(working.definition->handle.incarnation);
30232 assert(native.event_window_start() <= receipt_cursor_ + 1);
30233 native.visit_commands_after(receipt_cursor_, [&](const native_order::CommandEvent& command) {
30234 std::visit([&](const auto& event) {
30235 using Event = std::decay_t<decltype(event)>;
30236 if constexpr (std::is_same_v<Event, native_order::RejectedEvent>
30237 || std::is_same_v<Event, native_order::NativeRiskEvent>) {
30238 // These events name no request.
30239 } else if constexpr (std::is_same_v<Event, native_order::ReplacedEvent>) {
30240 root(event.predecessor().incarnation); root(event.successor().incarnation);
30241 } else if constexpr (std::is_same_v<Event, native_order::ReplaceRejectedEvent>) {
30242 root(event.target().incarnation);
30243 } else if constexpr (std::is_same_v<Event, native_order::NotWorkingEvent>
30244 || std::is_same_v<Event, native_order::InvalidHandleEvent>) {
30245 root(event.target.incarnation);
30246 } else {
30247 if (event.definition) root(event.definition->handle.incarnation);
30248 if constexpr (std::is_same_v<Event, native_order::ReservationReducedEvent>
30249 || std::is_same_v<Event, native_order::DeferredGroupAdjustmentEvent>)
30250 root(event.recipient.incarnation);
30251 }
30252 }, command);
30253 return false;
30254 });
30255 }
30256 // Multiplicity can fall while the distinct sets stay equal. Report that
30257 // capacity debt before canonicalization so it can invalidate the gate.
30258 const bool excess_capacity = retirement_capacity_excessive(inputs.roots)
30259 || retirement_capacity_excessive(inputs.askable)
30260 || retirement_capacity_excessive(inputs.groups)
30261 || retirement_capacity_excessive(inputs.bar_numbers)
30262 || retirement_capacity_excessive(inputs.marks);
30263 const bool compact = !retirement_gate_.certified;
30264 canonical_retirement_set(inputs.roots, compact);
30265 canonical_retirement_set(inputs.askable, compact);
30266 canonical_retirement_set(inputs.groups, compact);
30267 canonical_retirement_set(inputs.bar_numbers, compact);
30268 canonical_retirement_set(inputs.marks, compact);
30269 return excess_capacity;
30270}
30271
30272void PineExecutionAdapter::observe_retirement_expiry(
30273 const PlacementSnapshot& value, std::int64_t interval) noexcept {
30274 auto& gate = retirement_gate_;
30275 const bool close_age = value.family == PineOrderFamily::Close && value.immediately;
30276 const bool stop_age = value.opening && value.family == PineOrderFamily::Entry
30277 && value.projection_position_side == static_cast<std::int32_t>(PositionSide::FLAT)
30278 && finite_positive(value.exit_levels.stop) && !finite_positive(value.exit_levels.limit);
30279 if ((close_age || stop_age) && value.projection_created_bar >= interval)
30280 gate.interval_expiry = std::min(gate.interval_expiry,
30281 static_cast<std::int64_t>(value.projection_created_bar) + 1);
30282 if ((value.family == PineOrderFamily::Entry || value.family == PineOrderFamily::Order)
30283 && value.placement_script_open_ms >= last_broker_open_ms_)
30284 gate.broker_expiry = std::min(gate.broker_expiry, value.placement_script_open_ms);
30285}
30286
30287void PineExecutionAdapter::finish_retirement_phases() {
30288 const std::size_t base = detail::run_consumer(require_host()).closed_trade_prefix().base;
30289 if (exit_phase_final_ < base)
30290 throw coded<std::logic_error>(RunFailureCode::engine_invariant, {},
30291 "exit-phase fold precedes the sealed prefix");
30292 for (; exit_phase_final_ < base + trade_exit_phase_.size(); ++exit_phase_final_)
30293 exit_phase_digest_ = fold_exit_phase(exit_phase_digest_,
30294 trade_exit_phase_.at(exit_phase_final_ - base));
30295 detail::run_consumer(require_host()).finish_exit_phase_restart();
30296}
30297
30298void PineExecutionAdapter::erase_retired_rows(const NativeDecisionContext& context) {
30299 auto& gate = retirement_gate_;
30300 if (detail::retain_retired_rows() || !bound_consumer()
30301 || placement_.size() <= live_handles_.size()) {
30302 gate = {};
30303 sweep_retired_rows(context, retirement_work_);
30304 return;
30305 }
30306 const bool excess_capacity = capture_retirement_inputs(gate.current);
30307 const auto interval = static_cast<std::int64_t>(context.coordinate.interval_index);
30308 const bool unchanged = gate.certified && gate.cycle == current_position_cycle_
30309 && interval >= gate.interval && interval < gate.interval_expiry
30310 && last_broker_open_ms_ >= gate.broker_open && last_broker_open_ms_ <= gate.broker_expiry
30311 && gate.current == gate.saved;
30312 if (unchanged && !excess_capacity) {
30313#ifndef NDEBUG
30314 // Use the original reader on these very inputs, including its phase
30315 // publication. Only retirement state must remain unchanged.
30316 const auto count = placement_.size();
30317 const auto erased = placement_.erased_rows();
30318 const auto digest = placement_.erased_digest();
30319 const auto before = retirement_validation_state();
30320 RetirementWork validation;
30321 sweep_retired_rows(context, validation);
30322 assert(placement_.size() == count && placement_.erased_rows() == erased
30323 && placement_.erased_digest() == digest
30324 && "unchanged retirement sweep erased a placement");
30325 assert(retired_row_scratch_.doomed.empty()
30326 && "unchanged retirement sweep found an erasable placement");
30327 assert(retirement_validation_state() == before
30328 && "unchanged retirement sweep changed placement, pin or cohort state");
30329 retirement_work_.validation_sweeps += validation.placement_sweeps;
30330 retirement_work_.validation_row_visits += validation.row_visits;
30331#else
30332 finish_retirement_phases();
30333#endif
30334 return;
30335 }
30336 gate.certified = false;
30337 std::swap(gate.saved, gate.current);
30338 // Saved inputs are only compared, so only their distinct values need
30339 // space. The refill buffer is compacted at its raw size during capture.
30340 compact_retirement_set(gate.saved.roots);
30341 compact_retirement_set(gate.saved.askable);
30342 compact_retirement_set(gate.saved.groups);
30343 compact_retirement_set(gate.saved.bar_numbers);
30344 compact_retirement_set(gate.saved.marks);
30345 gate.interval_expiry = gate.broker_expiry = std::numeric_limits<std::int64_t>::max();
30346 const auto erased = placement_.erased_rows();
30347 sweep_retired_rows(context, retirement_work_);
30348 // Refresh both buffers even after a removal, releasing obsolete capacity
30349 // while uncertified and warming the next refill before certification.
30350 capture_retirement_inputs(gate.current);
30351 // A removal can expose a new representative. Do not certify its result
30352 // until a later original sweep has proved there is no more work.
30353 if (placement_.erased_rows() != erased || !retired_row_scratch_.doomed.empty()) return;
30354 if (!(gate.current == gate.saved)) return;
30355 gate.cycle = current_position_cycle_;
30356 gate.interval = interval;
30357 gate.broker_open = last_broker_open_ms_;
30358 gate.certified = true;
30359}
30360
30361#ifndef NDEBUG
30362std::array<std::uint64_t, 3> PineExecutionAdapter::retirement_validation_state() const {
30363 // Bounded workspace: stream the same objects before and after, including
30364 // object bytes, owned names and lifecycle payloads. No historical copy.
30365 BrokerStateHashSink placements;
30366 placements.u(placement_.size());
30367 placements.u(placement_.high_water());
30368 placements.u(placement_.erased_rows());
30369 placements.u(placement_.erased_digest());
30370 placement_.visit_rows([&](std::uint64_t incarnation, const PlacementSnapshot& row) {
30371 placements.u(incarnation);
30372 placements.bytes(&row, sizeof(row));
30373 placements.s(row.source_id); placements.s(row.from_entry);
30374 placements.s(row.comment); placements.s(row.oca_name); placements.s(row.close_first_id);
30375 row.legs.visit(placements);
30376 return false;
30377 });
30378 std::uint64_t pins = 0, cohorts = 0;
30379 for (const auto& entry : bracket_families_) {
30380 BrokerStateHashSink row;
30381 row.u(entry.first); row.u(entry.second.settled_count()); row.u(entry.second.settled_digest());
30382 for (const auto& handle : entry.second.working_tail()) row.u(handle.incarnation);
30383 for (const auto& fact : entry.second.consumed_facts()) { row.u(fact.first); row.u(fact.second); }
30384 pins ^= row.h;
30385 }
30386 for (const auto mark : market_pyramid_adds_) {
30387 BrokerStateHashSink row;
30388 row.u(mark);
30389 pins ^= row.h;
30390 }
30391 BrokerStateHashSink witnesses;
30392 const auto& sweep = retired_row_scratch_;
30393 for (const auto* names : {&sweep.roots, &sweep.askable_origins,
30394 &sweep.candidate_names, &sweep.first_reissues, &sweep.released}) {
30395 witnesses.u(names->size());
30396 for (const auto value : *names) witnesses.u(value);
30397 }
30398 witnesses.u(sweep.origin_pins.size());
30399 for (const auto* pin : sweep.origin_pins)
30400 witnesses.u(reinterpret_cast<std::uintptr_t>(pin));
30401 for (const auto* memo : {&sweep.unsuperseded, &sweep.reissues}) {
30402 witnesses.u(memo->size());
30403 for (const auto& value : *memo) { witnesses.u(value.first); witnesses.u(value.second); }
30404 }
30405 witnesses.u(sweep.reissue_high_water); witnesses.i(sweep.reissue_cycle);
30406 pins ^= witnesses.h;
30407 for (const auto& entry : cohorts_by_id_.resident_rows()) {
30408 const auto& facts = entry.second;
30409 BrokerStateHashSink row;
30410 row.s(entry.first); row.u(facts.handle.value); row.i(facts.cycle);
30411 row.u(facts.origins.base()); row.u(facts.origins.size()); row.u(facts.origins.digest());
30412 row.u(facts.origins.members().size());
30413 if (!facts.origins.members().empty()) row.u(facts.origins.members().front().incarnation);
30414 row.u(facts.erased_opening_sides);
30415 for (const auto& opening : facts.opened) row.u(opening.incarnation);
30416 std::uint64_t units = 0;
30417 for (const auto& value : facts.live_units_by_origin) {
30418 BrokerStateHashSink unit;
30419 unit.u(value.first); unit.d(value.second);
30420 units ^= unit.h;
30421 }
30422 row.u(units);
30423 cohorts ^= row.h;
30424 }
30425 BrokerStateHashSink summary;
30426 summary.u(cohorts_by_id_.size()); summary.u(cohorts);
30427 summary.u(settled_bracket_digest_); summary.u(settled_bracket_families_.size());
30428 summary.b(retired_row_scratch_.origin_fronts_pinned);
30429 return {placements.h, pins, summary.h};
30430}
30431#endif
30432} // namespace pineforge::source
30433
30434#include "pine_adapter_probe.hpp"
30435
30436namespace pineforge::source::detail {
30437
30439 const OriginRoster& origins,
30440 const std::unordered_map<std::uint64_t, double>& live_units_by_origin) noexcept {
30441 if (live_units_by_origin.empty() || origins.members().empty()) return {0.0, 0, 0};
30442 std::uint64_t steps = 0;
30443 std::uint8_t path = 1;
30444 if (origins.members().size() > 4 * live_units_by_origin.size() + 16) {
30445 // Positions are disjoint across incarnations and ascending within
30446 // each row. Sorting all occurrences restores the roster's order.
30447 struct Term { std::size_t position; double units; };
30448 Term buf[kCohortFoldStack];
30449 std::size_t n = 0;
30450 std::size_t mapped = 0;
30451 bool abandoned = false;
30452 for (const auto& unit : live_units_by_origin) {
30453 ++steps;
30454 const auto* positions = origins.positions_of(unit.first);
30455 if (!positions) continue;
30456 for (const auto position : *positions) {
30457 ++steps;
30458 ++mapped;
30459 if (n == kCohortFoldStack) {
30460 abandoned = true;
30461 continue;
30462 }
30463 buf[n++] = {position, unit.second};
30464 }
30465 }
30466 if (!abandoned) {
30467 std::sort(buf, buf + n, [](const Term& a, const Term& b) noexcept {
30468 return a.position < b.position;
30469 });
30470 // Deterministic sort charge: P * ceil(log2 P), zero for P <= 1.
30471 std::uint64_t levels = 0;
30472 for (auto remaining = n > 0 ? n - 1 : 0; remaining; remaining >>= 1)
30473 ++levels;
30474 steps += static_cast<std::uint64_t>(n) * levels;
30475 double total = 0.0;
30476 for (std::size_t i = 0; i < n; ++i) {
30477 ++steps;
30478 const double units = buf[i].units;
30479 if (std::isfinite(units) && units > 0.0) total += units;
30480 }
30481 return {std::isfinite(total) && total > 0.0 ? total : 0.0, steps, 2};
30482 }
30483 // The partial stack gather is discarded. Comparing this charge with
30484 // M keeps either overflow completion bounded by the frozen selector.
30485 const auto mapped_count = static_cast<std::uint64_t>(mapped);
30486 const auto row_count =
30487 static_cast<std::uint64_t>(live_units_by_origin.size());
30488 // The selector has P + 1 passes. Each pass can inspect all R rows
30489 // and all P positions, including positions in exhausted rows.
30490 const auto charge_max = std::numeric_limits<std::uint64_t>::max();
30491 const auto charge_add = [charge_max](std::uint64_t left,
30492 std::uint64_t right) noexcept {
30493 return right > charge_max - left ? charge_max : left + right;
30494 };
30495 const auto charge_mul = [charge_max](std::uint64_t left,
30496 std::uint64_t right) noexcept {
30497 return left != 0 && right > charge_max / left
30498 ? charge_max : left * right;
30499 };
30500 const auto pass_count = mapped_count == charge_max
30501 ? charge_max : mapped_count + 1;
30502 const auto selector_charge =
30503 charge_mul(pass_count, charge_add(row_count, mapped_count));
30504 path = 3;
30505 if (static_cast<std::uint64_t>(origins.members().size()) >= selector_charge) {
30506 // Frozen selector, kept term-for-term so its acceptance-ordered
30507 // floating-point additions remain the old fold's additions.
30508 double total = 0.0;
30509 bool have_previous = false;
30510 std::size_t previous = 0;
30511 for (;;) {
30512 bool have_next = false;
30513 std::size_t next = 0;
30514 double units = 0.0;
30515 for (const auto& unit : live_units_by_origin) {
30516 ++steps;
30517 const auto* positions = origins.positions_of(unit.first);
30518 if (!positions) continue;
30519 for (const auto position : *positions) {
30520 ++steps;
30521 if (have_previous && position <= previous) continue;
30522 if (!have_next || position < next) {
30523 have_next = true;
30524 next = position;
30525 units = unit.second;
30526 }
30527 break;
30528 }
30529 }
30530 if (!have_next) break;
30531 if (std::isfinite(units) && units > 0.0) total += units;
30532 have_previous = true;
30533 previous = next;
30534 }
30535 return {std::isfinite(total) && total > 0.0 ? total : 0.0, steps, path};
30536 }
30537 }
30538 double total = 0.0;
30539 for (const auto& origin : origins) {
30540 ++steps;
30541 const auto unit = live_units_by_origin.find(origin.incarnation);
30542 if (unit == live_units_by_origin.end()) continue;
30543 const double units = unit->second;
30544 if (std::isfinite(units) && units > 0.0) total += units;
30545 }
30546 return {std::isfinite(total) && total > 0.0 ? total : 0.0, steps, path};
30547}
30548
30549#ifdef PINEFORGE_COHORT_EXPOSURE_FIXTURES
30550void verify_cohort_exposure(
30551 const OriginRoster& origins,
30552 const std::unordered_map<std::uint64_t, double>& live_units_by_origin,
30553 double actual) noexcept {
30554 // Frozen selector: deliberately independent of the production fold.
30555 double total = 0.0;
30556 // `live_units_by_origin` is a lookup table. Quantities must follow the
30557 // cohort's deterministic source insertion order, never its hash buckets.
30558 // Only roster positions whose incarnation has a unit row can contribute:
30559 // step through just those, ascending, rather than the id's whole entry
30560 // history. Allocation-free (the pending-order readback calls this).
30561 bool have_previous = false;
30562 std::size_t previous = 0;
30563 for (;;) {
30564 bool have_next = false;
30565 std::size_t next = 0;
30566 double units = 0.0;
30567 for (const auto& unit : live_units_by_origin) {
30568 const auto* positions = origins.positions_of(unit.first);
30569 if (!positions) continue;
30570 for (const auto position : *positions) {
30571 if (have_previous && position <= previous) continue;
30572 if (!have_next || position < next) {
30573 have_next = true;
30574 next = position;
30575 units = unit.second;
30576 }
30577 break;
30578 }
30579 }
30580 if (!have_next) break;
30581 if (std::isfinite(units) && units > 0.0) total += units;
30582 have_previous = true;
30583 previous = next;
30584 }
30585 const double expected = std::isfinite(total) && total > 0.0 ? total : 0.0;
30586 if (std::memcmp(&actual, &expected, sizeof expected) != 0) std::abort();
30587}
30588#endif
30589
30590LookupIndexCounts probe_lookup_index(const NativeHostCache* cache) noexcept {
30591 if (cache == nullptr || typeid(*cache) != typeid(AdapterLookupIndex)) return {};
30592 const auto& index = static_cast<const AdapterLookupIndex&>(*cache);
30593 LookupIndexCounts counts{true, index.cohort_sides.size(), index.unbound.size()};
30594 std::size_t kind = 0;
30595 for (const auto& by_origin : index.legs_by_origin) {
30596 counts.legs_by_origin_keys[kind] = by_origin.size();
30597 for (const auto& row : by_origin)
30598 counts.legs_by_origin_ids[kind] += row.second.size();
30599 ++kind;
30600 }
30601 counts.immediate_closes_keys = index.immediate_closes_by_bar.size();
30602 for (const auto& row : index.immediate_closes_by_bar)
30603 counts.immediate_closes_ids += row.second.size();
30604 return counts;
30605}
30606
30607} // namespace pineforge::source::detail
30608
30609namespace pineforge::source {
30610
30611// Storage policy and work counters are derived observers, outside broker
30612// state. Keep these bodies after the adapter's cited implementation regions.
30613void PlacementTable::reset_storage_work() noexcept {
30614 old_only_ = false;
30615 placement_work_ = 0;
30616 lookup_work_ = 0;
30617 removal_work_ = 0;
30618}
30619
30620void PlacementTable::grow_window(std::size_t index) {
30621 placement_work_ += static_cast<std::uint64_t>(index + 1U - window_.size());
30622 window_.resize(index + 1U);
30623}
30624
30625void PlacementTable::place_old(std::uint64_t incarnation,
30626 std::unique_ptr<PlacementSnapshot> node) {
30627 // Monotone incarnations append; an earlier incarnation still uses the
30628 // same sorted representation and shifts only the rows above it.
30629 const auto at = old_.empty() || incarnation > old_.back().first
30630 ? old_.end()
30631 : std::lower_bound(old_.begin(), old_.end(), incarnation,
30632 [this](const auto& entry, std::uint64_t key) {
30633 ++lookup_work_;
30634 return entry.first < key;
30635 });
30636 placement_work_ += static_cast<std::uint64_t>(old_.end() - at) + 1U;
30637 old_.emplace(at, incarnation, std::move(node));
30638}
30639
30640bool PlacementTable::compact_old() noexcept {
30641 if (!old_only_) return false;
30642 if (old_.empty()) {
30643 old_only_ = false;
30644 base_ = high_water_ + 1U;
30645 }
30646 return true;
30647}
30648
30649} // namespace pineforge::source
#define F(name)
static BirthCursor point(BirthCursorDomain domain, int index, int count)
bool at_terminal_fill() const
static OrderBirth direct_command(int bar, int64_t timestamp)
static OrderBirth chart_evaluation(int bar, int64_t timestamp)
static OrderBirth fill_evaluation(int bar, int64_t timestamp, BirthCursor cursor, double price, uint64_t first_fill, uint64_t last_fill, uint64_t evaluation_ordinal)
const std::optional< SizingRevision > & sizing_revision() const
void retire_prefix(std::size_t count, uint64_t digest, uint64_t last_sequence)
Base with a code: throw coded<std::invalid_argument>(code, {args}, text).
coded(RunFailureCode code, const RunFailureArgs &args, const std::string &what)
std::pair< Offset, bool > insert(const std::string &key, const Value &value)
void require_insert(const std::string &key) const
void bind_scratch(const std::shared_ptr< ScratchSlab > &slab, ScratchResource resource)
std::optional< Offset > find(const std::string &key) const
std::uint64_t size() const noexcept
void require_insert(const std::string &key) const
void bind_scratch(const std::shared_ptr< ScratchSlab > &slab, ScratchResource resource)
void admit(const std::string &key, bool spillable)
static constexpr std::size_t resident_row_bytes() noexcept
void bind_scratch_tables(const std::shared_ptr< ScratchSlab > &slab, ScratchResource data, ScratchResource index)
void set_directory(const std::string &directory)
void resize(std::size_t count, T value=T{})
std::size_t size() const noexcept
The public native host: an abstract subclass of BacktestEngine with no PineScript on it.
native_order::CohortHandle cohort_open()
Open a roster a later close can bind to.
void retire_before(std::size_t global)
void append_positions_of(const native_order::RequestHandle &handle, std::vector< std::size_t > &out) const
std::size_t size() const noexcept
const native_order::RequestHandle & operator[](std::size_t global) const
static OriginRoster from_retired(std::size_t count, std::uint64_t digest)
std::uint64_t digest() 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
std::optional< double > resolve_anchored_level(const NativeAnchoredLevelView &) const
void set_risk_max_drawdown(double value, bool percent) noexcept
friend detail::ExitScratchContext & detail::exit_scratch(const PineExecutionAdapter &)
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
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)
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
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
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()
ExitGroupWork exit_group_work() 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)
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
PositionTailWork position_tail_work() const noexcept
void set_id_tables_directory(const std::string &directory)
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)
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)
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)
bool intrabar_sample_checked(const NativeMarginCheckPoint &) const
PineCancellationReceipt * fixture_mutable_cancellation(int index) noexcept
std::uint64_t fixture_auxiliary_disk_bytes() 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
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 input_is_observed_ticks() const noexcept
double default_entry_qty(double fill_price) const
void set_auxiliary_tables_directory(const std::string &directory)
PlacementSnapshot & at(std::uint64_t incarnation)
std::uint64_t command_sequence_for(const std::string &source_id, const std::string &from_entry) const
std::array< std::uint64_t, 5 > sequence_work() const noexcept
void bind_auxiliary_scratch_tables(const std::shared_ptr<::pineforge::detail::ScratchSlab > &)
std::uint64_t auxiliary_disk_bytes() const noexcept
ScratchStatus status() const noexcept
void put(std::uint64_t major, std::uint64_t value, std::uint64_t ordinal=0)
std::uint64_t find(std::uint64_t major) const
static constexpr std::uint64_t absent
void ordered(Key key, Visit visit) const
std::uint64_t scope() const noexcept
ExitScratchContext & context() const noexcept
void set(std::size_t index, const T &value)
virtual native_order::Owner freeze_openings(const native_order::RunIdentity &, std::int64_t cycle, std::uint64_t count, void *, OpeningReader)=0
std::vector< Field > fields(const Draft &draft)
std::variant< CommandEvent, ReviewEvent, SizingEvent > Event
bool operator==(const OpeningOwner &owner, const OpeningOwner &other)
void require(bool condition, const char *message, int status=PF_SETTINGS_INVALID_ARGUMENT)
std::string quote(const std::string &value)
double trail_offset_to_ticks(double trail_offset)
exit_legs::Operation select_pair_hold(const exit_legs::Lifecycle &, exit_legs::Frame)
bool explicit_qualification(const admission::Draft &draft)
bool admits_reservation_expansion(const std::vector< std::uint64_t > &selected, bool partial, double reserved, double live) noexcept
double kernel_trail_stop_level(double best, double distance, bool is_buy, double tick) noexcept
double trail_points_to_ticks(double trail_points)
double snap_trail_level_to_tick_grid(double price, double mintick)
int last_rejected_command_bar(const admission::Journal &journal)
std::vector< std::uint64_t > select_reservation_growth_sources(const std::vector< ReservationGrowthCandidate > &candidates, const std::string &from_entry, bool process_on_close, bool effectively_flat, double percent, int bar, PositionSide side)
bool awaits_pair_review(const admission::Draft &draft)
double select_margin_revival_stop(const exit_legs::Lifecycle &)
HistoricalBirthReach select_historical_birth_reach(const OrderBirth &birth, bool requested_trailing_exit) noexcept
std::optional< exit_legs::Operation > select_exit_completion(const exit_legs::Lifecycle &, exit_legs::Frame completed)
bool awaits_default_review(const admission::Draft &draft)
std::optional< exit_legs::Operation > select_exit_suspension(const exit_legs::Lifecycle &, const ExitSuspensionContext &)
constexpr double kTrailPointsCeilMintickScale
bool historical_cascade_reach(HistoricalBirthReach reach) noexcept
ExitActivationPolicy select_exit_activation(const ExitActivationRequest &request, double stop, double limit, const ExitActivationContext &context)
ScratchStatus find(CommandScratchStore &store, std::uint64_t key, ScratchRecordInfo &out) noexcept
ScratchStatus put(CommandScratchStore &store, std::uint64_t key, const ScratchByteSource &value) noexcept
std::uint64_t add(std::uint64_t left, std::uint64_t right)
void bind_id_pair_scratch_tables(source::PineExecutionAdapter &, const std::shared_ptr< ScratchSlab > &)
std::uint64_t ScratchHandle
constexpr ScratchHandle scratch_no_handle
void bind_tree_scratch_tables(source::PineExecutionAdapter &, const std::shared_ptr< ScratchSlab > &)
void rethrow_carried_scratch_failure(const ReportScratchIoError *failure)
void raise_scratch_io_error(int error, const char *operation)
void clear_exposure_storage(Container &container)
void release_empty_exposure_capacity(Container &container)
NativePrecommitVerdict
The host is consulted before generic opening-margin admission.
bool entry_stop_first_touch(const Bar &bar, bool high_first, double stop_level, bool is_long, double *out_pos)
NativeRunSpecValidation normalize_native_run_spec(NativeRunSpec &spec, const NativeBarViews &views) noexcept
Existing full validation and zero-fee normalization over effective data.
constexpr NativeCivilDate native_civil_date(int64_t days) noexcept
NativePathPhase
Which leg of a modeled OHLC walk a point sits on.
bool quantity_on_grid(double q, double step) noexcept
v2 exact binary64 grid: r=abs(q)/s, n=round(r) half away from zero, g=n*s.
std::variant< Independent, WaitForApplied, BindOpening, BindOpenings, BindCohort > Owner
std::variant< AcceptedEvent, RejectedEvent, ReplacedEvent, ReplaceRejectedEvent, CancelledEvent, NotWorkingEvent, InvalidHandleEvent, NoEffectEvent, MatchRejectedEvent, ExecutionAppliedEvent, CloseBoundEvent, ActivatedEvent, ReservationReducedEvent, DeferredGroupAdjustmentEvent, QuantityBoundEvent, ArmedEvent, TermsResolvedEvent, MarginCallEvent, NativeRiskEvent > CommandEvent
Every alternative of one request's history, as one value.
std::variant< Market, Limit, Stop, StopLimit, Trail > Trigger
std::variant< Flatten, Reduce, Transact, ReverseTo, HostSized, Sized > OrderIntent
What a request does.
std::variant< NoGroup, Member > Group
order_action::Transact Transact
A signed book transaction: finite nonzero units on the side their sign names.
NativePathOrder
Generic ordering for a modeled OHLC path.
NativeOpenDirections
Which opening directions the run admits at all.
NativeFeedTolerance
Explicit, opt-in admission exceptions for a tolerated input-feed shape.
NativeFeeKind
Encodings coincide with the versioned native-v1 C transport.
NativeRunSpecField
Which field a validation refused, in deterministic first-error order.
std::optional< Plan > plan(double signed_position, Reduce request) noexcept
int b(int64_t c)
Definition color.hpp:44
bool carry_reissue_bindings() noexcept
constexpr std::size_t kCohortFoldStack
static constexpr std::uint64_t source_key_test_mask(std::uint64_t value) noexcept
double replay_position_tail(std::size_t end, std::size_t base, Read &&read, const PositionTailSpill *spill, std::int64_t barrier, Include &&include, const std::string &from_entry, PineExecutionAdapter::PositionTailWork &work, Diagnostics={})
MarginRuleSwitches & margin_rule_switches() noexcept
void placement_audit_erase(const PlacementTable *table, std::uint64_t incarnation, PlacementSnapshot &&row)
RunState run_state(const NativeStrategyHost &host)
NativePhysicalPosition run_position(const NativeStrategyHost &host)
LookupIndexCounts probe_lookup_index(const NativeHostCache *cache) noexcept
void take_run_facts(NativeExecutionConsumer &consumer, const PineExecutionAdapter &adapter, std::uint64_t run)
void exit_scratch_lots(ExitScratchContext &scratch, const NativeStrategyHost &host, double mark, Visit visit)
MarginOpeningSwitches & margin_opening_switches() noexcept
bool retain_retired_rows() noexcept
const NativeCurrentPointView * callback_point(const NativeStrategyHost &host)
ScriptRuleSwitches & script_rule_switches() noexcept
ExitScratchVersions & exit_scratch_versions(ExitScratchContext &context)
MarginScheduleSwitches & margin_schedule_switches() noexcept
ExitScratchContext & exit_scratch(const PineExecutionAdapter &)
void exit_scratch_check(ScratchStatus status)
std::uint64_t retired_rows_erased() noexcept
bool exit_scratch_equal(const ScratchByteSource &left, const ScratchByteSource &right)
ScratchKeyOrder exit_scratch_key_order(ExitScratchOrderStats *stats=nullptr) noexcept
ExitScratchRows< T, ExitScratchRowMode::bounded_batch > ExitScratchBatchRows
void exit_scratch_working(ExitScratchContext &scratch, const NativeStrategyHost &host, Visit visit)
ExitBindingRuleSwitches & exit_binding_rule_switches() noexcept
void set_carry_reissue_bindings(bool carry) noexcept
void placement_audit_forget(const PlacementTable *table) noexcept
void set_retain_retired_rows(bool retain) noexcept
PoocCloseFillViewSwitches & pooc_close_fill_view_switches() noexcept
ScratchSpanSource exit_scratch_text(const std::string &text) noexcept
bool pine_callback_lifecycle_rule(PineCallbackLifecycleRule rule) noexcept
const std::vector< std::pair< std::uint64_t, const PlacementSnapshot * > > * placement_audit_tombstones(const PlacementTable *table)
const NativeCurrentPointView * live_callback_point(const NativeStrategyHost &host)
const NativeRunSpec * run_spec(const NativeStrategyHost &host)
NativeExecutionConsumer & run_consumer(const NativeStrategyHost &host)
void placement_audit_scan_hit(const PlacementTable *table, std::uint64_t incarnation, unsigned line) noexcept
CohortExposureFold fold_cohort_exposure(const OriginRoster &origins, const std::unordered_map< std::uint64_t, double > &live_units_by_origin) noexcept
bool skip_quiet(QuietHook hook, bool quiet) noexcept
void set_pine_callback_lifecycle_rule(PineCallbackLifecycleRule rule, bool on) noexcept
void exit_scratch_failure(ScratchStatus)
void placement_audit_miss(const PlacementTable *table, std::uint64_t incarnation, unsigned line) noexcept
constexpr const char * order_limit_price
constexpr const char * exit_trail_points
constexpr const char * order_stop_price
constexpr const char * entry_stop_price
constexpr const char * entry_limit_price
constexpr std::uint32_t kCoofLoopGuard
constexpr char kMarginCallLabel[]
void throw_strategy_value_invalid(const char *field, StrategyValueReason reason)
The coded failure of a source-owned invalid actionable value.
constexpr char kIntradayLossComment[]
std::unique_ptr< NativeLowerPathSource > make_magnifier_path_source(const Bar *bars, int n, const std::string &input_tf, const std::string &script_tf, const std::string &session, const std::string &timezone)
The same logical path without retaining the input or a whole-run path.
bool modeled_pine_input(NativePriceProvenance provenance) noexcept
std::optional< double > checked_mul(double left, double right) noexcept
std::string SourceId
const char * instrument_feed_field_name(NativeRunSpecField field) noexcept
std::optional< double > checked_add(double left, double right) noexcept
RunFailureValue run_spec_field_failure(NativeRunSpecField field) noexcept
std::vector< Bar > tradingview_magnifier_bars(const Bar *bars, int n, const std::string &input_tf, const std::string &script_tf, const std::string &session, const std::string &timezone)
The lower-timeframe path TradingView's bar magnifier walks for every chart bar of a run whose chart b...
std::optional< double > checked_div(double left, double right) noexcept
int tf_ratio(const std::string &input_tf, const std::string &target_tf)
Compute how many input bars fit into one target bar.
RunFailureValue make_run_failure(RunFailureCode code, const RunFailureArgs &args) noexcept
Validate args against code's registry entry and canonicalize them.
std::string detect_timeframe(const Bar *bars, int n, int max_samples=100)
Detect the timeframe string from an array of bars by computing the median timestamp delta and mapping...
static void submit(struct host_state *state, uint32_t intent, double value, uint32_t trigger, double price, const char *label)
#define PINEFORGE_AUDIT_PLACEMENT_SCAN(table,...)
#define PINEFORGE_MARGIN_REVIVAL_OBSERVE(before, rows, event, context, units, cycle, selected)
#define PINEFORGE_FULL_CLOSE_EXIT_CALL()
#define PINEFORGE_LOOKUP_INDEX_WORK(site, owner, run, count)
#define PINEFORGE_RETIREMENT_OBSERVE(rows, cycle, askable, roots, candidates, first)
#define PINEFORGE_POSITION_TAIL_DIAGNOSTICS
#define PF_RARE(condition)
#define PINEFORGE_FULL_CLOSE_EXIT_ORACLE()
#define PINEFORGE_TAIL_WORK(site, count)
#define PF_ROSTER_PASS_WORK(field)
double open
Definition bar.hpp:7
double close
Definition bar.hpp:7
double low
Definition bar.hpp:7
int64_t timestamp
Definition bar.hpp:8
double high
Definition bar.hpp:7
CacheCapacity consumed_cycle_pages
CacheCapacity cohort_id_pages
CacheCapacity retained_sequence_pages
CacheCapacity erased_minima_pages
CacheCapacity exit_rank_pages
A validated code and its canonical arguments: the JSON object with its keys sorted and no whitespace,...
std::string timezone
Definition engine.hpp:485
std::string tickerid
Definition engine.hpp:481
std::string ticker
Definition engine.hpp:480
std::string basecurrency
Definition engine.hpp:483
std::string volumetype
Definition engine.hpp:487
std::string type
Definition engine.hpp:484
std::string description
Definition engine.hpp:488
std::string currency
Definition engine.hpp:482
std::string session
Definition engine.hpp:486
Ephemeral read-only facts of one anchored-leg materialization (L7b), offered to the host exactly once...
Borrowed begin-call facts.
Read-only owning-value facts for one candidate.
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.
bool liquidation_resting
Whether the model already holds a liquidation resting from an earlier admitted point.
The host's answer to one requirement view.
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,...
Bind to a host-built roster (cohort_open / cohort_add / cohort_remove), read at the match rather than...
Bind to a fixed cohort of already-live openings.
A host-maintained, run-scoped roster identity.
One committed execution: the definition, the cursor, the resolved price and units,...
A reduction of exactly these units, finite and positive.
Defer a level to the owner's fill: at the arm the level becomes fill + offset, with offset signed (ad...
An opening or a closing whose quantity the HOST resolves, in NativeStrategyHost::resolve_execution_te...
No owner: the request stands on its own book authority.
fill_through makes the limit a touch trigger (market-if-touched): the level still gates when the requ...
Match at the next eligible matching point, with no level to reach.
Replacement behaviour that is not expressible in the successor request.
Aggregate field order keeps market construction: Request{Transact{1.0}, "buy", "comment"}...
A stop trigger: the modeled path has to reach price from the adverse side.
The one owner relation that arms (the ArmedEvent).
@ ContinuousSegments
Native hosts retain continuous matching between generated samples unless they explicitly request poin...
A generic per-side broker margin model (L4).
One complete setup value, staged/copied by NativeStrategyHost before it is applied at begin.
NativeSlotLabelPolicy slot_label_policy
Strict native hosts retain the canonical slot-label rule.
NativeCalculationTrigger calculation
Calculation timing.
std::vector< NativeInstrumentFeed > instrument_feeds
Opt-in feeds of other instruments.
std::optional< NativeMarginModel > margin
Opt-in generic margin model.
bool timeframe_undetected
A public begin with fewer than two bars may not establish a timeframe.
NativeEventRetention event_retention
What native_events() can still return; see NativeEventRetention.
std::uint64_t recreated_after_named_cancelled_entry_incarnation
native_order::RequestHandle paired_reversal_parent
native_order::RequestHandle bracket_origin
native_order::RequestHandle preserved_by_close_all
native_order::RequestHandle final_short
static void preserve(std::shared_ptr< PositionTailSpill > &spill, const TradeLedger &trades, std::size_t end, std::int64_t position_open, const std::string &directory, std::size_t cache_bytes=PositionTailSpill::kWindowBytes)