diff --git a/CMakeLists.txt b/CMakeLists.txt index b157db1..4125ce4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -95,6 +95,8 @@ add_library(pineforge STATIC src/engine_stream.cpp src/engine_strategy_commands.cpp src/engine_trade_accessors.cpp + src/reservation_expansion.cpp + src/compat/pine/reservation_expansion.cpp src/pending_order_mirror.cpp src/magnifier.cpp src/math.cpp diff --git a/docs/Doxyfile b/docs/Doxyfile index 0607775..ef7d609 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -90,6 +90,7 @@ INPUT = ../include/pineforge/pineforge.h \ pages/install.md \ pages/integration-cmake.md \ pages/lifecycle.md \ + pages/fill-model.md \ pages/streaming.md \ pages/live-surface.md \ pages/report-schema.md \ diff --git a/docs/frozen-market-instruction.md b/docs/frozen-market-instruction.md new file mode 100644 index 0000000..452e856 --- /dev/null +++ b/docs/frozen-market-instruction.md @@ -0,0 +1,87 @@ +# Frozen Pine market instruction + +This is a bounded representation migration of an existing Pine compatibility +policy. `compat::pine::FrozenMarketInstruction` replaces six independent +PendingOrder sidecars with one exclusive source operation. It does not make +the existing cohort selector a generic native execution contract. + +## Source operation and state ownership + +| Role | Live owned facts | Existing facts consumed | +| --- | --- | --- | +| Ordinary | None | Ordinary sizing and execution state | +| Transaction | Own units; frozen total transaction units | Requested side; immutable placement-cap snapshot | +| TargetedClose | Explicit target entry ID | Original QuantityRequest Units; immutable created-position side | + +Only complete role construction and whole-operation revocation are supported. +Transaction and TargetedClose are mutually exclusive; there are no independent +membership, cap-retention or close-side switches. Revocation destroys the active +payload. A targeted close has no second quantity authority: later reservation +changes leave QuantityRequest's original Units amount intact. + +A source transaction captures the same expression as before: + + own + opposite position held net of earlier same-bar closes + + opposite pending entries' own units + +Its retained-over-cap property is true only when the Transaction role is active +and `over_pyramiding_cap_at_placement` is true. The former duplicate calculation +and the placement snapshot use the same position-side/count/cap expression; +none of those inputs changes between the two capture sites. A targeted close's +buy direction is its captured position side being SHORT. + +The own amount must be positive and finite. A positive-infinite total caused +by source-sum overflow remains representable because the prior source capture +allowed it. This is not a validated native execution amount. The existing +finite-total execution guards still exclude it from frozen transaction sizing; +ordinary sizing follows the same prior path. No new overflow admission policy +is introduced by this structural change. + +## Compatibility boundary retained + +The selector, full-book admission and execution scopes are unchanged: historical +close calculation; FIXED sizing; default FIFO; pyramiding at most one; no costs, +slippage, magnifier, stream or risk extensions. Exact source conditions remain +in `same_bar_market_tx_scope_is_live` and its command producers. + +The complete pending book must contain only accepted operations and at most +two MARKET entries with distinct IDs. An unrelated priced/raw/bracket/close-all +order, a third market entry, or leaving the scope revokes the source operations +for the whole book. Subsequent native dispatch uses the prior ordinary paths. +Same-ID replacement and named cancellation/recreation construct a fresh +operation with a new incarnation and current source snapshots. + +TargetedClose is intentionally not named ReducePosition. Existing Pine behavior +can materialize an opening artifact lot if the originally targeted side is gone +and a matching entry remains pending later in the source execution order. That +path remains intact. Lowering this source operation into separate generic +native transaction/reduction instructions remains future adapter work. + +This migration removes three direct PendingOrder booleans, rather than claiming +that an entire engine has reached a four-flag goal. The Pine cohort selector and +source-operation discriminator survive and must remain in compatibility audits. +No Pine selection predicate has been moved into the native contract by renaming. + +## Observation and compatibility + +The existing 142-field public pending-order mirror is append-only. Every old +field retains its type, size and offset, including the old full-prefix trailing +padding. The six old `sbmt_*` outputs are derived projections only; they are not +writable engine state. Ordinary/revoked operations project false flags and NaN +legacy quantity values exactly as the old inactive sidecars did. + +Six fields append the source kind, own/total units and target ID (char[64], +truncation indicator and full string hash). The broker hash folds the kind and +only its live payload; QuantityRequest and placement facts are already folded +at their owning order. Metadata mutation checks refuse hidden nested fields, +changed variant/enum alternatives, removed or conditional folds, and waivers. +The aggregate representation uses internal engine/PendingOrder ABI v7 and +broker/stream fingerprint epoch7. The complete mirror has155 fields: the142 +shipped fields, seven reservation facts and these six Pine instruction facts. +Public C ABI4 and stream API1 are unchanged. + +`test_frozen_market_instruction` uses literal price-100 fixtures and direct +native command transitions. It pins role construction, amounts in both +orientations, quantity ownership, whole-book revocation, replacement, +cancellation, hash sensitivity, string truncation and the full public prefix. +It does not use external tapes, expected provider trades or a grader. diff --git a/docs/pages/abi-stability.md b/docs/pages/abi-stability.md index e167ce3..0c9abb7 100644 --- a/docs/pages/abi-stability.md +++ b/docs/pages/abi-stability.md @@ -112,34 +112,45 @@ 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. Owner-bound leg activation changes `PendingOrder` layout again; both -`PendingOrder` and `BacktestEngine` use internal `engine_script_run_v6`. Exact -shipped149/v5 headers are compiled before mismatch links. Native and -generated-style old engine callers, plus standalone priority callers using -`vector` without any engine-method reference, must reject the -v6 archive. Matching current and historical symbol-control links must succeed. -Earlier base38/v2, f864/v3 and c45/v4 controls remain. A compilation failure -cannot masquerade as stale-layout rejection; no pairing executable runs. +runtime. Exact-owner reservation expansion and typed Pine market instructions +change `PendingOrder` layout after +shipped ff54/v6; both `PendingOrder` and `BacktestEngine` now use internal +`engine_script_run_v7`. Exact shipped ff54/v6 headers are authenticated before +compilation. Old native and generated-style engine callers and standalone +priority callers using `vector` must reject the v7 archive. +Matching current and historical symbol-control links must succeed. Earlier +base38/v2, f864/v3, c45/v4 and shipped149/v5 controls remain. Every translation +unit must compile before a mismatch link is accepted; no pairing executable runs. + +The standalone `ReservationExpansionCapture`, `ReservationExpansion` and +`ReservationGrowthSource` types establish their first C++ contract in the inline +`reservation_expansion_v1` namespace, including out-of-line methods. This version +is independent of the containing engine epoch. A frozen unshipped bf312 header +closure provides a negative control for the preceding unversioned draft; these +types did not exist in shipped ff54. Standalone method and capture-argument +pairings reject both stale directions, with matching positives. Inline accessors +still require matching headers; namespace versioning is not a serialization ABI. `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, `PF_ABI_VERSION` (4), and `strategy_stream_api_version()` (1) are unchanged. The pending-order v1 -mirror keeps all 128 shipped149 field names/types/offsets and its full old prefix; +mirror keeps all 142 shipped ff54 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 v6 module. +its own matching runtime; this check does not turn it into a v7 module. The integrated representation advances the broker fingerprint domain to -`pineforge-broker-state/v6` and stream fingerprint version to 6. These identify -changed serialized leg owner/bounds and Pine placement evidence, alongside -existing quantity, predecessor and birth facts. The Pine component schema remains 1; it is -independent of the aggregate fingerprint version. Prior v2/v3/v4/v5 fingerprints are +`pineforge-broker-state/v7` and stream fingerprint version to 7. These identify +changed serialized reservation capture/source ownership and Pine instruction +roles/payloads, alongside existing +leg activation, quantity, predecessor and birth facts. The Pine component schema remains 1; it is +independent of the aggregate fingerprint version. Prior v2/v3/v4/v5/v6 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/exit-leg-activation.md b/docs/pages/exit-leg-activation.md index 758246e..703c55d 100644 --- a/docs/pages/exit-leg-activation.md +++ b/docs/pages/exit-leg-activation.md @@ -54,7 +54,8 @@ other historical execution rules remain separate work. The effective-level accessor still reports resolved levels rather than masking eligibility. The internal object layout changes and requires matching C++ headers/library. -Both PendingOrder and BacktestEngine now use internal v6 with broker/stream -fingerprint v6. Exact shipped149/v5 engine and standalone PendingOrder clients +The activation slice shipped with internal v6. Subsequent reservation ownership +integration advances PendingOrder and BacktestEngine to v7 with broker/stream +fingerprint v7. Exact shipped ff54/v6 and earlier149/v5 standalone PendingOrder clients are checked by compile/link-only mismatch controls with matching positives. Public C ABI 4, stream API 1 and pending mirror version 1 are preserved. diff --git a/docs/pages/fill-model.md b/docs/pages/fill-model.md index cceba66..172bd6d 100644 --- a/docs/pages/fill-model.md +++ b/docs/pages/fill-model.md @@ -1,15 +1,30 @@ -# Broker state and fill ownership - -The broker processes a source-ordered command book against a price path. Its -physical position, logical close claims, reserved quantities and pyramiding -capacity are separate ledgers. A Pine entry ID is a reusable name; the order -incarnation and position cycle identify the owners of those ledgers. - -This document describes the first extracted ownership model. Other order, -reservation and path state is still represented in `BacktestEngine` and -`PendingOrder`; their consolidation is ongoing. The current economic rules -retain their tested domains. Moving a rule into a type does not establish -that it describes every TradingView configuration. +# Order execution model {#fill_model} + +PineForge is being developed as an independent C++ backtest and forward-execution +engine. Its native contracts describe orders, exposure, reservations and actual +execution events. Pine source interpretation belongs to the frontend boundary. +TradingView comparisons are compatibility evidence for a declared configuration; +they do not define every native operation. + +The migration is incomplete. The current engine still contains Pine-specific +admission and scheduling rules, and the ordinary bar scan and callback-driven +scan are separate. The responsibilities below guide their consolidation without +claiming that a single unified scheduler already exists. + +## Responsibilities + +| Submodel | Owned facts and transitions | Boundary | +|---|---|---| +| Command admission | Original quantity request, placement observation, accepted instruction identity, rejection cause | A source frontend interprets its calls; native admission operates on explicit transaction or position intent. | +| Exposure and reservation | Position cycle, physical quantity, reservation owner, admitted growth sources, committed growth and retirement | A proposed add is not an executed add. There is one mutable reservation capacity. | +| Trigger lifecycle | Stop/limit/trail definitions, activation bounds and owner binding, stop-limit activation and trail progress | An unready leg cannot supply a price or authorize a ready sibling. | +| Execution schedule | Candidate identity, event coordinate, dependency and explicit priority | Ordering does not grant admission, create quantity or activate a leg. | +| Settlement and observation | Executed quantity, physical lots, paid fees, OCA effects, risk follow-up and callbacks | Publish observations after the corresponding state change; report projections must not change admission equity. | + +These are responsibilities, not five independently switchable modes. Direction, +order kind, quantity intent and an outstanding owned claim are normal domain +state. Encoding unrelated permissions as enum values or moving them to another +object does not simplify the model. ## What qualifies as a generic flag @@ -37,6 +52,32 @@ scores are regression evidence, not a justification for keeping a compensating flag. Actual conflicting TradingView observations belong in an anomaly review record, separate from an unknown rule or an engine defect. +## Identity and quantity + +Every accepted pending object receives a fresh `incarnation`. A user-supplied ID +can be reused, and same-ID replacement can retain queue priority; neither implies +that the new object owns the old object's claims. `replaced_order_incarnation` +records the exact predecessor. A position cycle similarly distinguishes two +positions that happen to have the same direction. + +`QuantityRequest` retains the original Units, Fraction or All request and its +reservation basis. The executable `PendingOrder::qty` can change through OCA +reduction or committed reserved growth. That does not rewrite the original +request, reclassify its historical partial/full meaning, or create another +quantity ledger. + +An expansion capture belongs to an exact EXIT object and exposure cycle. Selected +source orders carry the receiving EXIT incarnation. After an actual primary fill, +the receiver gains only the positive same-side quantity increase belonging to its +captured cycle. A canceled or logically retired receiver cannot redirect that +growth to another exit with the same user ID. + +The first later successful entry-like admission closes the capture's population. +Canceling that later admission does not erase its historical cause. Previously +captured sources can still pay their exact receiver; later unrelated adds do not +join the capture. An old-cycle capture loses live-All authority while its ordinary +finite reservation remains available for the normal settlement path. + ## Opening checkpoint An accepted opening or add can create an opening-affordability checkpoint. @@ -77,6 +118,27 @@ Four historical long/short lifecycle labels had no economic consumers. They have been removed, along with their producers; the numerical floor-zero rules and their trade fixtures remain. Those labels are not alternate model states. +## Current dispatch sequence + +The ordinary pending-order scan updates risk state, processes due opening work, +finalizes source cohorts, updates trailing/relative prices and orders the book. +It then classifies and matches an exact pending handle. A pre-exit margin slice +can change the book between matching and dispatch, so the handle is resolved +again before applying the selected order. + +The primary fill updates physical exposure. Reservation growth is settled at the +existing post-primary checkpoint, before that order's OCA and risk follow-up. +Logically retired objects cannot be dispatched again while awaiting compaction. +Callbacks use committed fill events; a declined or zero-effect attempt is not +itself a fill event. Pine's optional quota interpretation separately observes +the attempt stages its own contract specifies. + +There is no universal `risk > exit > entry` priority. A forced action and a user +order have event coordinates, and an earlier event must be accounted before a +later one. Forced liquidation cannot be suppressed by an unready user stop. +An earlier risk fill can also invalidate the quantity or owner of a later +candidate, requiring another resolution of its identity and eligibility. + ## Distinct execution domains An opening receipt is broker-local. It is not an identity for every output: @@ -94,6 +156,28 @@ different information. The opening ownership extraction changes none of their path, callback or warmup policies. Terminal-close deferral is still selected by the existing financial policy; it has not been generalized by this model. +## Remaining consolidation + +The complete scheduler should build immutable candidates, choose the earliest +eligible event coordinate, satisfy causal dependencies, and then apply explicit +priority and stable submission identity. Losing candidate scans must not commit +trigger activation. Dependency cycles must be reported rather than hidden by +another pairwise preference. + +The existing two-sort arrangement does not yet provide that contract. In +particular, the sibling comparator has reproduced ordering-law failures for +unrelated interleavings and mixed trailing/non-trailing exits. Grouping by owner +alone does not repair the mixed-leg case. Replacing these sorts is separate from +the reservation-ownership change and requires its own native ordering tests and +Cloud compatibility assessment. + +A native reduction must never create new exposure. Some existing Pine close +interpretations can produce a new transaction after their old target disappears; +that interpretation must remain explicit at the source boundary until lowering +to native operations is complete. The current engine also has separate forward +ingress, callback, affordability and per-leg lifecycle work. This page does not +claim those migrations or the large-function cleanup are finished. + ## Verification and compatibility Literal state tests exercise consume-before-use, exempt presence, replacement, @@ -103,11 +187,9 @@ fixed reference population is required before reporting parity preservation. Hashes supplement those comparisons; a matching fingerprint is not a proof that all hidden strategy state is equal. -The new receipt replaces protected C++ members and changes the fingerprint -representation. Rebuild generated and native modules against matching headers -and runtime. The cap extraction advances the internal class namespace to -`engine_script_run_v3`, broker hash domain to `pineforge-broker-state/v3`, and -stream fingerprint prefix to 3. These pairing/serialization versions change -no financial rule; public C signatures, POD layouts and API versions remain -unchanged. See -[ABI stability](abi-stability.md). +Internal C++ layouts and broker/stream fingerprint domains are versioned separately +from the public C ABI and its append-only pending-order mirror. Rebuild generated +and native modules against matching headers and runtime. See +[ABI stability](@ref abi_stability) for the current versions and stale-object +pairing checks. A lower Boolean field count is not proof that every compatibility +policy or possible order history has been covered. diff --git a/docs/pages/index.md b/docs/pages/index.md index c9e1eba..59129ea 100644 --- a/docs/pages/index.md +++ b/docs/pages/index.md @@ -57,6 +57,8 @@ consumption. `pf_report_t` layout and the equity curve. - I'm a transpiler / backend author + Read **[Order execution model](@ref fill_model)** for native ownership, + reservation and ordering contracts and the remaining frontend boundary work. Read **[Coverage](@ref coverage)** — the complete map of which Pine v6 surface this runtime owns versus what your codegen has to emit inline. diff --git a/docs/reservation-expansion-candidate.md b/docs/reservation-expansion-candidate.md new file mode 100644 index 0000000..c08018d --- /dev/null +++ b/docs/reservation-expansion-candidate.md @@ -0,0 +1,27 @@ +# Reservation expansion ownership + +Reservation expansion and typed Pine instructions change the internal order +layout relative to ff54. The engine/PendingOrder namespace is +`engine_script_run_v7`, with broker/stream fingerprint epoch7. Standalone capture, +expansion and growth-source types establish `reservation_expansion_v1`. +Public C ABI4, stream API1 and pending-mirror schema1 remain unchanged. The +142-field shipped mirror prefix is preserved; seven reservation facts and six +Pine instruction facts append in the combined155-field mirror. + +The EXIT owns a capture of cycle and side plus its first later admitted +incarnation. Its selected MARKET sources own exact receiver receipts. A new +successful capture can explicitly reassign a pending source; erasure or +replacement alone never transfers it. Executable `qty` is the only mutable +finite capacity. Original QuantityRequest intent and reservation basis remain +historical. Open capture permits live-All only for its captured exposure. + +Pine still owns selection, timing, OCA ordering, risk behavior and the separate +margin-revival direct-close path. Exact multiple-tracker routing and captured +cycle validity are source corrections, not proven behavior-neutral changes. +The existing dispatch retirement ledger prevents replay and excludes retired +receivers before physical compaction; no second consumed bit or owner map exists. + +Native literal tests are in `tests/test_reservation_expansion.cpp`; old and new +C++ header/link boundaries are checked by `scripts/check_script_cpp_abi.py`. +They establish bounded ownership and pairing contracts. Compatibility outcomes +require a separate assessment with unchanged reference evidence. diff --git a/include/pineforge/compat/pine/exit_activation.hpp b/include/pineforge/compat/pine/exit_activation.hpp index 5024b01..479bd69 100644 --- a/include/pineforge/compat/pine/exit_activation.hpp +++ b/include/pineforge/compat/pine/exit_activation.hpp @@ -9,7 +9,7 @@ namespace pineforge { enum class PositionSide; -inline namespace engine_script_run_v6 { struct PendingOrder; } +inline namespace engine_script_run_v7 { struct PendingOrder; } } namespace pineforge::compat::pine { diff --git a/include/pineforge/compat/pine/frozen_market_instruction.hpp b/include/pineforge/compat/pine/frozen_market_instruction.hpp new file mode 100644 index 0000000..824bd34 --- /dev/null +++ b/include/pineforge/compat/pine/frozen_market_instruction.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace pineforge { +inline namespace engine_script_run_v7 { struct PendingOrder; } +} + +namespace pineforge::compat::pine { + +enum class FrozenMarketInstructionKind { Ordinary, Transaction, TargetedClose }; + +// Pine source interpretation retained at command admission. This is not a +// generic native reduce-position operation: its existing close-artifact path +// can open an artifact lot after the original side has disappeared. Native +// lowering of that behavior remains a separate adapter-boundary change. +// The transaction keeps its own admitted units and total transaction units; +// a targeted close uses QuantityRequest's original resolved Units amount. +class FrozenMarketInstruction { +public: + struct Transaction { + double own_units; + double transaction_units; + }; + struct TargetedClose { + std::string target_id; + }; + FrozenMarketInstruction() = default; + static FrozenMarketInstruction transaction(double own, double total) { + if (!std::isfinite(own) || own <= 0.0 || std::isnan(total) || total < own) + throw std::invalid_argument("invalid frozen market transaction amounts"); + // Keep the existing execution-side finite-total guard. A positive + // overflowed source sum is not silently replaced by an ordinary order + // or a second default-sized amount by this representation change. + return FrozenMarketInstruction(Transaction{own, total}); + } + static FrozenMarketInstruction targeted_close(std::string target, + const QuantityRequest& request) { + if (target.empty() || !request.intent() + || request.intent()->kind() != QuantityIntent::Kind::Units + || !std::isfinite(request.intent()->units()) || request.intent()->units() <= 0.0) + throw std::invalid_argument("targeted close requires a resolved positive units request"); + return FrozenMarketInstruction(TargetedClose{std::move(target)}); + } + FrozenMarketInstructionKind kind() const { + return static_cast(value_.index()); + } + bool active() const { return !std::holds_alternative(value_); } + const Transaction* transaction() const { return std::get_if(&value_); } + const TargetedClose* targeted_close() const { return std::get_if(&value_); } + void revoke() { value_ = std::monostate{}; } +private: + using Value = std::variant; + explicit FrozenMarketInstruction(Value value) : value_(std::move(value)) {} + Value value_; +}; + +// Full-book admission is one source-policy step before native matching. +// source_scope_live is the engine's current selector result, not stored state. +void finalize_frozen_market_book(std::vector& orders, bool source_scope_live); + +} // namespace pineforge::compat::pine + +namespace pineforge { +using PineFrozenMarketInstruction = compat::pine::FrozenMarketInstruction; +} // namespace pineforge diff --git a/include/pineforge/compat/pine/order_birth.hpp b/include/pineforge/compat/pine/order_birth.hpp index b57ffe0..14fef7d 100644 --- a/include/pineforge/compat/pine/order_birth.hpp +++ b/include/pineforge/compat/pine/order_birth.hpp @@ -1,7 +1,7 @@ #pragma once #include "../../order_birth.hpp" -namespace pineforge { inline namespace engine_script_run_v6 { struct PendingOrder; } } +namespace pineforge { inline namespace engine_script_run_v7 { struct PendingOrder; } } namespace pineforge::compat::pine { // Historical Pine permissions remain policy, not physical birth facts. diff --git a/include/pineforge/compat/pine/order_priority.hpp b/include/pineforge/compat/pine/order_priority.hpp index 86aac96..71e69e1 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 { inline namespace engine_script_run_v6 { struct PendingOrder; } } +namespace pineforge { inline namespace engine_script_run_v7 { struct PendingOrder; } } namespace pineforge::compat::pine { struct OrderPriorityContext { diff --git a/include/pineforge/compat/pine/reservation_expansion.hpp b/include/pineforge/compat/pine/reservation_expansion.hpp new file mode 100644 index 0000000..da131c7 --- /dev/null +++ b/include/pineforge/compat/pine/reservation_expansion.hpp @@ -0,0 +1,17 @@ +#pragma once +#include +#include +#include +namespace pineforge { +enum class PositionSide; +inline namespace engine_script_run_v7 { struct PendingOrder; } +} +namespace pineforge::compat::pine { +// Called only for omitted explicit exit qty. Pine owns the population selector; +// native capture/closure/resize never reads these compatibility settings. +std::vector select_reservation_growth_sources(const std::vector& book, + const std::string& from_entry, bool process_on_close, bool effectively_flat, + double percent, int bar, PositionSide side); +bool admits_reservation_expansion(const std::vector& selected, + bool partial, double reserved, double live); +} // namespace pineforge::compat::pine diff --git a/include/pineforge/engine.hpp b/include/pineforge/engine.hpp index 64c7322..82fc87d 100644 --- a/include/pineforge/engine.hpp +++ b/include/pineforge/engine.hpp @@ -14,6 +14,8 @@ #include "bar.hpp" #include "broker_events.hpp" #include "quantity_intent.hpp" +#include "reservation_expansion.hpp" +#include "compat/pine/frozen_market_instruction.hpp" #include "leg_activation.hpp" #include "compat/pine/exit_activation.hpp" #include "order_birth.hpp" @@ -406,7 +408,7 @@ enum class ShortSeedCollisionRole : uint8_t { // 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_v6 { +inline namespace engine_script_run_v7 { struct PendingOrder { std::string id; std::string from_entry; // for exit orders @@ -857,23 +859,10 @@ struct PendingOrder { // 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 - // position, covering same-bar MARKET pyramid adds that were already - // pending when the exit was placed. Any later admitted entry-like order - // clears the bit, making the finite qty the automatic conservative - // fallback—even when that later order is placed on a future bar. - bool pooc_global_full_exit_dynamic_qty = false; - // Persistent half of the bounded POOC relation. Unlike ``dynamic_qty``, - // later entries do not clear this bit: pre-exit adds may still be waiting - // to fill when invalidation occurs. Each successfully filled bound add - // grows this EXIT's finite qty by its exact same-side position delta. - bool pooc_global_full_exit_tracks_bound_adds = false; - // Set only on qualifying high-level MARKET adds already pending when the - // 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; + // EXIT-owned exposure capture and source-owned exact receiver receipt. + // The public legacy flags are one-way projections of these causal facts. + ReservationExpansion reservation_expansion; + ReservationGrowthSource reservation_growth_source; // 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! @@ -911,14 +900,11 @@ struct PendingOrder { // dropped, so its same-id close finds no pending entry and is cancelled // (LONG 1, no artifact row). The generalized form of the short-seed // collision kernel (finding 272), with which it agrees on that book. - bool sbmt_member = false; - double sbmt_own_qty = std::numeric_limits::quiet_NaN(); - double sbmt_tx_qty = std::numeric_limits::quiet_NaN(); - bool sbmt_kept_over_cap = false; - // strategy.close member: frozen target and broker side (buy closes a - // short). The target id is order.id without the "__close__" prefix. - double sbmt_close_qty = std::numeric_limits::quiet_NaN(); - bool sbmt_close_buy = false; + // One typed source instruction. Transaction quantities live here; a + // targeted close consumes quantity_request's resolved original Units. + // Cap and closing-side facts remain the existing immutable placement + // snapshots, rather than separately writable coordination booleans. + PineFrozenMarketInstruction pine_frozen_market_instruction; // design-declined-reversal-close-leg: set at the KI-54 percent-of-equity // reversal-decline site when this pending FULL close was co-queued AFTER, // and on the same bar as, the declined MARKET reversal entry targeting the @@ -1033,7 +1019,7 @@ struct PendingOrder { ShortSeedCollisionRole::NONE; }; - } // inline namespace engine_script_run_v6 (PendingOrder) + } // inline namespace engine_script_run_v7 (PendingOrder) // default_qty_type constants (matches TradingView) enum class QtyType { FIXED = 0, PERCENT_OF_EQUITY = 1, CASH = 2 }; @@ -1091,7 +1077,7 @@ struct StrategyOverrides { // v6 adds explicit owner-bound exit-leg activation and Pine placement evidence. // 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_v6 { +inline namespace engine_script_run_v7 { class BacktestEngine { protected: // --- Position state --- @@ -2169,8 +2155,11 @@ class BacktestEngine { && std::isfinite(order.affordability_placement_equity) && order.affordability_placement_equity > 0.0 && order.affordability_held_qty == 0.0 - && (!order.sbmt_member || (order.sbmt_tx_qty == order.sbmt_own_qty - && !order.sbmt_kept_over_cap)); + && (!order.pine_frozen_market_instruction.active() + || (order.pine_frozen_market_instruction.transaction() + && order.pine_frozen_market_instruction.transaction()->transaction_units + == order.pine_frozen_market_instruction.transaction()->own_units + && !order.over_pyramiding_cap_at_placement)); if (!default_all_in && !explicit_fixed) return false; const double mark = default_all_in ? order.sizing_mark : order.affordability_signal_price; @@ -3983,7 +3972,7 @@ class BacktestEngine { const PendingOrder& order) const; bool short_seed_collision_final_short_is_live( const PendingOrder& order) const; - // round 8 family S (PendingOrder::sbmt_member): the same-bar MARKET + // round 8 family S (PendingOrder::pine_frozen_market_instruction): the same-bar MARKET // transaction's scope, the close-artifact predicate (rule 4) and the // frozen-transaction reversal kernel (rules 1/2). bool same_bar_market_tx_scope_is_live() const; @@ -4253,7 +4242,7 @@ class BacktestEngine { double& qp_io, bool& is_partial_io, double& reserved_qty_out); - void invalidate_unsafe_pooc_global_full_exit_dynamic_qty(); + void close_reservation_capture_populations(uint64_t admitted_incarnation); // execute_market_entry / execute_partial_exit_* helpers (defined in // engine_orders.cpp). @@ -4976,12 +4965,12 @@ class BacktestEngine { // strategy.order size at the signal close), a // MARKET's frozen broker transaction // (paired_flat_market_transaction_qty; a - // same-bar-market member's sbmt_tx_qty from + // same-bar-market transaction's frozen total from // FLAT or as a kept over-cap add), or what one // of the two MARKET reversal kernels opens: // a same-bar-market member against an // opposite live position opens the remainder - // sbmt_tx_qty - min(sbmt_tx_qty, live qty) + // transaction_units - min(transaction_units, live qty) // (apply_same_bar_market_tx_reversal), and the // exact SHORT-seed collision's final short // re-opens the residual pyramid_entries_[0].qty @@ -5129,5 +5118,5 @@ class BacktestEngine { void trace(const std::string& name, int value) { trace(name, static_cast(value)); } }; -} // inline namespace engine_script_run_v6 +} // inline namespace engine_script_run_v7 } // namespace pineforge diff --git a/include/pineforge/pending_order_mirror.hpp b/include/pineforge/pending_order_mirror.hpp index 3d23478..fd34cba 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. -// 94 PendingOrder members mirrored (142 POD fields incl. struct_version/size). +// 88 PendingOrder members mirrored (155 POD fields incl. struct_version/size). #pragma once #include @@ -98,16 +98,16 @@ typedef struct pf_pending_order_v1_s { uint64_t comment_hash64; 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 pooc_global_full_exit_dynamic_qty; // deprecated, derived output only + uint8_t pooc_global_full_exit_tracks_bound_adds; // deprecated, derived output only + uint8_t pooc_global_full_exit_bound_add; // deprecated, derived output only uint8_t created_while_in_position; // deprecated, derived output only - 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 sbmt_member; // deprecated, derived output only + double sbmt_own_qty; // deprecated, derived output only + double sbmt_tx_qty; // deprecated, derived output only + uint8_t sbmt_kept_over_cap; // deprecated, derived output only + double sbmt_close_qty; // deprecated, derived output only + uint8_t sbmt_close_buy; // deprecated, derived output only uint8_t suppress_as_declined_reversal_close; uint8_t dormant_bracket; uint8_t dormant_reissue_pending; @@ -154,6 +154,19 @@ typedef struct pf_pending_order_v1_s { uint8_t pine_exit_activation_limit_continuation_present; int32_t pine_exit_activation_limit_continuation_cause; uint64_t pine_exit_activation_limit_continuation_fill; + int64_t reservation_expansion_position_cycle; + uint8_t reservation_expansion_present; + int32_t reservation_expansion_side; + uint8_t reservation_expansion_first_later_admission_present; + uint64_t reservation_expansion_first_later_admission; + uint8_t reservation_growth_source_present; + uint64_t reservation_growth_source_reservation_owner; + uint64_t pine_frozen_market_instruction_kind; + double pine_frozen_market_instruction_own_units; + double pine_frozen_market_instruction_transaction_units; + char pine_frozen_market_instruction_target_id[64]; + uint8_t pine_frozen_market_instruction_target_id_truncated; + uint64_t pine_frozen_market_instruction_target_id_hash64; } pf_pending_order_v1_t; /* One row of the self-describing layout table returned by diff --git a/include/pineforge/reservation_expansion.hpp b/include/pineforge/reservation_expansion.hpp new file mode 100644 index 0000000..b4302c9 --- /dev/null +++ b/include/pineforge/reservation_expansion.hpp @@ -0,0 +1,53 @@ +#pragma once +#include +#include + +namespace pineforge { +enum class PositionSide; + +// First standalone C++ ABI for the reservation ownership model. +inline namespace reservation_expansion_v1 { + +// The containing EXIT incarnation owns this capture. Admission is an immutable +// historical cause; neither cancellation nor a new exposure erases the receipt. +struct ReservationExpansionCapture { + int64_t position_cycle; + PositionSide side; + std::optional first_later_admission; +}; + +class ReservationExpansion { +public: + void capture(uint64_t receiver, int64_t cycle, PositionSide side, double capacity); + void close_population(uint64_t admitted_incarnation); + const std::optional& capture() const { return capture_; } + bool population_open() const { return capture_ && !capture_->first_later_admission; } + bool owns_exposure(int64_t cycle, PositionSide side) const; + bool live_all(int64_t cycle, PositionSide side) const { + return population_open() && owns_exposure(cycle, side); + } + // The caller resolves the exact actionable receiver. This operation knows + // only exposure facts and the committed primary delta; qty is the sole + // mutable capacity, and QuantityRequest is never rebased here. + // Preconditions for standalone callers: finite nonnegative qty, endpoint + // quantities and epsilon; the resulting capacity must remain finite. The + // caller supplies real before/after exposure facts. This method preserves + // the native after_qty > before_qty + epsilon arithmetic without repricing + // or re-rounding; it does not validate every standalone precondition. + void grow(double& qty, int64_t before_cycle, PositionSide before_side, double before_qty, + int64_t after_cycle, PositionSide after_side, double after_qty, double epsilon) const; +private: + std::optional capture_; +}; + +// The containing source incarnation is the source identity. This is the only +// authoritative edge; replacement is allowed only at a new successful capture. +class ReservationGrowthSource { +public: + void assign_capture(uint64_t source, uint64_t receiver); + const std::optional& reservation_owner() const { return reservation_owner_; } +private: + std::optional reservation_owner_; +}; +} // inline namespace reservation_expansion_v1 +} // namespace pineforge diff --git a/scripts/check_broker_state_hash_coverage.py b/scripts/check_broker_state_hash_coverage.py index be8190e..9654f5d 100644 --- a/scripts/check_broker_state_hash_coverage.py +++ b/scripts/check_broker_state_hash_coverage.py @@ -195,6 +195,16 @@ def _class_fields(src: str, name: str) -> dict[str, str]: continue if re.fullmatch(r"(?:bool|ExitLegActivationBounds)\s+\w+\([^;{}]*\)\s+const", decl): continue # declared read-only value query, never a stored field + reservation_methods = { + "ReservationExpansion": { + "void capture(uint64_t receiver, int64_t cycle, PositionSide side, double capacity)", + "void close_population(uint64_t admitted_incarnation)", + "void grow(double& qty, int64_t before_cycle, PositionSide before_side, double before_qty, int64_t after_cycle, PositionSide after_side, double after_qty, double epsilon) const", + }, + "ReservationGrowthSource": {"void assign_capture(uint64_t source, uint64_t receiver)"}, + } + if decl in reservation_methods.get(name, set()): + continue # exact declared operations, never a blanket declaration waiver 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: @@ -384,6 +394,40 @@ def _quantity_request_coverage(quantity: str, source: str) -> None: if prefix.count("{") != prefix.count("}"): raise ValueError("quantity request hash must be unconditional inside its order loop") +def _pine_frozen_market_instruction_coverage(header: str, source: str) -> None: + """An exclusive instruction, with each live payload folded in its owner loop.""" + header = _strip_cpp_comments(header) + if _class_fields(header, "FrozenMarketInstruction") != {"value_": "Value"}: + raise ValueError("FrozenMarketInstruction fields changed; classify every frozen fact") + for name, fields in [ + ("Transaction", [("double", "own_units"), ("double", "transaction_units")]), + ("TargetedClose", [("std::string", "target_id")]), + ]: + if struct_members(header, name) != fields: + raise ValueError(name + " instruction payload changed; update hash and mirror") + if not re.search(r"using\s+Value\s*=\s*std::variant\s*;", header): + raise ValueError("FrozenMarketInstruction variant discriminator changed") + body = _one_braced_body(header, + r"enum\s+class\s+FrozenMarketInstructionKind\s*\{", "FrozenMarketInstructionKind") + if [x.strip() for x in body.split(",")] != ["Ordinary", "Transaction", "TargetedClose"]: + raise ValueError("FrozenMarketInstructionKind hash encoding changed") + loop = _collection_loop_body(source, "pending_orders_", "o") + expected = """f.i(static_cast(o.pine_frozen_market_instruction.kind())); + if (const auto* transaction = o.pine_frozen_market_instruction.transaction()) { + f.d(transaction->own_units); f.d(transaction->transaction_units); + } + if (const auto* close = o.pine_frozen_market_instruction.targeted_close()) { + f.s(close->target_id); + }""" + compact = re.sub(r"\s+", "", loop) + folded = re.sub(r"\s+", "", expected) + if compact.count(folded) != 1: + raise ValueError("frozen market instruction requires every live field and role discriminator") + prefix = compact[:compact.index(folded)] + if prefix.count("{") != prefix.count("}") or (prefix and prefix[-1] not in ";}"): + raise ValueError("frozen market instruction hash must be unconditional in its order loop") + + def _birth_coverage(header: str, source: str) -> None: header = _strip_cpp_comments(header) expected = { @@ -449,8 +493,60 @@ def _exit_activation_coverage(activation: str, policy: str, source: str) -> None raise ValueError("exit activation hash block must be unconditional") +def _reservation_expansion_fields(header: str) -> None: + header = _strip_cpp_comments(header) + expected = { + "ReservationExpansion": {"capture_": "std::optional"}, + "ReservationExpansionCapture": {"position_cycle": "int64_t", "side": "PositionSide", "first_later_admission": "std::optional"}, + "ReservationGrowthSource": {"reservation_owner_": "std::optional"}, + } + for name, fields in expected.items(): + if _class_fields(header, name) != fields: + raise ValueError(name + " fields changed; every capture/source fact must be hashed") + +def _reservation_expansion_coverage(header: str, source: str) -> None: + _reservation_expansion_fields(header) + loop = _collection_loop_body(source, "pending_orders_", "o") + expected = """f.b(o.reservation_expansion.capture().has_value()); + if (const auto& capture = o.reservation_expansion.capture()) { + f.i(capture->position_cycle); + f.i(static_cast(capture->side)); + f.b(capture->first_later_admission.has_value()); + if (const auto& admission = capture->first_later_admission) { + f.u(*admission); + } + } + f.b(o.reservation_growth_source.reservation_owner().has_value()); + if (const auto& receiver = o.reservation_growth_source.reservation_owner()) { + f.u(*receiver); + }""" + compact = re.sub(r"\s+", "", loop) + folded = re.sub(r"\s+", "", expected) + if compact.count(folded) != 1: + raise ValueError("reservation capture/source encoding needs every nested fact and discriminator once") + prefix = compact[:compact.index(folded)] + if prefix.count("{") != prefix.count("}") or (prefix and prefix[-1] not in ";}"): + raise ValueError("reservation capture/source hash block must be unconditional") + + +def _reservation_expansion_version_coverage(header: str, source: str) -> None: + """Standalone layout-sensitive types and methods own their first ABI.""" + declaration = _one_braced_body(_strip_cpp_comments(header), + r"inline\s+namespace\s+reservation_expansion_v1\s*\{", "reservation ABI") + implementation = _one_braced_body(_strip_cpp_comments(source), + r"inline\s+namespace\s+reservation_expansion_v1\s*\{", "reservation implementation ABI") + for name in ("ReservationExpansionCapture", "ReservationExpansion", "ReservationGrowthSource"): + if not re.search(r"\b(?:class|struct)\s+" + name + r"\s*\{", declaration): + raise ValueError(name + " must belong to reservation_expansion_v1") + for name in ("ReservationExpansion::capture", "ReservationExpansion::close_population", + "ReservationExpansion::owns_exposure", "ReservationExpansion::grow", + "ReservationGrowthSource::assign_capture"): + if not re.search(r"\b" + re.escape(name) + r"\s*\(", implementation): + raise ValueError(name + " must be implemented in reservation_expansion_v1") + + def _runtime_version_coverage(header: str, source: str, stream: str) -> None: - """The v6 layout and serialized-state contracts must advance together. + """The v7 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 @@ -458,18 +554,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_v6", "engine_script_run_v6"]: - raise ValueError("PendingOrder and BacktestEngine layouts require internal namespace engine_script_run_v6") + if namespaces != ["engine_script_run_v7", "engine_script_run_v7"]: + raise ValueError("PendingOrder and BacktestEngine layouts require internal namespace engine_script_run_v7") 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/v6"\);', broker): - raise ValueError("broker hash must start with pineforge-broker-state/v6") + if not re.match(r'\s*Fnv\s+f;\s*f\.s\("pineforge-broker-state/v7"\);', broker): + raise ValueError("broker hash must start with pineforge-broker-state/v7") 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(6);integer(broker_state_hash());" + fold = "integer(7);integer(broker_state_hash());" if compact.count(fold) != 1: - raise ValueError("stream hash requires version 6 followed by the broker hash") + raise ValueError("stream hash requires version 7 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") @@ -484,6 +580,12 @@ 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()) + _reservation_expansion_version_coverage( + (root / "include/pineforge/reservation_expansion.hpp").read_text(), + (root / "src/reservation_expansion.cpp").read_text()) + _reservation_expansion_coverage((root / "include/pineforge/reservation_expansion.hpp").read_text(), src) + _pine_frozen_market_instruction_coverage( + (root / "include/pineforge/compat/pine/frozen_market_instruction.hpp").read_text(), src) _birth_coverage((root / "include/pineforge/order_birth.hpp").read_text(), src) _exit_activation_coverage((root / "include/pineforge/leg_activation.hpp").read_text(), (root / "include/pineforge/compat/pine/exit_activation.hpp").read_text(), src) @@ -504,8 +606,11 @@ 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) + if {"pending_order.reservation_expansion", "pending_order.reservation_growth_source"} & all_waivers.keys(): + print("check_broker_state_hash_coverage: reservation capture/source cannot be waived", file=sys.stderr) + return 1 + if {"pending_order.quantity_request", "pending_order.pine_frozen_market_instruction"} & all_waivers.keys(): + print("check_broker_state_hash_coverage: quantity_request and pine_frozen_market_instruction 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)} diff --git a/scripts/check_script_cpp_abi.py b/scripts/check_script_cpp_abi.py index afed8a7..2752b02 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_v6" +CURRENT_NAMESPACE = "engine_script_run_v7" BASE_NAMESPACE = "engine_script_run_v2" FIXTURE = Path(__file__).resolve().parents[1] / "tests/fixtures/script_cpp_abi/base38" @@ -30,13 +30,14 @@ def entry_diagnostic(lines, namespace, method): def frozen_headers(destination, fixture=FIXTURE, commit=BASE_COMMIT, - namespace=BASE_NAMESPACE, engine_sha=BASE_ENGINE_SHA256): + namespace=BASE_NAMESPACE, engine_sha=BASE_ENGINE_SHA256, tree=None): """Authenticate and unpack the exact tracked base38 header closure.""" manifest = json.loads((fixture / "manifest.json").read_text()) if (manifest["source_commit"] != commit or manifest["internal_namespace"] != namespace - or manifest["files"]["pineforge/engine.hpp"]["sha256"] != engine_sha): - raise RuntimeError("stale-header fixture does not identify the pinned base38 contract") + or manifest["files"]["pineforge/engine.hpp"]["sha256"] != engine_sha + or (tree is not None and manifest.get("source_tree") != tree)): + raise RuntimeError("stale-header fixture does not identify the pinned contract") archive = (fixture / "headers.json.gz").read_bytes() if hashlib.sha256(archive).hexdigest() != manifest["archive_sha256"]: raise RuntimeError("stale-header fixture archive digest mismatch") @@ -130,7 +131,12 @@ def main(): parser.add_argument("--include", required=True) parser.add_argument("--generated-include", required=True) parser.add_argument("--extra-flag", action="append", default=[]) + parser.add_argument("--receipt", type=Path) args = parser.parse_args() + receipt = {"library_sha256": hashlib.sha256(Path(args.library).read_bytes()).hexdigest(), + "current_namespace": CURRENT_NAMESPACE, + "standalone_namespace": "reservation_expansion_v1", + "executable_runs": 0, "compiles": [], "links": []} # Literal diagnostic controls guard the link-failure parser itself. for namespace in (BASE_NAMESPACE, CURRENT_NAMESPACE): report_only = [f"undefined pineforge::{namespace}::BacktestEngine::fill_report(pineforge::ReportC*) const"] @@ -154,6 +160,16 @@ def main(): frozen_headers(activation_include, FIXTURE.parent / "base149", "149f77ce16ef84c6da77e67d812bf8fa88e51cde", "engine_script_run_v5", "5ba773889d947e4fdab3995cc55f22f88ab037a86e0ad4016a126d297ce82eed") + shipped_include = root / "baseff54/include" + frozen_headers(shipped_include, FIXTURE.parent / "baseff54", + "ff54a557ac751244dafd60df0bb22886ec35792d", "engine_script_run_v6", + "d5d74b2b0542ce7aa2bf3e0a95bac8d05318ce15494148f4f07f92c7e332b237", + "60431e5da18d4bce0777f0e3b4df03d41163c350") + growth_include = root / "growthbf312/include" + frozen_headers(growth_include, FIXTURE.parent / "growthbf312", + "ff54a557ac751244dafd60df0bb22886ec35792d", "engine_script_run_v6", + "381a18d59f20ff94c6eed9dec40497fdaa175637fc96169a0a9875a38b071736", + "bf312b9d5a705d3d0ca16d4fb897e16c6e73b0d1") common = [args.compiler, "-std=c++17", "-O0", *args.extra_flag] def compile_object(name, source, include): @@ -168,6 +184,9 @@ def compile_object(name, source, include): if compiled.returncode: raise RuntimeError(name + " failed to compile (not a pairing rejection):\n" + compiled.stderr) + receipt["compiles"].append({"name": name, "exit": compiled.returncode, + "source_sha256": hashlib.sha256(source.encode()).hexdigest(), + "object_sha256": hashlib.sha256(obj.read_bytes()).hexdigest()}) return obj current_native = compile_object("current_native", caller(CURRENT_NAMESPACE), args.include) @@ -214,6 +233,62 @@ def compile_object(name, source, include): BASE_SYMBOL_CONTROL.replace("engine_script_run_v2", "engine_script_run_v5"), activation_include) activation_priority = compile_object("base149_pending_priority", priority_caller, activation_include) activation_priority_symbols = compile_object("base149_pending_priority_symbols", priority_symbols, activation_include) + shipped_native = compile_object("baseff54_native", caller("engine_script_run_v6"), shipped_include) + shipped_generated = compile_object("baseff54_generated", caller("engine_script_run_v6", True), shipped_include) + shipped_symbols = compile_object("baseff54_symbol_control", + BASE_SYMBOL_CONTROL.replace("engine_script_run_v2", "engine_script_run_v6"), shipped_include) + shipped_priority = compile_object("baseff54_pending_priority", priority_caller, shipped_include) + shipped_priority_symbols = compile_object("baseff54_pending_priority_symbols", priority_symbols, shipped_include) + + reservation_caller = '''#include +int main(int argc, char**) { + pineforge::ReservationExpansion expansion; + pineforge::ReservationGrowthSource source; + const auto side = static_cast(argc); + double capacity = 4.0; + expansion.capture(1, 1, side, capacity); + expansion.close_population(2); + expansion.grow(capacity, 1, side, 1.0, 1, side, 2.0, 1e-10); + source.assign_capture(3, 1); + return expansion.owns_exposure(1, side) ? 0 : 1; +} +''' + reservation_symbols = '''#include +namespace pineforge { +void ReservationExpansion::capture(uint64_t, int64_t, PositionSide, double) {} +void ReservationExpansion::close_population(uint64_t) {} +bool ReservationExpansion::owns_exposure(int64_t, PositionSide) const { return false; } +void ReservationExpansion::grow(double&, int64_t, PositionSide, double, + int64_t, PositionSide, double, double) const {} +void ReservationGrowthSource::assign_capture(uint64_t, uint64_t) {} +} +''' + reservation_assertions = '''#include +static_assert(std::is_same::value); +static_assert(std::is_same::value); +static_assert(std::is_same::value); +''' + current_reservation = compile_object("current_standalone_reservation", + reservation_caller + reservation_assertions, args.include) + draft_reservation = compile_object("growthbf312_unversioned_reservation", reservation_caller, growth_include) + draft_reservation_symbols = compile_object("growthbf312_reservation_symbols", reservation_symbols, growth_include) + capture_caller = '''#include +void pairing_capture(const pineforge::ReservationExpansionCapture&); +int main() { + pineforge::ReservationExpansionCapture capture{1, static_cast(1), {}}; + pairing_capture(capture); +} +''' + capture_provider = '''#include +void pairing_capture(const pineforge::ReservationExpansionCapture&) {} +''' + current_capture = compile_object("current_capture_argument", capture_caller, args.include) + draft_capture = compile_object("growthbf312_capture_argument", capture_caller, growth_include) + current_capture_symbols = compile_object("current_capture_symbols", capture_provider, args.include) + draft_capture_symbols = compile_object("growthbf312_capture_symbols", capture_provider, growth_include) def link(name, obj, runtime, missing_namespace=None): linked = subprocess.run( @@ -224,6 +299,7 @@ def link(name, obj, runtime, missing_namespace=None): if linked.returncode: raise RuntimeError(name + " positive control failed to link:\n" + linked.stderr) print(name + ": linked (not executed)") + receipt["links"].append({"name": name, "outcome": "linked", "exit": 0}) return if not linked.returncode: raise RuntimeError(name + " stale C++ pairing unexpectedly linked") @@ -240,6 +316,8 @@ def link(name, obj, runtime, missing_namespace=None): + "BacktestEngine entry symbols") for method in required_methods: print(" " + entries[method].strip()) + receipt["links"].append({"name": name, "outcome": "expected_rejection", + "exit": linked.returncode, "diagnostics": list(entries.values())}) link("current_native_to_current", current_native, args.library) link("current_generated_to_current", current_generated, args.library) @@ -271,12 +349,21 @@ def link(name, obj, runtime, missing_namespace=None): link("current_native_to_v5_symbol_control", current_native, activation_symbols, CURRENT_NAMESPACE) link("current_generated_to_v5_symbol_control", current_generated, activation_symbols, CURRENT_NAMESPACE) link("base149_pending_priority_to_v5_symbols", activation_priority, activation_priority_symbols) + link("baseff54_native_to_v6_symbols", shipped_native, shipped_symbols) + link("baseff54_generated_to_v6_symbols", shipped_generated, shipped_symbols) + link("baseff54_native_to_current", shipped_native, args.library, "engine_script_run_v6") + link("baseff54_generated_to_current", shipped_generated, args.library, "engine_script_run_v6") + link("current_native_to_v6_symbols", current_native, shipped_symbols, CURRENT_NAMESPACE) + link("current_generated_to_v6_symbols", current_generated, shipped_symbols, CURRENT_NAMESPACE) + link("baseff54_pending_priority_to_v6_symbols", shipped_priority, shipped_priority_symbols) for name, obj, runtime, expected in [ + ("baseff54_pending_priority_to_current", shipped_priority, args.library, "pineforge::engine_script_run_v6::PendingOrder"), + ("current_pending_priority_to_v6_symbols", current_priority, shipped_priority_symbols, "pineforge::engine_script_run_v7::PendingOrder"), ("base149_pending_priority_to_current", activation_priority, args.library, "pineforge::engine_script_run_v5::PendingOrder"), - ("current_pending_priority_to_v5_symbols", current_priority, activation_priority_symbols, "pineforge::engine_script_run_v6::PendingOrder"), + ("current_pending_priority_to_v5_symbols", current_priority, activation_priority_symbols, "pineforge::engine_script_run_v7::PendingOrder"), ("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_v6::PendingOrder"), + "pineforge::engine_script_run_v7::PendingOrder"), ]: result = subprocess.run([*common, str(obj), str(runtime), "-pthread", "-o", str(root / name)], capture_output=True, text=True, timeout=60) @@ -284,7 +371,39 @@ def link(name, obj, runtime, missing_namespace=None): 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("20 translation units compiled; 13 positive links; 21 rejected links; no executable run") + receipt["links"].append({"name": name, "outcome": "expected_rejection", + "exit": result.returncode, "diagnostics": result.stderr}) + + link("current_reservation_to_current", current_reservation, args.library) + link("draft_reservation_to_draft_symbols", draft_reservation, draft_reservation_symbols) + link("current_capture_to_current_symbols", current_capture, current_capture_symbols) + link("draft_capture_to_draft_symbols", draft_capture, draft_capture_symbols) + for name, obj, runtime, expected in [ + ("draft_reservation_to_current", draft_reservation, args.library, + ["pineforge::ReservationExpansion::capture(", "pineforge::ReservationGrowthSource::assign_capture("]), + ("current_reservation_to_draft_symbols", current_reservation, draft_reservation_symbols, + ["pineforge::reservation_expansion_v1::ReservationExpansion::capture(", + "pineforge::reservation_expansion_v1::ReservationGrowthSource::assign_capture("]), + ("draft_capture_to_current_symbols", draft_capture, current_capture_symbols, + ["pairing_capture(pineforge::ReservationExpansionCapture const&)"]), + ("current_capture_to_draft_symbols", current_capture, draft_capture_symbols, + ["pairing_capture(pineforge::reservation_expansion_v1::ReservationExpansionCapture const&)"]), + ]: + 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 any(needle not in result.stderr for needle in expected)): + raise RuntimeError(name + " did not reject the expected reservation ABI: " + result.stderr) + print(name + ": rejected stale standalone reservation ABI (not executed)") + receipt["links"].append({"name": name, "outcome": "expected_rejection", + "exit": result.returncode, "diagnostics": result.stderr}) + linked = sum(item["outcome"] == "linked" for item in receipt["links"]) + rejected = sum(item["outcome"] == "expected_rejection" for item in receipt["links"]) + receipt["summary"] = {"compiled": len(receipt["compiles"]), "linked": linked, "rejected": rejected} + if args.receipt: + args.receipt.write_text(json.dumps(receipt, indent=2) + "\n") + print(f"{len(receipt['compiles'])} translation units compiled; {linked} positive links; " + f"{rejected} 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 f740407..41d512a 100644 --- a/scripts/gen_pending_order_mirror.py +++ b/scripts/gen_pending_order_mirror.py @@ -61,6 +61,9 @@ # Public v1 is append-only. Removed native fields survive only as one-way # deprecated output projections at their original offsets. LEGACY_OUTPUTS = { + "pooc_global_full_exit_dynamic_qty": "src.reservation_expansion.population_open() ? 1 : 0", + "pooc_global_full_exit_tracks_bound_adds": "src.reservation_expansion.capture().has_value() ? 1 : 0", + "pooc_global_full_exit_bound_add": "src.reservation_growth_source.reservation_owner().has_value() ? 1 : 0", "coof_suppress_stop_on_entry_bar": "src.pine_exit_activation.holds_stop() ? 1 : 0", "coof_suppress_limit_on_entry_bar": "src.pine_exit_activation.holds_limit() ? 1 : 0", "created_during_coof_recalc": "src.birth.from_fill() ? 1 : 0", @@ -71,8 +74,34 @@ "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", + "sbmt_member": "src.pine_frozen_market_instruction.active() ? 1 : 0", + "sbmt_own_qty": "src.pine_frozen_market_instruction.transaction() ? src.pine_frozen_market_instruction.transaction()->own_units : std::numeric_limits::quiet_NaN()", + "sbmt_tx_qty": "src.pine_frozen_market_instruction.transaction() ? src.pine_frozen_market_instruction.transaction()->transaction_units : std::numeric_limits::quiet_NaN()", + "sbmt_kept_over_cap": "src.pine_frozen_market_instruction.transaction() && src.over_pyramiding_cap_at_placement ? 1 : 0", + "sbmt_close_qty": "src.pine_frozen_market_instruction.targeted_close() ? src.quantity_request.intent()->units() : std::numeric_limits::quiet_NaN()", + "sbmt_close_buy": "src.pine_frozen_market_instruction.targeted_close() && src.created_position_side == PositionSide::SHORT ? 1 : 0", } COMPOSITE_MAP = { + "ReservationExpansion": [ + # An eight-byte first field preserves ff54's entire 142-field object, + # including trailing padding, before any appended smaller fields. + ("position_cycle", "int64_t", "src.{m}.capture() ? src.{m}.capture()->position_cycle : 0"), + ("present", "uint8_t", "src.{m}.capture().has_value() ? 1 : 0"), + ("side", "int32_t", "src.{m}.capture() ? static_cast(src.{m}.capture()->side) : 0"), + ("first_later_admission_present", "uint8_t", "src.{m}.capture() && src.{m}.capture()->first_later_admission ? 1 : 0"), + ("first_later_admission", "uint64_t", "src.{m}.capture() && src.{m}.capture()->first_later_admission ? *src.{m}.capture()->first_later_admission : 0"), + ], + "ReservationGrowthSource": [ + ("present", "uint8_t", "src.{m}.reservation_owner().has_value() ? 1 : 0"), + ("reservation_owner", "uint64_t", "src.{m}.reservation_owner() ? *src.{m}.reservation_owner() : 0"), + ], + "PineFrozenMarketInstruction": [ + # Start after the full ff54 142-field prefix, including trailing padding. + ("kind", "uint64_t", "static_cast(src.{m}.kind())"), + ("own_units", "double", "src.{m}.transaction() ? src.{m}.transaction()->own_units : 0.0"), + ("transaction_units", "double", "src.{m}.transaction() ? src.{m}.transaction()->transaction_units : 0.0"), + ("target_id", "std::string", "src.{m}.targeted_close() ? src.{m}.targeted_close()->target_id : std::string()"), + ], "ExitLegActivation": [ ("owner_cycle", "int64_t", "src.{m}.bounds() ? src.{m}.bounds()->position_cycle : 0"), ("present", "uint8_t", "src.{m}.bounds().has_value() ? 1 : 0"), @@ -211,6 +240,8 @@ def classify(ms: list[tuple[str, str]], waivers: dict[str, str]): """Return (mirrored, waived) where mirrored = [(cpp_type, name)] kept in the POD and waived = [(cpp_type, name, reason)]. Aborts on an unmapped, unwaived type or a waiver naming a non-member.""" + if {"reservation_expansion", "reservation_growth_source"} & waivers.keys(): + _fail("reservation expansion and source receipts cannot be waived") names = {n for _, n in ms} orphans = sorted(w for w in waivers if w not in names) if orphans: @@ -228,6 +259,11 @@ def classify(ms: list[tuple[str, str]], waivers: dict[str, str]): def generate() -> tuple[str, str]: + # Share the strict nested storage census; newly stored fields cannot hide + # behind an unchanged composite-map name. Imported lazily (checker also + # uses this module's PendingOrder parser). + from check_broker_state_hash_coverage import _reservation_expansion_fields + _reservation_expansion_fields((ROOT / "include/pineforge/reservation_expansion.hpp").read_text()) mirrored, waived = classify(members(), load_waivers()) fields: list[str] = [] copies: list[str] = [] @@ -238,8 +274,8 @@ def generate() -> tuple[str, str]: 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} - # Preserve the full 149f77c extension as well as the original c45 prefix. - existing_extension = ["replaced_order_incarnation", "birth", "pine_birth_reach", "quantity_request"] + # Preserve all 142 ff54 fields, including activation, before new composites. + existing_extension = ["replaced_order_incarnation", "birth", "pine_birth_reach", "quantity_request", "leg_activation", "pine_exit_activation"] tail = [(native[name], name) for name in existing_extension] tail += [(kind, name) for kind, name in mirrored if name not in prefix_names and name not in existing_extension] @@ -254,9 +290,18 @@ def generate() -> tuple[str, str]: 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)) + if ct in STRING_TYPES: + fields += [f" char {name}[{STR_CAP}];", + f" uint8_t {name}_truncated;", + f" uint64_t {name}_hash64;"] + copies.append(f" copy_str({expr.format(m=m)}, out->{name}, &out->{name}_truncated, &out->{name}_hash64);") + descs += [(name, f"char[{STR_CAP}]"), + (f"{name}_truncated", "uint8_t"), + (f"{name}_hash64", "uint64_t")] + else: + 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;", diff --git a/scripts/test_broker_state_hash_coverage.py b/scripts/test_broker_state_hash_coverage.py index afa5398..3f210e2 100644 --- a/scripts/test_broker_state_hash_coverage.py +++ b/scripts/test_broker_state_hash_coverage.py @@ -15,6 +15,9 @@ ROOT = Path(__file__).resolve().parents[1] HEADER = (ROOT / "include/pineforge/engine.hpp").read_text() +EXPANSION = (ROOT / "include/pineforge/reservation_expansion.hpp").read_text() +EXPANSION_SOURCE = (ROOT / "src/reservation_expansion.cpp").read_text() +FROZEN = (ROOT / "include/pineforge/compat/pine/frozen_market_instruction.hpp").read_text() QUANTITY = (ROOT / "include/pineforge/quantity_intent.hpp").read_text() EVENTS = (ROOT / "include/pineforge/broker_events.hpp").read_text() ACTIVATION = (ROOT / "include/pineforge/leg_activation.hpp").read_text() @@ -30,13 +33,16 @@ class PhysicalLotCoverage(unittest.TestCase): def check(self, header=HEADER, source=SOURCE, waivers=WAIVERS, events=EVENTS, - intraday=INTRADAY, policy=POLICY, obligation=OBLIGATION, stream=STREAM, quantity=QUANTITY, birth=BIRTH, activation=ACTIVATION, exit_policy=EXIT_POLICY): + intraday=INTRADAY, policy=POLICY, obligation=OBLIGATION, stream=STREAM, quantity=QUANTITY, birth=BIRTH, activation=ACTIVATION, exit_policy=EXIT_POLICY, expansion=EXPANSION, expansion_source=EXPANSION_SOURCE, frozen=FROZEN): with tempfile.TemporaryDirectory(prefix="pf-lot-hash-check-") as temp: root = Path(temp) for name, content in [ ("include/pineforge/engine.hpp", header), + ("include/pineforge/compat/pine/frozen_market_instruction.hpp", frozen), ("include/pineforge/broker_events.hpp", events), ("include/pineforge/quantity_intent.hpp", quantity), + ("include/pineforge/reservation_expansion.hpp", expansion), + ("src/reservation_expansion.cpp", expansion_source), ("include/pineforge/order_birth.hpp", birth), ("include/pineforge/leg_activation.hpp", activation), ("include/pineforge/compat/pine/exit_activation.hpp", exit_policy), @@ -59,6 +65,72 @@ def check(self, header=HEADER, source=SOURCE, waivers=WAIVERS, events=EVENTS, output.write(str(exc.code)) return code, output.getvalue() + def test_standalone_reservation_abi_is_versioned_in_header_and_source(self): + for old in ("reservation_expansion_v0", "reservation_expansion_v2"): + self.assertEqual(self.check(expansion=EXPANSION.replace( + "reservation_expansion_v1", old))[0], 1) + self.assertEqual(self.check(expansion_source=EXPANSION_SOURCE.replace( + "reservation_expansion_v1", old))[0], 1) + for name in ("ReservationExpansionCapture", "ReservationExpansion", "ReservationGrowthSource"): + self.assertEqual(self.check(expansion=EXPANSION.replace( + name + " {", name + "Outside {"))[0], 1) + + def test_every_capture_and_source_fold_is_required(self): + for fold in ["f.b(o.reservation_expansion.capture().has_value());", + "f.i(capture->position_cycle);", "f.i(static_cast(capture->side));", + "f.b(capture->first_later_admission.has_value());", "f.u(*admission);", + "f.b(o.reservation_growth_source.reservation_owner().has_value());", "f.u(*receiver);"]: + with self.subTest(fold=fold): + self.assertEqual(self.check(source=SOURCE.replace(fold, ""))[0], 1) + self.assertEqual(self.check(source=SOURCE.replace(fold, "/* " + fold + " */"))[0], 1) + self.assertEqual(self.check(source=SOURCE.replace(fold, fold.replace("f.", "wrong.")))[0], 1) + for owner in ["reservation_expansion", "reservation_growth_source"]: + self.assertEqual(self.check(waivers=WAIVERS + "\npending_order." + owner + " # forbidden\n")[0], 1) + + def test_capture_nested_storage_cannot_hide(self): + for field in ["std::optional capture_;", "int64_t position_cycle;", + "PositionSide side;", "std::optional first_later_admission;", + "std::optional reservation_owner_;"]: + with self.subTest(field=field): + self.assertEqual(self.check(expansion=EXPANSION.replace(field,field + " int hidden;"))[0],1) + self.assertEqual(self.check(expansion=EXPANSION.replace(field,field.replace("int64_t", "int").replace("PositionSide", "int").replace("ReservationExpansionCapture", "int")))[0],1) + + def test_capture_folds_cannot_be_conditional_rebound_or_duplicated(self): + start = SOURCE.index(" f.b(o.reservation_expansion.capture().has_value());") + end = SOURCE.index(" // Suppressed-close", start) + block = SOURCE[start:end] + for mutated in ["if (false) {" + block + "}", block + block, + block.replace("capture = o.reservation_expansion.capture()", "capture = foreign.capture()"), + block.replace("receiver = o.reservation_growth_source.reservation_owner()", "receiver = foreign.reservation_owner()"), + block.replace("admission = capture->first_later_admission", "admission = foreign.first_later_admission"), + "if(false) " + block]: + with self.subTest(mutated=mutated): + self.assertEqual(self.check(source=SOURCE[:start] + mutated + SOURCE[end:])[0],1) + def test_frozen_instruction_live_payloads_and_role_cannot_escape(self): + for fold in ["f.i(static_cast(o.pine_frozen_market_instruction.kind()));", + "f.d(transaction->own_units);", "f.d(transaction->transaction_units);", + "f.s(close->target_id);"]: + for replacement in ["", "// " + fold, "if (false) " + fold, + fold.replace("f.", "other.")]: + with self.subTest(fold=fold, replacement=replacement): + self.assertIn(fold, SOURCE) + self.assertEqual(self.check(source=SOURCE.replace(fold, replacement))[0], 1) + self.assertEqual(self.check(source=SOURCE.replace(fold, "") + "\n" + fold)[0], 1) + self.assertEqual(self.check(waivers=WAIVERS + + "\npending_order.pine_frozen_market_instruction # attempted omission\n")[0], 1) + + def test_frozen_instruction_new_fields_and_alternatives_fail_closed(self): + for old, new in [ + ("double own_units;", "double own_units; double hidden;"), + ("std::string target_id;", "std::string target_id; uint64_t hidden;"), + ("Value value_;", "Value value_; bool hidden_;"), + ("std::monostate, Transaction, TargetedClose", "std::monostate, TargetedClose, Transaction"), + ("Ordinary, Transaction, TargetedClose", "Ordinary, Transaction, TargetedClose, Extra"), + ]: + with self.subTest(old=old): + self.assertIn(old, FROZEN) + self.assertEqual(self.check(frozen=FROZEN.replace(old, new))[0], 1) + def test_exit_activation_has_complete_unconditional_coverage(self): for fold in ["f.b(o.leg_activation.bounds().has_value());", "f.i(bounds->position_cycle);", "f.i(bounds->stop_first_bar);", "f.i(bounds->limit_first_bar);", @@ -99,26 +171,26 @@ def test_quantity_model_additions_and_variant_drift_fail_closed(self): self.assertIn(old, QUANTITY) self.assertEqual(self.check(quantity=QUANTITY.replace(old, new))[0], 1) - def test_layout_and_hash_versions_must_match_v6_contract(self): - self.assertIn("engine_script_run_v6", HEADER) - self.assertIn('f.s("pineforge-broker-state/v6");', SOURCE) - self.assertIn("integer(6); integer(broker_state_hash());", STREAM) - self.assertEqual(self.check(header=HEADER.replace("engine_script_run_v6", "engine_script_run_v2"))[0], 1) + def test_layout_and_hash_versions_must_match_v7_contract(self): + self.assertIn("engine_script_run_v7", HEADER) + self.assertIn('f.s("pineforge-broker-state/v7");', SOURCE) + self.assertIn("integer(7); integer(broker_state_hash());", STREAM) + self.assertEqual(self.check(header=HEADER.replace("engine_script_run_v7", "engine_script_run_v2"))[0], 1) for replacement in ['f.s("pineforge-broker-state/v2");', '', - '// f.s("pineforge-broker-state/v6");']: + '// f.s("pineforge-broker-state/v7");']: self.assertEqual(self.check(source=SOURCE.replace( - 'f.s("pineforge-broker-state/v6");', replacement))[0], 1) + 'f.s("pineforge-broker-state/v7");', replacement))[0], 1) for replacement in ["integer(2); integer(broker_state_hash());", "integer(broker_state_hash());", - "if (false) { integer(6); integer(broker_state_hash()); }"]: + "if (false) { integer(7); integer(broker_state_hash()); }"]: self.assertEqual(self.check(stream=STREAM.replace( - "integer(6); integer(broker_state_hash());", replacement))[0], 1) + "integer(7); 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/v6");' + broker_fold = 'f.s("pineforge-broker-state/v7");' altered = SOURCE.replace(broker_fold, '') + '\nvoid other() { ' + broker_fold + ' }\n' self.assertEqual(self.check(source=altered)[0], 1) - stream_fold = "integer(6); integer(broker_state_hash());" + stream_fold = "integer(7); integer(broker_state_hash());" altered = STREAM.replace(stream_fold, '') + '\nvoid other() { ' + stream_fold + ' }\n' self.assertEqual(self.check(stream=altered)[0], 1) diff --git a/scripts/test_reservation_expansion_mirror.py b/scripts/test_reservation_expansion_mirror.py new file mode 100644 index 0000000..08a2c5a --- /dev/null +++ b/scripts/test_reservation_expansion_mirror.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +"""Metadata-only mutations of the exact nested reservation mirror contract.""" +from contextlib import redirect_stderr, redirect_stdout +from io import StringIO +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch +import gen_pending_order_mirror as mirror + +ROOT = Path(__file__).resolve().parents[1] +HEADER = (ROOT / 'include/pineforge/reservation_expansion.hpp').read_text() + +class ReservationMirror(unittest.TestCase): + def test_complete_schema_and_generated_output(self): + header, source = mirror.generate() + self.assertEqual(header, mirror.OUT_H.read_text()) + self.assertEqual(source, mirror.OUT_C.read_text()) + self.assertIn('155 POD fields', header) + + def test_every_nested_mapping_is_observable(self): + original = mirror.generate() + for typename in ['ReservationExpansion', 'ReservationGrowthSource']: + values = mirror.COMPOSITE_MAP[typename] + for index, (name, kind, expression) in enumerate(values): + for mutation in ['omit', 'name', 'type', 'value']: + with self.subTest(typename=typename, field=name, mutation=mutation): + changed = values.copy() + if mutation == 'omit': del changed[index] + if mutation == 'name': changed[index] = ('wrong_' + name, kind, expression) + if mutation == 'type': changed[index] = (name, 'double' if kind != 'double' else 'int64_t', expression) + if mutation == 'value': changed[index] = (name, kind, '0') + with patch.dict(mirror.COMPOSITE_MAP, {typename:changed}): + self.assertNotEqual(mirror.generate(), original) + with redirect_stdout(StringIO()): self.assertEqual(mirror.main(['--check']), 1) + + def test_legacy_projection_mutations_are_observable(self): + for field in ['pooc_global_full_exit_dynamic_qty', 'pooc_global_full_exit_tracks_bound_adds', 'pooc_global_full_exit_bound_add']: + with self.subTest(field=field), patch.dict(mirror.LEGACY_OUTPUTS, {field:'0'}), redirect_stdout(StringIO()): + self.assertEqual(mirror.main(['--check']),1) + + def test_nested_storage_growth_fails_generation(self): + for declaration in ['std::optional capture_;', + 'int64_t position_cycle;', 'PositionSide side;', + 'std::optional first_later_admission;', + 'std::optional reservation_owner_;']: + with self.subTest(declaration=declaration), tempfile.TemporaryDirectory() as temp: + root = Path(temp) + path = root / 'include/pineforge/reservation_expansion.hpp' + path.parent.mkdir(parents=True) + path.write_text(HEADER.replace(declaration,declaration + ' int hidden;')) + with patch.object(mirror,'ROOT',root), self.assertRaises(ValueError): mirror.generate() + + def test_receipts_cannot_be_waived(self): + for field in ['reservation_expansion','reservation_growth_source']: + with self.subTest(field=field), self.assertRaises(SystemExit), redirect_stderr(StringIO()): + mirror.classify(mirror.members(),{field:'forbidden'}) + +if __name__ == '__main__': unittest.main() diff --git a/src/compat/pine/reservation_expansion.cpp b/src/compat/pine/reservation_expansion.cpp new file mode 100644 index 0000000..0a6cdea --- /dev/null +++ b/src/compat/pine/reservation_expansion.cpp @@ -0,0 +1,28 @@ +#include +#include +#include "../../engine_internal.hpp" + +namespace pineforge::compat::pine { +std::vector select_reservation_growth_sources(const std::vector& book, + const std::string& from_entry, bool process_on_close, bool effectively_flat, + double percent, int bar, PositionSide side) { + if (!from_entry.empty() || !process_on_close || effectively_flat + || percent < 100.0 - internal::kFullPercentEps) return {}; + std::vector selected; + for (const auto& source : book) { + if (source.type != OrderType::MARKET && source.type != OrderType::ENTRY + && source.type != OrderType::RAW_ORDER) continue; + const auto requested = source.is_long ? PositionSide::LONG : PositionSide::SHORT; + if (source.created_bar != bar || source.type != OrderType::MARKET + || source.birth.from_fill() || source.over_pyramiding_cap_at_placement + || requested != side || source.created_position_side != side) return {}; + selected.push_back(source.incarnation); + } + return selected; +} +bool admits_reservation_expansion(const std::vector& selected, + bool partial, double reserved, double live) { + return !selected.empty() && !partial && std::isfinite(reserved) + && reserved >= live - internal::kFullQtyEps; +} +} // namespace pineforge::compat::pine diff --git a/src/engine_fills.cpp b/src/engine_fills.cpp index eb5db22..88959e1 100644 --- a/src/engine_fills.cpp +++ b/src/engine_fills.cpp @@ -177,13 +177,14 @@ bool internal::dual_stop_margin_decline_can_continue_path( // resting priced order, a strategy.exit bracket, a close_all, a third entry, // a same-id pair) is outside the tapes; strip the membership so every order // takes its established kernel, byte-identical to the pre-famS engine. -void BacktestEngine::finalize_same_bar_market_tx_book() { +void compat::pine::finalize_frozen_market_book( + std::vector& orders, bool source_scope_live) { bool any_member = false; bool exact = true; int market_members = 0; std::string first_market_id; - for (const PendingOrder& order : pending_orders_) { - if (!order.sbmt_member) { + for (const PendingOrder& order : orders) { + if (!order.pine_frozen_market_instruction.active()) { exact = false; continue; } @@ -197,17 +198,17 @@ void BacktestEngine::finalize_same_bar_market_tx_book() { } } } - if (!any_member || (exact && same_bar_market_tx_scope_is_live())) return; - for (PendingOrder& order : pending_orders_) { - order.sbmt_member = false; - order.sbmt_own_qty = std::numeric_limits::quiet_NaN(); - order.sbmt_tx_qty = std::numeric_limits::quiet_NaN(); - order.sbmt_kept_over_cap = false; - order.sbmt_close_qty = std::numeric_limits::quiet_NaN(); - order.sbmt_close_buy = false; + if (!any_member || (exact && source_scope_live)) return; + for (PendingOrder& order : orders) { + order.pine_frozen_market_instruction.revoke(); } } +void BacktestEngine::finalize_same_bar_market_tx_book() { + compat::pine::finalize_frozen_market_book( + pending_orders_, same_bar_market_tx_scope_is_live()); +} + // A carried long can owe the broker's one-contract money-rounding trim at // the OPEN before its resting take-profit/stop is reached later on the path. // The full-bar exit pass used to erase that position first. Covered controls @@ -3932,8 +3933,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.pooc_global_full_exit_dynamic_qty - && !order.pooc_global_full_exit_tracks_bound_adds + && !order.reservation_expansion.capture() && !order.suppress_as_declined_reversal_close && std::isfinite(order.suppressed_close_consumed_ledger_qty) && order.suppressed_close_consumed_ledger_qty > kQtyEpsilon; @@ -4165,9 +4165,9 @@ void BacktestEngine::sort_orders_by_fill_phase(const Bar& bar) { // rank with the buys; the key is a pure function of the order. if (pa == 0) { auto sbmt_sell_rank = [](const PendingOrder& o) { - if (!o.sbmt_member) return 0; + if (!o.pine_frozen_market_instruction.active()) return 0; const bool buy = o.type == OrderType::MARKET - ? o.is_long : o.sbmt_close_buy; + ? o.is_long : (o.created_position_side == PositionSide::SHORT); return buy ? 0 : 1; }; const int ra = sbmt_sell_rank(a); @@ -4494,10 +4494,10 @@ bool BacktestEngine::same_bar_market_tx_scope_is_live() const { // close is cancelled (rev-plus-close, dbl-short-swapped: no artifact row). bool BacktestEngine::same_bar_market_close_artifact_is_live( const PendingOrder& order) const { - if (!order.sbmt_member + if (!order.pine_frozen_market_instruction.targeted_close() || order.type != OrderType::EXIT - || !std::isfinite(order.sbmt_close_qty) - || order.sbmt_close_qty <= kQtyEpsilon + || !std::isfinite(order.quantity_request.intent()->units()) + || order.quantity_request.intent()->units() <= kQtyEpsilon || order.created_bar + 1 != bar_index_ || order.suppress_as_declined_reversal_close || position_side_ == PositionSide::FLAT @@ -4505,13 +4505,14 @@ bool BacktestEngine::same_bar_market_close_artifact_is_live( return false; } const PositionSide target_side = - order.sbmt_close_buy ? PositionSide::SHORT : PositionSide::LONG; + order.created_position_side; if (position_side_ == target_side) return false; if (order.id.size() <= kClosePrefix.size() || order.id.compare(0, kClosePrefix.size(), kClosePrefix) != 0) { return false; } - const std::string target_id = order.id.substr(kClosePrefix.size()); + const std::string& target_id = order.pine_frozen_market_instruction.targeted_close()->target_id; + if (order.id.substr(kClosePrefix.size()) != target_id) return false; const auto live = std::find_if(pending_orders_.begin(), pending_orders_.end(), [&](const PendingOrder& pending) { return same_pending_order(pending, order); }); if (live == pending_orders_.end()) return false; @@ -4519,10 +4520,10 @@ bool BacktestEngine::same_bar_market_close_artifact_is_live( for (size_t j = self + 1; j < pending_orders_.size(); ++j) { const PendingOrder& sib = pending_orders_[j]; if (sib.type == OrderType::MARKET - && sib.sbmt_member + && sib.pine_frozen_market_instruction.transaction() && sib.id == target_id && sib.created_bar == order.created_bar - && sib.is_long != order.sbmt_close_buy) { + && sib.is_long != (order.created_position_side == PositionSide::SHORT)) { return true; } } @@ -4541,7 +4542,7 @@ void BacktestEngine::apply_same_bar_market_tx_reversal( double& trail_best_path_state) { const PositionSide requested = order.is_long ? PositionSide::LONG : PositionSide::SHORT; - const double tx = order.sbmt_tx_qty; + const double tx = order.pine_frozen_market_instruction.transaction()->transaction_units; const double close_qty = std::min(tx, position_qty_); if (close_qty >= position_qty_ - kQtyEpsilon) { execute_market_exit(fill_price); @@ -4798,7 +4799,8 @@ void BacktestEngine::compact_filled_pending_orders( && pending_orders_[read].created_position_side == closed_side && !resting_limit_entry_carry // round 8 family S, rule 2 (lockstep with classify_order_eligibility). - && !pending_orders_[read].sbmt_kept_over_cap; + && !(pending_orders_[read].pine_frozen_market_instruction.transaction() + && pending_orders_[read].over_pyramiding_cap_at_placement); if (!is_filled(pending_orders_[read].incarnation) && !stale_same_direction_entry_after_exit) { if (write != read) pending_orders_[write] = std::move(pending_orders_[read]); @@ -4944,24 +4946,24 @@ int BacktestEngine::probe_fill_qty(int index, double fill_price, double* qty, *partition = 1; kernel_close_only = !(residual > kQtyEpsilon); sized = true; - } else if (o.type == OrderType::MARKET && o.sbmt_member - && std::isfinite(o.sbmt_tx_qty) && o.sbmt_tx_qty > kQtyEpsilon + } else if (o.type == OrderType::MARKET && o.pine_frozen_market_instruction.transaction() + && std::isfinite(o.pine_frozen_market_instruction.transaction()->transaction_units) && o.pine_frozen_market_instruction.transaction()->transaction_units > kQtyEpsilon && same_bar_market_tx_scope_is_live()) { if (opposite_live_position) { - const double close_qty = std::min(o.sbmt_tx_qty, position_qty_); - const double remainder = o.sbmt_tx_qty - close_qty; + const double close_qty = std::min(o.pine_frozen_market_instruction.transaction()->transaction_units, position_qty_); + const double remainder = o.pine_frozen_market_instruction.transaction()->transaction_units - close_qty; *qty = remainder; *partition = 1; kernel_close_only = !(remainder > kQtyEpsilon); sized = true; - } else if (position_side_ == requested_side && o.sbmt_kept_over_cap) { - *qty = o.sbmt_tx_qty; + } else if (position_side_ == requested_side && o.over_pyramiding_cap_at_placement) { + *qty = o.pine_frozen_market_instruction.transaction()->transaction_units; *partition = 1; sized = true; } else if (position_side_ == PositionSide::FLAT - && std::isfinite(o.sbmt_own_qty) - && o.sbmt_tx_qty > o.sbmt_own_qty + kQtyEpsilon) { - *qty = o.sbmt_tx_qty; + && std::isfinite(o.pine_frozen_market_instruction.transaction()->own_units) + && o.pine_frozen_market_instruction.transaction()->transaction_units > o.pine_frozen_market_instruction.transaction()->own_units + kQtyEpsilon) { + *qty = o.pine_frozen_market_instruction.transaction()->transaction_units; *partition = 1; sized = true; } @@ -5265,6 +5267,10 @@ void BacktestEngine::apply_filled_order_to_state( bool admitted_flat_on_price_gap_band = false; { PendingOrder& order = pending_orders_.at(order_index); + // A deferred-compaction object is not actionable twice. The owned copy + // of this dispatch may still settle after scheduling its own retirement. + if (std::find(retired_incarnations.begin(), retired_incarnations.end(), order.incarnation) + != retired_incarnations.end()) return; cap_origin = max_intraday_filled_orders_.origin( pine_cap_clock(), pine_cap_calculation(), order.incarnation, broker_fill_event_seq_); auto decline_and_cancel = [&]() { @@ -5351,7 +5357,7 @@ void BacktestEngine::apply_filled_order_to_state( && std::isnan(order.qty) && default_qty_type_ == QtyType::FIXED && position_side_ != PositionSide::FLAT - && !order.sbmt_member) { + && !order.pine_frozen_market_instruction.active()) { const PositionSide requested = order.is_long ? PositionSide::LONG : PositionSide::SHORT; const bool same_side_at_creation = @@ -5768,7 +5774,7 @@ void BacktestEngine::apply_filled_order_to_state( return true; }; if (order.type == OrderType::MARKET && std::isnan(order.qty) - && !order.affordability_close_only && !order.sbmt_member + && !order.affordability_close_only && !order.pine_frozen_market_instruction.active() && position_side_ == PositionSide::FLAT && (order.created_position_side == PositionSide::FLAT || order.created_after_position_close_in_bar) @@ -6185,9 +6191,11 @@ void BacktestEngine::apply_filled_order_to_state( // FIXED/no-fee orders carry the transaction marker even // when no sibling exists. Exclude an expanded transaction, // not an otherwise-unused default sizing declaration. - && (!order.sbmt_member - || (order.sbmt_tx_qty == order.sbmt_own_qty - && !order.sbmt_kept_over_cap)) + && (!order.pine_frozen_market_instruction.active() + || (order.pine_frozen_market_instruction.transaction() + && order.pine_frozen_market_instruction.transaction()->transaction_units + == order.pine_frozen_market_instruction.transaction()->own_units + && !order.over_pyramiding_cap_at_placement)) && order.created_bar == bar_index_ - 1 && order.oca_type == 0 && order.oca_name.empty() && pending_orders_.size() == 1 @@ -6305,6 +6313,7 @@ void BacktestEngine::apply_filled_order_to_state( if (position_side_ == PositionSide::SHORT) return -position_qty_; return 0.0; }; + const int64_t position_cycle_before_fill = position_cycle_seq_; const PositionSide position_side_before_fill = position_side_; const double position_qty_before_fill = position_qty_; const size_t pyramid_lots_before_fill = pyramid_entries_.size(); @@ -6450,33 +6459,23 @@ void BacktestEngine::apply_filled_order_to_state( primary_fill_applied ? compat::pine::FillOutcome::Committed : compat::pine::FillOutcome::NoEffect, cap_origin); - // Bounded POOC global-exit growth. Only MARKET adds that were already - // pending when the one tracking EXIT was armed carry this relation bit. - // Grow its ordinary finite reservation by the actual same-side quantity - // delta—not requested qty—so fill-time rejection, zero-fill, reversal, or - // flat-open role changes add nothing. The tracking bit intentionally - // survives dynamic-marker invalidation: a post-exit order can be admitted - // before this pre-exit add reaches the POOC close fill point. - if (order.type == OrderType::MARKET - && order.pooc_global_full_exit_bound_add) { - const PositionSide requested_side = order.is_long - ? PositionSide::LONG : PositionSide::SHORT; - const bool successful_same_side_add = - requested_side == order.created_position_side - && position_side_before_fill == requested_side - && position_side_ == requested_side - && position_qty_ > position_qty_before_fill + kQtyEpsilon; - if (successful_same_side_add) { - const double added_qty = - position_qty_ - position_qty_before_fill; - for (auto& candidate : pending_orders_) { - if (candidate.type != OrderType::EXIT - || !candidate.pooc_global_full_exit_tracks_bound_adds - || !std::isfinite(candidate.qty)) { - continue; - } - candidate.qty += added_qty; - break; // reservation accounting admits at most one tracker + // Settle at the existing post-primary checkpoint, before OCA/risk. Source + // receipts are the only edges; historical dead-owner receipts never fall + // back to another EXIT, even when labels or queue priority match. + if (order.type == OrderType::MARKET && order.reservation_growth_source.reservation_owner()) { + const PositionSide requested = order.is_long ? PositionSide::LONG : PositionSide::SHORT; + const uint64_t receiver = *order.reservation_growth_source.reservation_owner(); + if (requested == order.created_position_side && position_side_before_fill == requested + && position_side_ == requested + && std::find(retired_incarnations.begin(), retired_incarnations.end(), receiver) + == retired_incarnations.end()) { + for (auto& target : pending_orders_) { + if (target.incarnation != receiver) continue; + if (target.type == OrderType::EXIT) + target.reservation_expansion.grow(target.qty, + position_cycle_before_fill, position_side_before_fill, position_qty_before_fill, + position_cycle_seq_, position_side_, position_qty_, kQtyEpsilon); + break; } } } @@ -6925,7 +6924,7 @@ bool BacktestEngine::replaced_percent_short_market_is_live( || (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.affordability_close_only || order.pine_frozen_market_instruction.active() || order.created_bar != bar_index_ - 1 || order.birth.from_fill() || order.created_after_position_close_in_bar @@ -6980,7 +6979,7 @@ bool BacktestEngine::replaced_percent_short_market_is_live( || other.created_after_position_close_in_bar || !std::isnan(other.qty) || other.qty_type >= 0 || other.frozen_default_qty != order.frozen_default_qty - || other.affordability_close_only || other.sbmt_member + || other.affordability_close_only || other.pine_frozen_market_instruction.active() || !other.oca_name.empty() || other.oca_type != 0) return false; } return true; @@ -7089,8 +7088,8 @@ void BacktestEngine::apply_market_order_fill(PendingOrder& order, double fill_pr // the frozen size. The ordinary single-entry shapes (tx == own) below // stay byte-identical. bool sbmt_flat_frozen_tx = false; - if (order.sbmt_member && std::isfinite(order.sbmt_tx_qty) - && order.sbmt_tx_qty > kQtyEpsilon + if (order.pine_frozen_market_instruction.transaction() && std::isfinite(order.pine_frozen_market_instruction.transaction()->transaction_units) + && order.pine_frozen_market_instruction.transaction()->transaction_units > kQtyEpsilon && same_bar_market_tx_scope_is_live()) { const PositionSide requested = order.is_long ? PositionSide::LONG : PositionSide::SHORT; @@ -7100,11 +7099,11 @@ void BacktestEngine::apply_market_order_fill(PendingOrder& order, double fill_pr trail_best_path_state); return; } - if (position_side_ == requested && order.sbmt_kept_over_cap) { + if (position_side_ == requested && order.over_pyramiding_cap_at_placement) { // dbl-long-mirror-closefirst: the kept Long buys its frozen 2 // while still long (long 3) before the Short and close-Long // sell — an add past the pyramiding cap, never a rejected add. - const double add_qty = order.sbmt_tx_qty; + const double add_qty = order.pine_frozen_market_instruction.transaction()->transaction_units; const double entry_fill = apply_fill_slippage(fill_price, order.is_long); if (std::isfinite(entry_fill) && add_qty > kQtyEpsilon) { @@ -7141,8 +7140,8 @@ void BacktestEngine::apply_market_order_fill(PendingOrder& order, double fill_pr } sbmt_flat_frozen_tx = position_side_ == PositionSide::FLAT - && std::isfinite(order.sbmt_own_qty) - && order.sbmt_tx_qty > order.sbmt_own_qty + kQtyEpsilon; + && std::isfinite(order.pine_frozen_market_instruction.transaction()->own_units) + && order.pine_frozen_market_instruction.transaction()->transaction_units > order.pine_frozen_market_instruction.transaction()->own_units + kQtyEpsilon; } // A default-sized market order carries a quantity frozen at the signal @@ -7181,7 +7180,7 @@ void BacktestEngine::apply_market_order_fill(PendingOrder& order, double fill_pr const double dispatch_qty = paired_flat_market ? order.paired_flat_market_transaction_qty : (sbmt_flat_frozen_tx - ? order.sbmt_tx_qty + ? order.pine_frozen_market_instruction.transaction()->transaction_units : (frozen ? order.frozen_default_qty : order.qty)); const int dispatch_qty_type = paired_flat_market ? -1 @@ -7471,16 +7470,16 @@ void BacktestEngine::apply_exit_order_fill(PendingOrder& order, double fill_pric // position, exactly the short-seed kernel's min(S, L) above. bool sbmt_frozen_close = false; double sbmt_frozen_close_qty = std::numeric_limits::quiet_NaN(); - if (order.sbmt_member && std::isfinite(order.sbmt_close_qty) - && order.sbmt_close_qty > kQtyEpsilon + if (order.pine_frozen_market_instruction.targeted_close() && std::isfinite(order.quantity_request.intent()->units()) + && order.quantity_request.intent()->units() > kQtyEpsilon && same_bar_market_tx_scope_is_live()) { const PositionSide target_side = - order.sbmt_close_buy ? PositionSide::SHORT : PositionSide::LONG; + order.created_position_side; if (position_side_ != target_side) { if (!same_bar_market_close_artifact_is_live(order)) return; - const double qty = std::min(order.sbmt_close_qty, position_qty_); + const double qty = std::min(order.quantity_request.intent()->units(), position_qty_); const double entry_fill = - apply_fill_slippage(fill_price, /*is_buy=*/order.sbmt_close_buy); + apply_fill_slippage(fill_price, /*is_buy=*/(order.created_position_side == PositionSide::SHORT)); if (!std::isfinite(entry_fill) || qty <= kQtyEpsilon) return; const double total_qty = position_qty_ + qty; position_entry_price_ = @@ -7505,12 +7504,12 @@ void BacktestEngine::apply_exit_order_fill(PendingOrder& order, double fill_pric return; } sbmt_frozen_close = true; - sbmt_frozen_close_qty = std::min(order.sbmt_close_qty, position_qty_); + sbmt_frozen_close_qty = std::min(order.quantity_request.intent()->units(), position_qty_); } double qp = std::isnan(order.qty_percent) ? 100.0 : std::clamp(order.qty_percent, 0.0, 100.0); const bool dynamic_full_live_qty = - order.pooc_global_full_exit_dynamic_qty; + order.reservation_expansion.live_all(position_cycle_seq_, position_side_); bool has_explicit_qty_to_close = !dynamic_full_live_qty && !std::isnan(order.qty); double qty_before_exit = position_qty_; @@ -8345,7 +8344,8 @@ BacktestEngine::OrderEligibility BacktestEngine::classify_order_eligibility( && !resting_limit_entry_carry && !coqueued_within_cap && !same_id_stop_preserved_by_deferred_close_all - && !order.sbmt_kept_over_cap) { + && !(order.pine_frozen_market_instruction.transaction() + && order.over_pyramiding_cap_at_placement)) { return OrderEligibility::Remove; } diff --git a/src/engine_state_hash.cpp b/src/engine_state_hash.cpp index 704892e..36c858c 100644 --- a/src/engine_state_hash.cpp +++ b/src/engine_state_hash.cpp @@ -88,7 +88,7 @@ uint64_t BacktestEngine::broker_state_hash() const { Fnv f; // v6 includes resolved exit-leg owner/coordinates and immutable Pine placement evidence. // It is a serialization boundary, independent of the public C ABI version. - f.s("pineforge-broker-state/v6"); + f.s("pineforge-broker-state/v7"); // --- Position core --- f.i(static_cast(position_side_)); @@ -211,8 +211,13 @@ uint64_t BacktestEngine::broker_state_hash() const { f.u(o.signal_close_mc_fill_seq); // Round-8 family S same-bar MARKET transaction (sizing frozen at // placement) and its strategy.close(id) companion. - f.b(o.sbmt_member); f.d(o.sbmt_own_qty); f.d(o.sbmt_tx_qty); - f.b(o.sbmt_kept_over_cap); f.d(o.sbmt_close_qty); f.b(o.sbmt_close_buy); + f.i(static_cast(o.pine_frozen_market_instruction.kind())); + if (const auto* transaction = o.pine_frozen_market_instruction.transaction()) { + f.d(transaction->own_units); f.d(transaction->transaction_units); + } + if (const auto* close = o.pine_frozen_market_instruction.targeted_close()) { + f.s(close->target_id); + } // KI-65 dual same-bar opposite-MARKET pairing candidate/finalization. f.b(o.paired_flat_market_candidate); f.d(o.paired_flat_market_own_qty); @@ -291,10 +296,19 @@ uint64_t BacktestEngine::broker_state_hash() const { f.d(o.explicit_slipped_signal_close); // Round-7 default-percent stop placement basis. f.d(o.default_stop_placement_signal_close); - // POOC global-full-exit relation bits. - f.b(o.pooc_global_full_exit_dynamic_qty); - f.b(o.pooc_global_full_exit_tracks_bound_adds); - f.b(o.pooc_global_full_exit_bound_add); + f.b(o.reservation_expansion.capture().has_value()); + if (const auto& capture = o.reservation_expansion.capture()) { + f.i(capture->position_cycle); + f.i(static_cast(capture->side)); + f.b(capture->first_later_admission.has_value()); + if (const auto& admission = capture->first_later_admission) { + f.u(*admission); + } + } + f.b(o.reservation_growth_source.reservation_owner().has_value()); + if (const auto& receiver = o.reservation_growth_source.reservation_owner()) { + f.u(*receiver); + } // Suppressed-close ledger re-credit amounts. f.d(o.suppressed_close_consumed_ledger_qty); f.d(o.suppressed_close_retired_ledger_qty); diff --git a/src/engine_strategy_commands.cpp b/src/engine_strategy_commands.cpp index 5a0fd56..abdc48b 100644 --- a/src/engine_strategy_commands.cpp +++ b/src/engine_strategy_commands.cpp @@ -25,6 +25,7 @@ #include "engine_internal.hpp" #include +#include #include #include @@ -330,10 +331,10 @@ void BacktestEngine::strategy_entry(const std::string& id, bool is_long, || sib.id == id) { continue; } - if (sib.type == OrderType::MARKET && sib.sbmt_member - && std::isfinite(sib.sbmt_own_qty)) { + if (sib.type == OrderType::MARKET && sib.pine_frozen_market_instruction.transaction() + && std::isfinite(sib.pine_frozen_market_instruction.transaction()->own_units)) { sbmt_opp_market_pending = true; - sbmt_opp_pending_own += sib.sbmt_own_qty; + sbmt_opp_pending_own += sib.pine_frozen_market_instruction.transaction()->own_units; } else if (sib.type == OrderType::MARKET || sib.type == OrderType::ENTRY || sib.type == OrderType::RAW_ORDER) { @@ -651,11 +652,8 @@ void BacktestEngine::strategy_entry(const std::string& id, bool is_long, const double held_opposite = opposite_live ? std::max(0.0, position_qty_ - pending_close_qty_in_bar_) : 0.0; - order.sbmt_member = true; - order.sbmt_own_qty = sbmt_own_qty; - order.sbmt_tx_qty = - sbmt_own_qty + held_opposite + sbmt_opp_pending_own; - order.sbmt_kept_over_cap = sbmt_over_cap; + order.pine_frozen_market_instruction = PineFrozenMarketInstruction::transaction( + sbmt_own_qty, sbmt_own_qty + held_opposite + sbmt_opp_pending_own); } if (paired_flat_market_candidate) { order.paired_flat_market_candidate = true; @@ -838,7 +836,7 @@ void BacktestEngine::strategy_entry(const std::string& id, bool is_long, } pending_orders_.push_back(std::move(order)); - invalidate_unsafe_pooc_global_full_exit_dynamic_qty(); + close_reservation_capture_populations(pending_orders_.back().incarnation); } void BacktestEngine::strategy_close(const std::string& id, @@ -1867,8 +1865,8 @@ void BacktestEngine::strategy_exit(const std::string& id, const std::string& fro &replaced_dormant, &replaced_dormant_stop); double reserved_qty = std::numeric_limits::quiet_NaN(); - bool bind_global_full_exit_dynamic_qty = false; - std::vector pooc_global_full_exit_bound_add_indices; + bool capture_expansion = false; + std::vector selected_reservation_sources; // Additional bracket legs beyond the primary one (see the leg-multiplicity // block in the explicit-qty branch below). Empty on every other path. std::vector extra_leg_qtys; @@ -2069,39 +2067,10 @@ void BacktestEngine::strategy_exit(const std::string& id, const std::string& fro // separate branch, while from_entry, partial, RAW_ORDER, priced, // opposite, prior-bar, COOF-recalc, over-cap, and mixed-queue shapes // retain the established frozen reservation. - bool eligible_global_full_exit_dynamic_qty = false; - if (from_entry.empty() - && process_orders_on_close_ - && !effectively_flat - && qp >= 100.0 - kFullPercentEps) { - bool found_qualifying_add = false; - bool entry_queue_is_bounded = true; - for (std::size_t i = 0; i < pending_orders_.size(); ++i) { - const PendingOrder& o = pending_orders_[i]; - if (o.type != OrderType::MARKET - && o.type != OrderType::ENTRY - && o.type != OrderType::RAW_ORDER) { - continue; - } - const PositionSide entry_dir = o.is_long - ? PositionSide::LONG : PositionSide::SHORT; - const bool qualifies = o.created_bar == bar_index_ - && o.type == OrderType::MARKET - && !o.birth.from_fill() - && !o.over_pyramiding_cap_at_placement - && entry_dir == position_side_ - && o.created_position_side == position_side_; - if (!qualifies) { - entry_queue_is_bounded = false; - break; - } - found_qualifying_add = true; - pooc_global_full_exit_bound_add_indices.push_back(i); - } - eligible_global_full_exit_dynamic_qty = - found_qualifying_add - && entry_queue_is_bounded; - } + selected_reservation_sources = compat::pine::select_reservation_growth_sources( + pending_orders_, from_entry, process_orders_on_close_, effectively_flat, + qp, bar_index_, position_side_); + bool eligible_expansion = !selected_reservation_sources.empty(); if (!bind_to_pending_reversal_entry) { if (!compute_exit_reserved_qty( @@ -2109,16 +2078,12 @@ void BacktestEngine::strategy_exit(const std::string& id, const std::string& fro qp, is_partial, reserved_qty)) { return; } - eligible_global_full_exit_dynamic_qty = - eligible_global_full_exit_dynamic_qty - && !is_partial - && std::isfinite(reserved_qty) - && reserved_qty >= live_pos_qty - kFullQtyEps; + eligible_expansion = compat::pine::admits_reservation_expansion( + selected_reservation_sources, is_partial, reserved_qty, live_pos_qty); } // The pending order stores this below after the common construction. - bind_global_full_exit_dynamic_qty = - eligible_global_full_exit_dynamic_qty; + capture_expansion = eligible_expansion; } PendingOrder order; @@ -2144,15 +2109,6 @@ void BacktestEngine::strategy_exit(const std::string& id, const std::string& fro // 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 = - bind_global_full_exit_dynamic_qty; - if (bind_global_full_exit_dynamic_qty) { - for (std::size_t index : pooc_global_full_exit_bound_add_indices) { - pending_orders_[index].pooc_global_full_exit_bound_add = true; - } - } // OCA-name plumbing: ``strategy.exit`` supports oca_name (Pine v6) so // siblings in different OCA groups can fire independently. The cancel // sweep predicate (engine_fills.cpp::apply_filled_order_to_state → @@ -2242,7 +2198,20 @@ void BacktestEngine::strategy_exit(const std::string& id, const std::string& fro } if (extra_leg_qtys.empty()) { + if (capture_expansion) + order.reservation_expansion.capture(order.incarnation, position_cycle_seq_, + position_side_, order.qty); + const uint64_t receiver = order.incarnation; pending_orders_.push_back(std::move(order)); + // Publication succeeded; selected identities survive vector reallocation. + // A new legitimate capture explicitly reassigns each authoritative edge. + if (capture_expansion) { + for (auto& source : pending_orders_) { + if (std::find(selected_reservation_sources.begin(), selected_reservation_sources.end(), + source.incarnation) != selected_reservation_sources.end()) + source.reservation_growth_source.assign_capture(source.incarnation, receiver); + } + } return; } @@ -2473,21 +2442,15 @@ void BacktestEngine::strategy_order(const std::string& id, bool is_long, double } pending_orders_.push_back(std::move(order)); - invalidate_unsafe_pooc_global_full_exit_dynamic_qty(); + close_reservation_capture_populations(pending_orders_.back().incarnation); } -void BacktestEngine::invalidate_unsafe_pooc_global_full_exit_dynamic_qty() { - // This helper is called only after a later entry-like placement was - // successfully admitted. The oracle covers adds already pending BEFORE - // the global exit, not any order emitted after it—even another same-side - // MARKET add. Clear every still-live marker, including candidates carried - // into a later bar; their finite ``qty`` remains the conservative - // reservation automatically. - for (auto& o : pending_orders_) { - if (o.type == OrderType::EXIT) { - o.pooc_global_full_exit_dynamic_qty = false; - } - } +void BacktestEngine::close_reservation_capture_populations(uint64_t admitted_incarnation) { + // Called only after an ENTRY/MARKET/RAW admission has actually appended. + // Queue priority can be retained on replacement; this cause cannot. + for (auto& order : pending_orders_) + if (order.type == OrderType::EXIT) + order.reservation_expansion.close_population(admitted_incarnation); } // ──────────────────────────────────────────────────────────────────── @@ -2869,9 +2832,8 @@ uint64_t BacktestEngine::queue_deferred_close_order( && std::isfinite(consumed_ledger_qty) && qty_to_close > eps && position_side_ != PositionSide::FLAT) { - order.sbmt_member = true; - order.sbmt_close_qty = qty_to_close; - order.sbmt_close_buy = position_side_ == PositionSide::SHORT; + order.pine_frozen_market_instruction = PineFrozenMarketInstruction::targeted_close( + id, order.quantity_request); } const uint64_t incarnation = order.incarnation; diff --git a/src/engine_stream.cpp b/src/engine_stream.cpp index 1e522ed..11943d2 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(6); integer(broker_state_hash()); + integer(7); 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 e6e82a7..5f676e6 100644 --- a/src/pending_order_mirror.cpp +++ b/src/pending_order_mirror.cpp @@ -108,16 +108,16 @@ void fill_pending_order_mirror(const PendingOrder& src, pf_pending_order_v1_t* o copy_str(src.comment, out->comment, &out->comment_truncated, &out->comment_hash64); 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->pooc_global_full_exit_dynamic_qty = src.reservation_expansion.population_open() ? 1 : 0; + out->pooc_global_full_exit_tracks_bound_adds = src.reservation_expansion.capture().has_value() ? 1 : 0; + out->pooc_global_full_exit_bound_add = src.reservation_growth_source.reservation_owner().has_value() ? 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; - out->sbmt_kept_over_cap = src.sbmt_kept_over_cap ? 1 : 0; - out->sbmt_close_qty = src.sbmt_close_qty; - out->sbmt_close_buy = src.sbmt_close_buy ? 1 : 0; + out->sbmt_member = src.pine_frozen_market_instruction.active() ? 1 : 0; + out->sbmt_own_qty = src.pine_frozen_market_instruction.transaction() ? src.pine_frozen_market_instruction.transaction()->own_units : std::numeric_limits::quiet_NaN(); + out->sbmt_tx_qty = src.pine_frozen_market_instruction.transaction() ? src.pine_frozen_market_instruction.transaction()->transaction_units : std::numeric_limits::quiet_NaN(); + out->sbmt_kept_over_cap = src.pine_frozen_market_instruction.transaction() && src.over_pyramiding_cap_at_placement ? 1 : 0; + out->sbmt_close_qty = src.pine_frozen_market_instruction.targeted_close() ? src.quantity_request.intent()->units() : std::numeric_limits::quiet_NaN(); + out->sbmt_close_buy = src.pine_frozen_market_instruction.targeted_close() && src.created_position_side == PositionSide::SHORT ? 1 : 0; out->suppress_as_declined_reversal_close = src.suppress_as_declined_reversal_close ? 1 : 0; out->dormant_bracket = src.dormant_bracket ? 1 : 0; out->dormant_reissue_pending = src.dormant_reissue_pending ? 1 : 0; @@ -164,6 +164,17 @@ void fill_pending_order_mirror(const PendingOrder& src, pf_pending_order_v1_t* o out->pine_exit_activation_limit_continuation_present = src.pine_exit_activation.evidence() && src.pine_exit_activation.evidence()->limit_continuation ? 1 : 0; out->pine_exit_activation_limit_continuation_cause = src.pine_exit_activation.evidence() && src.pine_exit_activation.evidence()->limit_continuation ? static_cast(src.pine_exit_activation.evidence()->limit_continuation->cause) : 0; out->pine_exit_activation_limit_continuation_fill = src.pine_exit_activation.evidence() && src.pine_exit_activation.evidence()->limit_continuation ? src.pine_exit_activation.evidence()->limit_continuation->observed_fill_sequence : 0; + out->reservation_expansion_position_cycle = src.reservation_expansion.capture() ? src.reservation_expansion.capture()->position_cycle : 0; + out->reservation_expansion_present = src.reservation_expansion.capture().has_value() ? 1 : 0; + out->reservation_expansion_side = src.reservation_expansion.capture() ? static_cast(src.reservation_expansion.capture()->side) : 0; + out->reservation_expansion_first_later_admission_present = src.reservation_expansion.capture() && src.reservation_expansion.capture()->first_later_admission ? 1 : 0; + out->reservation_expansion_first_later_admission = src.reservation_expansion.capture() && src.reservation_expansion.capture()->first_later_admission ? *src.reservation_expansion.capture()->first_later_admission : 0; + out->reservation_growth_source_present = src.reservation_growth_source.reservation_owner().has_value() ? 1 : 0; + out->reservation_growth_source_reservation_owner = src.reservation_growth_source.reservation_owner() ? *src.reservation_growth_source.reservation_owner() : 0; + out->pine_frozen_market_instruction_kind = static_cast(src.pine_frozen_market_instruction.kind()); + out->pine_frozen_market_instruction_own_units = src.pine_frozen_market_instruction.transaction() ? src.pine_frozen_market_instruction.transaction()->own_units : 0.0; + out->pine_frozen_market_instruction_transaction_units = src.pine_frozen_market_instruction.transaction() ? src.pine_frozen_market_instruction.transaction()->transaction_units : 0.0; + copy_str(src.pine_frozen_market_instruction.targeted_close() ? src.pine_frozen_market_instruction.targeted_close()->target_id : std::string(), out->pine_frozen_market_instruction_target_id, &out->pine_frozen_market_instruction_target_id_truncated, &out->pine_frozen_market_instruction_target_id_hash64); } namespace { @@ -315,6 +326,19 @@ const pf_field_desc_t kLayout[] = { PF_PO_FIELD(pine_exit_activation_limit_continuation_present, "uint8_t"), PF_PO_FIELD(pine_exit_activation_limit_continuation_cause, "int32_t"), PF_PO_FIELD(pine_exit_activation_limit_continuation_fill, "uint64_t"), + PF_PO_FIELD(reservation_expansion_position_cycle, "int64_t"), + PF_PO_FIELD(reservation_expansion_present, "uint8_t"), + PF_PO_FIELD(reservation_expansion_side, "int32_t"), + PF_PO_FIELD(reservation_expansion_first_later_admission_present, "uint8_t"), + PF_PO_FIELD(reservation_expansion_first_later_admission, "uint64_t"), + PF_PO_FIELD(reservation_growth_source_present, "uint8_t"), + PF_PO_FIELD(reservation_growth_source_reservation_owner, "uint64_t"), + PF_PO_FIELD(pine_frozen_market_instruction_kind, "uint64_t"), + PF_PO_FIELD(pine_frozen_market_instruction_own_units, "double"), + PF_PO_FIELD(pine_frozen_market_instruction_transaction_units, "double"), + PF_PO_FIELD(pine_frozen_market_instruction_target_id, "char[64]"), + PF_PO_FIELD(pine_frozen_market_instruction_target_id_truncated, "uint8_t"), + PF_PO_FIELD(pine_frozen_market_instruction_target_id_hash64, "uint64_t"), }; #undef PF_PO_FIELD diff --git a/src/reservation_expansion.cpp b/src/reservation_expansion.cpp new file mode 100644 index 0000000..ad6acca --- /dev/null +++ b/src/reservation_expansion.cpp @@ -0,0 +1,36 @@ +#include +#include +#include +#include + +namespace pineforge { +inline namespace reservation_expansion_v1 { +void ReservationExpansion::capture(uint64_t receiver, int64_t cycle, PositionSide side, double capacity) { + if (capture_ || receiver == 0 || cycle <= 0 + || (side != PositionSide::LONG && side != PositionSide::SHORT) + || !std::isfinite(capacity) || capacity <= 0) + throw std::invalid_argument("invalid or repeated reservation expansion capture"); + capture_ = ReservationExpansionCapture{cycle, side, std::nullopt}; +} +void ReservationExpansion::close_population(uint64_t admitted_incarnation) { + if (admitted_incarnation == 0) + throw std::invalid_argument("reservation closure requires an admitted incarnation"); + if (population_open()) capture_->first_later_admission = admitted_incarnation; +} +bool ReservationExpansion::owns_exposure(int64_t cycle, PositionSide side) const { + return capture_ && capture_->position_cycle == cycle && capture_->side == side; +} +void ReservationExpansion::grow(double& qty, + int64_t before_cycle, PositionSide before_side, double before_qty, + int64_t after_cycle, PositionSide after_side, double after_qty, double epsilon) const { + if (owns_exposure(before_cycle, before_side) && owns_exposure(after_cycle, after_side) + && std::isfinite(qty) && after_qty > before_qty + epsilon) + qty += after_qty - before_qty; +} +void ReservationGrowthSource::assign_capture(uint64_t source, uint64_t receiver) { + if (source == 0 || receiver == 0 || source == receiver) + throw std::invalid_argument("reservation source requires distinct live incarnations"); + reservation_owner_ = receiver; +} +} // inline namespace reservation_expansion_v1 +} // namespace pineforge diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5a7de69..4638944 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,4 +1,7 @@ set(TEST_SOURCES + test_reservation_expansion + test_frozen_market_instruction + test_pine_transaction_settlement test_exit_leg_activation test_exit_activation_routes test_pending_order_core @@ -369,6 +372,12 @@ add_test( ${PROJECT_SOURCE_DIR}/scripts/test_broker_state_hash_coverage.py ) +add_test( + NAME test_reservation_expansion_mirror_mutations + COMMAND ${Python3_EXECUTABLE} + ${PROJECT_SOURCE_DIR}/scripts/test_reservation_expansion_mirror.py +) + # When PINEFORGE_ENABLE_COVERAGE is ON we also instrument the test # binaries — header-only code (Series, na, color, log, math::pine_random) # is otherwise reported as 0% because it's only inlined into test TUs. diff --git a/tests/fixtures/frozen_market/README.md b/tests/fixtures/frozen_market/README.md new file mode 100644 index 0000000..6379a39 --- /dev/null +++ b/tests/fixtures/frozen_market/README.md @@ -0,0 +1,7 @@ +# Frozen public mirror prefix + +`ff54_pending_order_mirror.hpp` is copied byte-for-byte from engine +ff54a557ac751244dafd60df0bb22886ec35792d. `ff54_fields.inc` enumerates its +142 fields for compile-time type, size and offset assertions. The native +frozen instruction extends the public snapshot only after this full prefix. +This fixture contains no strategy, tape or expected trades. diff --git a/tests/fixtures/frozen_market/ff54_fields.inc b/tests/fixtures/frozen_market/ff54_fields.inc new file mode 100644 index 0000000..a98fa60 --- /dev/null +++ b/tests/fixtures/frozen_market/ff54_fields.inc @@ -0,0 +1,143 @@ +// Frozen ff54 public descriptor prefix: all 142 fields. +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) +PF_PREFIX_FIELD(replaced_order_incarnation) +PF_PREFIX_FIELD(birth_timestamp) +PF_PREFIX_FIELD(birth_cause) +PF_PREFIX_FIELD(birth_bar) +PF_PREFIX_FIELD(birth_cursor_domain) +PF_PREFIX_FIELD(birth_cursor_position) +PF_PREFIX_FIELD(birth_cursor_index) +PF_PREFIX_FIELD(birth_cursor_count) +PF_PREFIX_FIELD(birth_cursor_price) +PF_PREFIX_FIELD(birth_first_fill) +PF_PREFIX_FIELD(birth_last_fill) +PF_PREFIX_FIELD(birth_evaluation_ordinal) +PF_PREFIX_FIELD(pine_birth_reach) +PF_PREFIX_FIELD(quantity_intent_kind) +PF_PREFIX_FIELD(quantity_intent_units) +PF_PREFIX_FIELD(quantity_intent_numerator) +PF_PREFIX_FIELD(quantity_intent_denominator) +PF_PREFIX_FIELD(quantity_reservation_present) +PF_PREFIX_FIELD(quantity_reservation_units) +PF_PREFIX_FIELD(quantity_reservation_basis_units) +PF_PREFIX_FIELD(leg_activation_owner_cycle) +PF_PREFIX_FIELD(leg_activation_present) +PF_PREFIX_FIELD(leg_activation_stop_first_bar) +PF_PREFIX_FIELD(leg_activation_limit_first_bar) +PF_PREFIX_FIELD(pine_exit_activation_owner_cycle_at_birth) +PF_PREFIX_FIELD(pine_exit_activation_present) +PF_PREFIX_FIELD(pine_exit_activation_entry_bar_at_birth) +PF_PREFIX_FIELD(pine_exit_activation_direction_at_birth) +PF_PREFIX_FIELD(pine_exit_activation_cursor_price_at_birth) +PF_PREFIX_FIELD(pine_exit_activation_stop_level_at_birth) +PF_PREFIX_FIELD(pine_exit_activation_limit_level_at_birth) +PF_PREFIX_FIELD(pine_exit_activation_limit_continuation_present) +PF_PREFIX_FIELD(pine_exit_activation_limit_continuation_cause) +PF_PREFIX_FIELD(pine_exit_activation_limit_continuation_fill) diff --git a/tests/fixtures/frozen_market/ff54_pending_order_mirror.hpp b/tests/fixtures/frozen_market/ff54_pending_order_mirror.hpp new file mode 100644 index 0000000..3d23478 --- /dev/null +++ b/tests/fixtures/frozen_market/ff54_pending_order_mirror.hpp @@ -0,0 +1,170 @@ +// GENERATED by scripts/gen_pending_order_mirror.py from include/pineforge/engine.hpp -- do not edit. +// 94 PendingOrder members mirrored (142 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; // deprecated, derived output only + uint64_t replaced_default_market_incarnation; + uint8_t declined_by_replaced_short_market; + 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; // deprecated, derived output only + uint8_t coof_suppress_limit_on_entry_bar; // deprecated, derived output only + 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; + 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; // 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; // deprecated, derived output only + 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; + 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; + int64_t leg_activation_owner_cycle; + uint8_t leg_activation_present; + int64_t leg_activation_stop_first_bar; + int64_t leg_activation_limit_first_bar; + int64_t pine_exit_activation_owner_cycle_at_birth; + uint8_t pine_exit_activation_present; + int32_t pine_exit_activation_entry_bar_at_birth; + int32_t pine_exit_activation_direction_at_birth; + double pine_exit_activation_cursor_price_at_birth; + double pine_exit_activation_stop_level_at_birth; + double pine_exit_activation_limit_level_at_birth; + uint8_t pine_exit_activation_limit_continuation_present; + int32_t pine_exit_activation_limit_continuation_cause; + uint64_t pine_exit_activation_limit_continuation_fill; +} 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/reservation_expansion/ff54-fields.inc b/tests/fixtures/reservation_expansion/ff54-fields.inc new file mode 100644 index 0000000..01f6e49 --- /dev/null +++ b/tests/fixtures/reservation_expansion/ff54-fields.inc @@ -0,0 +1,143 @@ +// Frozen ff54 field order, types and offsets; generated from its Git object. +OLD_FIELD(struct_version, "uint32_t") +OLD_FIELD(size, "uint32_t") +OLD_FIELD(id, "char[64]") +OLD_FIELD(id_truncated, "uint8_t") +OLD_FIELD(id_hash64, "uint64_t") +OLD_FIELD(from_entry, "char[64]") +OLD_FIELD(from_entry_truncated, "uint8_t") +OLD_FIELD(from_entry_hash64, "uint64_t") +OLD_FIELD(type, "int32_t") +OLD_FIELD(is_long, "uint8_t") +OLD_FIELD(limit_price, "double") +OLD_FIELD(stop_price, "double") +OLD_FIELD(trail_points, "double") +OLD_FIELD(trail_price, "double") +OLD_FIELD(trail_offset, "double") +OLD_FIELD(profit_ticks, "double") +OLD_FIELD(loss_ticks, "double") +OLD_FIELD(qty, "double") +OLD_FIELD(qty_type, "int32_t") +OLD_FIELD(qty_percent, "double") +OLD_FIELD(oca_name, "char[64]") +OLD_FIELD(oca_name_truncated, "uint8_t") +OLD_FIELD(oca_name_hash64, "uint64_t") +OLD_FIELD(oca_type, "int32_t") +OLD_FIELD(created_bar, "int32_t") +OLD_FIELD(created_seq, "int64_t") +OLD_FIELD(incarnation, "uint64_t") +OLD_FIELD(created_by_same_id_replacement, "uint8_t") +OLD_FIELD(replaced_default_market_incarnation, "uint64_t") +OLD_FIELD(declined_by_replaced_short_market, "uint8_t") +OLD_FIELD(replaced_exit_order_incarnation, "uint64_t") +OLD_FIELD(recreated_after_named_cancelled_entry_incarnation, "uint64_t") +OLD_FIELD(named_cancel_surviving_exit_incarnation, "uint64_t") +OLD_FIELD(stop_limit_activated, "uint8_t") +OLD_FIELD(coof_suppress_stop_on_entry_bar, "uint8_t") +OLD_FIELD(coof_suppress_limit_on_entry_bar, "uint8_t") +OLD_FIELD(created_during_coof_recalc, "uint8_t") +OLD_FIELD(coof_born_at_close_recalc, "uint8_t") +OLD_FIELD(coof_born_mid_bar, "uint8_t") +OLD_FIELD(coof_cascade_seg_i, "int32_t") +OLD_FIELD(coof_cascade_inflight_fires, "uint8_t") +OLD_FIELD(created_position_side, "int32_t") +OLD_FIELD(created_position_cycle_seq, "int64_t") +OLD_FIELD(created_after_position_close_in_bar, "uint8_t") +OLD_FIELD(over_pyramiding_cap_at_placement, "uint8_t") +OLD_FIELD(same_id_stop_deferred_close_all_bar, "int32_t") +OLD_FIELD(same_id_stop_deferred_close_all_incarnation, "uint64_t") +OLD_FIELD(reverses_same_bar_market_from_flat, "uint8_t") +OLD_FIELD(paired_flat_market_candidate, "uint8_t") +OLD_FIELD(paired_flat_market_own_qty, "double") +OLD_FIELD(paired_flat_market_signal_close, "double") +OLD_FIELD(paired_flat_market_signal_equity, "double") +OLD_FIELD(paired_flat_market_signal_margin_pct, "double") +OLD_FIELD(paired_flat_market_signal_pointvalue, "double") +OLD_FIELD(paired_flat_market_signal_fx, "double") +OLD_FIELD(paired_flat_market_peer_seq, "int64_t") +OLD_FIELD(paired_flat_market_transaction_qty, "double") +OLD_FIELD(default_flat_market_gross_candidate, "uint8_t") +OLD_FIELD(tv_carry_qty, "double") +OLD_FIELD(frozen_default_qty, "double") +OLD_FIELD(default_stop_placement_qty, "double") +OLD_FIELD(default_stop_placement_equity, "double") +OLD_FIELD(default_stop_placement_signal_close, "double") +OLD_FIELD(default_stop_sizing_price, "double") +OLD_FIELD(sizing_equity, "double") +OLD_FIELD(sizing_price, "double") +OLD_FIELD(sizing_fx, "double") +OLD_FIELD(sizing_mark, "double") +OLD_FIELD(opening_affordability_exemption_candidate, "uint8_t") +OLD_FIELD(explicit_flat_admission_candidate, "uint8_t") +OLD_FIELD(explicit_placement_equity, "double") +OLD_FIELD(explicit_slipped_signal_close, "double") +OLD_FIELD(affordability_placement_equity, "double") +OLD_FIELD(affordability_signal_price, "double") +OLD_FIELD(affordability_held_qty, "double") +OLD_FIELD(affordability_close_only, "uint8_t") +OLD_FIELD(rounded_signal_cost_close_only, "uint8_t") +OLD_FIELD(signal_close_mc_bar, "int32_t") +OLD_FIELD(signal_close_mc_entry_incarnation, "uint64_t") +OLD_FIELD(signal_close_mc_fill_seq, "uint64_t") +OLD_FIELD(signal_close_mc_remaining_qty, "double") +OLD_FIELD(comment, "char[64]") +OLD_FIELD(comment_truncated, "uint8_t") +OLD_FIELD(comment_hash64, "uint64_t") +OLD_FIELD(requested_partial, "uint8_t") +OLD_FIELD(full_percent_exit_request, "uint8_t") +OLD_FIELD(pooc_global_full_exit_dynamic_qty, "uint8_t") +OLD_FIELD(pooc_global_full_exit_tracks_bound_adds, "uint8_t") +OLD_FIELD(pooc_global_full_exit_bound_add, "uint8_t") +OLD_FIELD(created_while_in_position, "uint8_t") +OLD_FIELD(sbmt_member, "uint8_t") +OLD_FIELD(sbmt_own_qty, "double") +OLD_FIELD(sbmt_tx_qty, "double") +OLD_FIELD(sbmt_kept_over_cap, "uint8_t") +OLD_FIELD(sbmt_close_qty, "double") +OLD_FIELD(sbmt_close_buy, "uint8_t") +OLD_FIELD(suppress_as_declined_reversal_close, "uint8_t") +OLD_FIELD(dormant_bracket, "uint8_t") +OLD_FIELD(dormant_reissue_pending, "uint8_t") +OLD_FIELD(dormant_original_stop_price, "double") +OLD_FIELD(dormant_hold_bar, "int32_t") +OLD_FIELD(dormant_reversal_kill_bar, "int32_t") +OLD_FIELD(dormant_trail_best, "double") +OLD_FIELD(dormant_trail_best_start, "double") +OLD_FIELD(dormant_trail_leg_dead, "uint8_t") +OLD_FIELD(suppressed_close_consumed_ledger_qty, "double") +OLD_FIELD(suppressed_close_retired_ledger_qty, "double") +OLD_FIELD(short_seed_collision_role, "int32_t") +OLD_FIELD(replaced_order_incarnation, "uint64_t") +OLD_FIELD(birth_timestamp, "int64_t") +OLD_FIELD(birth_cause, "int32_t") +OLD_FIELD(birth_bar, "int32_t") +OLD_FIELD(birth_cursor_domain, "int32_t") +OLD_FIELD(birth_cursor_position, "int32_t") +OLD_FIELD(birth_cursor_index, "int32_t") +OLD_FIELD(birth_cursor_count, "int32_t") +OLD_FIELD(birth_cursor_price, "double") +OLD_FIELD(birth_first_fill, "uint64_t") +OLD_FIELD(birth_last_fill, "uint64_t") +OLD_FIELD(birth_evaluation_ordinal, "uint64_t") +OLD_FIELD(pine_birth_reach, "int32_t") +OLD_FIELD(quantity_intent_kind, "uint64_t") +OLD_FIELD(quantity_intent_units, "double") +OLD_FIELD(quantity_intent_numerator, "double") +OLD_FIELD(quantity_intent_denominator, "double") +OLD_FIELD(quantity_reservation_present, "uint8_t") +OLD_FIELD(quantity_reservation_units, "double") +OLD_FIELD(quantity_reservation_basis_units, "double") +OLD_FIELD(leg_activation_owner_cycle, "int64_t") +OLD_FIELD(leg_activation_present, "uint8_t") +OLD_FIELD(leg_activation_stop_first_bar, "int64_t") +OLD_FIELD(leg_activation_limit_first_bar, "int64_t") +OLD_FIELD(pine_exit_activation_owner_cycle_at_birth, "int64_t") +OLD_FIELD(pine_exit_activation_present, "uint8_t") +OLD_FIELD(pine_exit_activation_entry_bar_at_birth, "int32_t") +OLD_FIELD(pine_exit_activation_direction_at_birth, "int32_t") +OLD_FIELD(pine_exit_activation_cursor_price_at_birth, "double") +OLD_FIELD(pine_exit_activation_stop_level_at_birth, "double") +OLD_FIELD(pine_exit_activation_limit_level_at_birth, "double") +OLD_FIELD(pine_exit_activation_limit_continuation_present, "uint8_t") +OLD_FIELD(pine_exit_activation_limit_continuation_cause, "int32_t") +OLD_FIELD(pine_exit_activation_limit_continuation_fill, "uint64_t") diff --git a/tests/fixtures/reservation_expansion/ff54_pending_order_mirror.hpp b/tests/fixtures/reservation_expansion/ff54_pending_order_mirror.hpp new file mode 100644 index 0000000..3d23478 --- /dev/null +++ b/tests/fixtures/reservation_expansion/ff54_pending_order_mirror.hpp @@ -0,0 +1,170 @@ +// GENERATED by scripts/gen_pending_order_mirror.py from include/pineforge/engine.hpp -- do not edit. +// 94 PendingOrder members mirrored (142 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; // deprecated, derived output only + uint64_t replaced_default_market_incarnation; + uint8_t declined_by_replaced_short_market; + 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; // deprecated, derived output only + uint8_t coof_suppress_limit_on_entry_bar; // deprecated, derived output only + 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; + 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; // 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; // deprecated, derived output only + 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; + 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; + int64_t leg_activation_owner_cycle; + uint8_t leg_activation_present; + int64_t leg_activation_stop_first_bar; + int64_t leg_activation_limit_first_bar; + int64_t pine_exit_activation_owner_cycle_at_birth; + uint8_t pine_exit_activation_present; + int32_t pine_exit_activation_entry_bar_at_birth; + int32_t pine_exit_activation_direction_at_birth; + double pine_exit_activation_cursor_price_at_birth; + double pine_exit_activation_stop_level_at_birth; + double pine_exit_activation_limit_level_at_birth; + uint8_t pine_exit_activation_limit_continuation_present; + int32_t pine_exit_activation_limit_continuation_cause; + uint64_t pine_exit_activation_limit_continuation_fill; +} 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/baseff54/README.md b/tests/fixtures/script_cpp_abi/baseff54/README.md new file mode 100644 index 0000000..f098a37 --- /dev/null +++ b/tests/fixtures/script_cpp_abi/baseff54/README.md @@ -0,0 +1 @@ +Exact shipped ff54/v6 header closure. Compile/link controls only; no executable runs. diff --git a/tests/fixtures/script_cpp_abi/baseff54/headers.json.gz b/tests/fixtures/script_cpp_abi/baseff54/headers.json.gz new file mode 100644 index 0000000..0da72b0 Binary files /dev/null and b/tests/fixtures/script_cpp_abi/baseff54/headers.json.gz differ diff --git a/tests/fixtures/script_cpp_abi/baseff54/manifest.json b/tests/fixtures/script_cpp_abi/baseff54/manifest.json new file mode 100644 index 0000000..a641d77 --- /dev/null +++ b/tests/fixtures/script_cpp_abi/baseff54/manifest.json @@ -0,0 +1,90 @@ +{ + "source_commit": "ff54a557ac751244dafd60df0bb22886ec35792d", + "source_tree": "60431e5da18d4bce0777f0e3b4df03d41163c350", + "internal_namespace": "engine_script_run_v6", + "archive": "headers.json.gz", + "archive_sha256": "206bdd71c0a6a7955b97b9b63ae437b3b5119d7ebcf6d9f2b506084caba48674", + "generated_dependency": "pineforge/version.h comes from the matching current configure; no frozen layout uses its value.", + "files": { + "pineforge/bar.hpp": { + "sha256": "665c2528dca64ac9474de622dfd7ffdb540ff91316f70317e39aada158d1e0de", + "git_blob": "836f73daab382e9c8d5a6d03a942a921e39b756f" + }, + "pineforge/broker_events.hpp": { + "sha256": "4f3d9948a4d1604750901fa3982d6ea49bd50dd15a86c4e5d690b83d1f5f23c0", + "git_blob": "2972d53c7b039af266f4da161301c01eec54c16d" + }, + "pineforge/compat/pine/exit_activation.hpp": { + "sha256": "c6a538e6dfc5c0e70f434e92e0d8329b40ffc7e87051ae2d1d9754d9db8c9a6d", + "git_blob": "5024b013c36df956cef1d77db067a68284067322" + }, + "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_birth.hpp": { + "sha256": "db379949112f0cd363fba4aecc5f7ad952f7c3eea0eec0425bcc3367dd4505b9", + "git_blob": "b57ffe0eba7bd10b4e0588e48ef8fa8e82b629ae" + }, + "pineforge/compat/pine/order_priority.hpp": { + "sha256": "0e77af11357cf9b2e3255ef2ef871ac0cd319b6548568c549f64c2598115f4d9", + "git_blob": "86aac9629e40ad19b7308468e95389598a9436e2" + }, + "pineforge/engine.hpp": { + "sha256": "d5d74b2b0542ce7aa2bf3e0a95bac8d05318ce15494148f4f07f92c7e332b237", + "git_blob": "64c73225dd3d67eaae9f0937651c2683ac94937e" + }, + "pineforge/leg_activation.hpp": { + "sha256": "859d75d0ae90ec3cc109765da066b37c3bbbff9c7c840368fbdd05dc7e178f3f", + "git_blob": "4160c841782800853bb574219b3dad2ca8aa0cc6" + }, + "pineforge/magnifier.hpp": { + "sha256": "c0ed220b6df054b44596a434d4f48b1c3e960ca329bb8644a405a608175e06ff", + "git_blob": "1bbc910a869d162900d7610fa0bd9a88776b7b0f" + }, + "pineforge/na.hpp": { + "sha256": "0e8c256fcdeda94a0855458fa3b8c4ad4f9864dc2aecce969934f6bea82fd0a5", + "git_blob": "e98505aadc68a09cbe5a14aacf591a799a15ecd2" + }, + "pineforge/order_birth.hpp": { + "sha256": "0eadb3e2a92248fbc8e6ba8f94285b15b0e83d7a0396f5904b40c216619604e4", + "git_blob": "ed4a4038e9d079c92b4ebd777a5f22f1f1256b0c" + }, + "pineforge/order_priority.hpp": { + "sha256": "25bb7898972153b044675245db230c1171644856ef904f4dd02353b8968d433b", + "git_blob": "3e9fa5ee4121971a7962db8a60859aba6fb9ec95" + }, + "pineforge/pending_order_mirror.hpp": { + "sha256": "a9b2eae6cbcab2b26a9ab6ba2463086f03857f5c52aae5c83b9ea5ccd5dd5444", + "git_blob": "3d23478be41bb88c08e5c54fa40e71547455c2a9" + }, + "pineforge/pineforge.h": { + "sha256": "1e0def60e31f529dcb821502c0f7104b473c06be0fa1df713be0f408e04e9288", + "git_blob": "232cd6c66d3f6e6ffdcffdf246e6a9dcce3ef3f8" + }, + "pineforge/position_close_obligation.hpp": { + "sha256": "6f6ad0aaca952dda7a9386cb78e57705b3494364dbd8970b0a051fe99d05b777", + "git_blob": "09576ee0f2a1d49becc46f5701f4369cf8f87bf6" + }, + "pineforge/quantity_intent.hpp": { + "sha256": "c4cc0f1cd5d6ed1f248825712a5ba4b4f5a808b787c05c58971934b89d770b90", + "git_blob": "41262fee5aa2db4032fc008542c573032563629b" + }, + "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/fixtures/script_cpp_abi/growthbf312/README.md b/tests/fixtures/script_cpp_abi/growthbf312/README.md new file mode 100644 index 0000000..13c3906 --- /dev/null +++ b/tests/fixtures/script_cpp_abi/growthbf312/README.md @@ -0,0 +1 @@ +Exact unshipped growth tree bf312 header closure: engine v6 with unversioned standalone reservation types. This is a draft ABI negative control, not a released ff54 definition. Compile/link controls only; no executable runs. diff --git a/tests/fixtures/script_cpp_abi/growthbf312/headers.json.gz b/tests/fixtures/script_cpp_abi/growthbf312/headers.json.gz new file mode 100644 index 0000000..62fac8d Binary files /dev/null and b/tests/fixtures/script_cpp_abi/growthbf312/headers.json.gz differ diff --git a/tests/fixtures/script_cpp_abi/growthbf312/manifest.json b/tests/fixtures/script_cpp_abi/growthbf312/manifest.json new file mode 100644 index 0000000..f9c7c32 --- /dev/null +++ b/tests/fixtures/script_cpp_abi/growthbf312/manifest.json @@ -0,0 +1,94 @@ +{ + "source_commit": "ff54a557ac751244dafd60df0bb22886ec35792d", + "source_tree": "bf312b9d5a705d3d0ca16d4fb897e16c6e73b0d1", + "internal_namespace": "engine_script_run_v6", + "archive": "headers.json.gz", + "archive_sha256": "7a585b1e66020b516d73e75bd763473ecb6248f51073ddd1cb2f16cf83c4d331", + "generated_dependency": "pineforge/version.h comes from the matching current configure; no frozen layout uses its value.", + "files": { + "pineforge/bar.hpp": { + "sha256": "665c2528dca64ac9474de622dfd7ffdb540ff91316f70317e39aada158d1e0de", + "git_blob": "836f73daab382e9c8d5a6d03a942a921e39b756f" + }, + "pineforge/broker_events.hpp": { + "sha256": "4f3d9948a4d1604750901fa3982d6ea49bd50dd15a86c4e5d690b83d1f5f23c0", + "git_blob": "2972d53c7b039af266f4da161301c01eec54c16d" + }, + "pineforge/compat/pine/exit_activation.hpp": { + "sha256": "c6a538e6dfc5c0e70f434e92e0d8329b40ffc7e87051ae2d1d9754d9db8c9a6d", + "git_blob": "5024b013c36df956cef1d77db067a68284067322" + }, + "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_birth.hpp": { + "sha256": "db379949112f0cd363fba4aecc5f7ad952f7c3eea0eec0425bcc3367dd4505b9", + "git_blob": "b57ffe0eba7bd10b4e0588e48ef8fa8e82b629ae" + }, + "pineforge/compat/pine/order_priority.hpp": { + "sha256": "0e77af11357cf9b2e3255ef2ef871ac0cd319b6548568c549f64c2598115f4d9", + "git_blob": "86aac9629e40ad19b7308468e95389598a9436e2" + }, + "pineforge/engine.hpp": { + "sha256": "381a18d59f20ff94c6eed9dec40497fdaa175637fc96169a0a9875a38b071736", + "git_blob": "40cece96402fae406e0f6c8d92d051fa232d2c18" + }, + "pineforge/leg_activation.hpp": { + "sha256": "859d75d0ae90ec3cc109765da066b37c3bbbff9c7c840368fbdd05dc7e178f3f", + "git_blob": "4160c841782800853bb574219b3dad2ca8aa0cc6" + }, + "pineforge/magnifier.hpp": { + "sha256": "c0ed220b6df054b44596a434d4f48b1c3e960ca329bb8644a405a608175e06ff", + "git_blob": "1bbc910a869d162900d7610fa0bd9a88776b7b0f" + }, + "pineforge/na.hpp": { + "sha256": "0e8c256fcdeda94a0855458fa3b8c4ad4f9864dc2aecce969934f6bea82fd0a5", + "git_blob": "e98505aadc68a09cbe5a14aacf591a799a15ecd2" + }, + "pineforge/order_birth.hpp": { + "sha256": "0eadb3e2a92248fbc8e6ba8f94285b15b0e83d7a0396f5904b40c216619604e4", + "git_blob": "ed4a4038e9d079c92b4ebd777a5f22f1f1256b0c" + }, + "pineforge/order_priority.hpp": { + "sha256": "25bb7898972153b044675245db230c1171644856ef904f4dd02353b8968d433b", + "git_blob": "3e9fa5ee4121971a7962db8a60859aba6fb9ec95" + }, + "pineforge/pending_order_mirror.hpp": { + "sha256": "5c1daae5baefd4a442f463aed61844fac98a07837525bd1a538731c732236051", + "git_blob": "20358bf3d878b8c431deaa12c833d0f9ec77482b" + }, + "pineforge/pineforge.h": { + "sha256": "1e0def60e31f529dcb821502c0f7104b473c06be0fa1df713be0f408e04e9288", + "git_blob": "232cd6c66d3f6e6ffdcffdf246e6a9dcce3ef3f8" + }, + "pineforge/position_close_obligation.hpp": { + "sha256": "6f6ad0aaca952dda7a9386cb78e57705b3494364dbd8970b0a051fe99d05b777", + "git_blob": "09576ee0f2a1d49becc46f5701f4369cf8f87bf6" + }, + "pineforge/quantity_intent.hpp": { + "sha256": "c4cc0f1cd5d6ed1f248825712a5ba4b4f5a808b787c05c58971934b89d770b90", + "git_blob": "41262fee5aa2db4032fc008542c573032563629b" + }, + "pineforge/reservation_expansion.hpp": { + "sha256": "24f5dc4ea210032d3005578515813a2f69b6bc48beb1415156792684904b17c7", + "git_blob": "039d0a06b173949f0e9703189bae805ad065fedb" + }, + "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/reservation_expansion_test_access.hpp b/tests/reservation_expansion_test_access.hpp new file mode 100644 index 0000000..53f96f7 --- /dev/null +++ b/tests/reservation_expansion_test_access.hpp @@ -0,0 +1,100 @@ +#pragma once +#include +#include +#include +#include +#include + +namespace pineforge { +void fill_pending_order_mirror(const PendingOrder&, pf_pending_order_v1_t*); +const pf_field_desc_t* pending_order_layout(int*); +} + +namespace reservation_test { +// Literal white-box tests at the existing native checkpoints, with no product +// test export, changed access specifier, or altered class definition. Explicit +// template instantiation can name a private member (C++ [temp.explicit]). +template struct Access { + friend auto access(Tag) { return Member; } +}; +#define CHECKPOINT(Tag, method) \ + struct Tag { friend auto access(Tag); }; \ + template struct Access +CHECKPOINT(Dispatch, apply_filled_order_to_state); +CHECKPOINT(Compact, compact_filled_pending_orders); +CHECKPOINT(Reduce, reduce_oca_group); +CHECKPOINT(CancelGroup, cancel_oca_group); +CHECKPOINT(Flat, reset_position_state_to_flat); +CHECKPOINT(Open, open_fresh_position); +CHECKPOINT(Revive, revive_position_brackets_after_margin_call_partial); +#undef CHECKPOINT + +using namespace pineforge; +constexpr double missing = std::numeric_limits::quiet_NaN(); +class Book : public BacktestEngine { +public: + Book() { + initial_capital_=100000; commission_value_=0; margin_long_=margin_short_=0; + pyramiding_=20; process_orders_on_close_=true; qty_step_=std::ldexp(1.0,-32); + current_bar_={100,100,100,100,1,0}; next_position_cycle_seq_=7; + next_order_incarnation_=40; + } + void on_bar(const Bar&) override {} + std::vector retired; + double trail=missing; int closed_bar=-1; uint64_t closed_id=0; bool was_long=false; + PendingOrder& get(const std::string& id) { + for(auto& o:pending_orders_) if(o.id==id) return o; + throw std::logic_error("missing " + id); + } + bool has(const std::string& id)const { + for(auto& o:pending_orders_) if(o.id==id) return true; + return false; + } + const std::vector& book()const{return pending_orders_;} + void add(const char* id,double qty=2,bool is_long=true,const char* group="",int oca=0) { + strategy_entry(id,is_long,missing,missing,qty,"",group,oca); + } + void priced(const char* id) {strategy_entry(id,true,120,missing,2);} + void raw(const char* id,double qty=2,bool is_long=true,const char* group="",int oca=0) { + strategy_order(id,is_long,qty,missing,missing,group,oca); + } + void exit(const char* id="E",double units=missing,const char* group="",double percent=100, + const char* binding="",double stop=90) { + strategy_exit(id,binding,120,stop,missing,missing,missing,percent,"",units,group); + } + void inert(const char* id="E") {strategy_exit(id,"",missing,missing);} + void cancel(const char* id){strategy_cancel(id);} + void cancel_all(){strategy_cancel_all();} + void advance(){++bar_index_; current_bar_.timestamp=bar_index_*60000LL;} + void step(){process_pending_orders(current_bar_);} + void fire(const char* id,bool compact=true) { + size_t i=0; while(i*access(Dispatch{}))(i,100,false,current_bar_,trail,closed_bar,closed_id,was_long,retired,false); + if(compact) finish(); + } + void finish() { + (this->*access(Compact{}))(retired,closed_bar,closed_id,was_long); + retired.clear();closed_bar=-1;closed_id=0;was_long=false; + } + void reduce(const char* group,double qty){(this->*access(Reduce{}))(group,"",qty);} + void cancel_group(const char* group){(this->*access(CancelGroup{}))(group,"");} + void flatten(){(this->*access(Flat{}))();} + void open(bool is_long=true,double qty=10){(this->*access(Open{}))(is_long?PositionSide::LONG:PositionSide::SHORT,100,qty,"fresh",next_order_incarnation_++);} + void revive(double price){(this->*access(Revive{}))(price);} + void seed(double qty=10){add("seed",qty);fire("seed");advance();} + void standard(double requested=2,const char* source_group="",const char* exit_group="") { + seed();add("A",requested,true,source_group);next_order_incarnation_=50;exit("E",missing,exit_group); + } + void cap(int value){pyramiding_=value;} + void pooc(bool value){process_orders_on_close_=value;} + void halt(bool value){risk_halted_=value;} + void close_partial(double qty){strategy_close("seed","",qty,missing,true);} + void reset(){run(nullptr,0);} + void sort_book(){std::reverse(pending_orders_.begin(),pending_orders_.end());} + double quantity()const{return position_qty_;} + int64_t cycle()const{return position_cycle_seq_;} + uint64_t owner(const char* id="A"){return get(id).reservation_growth_source.reservation_owner().value_or(0);} + uint64_t closure(const char* id="E"){const auto& c=get(id).reservation_expansion.capture();return c?c->first_later_admission.value_or(0):0;} + bool live_all(const char* id="E"){return get(id).reservation_expansion.live_all(cycle(),position_side_);} +}; +} // namespace reservation_test diff --git a/tests/test_frozen_market_instruction.cpp b/tests/test_frozen_market_instruction.cpp new file mode 100644 index 0000000..a1a479e --- /dev/null +++ b/tests/test_frozen_market_instruction.cpp @@ -0,0 +1,328 @@ +// Literal native instruction and book transitions. No external data, reference +// trades, strategy compiler or campaign grading participates in this test. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace pineforge { +void fill_pending_order_mirror(const PendingOrder&, pf_pending_order_v1_t*); +const pf_field_desc_t* pending_order_layout(int*); +} + +namespace frozen_prefix { +#include "fixtures/frozen_market/ff54_pending_order_mirror.hpp" +} + +using namespace pineforge; +using compat::pine::FrozenMarketInstruction; +using compat::pine::FrozenMarketInstructionKind; +#define PF_PREFIX_FIELD(name) \ + static_assert(offsetof(pf_pending_order_v1_t, name) == \ + offsetof(frozen_prefix::pf_pending_order_v1_t, name), "frozen prefix offset"); \ + static_assert(std::is_same::value, "frozen prefix type"); \ + static_assert(sizeof(pf_pending_order_v1_t::name) == \ + sizeof(frozen_prefix::pf_pending_order_v1_t::name), "frozen prefix size"); +#include "fixtures/frozen_market/ff54_fields.inc" +#undef PF_PREFIX_FIELD +static_assert(offsetof(pf_pending_order_v1_t, pine_frozen_market_instruction_kind) >= + sizeof(frozen_prefix::pf_pending_order_v1_t), "preserve old trailing padding"); + +namespace { +const double nan = std::numeric_limits::quiet_NaN(); +const double inf = std::numeric_limits::infinity(); +int failures = 0; +#define CHECK(cond) do { if (!(cond)) { \ + std::fprintf(stderr, "FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); ++failures; \ +} } while (0) + +template void invalid(Function make) { + try { make(); CHECK(false); } + catch (const std::invalid_argument&) {} +} + +pf_pending_order_v1_t mirror(const PendingOrder& order) { + pf_pending_order_v1_t result; + fill_pending_order_mirror(order, &result); + return result; +} + +void exclusive_instruction_and_revocation() { + FrozenMarketInstruction ordinary; + CHECK(ordinary.kind() == FrozenMarketInstructionKind::Ordinary); + CHECK(!ordinary.active() && !ordinary.transaction() && !ordinary.targeted_close()); + for (double own : {0.0, -1.0, nan, inf}) + invalid([&] { return FrozenMarketInstruction::transaction(own, 2); }); + for (double total : {0.0, 1.0, nan, -inf}) + invalid([&] { return FrozenMarketInstruction::transaction(2, total); }); + auto transaction = FrozenMarketInstruction::transaction(2, 5); + CHECK(transaction.active() && !transaction.targeted_close()); + CHECK(transaction.transaction()->own_units == 2); + CHECK(transaction.transaction()->transaction_units == 5); + transaction.revoke(); + CHECK(!transaction.active() && !transaction.transaction()); + transaction.revoke(); + CHECK(transaction.kind() == FrozenMarketInstructionKind::Ordinary); + + QuantityRequest request; + invalid([&] { return FrozenMarketInstruction::targeted_close("E", request); }); + request.request(QuantityIntent::all()); + invalid([&] { return FrozenMarketInstruction::targeted_close("E", request); }); + request.request(QuantityIntent::fraction(1, 2)); + invalid([&] { return FrozenMarketInstruction::targeted_close("E", request); }); + for (double units : {0.0, -1.0, nan, inf}) { + request.request(QuantityIntent::units(units)); + invalid([&] { return FrozenMarketInstruction::targeted_close("E", request); }); + } + request.request(QuantityIntent::units(3)); + invalid([&] { return FrozenMarketInstruction::targeted_close("", request); }); + auto close = FrozenMarketInstruction::targeted_close("E", request); + CHECK(close.kind() == FrozenMarketInstructionKind::TargetedClose); + CHECK(close.active() && !close.transaction()); + CHECK(close.targeted_close()->target_id == "E"); + request.reserve(1, 3); + CHECK(request.intent()->units() == 3); // Reservation does not change source units. + close.revoke(); + CHECK(!close.active() && !close.targeted_close()); +} + +class Book : public BacktestEngine { +public: + Book() { + initial_capital_ = 1000000; + default_qty_type_ = QtyType::FIXED; + default_qty_value_ = 1; + commission_value_ = 0; + margin_long_ = margin_short_ = 0; + pyramiding_ = 1; + bar_index_ = 0; + current_bar_ = {100, 100, 100, 100, 1, 0}; + } + void on_bar(const Bar&) override {} + void entry(const std::string& id, bool buy, double units) { + strategy_entry(id, buy, nan, nan, units); + } + void step() { + ++bar_index_; + current_bar_ = {100, 100, 100, 100, 1, int64_t(bar_index_) * 60000}; + process_pending_orders(current_bar_); + } + void seed(bool buy, double units) { entry("seed", buy, units); step(); } + void close_seed() { strategy_close("seed"); } + void cancel(const std::string& id) { strategy_cancel(id); } + void finalize() { compat::pine::finalize_frozen_market_book(pending_orders_, true); } + + void priced() { strategy_entry("priced", true, 90, nan, 1); } + void raw() { strategy_order("raw", true, 1); } + void bracket() { strategy_exit("bracket", "A", 120, 90); } + void close_all() { strategy_close_all(); } + void leave_scope() { compat::pine::finalize_frozen_market_book(pending_orders_, false); } + double position() const { return position_qty_; } + PositionSide side() const { return position_side_; } + 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; + std::string present; + for (const auto& o : pending_orders_) present += " " + o.id; + throw std::logic_error("missing literal order " + id + "; present:" + present); + } + void put(PendingOrder order) { pending_orders_.push_back(std::move(order)); } +}; + +void capture_amounts_and_immutable_placement() { + for (bool seed_buy : {false, true}) { + Book book; book.seed(seed_buy, 3); + CHECK(book.position() == 3); + book.entry("opposite", !seed_buy, 2); + const auto& first = book.order("opposite"); + CHECK(first.pine_frozen_market_instruction.transaction()->own_units == 2); + CHECK(first.pine_frozen_market_instruction.transaction()->transaction_units == 5); + CHECK(!mirror(first).sbmt_kept_over_cap); + book.entry("same", seed_buy, 4); + const auto& retained = book.order("same"); + CHECK(retained.pine_frozen_market_instruction.transaction()->own_units == 4); + CHECK(retained.pine_frozen_market_instruction.transaction()->transaction_units == 6); + CHECK(retained.over_pyramiding_cap_at_placement); + CHECK(mirror(retained).sbmt_kept_over_cap == 1); + const auto old_incarnation = book.order("opposite").incarnation; + book.entry("opposite", !seed_buy, 5); + const auto& replacement = book.order("opposite"); + CHECK(replacement.incarnation != old_incarnation); + CHECK(replacement.replaced_order_incarnation == old_incarnation); + // Own 5 + held opposite 3 + pending opposite's own 4, never its total 6. + CHECK(replacement.pine_frozen_market_instruction.transaction()->own_units == 5); + CHECK(replacement.pine_frozen_market_instruction.transaction()->transaction_units == 12); + CHECK(replacement.created_position_side == (seed_buy ? PositionSide::LONG : PositionSide::SHORT)); + book.finalize(); + CHECK(book.order("same").pine_frozen_market_instruction.active()); + CHECK(mirror(book.order("same")).sbmt_kept_over_cap == 1); + } +} + +void targeted_close_has_one_quantity_authority() { + for (bool buy : {false, true}) { + Book book; book.seed(buy, 3); book.close_seed(); + auto& close = book.order("__close__seed"); + CHECK(close.pine_frozen_market_instruction.targeted_close()->target_id == "seed"); + CHECK(!close.pine_frozen_market_instruction.transaction()); + CHECK(close.quantity_request.intent()->units() == 3); + const auto before = mirror(close); + CHECK(before.sbmt_member == 1 && before.sbmt_close_qty == 3); + CHECK(before.sbmt_close_buy == (buy ? 0 : 1)); + CHECK(!before.sbmt_kept_over_cap && std::isnan(before.sbmt_tx_qty)); + close.quantity_request.reserve(1, 3); + close.qty = 1; // Execution reservation differs; original close still resolves to 3. + CHECK(mirror(close).sbmt_close_qty == 3); + CHECK(close.quantity_request.reservation()->units == 1); + book.step(); + CHECK(book.position() == 0); + CHECK(book.orders().empty()); + book.entry("fresh", !buy, 2); + CHECK(book.order("fresh").pine_frozen_market_instruction.transaction()->own_units == 2); + CHECK(book.order("fresh").pine_frozen_market_instruction.transaction()->transaction_units == 2); + } +} + +void whole_book_revocation() { + using Add = void (Book::*)(); + for (Add add : {&Book::priced, &Book::raw, &Book::bracket, &Book::leave_scope}) { + Book book; book.entry("A", true, 2); book.entry("B", false, 3); + (book.*add)(); book.finalize(); + CHECK(book.orders().size() >= 2); + for (const auto& order : book.orders()) { + CHECK(!order.pine_frozen_market_instruction.active()); + const auto legacy = mirror(order); + CHECK(!legacy.sbmt_member && !legacy.sbmt_kept_over_cap && !legacy.sbmt_close_buy); + CHECK(std::isnan(legacy.sbmt_own_qty) && std::isnan(legacy.sbmt_tx_qty)); + CHECK(std::isnan(legacy.sbmt_close_qty)); + } + } + Book third; third.entry("A", true, 1); third.entry("B", false, 2); + third.entry("C", true, 3); third.finalize(); + CHECK(third.orders().size() == 3); + for (const auto& order : third.orders()) CHECK(!order.pine_frozen_market_instruction.active()); + Book all; all.seed(true, 3); all.entry("A", false, 1); all.close_all(); all.finalize(); + for (const auto& order : all.orders()) CHECK(!order.pine_frozen_market_instruction.active()); + // The generic cap snapshot alone never creates the removed retention permit. + PendingOrder ordinary{}; ordinary.over_pyramiding_cap_at_placement = true; + CHECK(!mirror(ordinary).sbmt_kept_over_cap); +} + +void cancellation_recreation_and_noop() { + Book book; book.entry("A", true, 2); book.entry("B", false, 3); + const auto cancelled = book.order("B").incarnation; + book.cancel("B"); book.entry("B", false, 4); + const auto& fresh = book.order("B"); + CHECK(fresh.incarnation != cancelled); + // That separate receipt only records a priced-entry cancel with a + // surviving exit. A MARKET cancellation must not borrow such a receipt. + CHECK(fresh.recreated_after_named_cancelled_entry_incarnation == 0); + CHECK(fresh.pine_frozen_market_instruction.transaction()->own_units == 4); + CHECK(fresh.pine_frozen_market_instruction.transaction()->transaction_units == 6); + book.cancel("A"); book.cancel("B"); + CHECK(book.orders().empty()); + book.close_seed(); // No position/owner: no close payload or permit is created. + CHECK(book.orders().empty()); + book.entry("B", false, 1); + CHECK(book.order("B").pine_frozen_market_instruction.transaction()->transaction_units == 1); + book.cancel("B"); book.priced(); + CHECK(!book.order("priced").pine_frozen_market_instruction.active()); +} + +void hash_and_mirror_live_facts() { + Book base; PendingOrder order{}; order.id = "M"; + base.put(order); + const auto empty_hash = base.broker_state_hash(); + Book transaction = base; + transaction.order("M").pine_frozen_market_instruction = FrozenMarketInstruction::transaction(2, 5); + const auto tx_hash = transaction.broker_state_hash(); + CHECK(tx_hash != empty_hash); + auto tx = mirror(transaction.order("M")); + CHECK(tx.pine_frozen_market_instruction_kind == 1); + CHECK(tx.pine_frozen_market_instruction_own_units == 2 && tx.sbmt_own_qty == 2); + CHECK(tx.pine_frozen_market_instruction_transaction_units == 5 && tx.sbmt_tx_qty == 5); + CHECK(std::strcmp(tx.pine_frozen_market_instruction_target_id, "") == 0); + for (auto amounts : {std::pair{3, 5}, {2, 6}}) { + Book changed = transaction; + changed.order("M").pine_frozen_market_instruction = FrozenMarketInstruction::transaction(amounts.first, amounts.second); + CHECK(changed.broker_state_hash() != tx_hash); + } + transaction.order("M").pine_frozen_market_instruction.revoke(); + CHECK(transaction.broker_state_hash() == empty_hash); + + Book close = base; + auto& c = close.order("M"); + c.type = OrderType::EXIT; + c.created_position_side = PositionSide::SHORT; + c.quantity_request.request(QuantityIntent::units(3)); + const std::string target(90, 'x'); + c.pine_frozen_market_instruction = FrozenMarketInstruction::targeted_close(target, c.quantity_request); + const auto close_hash = close.broker_state_hash(); + auto cm = mirror(c); + CHECK(cm.pine_frozen_market_instruction_kind == 2); + CHECK(cm.pine_frozen_market_instruction_target_id_truncated == 1); + CHECK(std::strlen(cm.pine_frozen_market_instruction_target_id) == 63); + uint64_t expected = 1469598103934665603ULL; + for (unsigned char ch : target) { expected ^= ch; expected *= 1099511628211ULL; } + CHECK(cm.pine_frozen_market_instruction_target_id_hash64 == expected); + Book changed = close; + changed.order("M").pine_frozen_market_instruction = FrozenMarketInstruction::targeted_close(target + "y", c.quantity_request); + CHECK(changed.broker_state_hash() != close_hash); + CHECK(mirror(changed.order("M")).pine_frozen_market_instruction_target_id_hash64 != expected); + changed = close; changed.order("M").quantity_request.request(QuantityIntent::units(4)); + CHECK(changed.broker_state_hash() != close_hash); + changed = close; changed.order("M").created_position_side = PositionSide::LONG; + CHECK(changed.broker_state_hash() != close_hash && !mirror(changed.order("M")).sbmt_close_buy); + c.pine_frozen_market_instruction.revoke(); + cm = mirror(c); + CHECK(!cm.sbmt_member && !cm.sbmt_close_buy && std::isnan(cm.sbmt_close_qty)); + CHECK(cm.pine_frozen_market_instruction_kind == 0 && !cm.pine_frozen_market_instruction_target_id[0]); + + int count = 0; + const auto* layout = pending_order_layout(&count); + CHECK(count == 155); + CHECK(std::strcmp(layout[149].name, "pine_frozen_market_instruction_kind") == 0); +} + +void overflowed_total_retains_existing_finite_execution_guard() { + Book book; book.entry("M", true, 2); + auto& order = book.order("M"); + order.pine_frozen_market_instruction = FrozenMarketInstruction::transaction(2, inf); + CHECK(order.pine_frozen_market_instruction.active()); + CHECK(std::isinf(mirror(order).sbmt_tx_qty)); + double qty = 0; int close_only = -1, partition = -1; + CHECK(book.probe_fill_qty(0, 100, &qty, &close_only, &partition) == 0); + CHECK(qty == 2 && partition == 0 && close_only == 0); + // Positive source-total overflow cannot enable the expanded-transaction arm. + book.step(); + CHECK(book.position() == 2 && book.side() == PositionSide::LONG); +} +} // namespace + +int main() { + const std::pair cases[] = { + {"exclusive operation", exclusive_instruction_and_revocation}, + {"capture and placement", capture_amounts_and_immutable_placement}, + {"target quantity", targeted_close_has_one_quantity_authority}, + {"whole book revocation", whole_book_revocation}, + {"cancel and recreate", cancellation_recreation_and_noop}, + {"hash and mirror", hash_and_mirror_live_facts}, + {"overflow total", overflowed_total_retains_existing_finite_execution_guard}, + }; + for (const auto& item : cases) { + try { item.second(); } + catch (const std::exception& error) { + std::fprintf(stderr, "FAIL %s: %s\n", item.first, error.what()); + ++failures; + } + } + std::printf("frozen market instruction: %d failure(s)\n", failures); + return failures ? 1 : 0; +} diff --git a/tests/test_live_order_derived.cpp b/tests/test_live_order_derived.cpp index 1f242d6..75d08d5 100644 --- a/tests/test_live_order_derived.cpp +++ b/tests/test_live_order_derived.cpp @@ -365,7 +365,7 @@ void test_sbmt_kernels() { CHECK(i >= 0); if (i >= 0) { const PendingOrder& o = s.pending_order_at(i); - CHECK(o.sbmt_member && near(o.sbmt_tx_qty, 2.0) && near(o.sbmt_own_qty, 1.0)); + CHECK(o.pine_frozen_market_instruction.active() && near(o.pine_frozen_market_instruction.transaction()->transaction_units, 2.0) && near(o.pine_frozen_market_instruction.transaction()->own_units, 1.0)); CHECK(s.probe_fill_qty(i, 100.0, &qty, &close_only, &partition) == 0); CHECK(near(qty, 1.0) && partition == kFrozenPlacement && close_only == 0); } @@ -378,7 +378,7 @@ void test_sbmt_kernels() { CHECK(i >= 0); if (i >= 0) { const PendingOrder& o = s.pending_order_at(i); - CHECK(o.sbmt_member && near(o.sbmt_tx_qty, 2.0)); + CHECK(o.pine_frozen_market_instruction.active() && near(o.pine_frozen_market_instruction.transaction()->transaction_units, 2.0)); CHECK(s.probe_fill_qty(i, 100.0, &qty, &close_only, &partition) == 0); CHECK(near(qty, 0.0) && partition == kFrozenPlacement && close_only == 1); } @@ -391,7 +391,7 @@ void test_sbmt_kernels() { CHECK(i >= 0); if (i >= 0) { const PendingOrder& o = s.pending_order_at(i); - CHECK(o.sbmt_member && o.sbmt_kept_over_cap && near(o.sbmt_tx_qty, 2.0)); + CHECK(o.pine_frozen_market_instruction.active() && (o.pine_frozen_market_instruction.transaction() && o.over_pyramiding_cap_at_placement) && near(o.pine_frozen_market_instruction.transaction()->transaction_units, 2.0)); CHECK(s.probe_fill_qty(i, 100.0, &qty, &close_only, &partition) == 0); CHECK(near(qty, 2.0) && partition == kFrozenPlacement && close_only == 0); } @@ -399,7 +399,7 @@ void test_sbmt_kernels() { const int j = find_market(s, "Short", false); CHECK(j >= 0); if (j >= 0) { - CHECK(near(s.pending_order_at(j).sbmt_tx_qty, 2.0)); + CHECK(near(s.pending_order_at(j).pine_frozen_market_instruction.transaction()->transaction_units, 2.0)); CHECK(s.probe_fill_qty(j, 100.0, &qty, &close_only, &partition) == 0); CHECK(near(qty, 1.0) && partition == kFrozenPlacement && close_only == 0); } @@ -414,8 +414,8 @@ void test_sbmt_kernels() { const int j = find_market(s, "Short", false); CHECK(i >= 0 && j >= 0); if (i >= 0 && j >= 0) { - CHECK(near(s.pending_order_at(i).sbmt_tx_qty, 1.0)); - CHECK(near(s.pending_order_at(j).sbmt_tx_qty, 2.0)); + CHECK(near(s.pending_order_at(i).pine_frozen_market_instruction.transaction()->transaction_units, 1.0)); + CHECK(near(s.pending_order_at(j).pine_frozen_market_instruction.transaction()->transaction_units, 2.0)); CHECK(s.probe_fill_qty(i, 100.0, &qty, &close_only, &partition) == 0); CHECK(near(qty, 1.0) && partition == kAtFill && close_only == 0); CHECK(s.probe_fill_qty(j, 100.0, &qty, &close_only, &partition) == 0); diff --git a/tests/test_live_state_hash.cpp b/tests/test_live_state_hash.cpp index f32af26..5858efa 100644 --- a/tests/test_live_state_hash.cpp +++ b/tests/test_live_state_hash.cpp @@ -353,9 +353,12 @@ class Probe final : public BacktestEngine { s.pending_orders_[0].over_pyramiding_cap_at_placement = !s.pending_orders_[0].over_pyramiding_cap_at_placement; }}, - {"pending_orders_[].sbmt_member", [](Probe& s) { - if (!s.pending_orders_.empty()) - s.pending_orders_[0].sbmt_member = !s.pending_orders_[0].sbmt_member; + {"pending_orders_[].pine_frozen_market_instruction", [](Probe& s) { + if (!s.pending_orders_.empty()) { + auto& instruction = s.pending_orders_[0].pine_frozen_market_instruction; + if (instruction.active()) instruction.revoke(); + else instruction = PineFrozenMarketInstruction::transaction(1, 2); + } }}, {"pending_orders_[].short_seed_collision_role", [](Probe& s) { if (!s.pending_orders_.empty()) @@ -443,17 +446,13 @@ class Probe final : public BacktestEngine { if (!s.pending_orders_.empty()) s.pending_orders_[0].explicit_flat_admission_candidate = !s.pending_orders_[0].explicit_flat_admission_candidate; }}, - {"pending_orders_[].pooc_global_full_exit_dynamic_qty", [](Probe& s) { - if (!s.pending_orders_.empty()) - s.pending_orders_[0].pooc_global_full_exit_dynamic_qty = !s.pending_orders_[0].pooc_global_full_exit_dynamic_qty; - }}, - {"pending_orders_[].pooc_global_full_exit_tracks_bound_adds", [](Probe& s) { + {"pending_orders_[].reservation_expansion", [](Probe& s) { if (!s.pending_orders_.empty()) - s.pending_orders_[0].pooc_global_full_exit_tracks_bound_adds = !s.pending_orders_[0].pooc_global_full_exit_tracks_bound_adds; + s.pending_orders_[0].reservation_expansion.capture(50,7,PositionSide::LONG,10); }}, - {"pending_orders_[].pooc_global_full_exit_bound_add", [](Probe& s) { + {"pending_orders_[].reservation_growth_source", [](Probe& s) { if (!s.pending_orders_.empty()) - s.pending_orders_[0].pooc_global_full_exit_bound_add = !s.pending_orders_[0].pooc_global_full_exit_bound_add; + s.pending_orders_[0].reservation_growth_source.assign_capture(41,50); }}, {"pending_orders_[].signal_close_mc_remaining_qty", [](Probe& s) { if (!s.pending_orders_.empty()) s.pending_orders_[0].signal_close_mc_remaining_qty = 424242.5; diff --git a/tests/test_pine_transaction_settlement.cpp b/tests/test_pine_transaction_settlement.cpp new file mode 100644 index 0000000..9443104 --- /dev/null +++ b/tests/test_pine_transaction_settlement.cpp @@ -0,0 +1,151 @@ +// Literal settlement controls for the existing Pine source interpretation. +// These commands intentionally exercise the Pine close-artifact contract; +// they do not claim that a generic native reduction can open exposure. +#include +#include +#include +#include +#include +#include + +using namespace pineforge; +namespace { +int failures = 0; +#define CHECK(condition) do { if (!(condition)) { \ + std::fprintf(stderr, "FAIL %s:%d %s\n", __FILE__, __LINE__, #condition); \ + ++failures; } } while (0) + +class Book : public BacktestEngine { +public: + Book() { + initial_capital_ = 1000000; + default_qty_type_ = QtyType::FIXED; + default_qty_value_ = 1; + commission_value_ = 0; + slippage_ = 0; + margin_long_ = margin_short_ = 0; + pyramiding_ = 1; + bar_index_ = 0; + current_bar_ = {100, 100, 100, 100, 1, 0}; + } + void on_bar(const Bar&) override {} + void entry(const std::string& id, bool buy, double amount) { + const double nan = std::numeric_limits::quiet_NaN(); + strategy_entry(id, buy, nan, nan, amount); + } + void settle() { + ++bar_index_; + current_bar_ = {100, 100, 100, 100, 1, int64_t(bar_index_) * 60000}; + process_pending_orders(current_bar_); + } + void close_seed() { strategy_close("seed"); } + void change_close_reservation() { + for (auto& order : pending_orders_) { + if (order.id == "__close__seed") { + order.qty = 1; + order.quantity_request.reserve(1, 3); + return; + } + } + CHECK(false); + } + double signed_exposure() const { + return position_side_ == PositionSide::FLAT ? 0 + : position_side_ == PositionSide::LONG ? position_qty_ : -position_qty_; + } + uint64_t fills() const { return broker_fill_event_seq_; } + const std::vector& rows() const { return trades_; } + const std::vector& lots() const { return pyramid_entries_; } +}; + +void check_rows(const Book& book, const std::vector& ids, + const std::vector& amounts, bool seed_buy) { + CHECK(book.rows().size() == ids.size()); + if (book.rows().size() != ids.size()) return; + for (size_t i = 0; i < ids.size(); ++i) { + const auto& row = book.rows()[i]; + CHECK(row.entry_id == ids[i]); + CHECK(row.qty == amounts[i]); + CHECK(row.is_long == (i == 0 ? seed_buy : true)); + CHECK(row.entry_price == 100 && row.exit_price == 100); + CHECK(row.pnl == 0 && row.commission == 0); + CHECK(row.exit_bar_index == 2); + CHECK(row.entry_bar_index == (i == 0 ? 1 : 2)); + CHECK(row.entry_incarnation != 0); + if (i > 0) CHECK(row.entry_incarnation != book.rows()[0].entry_incarnation); + if (i > 1) CHECK((row.entry_incarnation == book.rows()[1].entry_incarnation) == seed_buy); + } +} + +void settle_batch(bool seed_buy, double seed, double opposite, double same, + bool close, bool alter_reservation = false) { + Book book; + book.entry("seed", seed_buy, seed); + book.settle(); + CHECK(book.signed_exposure() == (seed_buy ? seed : -seed)); + CHECK(book.rows().empty() && book.fills() == 1); + book.entry("opposite", !seed_buy, opposite); + book.entry("seed", seed_buy, same); + if (close) book.close_seed(); + // Explicit native reservation checkpoint: the original close Units stay3. + // This is not a claim that a Pine source call changes this reservation. + if (alter_reservation) book.change_close_reservation(); + book.settle(); + + // Buy-before-sell, frozen signed transaction quantities, and FIFO give + // these independent integer conservation results at the same price. + double expected = seed_buy ? same : -same; + if (close) expected = seed_buy ? same - seed + : -(same - std::min(seed, opposite)); + CHECK(book.signed_exposure() == expected); + CHECK(book.fills() == (close ? 4u : 3u)); + if (seed_buy) { + if (close) check_rows(book, {"seed", "seed", "seed"}, + {seed, opposite, seed}, true); + else check_rows(book, {"seed", "seed"}, {seed, opposite}, true); + } else { + if (close) check_rows(book, {"seed", "opposite", "__close__seed"}, + {seed, opposite, std::min(seed, opposite)}, false); + else check_rows(book, {"seed", "opposite"}, {seed, opposite}, false); + } + double lot_total = 0; + for (const auto& lot : book.lots()) lot_total += lot.qty; + CHECK(lot_total == std::abs(expected)); + std::printf("settlement side=%s seed=%.0f opposite=%.0f same=%.0f close=%d reserve=%d exposure=%.0f rows=%zu fills=%llu\n", + seed_buy ? "long" : "short", seed, opposite, same, close, + alter_reservation, book.signed_exposure(), book.rows().size(), + static_cast(book.fills())); + const auto row_count = book.rows().size(); + const auto fill_count = book.fills(); + book.settle(); + CHECK(book.signed_exposure() == expected); + CHECK(book.rows().size() == row_count && book.fills() == fill_count); +} + +void vanished_target_without_matching_entry_cannot_open_artifact() { + Book book; + book.entry("seed", false, 3); + book.settle(); + book.entry("opposite", true, 2); + book.entry("different", false, 4); + book.close_seed(); + book.settle(); + CHECK(book.signed_exposure() == -4); + CHECK(book.fills() == 3); + check_rows(book, {"seed", "opposite"}, {3, 2}, false); + for (const auto& lot : book.lots()) CHECK(lot.entry_id != "__close__seed"); +} +} // namespace + +int main() { + for (bool buy : {false, true}) { + settle_batch(buy, 1, 1, 1, false); + settle_batch(buy, 1, 1, 1, true); + settle_batch(buy, 3, 2, 4, false); + settle_batch(buy, 3, 2, 4, true); + } + settle_batch(false, 3, 2, 4, true, true); + vanished_target_without_matching_entry_cannot_open_artifact(); + std::printf("Pine transaction settlement: %d failure(s)\n", failures); + return failures ? 1 : 0; +} diff --git a/tests/test_pooc_global_full_exit.cpp b/tests/test_pooc_global_full_exit.cpp index 01f24f6..45991da 100644 --- a/tests/test_pooc_global_full_exit.cpp +++ b/tests/test_pooc_global_full_exit.cpp @@ -107,7 +107,7 @@ class ReservationProbe final : public BacktestEngine { for (const auto& order : pending_orders_) { if (order.type == OrderType::EXIT && order.id == "EXIT") { later_bar_exit_dynamic_qty_ = - order.pooc_global_full_exit_dynamic_qty; + order.reservation_expansion.population_open(); post_fill_exit_qty_ = order.qty; } if (order.type == OrderType::EXIT @@ -174,7 +174,7 @@ class ReservationProbe final : public BacktestEngine { exit_qty_ = order.qty; exit_qty_percent_ = order.qty_percent; exit_dynamic_qty_ = - order.pooc_global_full_exit_dynamic_qty; + order.reservation_expansion.population_open(); } } } diff --git a/tests/test_reservation_expansion.cpp b/tests/test_reservation_expansion.cpp new file mode 100644 index 0000000..a751dd1 --- /dev/null +++ b/tests/test_reservation_expansion.cpp @@ -0,0 +1,268 @@ +// Ten literal native contracts, derived from causal facts and integer/dyadic +// quantities. No canonical trades, tape, Pine/corpus, reference or grader. +#include "reservation_expansion_test_access.hpp" +#include +#include +#include +namespace prior_growth_mirror { +#include "fixtures/reservation_expansion/ff54_pending_order_mirror.hpp" +} +using namespace reservation_test; +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) +#define OLD_FIELD(name, type) static_assert(offsetof(pf_pending_order_v1_t,name)==offsetof(prior_growth_mirror::pf_pending_order_v1_t,name),"ff54 offset " #name); +#include "fixtures/reservation_expansion/ff54-fields.inc" +#undef OLD_FIELD +static_assert(offsetof(pf_pending_order_v1_t,reservation_expansion_position_cycle)>=sizeof(prior_growth_mirror::pf_pending_order_v1_t),"preserve ff54 trailing padding"); +void history(const PendingOrder& e,double units=10,double basis=10,bool partial=false) { + CHECK(e.quantity_request.reservation()); + if(!e.quantity_request.reservation())return; + CHECK(e.quantity_request.reservation()->units==units); + CHECK(e.quantity_request.reservation()->basis_units==basis); + CHECK(e.quantity_request.is_partial(1e-9,1e-9)==partial); +} +void capture_and_committed_growth() { + Book b;b.standard(3,"source"); + CHECK(b.owner()==50 && b.get("A").incarnation==41); + CHECK(b.get("E").reservation_expansion.capture()->position_cycle==7); + CHECK(b.get("E").reservation_expansion.capture()->side==PositionSide::LONG); + CHECK(b.live_all() && b.get("E").quantity_request.requests_all()); + // A requested 3. Native OCA reduces its executable capacity to 2 before + // dispatch; growth must consume committed delta, not the original request. + CHECK(b.get("A").qty==3);b.reduce("source",1);CHECK(b.get("A").qty==2); + b.fire("A");CHECK(b.quantity()==12 && b.get("E").qty==12);history(b.get("E")); + CHECK(b.live_all());b.close_partial(2);CHECK(b.quantity()==10);history(b.get("E")); + b.fire("E");CHECK(!b.has("E") && b.quantity()==0); + Book short_side;short_side.add("seed",10,false);short_side.fire("seed");short_side.advance(); + short_side.add("A",2,false);short_side.exit();CHECK(short_side.live_all()); + CHECK(short_side.get("E").reservation_expansion.capture()->side==PositionSide::SHORT); + short_side.fire("A");CHECK(short_side.quantity()==12 && short_side.get("E").qty==12); + ReservationGrowthSource invalid_source; + for(auto ids:std::vector>{{0,50},{41,0},{41,41}}) { + bool threw=false;try{invalid_source.assign_capture(ids.first,ids.second);}catch(const std::invalid_argument&){threw=true;}CHECK(threw); + } + ReservationExpansion invalid; + for(auto f:std::vector>{ + [&]{invalid.capture(0,7,PositionSide::LONG,10);}, + [&]{invalid.capture(50,0,PositionSide::LONG,10);}, + [&]{invalid.capture(50,7,PositionSide::FLAT,10);}, + [&]{invalid.capture(50,7,PositionSide::LONG,0);}, + [&]{invalid.close_population(0);}}) { + bool threw=false;try{f();}catch(const std::invalid_argument&){threw=true;}CHECK(threw); + } + invalid.capture(50,7,PositionSide::LONG,10); + bool threw=false;try{invalid.capture(50,8,PositionSide::LONG,10);}catch(const std::invalid_argument&){threw=true;}CHECK(threw); +} +void closure_is_causal() { + Book b;b.standard();b.add("B",4);const auto cause=b.get("B").incarnation; + CHECK(cause==51 && b.closure()==51 && !b.live_all()); + b.cancel("B");CHECK(b.closure()==51);b.fire("A");CHECK(b.get("E").qty==12); + b.add("C",4);CHECK(b.closure()==51);b.fire("C");CHECK(b.quantity()==16 && b.get("E").qty==12); + b.fire("E");CHECK(!b.has("E") && b.quantity()==4); + Book rejected;rejected.standard();rejected.cap(1);rejected.pooc(false);rejected.priced("B"); + CHECK(!rejected.has("B") && rejected.closure()==0 && rejected.live_all()); + Book declined;declined.standard();declined.add("B");auto first=declined.get("B").incarnation; + declined.halt(true);declined.fire("B");CHECK(!declined.has("B") && declined.closure()==first); + CHECK(declined.get("E").qty==10);declined.halt(false);declined.fire("A");CHECK(declined.get("E").qty==12); + for(bool raw:{false,true}) { + Book later;later.standard();later.advance(); + if(raw)later.raw("later");else later.priced("later"); + CHECK(later.closure()==later.get("later").incarnation); + later.cancel("later");CHECK(!later.live_all()); + } +} +void priority_is_not_identity() { + Book b;b.standard();const auto priority=b.get("A").created_seq; + CHECK(priorityposition_cycle==7); + CHECK(!b.live_all());b.open(true,20);CHECK(b.cycle()==8 && b.closure()==0 && !b.live_all()); + CHECK(b.get("E").leg_activation.bounds()->position_cycle==8); + b.fire("A");CHECK(b.quantity()==22 && b.get("E").qty==10); + b.fire("E");CHECK(b.quantity()==12 && !b.has("E")); // finite reservation still works + Book raw;raw.standard();raw.flatten();raw.raw("fresh",20);raw.fire("fresh");CHECK(raw.cycle()==8); + raw.fire("A");CHECK(raw.get("E").qty==10 && !raw.live_all()); + Book recaptured;recaptured.standard();recaptured.flatten();recaptured.open(true,20);recaptured.exit(); + CHECK(recaptured.get("E").reservation_expansion.capture()->position_cycle==8 && recaptured.live_all()); + // Source birth cycle 7 is not a Pine exclusion: intentional cycle-8 capture. + CHECK(recaptured.get("A").created_position_cycle_seq==7);recaptured.fire("A");CHECK(recaptured.get("E").qty==22); + Book retired;retired.standard();retired.retired.push_back(50);retired.fire("A",false); + CHECK(retired.has("E") && retired.get("E").qty==10 && retired.quantity()==12);retired.finish();CHECK(!retired.has("E")); + Book dormant;dormant.standard();dormant.get("E").dormant_bracket=true; + dormant.fire("E",false);CHECK(dormant.retired.empty() && dormant.live_all()); + dormant.fire("A");CHECK(dormant.get("E").qty==12 && dormant.get("E").dormant_bracket); + dormant.revive(100);CHECK(!dormant.get("E").dormant_bracket && dormant.live_all()); + dormant.fire("E");CHECK(dormant.quantity()==0 && !dormant.has("E")); + Book direct;direct.standard();direct.get("E").dormant_bracket=true;direct.revive(80); + CHECK(direct.quantity()==0 && !direct.has("E")); // direct revival close erases receiver + if(direct.has("A")){CHECK(direct.owner()==50);direct.fire("A");CHECK(!direct.has("E"));} + Book rearmed;rearmed.standard();rearmed.get("E").dormant_bracket=true;rearmed.exit(); + CHECK(rearmed.get("E").dormant_bracket && rearmed.get("E").dormant_reissue_pending); + CHECK(rearmed.owner()==rearmed.get("E").incarnation && rearmed.closure()==0); + rearmed.fire("A");CHECK(rearmed.get("E").qty==12); + Book copy;copy.standard();Book same=copy;CHECK(copy.broker_state_hash()==same.broker_state_hash()); + copy.reset();CHECK(copy.book().empty());CHECK(same.owner()==50 && same.live_all()); + copy.seed();copy.add("A");CHECK(copy.owner()==0); +} +void mirror_and_fingerprint() { + Book b;b.standard();pf_pending_order_v1_t m{};fill_pending_order_mirror(b.get("E"),&m); + CHECK(m.pooc_global_full_exit_dynamic_qty==1 && m.pooc_global_full_exit_tracks_bound_adds==1); + CHECK(m.reservation_expansion_present==1 && m.reservation_expansion_position_cycle==7 && m.reservation_expansion_side==1); + CHECK(m.reservation_expansion_first_later_admission_present==0 && m.reservation_expansion_first_later_admission==0); + b.add("B");fill_pending_order_mirror(b.get("E"),&m); + CHECK(m.pooc_global_full_exit_dynamic_qty==0 && m.reservation_expansion_first_later_admission_present==1 && m.reservation_expansion_first_later_admission==51); + b.cancel("E");fill_pending_order_mirror(b.get("A"),&m); + CHECK(m.pooc_global_full_exit_bound_add==1 && m.reservation_growth_source_present==1 && m.reservation_growth_source_reservation_owner==50); + fill_pending_order_mirror(b.get("B"),&m);CHECK(m.pooc_global_full_exit_bound_add==0 && m.reservation_growth_source_present==0 && m.reservation_growth_source_reservation_owner==0); + Book base;base.standard();base.add("B");const auto hash=base.broker_state_hash(); + const std::vector>> mutations={ + {"capture presence",[](Book& x){x.get("E").reservation_expansion={};}}, + {"cycle",[](Book& x){auto& c=x.get("E").reservation_expansion;c={};c.capture(50,8,PositionSide::LONG,10);c.close_population(51);}}, + {"side",[](Book& x){auto& c=x.get("E").reservation_expansion;c={};c.capture(50,7,PositionSide::SHORT,10);c.close_population(51);}}, + {"closure presence",[](Book& x){auto& c=x.get("E").reservation_expansion;c={};c.capture(50,7,PositionSide::LONG,10);}}, + {"closure incarnation",[](Book& x){auto& c=x.get("E").reservation_expansion;c={};c.capture(50,7,PositionSide::LONG,10);c.close_population(53);}}, + {"source presence",[](Book& x){x.get("A").reservation_growth_source={};}}, + {"source owner",[](Book& x){x.get("A").reservation_growth_source.assign_capture(41,52);}}, + }; + const char* mutation_fields[]={"reservation_expansion_present", "reservation_expansion_position_cycle", + "reservation_expansion_side", "reservation_expansion_first_later_admission_present", + "reservation_expansion_first_later_admission", "reservation_growth_source_present", + "reservation_growth_source_reservation_owner"}; + size_t mutation_index=0; + for(const auto& test:mutations){ + Book changed=base;test.second(changed);CHECK(changed.broker_state_hash()!=hash); + const char* id=mutation_index<5?"E":"A"; + pf_pending_order_v1_t before{},after{}; + fill_pending_order_mirror(base.get(id),&before);fill_pending_order_mirror(changed.get(id),&after); + int count=0;const auto* fields=pending_order_layout(&count);const pf_field_desc_t* field=nullptr; + for(int j=0;j(&before)+field->offset, + reinterpret_cast(&after)+field->offset,field->size)!=0); + ++mutation_index;std::printf("hash/mirror mutation: %s\n",test.first); + } + int n=0;const auto* layout=pending_order_layout(&n);CHECK(n==155); + int i=0; +#define OLD_FIELD(field, ctype) CHECK(std::strcmp(layout[i].name,#field)==0); CHECK(std::strcmp(layout[i].type,ctype)==0); CHECK(layout[i].offset==offsetof(prior_growth_mirror::pf_pending_order_v1_t,field)); ++i; +#include "fixtures/reservation_expansion/ff54-fields.inc" +#undef OLD_FIELD + CHECK(i==142); + const char* new_fields[]={"reservation_expansion_position_cycle","reservation_expansion_present","reservation_expansion_side","reservation_expansion_first_later_admission_present","reservation_expansion_first_later_admission","reservation_growth_source_present","reservation_growth_source_reservation_owner"}; + for(auto name:new_fields){CHECK(std::strcmp(layout[i].name,name)==0);CHECK(layout[i].offset>=sizeof(prior_growth_mirror::pf_pending_order_v1_t));++i;} + CHECK(strategy_pending_order_get(&base,0,&m,sizeof(m))==0); + std::vector bytes(sizeof(m)+8,0xA5); + CHECK(strategy_pending_order_get(&base,0,bytes.data(),sizeof(prior_growth_mirror::pf_pending_order_v1_t))==0); + CHECK(std::memcmp(bytes.data(),&m,sizeof(prior_growth_mirror::pf_pending_order_v1_t))==0); + for(size_t j=sizeof(prior_growth_mirror::pf_pending_order_v1_t);jkind()==QuantityIntent::Kind::Fraction); + const auto prior=fraction.get("E");fraction.exit();CHECK(fraction.get("E").incarnation!=prior.incarnation); + CHECK(fraction.get("E").quantity_request.requests_all() && !prior.quantity_request.requests_all()); + // Other selection exclusions, unchanged Pine ownership. + for(int kind=0;kind<5;++kind){Book x;x.seed();x.add("A"); + if(kind==0)x.pooc(false); + if(kind==1)x.get("A").over_pyramiding_cap_at_placement=true; + if(kind==2)x.get("A").is_long=false; + if(kind==3)x.get("A").created_position_side=PositionSide::SHORT; + if(kind==4)x.get("A").created_bar-=1; + x.exit();CHECK(!x.get("E").reservation_expansion.capture() && x.owner()==0); + } +} +} +int main(){ + const std::pair cases[]={ + {"1 capture/committed growth",capture_and_committed_growth},{"2 causal closure",closure_is_causal}, + {"3 priority versus incarnation",priority_is_not_identity},{"4 death/rearm/recapture",target_death_and_recapture}, + {"5 attempts/retirement",attempts_and_retirement},{"6 OCA capacity",oca_capacity_and_history}, + {"7 old/new multiple-tracker witness",multiple_tracker_witness},{"8 cycle/retirement/dormancy",cycle_retirement_and_dormancy}, + {"9 mirror/fingerprint",mirror_and_fingerprint},{"10 historical intent/selection",historical_quantity_and_selection}}; + for(auto test:cases){std::printf("contract: %s\n",test.first);try{test.second();}catch(const std::exception& e){++failures;std::fprintf(stderr,"FAIL contract %s: %s\n",test.first,e.what());}} + std::printf("%d checks, %d failures\n",checks,failures);return failures?1:0; +}