diff --git a/CMakeLists.txt b/CMakeLists.txt index 6de6d2f7..d8503f29 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,6 +78,7 @@ endif() # === pineforge library ================================================= add_library(pineforge STATIC src/c_abi.cpp + src/compat/pine/order_birth.cpp src/compat/pine/order_priority.cpp src/engine_aux_security.cpp src/engine_fills.cpp diff --git a/docs/pages/abi-stability.md b/docs/pages/abi-stability.md index 7b614659..97186c61 100644 --- a/docs/pages/abi-stability.md +++ b/docs/pages/abi-stability.md @@ -112,30 +112,32 @@ notice: - The shape of internal log lines (use them for humans, not parsers). Rebuild generated and native C++ objects against matching engine headers and -runtime. The cap and priority extractions change the v2/v3 object layouts and -therefore use the internal `engine_script_run_v4` inline namespace. An object -built from base `38dc73e` headers references v2 out-of-line members and must -fail to link to this v4 runtime. The source-pairing check compiles frozen exact -base headers separately before testing the link, so a missing include or -compile failure cannot masquerade as mismatch protection. Frozen f864/v3 headers are also rejected by this v4 runtime. Both generated-style -and native-style current callers must still link. +runtime. The integrated quantity, predecessor and order-birth representation +changes `PendingOrder` and uses internal `engine_script_run_v5`. Exact frozen +c45/v4 headers are compiled before link tests: both native and generated-style +v4 callers must fail to link to the v5 archive, while matched v5 callers link. +Earlier exact base38/v2 and f864/v3 mismatch controls remain. A compile failure +cannot masquerade as mismatch protection; no pairing executable is run. `PINEFORGE_HAS_SCRIPT_RUN_PREPARE_V1` remains 1: it describes the existing hook capability, not the class layout version. Regenerate and rebuild a strategy module to obtain complete script-state reset; replacing an archive does not -retrofit an old module. Public C function signatures, POD layouts, -`PF_ABI_VERSION` (4), and `strategy_stream_api_version()` (1) are unchanged. +retrofit an old module. Public C function signatures, `PF_ABI_VERSION` (4), +and `strategy_stream_api_version()` (1) are unchanged. The pending-order v1 +mirror keeps all 108 existing field names/types/offsets and its full old prefix; +new typed facts append, and removed native booleans survive only as read-only +derived outputs. Size-limited reads keep old callers within their buffers. Namespace versioning protects referenced internal C++ symbols; it does not validate an erased `pf_strategy_t` handle. Use a handle only with functions from its creating strategy module. A fully self-contained old module can still use -its own matching runtime; this check does not turn it into a v4 module. +its own matching runtime; this check does not turn it into a v5 module. -The cap and priority boundaries advance the broker fingerprint domain to -`pineforge-broker-state/v4` and stream fingerprint version to 4. These identify -changed serialized state, including Pine priority attachment/configuration, cap quota/cause and -separate generic close request. The Pine component schema remains 1; it is -independent of the aggregate fingerprint version. Prior v2/v3 fingerprints are +The integrated representation advances the broker fingerprint domain to +`pineforge-broker-state/v5` and stream fingerprint version to 5. These identify +changed serialized quantity/reservation, predecessor and birth facts, alongside +existing Pine policy state. The Pine component schema remains 1; it is +independent of the aggregate fingerprint version. Prior v2/v3/v4 fingerprints are not comparable. Fingerprints are replay checks, not serialized checkpoints or complete hashes of private strategy state. The native runner already binds its strategy-library SHA; its ledger format and Python provenance fingerprints diff --git a/docs/pages/quantity-intent.md b/docs/pages/quantity-intent.md new file mode 100644 index 00000000..d575c0e8 --- /dev/null +++ b/docs/pages/quantity-intent.md @@ -0,0 +1,61 @@ +`PendingOrder::quantity_request` separates an exit's original requested amount +from the numeric reservation made for that request. It replaces two stored +booleans that mixed those lifetimes. + +`QuantityIntent` contains exactly one of `Units(amount)`, +`Fraction(numerator, denominator)` or `All`. Fractional requests retain their +original representation; the Pine boundary supplies percentages as `P / 100` +without a floating-point divide/multiply round trip. An absent request belongs +to pending commands outside this exit-request contract. These are request +descriptors, not an additional numerical-admission policy. + +`QuantityReservation` records admitted units and the exposure basis used when +reserving them. A new request clears the old reservation; a reservation cannot +exist without an original request. Deferred exits acquire this receipt when a +live owner is bound. Additional per-entry bindings copy the original request +and record their own admitted amount and basis. Copying is by value. + +`qty` and `qty_percent` retain their current executable/reserved meaning in the +existing order paths. OCA can reduce executable quantity without changing +the original intent or its prior reservation receipt. Whether that receipt +covered less than its basis is a derived numerical comparison, using the +existing caller-supplied tolerances. A deferred fraction can be classified +before resolution; an unbound Units request cannot infer coverage without a +numeric basis. + +Consequently, a half-position request rounded to a minimum one-unit slot may +have `Fraction(50, 100)` intent and a full `1 / 1` reservation. Conversely an +`All` request clipped behind another reservation may hold only `3 / 4` of its +basis. Neither case can be represented correctly by one “partial” label. + +Deferred market-close instructions are another producer: `strategy.close` +has already resolved its source amount to a placement target before +`queue_deferred_close_order` runs. The request records that resolved source +target as `Units(qty_to_close)`, without an exposure reservation until a later +layered binding occurs. This is not a fixed executable-quantity promise: the +preserved Pine ANY-relative rule can turn target 1 on E2 into reservation 2 +with basis E4 after reversal. The initial target remains 1 while executable +`qty` and the later reservation are 2. It does not claim to retain the original +Pine percentage expression; that conversion already occurred. This preserves +the old initial nonpartial classification without fabricating a basis. + +The Pine percentage rounding, minimum-slot, reservation retention, one-shot +exit-ID and deferred/replacement policies remain explicit compatibility debt +in their existing callers. This change does not generalize those policies or +change their thresholds, financial assertions or execution order. + +The public `pf_pending_order_v1_t` retains its complete existing field prefix. +Its `requested_partial` and `full_percent_exit_request` fields are deprecated +read-only projections derived from the new authoritative values; native +decisions never read those output fields. New fields append: + +- `quantity_intent_kind`: 0 absent, 1 Units, 2 Fraction, 3 All. +- The relevant units or fraction numerator/denominator; inactive values are 0. +- Reservation presence, admitted units and basis units. + +All new facts and optional-presence discriminators participate in broker +hashing and mirror output. They are not waived. C ABI version 4 and stream +API version 1 remain unchanged, and size-limited mirror reads preserve older +callers. The internal C++ layout changes, so all consumers require a matching +rebuild and the integrated representation change requires a new internal +namespace/fingerprint epoch before publication. diff --git a/docs/pending-placement-receipts.md b/docs/pending-placement-receipts.md new file mode 100644 index 00000000..ff23599a --- /dev/null +++ b/docs/pending-placement-receipts.md @@ -0,0 +1,53 @@ +# Pending-order placement and replacement facts + +`PendingOrder::replaced_order_incarnation` identifies the immediate live +predecessor whose priority slot the newly accepted order retains. Zero means +fresh construction. The new order still receives its own fresh `incarnation`; +`created_seq` remains its scheduling priority, not identity. + +The receipt is populated by high-level MARKET/ENTRY, RAW, and primary EXIT +replacement. Named cancel followed by recreation is fresh; its separate +cancel/recreate receipt does not become a replacement. When a Pine exit +reissue materializes multiple legs, the primary leg inherits the preceding +primary's priority and predecessor; additional legs are fresh. This receipt +does not claim to enumerate all sibling objects erased by that reissue. + +The former native `created_by_same_id_replacement` Boolean and redundant +`replaced_exit_order_incarnation` scalar are removed. Replacement readers use +the authoritative predecessor. The conditional +`replaced_default_market_incarnation` remains a Pine qualification receipt: +it records additional predecessor kind, sizing, side, source-bar and cycle +conditions that generic replacement identity alone cannot establish later. + +The former `created_while_in_position` Boolean is also removed. Its production +meaning was EXIT-only: `strategy.exit` derived it from the same `effectively_flat` +calculation used for `created_position_side`; a positive deferred close used the +nonflat side it targeted. EXIT consumers now read that existing placement side. +The two non-EXIT checks were vacuous because their producers always left the +old Boolean false; those checks are removed without requiring flat placement. + +`created_position_side` is not renamed or reinterpreted as a universal physical +snapshot. MARKET/ENTRY/RAW capture physical exposure; a Pine EXIT captures +exposure after earlier same-evaluation close claims. The physical position can +still be open when such an EXIT captures FLAT. Existing cycle/carry fields and +their scopes are unchanged. A complete physical placement/close-claim model is +separate work. + +The public size-aware `pf_pending_order_v1_t` retains every existing field at +its original offset. Its old replacement Boolean, EXIT predecessor scalar and +in-position Boolean are deprecated derived **output projections**, never core +state or inputs. The legacy RAW projections remain false/zero; the appended +`replaced_order_incarnation` reports the true RAW predecessor. Dynamic-layout +readers can discover the new field; older prefix readers retain their layout. +The generator emits these projections explicitly without native storage or a +readback path. Native hashing includes the predecessor once and placement side +once; removed redundant fields need no independent hash state. + +This removes two of the 32 direct PendingOrder Boolean members, leaving 30 in +this component. It replaces one bit with factual identity and removes one +duplicate placement value; it is not a Boolean wrapper or a renamed policy +mask. Existing Pine priority/admission/close policies still read these facts +and retain their existing qualification rules. Public C ABI version 4 and +pending mirror version 1 remain unchanged. Final aggregate internal C++/hash +versioning and stale-object pairing are owned by the integrated refactor; +this component must not be published separately without that boundary. diff --git a/include/pineforge/compat/pine/order_birth.hpp b/include/pineforge/compat/pine/order_birth.hpp new file mode 100644 index 00000000..78906582 --- /dev/null +++ b/include/pineforge/compat/pine/order_birth.hpp @@ -0,0 +1,21 @@ +#pragma once +#include "../../order_birth.hpp" + +namespace pineforge { inline namespace engine_script_run_v5 { struct PendingOrder; } } +namespace pineforge::compat::pine { + +// Historical Pine permissions remain policy, not physical birth facts. +enum class HistoricalBirthReach : int32_t { Standard, ExtremeWaypoints }; +inline bool first_open_fill_evaluation(const OrderBirth& birth) { + return birth.from_fill() && birth.evaluation_ordinal() == 1 + && birth.cursor().first_point(); +} +HistoricalBirthReach select_historical_birth_reach(const OrderBirth& birth, + bool requested_trailing_exit); +bool historical_cascade_reach(const PendingOrder& order); + +} // namespace pineforge::compat::pine + +namespace pineforge { +using PineHistoricalBirthReach = compat::pine::HistoricalBirthReach; +} diff --git a/include/pineforge/compat/pine/order_priority.hpp b/include/pineforge/compat/pine/order_priority.hpp index 2e22275b..3621990f 100644 --- a/include/pineforge/compat/pine/order_priority.hpp +++ b/include/pineforge/compat/pine/order_priority.hpp @@ -5,7 +5,7 @@ #include #include -namespace pineforge { struct PendingOrder; } +namespace pineforge { inline namespace engine_script_run_v5 { struct PendingOrder; } } namespace pineforge::compat::pine { struct OrderPriorityContext { diff --git a/include/pineforge/engine.hpp b/include/pineforge/engine.hpp index 2c7eb76f..7a75d98a 100644 --- a/include/pineforge/engine.hpp +++ b/include/pineforge/engine.hpp @@ -13,6 +13,9 @@ #include "na.hpp" #include "bar.hpp" #include "broker_events.hpp" +#include "quantity_intent.hpp" +#include "order_birth.hpp" +#include "compat/pine/order_birth.hpp" #include "compat/pine/intraday_cap.hpp" #include "compat/pine/order_priority.hpp" #include "series.hpp" @@ -399,6 +402,9 @@ enum class ShortSeedCollisionRole : uint8_t { FINAL_SHORT, }; +// PendingOrder crosses out-of-line helper boundaries independently of the +// engine class, so its changed layout must carry the same internal epoch. +inline namespace engine_script_run_v5 { struct PendingOrder { std::string id; std::string from_entry; // for exit orders @@ -426,12 +432,12 @@ struct PendingOrder { // which intentionally survives same-id replacement to keep broker ordering // stable, incarnation is never copied or reused by a replacement. uint64_t incarnation = 0; - // True when this pending object was created by reissuing an id that was - // already live. The fresh incarnation above identifies the new call, but - // created_seq intentionally retains the replaced order's broker priority. - // Exact clean-room two-call rules must fail closed on this provenance - // rather than mistaking retained priority for current source order. - bool created_by_same_id_replacement = false; + // Exact live object whose priority slot this newly accepted order replaces. + // Fresh and cancel-then-recreate orders carry zero. This is causal identity, + // not a Boolean source-shape label: every entry, RAW and primary exit path + // records its immediate predecessor before that object is erased. Reissued + // extra exit legs are fresh objects and do not share the primary receipt. + uint64_t replaced_order_incarnation = 0; // Exact default MARKET replaced on this source bar. A priced order or // a prior-bar carry with the same id does not prove this call topology. uint64_t replaced_default_market_incarnation = 0; @@ -440,11 +446,6 @@ struct PendingOrder { // Mark only the exact later MARKET objects after that fill; a reissue // creates a fresh object, and cancelled siblings spend no broker event. bool declined_by_replaced_short_market = false; - // For a strategy.exit replacement, the unique incarnation of the exact - // matching (id, from_entry) EXIT object it replaced. Zero for a fresh - // child. This correlates retained broker priority with a concrete prior - // child rather than a replacement-shaped call sequence created later. - uint64_t replaced_exit_order_incarnation = 0; // Incarnation of the live priced ENTRY removed by strategy.cancel(id) // earlier in the same source evaluation, copied only onto the first fresh // same-id strategy.entry call and then consumed. Zero means there is no @@ -466,32 +467,14 @@ struct PendingOrder { // suppressed leg carries into the next bar as an ordinary order. bool coof_suppress_stop_on_entry_bar = false; bool coof_suppress_limit_on_entry_bar = false; - // True only when this order was emitted by a historical - // calc_on_order_fills execution. POOC must not confuse that intrabar - // origin with an order emitted by the ordinary close-time execution. - bool created_during_coof_recalc = false; - // Stronger provenance for orders born specifically in the recalculation - // triggered by a close-point (C) fill. C has already been consumed: no - // order from that recalculation may refill at C or inspect the elapsed - // wick. A POOC market instruction has missed its only eligible close and - // expires unless an ordinary execution reissues it; priced GTC orders - // become ordinary carried orders on the next bar. - bool coof_born_at_close_recalc = false; - // KI-67 cascade provenance: true when this order was placed by a MID-BAR - // fill recalc (a recalc chain that did not own the first fill event at the - // bar-open tick). Such "cascade" orders are eligible ONLY at the remaining - // EXTREME waypoints (W1/W2) of the historical 4-tick path — never - // intra-segment at an exact level and never at C — for the bar they are - // born on; at bar end they convert to ordinary resting orders. Orders born - // in that first BAR-OPEN recalc (or resting at bar start) keep standard - // exact-level semantics and leave this false. Scoped to the historical - // path; the magnifier path (bar_magnifier_enabled_) ignores this bit. - // - // ENTRY cascade orders use the plain "extreme-waypoint only" reach above. - // strategy.exit cascade orders follow the finer KI-67 Model S rule - // ("R-cascade-gapjump") captured by the two fields below. - bool coof_born_mid_bar = false; - // KI-67 exit cascade (Model S). Set at birth for a coof_born_mid_bar + // Immutable evaluation/fill origin, captured once for this incarnation. + // Historical extreme-only/trailing permissions live in compat::pine. + OrderBirth birth; + // Disclosed Pine historical fill permission. Trigger fields can be + // neutralized before deferred compaction; those mutations must not + // rewrite a birth-time permission or masquerade as a different origin. + PineHistoricalBirthReach pine_birth_reach = PineHistoricalBirthReach::Standard; + // KI-67 exit cascade (Model S). Set at birth for a Pine historical-cascade // strategy.exit order: the historical-path LEG index (0 = O->W1, 1 = W1->W2, // 2 = W2->C) the triggering intrabar fill (coof_cursor_price_ "ap") landed // on — the "in-flight" leg. -1 when the order is not a mid-bar cascade exit, @@ -506,6 +489,9 @@ struct PendingOrder { // 0 and gets its gap attempt at W1. Marketable STOP never uses that extension. // Otherwise subsequent legs exact-fill, while a terminal/off-path order rolls. bool coof_cascade_inflight_fires = false; + // Placement exposure used by this order. ENTRY/RAW capture the physical + // side; Pine EXIT captures the exposure after earlier same-evaluation + // close claims. This is not a universal physical-position snapshot. PositionSide created_position_side = PositionSide::FLAT; // Monotonic identity of the live position instance at placement. Side // alone is insufficient: a resting order can survive LONG -> SHORT -> @@ -869,10 +855,10 @@ struct PendingOrder { double signal_close_mc_remaining_qty = std::numeric_limits::quiet_NaN(); std::string comment; // order comment for trade reporting - bool requested_partial = false; // true iff caller passed qty_percent < 100 - // Preserve the original default/full-percent EXIT call before reservation - // normalization can turn a sub-lot partial request into a full-size order. - bool full_percent_exit_request = false; + // Original exit amount and its latest resolved reservation basis. qty and + // qty_percent remain the executable/reserved values used by existing Pine + // reservation rules; their later reduction cannot rewrite caller intent. + QuantityRequest quantity_request; // Narrow POOC global-full-exit candidate. ``qty`` deliberately keeps the // normal finite reservation so sibling exits see and respect its capacity. // At fill time this bit upgrades that one reservation to the full live @@ -890,7 +876,6 @@ struct PendingOrder { // tracking global EXIT is placed. Same-id replacement constructs a fresh // PendingOrder and therefore drops the relation; later orders never get it. bool pooc_global_full_exit_bound_add = false; - bool created_while_in_position = false; // true if position was open when order was placed // round 8 family S — TradingView's same-bar MARKET transaction (ledger // note log-20260905t143024z-76025577; 15 lab tv sensor tapes famS-dbl-*, // famS-rev-plus-close, famS-adm-{es,nq}-{1e6,500k} on CME_MINI:ES1!/NQ1! @@ -1050,6 +1035,8 @@ struct PendingOrder { ShortSeedCollisionRole::NONE; }; + } // inline namespace engine_script_run_v5 (PendingOrder) + // default_qty_type constants (matches TradingView) enum class QtyType { FIXED = 0, PERCENT_OF_EQUITY = 1, CASH = 2 }; @@ -1103,10 +1090,10 @@ struct StrategyOverrides { // The C++ subclass contract is internal, unlike pineforge.h's stable C ABI. // Changing its layout or vtable requires all generated/native C++ objects to be rebuilt. -// v4 adds detached Pine order-priority ownership to the v3 cap boundary. +// v5 integrates typed quantity, replacement identity and causal order birth. // Version the mangled class name so older headers' member offsets/vtable cannot // silently bind out-of-line members of this different object layout. -inline namespace engine_script_run_v4 { +inline namespace engine_script_run_v5 { class BacktestEngine { protected: // --- Position state --- @@ -2106,7 +2093,7 @@ class BacktestEngine { || order.created_bar != bar_index_ || order.created_position_side != PositionSide::FLAT || order.created_after_position_close_in_bar - || order.created_by_same_id_replacement + || (order.replaced_order_incarnation != 0) || order.oca_type != 0 || !order.oca_name.empty() || position_side_ != PositionSide::FLAT || position_entry_count_ != 0 || !pyramid_entries_.empty() @@ -2150,7 +2137,7 @@ class BacktestEngine { || order.created_bar != bar_index_ || order.created_position_side != PositionSide::FLAT || order.created_after_position_close_in_bar - || order.created_during_coof_recalc || order.created_by_same_id_replacement + || order.birth.from_fill() || (order.replaced_order_incarnation != 0) || !order.oca_name.empty() || order.oca_type != 0 || position_side_ != PositionSide::FLAT || position_entry_count_ != 0 || !pyramid_entries_.empty() || pyramiding_ < 0 || pyramiding_ > 1 @@ -3162,7 +3149,7 @@ class BacktestEngine { // KI-67: true only while the active fill recalc owns the FIRST fill event // at the bar-open tick (O). Orders placed while this holds keep STANDARD // exact-level semantics. Later fills at that same O, like fills at every - // other path point, are MID-BAR cascades (PendingOrder::coof_born_mid_bar). + // other path point, are MID-BAR cascades (the Pine historical cascade permission). bool coof_recalc_at_bar_open_ = false; // True only while executing a fill recalc triggered by a later fill event // at O, after the first O fill has already consumed bar-open provenance. @@ -4409,6 +4396,7 @@ class BacktestEngine { // Shared by run(), run_simple_bar_loop, and the no-magnifier aggregation // path. The magnifier tick loop does NOT use this — it gates the sequence // on is_last_tick_ and forces is_first_tick_ before on_bar. + OrderBirth capture_order_birth() const; void invoke_chart_on_bar(const Bar& bar); void dispatch_bar(); void dispatch_bar_calc_on_order_fills(); @@ -4418,16 +4406,14 @@ class BacktestEngine { uint64_t execute_coof_script_body(const Bar& script_bar, double broker_cursor_price, bool cursor_is_bar_point, - bool is_fill_recalc, - bool cursor_is_bar_close, - bool recalc_at_bar_open, + const OrderBirth& evaluation_origin, uint64_t direct_fill_event_budget, bool opening_money_prefix = false); uint64_t run_coof_recalc_chain(const Bar& script_bar, double broker_cursor_price, bool cursor_is_bar_point, - bool cursor_is_bar_close, - bool recalc_at_bar_open, + BirthCursor cursor, + uint64_t& evaluation_ordinal, uint64_t triggering_events, uint64_t max_events, uint64_t events_already, @@ -5142,5 +5128,5 @@ class BacktestEngine { void trace(const std::string& name, int value) { trace(name, static_cast(value)); } }; -} // inline namespace engine_script_run_v4 +} // inline namespace engine_script_run_v5 } // namespace pineforge diff --git a/include/pineforge/order_birth.hpp b/include/pineforge/order_birth.hpp new file mode 100644 index 00000000..ed4a4038 --- /dev/null +++ b/include/pineforge/order_birth.hpp @@ -0,0 +1,106 @@ +#pragma once +#include +#include +#include +#include + +namespace pineforge { + +// Geometry supplied by the dispatcher, never inferred from equal OHLC prices. +enum class BirthCursorDomain : int32_t { None, HistoricalPath, MagnifierTicks }; +enum class BirthCursorPosition : int32_t { None, Point, Segment }; +enum class OrderBirthCause : int32_t { + Unattributed, // manually constructed records; no event is asserted + DirectCommand, // native API call outside a chart evaluation + ChartEvaluation, + FillEvaluation, +}; + +class BirthCursor { +public: + BirthCursor() = default; + static BirthCursor point(BirthCursorDomain domain, int index, int count) { + return make(domain, BirthCursorPosition::Point, index, count); + } + static BirthCursor segment(BirthCursorDomain domain, int index, int count) { + return make(domain, BirthCursorPosition::Segment, index, count); + } + BirthCursorDomain domain() const { return domain_; } + BirthCursorPosition position() const { return position_; } + int index() const { return index_; } + int count() const { return count_; } + bool first_point() const { return position_ == BirthCursorPosition::Point && index_ == 0; } + bool terminal_point() const { + return position_ == BirthCursorPosition::Point && index_ + 1 == count_; + } + int following_segment() const { return terminal_point() ? -1 : index_; } +private: + static BirthCursor make(BirthCursorDomain domain, BirthCursorPosition position, + int index, int count) { + if ((domain != BirthCursorDomain::HistoricalPath && domain != BirthCursorDomain::MagnifierTicks) + || count <= 0 || (domain == BirthCursorDomain::HistoricalPath && count != 4) + || index < 0 || index >= count + || (position == BirthCursorPosition::Segment && index + 1 >= count)) + throw std::invalid_argument("invalid order-birth dispatch cursor"); + BirthCursor out; + out.domain_ = domain; out.position_ = position; + out.index_ = index; out.count_ = count; + return out; + } + BirthCursorDomain domain_ = BirthCursorDomain::None; + BirthCursorPosition position_ = BirthCursorPosition::None; + int index_ = -1; + int count_ = 0; +}; + +// Immutable value: fields have no setters. Replacement constructs a new value; +// copy/move of an order preserves the complete receipt. A grouped callback may +// cover an actual contiguous fill-event interval; it does not invent one fill. +class OrderBirth { +public: + OrderBirth() = default; + static OrderBirth direct_command(int bar, int64_t timestamp) { + return ordinary(OrderBirthCause::DirectCommand, bar, timestamp); + } + static OrderBirth chart_evaluation(int bar, int64_t timestamp) { + return ordinary(OrderBirthCause::ChartEvaluation, bar, 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) { + if (bar < 0 || cursor.domain() == BirthCursorDomain::None || !std::isfinite(price) + || first_fill == 0 || last_fill < first_fill || evaluation_ordinal == 0) + throw std::invalid_argument("invalid order-birth fill evaluation"); + OrderBirth out = ordinary(OrderBirthCause::FillEvaluation, bar, timestamp); + out.cursor_ = cursor; out.cursor_price_ = price; + out.first_fill_ = first_fill; out.last_fill_ = last_fill; + out.evaluation_ordinal_ = evaluation_ordinal; + return out; + } + OrderBirthCause cause() const { return cause_; } + bool from_fill() const { return cause_ == OrderBirthCause::FillEvaluation; } + bool at_terminal_fill() const { return from_fill() && cursor_.terminal_point(); } + int bar() const { return bar_; } + int64_t timestamp() const { return timestamp_; } + const BirthCursor& cursor() const { return cursor_; } + double cursor_price() const { return cursor_price_; } + uint64_t first_fill() const { return first_fill_; } + uint64_t last_fill() const { return last_fill_; } + uint64_t evaluation_ordinal() const { return evaluation_ordinal_; } +private: + static OrderBirth ordinary(OrderBirthCause cause, int bar, int64_t timestamp) { + OrderBirth out; + out.cause_ = cause; out.bar_ = bar; out.timestamp_ = timestamp; + return out; + } + OrderBirthCause cause_ = OrderBirthCause::Unattributed; + int bar_ = -1; + int64_t timestamp_ = 0; + BirthCursor cursor_; + double cursor_price_ = std::numeric_limits::quiet_NaN(); + uint64_t first_fill_ = 0; + uint64_t last_fill_ = 0; + uint64_t evaluation_ordinal_ = 0; +}; + +} // namespace pineforge diff --git a/include/pineforge/pending_order_mirror.hpp b/include/pineforge/pending_order_mirror.hpp index ce672052..0bc79a65 100644 --- a/include/pineforge/pending_order_mirror.hpp +++ b/include/pineforge/pending_order_mirror.hpp @@ -1,5 +1,5 @@ // GENERATED by scripts/gen_pending_order_mirror.py from include/pineforge/engine.hpp -- do not edit. -// 98 PendingOrder members mirrored (108 POD fields incl. struct_version/size). +// 94 PendingOrder members mirrored (128 POD fields incl. struct_version/size). #pragma once #include @@ -39,18 +39,18 @@ typedef struct pf_pending_order_v1_s { int32_t created_bar; int64_t created_seq; uint64_t incarnation; - uint8_t created_by_same_id_replacement; + uint8_t created_by_same_id_replacement; // deprecated, derived output only uint64_t replaced_default_market_incarnation; uint8_t declined_by_replaced_short_market; - uint64_t replaced_exit_order_incarnation; + uint64_t replaced_exit_order_incarnation; // deprecated, derived output only uint64_t recreated_after_named_cancelled_entry_incarnation; uint64_t named_cancel_surviving_exit_incarnation; uint8_t stop_limit_activated; uint8_t coof_suppress_stop_on_entry_bar; uint8_t coof_suppress_limit_on_entry_bar; - uint8_t created_during_coof_recalc; - uint8_t coof_born_at_close_recalc; - uint8_t coof_born_mid_bar; + uint8_t created_during_coof_recalc; // deprecated, derived output only + uint8_t coof_born_at_close_recalc; // deprecated, derived output only + uint8_t coof_born_mid_bar; // deprecated, derived output only int32_t coof_cascade_seg_i; uint8_t coof_cascade_inflight_fires; int32_t created_position_side; @@ -96,12 +96,12 @@ typedef struct pf_pending_order_v1_s { char comment[64]; uint8_t comment_truncated; uint64_t comment_hash64; - uint8_t requested_partial; - uint8_t full_percent_exit_request; + uint8_t requested_partial; // deprecated, derived output only + uint8_t full_percent_exit_request; // deprecated, derived output only uint8_t pooc_global_full_exit_dynamic_qty; uint8_t pooc_global_full_exit_tracks_bound_adds; uint8_t pooc_global_full_exit_bound_add; - uint8_t created_while_in_position; + uint8_t created_while_in_position; // deprecated, derived output only uint8_t sbmt_member; double sbmt_own_qty; double sbmt_tx_qty; @@ -120,6 +120,26 @@ typedef struct pf_pending_order_v1_s { double suppressed_close_consumed_ledger_qty; double suppressed_close_retired_ledger_qty; int32_t short_seed_collision_role; + uint64_t replaced_order_incarnation; + int64_t birth_timestamp; + int32_t birth_cause; + int32_t birth_bar; + int32_t birth_cursor_domain; + int32_t birth_cursor_position; + int32_t birth_cursor_index; + int32_t birth_cursor_count; + double birth_cursor_price; + uint64_t birth_first_fill; + uint64_t birth_last_fill; + uint64_t birth_evaluation_ordinal; + int32_t pine_birth_reach; + uint64_t quantity_intent_kind; + double quantity_intent_units; + double quantity_intent_numerator; + double quantity_intent_denominator; + uint8_t quantity_reservation_present; + double quantity_reservation_units; + double quantity_reservation_basis_units; } pf_pending_order_v1_t; /* One row of the self-describing layout table returned by diff --git a/include/pineforge/quantity_intent.hpp b/include/pineforge/quantity_intent.hpp new file mode 100644 index 00000000..41262fee --- /dev/null +++ b/include/pineforge/quantity_intent.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include +#include + +namespace pineforge { + +enum class QuantityIntentKind { Units, Fraction, All }; + +// A requested amount, before executable quantity is rounded, reserved or +// reduced. A fraction retains its numerator and denominator so a frontend can +// preserve the caller's exact representation (for example 50 / 100). +class QuantityIntent { + struct Units { double amount; }; + struct Fraction { double numerator; double denominator; }; + struct All {}; + using Value = std::variant; +public: + using Kind = QuantityIntentKind; + static QuantityIntent units(double amount) { return QuantityIntent(Units{amount}); } + static QuantityIntent fraction(double numerator, double denominator) { + if (!std::isfinite(denominator) || denominator <= 0.0) + throw std::invalid_argument("quantity fraction requires a positive finite denominator"); + return QuantityIntent(Fraction{numerator, denominator}); + } + static QuantityIntent all() { return QuantityIntent(All{}); } + Kind kind() const { return static_cast(value_.index()); } + double units() const { return std::get(value_).amount; } + double numerator() const { return std::get(value_).numerator; } + double denominator() const { return std::get(value_).denominator; } +private: + explicit QuantityIntent(Value value) : value_(value) {} + Value value_; +}; + +// A reservation is a causal quantity snapshot. Later OCA reductions of the +// executable order do not rewrite its original request or this basis. +struct QuantityReservation { + double units; + double basis_units; +}; + +class QuantityRequest { +public: + const std::optional& intent() const { return intent_; } + const std::optional& reservation() const { return reservation_; } + void request(QuantityIntent intent) { + intent_ = intent; + reservation_.reset(); + } + void reserve(double units, double basis_units) { + if (!intent_) throw std::logic_error("quantity reservation requires an original request"); + reservation_ = QuantityReservation{units, basis_units}; + } + bool requests_all() const { + return intent_ && intent_->kind() == QuantityIntent::Kind::All; + } + // Tolerances belong to the caller's quantity policy. No minimum lot, + // percentage rounding or strategy-specific eligibility lives here. + bool is_partial(double units_tolerance, double fraction_tolerance) const { + if (reservation_) + return reservation_->units < reservation_->basis_units - units_tolerance; + return intent_ && intent_->kind() == QuantityIntent::Kind::Fraction + && intent_->numerator() < intent_->denominator() - fraction_tolerance; + } +private: + // Absent for commands that do not carry this exit-request contract. + std::optional intent_; + std::optional reservation_; +}; + +} // namespace pineforge diff --git a/scripts/check_broker_state_hash_coverage.py b/scripts/check_broker_state_hash_coverage.py index 397ec222..78d35a62 100644 --- a/scripts/check_broker_state_hash_coverage.py +++ b/scripts/check_broker_state_hash_coverage.py @@ -191,6 +191,8 @@ def _class_fields(src: str, name: str) -> dict[str, str]: if ch == ";": decl = " ".join(statement.split()) statement = "" + if re.fullmatch(re.escape(name) + r"\(\)\s*=\s*default", decl): + continue if decl and not decl.startswith("using "): match = re.fullmatch(r"((?:(?:static|constexpr|const)\s+)*[\w:<>]+)\s+(\w+)(?:\s*=\s*[^,]+)?", decl) if not match or match[2] in fields: @@ -339,8 +341,78 @@ def folds(name: str, expression: str) -> list[tuple[str, str]]: raise ValueError("intraday hash block must be unconditional at broker_state_hash function scope") +def _quantity_request_coverage(quantity: str, source: str) -> None: + quantity = _strip_cpp_comments(quantity) + if _class_fields(quantity, "QuantityIntent") != {"value_": "Value"}: + raise ValueError("QuantityIntent data changed; update its complete hash encoding") + if _class_fields(quantity, "QuantityRequest") != { + "intent_": "std::optional", + "reservation_": "std::optional"}: + raise ValueError("QuantityRequest data changed; update its complete hash encoding") + if struct_members(quantity, "Units") != [("double", "amount")]: + raise ValueError("Units intent fields changed") + if struct_members(quantity, "Fraction") != [("double", "numerator"), ("double", "denominator")]: + raise ValueError("Fraction intent fields changed") + if struct_members(quantity, "QuantityReservation") != [("double", "units"), ("double", "basis_units")]: + raise ValueError("QuantityReservation fields changed") + if not re.search(r"struct\s+All\s*\{\s*\}", quantity): + raise ValueError("All intent must have no numeric payload") + if not re.search(r"enum\s+class\s+QuantityIntentKind\s*\{\s*Units,\s*Fraction,\s*All\s*\}", quantity): + raise ValueError("QuantityIntentKind hash encoding changed") + if not re.search(r"using\s+Value\s*=\s*std::variant", quantity): + raise ValueError("QuantityIntent variant discriminator changed") + loop = _collection_loop_body(source, "pending_orders_", "o") + expected = """f.b(o.quantity_request.intent().has_value()); + if (const auto& intent = o.quantity_request.intent()) { + f.i(static_cast(intent->kind())); + if (intent->kind() == QuantityIntent::Kind::Units) f.d(intent->units()); + else if (intent->kind() == QuantityIntent::Kind::Fraction) { + f.d(intent->numerator()); f.d(intent->denominator()); + } + } + f.b(o.quantity_request.reservation().has_value()); + if (const auto& reservation = o.quantity_request.reservation()) { + f.d(reservation->units); f.d(reservation->basis_units); + }""" + compact = re.sub(r"\s+", "", loop) + folded = re.sub(r"\s+", "", expected) + if compact.count(folded) != 1: + raise ValueError("quantity intent/reservation hash requires every field and presence discriminator") + prefix = compact[:compact.index(folded)] + if prefix.count("{") != prefix.count("}"): + raise ValueError("quantity request hash must be unconditional inside its order loop") + +def _birth_coverage(header: str, source: str) -> None: + header = _strip_cpp_comments(header) + expected = { + "BirthCursor": {"domain_": "BirthCursorDomain", "position_": "BirthCursorPosition", "index_": "int", "count_": "int"}, + "OrderBirth": {"cause_": "OrderBirthCause", "bar_": "int", "timestamp_": "int64_t", "cursor_": "BirthCursor", "cursor_price_": "double", "first_fill_": "uint64_t", "last_fill_": "uint64_t", "evaluation_ordinal_": "uint64_t"}, + } + for name, fields in expected.items(): + if _class_fields(header, name) != fields: + raise ValueError(f"{name} fields changed; classify every nested birth fact") + loop = _collection_loop_body(source, "pending_orders_", "o") + compact = re.sub(r"\s+", "", loop) + expressions = [ + "f.i(static_cast(o.birth.cause()));", + "f.i(o.birth.bar());", "f.i(o.birth.timestamp());", + "f.i(static_cast(o.birth.cursor().domain()));", + "f.i(static_cast(o.birth.cursor().position()));", + "f.i(o.birth.cursor().index());", "f.i(o.birth.cursor().count());", + "f.d(o.birth.cursor_price());", "f.u(o.birth.first_fill());", + "f.u(o.birth.last_fill());", "f.u(o.birth.evaluation_ordinal());", + "f.i(static_cast(o.pine_birth_reach));", + ] + expected = "".join(expressions) + if compact.count(expected) != 1: + raise ValueError("birth facts and Pine reach require one complete contiguous hash block") + prefix = compact[:compact.index(expected)] + if prefix.count("{") != prefix.count("}"): + raise ValueError("birth facts must be unconditionally hashed at order-loop scope") + + def _runtime_version_coverage(header: str, source: str, stream: str) -> None: - """The v4 layout and serialized-state contracts must advance together. + """The v5 layout and serialized-state contracts must advance together. Pin the actual hash entry points, rather than accepting a version string mentioned in a comment or an unrelated helper. Public C ABI versions have @@ -348,18 +420,18 @@ def _runtime_version_coverage(header: str, source: str, stream: str) -> None: """ header = _strip_cpp_comments(header) namespaces = re.findall(r"inline\s+namespace\s+(engine_script_run_v\d+)\s*\{", header) - if namespaces != ["engine_script_run_v4"]: - raise ValueError("BacktestEngine layout requires internal namespace engine_script_run_v4") + if namespaces != ["engine_script_run_v5", "engine_script_run_v5"]: + raise ValueError("PendingOrder and BacktestEngine layouts require internal namespace engine_script_run_v5") broker = _one_braced_body(source, r"uint64_t\s+BacktestEngine::broker_state_hash\(\)\s+const\s*\{", "broker hash") - if not re.match(r'\s*Fnv\s+f;\s*f\.s\("pineforge-broker-state/v4"\);', broker): - raise ValueError("broker hash must start with pineforge-broker-state/v4") + if not re.match(r'\s*Fnv\s+f;\s*f\.s\("pineforge-broker-state/v5"\);', broker): + raise ValueError("broker hash must start with pineforge-broker-state/v5") stream_body = _one_braced_body(_strip_cpp_comments(stream), r"uint64_t\s+BacktestEngine::stream_state_hash\(\)\s+const\s*\{", "stream hash") compact = re.sub(r"\s+", "", stream_body) - fold = "integer(4);integer(broker_state_hash());" + fold = "integer(5);integer(broker_state_hash());" if compact.count(fold) != 1: - raise ValueError("stream hash requires version 4 followed by the broker hash") + raise ValueError("stream hash requires version 5 followed by the broker hash") prefix = compact[:compact.index(fold)] if prefix.count("{") != prefix.count("}") or (prefix and prefix[-1] not in ";}"): raise ValueError("stream version fold must be unconditional at function scope") @@ -374,7 +446,9 @@ def main(root: Path = ROOT) -> int: src = _strip_cpp_comments(src_raw) try: _runtime_version_coverage(hpp, src, (root / "src/engine_stream.cpp").read_text()) + _birth_coverage((root / "include/pineforge/order_birth.hpp").read_text(), src) _opening_coverage((root / "include/pineforge/broker_events.hpp").read_text(), src) + _quantity_request_coverage((root / "include/pineforge/quantity_intent.hpp").read_text(), src) _intraday_coverage( (root / "include/pineforge/compat/pine/intraday_cap.hpp").read_text(), (root / "include/pineforge/compat/pine/intraday_order_budget.hpp").read_text(), @@ -390,6 +464,9 @@ def main(root: Path = ROOT) -> int: return 1 waivers = {k: v for k, v in all_waivers.items() if not k.startswith((PENDING_WAIVER_PREFIX, PYRAMID_WAIVER_PREFIX))} + if "pending_order.quantity_request" in all_waivers: + print("check_broker_state_hash_coverage: quantity_request cannot be waived", file=sys.stderr) + return 1 po_waivers = {k[len(PENDING_WAIVER_PREFIX):]: v for k, v in all_waivers.items() if k.startswith(PENDING_WAIVER_PREFIX)} pe_waivers = {k[len(PYRAMID_WAIVER_PREFIX):]: v diff --git a/scripts/check_script_cpp_abi.py b/scripts/check_script_cpp_abi.py index 17ef0b03..32797b35 100644 --- a/scripts/check_script_cpp_abi.py +++ b/scripts/check_script_cpp_abi.py @@ -16,7 +16,7 @@ BASE_COMMIT = "38dc73e5503fe5395458e5f8df2a2ad78054a1ae" BASE_ENGINE_SHA256 = "06c937a1ccd31815ca7775268ac699ffdfddb1a1f19de4628b777f37e9a6d193" -CURRENT_NAMESPACE = "engine_script_run_v4" +CURRENT_NAMESPACE = "engine_script_run_v5" BASE_NAMESPACE = "engine_script_run_v2" FIXTURE = Path(__file__).resolve().parents[1] / "tests/fixtures/script_cpp_abi/base38" @@ -146,6 +146,10 @@ def main(): frozen_headers(cap_include, FIXTURE.parent / "basef864", "f864be590931ba08c8df5af983b33b2c29be9c67", "engine_script_run_v3", "54b35fffaa163a31467f8ba883e44e02f013a37216b558dfe3a4f28fbbe84dc2") + prior_include = root / "basec45/include" + frozen_headers(prior_include, FIXTURE.parent / "basec45", + "c45cf5a4d0e67a2ac098d9066977e1fa21c408a9", "engine_script_run_v4", + "3b4e2937a9b5f275dd119144373b1bf15e433092009500092cd32ea34963b293") common = [args.compiler, "-std=c++17", "-O0", *args.extra_flag] def compile_object(name, source, include): @@ -174,6 +178,32 @@ def compile_object(name, source, include): cap_symbols = compile_object("basef864_symbol_control", BASE_SYMBOL_CONTROL.replace("engine_script_run_v2", "engine_script_run_v3"), cap_include) + prior_native = compile_object("basec45_native", caller("engine_script_run_v4"), prior_include) + prior_generated = compile_object("basec45_generated", caller("engine_script_run_v4", True), prior_include) + prior_symbols = compile_object("basec45_symbol_control", + BASE_SYMBOL_CONTROL.replace("engine_script_run_v2", "engine_script_run_v4"), prior_include) + + priority_caller = """#include +#include +int main() { + pineforge::compat::pine::OrderPriority policy; + policy.attach(); + pineforge::compat::pine::OrderPriorityContext context{}; + std::vector orders(2); + return policy.select(context, orders).has_value() ? 1 : 0; +} +""" + priority_symbols = """#include +#include +namespace pineforge::compat::pine { +std::optional OrderPriority::select( + const OrderPriorityContext&, const std::vector&) const { return std::nullopt; } +} +""" + current_priority = compile_object("current_pending_priority", priority_caller, args.include) + prior_priority = compile_object("basec45_pending_priority", priority_caller, prior_include) + prior_priority_symbols = compile_object("basec45_pending_priority_symbols", priority_symbols, prior_include) + def link(name, obj, runtime, missing_namespace=None): linked = subprocess.run( [*common, str(obj), str(runtime), "-pthread", "-o", str(root / name)], @@ -215,7 +245,26 @@ def link(name, obj, runtime, missing_namespace=None): link("basef864_generated_to_current", cap_generated, args.library, "engine_script_run_v3") link("current_native_to_v3_symbol_control", current_native, cap_symbols, CURRENT_NAMESPACE) link("current_generated_to_v3_symbol_control", current_generated, cap_symbols, CURRENT_NAMESPACE) - print("9 translation units compiled; 6 positive links; 9 rejected links; no executable run") + link("basec45_native_to_v4_symbol_control", prior_native, prior_symbols) + link("basec45_generated_to_v4_symbol_control", prior_generated, prior_symbols) + link("basec45_native_to_current", prior_native, args.library, "engine_script_run_v4") + link("basec45_generated_to_current", prior_generated, args.library, "engine_script_run_v4") + link("current_native_to_v4_symbol_control", current_native, prior_symbols, CURRENT_NAMESPACE) + link("current_generated_to_v4_symbol_control", current_generated, prior_symbols, CURRENT_NAMESPACE) + link("current_pending_priority_to_current", current_priority, args.library) + link("basec45_pending_priority_to_v4_symbols", prior_priority, prior_priority_symbols) + for name, obj, runtime, expected in [ + ("basec45_pending_priority_to_current", prior_priority, args.library, "pineforge::PendingOrder"), + ("current_pending_priority_to_v4_symbols", current_priority, prior_priority_symbols, + "pineforge::engine_script_run_v5::PendingOrder"), + ]: + result = subprocess.run([*common, str(obj), str(runtime), "-pthread", "-o", str(root / name)], + capture_output=True, text=True, timeout=60) + if (result.returncode == 0 or "undefined" not in result.stderr.lower() + or "OrderPriority::select(" not in result.stderr or expected not in result.stderr): + raise RuntimeError(name + " did not reject the expected PendingOrder type: " + result.stderr) + print(name + ": rejected stale standalone PendingOrder argument type (not executed)") + print("15 translation units compiled; 10 positive links; 15 rejected links; no executable run") if __name__ == "__main__": diff --git a/scripts/gen_pending_order_mirror.py b/scripts/gen_pending_order_mirror.py index 57913243..4d127ad7 100644 --- a/scripts/gen_pending_order_mirror.py +++ b/scripts/gen_pending_order_mirror.py @@ -30,6 +30,7 @@ from __future__ import annotations import re +import json import sys from pathlib import Path @@ -55,6 +56,45 @@ "OrderType": ("int32_t", "(int32_t)src.{m}"), "PositionSide": ("int32_t", "(int32_t)src.{m}"), "ShortSeedCollisionRole": ("int32_t", "(int32_t)src.{m}"), + "PineHistoricalBirthReach": ("int32_t", "(int32_t)src.{m}"), +} +# Public v1 is append-only. Removed native fields survive only as one-way +# deprecated output projections at their original offsets. +LEGACY_OUTPUTS = { + "created_during_coof_recalc": "src.birth.from_fill() ? 1 : 0", + "coof_born_at_close_recalc": "src.birth.at_terminal_fill() ? 1 : 0", + "coof_born_mid_bar": "compat::pine::historical_cascade_reach(src) ? 1 : 0", + "created_by_same_id_replacement": "src.type != OrderType::RAW_ORDER && src.replaced_order_incarnation != 0 ? 1 : 0", + "replaced_exit_order_incarnation": "src.type == OrderType::EXIT ? src.replaced_order_incarnation : 0", + "created_while_in_position": "src.type == OrderType::EXIT && src.created_position_side != PositionSide::FLAT ? 1 : 0", + "requested_partial": "src.quantity_request.is_partial(1e-9, 1e-9) ? 1 : 0", + "full_percent_exit_request": "src.quantity_request.requests_all() ? 1 : 0", +} +COMPOSITE_MAP = { + "QuantityRequest": [ + ("intent_kind", "uint64_t", "src.{m}.intent() ? static_cast(src.{m}.intent()->kind()) + 1 : 0"), + ("intent_units", "double", "src.{m}.intent() && src.{m}.intent()->kind() == QuantityIntent::Kind::Units ? src.{m}.intent()->units() : 0.0"), + ("intent_numerator", "double", "src.{m}.intent() && src.{m}.intent()->kind() == QuantityIntent::Kind::Fraction ? src.{m}.intent()->numerator() : 0.0"), + ("intent_denominator", "double", "src.{m}.intent() && src.{m}.intent()->kind() == QuantityIntent::Kind::Fraction ? src.{m}.intent()->denominator() : 0.0"), + ("reservation_present", "uint8_t", "src.{m}.reservation().has_value() ? 1 : 0"), + ("reservation_units", "double", "src.{m}.reservation() ? src.{m}.reservation()->units : 0.0"), + ("reservation_basis_units", "double", "src.{m}.reservation() ? src.{m}.reservation()->basis_units : 0.0"), + ], + "OrderBirth": [ + # Start appended facts at the v1 struct's 8-byte boundary, preserving + # its trailing padding as well as all 108 field offsets. + ("timestamp", "int64_t", "src.{m}.timestamp()"), + ("cause", "int32_t", "(int32_t)src.{m}.cause()"), + ("bar", "int32_t", "src.{m}.bar()"), + ("cursor_domain", "int32_t", "(int32_t)src.{m}.cursor().domain()"), + ("cursor_position", "int32_t", "(int32_t)src.{m}.cursor().position()"), + ("cursor_index", "int32_t", "src.{m}.cursor().index()"), + ("cursor_count", "int32_t", "src.{m}.cursor().count()"), + ("cursor_price", "double", "src.{m}.cursor_price()"), + ("first_fill", "uint64_t", "src.{m}.first_fill()"), + ("last_fill", "uint64_t", "src.{m}.last_fill()"), + ("evaluation_ordinal", "uint64_t", "src.{m}.evaluation_ordinal()"), + ], } STRING_TYPES = frozenset({"std::string"}) @@ -159,7 +199,7 @@ def classify(ms: list[tuple[str, str]], waivers: dict[str, str]): for t, n in ms: if n in waivers: waived.append((t, n, waivers[n])) - elif t in STRING_TYPES or t in TYPE_MAP: + elif t in STRING_TYPES or t in TYPE_MAP or t in COMPOSITE_MAP: mirrored.append((t, n)) else: _fail(f"member {n} has unmapped type {t}; add it to TYPE_MAP or " @@ -172,8 +212,27 @@ def generate() -> tuple[str, str]: fields: list[str] = [] copies: list[str] = [] descs: list[tuple[str, str]] = [("struct_version", "uint32_t"), ("size", "uint32_t")] - for t, m in mirrored: - if t in STRING_TYPES: + prefix = json.loads((ROOT / "scripts/pending_order_v1_prefix.json").read_text())["members"] + native = dict((name, kind) for kind, name in mirrored) + for kind, name in prefix: + if name not in LEGACY_OUTPUTS and native.get(name) != kind: + _fail(f"public v1 prefix member {name} needs an explicit derived projection") + prefix_names = {name for _, name in prefix} + ordered = prefix + [(kind, name) for kind, name in mirrored if name not in prefix_names] + for t, m in ordered: + if m in LEGACY_OUTPUTS: + ct = TYPE_MAP[t][0] + fields.append(f" {ct} {m}; // deprecated, derived output only") + copies.append(f" out->{m} = {LEGACY_OUTPUTS[m]};") + descs.append((m, ct)) + elif t in COMPOSITE_MAP: + for suffix, ct, expr in COMPOSITE_MAP[t]: + prefix = "quantity" if t == "QuantityRequest" else m + name = f"{prefix}_{suffix}" + fields.append(f" {ct} {name};") + copies.append(f" out->{name} = {expr.format(m=m)};") + descs.append((name, ct)) + elif t in STRING_TYPES: fields += [f" char {m}[{STR_CAP}];", f" uint8_t {m}_truncated;", f" uint64_t {m}_hash64;"] diff --git a/scripts/pending_order_v1_prefix.json b/scripts/pending_order_v1_prefix.json new file mode 100644 index 00000000..6d9e0b3d --- /dev/null +++ b/scripts/pending_order_v1_prefix.json @@ -0,0 +1,397 @@ +{ + "source_commit": "c45cf5a4d0e67a2ac098d9066977e1fa21c408a9", + "members": [ + [ + "std::string", + "id" + ], + [ + "std::string", + "from_entry" + ], + [ + "OrderType", + "type" + ], + [ + "bool", + "is_long" + ], + [ + "double", + "limit_price" + ], + [ + "double", + "stop_price" + ], + [ + "double", + "trail_points" + ], + [ + "double", + "trail_price" + ], + [ + "double", + "trail_offset" + ], + [ + "double", + "profit_ticks" + ], + [ + "double", + "loss_ticks" + ], + [ + "double", + "qty" + ], + [ + "int", + "qty_type" + ], + [ + "double", + "qty_percent" + ], + [ + "std::string", + "oca_name" + ], + [ + "int", + "oca_type" + ], + [ + "int", + "created_bar" + ], + [ + "int64_t", + "created_seq" + ], + [ + "uint64_t", + "incarnation" + ], + [ + "bool", + "created_by_same_id_replacement" + ], + [ + "uint64_t", + "replaced_default_market_incarnation" + ], + [ + "bool", + "declined_by_replaced_short_market" + ], + [ + "uint64_t", + "replaced_exit_order_incarnation" + ], + [ + "uint64_t", + "recreated_after_named_cancelled_entry_incarnation" + ], + [ + "uint64_t", + "named_cancel_surviving_exit_incarnation" + ], + [ + "bool", + "stop_limit_activated" + ], + [ + "bool", + "coof_suppress_stop_on_entry_bar" + ], + [ + "bool", + "coof_suppress_limit_on_entry_bar" + ], + [ + "bool", + "created_during_coof_recalc" + ], + [ + "bool", + "coof_born_at_close_recalc" + ], + [ + "bool", + "coof_born_mid_bar" + ], + [ + "int8_t", + "coof_cascade_seg_i" + ], + [ + "bool", + "coof_cascade_inflight_fires" + ], + [ + "PositionSide", + "created_position_side" + ], + [ + "int64_t", + "created_position_cycle_seq" + ], + [ + "bool", + "created_after_position_close_in_bar" + ], + [ + "bool", + "over_pyramiding_cap_at_placement" + ], + [ + "int", + "same_id_stop_deferred_close_all_bar" + ], + [ + "uint64_t", + "same_id_stop_deferred_close_all_incarnation" + ], + [ + "bool", + "reverses_same_bar_market_from_flat" + ], + [ + "bool", + "paired_flat_market_candidate" + ], + [ + "double", + "paired_flat_market_own_qty" + ], + [ + "double", + "paired_flat_market_signal_close" + ], + [ + "double", + "paired_flat_market_signal_equity" + ], + [ + "double", + "paired_flat_market_signal_margin_pct" + ], + [ + "double", + "paired_flat_market_signal_pointvalue" + ], + [ + "double", + "paired_flat_market_signal_fx" + ], + [ + "int64_t", + "paired_flat_market_peer_seq" + ], + [ + "double", + "paired_flat_market_transaction_qty" + ], + [ + "bool", + "default_flat_market_gross_candidate" + ], + [ + "double", + "tv_carry_qty" + ], + [ + "double", + "frozen_default_qty" + ], + [ + "double", + "default_stop_placement_qty" + ], + [ + "double", + "default_stop_placement_equity" + ], + [ + "double", + "default_stop_placement_signal_close" + ], + [ + "double", + "default_stop_sizing_price" + ], + [ + "double", + "sizing_equity" + ], + [ + "double", + "sizing_price" + ], + [ + "double", + "sizing_fx" + ], + [ + "double", + "sizing_mark" + ], + [ + "bool", + "opening_affordability_exemption_candidate" + ], + [ + "bool", + "explicit_flat_admission_candidate" + ], + [ + "double", + "explicit_placement_equity" + ], + [ + "double", + "explicit_slipped_signal_close" + ], + [ + "double", + "affordability_placement_equity" + ], + [ + "double", + "affordability_signal_price" + ], + [ + "double", + "affordability_held_qty" + ], + [ + "bool", + "affordability_close_only" + ], + [ + "bool", + "rounded_signal_cost_close_only" + ], + [ + "int", + "signal_close_mc_bar" + ], + [ + "uint64_t", + "signal_close_mc_entry_incarnation" + ], + [ + "uint64_t", + "signal_close_mc_fill_seq" + ], + [ + "double", + "signal_close_mc_remaining_qty" + ], + [ + "std::string", + "comment" + ], + [ + "bool", + "requested_partial" + ], + [ + "bool", + "full_percent_exit_request" + ], + [ + "bool", + "pooc_global_full_exit_dynamic_qty" + ], + [ + "bool", + "pooc_global_full_exit_tracks_bound_adds" + ], + [ + "bool", + "pooc_global_full_exit_bound_add" + ], + [ + "bool", + "created_while_in_position" + ], + [ + "bool", + "sbmt_member" + ], + [ + "double", + "sbmt_own_qty" + ], + [ + "double", + "sbmt_tx_qty" + ], + [ + "bool", + "sbmt_kept_over_cap" + ], + [ + "double", + "sbmt_close_qty" + ], + [ + "bool", + "sbmt_close_buy" + ], + [ + "bool", + "suppress_as_declined_reversal_close" + ], + [ + "bool", + "dormant_bracket" + ], + [ + "bool", + "dormant_reissue_pending" + ], + [ + "double", + "dormant_original_stop_price" + ], + [ + "int", + "dormant_hold_bar" + ], + [ + "int", + "dormant_reversal_kill_bar" + ], + [ + "double", + "dormant_trail_best" + ], + [ + "double", + "dormant_trail_best_start" + ], + [ + "bool", + "dormant_trail_leg_dead" + ], + [ + "double", + "suppressed_close_consumed_ledger_qty" + ], + [ + "double", + "suppressed_close_retired_ledger_qty" + ], + [ + "ShortSeedCollisionRole", + "short_seed_collision_role" + ] + ] +} diff --git a/scripts/test_broker_state_hash_coverage.py b/scripts/test_broker_state_hash_coverage.py index 462bdcfc..07b450f4 100644 --- a/scripts/test_broker_state_hash_coverage.py +++ b/scripts/test_broker_state_hash_coverage.py @@ -15,7 +15,9 @@ ROOT = Path(__file__).resolve().parents[1] HEADER = (ROOT / "include/pineforge/engine.hpp").read_text() +QUANTITY = (ROOT / "include/pineforge/quantity_intent.hpp").read_text() EVENTS = (ROOT / "include/pineforge/broker_events.hpp").read_text() +BIRTH = (ROOT / "include/pineforge/order_birth.hpp").read_text() INTRADAY = (ROOT / "include/pineforge/compat/pine/intraday_order_budget.hpp").read_text() POLICY = (ROOT / "include/pineforge/compat/pine/intraday_cap.hpp").read_text() OBLIGATION = (ROOT / "include/pineforge/position_close_obligation.hpp").read_text() @@ -26,12 +28,14 @@ class PhysicalLotCoverage(unittest.TestCase): def check(self, header=HEADER, source=SOURCE, waivers=WAIVERS, events=EVENTS, - intraday=INTRADAY, policy=POLICY, obligation=OBLIGATION, stream=STREAM): + intraday=INTRADAY, policy=POLICY, obligation=OBLIGATION, stream=STREAM, quantity=QUANTITY, birth=BIRTH): with tempfile.TemporaryDirectory(prefix="pf-lot-hash-check-") as temp: root = Path(temp) for name, content in [ ("include/pineforge/engine.hpp", header), ("include/pineforge/broker_events.hpp", events), + ("include/pineforge/quantity_intent.hpp", quantity), + ("include/pineforge/order_birth.hpp", birth), ("include/pineforge/compat/pine/intraday_order_budget.hpp", intraday), ("include/pineforge/compat/pine/intraday_cap.hpp", policy), ("include/pineforge/position_close_obligation.hpp", obligation), @@ -56,26 +60,51 @@ def test_actual_source_and_named_parser(self): self.assertEqual(len(members(HEADER, "PyramidEntry")), 18) self.assertEqual(members(HEADER), members(HEADER, "PendingOrder")) - def test_layout_and_hash_versions_must_match_v4_contract(self): - self.assertIn("engine_script_run_v4", HEADER) - self.assertIn('f.s("pineforge-broker-state/v4");', SOURCE) - self.assertIn("integer(4); integer(broker_state_hash());", STREAM) - self.assertEqual(self.check(header=HEADER.replace("engine_script_run_v4", "engine_script_run_v2"))[0], 1) + def test_quantity_request_presence_and_every_value_are_hashed(self): + for fold in ["f.b(o.quantity_request.intent().has_value());", + "f.i(static_cast(intent->kind()));", + "f.d(intent->units());", "f.d(intent->numerator());", + "f.d(intent->denominator());", + "f.b(o.quantity_request.reservation().has_value());", + "f.d(reservation->units);", "f.d(reservation->basis_units);"]: + with self.subTest(fold=fold): + self.assertIn(fold, SOURCE) + self.assertEqual(self.check(source=SOURCE.replace(fold, ""))[0], 1) + self.assertEqual(self.check(waivers=WAIVERS + + "\npending_order.quantity_request # attempted omission\n")[0], 1) + + def test_quantity_model_additions_and_variant_drift_fail_closed(self): + for old, new in [ + ("struct Units { double amount; };", "struct Units { double amount; double extra; };"), + ("double basis_units;", "double basis_units; double extra;"), + ("Units, Fraction, All", "Units, All, Fraction"), + ("Value value_;", "Value value_; double extra_;"), + ("std::optional intent_;", "std::optional intent_; double extra_;"), + ]: + with self.subTest(old=old): + self.assertIn(old, QUANTITY) + self.assertEqual(self.check(quantity=QUANTITY.replace(old, new))[0], 1) + + def test_layout_and_hash_versions_must_match_v5_contract(self): + self.assertIn("engine_script_run_v5", HEADER) + self.assertIn('f.s("pineforge-broker-state/v5");', SOURCE) + self.assertIn("integer(5); integer(broker_state_hash());", STREAM) + self.assertEqual(self.check(header=HEADER.replace("engine_script_run_v5", "engine_script_run_v2"))[0], 1) for replacement in ['f.s("pineforge-broker-state/v2");', '', - '// f.s("pineforge-broker-state/v4");']: + '// f.s("pineforge-broker-state/v5");']: self.assertEqual(self.check(source=SOURCE.replace( - 'f.s("pineforge-broker-state/v4");', replacement))[0], 1) + 'f.s("pineforge-broker-state/v5");', replacement))[0], 1) for replacement in ["integer(2); integer(broker_state_hash());", "integer(broker_state_hash());", - "if (false) { integer(4); integer(broker_state_hash()); }"]: + "if (false) { integer(5); integer(broker_state_hash()); }"]: self.assertEqual(self.check(stream=STREAM.replace( - "integer(4); integer(broker_state_hash());", replacement))[0], 1) + "integer(5); integer(broker_state_hash());", replacement))[0], 1) def test_version_folds_in_unrelated_helpers_do_not_cover_entry_points(self): - broker_fold = 'f.s("pineforge-broker-state/v4");' + broker_fold = 'f.s("pineforge-broker-state/v5");' altered = SOURCE.replace(broker_fold, '') + '\nvoid other() { ' + broker_fold + ' }\n' self.assertEqual(self.check(source=altered)[0], 1) - stream_fold = "integer(4); integer(broker_state_hash());" + stream_fold = "integer(5); integer(broker_state_hash());" altered = STREAM.replace(stream_fold, '') + '\nvoid other() { ' + stream_fold + ' }\n' self.assertEqual(self.check(stream=altered)[0], 1) @@ -133,6 +162,20 @@ def test_unclassified_nested_declaration_refuses(self): header = HEADER.replace("struct PyramidEntry {", "struct PyramidEntry {\n " + declaration) self.assertNotEqual(self.check(header=header)[0], 0) + def test_complete_birth_hash_block_cannot_be_conditional(self): + start = SOURCE.index(" f.i(static_cast(o.birth.cause()));") + end = SOURCE.index(" f.i(static_cast(o.pine_birth_reach));", start) + end += len(" f.i(static_cast(o.pine_birth_reach));") + block = SOURCE[start:end] + self.assertEqual(self.check(source=SOURCE[:start] + "if (false) {\n" + block + "\n}" + SOURCE[end:])[0], 1) + + def test_birth_fields_and_nested_cursor_cannot_escape_hash_coverage(self): + for fold in ["f.u(o.birth.first_fill());", "f.i(o.birth.cursor().index());", + "f.i(static_cast(o.pine_birth_reach));"]: + self.assertNotEqual(self.check(source=SOURCE.replace(fold, ""))[0], 0) + altered = BIRTH.replace(" int count_ = 0;", " int count_ = 0;\n int hidden_cursor_fact = 0;") + self.assertNotEqual(self.check(birth=altered)[0], 0) + def test_pending_order_coverage_still_refuses_omission(self): source = SOURCE.replace("f.d(o.stop_price);", "") self.assertNotEqual(source, SOURCE) diff --git a/src/compat/pine/order_birth.cpp b/src/compat/pine/order_birth.cpp new file mode 100644 index 00000000..9680bfa7 --- /dev/null +++ b/src/compat/pine/order_birth.cpp @@ -0,0 +1,23 @@ +#include +#include + +namespace pineforge::compat::pine { + +HistoricalBirthReach select_historical_birth_reach(const OrderBirth& birth, + bool requested_trailing_exit) { + if (!birth.from_fill() || first_open_fill_evaluation(birth)) + return HistoricalBirthReach::Standard; + // Existing later-same-open trailing-exit exception: the origin is still + // the later fill callback at O. Only its Pine historical reach differs. + const bool later_open_trailing_exit = + birth.cursor().domain() == BirthCursorDomain::HistoricalPath + && birth.cursor().first_point() && requested_trailing_exit; + return later_open_trailing_exit ? HistoricalBirthReach::Standard + : HistoricalBirthReach::ExtremeWaypoints; +} + +bool historical_cascade_reach(const PendingOrder& order) { + return order.pine_birth_reach == HistoricalBirthReach::ExtremeWaypoints; +} + +} // namespace pineforge::compat::pine diff --git a/src/compat/pine/order_priority.cpp b/src/compat/pine/order_priority.cpp index ae6d4f5c..8e7536ec 100644 --- a/src/compat/pine/order_priority.cpp +++ b/src/compat/pine/order_priority.cpp @@ -30,7 +30,7 @@ std::optional OrderPriority::select( const bool parent_is_exact_fresh_stop = parent->type == OrderType::ENTRY && parent->created_position_side == PositionSide::FLAT - && !parent->created_by_same_id_replacement + && (parent->replaced_order_incarnation == 0) && cancelled_incarnation != 0 && cancelled_incarnation < parent->incarnation && cancelled_incarnation != child->incarnation @@ -38,7 +38,7 @@ std::optional OrderPriority::select( && surviving_exit_incarnation < parent->incarnation && parent->created_bar == ctx.bar_index - 1 && std::isnan(parent->qty) - && !parent->created_during_coof_recalc + && !parent->birth.from_fill() && !parent->created_after_position_close_in_bar && !parent->over_pyramiding_cap_at_placement && !parent->stop_limit_activated @@ -54,15 +54,14 @@ std::optional OrderPriority::select( const bool child_is_exact_retained_bracket = child->type == OrderType::EXIT && !child->from_entry.empty() - && child->created_by_same_id_replacement - && child->replaced_exit_order_incarnation + && (child->replaced_order_incarnation != 0) + && child->replaced_order_incarnation == surviving_exit_incarnation - && !child->created_while_in_position && child->created_position_side == PositionSide::FLAT && child->created_bar == ctx.bar_index - 1 - && !child->created_during_coof_recalc + && !child->birth.from_fill() && !child->created_after_position_close_in_bar - && !child->requested_partial + && !child->quantity_request.is_partial(1e-9, 1e-9) && std::isnan(child->qty) && child_qp >= 100.0 - 1e-9 && std::isfinite(child->stop_price) diff --git a/src/engine_fills.cpp b/src/engine_fills.cpp index 7eae1107..e124c43c 100644 --- a/src/engine_fills.cpp +++ b/src/engine_fills.cpp @@ -534,7 +534,7 @@ BacktestEngine::CoofFillResult BacktestEngine::process_next_pending_order( traversed.low = std::min(bar.open, cursor_price); traversed.close = cursor_price; for (PendingOrder& pending : pending_orders_) { - if (pending.coof_born_at_close_recalc + if (pending.birth.at_terminal_fill() && pending.created_bar == bar_index_) { continue; } @@ -598,7 +598,7 @@ BacktestEngine::CoofFillResult BacktestEngine::process_next_pending_order( // its own tick model and is scoped out. coof_cascade_force_wp_gap_ = false; if (!bar_magnifier_enabled_ && coof_scheduler_active_ - && order.coof_born_mid_bar + && compat::pine::historical_cascade_reach(order) && order.created_bar == bar_index_) { // Model S governs only PRICED (stop/limit, non-trail) // strategy.exit cascade orders — the class the probe pinned. @@ -665,8 +665,8 @@ BacktestEngine::CoofFillResult BacktestEngine::process_next_pending_order( && position_open_bar_ >= 0 && position_open_bar_ < bar_index_ && position_entry_count_ == 1 && pyramid_entries_.size() == 1 && pyramiding_ == 0 && order.type == OrderType::EXIT - && order.created_bar < bar_index_ && !order.requested_partial - && order.created_while_in_position && !order.dormant_bracket + && order.created_bar < bar_index_ && !order.quantity_request.is_partial(kFullQtyEps, kFullPercentEps) + && (order.created_position_side != PositionSide::FLAT) && !order.dormant_bracket && !order.from_entry.empty() && order.from_entry == pyramid_entries_.front().entry_id && std::isnan(order.trail_points) && std::isnan(order.trail_price) @@ -798,7 +798,7 @@ BacktestEngine::CoofFillResult BacktestEngine::process_next_pending_order( // siblings, invalidating references into the pending-order vector. const bool fresh_coof_market_entry = pending_orders_[order_index].type == OrderType::MARKET - && pending_orders_[order_index].created_during_coof_recalc + && pending_orders_[order_index].birth.from_fill() && pending_orders_[order_index].created_bar == bar_index_ && side_before_fill == PositionSide::FLAT; const uint64_t opening_incarnation = candidate.order.incarnation; @@ -1226,9 +1226,8 @@ void BacktestEngine::process_short_margin_before_script(const Bar& bar) { double pending_touch = 0.0; if (order.created_bar >= bar_index_ || order.created_position_side != PositionSide::FLAT - || order.created_while_in_position || order.created_after_position_close_in_bar - || order.created_during_coof_recalc + || order.birth.from_fill() || !std::isfinite(order.stop_price) || !std::isnan(order.limit_price) || order.stop_limit_activated @@ -2226,7 +2225,7 @@ bool BacktestEngine::tv_money_long_margin_call(const Bar& bar, && pending.created_position_side == PositionSide::LONG && pending.created_position_cycle_seq == close_mc_cycle && !pending.created_after_position_close_in_bar - && !pending.created_during_coof_recalc + && !pending.birth.from_fill() && pending.tv_carry_qty == qty && std::isfinite(pending.frozen_default_qty)) { pending.signal_close_mc_bar = bar_index_; @@ -2297,7 +2296,7 @@ void BacktestEngine::revive_position_brackets_after_margin_call_partial( // remainder next bar @214.68. const bool full_pct = std::isnan(o.qty) ? o.qty_percent >= 100.0 - internal::kFullPercentEps - : (!o.requested_partial + : (!o.quantity_request.is_partial(kFullQtyEps, kFullPercentEps) || o.qty >= position_qty_ - kQtyEpsilon); if (!full_pct || std::isnan(revive_stop) || !std::isfinite(mc_price)) continue; @@ -3262,9 +3261,9 @@ void BacktestEngine::finalize_default_flat_market_gross_admission() { && std::isnan(other.trail_points) && std::isnan(other.trail_price) && std::isnan(other.trail_offset) - && !other.created_during_coof_recalc - && !other.coof_born_at_close_recalc - && !other.coof_born_mid_bar; + && !other.birth.from_fill() + && !other.birth.at_terminal_fill() + && !compat::pine::historical_cascade_reach(other); if (!same_bar_market_close) { consume_source_tombstones(); return; @@ -3297,10 +3296,10 @@ void BacktestEngine::finalize_default_flat_market_gross_admission() { && order.created_bar == source_bar && order.incarnation > 0 && order.created_seq > 0 - && !order.created_by_same_id_replacement - && !order.created_during_coof_recalc - && !order.coof_born_at_close_recalc - && !order.coof_born_mid_bar + && (order.replaced_order_incarnation == 0) + && !order.birth.from_fill() + && !order.birth.at_terminal_fill() + && !compat::pine::historical_cascade_reach(order) && std::isfinite(order.sizing_equity) && order.sizing_equity > 0.0 && std::isfinite(order.sizing_mark) @@ -3469,12 +3468,12 @@ void BacktestEngine::apply_pooc_coof_explicit_flat_market_gross_admission() { && order.oca_name.empty() && order.created_bar == bar_index_ && order.incarnation > 0 - && !order.created_by_same_id_replacement + && (order.replaced_order_incarnation == 0) && order.created_position_side == PositionSide::FLAT && !order.created_after_position_close_in_bar - && !order.created_during_coof_recalc - && !order.coof_born_at_close_recalc - && !order.coof_born_mid_bar + && !order.birth.from_fill() + && !order.birth.at_terminal_fill() + && !compat::pine::historical_cascade_reach(order) && std::isfinite(order.explicit_placement_equity) && order.explicit_placement_equity > 0.0 && std::isfinite(order.explicit_slipped_signal_close) @@ -3758,7 +3757,7 @@ void BacktestEngine::sort_orders_by_fill_phase(const Bar& bar) { && !order.id.empty() && order.created_position_side == PositionSide::FLAT && order.created_bar < bar_index_ - && !order.created_during_coof_recalc + && !order.birth.from_fill() && std::isfinite(order.limit_price) && std::isnan(order.stop_price) && std::isnan(order.trail_points) @@ -3793,9 +3792,8 @@ void BacktestEngine::sort_orders_by_fill_phase(const Bar& bar) { // parent filled AT the open already shared the open phase. const bool exact_relative_child = order.type == OrderType::EXIT - && !order.created_while_in_position && order.created_position_side == PositionSide::FLAT - && !order.created_during_coof_recalc + && !order.birth.from_fill() && exit_children_by_parent[order.from_entry] == 1 && order.created_bar >= parent->second.created_bar && parent->second.created_seq < order.created_seq @@ -3871,13 +3869,12 @@ void BacktestEngine::sort_orders_by_fill_phase(const Bar& bar) { return order.created_bar == source_bar && source_bar + 1 == bar_index_ && order.created_position_side == PositionSide::SHORT - && !order.created_by_same_id_replacement - && order.replaced_exit_order_incarnation == 0 + && (order.replaced_order_incarnation == 0) && order.recreated_after_named_cancelled_entry_incarnation == 0 && order.named_cancel_surviving_exit_incarnation == 0 - && !order.created_during_coof_recalc - && !order.coof_born_at_close_recalc - && !order.coof_born_mid_bar + && !order.birth.from_fill() + && !order.birth.at_terminal_fill() + && !compat::pine::historical_cascade_reach(order) && order.oca_name.empty() && order.oca_type == 0; }; @@ -3912,8 +3909,7 @@ void BacktestEngine::sort_orders_by_fill_phase(const Bar& bar) { && std::isnan(order.trail_offset) && std::isnan(order.profit_ticks) && std::isnan(order.loss_ticks) - && !order.created_after_position_close_in_bar - && !order.created_while_in_position; + && !order.created_after_position_close_in_bar; }; const auto exact_full_fifo_close_short = [&](const PendingOrder& order, const std::string& held_id) { @@ -3921,8 +3917,8 @@ void BacktestEngine::sort_orders_by_fill_phase(const Bar& bar) { && order.id == "__close__" + held_id && order.from_entry.empty() && !order.is_long - && order.created_while_in_position - && !order.requested_partial + && (order.created_position_side != PositionSide::FLAT) + && !order.quantity_request.is_partial(kFullQtyEps, kFullPercentEps) && std::isnan(order.qty) && std::abs(order.qty_percent - 100.0) <= kFullPercentEps && std::isnan(order.limit_price) @@ -4614,8 +4610,8 @@ bool BacktestEngine::prearmed_market_parent_bracket_gaps_at_open( || order.type != OrderType::EXIT || order.from_entry.empty() || order.created_bar != bar_index_ - 1 - || order.created_during_coof_recalc - || order.requested_partial + || order.birth.from_fill() + || order.quantity_request.is_partial(kFullQtyEps, kFullPercentEps) || order.qty_percent < 100.0 - kFullPercentEps || (!std::isfinite(order.stop_price) && !std::isfinite(order.limit_price))) { @@ -5772,7 +5768,7 @@ void BacktestEngine::apply_filled_order_to_state( && position_side_ == PositionSide::FLAT && (order.created_position_side == PositionSide::FLAT || order.created_after_position_close_in_bar) - && !order.created_by_same_id_replacement + && (order.replaced_order_incarnation == 0) && order.created_bar == bar_index_ - 1 && sole_opening_after_closes() && default_qty_type_ == QtyType::PERCENT_OF_EQUITY && default_qty_value_ == 100 @@ -5846,7 +5842,7 @@ void BacktestEngine::apply_filled_order_to_state( && !process_orders_on_close_ && !calc_on_order_fills_ && !bar_magnifier_enabled_ && !coof_scheduler_active_ && !stream_warmup_mode_ && stream_phase_ == StreamPhase::IDLE - && !order.created_during_coof_recalc + && !order.birth.from_fill() && !order.created_after_position_close_in_bar && std::isfinite(order.sizing_equity) && std::isfinite(order.frozen_default_qty) @@ -6181,7 +6177,7 @@ void BacktestEngine::apply_filled_order_to_state( && position_side_ == PositionSide::FLAT && order.created_position_side == PositionSide::FLAT && !order.created_after_position_close_in_bar - && !order.created_by_same_id_replacement + && (order.replaced_order_incarnation == 0) // FIXED/no-fee orders carry the transaction marker even // when no sibling exists. Exclude an expanded transaction, // not an otherwise-unused default sizing declaration. @@ -6387,7 +6383,7 @@ void BacktestEngine::apply_filled_order_to_state( || child.created_bar != order.created_bar || child.suppress_as_declined_reversal_close || !actionable - || child.requested_partial + || child.quantity_request.is_partial(kFullQtyEps, kFullPercentEps) || !std::isnan(child.qty) || qp < 100.0 - kFullPercentEps) { continue; @@ -6486,8 +6482,8 @@ void BacktestEngine::apply_filled_order_to_state( && order.created_bar == bar_index_ && order.created_position_side == PositionSide::FLAT && !order.created_after_position_close_in_bar - && !order.created_during_coof_recalc - && !order.created_by_same_id_replacement + && !order.birth.from_fill() + && (order.replaced_order_incarnation == 0) && order.oca_name.empty() && order.oca_type == 0 && position_side_before_fill == PositionSide::FLAT && order.incarnation != 0 && pyramid_entries_.size() == 1 @@ -6519,8 +6515,7 @@ void BacktestEngine::apply_filled_order_to_state( && !process_orders_on_close_ && !calc_on_order_fills_ && !bar_magnifier_enabled_ && !coof_scheduler_active_ && !stream_warmup_mode_ && stream_phase_ == StreamPhase::IDLE - && !order.created_during_coof_recalc - && !order.created_while_in_position + && !order.birth.from_fill() && !order.created_after_position_close_in_bar && order.created_position_side == PositionSide::FLAT && order.created_bar < bar_index_ @@ -6635,7 +6630,7 @@ void BacktestEngine::apply_filled_order_to_state( && !bar_magnifier_enabled_ && !stream_warmup_mode_ && stream_phase_ == StreamPhase::IDLE - && !order.created_during_coof_recalc + && !order.birth.from_fill() && order.created_bar < bar_index_ && order.oca_name.empty() && order.oca_type == 0; @@ -6680,7 +6675,7 @@ void BacktestEngine::apply_filled_order_to_state( && !bar_magnifier_enabled_ && !stream_warmup_mode_ && stream_phase_ == StreamPhase::IDLE - && !order.created_during_coof_recalc + && !order.birth.from_fill() && order.created_bar < bar_index_ && order.oca_name.empty() && order.oca_type == 0; @@ -6923,12 +6918,12 @@ static void set_entry_fill_excursion_masks(PyramidEntry& pe, const Bar& bar, bool BacktestEngine::replaced_percent_short_market_is_live( const PendingOrder& order) const { if (order.type != OrderType::MARKET || order.is_long - || !order.created_by_same_id_replacement + || (order.replaced_order_incarnation == 0) || order.replaced_default_market_incarnation == 0 || !std::isnan(order.qty) || order.qty_type >= 0 || order.affordability_close_only || order.sbmt_member || order.created_bar != bar_index_ - 1 - || order.created_during_coof_recalc + || order.birth.from_fill() || order.created_after_position_close_in_bar || order.created_position_side != PositionSide::LONG || position_side_ != PositionSide::LONG @@ -6963,7 +6958,7 @@ bool BacktestEngine::replaced_percent_short_market_is_live( || std::isfinite(other.limit_price) || std::isfinite(other.profit_ticks) || std::isfinite(other.loss_ticks); - if (other.from_entry.empty() || other.requested_partial + if (other.from_entry.empty() || other.quantity_request.is_partial(kFullQtyEps, kFullPercentEps) || !std::isnan(other.qty) || other.qty_percent != 100 || !bracket || other.suppress_as_declined_reversal_close || !other.oca_name.empty() @@ -7213,7 +7208,7 @@ void BacktestEngine::apply_market_order_fill(PendingOrder& order, double fill_pr // intrabar retrace price. See apply_entry_order_fill's matching guard. bool same_bar_close_fill = process_orders_on_close_ && order.created_bar == bar_index_ - && !order.created_during_coof_recalc; + && !order.birth.from_fill(); if (!same_bar_close_fill) { if (position_side_ == PositionSide::LONG) trail_best_price_ = std::max(trail_best_price_, bar.high); @@ -7364,7 +7359,7 @@ void BacktestEngine::apply_entry_order_fill(PendingOrder& order, double fill_pri // the bar's close (a POOC entry created and filled this same bar). bool same_bar_close_fill = process_orders_on_close_ && order.created_bar == bar_index_ - && !order.created_during_coof_recalc; + && !order.birth.from_fill(); if (!same_bar_close_fill) { if (position_side_ == PositionSide::LONG) trail_best_price_ = std::max(trail_best_price_, bar.high); @@ -7637,7 +7632,7 @@ void BacktestEngine::apply_exit_order_fill(PendingOrder& order, double fill_pric // Consuming the id on the FIRST leg's fill would make the surviving sibling // unre-issuable while the position is still open. Mark the id consumed only // when the last leg carrying it is gone. - if (order.requested_partial && trades_.size() > trades_before_exit) { + if (order.quantity_request.is_partial(kFullQtyEps, kFullPercentEps) && trades_.size() > trades_before_exit) { bool sibling_leg_still_live = false; for (const PendingOrder& sibling : pending_orders_) { if (sibling.type != OrderType::EXIT) continue; @@ -7748,7 +7743,7 @@ void BacktestEngine::reconcile_deferred_layered_exits( // to flatten the whole position. Mirrors the live-armed normalization // at engine_strategy_commands.cpp (reserved_qty_out / live_pos * 100). if (live_pos > kQtyEpsilon) o.qty_percent = (res / live_pos) * 100.0; - o.requested_partial = res < live_pos - kFullQtyEps; + o.quantity_request.reserve(res, live_pos); reserved += res; } } @@ -8075,14 +8070,13 @@ double BacktestEngine::pooc_short_exit_trigger_close( && position_entry_count_ == 1 && pyramiding_ == 0 && pyramid_entries_.size() == 1 && order.type == OrderType::EXIT && !order.is_long - && order.created_bar == bar_index_ && !order.created_during_coof_recalc - && order.created_by_same_id_replacement - && order.replaced_exit_order_incarnation != 0 - && order.created_while_in_position && !order.dormant_bracket + && order.created_bar == bar_index_ && !order.birth.from_fill() + && (order.replaced_order_incarnation != 0) + && (order.created_position_side != PositionSide::FLAT) && !order.dormant_bracket && !order.from_entry.empty() && order.from_entry == pyramid_entries_.front().entry_id - && order.full_percent_exit_request - && !order.requested_partial && order.qty_percent == 100.0 + && order.quantity_request.requests_all() + && !order.quantity_request.is_partial(kFullQtyEps, kFullPercentEps) && order.qty_percent == 100.0 && std::isfinite(order.qty) && std::abs(order.qty - position_qty_) <= kQtyEpsilon && order.oca_name.empty() @@ -8183,7 +8177,7 @@ BacktestEngine::OrderEligibility BacktestEngine::classify_order_eligibility( // point and expires unless a later ordinary-close execution reissues it; // carrying it creates Delta's spurious out-of-session lifecycle. if (calc_on_order_fills_ && coof_scheduler_active_ - && order.coof_born_at_close_recalc) { + && order.birth.at_terminal_fill()) { if (order.created_bar == bar_index_) { return OrderEligibility::Skip; } @@ -8198,7 +8192,7 @@ BacktestEngine::OrderEligibility BacktestEngine::classify_order_eligibility( bool stale_close_order_for_new_position = order.type == OrderType::EXIT - && order.created_while_in_position + && (order.created_position_side != PositionSide::FLAT) && order.id.rfind("__close__", 0) == 0 && position_side_ != PositionSide::FLAT && position_open_bar_ > order.created_bar @@ -8212,7 +8206,7 @@ BacktestEngine::OrderEligibility BacktestEngine::classify_order_eligibility( // was open. This prevents old strategy.exit brackets from leaking into // future positions after a market close/reversal. if (order.type == OrderType::EXIT && position_side_ == PositionSide::FLAT) { - return order.created_while_in_position + return (order.created_position_side != PositionSide::FLAT) ? OrderEligibility::Remove : OrderEligibility::Skip; } @@ -8228,7 +8222,7 @@ BacktestEngine::OrderEligibility BacktestEngine::classify_order_eligibility( // touched together — TV emits both as separate trades). const bool coof_fill_recalc_entry = calc_on_order_fills_ && coof_scheduler_active_ - && order.created_during_coof_recalc + && order.birth.from_fill() && order.created_bar == bar_index_; if (priced_entry_filled_this_bar_ && order.type == OrderType::ENTRY && !coof_fill_recalc_entry) { @@ -8368,7 +8362,7 @@ BacktestEngine::OrderEligibility BacktestEngine::classify_order_eligibility( // from the next bar on. See evaluate_fill_price's has_limit/has_stop // branches for the matching same-bar fill-price rules. if (process_orders_on_close_ && order.created_bar == bar_index_ - && !order.created_during_coof_recalc) { + && !order.birth.from_fill()) { bool has_stop_or_trail = !std::isnan(order.stop_price) || !std::isnan(order.trail_points) || !std::isnan(order.trail_price); @@ -8464,7 +8458,7 @@ BacktestEngine::OrderEligibility BacktestEngine::classify_order_eligibility( order, broker_trigger_bar(bar)); if (!prearmed_market_gap && !bar_magnifier_enabled_ && !(calc_on_order_fills_ && coof_scheduler_active_ - && order.created_during_coof_recalc)) { + && order.birth.from_fill())) { double ep = position_entry_price_; if (position_side_ == PositionSide::LONG) { if (!std::isnan(order.stop_price) && order.stop_price > ep) return OrderEligibility::Skip; @@ -8555,7 +8549,7 @@ BacktestEngine::FillEvaluation BacktestEngine::evaluate_fill_price( bool exit_same_bar_reissue = exit_style && !has_trail && process_orders_on_close_ && order.created_bar == bar_index_ - && !order.created_during_coof_recalc; + && !order.birth.from_fill(); if (!should_fill && exit_same_bar_reissue && (has_stop || has_limit)) { // A mid-trade exit re-issue (e.g. a break-even stop moved by a // time-gated block) that's already marketable against THIS bar's @@ -8599,7 +8593,7 @@ BacktestEngine::FillEvaluation BacktestEngine::evaluate_fill_price( if (is_entry_bar && order.type == OrderType::EXIT && !order.from_entry.empty() - && !order.created_while_in_position + && (order.created_position_side == PositionSide::FLAT) && std::isnan(order.trail_points) && std::isnan(order.trail_price) && !bar_magnifier_enabled_ @@ -8767,7 +8761,7 @@ BacktestEngine::FillEvaluation BacktestEngine::evaluate_fill_price( } else if (!should_fill && has_limit) { // Entry limit order if (process_orders_on_close_ && order.created_bar == bar_index_ - && !order.created_during_coof_recalc) { + && !order.birth.from_fill()) { // Same-bar pure-limit entry (see classify_order_eligibility's // matching carve-out): TV evaluates it against THIS bar's // close (the moment it was placed), not the bar's full diff --git a/src/engine_path_resolve.cpp b/src/engine_path_resolve.cpp index 11c22363..3ddd64cf 100644 --- a/src/engine_path_resolve.cpp +++ b/src/engine_path_resolve.cpp @@ -195,7 +195,7 @@ bool opposing_stop_entry_hits_first(const Bar& bar, bool high_first, const PendingOrder& current = orders[current_idx]; auto deferred_at_consumed_close = [&](const PendingOrder& order) { return current_bar_index >= 0 - && order.coof_born_at_close_recalc + && order.birth.at_terminal_fill() && order.created_bar == current_bar_index; }; if (deferred_at_consumed_close(current)) return false; @@ -252,7 +252,7 @@ DualEntryStopPathWinner dual_entry_stop_path_winner(const Bar& bar, bool high_fi const PendingOrder* short_ord = nullptr; for (const PendingOrder& o : orders) { if (current_bar_index >= 0 - && o.coof_born_at_close_recalc + && o.birth.at_terminal_fill() && o.created_bar == current_bar_index) { continue; } diff --git a/src/engine_run.cpp b/src/engine_run.cpp index 614bd99d..ce41472c 100644 --- a/src/engine_run.cpp +++ b/src/engine_run.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -17,6 +18,23 @@ namespace pineforge { using namespace internal; namespace { +// A callback owns its origin, not an engine clone. Nesting and exceptions restore +// the previous owner; copying an engine cannot inherit a live callback token. +thread_local const BacktestEngine* birth_context_owner = nullptr; +thread_local std::optional birth_context; +class ScopedBirthContext { +public: + ScopedBirthContext(const BacktestEngine* owner, const OrderBirth& birth) + : previous_owner_(birth_context_owner), previous_(birth_context) { + birth_context_owner = owner; birth_context = birth; + } + ~ScopedBirthContext() { + birth_context_owner = previous_owner_; birth_context = previous_; + } +private: + const BacktestEngine* previous_owner_; + std::optional previous_; +}; [[noreturn]] void reject_chart_bar(int index, const char* rule) { throw std::invalid_argument("chart bar[" + std::to_string(index) + "]." + rule); } @@ -134,7 +152,15 @@ double BacktestEngine::active_account_currency_fx() const { // cross-engine contamination and leakage between chart and request.security // evaluation. The latter installs its own scope around every security // evaluator dispatch and restores the prior thread-local value on return. +OrderBirth BacktestEngine::capture_order_birth() const { + if (birth_context_owner == this && birth_context) return *birth_context; + return OrderBirth::direct_command(bar_index_, current_bar_.timestamp); +} + void BacktestEngine::invoke_chart_on_bar(const Bar& bar) { + const OrderBirth origin = birth_context_owner == this && birth_context + ? *birth_context : OrderBirth::chart_evaluation(bar_index_, bar.timestamp); + ScopedBirthContext origin_scope(this, origin); process_short_margin_before_script(bar); struct ChartEmaNaWarmupScope { bool previous; @@ -360,9 +386,7 @@ uint64_t BacktestEngine::execute_coof_script_body( const Bar& script_bar, double broker_cursor_price, bool cursor_is_bar_point, - bool is_fill_recalc, - bool cursor_is_bar_close, - bool recalc_at_bar_open, + const OrderBirth& evaluation_origin, uint64_t direct_fill_event_budget, bool opening_money_prefix) { restore_coof_script_state(); @@ -389,16 +413,18 @@ uint64_t BacktestEngine::execute_coof_script_body( } coof_scheduler_active_ = true; - coof_fill_recalc_active_ = is_fill_recalc; - coof_cursor_is_bar_close_ = cursor_is_bar_close; + coof_fill_recalc_active_ = evaluation_origin.from_fill(); + coof_cursor_is_bar_close_ = evaluation_origin.from_fill() + ? evaluation_origin.cursor().terminal_point() : true; // KI-67: only the first fill event at O owns "bar-open" provenance and // places standard orders. A later fill at the same O, like a fill at any // segment/extreme/close point, is mid-bar and places cascade orders. - coof_recalc_at_bar_open_ = is_fill_recalc && recalc_at_bar_open; + coof_recalc_at_bar_open_ = compat::pine::first_open_fill_evaluation(evaluation_origin); coof_cursor_price_ = broker_cursor_price; coof_cursor_is_bar_point_ = cursor_is_bar_point; coof_direct_fill_events_remaining_ = direct_fill_event_budget; const uint64_t before = broker_fill_event_seq_; + ScopedBirthContext origin_scope(this, evaluation_origin); invoke_chart_on_bar(current_bar_); if (process_orders_on_close_) { // A same-bar close batch is a broker fill at the current monotonic @@ -416,43 +442,49 @@ uint64_t BacktestEngine::execute_coof_script_body( } uint64_t BacktestEngine::run_coof_recalc_chain( - const Bar& script_bar, - double broker_cursor_price, - bool cursor_is_bar_point, - bool cursor_is_bar_close, - bool recalc_at_bar_open, - uint64_t triggering_events, - uint64_t max_events, - uint64_t events_already, - bool grouped_stop_recalc, - uint64_t market_entry_incarnation, + const Bar& script_bar, double broker_cursor_price, + bool cursor_is_bar_point, BirthCursor cursor, + uint64_t& evaluation_ordinal, uint64_t triggering_events, + uint64_t max_events, uint64_t events_already, + bool grouped_stop_recalc, uint64_t market_entry_incarnation, bool opening_money_prefix) { + // Bind callbacks to the actual simulator events that scheduled them. Direct + // fills append their exact sequence interval to this FIFO; a later callback + // never borrows the newest global sequence as its alleged triggering fill. + using Interval = std::pair; + std::deque pending; + auto append_events = [&](uint64_t first, uint64_t last, bool grouped) { + if (first == 0 || last < first) throw std::logic_error("invalid callback fill interval"); + if (grouped) pending.emplace_back(first, last); + else for (uint64_t seq = first;; ++seq) { + pending.emplace_back(seq, seq); + if (seq == last) break; + } + }; + if (triggering_events > broker_fill_event_seq_) + throw std::logic_error("callback interval exceeds committed fills"); + if (triggering_events > 0) + append_events(broker_fill_event_seq_ - triggering_events + 1, + broker_fill_event_seq_, grouped_stop_recalc); uint64_t total_events = triggering_events; - uint64_t pending_recalcs = grouped_stop_recalc ? 1 : triggering_events; uint64_t handled = 0; - while (pending_recalcs > 0 && events_already + handled < max_events) { - --pending_recalcs; - ++handled; + while (!pending.empty() && events_already + handled < max_events) { + const auto trigger = pending.front(); pending.pop_front(); ++handled; + const auto origin = OrderBirth::fill_evaluation( + bar_index_, script_bar.timestamp, cursor, broker_cursor_price, + trigger.first, trigger.second, ++evaluation_ordinal); const uint64_t used = events_already + total_events; - const uint64_t direct_budget = - used < max_events ? max_events - used : 0; - // Only the first fill event at O owns bar-open provenance. A direct or - // separately-dispatched later fill at that same O is a KI-67 cascade - // recalc whose remaining path starts on leg 0 (O->W1). - const bool first_open_fill_recalc = - recalc_at_bar_open && events_already == 0 && handled == 1; - coof_recalc_after_first_open_fill_ = - recalc_at_bar_open && !first_open_fill_recalc; - coof_market_entry_recalc_incarnation_ = - handled == 1 ? market_entry_incarnation : 0; + const uint64_t direct_budget = used < max_events ? max_events - used : 0; + coof_recalc_after_first_open_fill_ = cursor.first_point() + && !compat::pine::first_open_fill_evaluation(origin); + coof_market_entry_recalc_incarnation_ = handled == 1 ? market_entry_incarnation : 0; coof_market_entry_recalc_fill_seq_ = broker_fill_event_seq_; + const uint64_t before = broker_fill_event_seq_; const uint64_t direct = execute_coof_script_body( script_bar, broker_cursor_price, cursor_is_bar_point, - /*is_fill_recalc=*/true, - cursor_is_bar_close, first_open_fill_recalc, - direct_budget, opening_money_prefix); + origin, direct_budget, opening_money_prefix); total_events += direct; - pending_recalcs += direct; + if (direct > 0) append_events(before + 1, broker_fill_event_seq_, false); } return total_events; } @@ -495,6 +527,7 @@ void BacktestEngine::dispatch_bar_calc_on_order_fills() { constexpr uint64_t kNoFillEventBudget = std::numeric_limits::max(); constexpr int kCoofLoopGuard = 1 << 20; uint64_t fill_events = 0; + uint64_t evaluation_ordinal = 0; int exit_closed_from_bar = -1; uint64_t exit_closed_from_incarnation = 0; bool exit_closed_was_long = false; @@ -540,8 +573,8 @@ void BacktestEngine::dispatch_bar_calc_on_order_fills() { coof_cascade_recalc_leg_ = 0; fill_events += run_coof_recalc_chain( script_bar, cursor, /*cursor_is_bar_point=*/true, - /*cursor_is_bar_close=*/false, /*recalc_at_bar_open=*/true, - broker_fill_event_seq_ - before, kNoFillEventBudget, 0, + BirthCursor::point(BirthCursorDomain::HistoricalPath, 0, 4), + evaluation_ordinal, broker_fill_event_seq_ - before, kNoFillEventBudget, 0, /*grouped_stop_recalc=*/false, /*market_entry_incarnation=*/0, /*opening_money_prefix=*/true); // The ordinary O exception permits just the first follow-up @@ -551,7 +584,7 @@ void BacktestEngine::dispatch_bar_calc_on_order_fills() { } auto consume_fill = [&](const CoofFillResult& fill, - bool cursor_is_close, + BirthCursor birth_cursor, bool filled_at_bar_open_point) { const uint64_t before = fill_events; const bool chart_tick_touch = std::isfinite(fill.chart_waypoint_price); @@ -559,11 +592,10 @@ void BacktestEngine::dispatch_bar_calc_on_order_fills() { cursor_is_bar_point = chart_tick_touch; // The recalc chain receives O-point provenance, but only its first fill // event is classified as bar-open. A later fill at the same O is a - // leg-0 cascade (PendingOrder::coof_born_mid_bar). + // leg-0 cascade (the Pine historical cascade permission). fill_events += run_coof_recalc_chain( - script_bar, fill.fill_price, /*cursor_is_bar_point=*/false, cursor_is_close, - filled_at_bar_open_point, - fill.fill_events, kNoFillEventBudget, fill_events, + script_bar, fill.fill_price, /*cursor_is_bar_point=*/false, + birth_cursor, evaluation_ordinal, fill.fill_events, kNoFillEventBudget, fill_events, fill.grouped_stop_recalc, fill.market_entry_incarnation); // The carried order's open fill triggers one execution at O, and the // order born in that first execution may also fill at O. Every later @@ -576,7 +608,6 @@ void BacktestEngine::dispatch_bar_calc_on_order_fills() { int loop_guard = 0; while (++loop_guard <= kCoofLoopGuard) { if (evaluate_current_point) { - const bool cursor_is_close = next_waypoint >= 4; // Cascade orders fill only AT an extreme waypoint (W1 = next_waypoint // 2, W2 = next_waypoint 3); the O point (1) and the C point (>=4) do // not admit them. @@ -598,7 +629,7 @@ void BacktestEngine::dispatch_bar_calc_on_order_fills() { // i.e. leg (next_waypoint-1) — the leg the loop traverses next. coof_cascade_recalc_leg_ = next_waypoint - 1; consume_fill( - fill, cursor_is_close, + fill, BirthCursor::point(BirthCursorDomain::HistoricalPath, next_waypoint - 1, 4), /*filled_at_bar_open_point=*/next_waypoint == 1); continue; } @@ -627,15 +658,15 @@ void BacktestEngine::dispatch_bar_calc_on_order_fills() { if (fill.filled) { const bool reached_target = std::abs(fill.fill_price - target) <= kSegmentDenomEps; - const bool cursor_is_close = next_waypoint == 3 - && reached_target; // A fill mid-leg leaves the in-flight leg at (next_waypoint-1); a fill // that reaches the leg-end waypoint (path[next_waypoint]) advances to // the NEXT leg (next_waypoint) — the loop's ++next_waypoint below. coof_cascade_recalc_leg_ = reached_target ? next_waypoint : (next_waypoint - 1); consume_fill( - fill, cursor_is_close, + fill, reached_target + ? BirthCursor::point(BirthCursorDomain::HistoricalPath, next_waypoint, 4) + : BirthCursor::segment(BirthCursorDomain::HistoricalPath, next_waypoint - 1, 4), /*filled_at_bar_open_point=*/false); // H/L/C itself has been consumed by this priced fill. Only O has // the same-point two-fill exception; a market order born in the @@ -673,8 +704,8 @@ void BacktestEngine::dispatch_bar_calc_on_order_fills() { cursor = path[3]; cursor_is_bar_point = true; uint64_t direct = execute_coof_script_body( - script_bar, cursor, cursor_is_bar_point, /*is_fill_recalc=*/false, - /*cursor_is_bar_close=*/true, /*recalc_at_bar_open=*/false, + script_bar, cursor, cursor_is_bar_point, + OrderBirth::chart_evaluation(bar_index_, script_bar.timestamp), kNoFillEventBudget); // C is the terminal historical tick. Direct fills produced by this // ordinary-close execution are real broker fills, but do not trigger @@ -720,8 +751,8 @@ void BacktestEngine::dispatch_bar_calc_on_order_fills() { if (margin_events > 0) { fill_events += run_coof_recalc_chain( script_bar, cursor, cursor_is_bar_point, - /*cursor_is_bar_close=*/true, - /*recalc_at_bar_open=*/false, margin_events, + BirthCursor::point(BirthCursorDomain::HistoricalPath, 3, 4), + evaluation_ordinal, margin_events, kNoFillEventBudget, fill_events); } @@ -1328,6 +1359,7 @@ void BacktestEngine::run_magnified_bar_calc_on_order_fills( // actual lower-timeframe broker ticks supplied by the magnifier. const uint64_t max_fill_events = static_cast(ticks.size()); uint64_t fill_events = 0; + uint64_t evaluation_ordinal = 0; int exit_closed_from_bar = -1; uint64_t exit_closed_from_incarnation = 0; bool exit_closed_was_long = false; @@ -1346,25 +1378,23 @@ void BacktestEngine::run_magnified_bar_calc_on_order_fills( bool evaluate_current_point = true; auto consume_fill = [&](const CoofFillResult& fill, - bool cursor_is_close, + BirthCursor birth_cursor, bool filled_at_first_tick) { const uint64_t before = fill_events; cursor = fill.fill_price; cursor_is_bar_point = false; - // Magnifier path: coof_born_mid_bar is inert here (the cascade gate is + // Magnifier path: historical cascade permission is inert here (the cascade gate is // guarded by !bar_magnifier_enabled_), but keep provenance consistent — // a first-tick fill is the magnifier analogue of a bar-open recalc. fill_events += run_coof_recalc_chain( - script_bar, cursor, cursor_is_bar_point, cursor_is_close, - filled_at_first_tick, - fill.fill_events, max_fill_events, fill_events); + script_bar, cursor, cursor_is_bar_point, birth_cursor, + evaluation_ordinal, fill.fill_events, max_fill_events, fill_events); evaluate_current_point = filled_at_first_tick && before == 0 && fill_events == 1; }; while (fill_events < max_fill_events) { if (evaluate_current_point) { - const bool cursor_is_close = next_tick >= ticks.size(); Bar point = coof_point_bar(script_bar, cursor); point.timestamp = cursor_ts; current_bar_ = point; @@ -1374,7 +1404,8 @@ void BacktestEngine::run_magnified_bar_calc_on_order_fills( exit_closed_was_long); if (fill.filled) { consume_fill( - fill, cursor_is_close, + fill, BirthCursor::point(BirthCursorDomain::MagnifierTicks, + static_cast(next_tick) - 1, static_cast(ticks.size())), /*filled_at_first_tick=*/next_tick == 1); continue; } @@ -1409,10 +1440,12 @@ void BacktestEngine::run_magnified_bar_calc_on_order_fills( cursor_ts = target.timestamp; const bool reached_target = std::abs(fill.fill_price - target.price) <= kSegmentDenomEps; - const bool cursor_is_close = next_tick + 1 == ticks.size() - && reached_target; consume_fill( - fill, cursor_is_close, + fill, reached_target + ? BirthCursor::point(BirthCursorDomain::MagnifierTicks, + static_cast(next_tick), static_cast(ticks.size())) + : BirthCursor::segment(BirthCursorDomain::MagnifierTicks, + static_cast(next_tick) - 1, static_cast(ticks.size())), /*filled_at_first_tick=*/false); // The real lower-TF endpoint is already consumed. Do not replay // a market-enabled point at the same H/L/C tick; O remains the @@ -1431,8 +1464,8 @@ void BacktestEngine::run_magnified_bar_calc_on_order_fills( cursor = ticks.back().price; cursor_is_bar_point = true; uint64_t direct = execute_coof_script_body( - script_bar, cursor, cursor_is_bar_point, /*is_fill_recalc=*/false, - /*cursor_is_bar_close=*/true, /*recalc_at_bar_open=*/false, + script_bar, cursor, cursor_is_bar_point, + OrderBirth::chart_evaluation(bar_index_, script_bar.timestamp), fill_events < max_fill_events ? max_fill_events - fill_events : 0); commit_coof_script_state(); // The last real lower-TF close is also terminal: count direct fills but do @@ -1465,8 +1498,9 @@ void BacktestEngine::run_magnified_bar_calc_on_order_fills( if (margin_events > 0 && fill_events < max_fill_events) { fill_events += run_coof_recalc_chain( script_bar, cursor, cursor_is_bar_point, - /*cursor_is_bar_close=*/true, - /*recalc_at_bar_open=*/false, margin_events, + BirthCursor::point(BirthCursorDomain::MagnifierTicks, + static_cast(ticks.size()) - 1, static_cast(ticks.size())), + evaluation_ordinal, margin_events, max_fill_events, fill_events); } diff --git a/src/engine_state_hash.cpp b/src/engine_state_hash.cpp index 1834222b..f7222195 100644 --- a/src/engine_state_hash.cpp +++ b/src/engine_state_hash.cpp @@ -86,9 +86,9 @@ void hash_str_set(Fnv& f, const std::unordered_set& s) { uint64_t BacktestEngine::broker_state_hash() const { Fnv f; - // v4 adds explicit Pine order-priority attachment and configuration. + // v5 hashes typed request/reservation, generic predecessor and immutable birth. // It is a serialization boundary, independent of the public C ABI version. - f.s("pineforge-broker-state/v4"); + f.s("pineforge-broker-state/v5"); // --- Position core --- f.i(static_cast(position_side_)); @@ -191,7 +191,18 @@ uint64_t BacktestEngine::broker_state_hash() const { f.b(o.over_pyramiding_cap_at_placement); f.b(o.affordability_close_only); f.i(static_cast(o.created_position_cycle_seq)); - f.b(o.requested_partial); f.b(o.full_percent_exit_request); + f.b(o.quantity_request.intent().has_value()); + if (const auto& intent = o.quantity_request.intent()) { + f.i(static_cast(intent->kind())); + if (intent->kind() == QuantityIntent::Kind::Units) f.d(intent->units()); + else if (intent->kind() == QuantityIntent::Kind::Fraction) { + f.d(intent->numerator()); f.d(intent->denominator()); + } + } + f.b(o.quantity_request.reservation().has_value()); + if (const auto& reservation = o.quantity_request.reservation()) { + f.d(reservation->units); f.d(reservation->basis_units); + } // Round-14 signal-close-margin-call receipt (cross-bar: compared // against broker_fill_event_seq_ on the bar AFTER the one it was // stamped on). @@ -223,25 +234,32 @@ uint64_t BacktestEngine::broker_state_hash() const { // Placement-side position/close provenance. f.i(static_cast(o.created_position_side)); f.b(o.created_after_position_close_in_bar); - f.b(o.created_while_in_position); // Round-14 rounded-signal-cost decline receipt + its remaining qty. f.b(o.rounded_signal_cost_close_only); f.d(o.signal_close_mc_remaining_qty); // Same-id replacement / named-cancel recreate provenance // (clean-room two-call rules fail closed on these). - f.b(o.created_by_same_id_replacement); + f.u(o.replaced_order_incarnation); f.u(o.replaced_default_market_incarnation); f.b(o.declined_by_replaced_short_market); - f.u(o.replaced_exit_order_incarnation); f.u(o.recreated_after_named_cancelled_entry_incarnation); f.u(o.named_cancel_surviving_exit_incarnation); // calc_on_order_fills birth provenance and per-leg suppression // (decide which waypoints / legs the order may fill at). f.b(o.coof_suppress_stop_on_entry_bar); f.b(o.coof_suppress_limit_on_entry_bar); - f.b(o.created_during_coof_recalc); - f.b(o.coof_born_at_close_recalc); - f.b(o.coof_born_mid_bar); + f.i(static_cast(o.birth.cause())); + f.i(o.birth.bar()); + f.i(o.birth.timestamp()); + f.i(static_cast(o.birth.cursor().domain())); + f.i(static_cast(o.birth.cursor().position())); + f.i(o.birth.cursor().index()); + f.i(o.birth.cursor().count()); + f.d(o.birth.cursor_price()); + f.u(o.birth.first_fill()); + f.u(o.birth.last_fill()); + f.u(o.birth.evaluation_ordinal()); + f.i(static_cast(o.pine_birth_reach)); f.i(static_cast(o.coof_cascade_seg_i)); f.b(o.coof_cascade_inflight_fires); // Deferred close_all same-id stop preservation token. diff --git a/src/engine_strategy_commands.cpp b/src/engine_strategy_commands.cpp index 04c2152f..302ea411 100644 --- a/src/engine_strategy_commands.cpp +++ b/src/engine_strategy_commands.cpp @@ -513,10 +513,12 @@ void BacktestEngine::strategy_entry(const std::string& id, bool is_long, } } int64_t preserved_seq = 0; + uint64_t replaced_order_incarnation = 0; uint64_t replaced_default_market_incarnation = 0; for (const auto& o : pending_orders_) { if (o.id == id) { preserved_seq = o.created_seq; + replaced_order_incarnation = o.incarnation; if (o.type == OrderType::MARKET && o.created_bar == bar_index_ && o.is_long == is_long && std::isnan(o.qty) && o.qty_type < 0 && o.created_position_cycle_seq == position_cycle_seq_) { @@ -564,7 +566,7 @@ void BacktestEngine::strategy_entry(const std::string& id, bool is_long, order.created_bar = bar_index_; order.created_seq = preserved_seq > 0 ? preserved_seq : next_order_seq_++; order.incarnation = next_order_incarnation_++; - order.created_by_same_id_replacement = preserved_seq > 0; + order.replaced_order_incarnation = replaced_order_incarnation; order.replaced_default_market_incarnation = replaced_default_market_incarnation; if (preserved_seq == 0) { @@ -573,14 +575,9 @@ void BacktestEngine::strategy_entry(const std::string& id, bool is_long, order.named_cancel_surviving_exit_incarnation = named_cancel_context.surviving_exit_incarnation; } - order.created_during_coof_recalc = coof_fill_recalc_active_; - order.coof_born_at_close_recalc = - coof_fill_recalc_active_ && coof_cursor_is_bar_close_; - // KI-67: a fill recalc without first-O provenance is mid-bar. This includes - // later fills at that same O; orders it places are cascade orders eligible - // only at the remaining extreme waypoints of the historical 4-tick path. - order.coof_born_mid_bar = - coof_fill_recalc_active_ && !coof_recalc_at_bar_open_; + order.birth = capture_order_birth(); + order.pine_birth_reach = compat::pine::select_historical_birth_reach( + order.birth, false); order.created_position_side = position_side_; order.created_position_cycle_seq = position_cycle_seq_; order.created_after_position_close_in_bar = @@ -2090,7 +2087,7 @@ void BacktestEngine::strategy_exit(const std::string& id, const std::string& fro ? PositionSide::LONG : PositionSide::SHORT; const bool qualifies = o.created_bar == bar_index_ && o.type == OrderType::MARKET - && !o.created_during_coof_recalc + && !o.birth.from_fill() && !o.over_pyramiding_cap_at_placement && entry_dir == position_side_ && o.created_position_side == position_side_; @@ -2139,9 +2136,14 @@ void BacktestEngine::strategy_exit(const std::string& id, const std::string& fro order.qty = reserved_qty; order.qty_type = -1; order.qty_percent = qp; - order.requested_partial = is_partial; - order.full_percent_exit_request = !has_explicit_qty - && (std::isnan(qty_percent) || qty_percent == 100.0); + order.quantity_request.request(has_explicit_qty + ? QuantityIntent::units(qty) + : (std::isnan(qty_percent) || qty_percent == 100.0) + ? QuantityIntent::all() : QuantityIntent::fraction(qty_percent, 100.0)); + // A resolved reservation owns its numeric basis. Deferred percentage + // requests retain their original fraction until a live owner binds them. + if (has_explicit_qty || std::isfinite(reserved_qty)) + order.quantity_request.reserve(reserved_qty, live_pos_qty); order.pooc_global_full_exit_dynamic_qty = bind_global_full_exit_dynamic_qty; order.pooc_global_full_exit_tracks_bound_adds = @@ -2163,33 +2165,18 @@ void BacktestEngine::strategy_exit(const std::string& id, const std::string& fro order.created_bar = bar_index_; order.created_seq = preserved_seq > 0 ? preserved_seq : next_order_seq_++; order.incarnation = next_order_incarnation_++; - order.created_by_same_id_replacement = preserved_seq > 0; - order.replaced_exit_order_incarnation = replaced_incarnation; - order.created_during_coof_recalc = coof_fill_recalc_active_; - order.coof_born_at_close_recalc = - coof_fill_recalc_active_ && coof_cursor_is_bar_close_; - // KI-67: a fill recalc that was NOT triggered at the bar-open tick is a - // mid-bar recalc; orders it places are cascade orders (eligible only at the - // remaining extreme waypoints of the historical 4-tick path). The new - // later-same-O refinement is priced/non-trail only: a trail born in that - // exact cell retains its established standard/open path reach. Otherwise - // the refinement would newly hold it to W1/W2 instead of letting it arm and - // cross continuously on the remaining legs. Magnifier keeps its own tick - // model and is unchanged. - const bool preserve_later_same_open_trail_provenance = - !bar_magnifier_enabled_ && has_trail_request - && coof_fill_recalc_active_ && !coof_recalc_at_bar_open_ - && coof_recalc_after_first_open_fill_ - && coof_cascade_recalc_leg_ == 0; - order.coof_born_mid_bar = - coof_fill_recalc_active_ && !coof_recalc_at_bar_open_ - && !preserve_later_same_open_trail_provenance; + order.replaced_order_incarnation = replaced_incarnation; + order.birth = capture_order_birth(); + order.pine_birth_reach = compat::pine::select_historical_birth_reach( + order.birth, has_trail_request); + // Later-open trailing permission is derived by the Pine policy from the + // immutable physical origin. It never rewrites that origin. // A priced exit born after a later fill at the SAME O is already held by // the KI-67 cascade gate for its in-flight leg 0. The pinned exception is // LIMIT-only: a marketable limit may resume at W1. A marketable stop keeps // the established whole-entry-bar suppression (including M1). const bool later_same_open_priced_exit_on_entry_bar = - !bar_magnifier_enabled_ && order.coof_born_mid_bar + !bar_magnifier_enabled_ && compat::pine::historical_cascade_reach(order) && coof_recalc_after_first_open_fill_ && coof_cascade_recalc_leg_ == 0 && position_open_bar_ == bar_index_ @@ -2221,7 +2208,7 @@ void BacktestEngine::strategy_exit(const std::string& id, const std::string& fro const bool first_high_market_limit_recross = !bar_magnifier_enabled_ && !process_orders_on_close_ && !stream_warmup_mode_ && stream_phase_ == StreamPhase::IDLE - && order.coof_born_mid_bar && !coof_hist_is_segment_ + && compat::pine::historical_cascade_reach(order) && !coof_hist_is_segment_ && coof_at_extreme_waypoint_ && coof_hist_path_index_ == 1 && coof_cascade_recalc_leg_ == 1 && coof_market_entry_recalc_incarnation_ != 0 @@ -2261,7 +2248,7 @@ void BacktestEngine::strategy_exit(const std::string& id, const std::string& fro // fill lands exactly on a waypoint ("a fill AT a waypoint starts the NEXT // leg"). current_bar_ is the full script bar during a fill recalc; the // magnifier path owns its own tick model and is scoped out. - if (order.coof_born_mid_bar && !bar_magnifier_enabled_ + if (compat::pine::historical_cascade_reach(order) && !bar_magnifier_enabled_ && coof_scheduler_active_ && std::isfinite(coof_cursor_price_) && position_side_ != PositionSide::FLAT && (!std::isnan(order.stop_price) || !std::isnan(order.limit_price)) @@ -2287,7 +2274,6 @@ void BacktestEngine::strategy_exit(const std::string& id, const std::string& fro order.created_position_side = effectively_flat ? PositionSide::FLAT : position_side_; order.tv_carry_qty = live_pos_qty; order.comment = comment; - order.created_while_in_position = !effectively_flat; // Round 7 family M mechanism 2a: a re-issue that replaces a DORMANT // bracket (finding-311 KILL) inside the close-time script body stays // dormant until this bar's process_margin_call has run — TradingView's @@ -2323,11 +2309,10 @@ void BacktestEngine::strategy_exit(const std::string& id, const std::string& fro extra.qty_percent = (live_pos_qty > kQtyEpsilon) ? (leg_qty / live_pos_qty) * 100.0 : order.qty_percent; - extra.requested_partial = leg_qty < live_pos_qty - kFullQtyEps; + extra.quantity_request.reserve(leg_qty, live_pos_qty); extra.created_seq = next_order_seq_++; extra.incarnation = next_order_incarnation_++; - extra.created_by_same_id_replacement = false; - extra.replaced_exit_order_incarnation = 0; + extra.replaced_order_incarnation = 0; pending_orders_.push_back(std::move(extra)); } } @@ -2424,9 +2409,11 @@ void BacktestEngine::strategy_order(const std::string& id, bool is_long, double bar_index_); } int64_t preserved_seq = 0; + uint64_t replaced_order_incarnation = 0; for (const auto& o : pending_orders_) { if (o.id == id) { preserved_seq = o.created_seq; + replaced_order_incarnation = o.incarnation; break; } } @@ -2476,14 +2463,10 @@ void BacktestEngine::strategy_order(const std::string& id, bool is_long, double order.created_bar = bar_index_; order.created_seq = preserved_seq > 0 ? preserved_seq : next_order_seq_++; order.incarnation = next_order_incarnation_++; - order.created_during_coof_recalc = coof_fill_recalc_active_; - order.coof_born_at_close_recalc = - coof_fill_recalc_active_ && coof_cursor_is_bar_close_; - // KI-67: a fill recalc that was NOT triggered at the bar-open tick is a - // mid-bar recalc; orders it places are cascade orders (eligible only at the - // remaining extreme waypoints of the historical 4-tick path). - order.coof_born_mid_bar = - coof_fill_recalc_active_ && !coof_recalc_at_bar_open_; + order.replaced_order_incarnation = replaced_order_incarnation; + order.birth = capture_order_birth(); + order.pine_birth_reach = compat::pine::select_historical_birth_reach( + order.birth, false); order.created_position_side = position_side_; order.created_after_position_close_in_bar = pending_close_qty_in_bar_ > kQtyEpsilon; @@ -2901,23 +2884,23 @@ uint64_t BacktestEngine::queue_deferred_close_order( order.qty_percent = closes_full_position ? 100.0 : (position_qty_ > eps ? (qty_to_close / position_qty_) * 100.0 : 100.0); } + // Preserve the source close's already-resolved placement target. This is + // not a fixed executable-quantity promise: the existing ANY-relative path + // can later bind its percentage to a replacement position (e.g. target 1 + // against E2 can reserve 2 against new E4). Do not fabricate an original + // Pine percentage, or an exposure-coverage receipt before that binding. + order.quantity_request.request(QuantityIntent::units(qty_to_close)); order.oca_name = ""; order.oca_type = 0; order.created_bar = bar_index_; order.created_seq = next_order_seq_++; order.incarnation = next_order_incarnation_++; - order.created_during_coof_recalc = coof_fill_recalc_active_; - order.coof_born_at_close_recalc = - coof_fill_recalc_active_ && coof_cursor_is_bar_close_; - // KI-67: a fill recalc that was NOT triggered at the bar-open tick is a - // mid-bar recalc; orders it places are cascade orders (eligible only at the - // remaining extreme waypoints of the historical 4-tick path). - order.coof_born_mid_bar = - coof_fill_recalc_active_ && !coof_recalc_at_bar_open_; + order.birth = capture_order_birth(); + order.pine_birth_reach = compat::pine::select_historical_birth_reach( + order.birth, false); order.created_position_side = position_side_; order.tv_carry_qty = position_qty_; order.comment = comment; - order.created_while_in_position = true; // design-declined-reversal-close-leg: the qty this close debited from // id_unclosed_qty_ at CALL time (default-FIFO branch), so a later // suppression can re-credit exactly that amount. NaN when nothing was diff --git a/src/engine_stream.cpp b/src/engine_stream.cpp index a3ca41f8..c121c11f 100644 --- a/src/engine_stream.cpp +++ b/src/engine_stream.cpp @@ -678,7 +678,7 @@ uint64_t BacktestEngine::stream_state_hash() const { integer(static_cast(bar.timestamp)); real(bar.open); real(bar.high); real(bar.low); real(bar.close); real(bar.volume); }; - integer(4); integer(broker_state_hash()); + integer(5); integer(broker_state_hash()); integer(static_cast(stream_phase_)); integer(static_cast(stream_input_mode_)); integer(static_cast(stream_input_tf_ms_)); diff --git a/src/pending_order_mirror.cpp b/src/pending_order_mirror.cpp index bfb8fe92..f0e99c57 100644 --- a/src/pending_order_mirror.cpp +++ b/src/pending_order_mirror.cpp @@ -51,18 +51,18 @@ void fill_pending_order_mirror(const PendingOrder& src, pf_pending_order_v1_t* o out->created_bar = (int32_t)src.created_bar; out->created_seq = src.created_seq; out->incarnation = src.incarnation; - out->created_by_same_id_replacement = src.created_by_same_id_replacement ? 1 : 0; + out->created_by_same_id_replacement = src.type != OrderType::RAW_ORDER && src.replaced_order_incarnation != 0 ? 1 : 0; out->replaced_default_market_incarnation = src.replaced_default_market_incarnation; out->declined_by_replaced_short_market = src.declined_by_replaced_short_market ? 1 : 0; - out->replaced_exit_order_incarnation = src.replaced_exit_order_incarnation; + out->replaced_exit_order_incarnation = src.type == OrderType::EXIT ? src.replaced_order_incarnation : 0; out->recreated_after_named_cancelled_entry_incarnation = src.recreated_after_named_cancelled_entry_incarnation; out->named_cancel_surviving_exit_incarnation = src.named_cancel_surviving_exit_incarnation; out->stop_limit_activated = src.stop_limit_activated ? 1 : 0; out->coof_suppress_stop_on_entry_bar = src.coof_suppress_stop_on_entry_bar ? 1 : 0; out->coof_suppress_limit_on_entry_bar = src.coof_suppress_limit_on_entry_bar ? 1 : 0; - out->created_during_coof_recalc = src.created_during_coof_recalc ? 1 : 0; - out->coof_born_at_close_recalc = src.coof_born_at_close_recalc ? 1 : 0; - out->coof_born_mid_bar = src.coof_born_mid_bar ? 1 : 0; + out->created_during_coof_recalc = src.birth.from_fill() ? 1 : 0; + out->coof_born_at_close_recalc = src.birth.at_terminal_fill() ? 1 : 0; + out->coof_born_mid_bar = compat::pine::historical_cascade_reach(src) ? 1 : 0; out->coof_cascade_seg_i = (int32_t)src.coof_cascade_seg_i; out->coof_cascade_inflight_fires = src.coof_cascade_inflight_fires ? 1 : 0; out->created_position_side = (int32_t)src.created_position_side; @@ -106,12 +106,12 @@ void fill_pending_order_mirror(const PendingOrder& src, pf_pending_order_v1_t* o out->signal_close_mc_fill_seq = src.signal_close_mc_fill_seq; out->signal_close_mc_remaining_qty = src.signal_close_mc_remaining_qty; copy_str(src.comment, out->comment, &out->comment_truncated, &out->comment_hash64); - out->requested_partial = src.requested_partial ? 1 : 0; - out->full_percent_exit_request = src.full_percent_exit_request ? 1 : 0; + out->requested_partial = src.quantity_request.is_partial(1e-9, 1e-9) ? 1 : 0; + out->full_percent_exit_request = src.quantity_request.requests_all() ? 1 : 0; out->pooc_global_full_exit_dynamic_qty = src.pooc_global_full_exit_dynamic_qty ? 1 : 0; out->pooc_global_full_exit_tracks_bound_adds = src.pooc_global_full_exit_tracks_bound_adds ? 1 : 0; out->pooc_global_full_exit_bound_add = src.pooc_global_full_exit_bound_add ? 1 : 0; - out->created_while_in_position = src.created_while_in_position ? 1 : 0; + out->created_while_in_position = src.type == OrderType::EXIT && src.created_position_side != PositionSide::FLAT ? 1 : 0; out->sbmt_member = src.sbmt_member ? 1 : 0; out->sbmt_own_qty = src.sbmt_own_qty; out->sbmt_tx_qty = src.sbmt_tx_qty; @@ -130,6 +130,26 @@ void fill_pending_order_mirror(const PendingOrder& src, pf_pending_order_v1_t* o out->suppressed_close_consumed_ledger_qty = src.suppressed_close_consumed_ledger_qty; out->suppressed_close_retired_ledger_qty = src.suppressed_close_retired_ledger_qty; out->short_seed_collision_role = (int32_t)src.short_seed_collision_role; + out->replaced_order_incarnation = src.replaced_order_incarnation; + out->birth_timestamp = src.birth.timestamp(); + out->birth_cause = (int32_t)src.birth.cause(); + out->birth_bar = src.birth.bar(); + out->birth_cursor_domain = (int32_t)src.birth.cursor().domain(); + out->birth_cursor_position = (int32_t)src.birth.cursor().position(); + out->birth_cursor_index = src.birth.cursor().index(); + out->birth_cursor_count = src.birth.cursor().count(); + out->birth_cursor_price = src.birth.cursor_price(); + out->birth_first_fill = src.birth.first_fill(); + out->birth_last_fill = src.birth.last_fill(); + out->birth_evaluation_ordinal = src.birth.evaluation_ordinal(); + out->pine_birth_reach = (int32_t)src.pine_birth_reach; + out->quantity_intent_kind = src.quantity_request.intent() ? static_cast(src.quantity_request.intent()->kind()) + 1 : 0; + out->quantity_intent_units = src.quantity_request.intent() && src.quantity_request.intent()->kind() == QuantityIntent::Kind::Units ? src.quantity_request.intent()->units() : 0.0; + out->quantity_intent_numerator = src.quantity_request.intent() && src.quantity_request.intent()->kind() == QuantityIntent::Kind::Fraction ? src.quantity_request.intent()->numerator() : 0.0; + out->quantity_intent_denominator = src.quantity_request.intent() && src.quantity_request.intent()->kind() == QuantityIntent::Kind::Fraction ? src.quantity_request.intent()->denominator() : 0.0; + out->quantity_reservation_present = src.quantity_request.reservation().has_value() ? 1 : 0; + out->quantity_reservation_units = src.quantity_request.reservation() ? src.quantity_request.reservation()->units : 0.0; + out->quantity_reservation_basis_units = src.quantity_request.reservation() ? src.quantity_request.reservation()->basis_units : 0.0; } namespace { @@ -247,6 +267,26 @@ const pf_field_desc_t kLayout[] = { PF_PO_FIELD(suppressed_close_consumed_ledger_qty, "double"), PF_PO_FIELD(suppressed_close_retired_ledger_qty, "double"), PF_PO_FIELD(short_seed_collision_role, "int32_t"), + PF_PO_FIELD(replaced_order_incarnation, "uint64_t"), + PF_PO_FIELD(birth_timestamp, "int64_t"), + PF_PO_FIELD(birth_cause, "int32_t"), + PF_PO_FIELD(birth_bar, "int32_t"), + PF_PO_FIELD(birth_cursor_domain, "int32_t"), + PF_PO_FIELD(birth_cursor_position, "int32_t"), + PF_PO_FIELD(birth_cursor_index, "int32_t"), + PF_PO_FIELD(birth_cursor_count, "int32_t"), + PF_PO_FIELD(birth_cursor_price, "double"), + PF_PO_FIELD(birth_first_fill, "uint64_t"), + PF_PO_FIELD(birth_last_fill, "uint64_t"), + PF_PO_FIELD(birth_evaluation_ordinal, "uint64_t"), + PF_PO_FIELD(pine_birth_reach, "int32_t"), + PF_PO_FIELD(quantity_intent_kind, "uint64_t"), + PF_PO_FIELD(quantity_intent_units, "double"), + PF_PO_FIELD(quantity_intent_numerator, "double"), + PF_PO_FIELD(quantity_intent_denominator, "double"), + PF_PO_FIELD(quantity_reservation_present, "uint8_t"), + PF_PO_FIELD(quantity_reservation_units, "double"), + PF_PO_FIELD(quantity_reservation_basis_units, "double"), }; #undef PF_PO_FIELD diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b5b72d63..aab7cf1f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,4 +1,6 @@ set(TEST_SOURCES + test_pending_order_core + test_pending_quantity_intent test_bulk_preflight test_opening_obligation test_high_value_price_admission @@ -78,6 +80,8 @@ set(TEST_SOURCES test_engine_risk test_strategy_oca test_pending_order_identity + test_pending_placement_receipts + test_order_birth_provenance test_oca_raw_pyramid_add test_strategy_pyramiding test_pyramiding_count_partial_drain diff --git a/tests/fixtures/pending_order_prefix/c45-fields.inc b/tests/fixtures/pending_order_prefix/c45-fields.inc new file mode 100644 index 00000000..37eb4375 --- /dev/null +++ b/tests/fixtures/pending_order_prefix/c45-fields.inc @@ -0,0 +1,108 @@ +PF_PREFIX_FIELD(struct_version) +PF_PREFIX_FIELD(size) +PF_PREFIX_FIELD(id) +PF_PREFIX_FIELD(id_truncated) +PF_PREFIX_FIELD(id_hash64) +PF_PREFIX_FIELD(from_entry) +PF_PREFIX_FIELD(from_entry_truncated) +PF_PREFIX_FIELD(from_entry_hash64) +PF_PREFIX_FIELD(type) +PF_PREFIX_FIELD(is_long) +PF_PREFIX_FIELD(limit_price) +PF_PREFIX_FIELD(stop_price) +PF_PREFIX_FIELD(trail_points) +PF_PREFIX_FIELD(trail_price) +PF_PREFIX_FIELD(trail_offset) +PF_PREFIX_FIELD(profit_ticks) +PF_PREFIX_FIELD(loss_ticks) +PF_PREFIX_FIELD(qty) +PF_PREFIX_FIELD(qty_type) +PF_PREFIX_FIELD(qty_percent) +PF_PREFIX_FIELD(oca_name) +PF_PREFIX_FIELD(oca_name_truncated) +PF_PREFIX_FIELD(oca_name_hash64) +PF_PREFIX_FIELD(oca_type) +PF_PREFIX_FIELD(created_bar) +PF_PREFIX_FIELD(created_seq) +PF_PREFIX_FIELD(incarnation) +PF_PREFIX_FIELD(created_by_same_id_replacement) +PF_PREFIX_FIELD(replaced_default_market_incarnation) +PF_PREFIX_FIELD(declined_by_replaced_short_market) +PF_PREFIX_FIELD(replaced_exit_order_incarnation) +PF_PREFIX_FIELD(recreated_after_named_cancelled_entry_incarnation) +PF_PREFIX_FIELD(named_cancel_surviving_exit_incarnation) +PF_PREFIX_FIELD(stop_limit_activated) +PF_PREFIX_FIELD(coof_suppress_stop_on_entry_bar) +PF_PREFIX_FIELD(coof_suppress_limit_on_entry_bar) +PF_PREFIX_FIELD(created_during_coof_recalc) +PF_PREFIX_FIELD(coof_born_at_close_recalc) +PF_PREFIX_FIELD(coof_born_mid_bar) +PF_PREFIX_FIELD(coof_cascade_seg_i) +PF_PREFIX_FIELD(coof_cascade_inflight_fires) +PF_PREFIX_FIELD(created_position_side) +PF_PREFIX_FIELD(created_position_cycle_seq) +PF_PREFIX_FIELD(created_after_position_close_in_bar) +PF_PREFIX_FIELD(over_pyramiding_cap_at_placement) +PF_PREFIX_FIELD(same_id_stop_deferred_close_all_bar) +PF_PREFIX_FIELD(same_id_stop_deferred_close_all_incarnation) +PF_PREFIX_FIELD(reverses_same_bar_market_from_flat) +PF_PREFIX_FIELD(paired_flat_market_candidate) +PF_PREFIX_FIELD(paired_flat_market_own_qty) +PF_PREFIX_FIELD(paired_flat_market_signal_close) +PF_PREFIX_FIELD(paired_flat_market_signal_equity) +PF_PREFIX_FIELD(paired_flat_market_signal_margin_pct) +PF_PREFIX_FIELD(paired_flat_market_signal_pointvalue) +PF_PREFIX_FIELD(paired_flat_market_signal_fx) +PF_PREFIX_FIELD(paired_flat_market_peer_seq) +PF_PREFIX_FIELD(paired_flat_market_transaction_qty) +PF_PREFIX_FIELD(default_flat_market_gross_candidate) +PF_PREFIX_FIELD(tv_carry_qty) +PF_PREFIX_FIELD(frozen_default_qty) +PF_PREFIX_FIELD(default_stop_placement_qty) +PF_PREFIX_FIELD(default_stop_placement_equity) +PF_PREFIX_FIELD(default_stop_placement_signal_close) +PF_PREFIX_FIELD(default_stop_sizing_price) +PF_PREFIX_FIELD(sizing_equity) +PF_PREFIX_FIELD(sizing_price) +PF_PREFIX_FIELD(sizing_fx) +PF_PREFIX_FIELD(sizing_mark) +PF_PREFIX_FIELD(opening_affordability_exemption_candidate) +PF_PREFIX_FIELD(explicit_flat_admission_candidate) +PF_PREFIX_FIELD(explicit_placement_equity) +PF_PREFIX_FIELD(explicit_slipped_signal_close) +PF_PREFIX_FIELD(affordability_placement_equity) +PF_PREFIX_FIELD(affordability_signal_price) +PF_PREFIX_FIELD(affordability_held_qty) +PF_PREFIX_FIELD(affordability_close_only) +PF_PREFIX_FIELD(rounded_signal_cost_close_only) +PF_PREFIX_FIELD(signal_close_mc_bar) +PF_PREFIX_FIELD(signal_close_mc_entry_incarnation) +PF_PREFIX_FIELD(signal_close_mc_fill_seq) +PF_PREFIX_FIELD(signal_close_mc_remaining_qty) +PF_PREFIX_FIELD(comment) +PF_PREFIX_FIELD(comment_truncated) +PF_PREFIX_FIELD(comment_hash64) +PF_PREFIX_FIELD(requested_partial) +PF_PREFIX_FIELD(full_percent_exit_request) +PF_PREFIX_FIELD(pooc_global_full_exit_dynamic_qty) +PF_PREFIX_FIELD(pooc_global_full_exit_tracks_bound_adds) +PF_PREFIX_FIELD(pooc_global_full_exit_bound_add) +PF_PREFIX_FIELD(created_while_in_position) +PF_PREFIX_FIELD(sbmt_member) +PF_PREFIX_FIELD(sbmt_own_qty) +PF_PREFIX_FIELD(sbmt_tx_qty) +PF_PREFIX_FIELD(sbmt_kept_over_cap) +PF_PREFIX_FIELD(sbmt_close_qty) +PF_PREFIX_FIELD(sbmt_close_buy) +PF_PREFIX_FIELD(suppress_as_declined_reversal_close) +PF_PREFIX_FIELD(dormant_bracket) +PF_PREFIX_FIELD(dormant_reissue_pending) +PF_PREFIX_FIELD(dormant_original_stop_price) +PF_PREFIX_FIELD(dormant_hold_bar) +PF_PREFIX_FIELD(dormant_reversal_kill_bar) +PF_PREFIX_FIELD(dormant_trail_best) +PF_PREFIX_FIELD(dormant_trail_best_start) +PF_PREFIX_FIELD(dormant_trail_leg_dead) +PF_PREFIX_FIELD(suppressed_close_consumed_ledger_qty) +PF_PREFIX_FIELD(suppressed_close_retired_ledger_qty) +PF_PREFIX_FIELD(short_seed_collision_role) diff --git a/tests/fixtures/pending_order_prefix/c45-v1.hpp b/tests/fixtures/pending_order_prefix/c45-v1.hpp new file mode 100644 index 00000000..a5fa49e9 --- /dev/null +++ b/tests/fixtures/pending_order_prefix/c45-v1.hpp @@ -0,0 +1,113 @@ +// Exact c45 v1 POD declarations; only typedef/tag names changed for compile comparison. +#pragma once +#include +typedef struct c45_pending_order_s { + uint32_t struct_version; + uint32_t size; + char id[64]; + uint8_t id_truncated; + uint64_t id_hash64; + char from_entry[64]; + uint8_t from_entry_truncated; + uint64_t from_entry_hash64; + int32_t type; + uint8_t is_long; + double limit_price; + double stop_price; + double trail_points; + double trail_price; + double trail_offset; + double profit_ticks; + double loss_ticks; + double qty; + int32_t qty_type; + double qty_percent; + char oca_name[64]; + uint8_t oca_name_truncated; + uint64_t oca_name_hash64; + int32_t oca_type; + int32_t created_bar; + int64_t created_seq; + uint64_t incarnation; + uint8_t created_by_same_id_replacement; + uint64_t replaced_default_market_incarnation; + uint8_t declined_by_replaced_short_market; + uint64_t replaced_exit_order_incarnation; + uint64_t recreated_after_named_cancelled_entry_incarnation; + uint64_t named_cancel_surviving_exit_incarnation; + uint8_t stop_limit_activated; + uint8_t coof_suppress_stop_on_entry_bar; + uint8_t coof_suppress_limit_on_entry_bar; + uint8_t created_during_coof_recalc; + uint8_t coof_born_at_close_recalc; + uint8_t coof_born_mid_bar; + int32_t coof_cascade_seg_i; + uint8_t coof_cascade_inflight_fires; + int32_t created_position_side; + int64_t created_position_cycle_seq; + uint8_t created_after_position_close_in_bar; + uint8_t over_pyramiding_cap_at_placement; + int32_t same_id_stop_deferred_close_all_bar; + uint64_t same_id_stop_deferred_close_all_incarnation; + uint8_t reverses_same_bar_market_from_flat; + uint8_t paired_flat_market_candidate; + double paired_flat_market_own_qty; + double paired_flat_market_signal_close; + double paired_flat_market_signal_equity; + double paired_flat_market_signal_margin_pct; + double paired_flat_market_signal_pointvalue; + double paired_flat_market_signal_fx; + int64_t paired_flat_market_peer_seq; + double paired_flat_market_transaction_qty; + uint8_t default_flat_market_gross_candidate; + double tv_carry_qty; + double frozen_default_qty; + double default_stop_placement_qty; + double default_stop_placement_equity; + double default_stop_placement_signal_close; + double default_stop_sizing_price; + double sizing_equity; + double sizing_price; + double sizing_fx; + double sizing_mark; + uint8_t opening_affordability_exemption_candidate; + uint8_t explicit_flat_admission_candidate; + double explicit_placement_equity; + double explicit_slipped_signal_close; + double affordability_placement_equity; + double affordability_signal_price; + double affordability_held_qty; + uint8_t affordability_close_only; + uint8_t rounded_signal_cost_close_only; + int32_t signal_close_mc_bar; + uint64_t signal_close_mc_entry_incarnation; + uint64_t signal_close_mc_fill_seq; + double signal_close_mc_remaining_qty; + char comment[64]; + uint8_t comment_truncated; + uint64_t comment_hash64; + uint8_t requested_partial; + uint8_t full_percent_exit_request; + uint8_t pooc_global_full_exit_dynamic_qty; + uint8_t pooc_global_full_exit_tracks_bound_adds; + uint8_t pooc_global_full_exit_bound_add; + uint8_t created_while_in_position; + uint8_t sbmt_member; + double sbmt_own_qty; + double sbmt_tx_qty; + uint8_t sbmt_kept_over_cap; + double sbmt_close_qty; + uint8_t sbmt_close_buy; + uint8_t suppress_as_declined_reversal_close; + uint8_t dormant_bracket; + uint8_t dormant_reissue_pending; + double dormant_original_stop_price; + int32_t dormant_hold_bar; + int32_t dormant_reversal_kill_bar; + double dormant_trail_best; + double dormant_trail_best_start; + uint8_t dormant_trail_leg_dead; + double suppressed_close_consumed_ledger_qty; + double suppressed_close_retired_ledger_qty; + int32_t short_seed_collision_role; +} c45_pending_order_t; diff --git a/tests/fixtures/pending_quantity/README.md b/tests/fixtures/pending_quantity/README.md new file mode 100644 index 00000000..e925c848 --- /dev/null +++ b/tests/fixtures/pending_quantity/README.md @@ -0,0 +1,3 @@ +Exact shipped c45cf5a pending-order mirror header, retained for compile-time prefix-layout checks. +SHA-256: cf4f756ce681dfec396918be6bc03d25e8ed5b1dbf9b2515c40af4c2b3df5236 +No runtime or strategy reference is embedded. diff --git a/tests/fixtures/pending_quantity/c45_pending_order_mirror.hpp b/tests/fixtures/pending_quantity/c45_pending_order_mirror.hpp new file mode 100644 index 00000000..ce672052 --- /dev/null +++ b/tests/fixtures/pending_quantity/c45_pending_order_mirror.hpp @@ -0,0 +1,136 @@ +// GENERATED by scripts/gen_pending_order_mirror.py from include/pineforge/engine.hpp -- do not edit. +// 98 PendingOrder members mirrored (108 POD fields incl. struct_version/size). +#pragma once +#include + +/* C-compatible value snapshot of one resting pineforge::PendingOrder + * (spec 3.6). struct_version identifies the field set (this file: + * 1); size is sizeof(pf_pending_order_v1_t) as the producer + * compiled it. Strings are copied into a NUL-terminated char[64] + * (name_truncated = 1 when the source was longer than 63 bytes) with + * name_hash64 = FNV-1a 64 of the FULL source string. Enums are their + * int32 value; bool is 0/1 in a uint8_t. Append-only, like every + * pineforge.h POD. */ +typedef struct pf_pending_order_v1_s { + uint32_t struct_version; + uint32_t size; + char id[64]; + uint8_t id_truncated; + uint64_t id_hash64; + char from_entry[64]; + uint8_t from_entry_truncated; + uint64_t from_entry_hash64; + int32_t type; + uint8_t is_long; + double limit_price; + double stop_price; + double trail_points; + double trail_price; + double trail_offset; + double profit_ticks; + double loss_ticks; + double qty; + int32_t qty_type; + double qty_percent; + char oca_name[64]; + uint8_t oca_name_truncated; + uint64_t oca_name_hash64; + int32_t oca_type; + int32_t created_bar; + int64_t created_seq; + uint64_t incarnation; + uint8_t created_by_same_id_replacement; + uint64_t replaced_default_market_incarnation; + uint8_t declined_by_replaced_short_market; + uint64_t replaced_exit_order_incarnation; + uint64_t recreated_after_named_cancelled_entry_incarnation; + uint64_t named_cancel_surviving_exit_incarnation; + uint8_t stop_limit_activated; + uint8_t coof_suppress_stop_on_entry_bar; + uint8_t coof_suppress_limit_on_entry_bar; + uint8_t created_during_coof_recalc; + uint8_t coof_born_at_close_recalc; + uint8_t coof_born_mid_bar; + int32_t coof_cascade_seg_i; + uint8_t coof_cascade_inflight_fires; + int32_t created_position_side; + int64_t created_position_cycle_seq; + uint8_t created_after_position_close_in_bar; + uint8_t over_pyramiding_cap_at_placement; + int32_t same_id_stop_deferred_close_all_bar; + uint64_t same_id_stop_deferred_close_all_incarnation; + uint8_t reverses_same_bar_market_from_flat; + uint8_t paired_flat_market_candidate; + double paired_flat_market_own_qty; + double paired_flat_market_signal_close; + double paired_flat_market_signal_equity; + double paired_flat_market_signal_margin_pct; + double paired_flat_market_signal_pointvalue; + double paired_flat_market_signal_fx; + int64_t paired_flat_market_peer_seq; + double paired_flat_market_transaction_qty; + uint8_t default_flat_market_gross_candidate; + double tv_carry_qty; + double frozen_default_qty; + double default_stop_placement_qty; + double default_stop_placement_equity; + double default_stop_placement_signal_close; + double default_stop_sizing_price; + double sizing_equity; + double sizing_price; + double sizing_fx; + double sizing_mark; + uint8_t opening_affordability_exemption_candidate; + uint8_t explicit_flat_admission_candidate; + double explicit_placement_equity; + double explicit_slipped_signal_close; + double affordability_placement_equity; + double affordability_signal_price; + double affordability_held_qty; + uint8_t affordability_close_only; + uint8_t rounded_signal_cost_close_only; + int32_t signal_close_mc_bar; + uint64_t signal_close_mc_entry_incarnation; + uint64_t signal_close_mc_fill_seq; + double signal_close_mc_remaining_qty; + char comment[64]; + uint8_t comment_truncated; + uint64_t comment_hash64; + uint8_t requested_partial; + uint8_t full_percent_exit_request; + uint8_t pooc_global_full_exit_dynamic_qty; + uint8_t pooc_global_full_exit_tracks_bound_adds; + uint8_t pooc_global_full_exit_bound_add; + uint8_t created_while_in_position; + uint8_t sbmt_member; + double sbmt_own_qty; + double sbmt_tx_qty; + uint8_t sbmt_kept_over_cap; + double sbmt_close_qty; + uint8_t sbmt_close_buy; + uint8_t suppress_as_declined_reversal_close; + uint8_t dormant_bracket; + uint8_t dormant_reissue_pending; + double dormant_original_stop_price; + int32_t dormant_hold_bar; + int32_t dormant_reversal_kill_bar; + double dormant_trail_best; + double dormant_trail_best_start; + uint8_t dormant_trail_leg_dead; + double suppressed_close_consumed_ledger_qty; + double suppressed_close_retired_ledger_qty; + int32_t short_seed_collision_role; +} pf_pending_order_v1_t; + +/* One row of the self-describing layout table returned by + * strategy_pending_order_layout(): field name, C type spelling + * ("uint8_t", "int32_t", "int64_t", "uint64_t", "double", "char[64]", + * "uint32_t"), byte offset inside pf_pending_order_v1_t, byte size. */ +typedef struct pf_field_desc_s { + const char* name; + const char* type; + uint32_t offset; + uint32_t size; +} pf_field_desc_t; +#define PF_PENDING_ORDER_STRUCT_VERSION 1 +#define PF_PENDING_ORDER_STR_CAP 64 diff --git a/tests/fixtures/script_cpp_abi/basec45/README.md b/tests/fixtures/script_cpp_abi/basec45/README.md new file mode 100644 index 00000000..5e615d94 --- /dev/null +++ b/tests/fixtures/script_cpp_abi/basec45/README.md @@ -0,0 +1 @@ +Frozen exact header closure of shipped engine c45 (internal C++ namespace v4). Compile/link-only input for rejecting old C++ objects after PendingOrder representation changes. No runtime or strategy reference data is archived; no execution is permitted. version.h is supplied by the current configured build. Each UTF-8 file, original Git blob and deterministic compressed archive are hash-pinned in manifest.json. diff --git a/tests/fixtures/script_cpp_abi/basec45/headers.json.gz b/tests/fixtures/script_cpp_abi/basec45/headers.json.gz new file mode 100644 index 00000000..855f472a Binary files /dev/null and b/tests/fixtures/script_cpp_abi/basec45/headers.json.gz differ diff --git a/tests/fixtures/script_cpp_abi/basec45/manifest.json b/tests/fixtures/script_cpp_abi/basec45/manifest.json new file mode 100644 index 00000000..0d19d8be --- /dev/null +++ b/tests/fixtures/script_cpp_abi/basec45/manifest.json @@ -0,0 +1,69 @@ +{ + "source_commit": "c45cf5a4d0e67a2ac098d9066977e1fa21c408a9", + "internal_namespace": "engine_script_run_v4", + "archive": "headers.json.gz", + "archive_sha256": "58f1f0c2cabb5283ecd61275d6c8108f8f817cd9c5ea2cf6bef54aceb02d6f38", + "generated_dependency": "pineforge/version.h is supplied by --generated-include.", + "files": { + "pineforge/bar.hpp": { + "sha256": "665c2528dca64ac9474de622dfd7ffdb540ff91316f70317e39aada158d1e0de", + "git_blob": "836f73daab382e9c8d5a6d03a942a921e39b756f" + }, + "pineforge/broker_events.hpp": { + "sha256": "4f3d9948a4d1604750901fa3982d6ea49bd50dd15a86c4e5d690b83d1f5f23c0", + "git_blob": "2972d53c7b039af266f4da161301c01eec54c16d" + }, + "pineforge/compat/pine/intraday_cap.hpp": { + "sha256": "0ac0eab34625ab9a974037ff8c0217603a01deb81599350a4b24f7bf48ac0cbd", + "git_blob": "0aba76b6a78251df33e92f19290473adecf5a61e" + }, + "pineforge/compat/pine/intraday_order_budget.hpp": { + "sha256": "43f7700f9c66c630ab8a9d8fd899eecb03c2fd9aabff11d2dc2b7fa85f92981b", + "git_blob": "730d941fffa3939e375489f40a50901ccea9ec87" + }, + "pineforge/compat/pine/order_priority.hpp": { + "sha256": "e8ca1aa1d6d20c6ed57f4204a983f28a814b0202c87ee8691ebe1b36d3b57206", + "git_blob": "2e22275bfcb7f194bbcd5104c8b530b91c2db340" + }, + "pineforge/engine.hpp": { + "sha256": "3b4e2937a9b5f275dd119144373b1bf15e433092009500092cd32ea34963b293", + "git_blob": "2c7eb76f450cb9f2cbb171d8567d14caf2db463c" + }, + "pineforge/magnifier.hpp": { + "sha256": "c0ed220b6df054b44596a434d4f48b1c3e960ca329bb8644a405a608175e06ff", + "git_blob": "1bbc910a869d162900d7610fa0bd9a88776b7b0f" + }, + "pineforge/na.hpp": { + "sha256": "0e8c256fcdeda94a0855458fa3b8c4ad4f9864dc2aecce969934f6bea82fd0a5", + "git_blob": "e98505aadc68a09cbe5a14aacf591a799a15ecd2" + }, + "pineforge/order_priority.hpp": { + "sha256": "25bb7898972153b044675245db230c1171644856ef904f4dd02353b8968d433b", + "git_blob": "3e9fa5ee4121971a7962db8a60859aba6fb9ec95" + }, + "pineforge/pending_order_mirror.hpp": { + "sha256": "cf4f756ce681dfec396918be6bc03d25e8ed5b1dbf9b2515c40af4c2b3df5236", + "git_blob": "ce672052f43add9d5965e7655c116f7874bbc8f7" + }, + "pineforge/pineforge.h": { + "sha256": "1e0def60e31f529dcb821502c0f7104b473c06be0fa1df713be0f408e04e9288", + "git_blob": "232cd6c66d3f6e6ffdcffdf246e6a9dcce3ef3f8" + }, + "pineforge/position_close_obligation.hpp": { + "sha256": "6f6ad0aaca952dda7a9386cb78e57705b3494364dbd8970b0a051fe99d05b777", + "git_blob": "09576ee0f2a1d49becc46f5701f4369cf8f87bf6" + }, + "pineforge/series.hpp": { + "sha256": "e7709f67df9046c9eba163c2dbe91f8161084a955265ccf6b163355856c0afde", + "git_blob": "61baaef2c351d37b0d6a5eb179e91ce670eb83c3" + }, + "pineforge/session_time.hpp": { + "sha256": "f58c94bfae948e08fa21e52f20eae3940b31e398d83b800baf3a31f3a5fde669", + "git_blob": "af3a4472d852e0965947c006d0b6ad85304539e7" + }, + "pineforge/timeframe.hpp": { + "sha256": "5e4c312da463fdcff55e0887188523c8bbf85a2ae62f3317a2b23f22fe0161e2", + "git_blob": "3d1bc9d495ac7e5b39ea5bae57c9d9afe580192b" + } + } +} diff --git a/tests/test_calc_on_order_fills.cpp b/tests/test_calc_on_order_fills.cpp index 4690990d..45bdb4bc 100644 --- a/tests/test_calc_on_order_fills.cpp +++ b/tests/test_calc_on_order_fills.cpp @@ -941,8 +941,8 @@ class PoocCloseAtCRecalcProbe final : public CoofBase { && order.id == "__close__B") { ++deferred_close_count; deferred_close_born_at_c = - order.created_during_coof_recalc - && order.coof_born_at_close_recalc; + order.birth.from_fill() + && order.birth.at_terminal_fill(); } } const auto ledger = id_unclosed_qty_.find("B"); diff --git a/tests/test_default_flat_market_gross_admission.cpp b/tests/test_default_flat_market_gross_admission.cpp index 2b2c2400..67fa52ab 100644 --- a/tests/test_default_flat_market_gross_admission.cpp +++ b/tests/test_default_flat_market_gross_admission.cpp @@ -141,7 +141,7 @@ struct Probe : public BacktestEngine { << ":o=" << (order.oca_name.empty() ? "-" : order.oca_name) << "/" << order.oca_type << ":c=" << order.default_flat_market_gross_candidate - << ":r=" << order.created_by_same_id_replacement; + << ":r=" << (order.replaced_order_incarnation != 0); } orders << "]"; result.pending_book = orders.str(); @@ -217,7 +217,7 @@ struct Probe : public BacktestEngine { if (order.default_flat_market_gross_candidate) { ++candidates_after_signal; } - if (order.created_by_same_id_replacement) { + if ((order.replaced_order_incarnation != 0)) { ++replacements_after_signal; } } @@ -424,7 +424,7 @@ struct ConfigProbe : public BacktestEngine { << ":o=" << (order.oca_name.empty() ? "-" : order.oca_name) << "/" << order.oca_type << ":c=" << order.default_flat_market_gross_candidate - << ":r=" << order.created_by_same_id_replacement; + << ":r=" << (order.replaced_order_incarnation != 0); } orders << "]"; result.pending_book = orders.str(); diff --git a/tests/test_integer_short_margin_state.cpp b/tests/test_integer_short_margin_state.cpp index c56aaa39..71dbcc58 100644 --- a/tests/test_integer_short_margin_state.cpp +++ b/tests/test_integer_short_margin_state.cpp @@ -246,7 +246,11 @@ class DormantCheckpoint : public BacktestEngine { break; case Shape::OFF_GRID: position_qty_ = pyramid_entries_[0].qty = 100.5; break; case Shape::LIMIT_ONLY: owned.stop_price = qnan; owned.limit_price = 98.0; break; - case Shape::PARTIAL: owned.qty = 50.0; owned.requested_partial = true; break; + case Shape::PARTIAL: + owned.qty = 50.0; + owned.quantity_request.request(QuantityIntent::units(50.0)); + owned.quantity_request.reserve(50.0, 100.0); + break; case Shape::COARSE_FRACTIONAL: qty_step_ = 1.5; position_qty_ = pyramid_entries_[0].qty = 100.5; diff --git a/tests/test_live_state_hash.cpp b/tests/test_live_state_hash.cpp index 3486be4c..5830e855 100644 --- a/tests/test_live_state_hash.cpp +++ b/tests/test_live_state_hash.cpp @@ -390,17 +390,13 @@ class Probe final : public BacktestEngine { if (!s.pending_orders_.empty()) s.pending_orders_[0].created_after_position_close_in_bar = !s.pending_orders_[0].created_after_position_close_in_bar; }}, - {"pending_orders_[].created_while_in_position", [](Probe& s) { - if (!s.pending_orders_.empty()) - s.pending_orders_[0].created_while_in_position = !s.pending_orders_[0].created_while_in_position; - }}, {"pending_orders_[].rounded_signal_cost_close_only", [](Probe& s) { if (!s.pending_orders_.empty()) s.pending_orders_[0].rounded_signal_cost_close_only = !s.pending_orders_[0].rounded_signal_cost_close_only; }}, - {"pending_orders_[].created_by_same_id_replacement", [](Probe& s) { + {"pending_orders_[].replaced_order_incarnation", [](Probe& s) { if (!s.pending_orders_.empty()) - s.pending_orders_[0].created_by_same_id_replacement = !s.pending_orders_[0].created_by_same_id_replacement; + ++s.pending_orders_[0].replaced_order_incarnation; }}, {"pending_orders_[].declined_by_replaced_short_market", [](Probe& s) { if (!s.pending_orders_.empty()) @@ -414,17 +410,19 @@ class Probe final : public BacktestEngine { if (!s.pending_orders_.empty()) s.pending_orders_[0].coof_suppress_limit_on_entry_bar = !s.pending_orders_[0].coof_suppress_limit_on_entry_bar; }}, - {"pending_orders_[].created_during_coof_recalc", [](Probe& s) { + {"pending_orders_[].birth.fill_origin", [](Probe& s) { if (!s.pending_orders_.empty()) - s.pending_orders_[0].created_during_coof_recalc = !s.pending_orders_[0].created_during_coof_recalc; + s.pending_orders_[0].birth = OrderBirth::fill_evaluation(0, 0, + BirthCursor::point(BirthCursorDomain::HistoricalPath, 0, 4), 100, 1, 1, 1); }}, - {"pending_orders_[].coof_born_at_close_recalc", [](Probe& s) { + {"pending_orders_[].birth.terminal_cursor", [](Probe& s) { if (!s.pending_orders_.empty()) - s.pending_orders_[0].coof_born_at_close_recalc = !s.pending_orders_[0].coof_born_at_close_recalc; + s.pending_orders_[0].birth = OrderBirth::fill_evaluation(0, 0, + BirthCursor::point(BirthCursorDomain::HistoricalPath, 3, 4), 100, 1, 1, 1); }}, - {"pending_orders_[].coof_born_mid_bar", [](Probe& s) { + {"pending_orders_[].pine_birth_reach", [](Probe& s) { if (!s.pending_orders_.empty()) - s.pending_orders_[0].coof_born_mid_bar = !s.pending_orders_[0].coof_born_mid_bar; + s.pending_orders_[0].pine_birth_reach = PineHistoricalBirthReach::ExtremeWaypoints; }}, {"pending_orders_[].coof_cascade_inflight_fires", [](Probe& s) { if (!s.pending_orders_.empty()) @@ -488,9 +486,6 @@ class Probe final : public BacktestEngine { {"pending_orders_[].replaced_default_market_incarnation", [](Probe& s) { if (!s.pending_orders_.empty()) s.pending_orders_[0].replaced_default_market_incarnation += 1; }}, - {"pending_orders_[].replaced_exit_order_incarnation", [](Probe& s) { - if (!s.pending_orders_.empty()) s.pending_orders_[0].replaced_exit_order_incarnation += 1; - }}, {"pending_orders_[].recreated_after_named_cancelled_entry_incarnation", [](Probe& s) { if (!s.pending_orders_.empty()) s.pending_orders_[0].recreated_after_named_cancelled_entry_incarnation += 1; }}, diff --git a/tests/test_order_birth_provenance.cpp b/tests/test_order_birth_provenance.cpp new file mode 100644 index 00000000..1baa3b67 --- /dev/null +++ b/tests/test_order_birth_provenance.cpp @@ -0,0 +1,284 @@ +// Literal native event/cursor tests. No Pine source, corpus, or external tape. +#include +#include +#include +#include +#include +#include +#include +#include "fixtures/pending_order_prefix/c45-v1.hpp" +#define PF_PREFIX_FIELD(name) \ + static_assert(offsetof(pf_pending_order_v1_t, name) == offsetof(c45_pending_order_t, name), "v1 prefix offset changed"); \ + static_assert(sizeof(((pf_pending_order_v1_t*)0)->name) == sizeof(((c45_pending_order_t*)0)->name), "v1 prefix field size changed"); +#include "fixtures/pending_order_prefix/c45-fields.inc" +#undef PF_PREFIX_FIELD +static_assert(offsetof(pf_pending_order_v1_t, birth_cause) >= sizeof(c45_pending_order_t), "new facts must append after the v1 prefix"); +using namespace pineforge; +namespace pineforge { +void fill_pending_order_mirror(const PendingOrder&, pf_pending_order_v1_t*); +} +namespace { +int failed = 0; +#define CHECK(x) do { if (!(x)) { std::fprintf(stderr, "%s:%d: %s\n", __FILE__, __LINE__, #x); ++failed; } } while (0) +const double nan = std::numeric_limits::quiet_NaN(); +const Bar bars[] = {{100, 101, 99, 100, 1, 0}, {100, 110, 95, 108, 1, 60000}}; + +class Probe : public BacktestEngine { +public: + Probe() { + initial_capital_ = 100000; + calc_on_order_fills_ = true; + default_qty_type_ = QtyType::FIXED; + default_qty_value_ = 1; + pyramiding_ = 10; + commission_value_ = 0; + syminfo_mintick_ = 0.01; + } + const PendingOrder& get(const std::string& id) const { + for (const auto& o : pending_orders_) if (o.id == id) return o; + throw std::runtime_error("missing test order " + id); + } + void direct(const std::string& id) { strategy_entry(id, true, 1, nan, 1); } + void set_birth(const std::string& id, const OrderBirth& birth) { + for (auto& o : pending_orders_) if (o.id == id) { o.birth = birth; return; } + throw std::runtime_error("missing mutation target"); + } + void clear_trailing_trigger(const std::string& id) { + for (auto& o : pending_orders_) if (o.id == id) { + o.trail_points = o.trail_price = nan; + return; + } + } + std::size_t pending_count() const { return pending_orders_.size(); } + const std::vector& recorded_hashes() const { return broker_state_hashes_; } +}; + +// The first callback is triggered by fill1. It directly closes that lot, +// advancing the broker to fill2, then emits another order in the SAME callback. +// That order must still name fill1, while the next callback names fill2. +class DirectCascade : public Probe { +public: + int bar_one_calls = 0; + std::vector observed; + OrderBirth after_direct_fill; + OrderBirth cloned_command; + OrderBirth replaced_birth, replacement_birth; + uint64_t replaced_incarnation = 0, replacement_incarnation = 0; + int64_t replaced_priority = 0, replacement_priority = 0; + void on_bar(const Bar&) override { + if (bar_index_ == 0) { + strategy_entry("seed", true, nan, nan, 1); + direct("replace"); + return; + } + if (bar_index_ != 1) return; + const int call = bar_one_calls++; + direct("witness-" + std::to_string(call)); + observed.push_back(get("witness-" + std::to_string(call)).birth); + if (call == 0) { + replaced_birth = get("replace").birth; + replaced_incarnation = get("replace").incarnation; + replaced_priority = get("replace").created_seq; + direct("replace"); + replacement_birth = get("replace").birth; + replacement_incarnation = get("replace").incarnation; + replacement_priority = get("replace").created_seq; + strategy_close("seed", "", nan, nan, true); + direct("after-direct"); + after_direct_fill = get("after-direct").birth; + DirectCascade copy(*this); + copy.direct("clone-command"); + cloned_command = copy.get("clone-command").birth; + } + } +}; + +class LaterOpenPolicy : public Probe { +public: + int bar_one_calls = 0; + OrderBirth trailing_birth, priced_birth; + void on_bar(const Bar&) override { + if (bar_index_ == 0) { + strategy_entry("A", true, nan, nan, 1); + strategy_entry("B", true, nan, nan, 1); + return; + } + if (bar_index_ != 1 || bar_one_calls++ != 1) return; + strategy_exit("trailing", "A", nan, nan, 100000, 1); + strategy_exit("priced", "B", nan, 1); + trailing_birth = get("trailing").birth; + priced_birth = get("priced").birth; + } +}; + +class SegmentOrigin : public Probe { +public: + int bar_one_calls = 0; + OrderBirth receipt; + void on_bar(const Bar&) override { + if (bar_index_ == 0) { strategy_entry("stop", true, nan, 105, 1); return; } + if (bar_index_ == 1 && bar_one_calls++ == 0) { + direct("segment-witness"); + receipt = get("segment-witness").birth; + } + } +}; + +class ProducerOrigins : public Probe { +public: + bool captured = false; + std::vector births; + void on_bar(const Bar&) override { + if (bar_index_ == 0) { strategy_entry("seed", true, nan, nan, 1); return; } + if (bar_index_ != 1 || captured) return; + captured = true; + strategy_entry("entry", true, 1, nan, 1); + strategy_order("raw", true, 1, 1); + strategy_exit("exit", "seed", nan, 1); + for (const std::string id : {"entry", "raw", "exit"}) births.push_back(get(id).birth); + } +}; + +class ThrowsInFill : public Probe { +public: + void on_bar(const Bar&) override { + if (bar_index_ == 0) strategy_entry("seed", true, nan, nan, 1); + else throw std::runtime_error("literal callback failure"); + } +}; + +void rejects(const std::function& f) { + bool rejected = false; + try { f(); } catch (const std::invalid_argument&) { rejected = true; } + CHECK(rejected); +} + +void value_contract() { + const auto open = BirthCursor::point(BirthCursorDomain::HistoricalPath, 0, 4); + const auto close = BirthCursor::point(BirthCursorDomain::HistoricalPath, 3, 4); + const auto first = OrderBirth::fill_evaluation(2, 120000, open, 100, 7, 7, 1); + const auto later = OrderBirth::fill_evaluation(2, 120000, open, 100, 8, 8, 2); + const auto terminal = OrderBirth::fill_evaluation(2, 120000, close, 100, 9, 9, 3); + CHECK(first.from_fill() && !first.at_terminal_fill()); + CHECK(compat::pine::first_open_fill_evaluation(first)); + CHECK(!compat::pine::first_open_fill_evaluation(later)); + CHECK(terminal.at_terminal_fill() && !terminal.cursor().first_point()); + CHECK(terminal.cursor().following_segment() == -1); + // Equal prices do not collapse distinct physical path positions. + CHECK(first.cursor_price() == terminal.cursor_price()); + CHECK(first.cursor().index() != terminal.cursor().index()); + const auto batch = OrderBirth::fill_evaluation(2, 120000, open, 100, 10, 12, 1); + CHECK(batch.first_fill() == 10 && batch.last_fill() == 12); + const auto copy = batch; + CHECK(copy.first_fill() == 10 && copy.last_fill() == 12); + rejects([&] { OrderBirth::fill_evaluation(2, 0, open, 100, 0, 1, 1); }); + rejects([&] { OrderBirth::fill_evaluation(2, 0, open, 100, 3, 2, 1); }); + rejects([&] { OrderBirth::fill_evaluation(2, 0, open, 100, 1, 1, 0); }); + rejects([&] { OrderBirth::fill_evaluation(2, 0, open, nan, 1, 1, 1); }); + rejects([&] { OrderBirth::fill_evaluation(2, 0, BirthCursor{}, 100, 1, 1, 1); }); + rejects([&] { BirthCursor::point(BirthCursorDomain::HistoricalPath, 4, 4); }); + rejects([&] { BirthCursor::point(BirthCursorDomain::HistoricalPath, 0, 3); }); + rejects([&] { BirthCursor::segment(BirthCursorDomain::HistoricalPath, 3, 4); }); + rejects([&] { BirthCursor::point(BirthCursorDomain::None, 0, 4); }); +} +} + +int main() { + value_contract(); + DirectCascade direct; + direct.run(bars, 2); + CHECK(direct.observed.size() == 3); + if (direct.observed.size() >= 3) { + const auto& first = direct.observed[0]; + CHECK(first.from_fill() && first.first_fill() == 1 && first.last_fill() == 1); + CHECK(first.cursor().domain() == BirthCursorDomain::HistoricalPath); + CHECK(first.cursor().first_point() && first.cursor().count() == 4); + CHECK(first.bar() == 1 && first.timestamp() == 60000 && first.cursor_price() == 100); + CHECK(direct.after_direct_fill.first_fill() == 1); + CHECK(direct.observed[1].first_fill() == 2 && direct.observed[1].last_fill() == 2); + CHECK(direct.observed[1].evaluation_ordinal() == 2); + CHECK(direct.observed[2].cause() == OrderBirthCause::ChartEvaluation); + CHECK(direct.observed[2].first_fill() == 0); + CHECK(direct.cloned_command.cause() == OrderBirthCause::DirectCommand); + CHECK(direct.replaced_birth.cause() == OrderBirthCause::ChartEvaluation); + CHECK(direct.replaced_birth.bar() == 0); + CHECK(direct.replacement_birth.from_fill() && direct.replacement_birth.first_fill() == 1); + CHECK(direct.replacement_birth.bar() == 1); + CHECK(direct.replacement_incarnation > direct.replaced_incarnation); + CHECK(direct.replacement_priority == direct.replaced_priority); + } + const uint64_t original_hash = direct.broker_state_hash(); + DirectCascade copied(direct); + CHECK(copied.broker_state_hash() == original_hash); + copied.direct("external-command"); + CHECK(copied.get("external-command").birth.cause() == OrderBirthCause::DirectCommand); + CHECK(direct.broker_state_hash() == original_hash); + const Bar extended[] = {bars[0], bars[1], {108, 109, 107, 108, 1, 120000}}; + DirectCascade prefix, complete; + prefix.set_broker_state_hash_recording(true); + complete.set_broker_state_hash_recording(true); + prefix.run(extended, 2); + complete.run(extended, 3); + CHECK(prefix.recorded_hashes().size() == 2 && complete.recorded_hashes().size() == 3); + if (prefix.recorded_hashes().size() == 2 && complete.recorded_hashes().size() == 3) { + CHECK(prefix.recorded_hashes()[0] == complete.recorded_hashes()[0]); + CHECK(prefix.recorded_hashes()[1] == complete.recorded_hashes()[1]); + } + CHECK(prefix.get("witness-0").birth.first_fill() == complete.get("witness-0").birth.first_fill()); + CHECK(prefix.get("witness-0").birth.cursor().index() == complete.get("witness-0").birth.cursor().index()); + prefix.run(nullptr, 0); + CHECK(prefix.pending_count() == 0 && prefix.recorded_hashes().empty()); + for (int field = 0; field < 9; ++field) { + DirectCascade changed(direct); + const auto receipt = OrderBirth::fill_evaluation( + field == 0 ? 2 : 1, field == 1 ? 60001 : 60000, + field == 2 ? BirthCursor::segment(BirthCursorDomain::HistoricalPath, 0, 4) + : BirthCursor::point(field == 3 ? BirthCursorDomain::MagnifierTicks : BirthCursorDomain::HistoricalPath, + field == 4 ? 1 : 0, field == 3 ? 8 : 4), + field == 5 ? 101 : 100, field == 6 ? 2 : 1, + field == 7 || field == 6 ? 2 : 1, field == 8 ? 2 : 1); + changed.set_birth("witness-0", receipt); + CHECK(changed.broker_state_hash() != original_hash); + } + LaterOpenPolicy policy; + policy.run(bars, 2); + CHECK(policy.trailing_birth.from_fill() && policy.priced_birth.from_fill()); + CHECK(policy.trailing_birth.first_fill() == 2 && policy.priced_birth.first_fill() == 2); + CHECK(policy.trailing_birth.cursor().first_point()); + CHECK(policy.trailing_birth.evaluation_ordinal() == 2); + CHECK(!compat::pine::historical_cascade_reach(policy.get("trailing"))); + CHECK(compat::pine::historical_cascade_reach(policy.get("priced"))); + policy.clear_trailing_trigger("trailing"); + CHECK(!compat::pine::historical_cascade_reach(policy.get("trailing"))); + CHECK(policy.get("trailing").birth.first_fill() == 2); + pf_pending_order_v1_t mirror{}; + fill_pending_order_mirror(policy.get("trailing"), &mirror); + CHECK(mirror.created_during_coof_recalc == 1 && mirror.coof_born_mid_bar == 0); + CHECK(mirror.birth_first_fill == 2 && mirror.birth_cursor_index == 0); + CHECK(mirror.birth_evaluation_ordinal == 2); + SegmentOrigin segment; + segment.run(bars, 2); + CHECK(segment.receipt.from_fill()); + CHECK(segment.receipt.cursor().position() == BirthCursorPosition::Segment); + CHECK(segment.receipt.cursor().index() == 1 && segment.receipt.cursor_price() == 105); + CHECK(segment.receipt.evaluation_ordinal() == 1); + CHECK(!compat::pine::first_open_fill_evaluation(segment.receipt)); + ProducerOrigins producers; + producers.run(bars, 2); + CHECK(producers.births.size() == 3); + for (const auto& birth : producers.births) CHECK(birth.from_fill() && birth.first_fill() == 1); + ProducerOrigins magnified; + magnified.run(bars, 2, "1", "1", true, 4, MagnifierDistribution::ENDPOINTS); + CHECK(magnified.births.size() == 3); + for (const auto& birth : magnified.births) { + CHECK(birth.from_fill() && birth.first_fill() == 1); + CHECK(birth.cursor().domain() == BirthCursorDomain::MagnifierTicks); + CHECK(birth.cursor().first_point() && birth.cursor().count() == 4); + } + ThrowsInFill throwing; + try { throwing.run(bars, 2); } catch (const std::runtime_error&) {} + throwing.direct("after-throw"); + CHECK(throwing.get("after-throw").birth.cause() == OrderBirthCause::DirectCommand); + std::printf("order birth provenance: %d failure(s)\n", failed); + return failed ? 1 : 0; +} diff --git a/tests/test_pending_order_core.cpp b/tests/test_pending_order_core.cpp new file mode 100644 index 00000000..0824249f --- /dev/null +++ b/tests/test_pending_order_core.cpp @@ -0,0 +1,153 @@ +// Literal cross-component controls. No Pine compilation, reference tape, +// external broker, campaign, or generated expected values. +#include +#include +#include +#include +#include +#include + +using namespace pineforge; +namespace { +int checks = 0, failures = 0; +#define CHECK(x) do { ++checks; if (!(x)) { ++failures; std::fprintf(stderr,"FAIL %d %s\n",__LINE__,#x); } } while (0) +constexpr double missing = std::numeric_limits::quiet_NaN(); + +template +struct PrivateMember { friend typename Tag::Type access(Tag) { return Member; } }; +struct BindLayers { + using Type = void (BacktestEngine::*)(const std::string&, std::vector&); + friend Type access(BindLayers); +}; +template struct PrivateMember; + +class Book : public BacktestEngine { +public: + Book() { + initial_capital_ = 100000; + commission_value_ = 0; + margin_long_ = margin_short_ = 0; + pyramiding_ = 10; + default_qty_type_ = QtyType::FIXED; + default_qty_value_ = 4; + close_entries_rule_any_ = true; + current_bar_ = {100,100,100,100,1,0}; + } + void on_bar(const Bar&) override {} + void step() { + ++bar_index_; + current_bar_ = {100,100,100,100,1,int64_t(bar_index_)*60000}; + process_pending_orders(current_bar_); + } + void seed(double qty=2) { strategy_entry("E",true,missing,missing,qty); step(); } + void import_pending(const PendingOrder& order) { pending_orders_.push_back(order); } + void bind_layers() { std::vector retired; (this->*access(BindLayers{}))("E",retired); } + + void reverse() { strategy_entry("E",false); } + void close_one() { strategy_close("E","",1); } + void bracket(double percent) { strategy_exit("X","E",120,80,missing,missing,missing,percent); } + void deferred_bracket() { strategy_exit("X","E",missing,150,missing,missing,missing,50); } + const PendingOrder& order(const std::string& id) const { + for (const auto& o : pending_orders_) if (o.id==id) return o; + throw std::logic_error("missing literal pending order"); + } + double quantity() const { return position_qty_; } + PositionSide side() const { return position_side_; } + pf_pending_order_v1_t mirror(const std::string& id) const { + for (size_t i=0;i(this),int(i),&result,sizeof(result))==0); + return result; + } + throw std::logic_error("missing mirror owner"); + } +}; + +void deferred_close_can_join_later_layer_binding() { + Book b; b.seed(); + b.reverse(); b.close_one(); b.deferred_bracket(); + CHECK(b.side()==PositionSide::LONG && b.quantity()==2); + CHECK(std::isnan(b.order("__close__E").qty)); + CHECK(b.order("__close__E").qty_percent==50); + const auto& initial=b.order("__close__E").quantity_request; + CHECK(initial.intent().has_value()); + if (!initial.intent()) return; + CHECK(initial.intent()->kind()==QuantityIntent::Kind::Units && initial.intent()->units()==1); + CHECK(!initial.reservation().has_value()); + CHECK(!initial.is_partial(1e-9,1e-9) && !initial.requests_all()); + const auto before=b.mirror("__close__E"); + CHECK(before.requested_partial==0 && before.full_percent_exit_request==0); + CHECK(before.quantity_intent_kind==1 && before.quantity_intent_units==1); + CHECK(before.quantity_reservation_present==0); + // The normal pass may retire the deferred market close after binding it. + // Independently inspect that real binder with the exact pending values on + // a synthetic E4 book, before retirement hides its numeric receipt. + Book bound; bound.seed(4); + bound.import_pending(b.order("__close__E")); + bound.import_pending(b.order("X")); + bound.bind_layers(); + bool threw=false; + try { b.step(); } + catch (const std::exception& error) { + threw=true; std::fprintf(stderr,"layer-binding exception: %s\n",error.what()); + } + CHECK(!threw); + if (threw) return; + CHECK(b.side()==PositionSide::SHORT && b.quantity()==4); + const auto& close=bound.order("__close__E"); + CHECK(close.quantity_request.intent().has_value()); + if (!close.quantity_request.intent()) return; + CHECK(close.quantity_request.intent()->kind()==QuantityIntent::Kind::Units); + CHECK(close.quantity_request.intent()->units()==1); + CHECK(close.quantity_request.reservation().has_value()); + if (!close.quantity_request.reservation()) return; + CHECK(close.quantity_request.reservation()->units==2); + CHECK(close.quantity_request.reservation()->basis_units==4); + CHECK(close.qty==2 && bound.order("X").qty==2); + const auto after=bound.mirror("__close__E"); + CHECK(after.requested_partial==1 && after.full_percent_exit_request==0); + CHECK(after.quantity_intent_kind==1 && after.quantity_intent_units==1); + CHECK(after.quantity_reservation_present==1); + CHECK(after.quantity_reservation_units==2 && after.quantity_reservation_basis_units==4); +} + +void replacement_preserves_independent_quantity_and_birth() { + Book b; b.seed(); b.bracket(25); + const PendingOrder first=b.order("X"); + b.step(); b.bracket(50); + const auto& next=b.order("X"); + CHECK(next.incarnation!=first.incarnation); + CHECK(next.replaced_order_incarnation==first.incarnation); + CHECK(next.created_seq==first.created_seq); + CHECK(next.birth.timestamp()>first.birth.timestamp()); + CHECK(next.birth.cause()==OrderBirthCause::DirectCommand); + CHECK(next.quantity_request.intent()->kind()==QuantityIntent::Kind::Fraction); + CHECK(next.quantity_request.intent()->numerator()==50); + CHECK(next.quantity_request.reservation()->basis_units==2); + // Existing partial replacement retains the prior admitted amount, while + // the current caller's original fraction is recorded independently. + CHECK(next.quantity_request.reservation()->units==0.5 && next.qty==0.5); + CHECK(first.quantity_request.intent()->numerator()==25); + const auto mirrored=b.mirror("X"); + CHECK(mirrored.created_by_same_id_replacement==1); + CHECK(mirrored.replaced_exit_order_incarnation==first.incarnation); + CHECK(mirrored.replaced_order_incarnation==first.incarnation); + CHECK(mirrored.quantity_intent_numerator==50); + CHECK(mirrored.quantity_reservation_units==0.5); + CHECK(mirrored.birth_timestamp==next.birth.timestamp()); + CHECK(mirrored.created_during_coof_recalc==0); +} +} + +int main() { + const std::pair cases[] = { + {"deferred close binding",deferred_close_can_join_later_layer_binding}, + {"replacement quantity birth",replacement_preserves_independent_quantity_and_birth}, + }; + for (const auto& test : cases) { + try { test.second(); } + catch (const std::exception& error) { ++failures; std::fprintf(stderr,"case %s exception: %s\n",test.first,error.what()); } + } + std::printf("%d checks, %d failures\n",checks,failures); + return failures ? 1 : 0; +} diff --git a/tests/test_pending_placement_receipts.cpp b/tests/test_pending_placement_receipts.cpp new file mode 100644 index 00000000..7afdbd85 --- /dev/null +++ b/tests/test_pending_placement_receipts.cpp @@ -0,0 +1,172 @@ +// Literal command/identity tests. No Pine source, reference tape or grader. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace pineforge; +namespace { +constexpr double missing = std::numeric_limits::quiet_NaN(); +int passed = 0, failed = 0; +#define CHECK(value) do { if (value) ++passed; else { ++failed; \ + std::printf("FAIL %d: %s\n", __LINE__, #value); } } while (false) + +class Book final : public BacktestEngine { +public: + Book() { + initial_capital_ = 10000; + commission_value_ = 0; + margin_long_ = margin_short_ = 0; + pyramiding_ = 10; + current_bar_ = {100,100,100,100,1,0}; + } + void on_bar(const Bar&) override {} + void entry(const std::string& id, bool buy=true, double qty=1, double stop=missing) { + strategy_entry(id,buy,missing,stop,qty); + } + void raw(const std::string& id, bool buy=true, double qty=1, double stop=missing) { + strategy_order(id,buy,qty,missing,stop); + } + void exit(const std::string& id,const std::string& parent,double qty=missing) { + strategy_exit(id,parent,120,80,missing,missing,missing,100,"",qty); + } + void cancel(const std::string& id) { strategy_cancel(id); } + void cancel_all() { strategy_cancel_all(); } + void close(const std::string& id) { strategy_close(id); } + void pooc(bool value) { process_orders_on_close_=value; } + void capacity(int value) { pyramiding_=value; } + void advance() { + ++bar_index_; + current_bar_.timestamp = int64_t(bar_index_)*60000; + process_pending_orders(current_bar_); + } + void reset() { run(nullptr,0); } + double physical_qty() const { return position_qty_; } + PositionSide physical_side() const { return position_side_; } + int64_t cycle() const { return position_cycle_seq_; } + std::vector orders(const std::string& id) const { + std::vector result; + for(const auto& order:pending_orders_)if(order.id==id)result.push_back(order); + return result; + } + PendingOrder order(const std::string& id) const { + const auto found=orders(id); + if(found.size()!=1)throw std::runtime_error("expected one order "+id); + return found.front(); + } + pf_pending_order_v1_t mirror(const std::string& id) { + for(size_t i=0;i(this), + static_cast(i),&result,sizeof result)==0); + return result; + } + throw std::runtime_error("missing mirror "+id); + } +}; + +void entry_and_raw_predecessors() { + Book b; + b.entry("E");const auto first=b.order("E"); + CHECK(first.type==OrderType::MARKET&&first.replaced_order_incarnation==0); + b.entry("E",true,2,110);const auto stop=b.order("E"); + CHECK(stop.type==OrderType::ENTRY&&stop.replaced_order_incarnation==first.incarnation); + CHECK(stop.created_seq==first.created_seq&&stop.incarnation>first.incarnation); + b.raw("E",false,3,90);const auto raw=b.order("E"); + CHECK(raw.type==OrderType::RAW_ORDER&&raw.replaced_order_incarnation==stop.incarnation); + CHECK(raw.created_seq==first.created_seq&&raw.incarnation>stop.incarnation); + const auto mirrored=b.mirror("E"); + CHECK(mirrored.replaced_order_incarnation==stop.incarnation); + CHECK(mirrored.created_by_same_id_replacement==0); // legacy RAW projection stays false + b.raw("E",true,4);const auto raw_again=b.order("E"); + CHECK(raw_again.replaced_order_incarnation==raw.incarnation); + b.entry("E");const auto market=b.order("E"); + CHECK(market.replaced_order_incarnation==raw_again.incarnation); + CHECK(market.created_seq==first.created_seq); + CHECK(b.mirror("E").created_by_same_id_replacement==1); + b.cancel("E");b.entry("E");const auto fresh=b.order("E"); + CHECK(fresh.replaced_order_incarnation==0&&fresh.created_seq>market.created_seq); + CHECK(fresh.incarnation>market.incarnation); +} + +void named_cancel_is_not_replacement() { + Book b;b.entry("E",true,1,110);const auto original=b.order("E"); + b.exit("X","E");const auto child=b.order("X"); + b.cancel("E");b.entry("E",true,1,115);const auto recreated=b.order("E"); + CHECK(recreated.replaced_order_incarnation==0); + CHECK(recreated.recreated_after_named_cancelled_entry_incarnation==original.incarnation); + CHECK(recreated.named_cancel_surviving_exit_incarnation==child.incarnation); + b.exit("X","E");const auto child_replaced=b.order("X"); + CHECK(child_replaced.replaced_order_incarnation==child.incarnation); + CHECK(b.mirror("X").replaced_exit_order_incarnation==child.incarnation); + CHECK(child_replaced.created_seq==child.created_seq); + b.cancel_all();b.exit("X","E"); + CHECK(b.order("X").replaced_order_incarnation==0); +} + +void physical_and_projected_placement() { + Book b;b.entry("E",true,2);b.advance(); + CHECK(b.physical_qty()==2&&b.cycle()>0); + b.entry("ADD",true,1,110);const auto add=b.order("ADD"); + CHECK(add.created_position_side==PositionSide::LONG); + CHECK(add.created_position_cycle_seq==b.cycle()); + CHECK(b.mirror("ADD").created_while_in_position==0); // legacy label was EXIT-only + b.exit("X","E"); + CHECK(b.order("X").created_position_side==PositionSide::LONG); + CHECK(b.mirror("X").created_while_in_position==1); + b.cancel("X");b.cancel("ADD");b.pooc(true);b.close("E"); + CHECK(b.physical_qty()==2); // batched close claim has not physically executed + b.exit("AFTER_CLOSE","E",1); + CHECK(b.order("AFTER_CLOSE").created_position_side==PositionSide::FLAT); + CHECK(b.mirror("AFTER_CLOSE").created_while_in_position==0); + CHECK(b.physical_side()==PositionSide::LONG); // do not replace projected side with physical +} + +void exit_primary_and_extra_identity() { + Book b;b.entry("E",true,2);b.advance(); + b.entry("E",true,2,110);b.exit("X","E",1); + const auto first=b.order("X"); + b.exit("X","E",1);const auto legs=b.orders("X"); + CHECK(legs.size()==2); + if(legs.size()!=2)return; + CHECK(legs[0].replaced_order_incarnation==first.incarnation); + CHECK(legs[0].created_seq==first.created_seq); + CHECK(legs[1].replaced_order_incarnation==0); + CHECK(legs[1].incarnation!=legs[0].incarnation&&legs[1].created_seq!=legs[0].created_seq); + b.exit("X","E",1);const auto next=b.orders("X"); + CHECK(next.size()==2); + if(next.size()==2) { + CHECK(next[0].replaced_order_incarnation==legs[0].incarnation); + CHECK(next[1].replaced_order_incarnation==0); + } +} + +void copy_reset_and_rejected_replacement() { + Book b;b.raw("R",true,1,110);b.raw("R",true,2,115); + const auto order=b.order("R");Book copy=b; + CHECK(copy.order("R").replaced_order_incarnation==order.replaced_order_incarnation); + CHECK(copy.broker_state_hash()==b.broker_state_hash()); + copy.cancel("R");copy.raw("R",false,1,90); + CHECK(copy.order("R").replaced_order_incarnation==0); + CHECK(b.order("R").incarnation==order.incarnation); + copy.reset();Book fresh;fresh.raw("R");copy.raw("R"); + CHECK(copy.order("R").incarnation==fresh.order("R").incarnation); + CHECK(copy.order("R").replaced_order_incarnation==0); + Book rejected;rejected.entry("E");rejected.advance(); + rejected.raw("X",true,1,110);rejected.capacity(1); + rejected.entry("X",true,1,115); // same-side over-cap priced replacement: old erased, no new order + CHECK(rejected.orders("X").empty()); +} +} +int main() { + entry_and_raw_predecessors();named_cancel_is_not_replacement(); + physical_and_projected_placement();exit_primary_and_extra_identity(); + copy_reset_and_rejected_replacement(); + std::printf("%d passed, %d failed\n",passed,failed);return failed?1:0; +} diff --git a/tests/test_pending_quantity_intent.cpp b/tests/test_pending_quantity_intent.cpp new file mode 100644 index 00000000..eed96f66 --- /dev/null +++ b/tests/test_pending_quantity_intent.cpp @@ -0,0 +1,367 @@ +// Literal native request/reservation contracts. No external tapes, embedded +// platform expected trades, strategy compilation or campaign measurement. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace prior_mirror { +#include "fixtures/pending_quantity/c45_pending_order_mirror.hpp" +} + +using namespace pineforge; +static_assert(offsetof(pf_pending_order_v1_t, struct_version) == offsetof(prior_mirror::pf_pending_order_v1_t, struct_version), "legacy struct_version offset"); +static_assert(offsetof(pf_pending_order_v1_t, size) == offsetof(prior_mirror::pf_pending_order_v1_t, size), "legacy size offset"); +static_assert(offsetof(pf_pending_order_v1_t, id) == offsetof(prior_mirror::pf_pending_order_v1_t, id), "legacy id offset"); +static_assert(offsetof(pf_pending_order_v1_t, id_truncated) == offsetof(prior_mirror::pf_pending_order_v1_t, id_truncated), "legacy id_truncated offset"); +static_assert(offsetof(pf_pending_order_v1_t, id_hash64) == offsetof(prior_mirror::pf_pending_order_v1_t, id_hash64), "legacy id_hash64 offset"); +static_assert(offsetof(pf_pending_order_v1_t, from_entry) == offsetof(prior_mirror::pf_pending_order_v1_t, from_entry), "legacy from_entry offset"); +static_assert(offsetof(pf_pending_order_v1_t, from_entry_truncated) == offsetof(prior_mirror::pf_pending_order_v1_t, from_entry_truncated), "legacy from_entry_truncated offset"); +static_assert(offsetof(pf_pending_order_v1_t, from_entry_hash64) == offsetof(prior_mirror::pf_pending_order_v1_t, from_entry_hash64), "legacy from_entry_hash64 offset"); +static_assert(offsetof(pf_pending_order_v1_t, type) == offsetof(prior_mirror::pf_pending_order_v1_t, type), "legacy type offset"); +static_assert(offsetof(pf_pending_order_v1_t, is_long) == offsetof(prior_mirror::pf_pending_order_v1_t, is_long), "legacy is_long offset"); +static_assert(offsetof(pf_pending_order_v1_t, limit_price) == offsetof(prior_mirror::pf_pending_order_v1_t, limit_price), "legacy limit_price offset"); +static_assert(offsetof(pf_pending_order_v1_t, stop_price) == offsetof(prior_mirror::pf_pending_order_v1_t, stop_price), "legacy stop_price offset"); +static_assert(offsetof(pf_pending_order_v1_t, trail_points) == offsetof(prior_mirror::pf_pending_order_v1_t, trail_points), "legacy trail_points offset"); +static_assert(offsetof(pf_pending_order_v1_t, trail_price) == offsetof(prior_mirror::pf_pending_order_v1_t, trail_price), "legacy trail_price offset"); +static_assert(offsetof(pf_pending_order_v1_t, trail_offset) == offsetof(prior_mirror::pf_pending_order_v1_t, trail_offset), "legacy trail_offset offset"); +static_assert(offsetof(pf_pending_order_v1_t, profit_ticks) == offsetof(prior_mirror::pf_pending_order_v1_t, profit_ticks), "legacy profit_ticks offset"); +static_assert(offsetof(pf_pending_order_v1_t, loss_ticks) == offsetof(prior_mirror::pf_pending_order_v1_t, loss_ticks), "legacy loss_ticks offset"); +static_assert(offsetof(pf_pending_order_v1_t, qty) == offsetof(prior_mirror::pf_pending_order_v1_t, qty), "legacy qty offset"); +static_assert(offsetof(pf_pending_order_v1_t, qty_type) == offsetof(prior_mirror::pf_pending_order_v1_t, qty_type), "legacy qty_type offset"); +static_assert(offsetof(pf_pending_order_v1_t, qty_percent) == offsetof(prior_mirror::pf_pending_order_v1_t, qty_percent), "legacy qty_percent offset"); +static_assert(offsetof(pf_pending_order_v1_t, oca_name) == offsetof(prior_mirror::pf_pending_order_v1_t, oca_name), "legacy oca_name offset"); +static_assert(offsetof(pf_pending_order_v1_t, oca_name_truncated) == offsetof(prior_mirror::pf_pending_order_v1_t, oca_name_truncated), "legacy oca_name_truncated offset"); +static_assert(offsetof(pf_pending_order_v1_t, oca_name_hash64) == offsetof(prior_mirror::pf_pending_order_v1_t, oca_name_hash64), "legacy oca_name_hash64 offset"); +static_assert(offsetof(pf_pending_order_v1_t, oca_type) == offsetof(prior_mirror::pf_pending_order_v1_t, oca_type), "legacy oca_type offset"); +static_assert(offsetof(pf_pending_order_v1_t, created_bar) == offsetof(prior_mirror::pf_pending_order_v1_t, created_bar), "legacy created_bar offset"); +static_assert(offsetof(pf_pending_order_v1_t, created_seq) == offsetof(prior_mirror::pf_pending_order_v1_t, created_seq), "legacy created_seq offset"); +static_assert(offsetof(pf_pending_order_v1_t, incarnation) == offsetof(prior_mirror::pf_pending_order_v1_t, incarnation), "legacy incarnation offset"); +static_assert(offsetof(pf_pending_order_v1_t, created_by_same_id_replacement) == offsetof(prior_mirror::pf_pending_order_v1_t, created_by_same_id_replacement), "legacy created_by_same_id_replacement offset"); +static_assert(offsetof(pf_pending_order_v1_t, replaced_default_market_incarnation) == offsetof(prior_mirror::pf_pending_order_v1_t, replaced_default_market_incarnation), "legacy replaced_default_market_incarnation offset"); +static_assert(offsetof(pf_pending_order_v1_t, declined_by_replaced_short_market) == offsetof(prior_mirror::pf_pending_order_v1_t, declined_by_replaced_short_market), "legacy declined_by_replaced_short_market offset"); +static_assert(offsetof(pf_pending_order_v1_t, replaced_exit_order_incarnation) == offsetof(prior_mirror::pf_pending_order_v1_t, replaced_exit_order_incarnation), "legacy replaced_exit_order_incarnation offset"); +static_assert(offsetof(pf_pending_order_v1_t, recreated_after_named_cancelled_entry_incarnation) == offsetof(prior_mirror::pf_pending_order_v1_t, recreated_after_named_cancelled_entry_incarnation), "legacy recreated_after_named_cancelled_entry_incarnation offset"); +static_assert(offsetof(pf_pending_order_v1_t, named_cancel_surviving_exit_incarnation) == offsetof(prior_mirror::pf_pending_order_v1_t, named_cancel_surviving_exit_incarnation), "legacy named_cancel_surviving_exit_incarnation offset"); +static_assert(offsetof(pf_pending_order_v1_t, stop_limit_activated) == offsetof(prior_mirror::pf_pending_order_v1_t, stop_limit_activated), "legacy stop_limit_activated offset"); +static_assert(offsetof(pf_pending_order_v1_t, coof_suppress_stop_on_entry_bar) == offsetof(prior_mirror::pf_pending_order_v1_t, coof_suppress_stop_on_entry_bar), "legacy coof_suppress_stop_on_entry_bar offset"); +static_assert(offsetof(pf_pending_order_v1_t, coof_suppress_limit_on_entry_bar) == offsetof(prior_mirror::pf_pending_order_v1_t, coof_suppress_limit_on_entry_bar), "legacy coof_suppress_limit_on_entry_bar offset"); +static_assert(offsetof(pf_pending_order_v1_t, created_during_coof_recalc) == offsetof(prior_mirror::pf_pending_order_v1_t, created_during_coof_recalc), "legacy created_during_coof_recalc offset"); +static_assert(offsetof(pf_pending_order_v1_t, coof_born_at_close_recalc) == offsetof(prior_mirror::pf_pending_order_v1_t, coof_born_at_close_recalc), "legacy coof_born_at_close_recalc offset"); +static_assert(offsetof(pf_pending_order_v1_t, coof_born_mid_bar) == offsetof(prior_mirror::pf_pending_order_v1_t, coof_born_mid_bar), "legacy coof_born_mid_bar offset"); +static_assert(offsetof(pf_pending_order_v1_t, coof_cascade_seg_i) == offsetof(prior_mirror::pf_pending_order_v1_t, coof_cascade_seg_i), "legacy coof_cascade_seg_i offset"); +static_assert(offsetof(pf_pending_order_v1_t, coof_cascade_inflight_fires) == offsetof(prior_mirror::pf_pending_order_v1_t, coof_cascade_inflight_fires), "legacy coof_cascade_inflight_fires offset"); +static_assert(offsetof(pf_pending_order_v1_t, created_position_side) == offsetof(prior_mirror::pf_pending_order_v1_t, created_position_side), "legacy created_position_side offset"); +static_assert(offsetof(pf_pending_order_v1_t, created_position_cycle_seq) == offsetof(prior_mirror::pf_pending_order_v1_t, created_position_cycle_seq), "legacy created_position_cycle_seq offset"); +static_assert(offsetof(pf_pending_order_v1_t, created_after_position_close_in_bar) == offsetof(prior_mirror::pf_pending_order_v1_t, created_after_position_close_in_bar), "legacy created_after_position_close_in_bar offset"); +static_assert(offsetof(pf_pending_order_v1_t, over_pyramiding_cap_at_placement) == offsetof(prior_mirror::pf_pending_order_v1_t, over_pyramiding_cap_at_placement), "legacy over_pyramiding_cap_at_placement offset"); +static_assert(offsetof(pf_pending_order_v1_t, same_id_stop_deferred_close_all_bar) == offsetof(prior_mirror::pf_pending_order_v1_t, same_id_stop_deferred_close_all_bar), "legacy same_id_stop_deferred_close_all_bar offset"); +static_assert(offsetof(pf_pending_order_v1_t, same_id_stop_deferred_close_all_incarnation) == offsetof(prior_mirror::pf_pending_order_v1_t, same_id_stop_deferred_close_all_incarnation), "legacy same_id_stop_deferred_close_all_incarnation offset"); +static_assert(offsetof(pf_pending_order_v1_t, reverses_same_bar_market_from_flat) == offsetof(prior_mirror::pf_pending_order_v1_t, reverses_same_bar_market_from_flat), "legacy reverses_same_bar_market_from_flat offset"); +static_assert(offsetof(pf_pending_order_v1_t, paired_flat_market_candidate) == offsetof(prior_mirror::pf_pending_order_v1_t, paired_flat_market_candidate), "legacy paired_flat_market_candidate offset"); +static_assert(offsetof(pf_pending_order_v1_t, paired_flat_market_own_qty) == offsetof(prior_mirror::pf_pending_order_v1_t, paired_flat_market_own_qty), "legacy paired_flat_market_own_qty offset"); +static_assert(offsetof(pf_pending_order_v1_t, paired_flat_market_signal_close) == offsetof(prior_mirror::pf_pending_order_v1_t, paired_flat_market_signal_close), "legacy paired_flat_market_signal_close offset"); +static_assert(offsetof(pf_pending_order_v1_t, paired_flat_market_signal_equity) == offsetof(prior_mirror::pf_pending_order_v1_t, paired_flat_market_signal_equity), "legacy paired_flat_market_signal_equity offset"); +static_assert(offsetof(pf_pending_order_v1_t, paired_flat_market_signal_margin_pct) == offsetof(prior_mirror::pf_pending_order_v1_t, paired_flat_market_signal_margin_pct), "legacy paired_flat_market_signal_margin_pct offset"); +static_assert(offsetof(pf_pending_order_v1_t, paired_flat_market_signal_pointvalue) == offsetof(prior_mirror::pf_pending_order_v1_t, paired_flat_market_signal_pointvalue), "legacy paired_flat_market_signal_pointvalue offset"); +static_assert(offsetof(pf_pending_order_v1_t, paired_flat_market_signal_fx) == offsetof(prior_mirror::pf_pending_order_v1_t, paired_flat_market_signal_fx), "legacy paired_flat_market_signal_fx offset"); +static_assert(offsetof(pf_pending_order_v1_t, paired_flat_market_peer_seq) == offsetof(prior_mirror::pf_pending_order_v1_t, paired_flat_market_peer_seq), "legacy paired_flat_market_peer_seq offset"); +static_assert(offsetof(pf_pending_order_v1_t, paired_flat_market_transaction_qty) == offsetof(prior_mirror::pf_pending_order_v1_t, paired_flat_market_transaction_qty), "legacy paired_flat_market_transaction_qty offset"); +static_assert(offsetof(pf_pending_order_v1_t, default_flat_market_gross_candidate) == offsetof(prior_mirror::pf_pending_order_v1_t, default_flat_market_gross_candidate), "legacy default_flat_market_gross_candidate offset"); +static_assert(offsetof(pf_pending_order_v1_t, tv_carry_qty) == offsetof(prior_mirror::pf_pending_order_v1_t, tv_carry_qty), "legacy tv_carry_qty offset"); +static_assert(offsetof(pf_pending_order_v1_t, frozen_default_qty) == offsetof(prior_mirror::pf_pending_order_v1_t, frozen_default_qty), "legacy frozen_default_qty offset"); +static_assert(offsetof(pf_pending_order_v1_t, default_stop_placement_qty) == offsetof(prior_mirror::pf_pending_order_v1_t, default_stop_placement_qty), "legacy default_stop_placement_qty offset"); +static_assert(offsetof(pf_pending_order_v1_t, default_stop_placement_equity) == offsetof(prior_mirror::pf_pending_order_v1_t, default_stop_placement_equity), "legacy default_stop_placement_equity offset"); +static_assert(offsetof(pf_pending_order_v1_t, default_stop_placement_signal_close) == offsetof(prior_mirror::pf_pending_order_v1_t, default_stop_placement_signal_close), "legacy default_stop_placement_signal_close offset"); +static_assert(offsetof(pf_pending_order_v1_t, default_stop_sizing_price) == offsetof(prior_mirror::pf_pending_order_v1_t, default_stop_sizing_price), "legacy default_stop_sizing_price offset"); +static_assert(offsetof(pf_pending_order_v1_t, sizing_equity) == offsetof(prior_mirror::pf_pending_order_v1_t, sizing_equity), "legacy sizing_equity offset"); +static_assert(offsetof(pf_pending_order_v1_t, sizing_price) == offsetof(prior_mirror::pf_pending_order_v1_t, sizing_price), "legacy sizing_price offset"); +static_assert(offsetof(pf_pending_order_v1_t, sizing_fx) == offsetof(prior_mirror::pf_pending_order_v1_t, sizing_fx), "legacy sizing_fx offset"); +static_assert(offsetof(pf_pending_order_v1_t, sizing_mark) == offsetof(prior_mirror::pf_pending_order_v1_t, sizing_mark), "legacy sizing_mark offset"); +static_assert(offsetof(pf_pending_order_v1_t, opening_affordability_exemption_candidate) == offsetof(prior_mirror::pf_pending_order_v1_t, opening_affordability_exemption_candidate), "legacy opening_affordability_exemption_candidate offset"); +static_assert(offsetof(pf_pending_order_v1_t, explicit_flat_admission_candidate) == offsetof(prior_mirror::pf_pending_order_v1_t, explicit_flat_admission_candidate), "legacy explicit_flat_admission_candidate offset"); +static_assert(offsetof(pf_pending_order_v1_t, explicit_placement_equity) == offsetof(prior_mirror::pf_pending_order_v1_t, explicit_placement_equity), "legacy explicit_placement_equity offset"); +static_assert(offsetof(pf_pending_order_v1_t, explicit_slipped_signal_close) == offsetof(prior_mirror::pf_pending_order_v1_t, explicit_slipped_signal_close), "legacy explicit_slipped_signal_close offset"); +static_assert(offsetof(pf_pending_order_v1_t, affordability_placement_equity) == offsetof(prior_mirror::pf_pending_order_v1_t, affordability_placement_equity), "legacy affordability_placement_equity offset"); +static_assert(offsetof(pf_pending_order_v1_t, affordability_signal_price) == offsetof(prior_mirror::pf_pending_order_v1_t, affordability_signal_price), "legacy affordability_signal_price offset"); +static_assert(offsetof(pf_pending_order_v1_t, affordability_held_qty) == offsetof(prior_mirror::pf_pending_order_v1_t, affordability_held_qty), "legacy affordability_held_qty offset"); +static_assert(offsetof(pf_pending_order_v1_t, affordability_close_only) == offsetof(prior_mirror::pf_pending_order_v1_t, affordability_close_only), "legacy affordability_close_only offset"); +static_assert(offsetof(pf_pending_order_v1_t, rounded_signal_cost_close_only) == offsetof(prior_mirror::pf_pending_order_v1_t, rounded_signal_cost_close_only), "legacy rounded_signal_cost_close_only offset"); +static_assert(offsetof(pf_pending_order_v1_t, signal_close_mc_bar) == offsetof(prior_mirror::pf_pending_order_v1_t, signal_close_mc_bar), "legacy signal_close_mc_bar offset"); +static_assert(offsetof(pf_pending_order_v1_t, signal_close_mc_entry_incarnation) == offsetof(prior_mirror::pf_pending_order_v1_t, signal_close_mc_entry_incarnation), "legacy signal_close_mc_entry_incarnation offset"); +static_assert(offsetof(pf_pending_order_v1_t, signal_close_mc_fill_seq) == offsetof(prior_mirror::pf_pending_order_v1_t, signal_close_mc_fill_seq), "legacy signal_close_mc_fill_seq offset"); +static_assert(offsetof(pf_pending_order_v1_t, signal_close_mc_remaining_qty) == offsetof(prior_mirror::pf_pending_order_v1_t, signal_close_mc_remaining_qty), "legacy signal_close_mc_remaining_qty offset"); +static_assert(offsetof(pf_pending_order_v1_t, comment) == offsetof(prior_mirror::pf_pending_order_v1_t, comment), "legacy comment offset"); +static_assert(offsetof(pf_pending_order_v1_t, comment_truncated) == offsetof(prior_mirror::pf_pending_order_v1_t, comment_truncated), "legacy comment_truncated offset"); +static_assert(offsetof(pf_pending_order_v1_t, comment_hash64) == offsetof(prior_mirror::pf_pending_order_v1_t, comment_hash64), "legacy comment_hash64 offset"); +static_assert(offsetof(pf_pending_order_v1_t, requested_partial) == offsetof(prior_mirror::pf_pending_order_v1_t, requested_partial), "legacy requested_partial offset"); +static_assert(offsetof(pf_pending_order_v1_t, full_percent_exit_request) == offsetof(prior_mirror::pf_pending_order_v1_t, full_percent_exit_request), "legacy full_percent_exit_request offset"); +static_assert(offsetof(pf_pending_order_v1_t, pooc_global_full_exit_dynamic_qty) == offsetof(prior_mirror::pf_pending_order_v1_t, pooc_global_full_exit_dynamic_qty), "legacy pooc_global_full_exit_dynamic_qty offset"); +static_assert(offsetof(pf_pending_order_v1_t, pooc_global_full_exit_tracks_bound_adds) == offsetof(prior_mirror::pf_pending_order_v1_t, pooc_global_full_exit_tracks_bound_adds), "legacy pooc_global_full_exit_tracks_bound_adds offset"); +static_assert(offsetof(pf_pending_order_v1_t, pooc_global_full_exit_bound_add) == offsetof(prior_mirror::pf_pending_order_v1_t, pooc_global_full_exit_bound_add), "legacy pooc_global_full_exit_bound_add offset"); +static_assert(offsetof(pf_pending_order_v1_t, created_while_in_position) == offsetof(prior_mirror::pf_pending_order_v1_t, created_while_in_position), "legacy created_while_in_position offset"); +static_assert(offsetof(pf_pending_order_v1_t, sbmt_member) == offsetof(prior_mirror::pf_pending_order_v1_t, sbmt_member), "legacy sbmt_member offset"); +static_assert(offsetof(pf_pending_order_v1_t, sbmt_own_qty) == offsetof(prior_mirror::pf_pending_order_v1_t, sbmt_own_qty), "legacy sbmt_own_qty offset"); +static_assert(offsetof(pf_pending_order_v1_t, sbmt_tx_qty) == offsetof(prior_mirror::pf_pending_order_v1_t, sbmt_tx_qty), "legacy sbmt_tx_qty offset"); +static_assert(offsetof(pf_pending_order_v1_t, sbmt_kept_over_cap) == offsetof(prior_mirror::pf_pending_order_v1_t, sbmt_kept_over_cap), "legacy sbmt_kept_over_cap offset"); +static_assert(offsetof(pf_pending_order_v1_t, sbmt_close_qty) == offsetof(prior_mirror::pf_pending_order_v1_t, sbmt_close_qty), "legacy sbmt_close_qty offset"); +static_assert(offsetof(pf_pending_order_v1_t, sbmt_close_buy) == offsetof(prior_mirror::pf_pending_order_v1_t, sbmt_close_buy), "legacy sbmt_close_buy offset"); +static_assert(offsetof(pf_pending_order_v1_t, suppress_as_declined_reversal_close) == offsetof(prior_mirror::pf_pending_order_v1_t, suppress_as_declined_reversal_close), "legacy suppress_as_declined_reversal_close offset"); +static_assert(offsetof(pf_pending_order_v1_t, dormant_bracket) == offsetof(prior_mirror::pf_pending_order_v1_t, dormant_bracket), "legacy dormant_bracket offset"); +static_assert(offsetof(pf_pending_order_v1_t, dormant_reissue_pending) == offsetof(prior_mirror::pf_pending_order_v1_t, dormant_reissue_pending), "legacy dormant_reissue_pending offset"); +static_assert(offsetof(pf_pending_order_v1_t, dormant_original_stop_price) == offsetof(prior_mirror::pf_pending_order_v1_t, dormant_original_stop_price), "legacy dormant_original_stop_price offset"); +static_assert(offsetof(pf_pending_order_v1_t, dormant_hold_bar) == offsetof(prior_mirror::pf_pending_order_v1_t, dormant_hold_bar), "legacy dormant_hold_bar offset"); +static_assert(offsetof(pf_pending_order_v1_t, dormant_reversal_kill_bar) == offsetof(prior_mirror::pf_pending_order_v1_t, dormant_reversal_kill_bar), "legacy dormant_reversal_kill_bar offset"); +static_assert(offsetof(pf_pending_order_v1_t, dormant_trail_best) == offsetof(prior_mirror::pf_pending_order_v1_t, dormant_trail_best), "legacy dormant_trail_best offset"); +static_assert(offsetof(pf_pending_order_v1_t, dormant_trail_best_start) == offsetof(prior_mirror::pf_pending_order_v1_t, dormant_trail_best_start), "legacy dormant_trail_best_start offset"); +static_assert(offsetof(pf_pending_order_v1_t, dormant_trail_leg_dead) == offsetof(prior_mirror::pf_pending_order_v1_t, dormant_trail_leg_dead), "legacy dormant_trail_leg_dead offset"); +static_assert(offsetof(pf_pending_order_v1_t, suppressed_close_consumed_ledger_qty) == offsetof(prior_mirror::pf_pending_order_v1_t, suppressed_close_consumed_ledger_qty), "legacy suppressed_close_consumed_ledger_qty offset"); +static_assert(offsetof(pf_pending_order_v1_t, suppressed_close_retired_ledger_qty) == offsetof(prior_mirror::pf_pending_order_v1_t, suppressed_close_retired_ledger_qty), "legacy suppressed_close_retired_ledger_qty offset"); +static_assert(offsetof(pf_pending_order_v1_t, short_seed_collision_role) == offsetof(prior_mirror::pf_pending_order_v1_t, short_seed_collision_role), "legacy short_seed_collision_role offset"); +namespace pineforge { +void fill_pending_order_mirror(const PendingOrder&, pf_pending_order_v1_t*); +} +namespace { +int checks = 0, failures = 0; +#define CHECK(x) do { ++checks; if (!(x)) { ++failures; std::printf("FAIL %d %s\n", __LINE__, #x); } } while (0) +constexpr double nan = std::numeric_limits::quiet_NaN(); +bool partial(const PendingOrder& o) { return o.quantity_request.is_partial(1e-9, 1e-9); } + +void quantity_values_have_distinct_meaning() { + QuantityRequest request; + CHECK(!request.intent() && !request.reservation()); + bool refused = false; + try { request.reserve(1,4); } + catch (const std::logic_error&) { refused = true; } + CHECK(refused && !request.reservation()); + for (double denominator : {0.0, -1.0, std::numeric_limits::infinity()}) { + bool invalid = false; + try { (void)QuantityIntent::fraction(1,denominator); } + catch (const std::invalid_argument&) { invalid = true; } + CHECK(invalid); + } + request.request(QuantityIntent::fraction(1,4)); + CHECK(request.is_partial(0,0)); + request.reserve(4,4); + CHECK(!request.is_partial(0,0)); + CHECK(request.intent()->numerator() == 1 && request.intent()->denominator() == 4); + request.request(QuantityIntent::units(3)); + CHECK(!request.reservation() && !request.requests_all()); + CHECK(request.intent()->units() == 3); + request.reserve(2,4); + CHECK(request.is_partial(0,0) && request.intent()->units() == 3); + request.request(QuantityIntent::all()); + CHECK(request.requests_all() && !request.reservation()); +} + +class Book : public BacktestEngine { +public: + Book() { + initial_capital_ = 100000; + commission_value_ = 0; + margin_long_ = margin_short_ = 0; + pyramiding_ = 10; + current_bar_ = {100,100,100,100,1,0}; + } + void on_bar(const Bar&) override {} + void entry(double qty) { strategy_entry("E", true, nan, nan, qty); } + void step(double price = 100) { + ++bar_index_; + current_bar_ = {price,price,price,price,1,int64_t(bar_index_) * 60000}; + process_pending_orders(current_bar_); + } + void seed(double qty) { entry(qty); step(); } + void exit(const char* id, double percent = 100, double units = nan, + const char* group = "") { + strategy_exit(id,"E",120,90,nan,nan,nan,percent,"",units,group); + } + void cancel(const char* id) { strategy_cancel(id); } + void reduce_group(double qty) { strategy_order("reduce",true,qty,nan,nan,"Q",2); } + void lot_step(double value) { qty_step_ = value; } + void reset() { run(nullptr,0); } + double position() const { return position_qty_; } + const std::vector& orders() const { return pending_orders_; } + PendingOrder& order(const std::string& id) { + for (auto& o : pending_orders_) if (o.id == id) return o; + throw std::logic_error("missing literal order"); + } +}; + +void requested_amount_is_separate_from_reserved_amount() { + Book units; units.seed(4); units.exit("X",100,1); + const auto& u = units.order("X"); + CHECK(u.qty == 1 && partial(u)); + CHECK(u.quantity_request.intent()->kind() == QuantityIntent::Kind::Units); + CHECK(u.quantity_request.intent()->units() == 1); + CHECK(u.quantity_request.reservation()->units == 1); + CHECK(u.quantity_request.reservation()->basis_units == 4); + Book fraction; fraction.seed(4); fraction.exit("X",25); + const auto& f = fraction.order("X"); + CHECK(f.qty == 1 && partial(f)); + CHECK(f.quantity_request.intent()->kind() == QuantityIntent::Kind::Fraction); + CHECK(f.quantity_request.intent()->numerator() == 25); + CHECK(f.quantity_request.intent()->denominator() == 100); + Book clipped; clipped.seed(4); clipped.exit("first",25); clipped.exit("all"); + const auto& a = clipped.order("all"); + CHECK(a.quantity_request.requests_all()); + CHECK(a.qty == 3 && partial(a)); + CHECK(a.quantity_request.reservation()->basis_units == 4); +} + +void minimum_slot_does_not_rewrite_fraction_intent() { + Book b; b.lot_step(1); b.seed(1); b.exit("half",50); + const auto& o = b.order("half"); + CHECK(o.qty == 1 && !partial(o)); + CHECK(!o.quantity_request.requests_all()); + CHECK(o.quantity_request.intent()->numerator() == 50); + CHECK(o.quantity_request.reservation()->units == 1); + CHECK(o.quantity_request.reservation()->basis_units == 1); + b.exit("blocked",50); + CHECK(b.orders().size() == 1); +} + +void normalized_full_amount_does_not_invent_all_intent() { + Book fraction; fraction.seed(4); fraction.exit("X",150); + const auto& f = fraction.order("X"); + CHECK(f.qty == 4 && f.qty_percent == 100 && !partial(f)); + CHECK(!f.quantity_request.requests_all()); + CHECK(f.quantity_request.intent()->numerator() == 150); + Book units; units.seed(4); units.exit("X",50,4); + const auto& u = units.order("X"); + CHECK(u.qty == 4 && !partial(u) && !u.quantity_request.requests_all()); + CHECK(u.quantity_request.intent()->kind() == QuantityIntent::Kind::Units); + CHECK(u.quantity_request.intent()->units() == 4); +} + +void deferred_reservation_binds_and_keeps_original_all() { + Book b; b.entry(4); b.exit("quarter",25); b.exit("rest"); + CHECK(std::isnan(b.order("quarter").qty)); + CHECK(!b.order("quarter").quantity_request.reservation()); + CHECK(partial(b.order("quarter"))); + CHECK(b.order("rest").quantity_request.requests_all()); + CHECK(!partial(b.order("rest"))); + b.step(); + CHECK(b.position() == 4); + CHECK(b.order("quarter").qty == 1); + CHECK(b.order("rest").qty == 3); + CHECK(partial(b.order("quarter")) && partial(b.order("rest"))); + CHECK(b.order("rest").quantity_request.requests_all()); + CHECK(b.order("rest").quantity_request.reservation()->basis_units == 4); +} + +void executable_reduction_does_not_change_reservation_history() { + Book b; b.seed(4); b.exit("all",100,nan,"Q"); + CHECK(!partial(b.order("all"))); + b.reduce_group(1); b.step(); + const auto& o = b.order("all"); + CHECK(o.qty < 4); // Actual OCA reduction, independent of source intent. + CHECK(o.quantity_request.requests_all() && !partial(o)); + CHECK(o.quantity_request.reservation()->units == 4); + CHECK(o.quantity_request.reservation()->basis_units == 4); +} + +void replacement_copy_cancel_and_reset() { + Book b; b.seed(4); b.exit("X",25); + const auto old = b.order("X").incarnation; + Book copy = b; + CHECK(copy.broker_state_hash() == b.broker_state_hash()); + b.exit("X"); + CHECK(b.order("X").incarnation != old); + CHECK(b.order("X").quantity_request.requests_all()); + CHECK(b.order("X").qty == 4); + CHECK(copy.order("X").qty == 1 && partial(copy.order("X"))); + copy.cancel("X"); CHECK(copy.orders().empty()); + CHECK(b.orders().size() == 1); + b.reset(); CHECK(b.orders().empty()); +} + +void partial_fill_preserves_existing_reissue_policy() { + Book b; b.seed(4); b.exit("X",25); b.step(90); + CHECK(b.position() == 3 && b.orders().empty()); + b.exit("X",25); + CHECK(b.orders().empty()); + b.exit("X"); + CHECK(b.orders().size() == 1); + CHECK(b.order("X").qty == 3 && b.order("X").quantity_request.requests_all()); +} + +void per_binding_leg_preserves_request() { + Book b; b.seed(1); b.exit("X",100,1); b.entry(2); b.exit("X",100,1); + int legs = 0; + for (const auto& o : b.orders()) if (o.type == OrderType::EXIT && o.id == "X") { + ++legs; + CHECK(o.quantity_request.intent()->kind() == QuantityIntent::Kind::Units); + CHECK(o.quantity_request.intent()->units() == 1); + CHECK(o.quantity_request.reservation()->units == o.qty); + CHECK(o.quantity_request.reservation()->basis_units == 1); + } + CHECK(legs == 2); +} + +void equal_legacy_flags_do_not_hide_distinct_intents_from_hash() { + Book a; a.seed(4); a.exit("X",25); + Book b = a; + b.order("X").quantity_request.request(QuantityIntent::units(1)); + b.order("X").quantity_request.reserve(1,4); + CHECK(partial(a.order("X")) && partial(b.order("X"))); + CHECK(a.broker_state_hash() != b.broker_state_hash()); + Book c = a; c.order("X").quantity_request.reserve(1,5); + CHECK(a.broker_state_hash() != c.broker_state_hash()); + Book d = a; d.order("X").quantity_request.request(QuantityIntent::fraction(25,100)); + CHECK(a.broker_state_hash() != d.broker_state_hash()); +} + +void legacy_mirror_prefix_and_new_facts() { + static_assert(offsetof(pf_pending_order_v1_t, quantity_intent_kind) + >= sizeof(prior_mirror::pf_pending_order_v1_t), "new fields append after old prefix"); + Book b; b.lot_step(1); b.seed(1); b.exit("half",50); + pf_pending_order_v1_t out{}; + CHECK(strategy_pending_order_get(&b,0,&out,sizeof(out)) == 0); + CHECK(out.requested_partial == 0 && out.full_percent_exit_request == 0); + CHECK(out.quantity_intent_kind == 2); + CHECK(out.quantity_intent_numerator == 50 && out.quantity_intent_denominator == 100); + CHECK(out.quantity_reservation_present == 1); + CHECK(out.quantity_reservation_units == 1 && out.quantity_reservation_basis_units == 1); + std::vector prefix(sizeof(out),0xA5); + CHECK(strategy_pending_order_get(&b,0,prefix.data(),sizeof(prior_mirror::pf_pending_order_v1_t)) == 0); + CHECK(std::memcmp(prefix.data(),&out,sizeof(prior_mirror::pf_pending_order_v1_t)) == 0); + for (size_t i=sizeof(prior_mirror::pf_pending_order_v1_t); i cases[] = { + {"quantity values", quantity_values_have_distinct_meaning}, + {"original versus reserved", requested_amount_is_separate_from_reserved_amount}, + {"minimum slot", minimum_slot_does_not_rewrite_fraction_intent}, + {"normalized full amount", normalized_full_amount_does_not_invent_all_intent}, + {"deferred binding", deferred_reservation_binds_and_keeps_original_all}, + {"OCA reduction", executable_reduction_does_not_change_reservation_history}, + {"replacement/copy/reset", replacement_copy_cancel_and_reset}, + {"partial fill/reissue", partial_fill_preserves_existing_reissue_policy}, + {"extra bindings", per_binding_leg_preserves_request}, + {"hash identity", equal_legacy_flags_do_not_hide_distinct_intents_from_hash}, + {"mirror prefix", legacy_mirror_prefix_and_new_facts}, + }; + for (const auto& test : cases) { + std::fprintf(stderr, "case: %s\n", test.first); + try { test.second(); } + catch (const std::exception& error) { + ++failures; + std::fprintf(stderr, "FAIL %s: %s\n", test.first, error.what()); + } + } + std::printf("%d checks, %d failures\n",checks,failures); + return failures ? 1 : 0; +} diff --git a/tests/test_pooc_global_full_exit.cpp b/tests/test_pooc_global_full_exit.cpp index 91542ed1..01f24f69 100644 --- a/tests/test_pooc_global_full_exit.cpp +++ b/tests/test_pooc_global_full_exit.cpp @@ -243,7 +243,7 @@ class ReservationProbe final : public BacktestEngine { // reservation decision runs. for (auto& order : pending_orders_) { if (order.id == "COOF_ADD" + suffix) { - order.created_during_coof_recalc = true; + order.birth = OrderBirth::fill_evaluation(0, 0, BirthCursor::point(BirthCursorDomain::HistoricalPath, 0, 4), 100.0, 1, 1, 1); } } break; diff --git a/tests/test_prearmed_exit_path_cursor.cpp b/tests/test_prearmed_exit_path_cursor.cpp index 977c7dc0..32fc3d98 100644 --- a/tests/test_prearmed_exit_path_cursor.cpp +++ b/tests/test_prearmed_exit_path_cursor.cpp @@ -224,7 +224,7 @@ class FreshParentProbe final : public BacktestEngine { } pending_book_size_on_reissue = pending_orders_.size(); fresh_parent_shape_seen = parent != nullptr && child != nullptr - && !parent->created_by_same_id_replacement + && (parent->replaced_order_incarnation == 0) && child->created_seq < parent->created_seq && child->created_bar == parent->created_bar; parent_cancel_provenance_seen = parent != nullptr @@ -247,13 +247,13 @@ class FreshParentProbe final : public BacktestEngine { < std::numeric_limits::max() && child->incarnation == parent->incarnation + 1; child_reissue_provenance_seen = child != nullptr - && child->created_by_same_id_replacement; + && (child->replaced_order_incarnation != 0); child_replacement_token_exact = child != nullptr && parent != nullptr && surviving_child_incarnation_at_cancel != 0 && parent->named_cancel_surviving_exit_incarnation == surviving_child_incarnation_at_cancel - && child->replaced_exit_order_incarnation + && child->replaced_order_incarnation == surviving_child_incarnation_at_cancel; } else if (bar_index_ == 2) { position_seen_on_trigger_bar = signed_position_size(); @@ -401,8 +401,7 @@ static bool retained_child_predicate_accepts(SortMutation mutation) { child.type = OrderType::EXIT; child.created_seq = 1; child.incarnation = 12; - child.created_by_same_id_replacement = true; - child.replaced_exit_order_incarnation = 10; + child.replaced_order_incarnation = 10; child.created_bar = 1; child.created_position_side = PositionSide::FLAT; child.qty = kNaN; @@ -459,7 +458,7 @@ static bool retained_child_predicate_accepts(SortMutation mutation) { context.stream_idle = false; break; case SortMutation::ParentReplacement: - parent.created_by_same_id_replacement = true; + parent.replaced_order_incarnation = 1; break; case SortMutation::MissingCancelToken: parent.recreated_after_named_cancelled_entry_incarnation = 0; @@ -468,7 +467,7 @@ static bool retained_child_predicate_accepts(SortMutation mutation) { parent.named_cancel_surviving_exit_incarnation = 0; break; case SortMutation::MismatchedChildReplacementToken: - child.replaced_exit_order_incarnation = 8; + child.replaced_order_incarnation = 8; break; case SortMutation::CancelTokenEqualsParent: parent.recreated_after_named_cancelled_entry_incarnation = @@ -509,10 +508,10 @@ static bool retained_child_predicate_accepts(SortMutation mutation) { child.qty = 1.0; break; case SortMutation::FreshChild: - child.created_by_same_id_replacement = false; + child.replaced_order_incarnation = 0; break; case SortMutation::ChildRequestedPartial: - child.requested_partial = true; + child.quantity_request.request(QuantityIntent::fraction(50.0, 100.0)); break; case SortMutation::ChildPercentPartial: child.qty_percent = 50.0; diff --git a/tests/test_stop_open_margin_script_state.cpp b/tests/test_stop_open_margin_script_state.cpp index 9d203552..05f7a61c 100644 --- a/tests/test_stop_open_margin_script_state.cpp +++ b/tests/test_stop_open_margin_script_state.cpp @@ -311,8 +311,10 @@ class PendingGuard : public BacktestEngine { pyramid_entries_[0].ordinary_stop_open = false; pyramid_entries_[0].ordinary_market_open = true; break; - case 15: pending.created_during_coof_recalc = true; break; - case 16: pending.created_while_in_position = true; break; + case 15: pending.birth = OrderBirth::fill_evaluation(0, 0, BirthCursor::point(BirthCursorDomain::HistoricalPath, 0, 4), 100.0, 1, 1, 1); break; + // A position-bound EXIT is distinct from the flat-born pending STOP. + case 16: pending.type = OrderType::EXIT; + pending.created_position_side = PositionSide::SHORT; break; case 17: pending_orders_.push_back(pending); break; default: break; }