diff --git a/CMakeLists.txt b/CMakeLists.txt index db85631..6de6d2f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,6 +78,7 @@ endif() # === pineforge library ================================================= add_library(pineforge STATIC src/c_abi.cpp + src/compat/pine/order_priority.cpp src/engine_aux_security.cpp src/engine_fills.cpp src/engine_lower_tf.cpp diff --git a/README.md b/README.md index 069a3da..367cc94 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,8 @@ live behavior or real broker fills. ## Validation scoreboard +**Native policy refactoring · 2026-09-10:** engine [#234](https://github.com/pineforge-4pass/pineforge-engine/pull/234) and codegen [#127](https://github.com/pineforge-4pass/pineforge-codegen-oss/pull/127) preserve **4,182 excellent / 8 strong**. Both the old-engine/new-codegen and new-engine/new-codegen Cloud runs retain all **4,190 raw trade CSVs, counts and full grades** unchanged. The target bands have zero entrants and zero leavers: **net 0, no individual regression**. These changes remove an unused source flag and give Pine cap behavior explicit ownership; they do not claim complete engine independence. The formal gates remain zero-improvement FAILs and the official baseline remains unchanged. + **Round 39 · 2026-09-09:** **4,182 excellent / 8 strong / zero moderate** across all **4,190 scored probes**. This round adds one excellent result, with zero regressions on any canonical metric. | Board | Test set | Result | TradingView trades evaluated | @@ -169,7 +171,7 @@ Every script is exported from TradingView as-is (its own inputs, its own default - **strong** — ≥ 95% matched, trade count within 6%, entries within 0.1% and exits within 0.5% at p90; - **moderate / weak** — ≥ 75% coverage, or less. -A grade is never one lucky run. Every candidate build is measured over the whole population on a reproducible cloud pipeline, each graded snapshot is published by content hash, and a build ships only through a deterministic gate: **no regression on any metric of any ETH 15m probe**, and, pooled over the other lanes, strictly more probes entering the excellent and excellent+strong bands than leaving them. Merges are fast-forwarded, so the commit that was gated is the commit on `main`. +Published parity results use a fixed population and reproducible Cloud Run measurements. The formal gate requires **no hard-surface regression** and strictly positive pooled movement across the target excellent and excellent+strong bands. A documented native-correctness exception permits exactly zero target-band movement with no individual regression, after full comparison, independent review and CI; its actual FAIL remains recorded and baseline promotion is deferred. Negative movement is outside this exception. Baseline promotion requires a recorded PASS and an exact-head merge with green CI. ### What the closed test taught the engine @@ -206,6 +208,10 @@ PyneCore's 15 non-excellent strategies involve `strategy.exit(stop=…, limit= --- +Explicit Pine frontends must use the [execution attachment and regeneration +contract](docs/pine-order-priority-boundary.md) for retained-parent priority. +Bare native engines and cap-only generated constructors do not opt into it. + ## What ships here - `libpineforge.a` — the static runtime: order matching and fills, sizing and margin, the bar magnifier, 66 indicator classes, `request.security()`, time and session math. diff --git a/docs/pages/abi-stability.md b/docs/pages/abi-stability.md index 7cca965..7b61465 100644 --- a/docs/pages/abi-stability.md +++ b/docs/pages/abi-stability.md @@ -112,12 +112,12 @@ notice: - The shape of internal log lines (use them for humans, not parsers). Rebuild generated and native C++ objects against matching engine headers and -runtime. The cap compatibility extraction changes the v2 object layout and -therefore uses the internal `engine_script_run_v3` inline namespace. An object +runtime. The cap and priority extractions change the v2/v3 object layouts and +therefore use the internal `engine_script_run_v4` inline namespace. An object built from base `38dc73e` headers references v2 out-of-line members and must -fail to link to this v3 runtime. The source-pairing check compiles frozen exact +fail to link to this v4 runtime. The source-pairing check compiles frozen exact base headers separately before testing the link, so a missing include or -compile failure cannot masquerade as mismatch protection. Both generated-style +compile failure cannot masquerade as mismatch protection. Frozen f864/v3 headers are also rejected by this v4 runtime. Both generated-style and native-style current callers must still link. `PINEFORGE_HAS_SCRIPT_RUN_PREPARE_V1` remains 1: it describes the existing hook @@ -129,13 +129,13 @@ retrofit an old module. Public C function signatures, POD layouts, 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 v3 module. +its own matching runtime; this check does not turn it into a v4 module. -The cap boundary also advances the broker fingerprint domain to -`pineforge-broker-state/v3` and stream fingerprint version to 3. These identify -changed serialized state, including the Pine configuration/quota/cause and +The cap and priority boundaries advance the broker fingerprint domain to +`pineforge-broker-state/v4` and stream fingerprint version to 4. These identify +changed serialized state, including Pine priority attachment/configuration, cap quota/cause and separate generic close request. The Pine component schema remains 1; it is -independent of the aggregate fingerprint version. Prior v2 fingerprints are +independent of the aggregate fingerprint version. Prior v2/v3 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/pine-intraday-cap-boundary.md b/docs/pine-intraday-cap-boundary.md index 0a2a716..f5b5535 100644 --- a/docs/pine-intraday-cap-boundary.md +++ b/docs/pine-intraday-cap-boundary.md @@ -7,9 +7,10 @@ extracts those decisions from existing execution paths; it does not make the engine independent of Pine or establish broader TradingView parity. The paired codegen explicitly selects this compatibility component in the -`GeneratedStrategy` constructor, guarded by -`PINEFORGE_HAS_EXPLICIT_PINE_CAP_V1`. Older supported engines do not define -that capability and keep their established default. Risk statements still +`GeneratedStrategy` constructor. Current codegen uses the scoped +`attach_pine_execution_adapter()` hook for cap and retained-parent priority, +with a guarded cap-only fallback for older engines; see +[pairing and regeneration](pine-order-priority-boundary.md). Risk statements still assign `max_intraday_filled_orders_ = (int)(expression)` at execution time; selecting a component does not evaluate or hoist a risk statement. @@ -117,18 +118,18 @@ Independent Grok, unchanged fixed Cloud controls and full regression/gate verification remain root-owned requirements before any parity or publication claim. -## Internal pairing and observable-state version 3 +## Internal pairing and observable-state version 4 Relative to base `38dc73e`, the new policy/obligation members change the -`BacktestEngine` layout. Its inline namespace is now `engine_script_run_v3`; +`BacktestEngine` layout. Its inline namespace is now `engine_script_run_v4`; base-header v2 native/generated objects must fail to link against this runtime. The exact source-pairing test compiles frozen base headers and checks explicit -undefined v2 symbols, alongside matched v3 positive controls. Its isolated v2 +undefined v2 symbols, alongside matched v4 positive controls. Its isolated v2 symbol stub is a reverse-link control, not a build of the entire old runtime. No mismatch test program is executed. -Broker hashes use `pineforge-broker-state/v3`; stream fingerprints begin with -version3. The Pine policy's own schema stays1. Public C ABI version4, stream API +Broker hashes use `pineforge-broker-state/v4`; stream fingerprints begin with +version 4. Each Pine component's own schema remains 1. Public C ABI version4, stream API version1, POD layouts, exports and `PINEFORGE_HAS_SCRIPT_RUN_PREPARE_V1` remain unchanged. This version correction changes linking and serialized hash bytes, not cap charging, fill prices or other economic behavior. It does not make @@ -151,3 +152,5 @@ separating requests, execution facts and exposure state; they do not establish Pine quota policy or predictable live fills. See [MQL5 OrderSend](https://www.mql5.com/en/docs/trading/ordersend) and [OnTradeTransaction](https://www.mql5.com/en/docs/event_handlers/ontradetransaction). This patch adds no external execution adapter or callback-ordering model. + +The priority attachment adds a v3/f864 frozen-header rejection control; the cap-only API and its financial rules retain their scope. See [order-priority ownership](pine-order-priority-boundary.md). diff --git a/docs/pine-order-priority-boundary.md b/docs/pine-order-priority-boundary.md new file mode 100644 index 0000000..9cdf7ab --- /dev/null +++ b/docs/pine-order-priority-boundary.md @@ -0,0 +1,76 @@ +# Explicit Pine execution attachment and order priority + +`BacktestEngine::attach_pine_execution_adapter()` explicitly selects two +compatibility components: the existing intraday cap and retained-parent order +priority. It is idempotent, preserves configuration and outstanding cap state, +and does not imply that all Pine behavior has been extracted from the engine. +`enable_pine_intraday_cap()`, the cap constructor argument and legacy integer +cap assignments remain cap-only operations. + +Bare construction leaves order priority detached. Syminfo metadata, including +`flat_retained_child_fresh_parent_order=1`, cannot attach it. The Pine component +owns that key's finite-positive interpretation and retains a supplied value +while detached; explicit attachment preserves a prior off override. Under Pine +attachment, an absent key enables the exact historical exception. Zero, negative +and nonfinite supplied values disable it. Copy/reset preserve attachment and +configuration without retaining a cached scheduling decision. + +The full predicate lives in `src/compat/pine/order_priority.cpp`: exact two-object +book, ordinary POOC context, flat placement, named-cancel and exact child-reissue +receipts, adjacent incarnations, default quantities, full absolute stop+limit +child, pure-stop parent, prior-bar age and all OCA/callback/magnifier/stream +exclusions. Three/four unrelated orders and an intervening accepted-and-canceled +order remain exclusions. This is one active rule transferred to Pine ownership, +not a claim that its source-shape policy disappeared globally. + +The comparator consumes an immutable `broker::OrderPriorityDecision` bound to +exact incarnations. It substitutes two sequence tie-breaks at their former +location, after fill phase. It does not change eligibility, fill prices, OCA +mutation, callbacks, admission or financial accounting. Shared incarnation +receipts and their other compatibility consumers remain unchanged. + +## Pairing and regeneration + +New codegen calls the fully qualified attachment method in the constructor, +before `strategy_create` returns and host metadata arrives: + +```cpp +#if defined(PINEFORGE_HAS_EXPLICIT_PINE_EXECUTION_ADAPTER_V1) + pineforge::BacktestEngine::attach_pine_execution_adapter(); +#elif defined(PINEFORGE_HAS_EXPLICIT_PINE_CAP_V1) + pineforge::BacktestEngine::enable_pine_intraday_cap(); +#endif +``` + +The fallback preserves existing cap-only engines' native-to-Pine cap selection +and their old default retained-parent rule. Engines lacking both capabilities +use their established defaults. This is generated-source portability with +matching headers/runtime, not binary compatibility across engine versions. + +Old cap-only generated source can compile with current headers but does **not** +select the priority exception. Regenerate it with the paired codegen, or +explicitly update a maintained C++ Pine frontend to call the attachment method +before metadata. Neither metadata nor an implicit constructor default repairs +old source. Existing checked-in corpus/tutorial C++ must likewise be regenerated +before using it for Pine behavior measurements; compilation alone is insufficient. + +Rebuild every C++ consumer with matching current headers and archive. The class +namespace is `engine_script_run_v4`; frozen v2/base38 and v3/f864 callers are +compile/link rejection controls. Broker fingerprint domain and stream fingerprint +version are now 4 and include priority attachment/configuration. Prior hashes +are not comparable. Public C functions, PODs and ABI/API versions are unchanged; +an erased handle still belongs to its creating module. + +## Remaining native gap and evidence limits + +Bare native execution retains the ordinary phase/sequence scan. An older child +visited while flat can still be skipped until a later pass, exposing a transient +position to the intervening close-time observer. No identity-bound child +activation/event queue has been added. That causal scheduler is a separate next +slice, not an unused reducer in this extraction. + +Literal C++ controls preserve the old Pine price, quantity, timing and financial +assertions with explicit fixture attachment; they separately pin native metadata +inertness, exclusions, OCA, copy/reset/hash and current lifetime safety. Codegen +checks compile source only. None establishes TV truth, measured campaign +neutrality, actualZERO, absence of population regressions or readiness to publish. diff --git a/include/pineforge/compat/pine/order_priority.hpp b/include/pineforge/compat/pine/order_priority.hpp new file mode 100644 index 0000000..2e22275 --- /dev/null +++ b/include/pineforge/compat/pine/order_priority.hpp @@ -0,0 +1,45 @@ +#pragma once +#include "../../order_priority.hpp" +#include +#include +#include +#include + +namespace pineforge { struct PendingOrder; } +namespace pineforge::compat::pine { + +struct OrderPriorityContext { + bool broker_flat; + bool process_orders_on_close; + bool calc_on_order_fills; + bool coof_scheduler_active; + bool bar_magnifier_enabled; + bool stream_warmup_mode; + bool stream_idle; + int bar_index; +}; + +// Pine's bounded retained-child/recreated-parent exception. The complete +// source-shape rule lives in this component; this is an ownership transfer, +// not a generic native activation/dependency scheduler. +class OrderPriority { +public: + static constexpr uint64_t schema_version = 1; + void attach() { attached_ = true; } + bool attached() const { return attached_; } + bool retained_parent_first() const { return retained_parent_first_; } + void metadata(const std::string& key, double value) { + // One declaration owner, including values received while detached. + // Metadata never selects Pine execution. Reattachment preserves off. + if (key == "flat_retained_child_fresh_parent_order") + retained_parent_first_ = std::isfinite(value) && value > 0.0; + } + std::optional select( + const OrderPriorityContext& ctx, + const std::vector& book) const; +private: + bool attached_ = false; + bool retained_parent_first_ = true; +}; + +} // namespace pineforge::compat::pine diff --git a/include/pineforge/engine.hpp b/include/pineforge/engine.hpp index 35af69a..2c7eb76 100644 --- a/include/pineforge/engine.hpp +++ b/include/pineforge/engine.hpp @@ -14,6 +14,7 @@ #include "bar.hpp" #include "broker_events.hpp" #include "compat/pine/intraday_cap.hpp" +#include "compat/pine/order_priority.hpp" #include "series.hpp" #include "timeframe.hpp" #include "magnifier.hpp" @@ -36,6 +37,8 @@ // Generated constructors can explicitly select Pine cap compatibility before // any host metadata setter. Older engines keep their legacy default behavior. #define PINEFORGE_HAS_EXPLICIT_PINE_CAP_V1 1 +// Explicitly selects cap and retained-parent priority compatibility only. +#define PINEFORGE_HAS_EXPLICIT_PINE_EXECUTION_ADAPTER_V1 1 namespace pineforge { @@ -1100,10 +1103,10 @@ struct StrategyOverrides { // The C++ subclass contract is internal, unlike pineforge.h's stable C ABI. // Changing its layout or vtable requires all generated/native C++ objects to be rebuilt. -// v3 replaces the v2 cap scalars with a Pine policy and a close obligation. -// Version the mangled class name so a v2 header's member offsets/vtable cannot +// v4 adds detached Pine order-priority ownership to the v3 cap boundary. +// 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_v3 { +inline namespace engine_script_run_v4 { class BacktestEngine { protected: // --- Position state --- @@ -1282,15 +1285,9 @@ class BacktestEngine { // Historical fill-triggered recalculation is strictly opt-in. The false // branch in dispatch_bar remains the legacy control path. bool calc_on_order_fills_ = false; - // Narrow ordinary-POOC broker ordering rule for one exact book shape: - // while truly flat, a single reissued from_entry bracket can retain an - // older sequence slot than its same-source-bar pure-stop parent after the - // prior parent was explicitly cancelled and freshly recreated. - // That exact two-order book scans the parent first so the child can inspect - // the post-entry path before the close-time script body. The metadata key - // remains as an explicit A/B override; ordinary execution enables the - // TV-pinned rule by default. - bool flat_retained_child_fresh_parent_order_ = true; + // Detached on bare native construction. Only the explicit Pine frontend + // attachment can select its source-shape priority interpretation. + compat::pine::OrderPriority pine_order_priority_; QtyType default_qty_type_ = QtyType::FIXED; double default_qty_value_ = 1.0; int pyramiding_ = 1; // max additional entries in same direction @@ -4497,6 +4494,13 @@ class BacktestEngine { // Explicit frontend selection, not a generic native risk switch. This // preserves any prior declaration values and does not reset quota/state. void enable_pine_intraday_cap() { max_intraday_filled_orders_.attach(); } + // Current execution-adapter scope: intraday cap + retained-parent priority. + // Idempotent configuration attachment, not a reset or universal Pine mode. + // Generated constructors call this before any host metadata is forwarded. + void attach_pine_execution_adapter() { + max_intraday_filled_orders_.attach(); + pine_order_priority_.attach(); + } virtual ~BacktestEngine() = default; virtual void on_bar(const Bar& bar) = 0; @@ -4795,10 +4799,7 @@ class BacktestEngine { margin_zero_cover_full_liquidation_ = std::isfinite(value) && value > 0.0; } - if (key == "flat_retained_child_fresh_parent_order") { - flat_retained_child_fresh_parent_order_ = - std::isfinite(value) && value > 0.0; - } + pine_order_priority_.metadata(key, value); // Forward through the real base setter used by the C ABI. A selected // frontend owns recognition and numeric validation; no derived shadow // setter or cap-key interpretation belongs in this transport. @@ -5141,5 +5142,5 @@ class BacktestEngine { void trace(const std::string& name, int value) { trace(name, static_cast(value)); } }; -} // inline namespace engine_script_run_v3 +} // inline namespace engine_script_run_v4 } // namespace pineforge diff --git a/include/pineforge/order_priority.hpp b/include/pineforge/order_priority.hpp new file mode 100644 index 0000000..3e9fa5e --- /dev/null +++ b/include/pineforge/order_priority.hpp @@ -0,0 +1,23 @@ +#pragma once +#include +#include + +namespace pineforge::broker { + +// Immutable assignments bound to exact pending objects, not reusable user IDs. +// A decision changes only the sequence tie-break, never phase or eligibility. +struct OrderPriorityAssignment { + uint64_t incarnation; + int64_t sequence; +}; +struct OrderPriorityDecision { + std::array assignments; + int64_t sequence(uint64_t incarnation, int64_t fallback) const { + for (const auto& assignment : assignments) { + if (assignment.incarnation == incarnation) return assignment.sequence; + } + return fallback; + } +}; + +} // namespace pineforge::broker diff --git a/scripts/broker_state_hash_waivers.txt b/scripts/broker_state_hash_waivers.txt index 9058dc9..ee7a47a 100644 --- a/scripts/broker_state_hash_waivers.txt +++ b/scripts/broker_state_hash_waivers.txt @@ -15,7 +15,6 @@ commission_type_ # config: strategy(commission_type=...) commission_value_ # config: strategy(commission_value=...) default_qty_type_ # config: strategy(default_qty_type=...) default_qty_value_ # config: strategy(default_qty_value=...) -flat_retained_child_fresh_parent_order_ # config: explicit A/B ordering override historical_security_lookahead_projection_ # config: opt-in request.security selector initial_capital_ # config: strategy(initial_capital=...) margin_call_enabled_ # config: set_margin_call_enabled toggle diff --git a/scripts/check_broker_state_hash_coverage.py b/scripts/check_broker_state_hash_coverage.py index ebc7c1f..397ec22 100644 --- a/scripts/check_broker_state_hash_coverage.py +++ b/scripts/check_broker_state_hash_coverage.py @@ -340,7 +340,7 @@ def folds(name: str, expression: str) -> list[tuple[str, str]]: def _runtime_version_coverage(header: str, source: str, stream: str) -> None: - """The v3 layout and serialized-state contracts must advance together. + """The v4 layout and serialized-state contracts must advance together. Pin the actual hash entry points, rather than accepting a version string mentioned in a comment or an unrelated helper. Public C ABI versions have @@ -348,18 +348,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_v3"]: - raise ValueError("BacktestEngine layout requires internal namespace engine_script_run_v3") + if namespaces != ["engine_script_run_v4"]: + raise ValueError("BacktestEngine layout requires internal namespace engine_script_run_v4") 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/v3"\);', broker): - raise ValueError("broker hash must start with pineforge-broker-state/v3") + if not re.match(r'\s*Fnv\s+f;\s*f\.s\("pineforge-broker-state/v4"\);', broker): + raise ValueError("broker hash must start with pineforge-broker-state/v4") 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(3);integer(broker_state_hash());" + fold = "integer(4);integer(broker_state_hash());" if compact.count(fold) != 1: - raise ValueError("stream hash requires version 3 followed by the broker hash") + raise ValueError("stream hash requires version 4 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") diff --git a/scripts/check_script_cpp_abi.py b/scripts/check_script_cpp_abi.py index 7b0c049..17ef0b0 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_v3" +CURRENT_NAMESPACE = "engine_script_run_v4" BASE_NAMESPACE = "engine_script_run_v2" FIXTURE = Path(__file__).resolve().parents[1] / "tests/fixtures/script_cpp_abi/base38" @@ -29,14 +29,15 @@ def entry_diagnostic(lines, namespace, method): return next((line for line in lines if needle in line), None) -def frozen_headers(destination): +def frozen_headers(destination, fixture=FIXTURE, commit=BASE_COMMIT, + namespace=BASE_NAMESPACE, engine_sha=BASE_ENGINE_SHA256): """Authenticate and unpack the exact tracked base38 header closure.""" - manifest = json.loads((FIXTURE / "manifest.json").read_text()) - if (manifest["source_commit"] != BASE_COMMIT - or manifest["internal_namespace"] != BASE_NAMESPACE - or manifest["files"]["pineforge/engine.hpp"]["sha256"] != BASE_ENGINE_SHA256): + 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") - archive = (FIXTURE / "headers.json.gz").read_bytes() + archive = (fixture / "headers.json.gz").read_bytes() if hashlib.sha256(archive).hexdigest() != manifest["archive_sha256"]: raise RuntimeError("stale-header fixture archive digest mismatch") contents = json.loads(gzip.decompress(archive)) @@ -141,6 +142,10 @@ def main(): root = Path(temporary) old_include = root / "base38/include" frozen_headers(old_include) + cap_include = root / "basef864/include" + frozen_headers(cap_include, FIXTURE.parent / "basef864", + "f864be590931ba08c8df5af983b33b2c29be9c67", "engine_script_run_v3", + "54b35fffaa163a31467f8ba883e44e02f013a37216b558dfe3a4f28fbbe84dc2") common = [args.compiler, "-std=c++17", "-O0", *args.extra_flag] def compile_object(name, source, include): @@ -164,6 +169,11 @@ def compile_object(name, source, include): old_symbols = compile_object("base38_symbol_control", BASE_SYMBOL_CONTROL, old_include) legacy = compile_object("legacy_unversioned", LEGACY_CALLER, old_include) + cap_native = compile_object("basef864_native", caller("engine_script_run_v3"), cap_include) + cap_generated = compile_object("basef864_generated", caller("engine_script_run_v3", True), cap_include) + cap_symbols = compile_object("basef864_symbol_control", + BASE_SYMBOL_CONTROL.replace("engine_script_run_v2", "engine_script_run_v3"), cap_include) + def link(name, obj, runtime, missing_namespace=None): linked = subprocess.run( [*common, str(obj), str(runtime), "-pthread", "-o", str(root / name)], @@ -199,7 +209,13 @@ def link(name, obj, runtime, missing_namespace=None): link("current_native_to_v2_symbol_control", current_native, old_symbols, CURRENT_NAMESPACE) link("current_generated_to_v2_symbol_control", current_generated, old_symbols, CURRENT_NAMESPACE) link("unversioned_to_current", legacy, args.library, "") - print("6 translation units compiled; 4 positive links; 5 rejected links; no executable run") + link("basef864_native_to_v3_symbol_control", cap_native, cap_symbols) + link("basef864_generated_to_v3_symbol_control", cap_generated, cap_symbols) + link("basef864_native_to_current", cap_native, args.library, "engine_script_run_v3") + link("basef864_generated_to_current", cap_generated, args.library, "engine_script_run_v3") + link("current_native_to_v3_symbol_control", current_native, cap_symbols, CURRENT_NAMESPACE) + link("current_generated_to_v3_symbol_control", current_generated, cap_symbols, CURRENT_NAMESPACE) + print("9 translation units compiled; 6 positive links; 9 rejected links; no executable run") if __name__ == "__main__": diff --git a/scripts/test_broker_state_hash_coverage.py b/scripts/test_broker_state_hash_coverage.py index a53faf6..462bdcf 100644 --- a/scripts/test_broker_state_hash_coverage.py +++ b/scripts/test_broker_state_hash_coverage.py @@ -56,26 +56,26 @@ def test_actual_source_and_named_parser(self): self.assertEqual(len(members(HEADER, "PyramidEntry")), 18) self.assertEqual(members(HEADER), members(HEADER, "PendingOrder")) - def test_layout_and_hash_versions_must_match_v3_contract(self): - self.assertIn("engine_script_run_v3", HEADER) - self.assertIn('f.s("pineforge-broker-state/v3");', SOURCE) - self.assertIn("integer(3); integer(broker_state_hash());", STREAM) - self.assertEqual(self.check(header=HEADER.replace("engine_script_run_v3", "engine_script_run_v2"))[0], 1) + def test_layout_and_hash_versions_must_match_v4_contract(self): + self.assertIn("engine_script_run_v4", HEADER) + self.assertIn('f.s("pineforge-broker-state/v4");', SOURCE) + self.assertIn("integer(4); integer(broker_state_hash());", STREAM) + self.assertEqual(self.check(header=HEADER.replace("engine_script_run_v4", "engine_script_run_v2"))[0], 1) for replacement in ['f.s("pineforge-broker-state/v2");', '', - '// f.s("pineforge-broker-state/v3");']: + '// f.s("pineforge-broker-state/v4");']: self.assertEqual(self.check(source=SOURCE.replace( - 'f.s("pineforge-broker-state/v3");', replacement))[0], 1) + 'f.s("pineforge-broker-state/v4");', replacement))[0], 1) for replacement in ["integer(2); integer(broker_state_hash());", "integer(broker_state_hash());", - "if (false) { integer(3); integer(broker_state_hash()); }"]: + "if (false) { integer(4); integer(broker_state_hash()); }"]: self.assertEqual(self.check(stream=STREAM.replace( - "integer(3); integer(broker_state_hash());", replacement))[0], 1) + "integer(4); 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/v3");' + broker_fold = 'f.s("pineforge-broker-state/v4");' altered = SOURCE.replace(broker_fold, '') + '\nvoid other() { ' + broker_fold + ' }\n' self.assertEqual(self.check(source=altered)[0], 1) - stream_fold = "integer(3); integer(broker_state_hash());" + stream_fold = "integer(4); 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/src/compat/pine/order_priority.cpp b/src/compat/pine/order_priority.cpp new file mode 100644 index 0000000..ae6d4f5 --- /dev/null +++ b/src/compat/pine/order_priority.cpp @@ -0,0 +1,93 @@ +#include +#include +#include + +namespace pineforge::compat::pine { + +std::optional OrderPriority::select( + const OrderPriorityContext& ctx, + const std::vector& book) const { + if (!attached_ || !retained_parent_first_ + || !ctx.broker_flat || !ctx.process_orders_on_close + || ctx.calc_on_order_fills || ctx.coof_scheduler_active + || ctx.bar_magnifier_enabled || ctx.stream_warmup_mode + || !ctx.stream_idle || book.size() != 2) return std::nullopt; + + const PendingOrder* parent = nullptr; + const PendingOrder* child = nullptr; + for (const PendingOrder& order : book) { + if (order.type == OrderType::ENTRY) parent = ℴ + else if (order.type == OrderType::EXIT) child = ℴ + } + if (!parent || !child) return std::nullopt; + + // Preserve every legacy exclusion. Incarnation adjacency and exact book + // size are Pine evidence boundaries, not native dependency invariants. + const uint64_t cancelled_incarnation = + parent->recreated_after_named_cancelled_entry_incarnation; + const uint64_t surviving_exit_incarnation = + parent->named_cancel_surviving_exit_incarnation; + const bool parent_is_exact_fresh_stop = + parent->type == OrderType::ENTRY + && parent->created_position_side == PositionSide::FLAT + && !parent->created_by_same_id_replacement + && cancelled_incarnation != 0 + && cancelled_incarnation < parent->incarnation + && cancelled_incarnation != child->incarnation + && surviving_exit_incarnation > cancelled_incarnation + && surviving_exit_incarnation < parent->incarnation + && parent->created_bar == ctx.bar_index - 1 + && std::isnan(parent->qty) + && !parent->created_during_coof_recalc + && !parent->created_after_position_close_in_bar + && !parent->over_pyramiding_cap_at_placement + && !parent->stop_limit_activated + && std::isfinite(parent->stop_price) + && std::isnan(parent->limit_price) + && std::isnan(parent->trail_points) + && std::isnan(parent->trail_price) + && std::isnan(parent->trail_offset) + && parent->oca_name.empty() + && parent->oca_type == 0; + const double child_qp = std::isnan(child->qty_percent) + ? 100.0 : child->qty_percent; + const bool child_is_exact_retained_bracket = + child->type == OrderType::EXIT + && !child->from_entry.empty() + && child->created_by_same_id_replacement + && child->replaced_exit_order_incarnation + == surviving_exit_incarnation + && !child->created_while_in_position + && child->created_position_side == PositionSide::FLAT + && child->created_bar == ctx.bar_index - 1 + && !child->created_during_coof_recalc + && !child->created_after_position_close_in_bar + && !child->requested_partial + && std::isnan(child->qty) + && child_qp >= 100.0 - 1e-9 + && std::isfinite(child->stop_price) + && std::isfinite(child->limit_price) + && std::isnan(child->profit_ticks) + && std::isnan(child->loss_ticks) + && std::isnan(child->trail_points) + && std::isnan(child->trail_price) + && std::isnan(child->trail_offset) + && child->oca_name.empty() + && child->oca_type == 0; + const bool exact_pair = parent_is_exact_fresh_stop + && child_is_exact_retained_bracket + && child->from_entry == parent->id + && child->created_seq < parent->created_seq + && child->incarnation != 0 + && parent->incarnation != 0 + && parent->incarnation + < std::numeric_limits::max() + && child->incarnation == parent->incarnation + 1; + if (!exact_pair) return std::nullopt; + return broker::OrderPriorityDecision{{{ + {parent->incarnation, child->created_seq}, + {child->incarnation, parent->created_seq}, + }}}; +} + +} // namespace pineforge::compat::pine diff --git a/src/engine_fills.cpp b/src/engine_fills.cpp index cce26e3..7eae110 100644 --- a/src/engine_fills.cpp +++ b/src/engine_fills.cpp @@ -3716,54 +3716,13 @@ void BacktestEngine::sort_orders_by_fill_phase(const Bar& bar) { } } - // Default-on retained-child / fresh-parent rule. A child retained across - // an explicit parent cancellation can keep an older broker slot than the - // freshly recreated parent; reissuing the child on that same source bar - // preserves those unequal priorities even though created_bar is now equal. The - // ordinary sequence tie-break then scans the child while flat (Skip) and - // the parent afterwards, so POOC on_bar observes a transient position - // before the second broker pass can revisit the child. - // - // Keep the comparator edge acyclic by admitting only an exact two-object - // book and recording immutable incarnation ids before stable_sort. Every - // provenance predicate below is intentional: ordinary POOC, no COOF or - // magnifier scheduler, one prior-bar freshly recreated pure-stop ENTRY, one - // same-source-bar flat-born full non-trailing EXIT child with older broker - // priority, and no OCA grouping. - uint64_t retained_child_parent_first_incarnation = 0; - uint64_t retained_child_later_incarnation = 0; - int64_t retained_child_parent_effective_seq = 0; - int64_t retained_child_later_effective_seq = 0; - if (pending_orders_.size() == 2) { - const PendingOrder* parent = nullptr; - const PendingOrder* child = nullptr; - for (const PendingOrder& order : pending_orders_) { - if (order.type == OrderType::ENTRY) { - parent = ℴ - } else if (order.type == OrderType::EXIT) { - child = ℴ - } - } - const RetainedChildFreshParentOrderContext context{ - flat_retained_child_fresh_parent_order_, - position_side_ == PositionSide::FLAT, - process_orders_on_close_, - calc_on_order_fills_, - coof_scheduler_active_, - bar_magnifier_enabled_, - stream_warmup_mode_, - stream_phase_ == StreamPhase::IDLE, - pending_orders_.size(), - bar_index_, - }; - if (retained_child_fresh_parent_order_pair( - context, parent, child)) { - retained_child_parent_first_incarnation = parent->incarnation; - retained_child_later_incarnation = child->incarnation; - retained_child_parent_effective_seq = child->created_seq; - retained_child_later_effective_seq = parent->created_seq; - } - } + // The Pine frontend alone selects source-shape priority policy. Snapshot + // an identity-bound value before sorting; the core only applies its keys. + const auto priority_decision = pine_order_priority_.select({ + position_side_ == PositionSide::FLAT, process_orders_on_close_, + calc_on_order_fills_, coof_scheduler_active_, bar_magnifier_enabled_, + stream_warmup_mode_, stream_phase_ == StreamPhase::IDLE, bar_index_, + }, pending_orders_); // A single relative strategy.exit armed while FLAT has no concrete // stop/limit until its earlier same-bar from_entry parent fills. It // otherwise looks like a phase-0 market order and sorts before a non-gap @@ -4250,19 +4209,11 @@ void BacktestEngine::sort_orders_by_fill_phase(const Bar& bar) { if (ga != gb) return ga < gb; } } - if (retained_child_parent_first_incarnation != 0) { - auto effective_seq = [&](const PendingOrder& order) { - if (order.incarnation - == retained_child_parent_first_incarnation) { - return retained_child_parent_effective_seq; - } - if (order.incarnation == retained_child_later_incarnation) { - return retained_child_later_effective_seq; - } - return order.created_seq; - }; - const int64_t a_seq = effective_seq(a); - const int64_t b_seq = effective_seq(b); + if (priority_decision) { + const int64_t a_seq = priority_decision->sequence( + a.incarnation, a.created_seq); + const int64_t b_seq = priority_decision->sequence( + b.incarnation, b.created_seq); if (a_seq != b_seq) return a_seq < b_seq; } auto is_entry_same_as_current_position = [&](const PendingOrder& o) { diff --git a/src/engine_internal.hpp b/src/engine_internal.hpp index 20f9547..4f41914 100644 --- a/src/engine_internal.hpp +++ b/src/engine_internal.hpp @@ -64,105 +64,6 @@ inline constexpr double kPathTimeEps = 1e-12; // function-local copy. One definition, so the predicate cannot drift. inline const std::string kClosePrefix = "__close__"; -struct RetainedChildFreshParentOrderContext { - bool enabled = false; - bool broker_flat = false; - bool process_orders_on_close = false; - bool calc_on_order_fills = false; - bool coof_scheduler_active = false; - bool bar_magnifier_enabled = false; - bool stream_warmup_mode = false; - bool stream_idle = false; - std::size_t pending_count = 0; - int bar_index = -1; -}; - -// Exact lifecycle predicate for the ordinary-POOC ordering exception where a -// retained strategy.exit child has an older broker slot than the pure-STOP -// parent explicitly cancelled and freshly recreated in the same evaluation. -// Keeping the full predicate runtime-private and side-effect-free makes every -// exclusion independently unit-testable; the comparator only swaps two -// immutable scalar sequence keys after this returns true. -inline bool retained_child_fresh_parent_order_pair( - const RetainedChildFreshParentOrderContext& ctx, - const PendingOrder* parent, - const PendingOrder* child) { - if (!ctx.enabled - || !ctx.broker_flat - || !ctx.process_orders_on_close - || ctx.calc_on_order_fills - || ctx.coof_scheduler_active - || ctx.bar_magnifier_enabled - || ctx.stream_warmup_mode - || !ctx.stream_idle - || ctx.pending_count != 2 - || parent == nullptr - || child == nullptr) { - return false; - } - - const uint64_t cancelled_incarnation = - parent->recreated_after_named_cancelled_entry_incarnation; - const uint64_t surviving_exit_incarnation = - parent->named_cancel_surviving_exit_incarnation; - const bool parent_is_exact_fresh_stop = - parent->type == OrderType::ENTRY - && parent->created_position_side == PositionSide::FLAT - && !parent->created_by_same_id_replacement - && cancelled_incarnation != 0 - && cancelled_incarnation < parent->incarnation - && cancelled_incarnation != child->incarnation - && surviving_exit_incarnation > cancelled_incarnation - && surviving_exit_incarnation < parent->incarnation - && parent->created_bar == ctx.bar_index - 1 - && std::isnan(parent->qty) - && !parent->created_during_coof_recalc - && !parent->created_after_position_close_in_bar - && !parent->over_pyramiding_cap_at_placement - && !parent->stop_limit_activated - && std::isfinite(parent->stop_price) - && std::isnan(parent->limit_price) - && std::isnan(parent->trail_points) - && std::isnan(parent->trail_price) - && std::isnan(parent->trail_offset) - && parent->oca_name.empty() - && parent->oca_type == 0; - const double child_qp = std::isnan(child->qty_percent) - ? 100.0 : child->qty_percent; - const bool child_is_exact_retained_bracket = - child->type == OrderType::EXIT - && !child->from_entry.empty() - && child->created_by_same_id_replacement - && child->replaced_exit_order_incarnation - == surviving_exit_incarnation - && !child->created_while_in_position - && child->created_position_side == PositionSide::FLAT - && child->created_bar == ctx.bar_index - 1 - && !child->created_during_coof_recalc - && !child->created_after_position_close_in_bar - && !child->requested_partial - && std::isnan(child->qty) - && child_qp >= 100.0 - kFullPercentEps - && std::isfinite(child->stop_price) - && std::isfinite(child->limit_price) - && std::isnan(child->profit_ticks) - && std::isnan(child->loss_ticks) - && std::isnan(child->trail_points) - && std::isnan(child->trail_price) - && std::isnan(child->trail_offset) - && child->oca_name.empty() - && child->oca_type == 0; - return parent_is_exact_fresh_stop - && child_is_exact_retained_bracket - && child->from_entry == parent->id - && child->created_seq < parent->created_seq - && child->incarnation != 0 - && parent->incarnation != 0 - && parent->incarnation - < std::numeric_limits::max() - && child->incarnation == parent->incarnation + 1; -} - // Among flat pending opposite ENTRY stop-only orders, which stop price is touched // first on the synthesized OHLC path (exactly one long + one short, both touched). // Forward-declared in with the same underlying type diff --git a/src/engine_state_hash.cpp b/src/engine_state_hash.cpp index a9fe8f6..1834222 100644 --- a/src/engine_state_hash.cpp +++ b/src/engine_state_hash.cpp @@ -86,9 +86,9 @@ void hash_str_set(Fnv& f, const std::unordered_set& s) { uint64_t BacktestEngine::broker_state_hash() const { Fnv f; - // v3 owns cap configuration, quota/cause and generic close-request state. + // v4 adds explicit Pine order-priority attachment and configuration. // It is a serialization boundary, independent of the public C ABI version. - f.s("pineforge-broker-state/v3"); + f.s("pineforge-broker-state/v4"); // --- Position core --- f.i(static_cast(position_side_)); @@ -294,6 +294,12 @@ uint64_t BacktestEngine::broker_state_hash() const { f.i(priced_entry_activity_bar_); f.b(priced_entry_filled_this_bar_); + // Explicit priority policy is future-behavioral configuration. No cached + // decision survives a broker boundary; hash semantic ownership only. + f.u(compat::pine::OrderPriority::schema_version); + f.b(pine_order_priority_.attached()); + f.b(pine_order_priority_.retained_parent_first()); + // --- Pine compatibility policy and its day-owned quota --- f.u(compat::pine::IntradayCap::schema_version); f.i(static_cast(max_intraday_filled_orders_.attachment())); diff --git a/src/engine_stream.cpp b/src/engine_stream.cpp index a64830d..a3ca41f 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(3); integer(broker_state_hash()); + integer(4); 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/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4cb4f92..b5b72d6 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -406,3 +406,8 @@ if(PINEFORGE_ENABLE_COVERAGE) target_compile_options(test_c_abi PRIVATE ${_pf_cov_test_flags}) endif() add_test(NAME test_c_abi COMMAND test_c_abi) + +# Run the same literal lifetime/OCA/callback and financial assertions with +# explicit Pine attachment as well as the unchanged bare-native test above. +add_test(NAME test_pending_order_identity_pine + COMMAND test_pending_order_identity --pine) diff --git a/tests/fixtures/script_cpp_abi/basef864/README.md b/tests/fixtures/script_cpp_abi/basef864/README.md new file mode 100644 index 0000000..5c39142 --- /dev/null +++ b/tests/fixtures/script_cpp_abi/basef864/README.md @@ -0,0 +1 @@ +Frozen public header closure of engine f864be590931ba08c8df5af983b33b2c29be9c67 (cap-only explicit attachment, C++ namespace v3). Digests authenticate the compile/link-only stale-layout controls. No runtime is archived or executed; version.h is generated by the matching check build. diff --git a/tests/fixtures/script_cpp_abi/basef864/headers.json.gz b/tests/fixtures/script_cpp_abi/basef864/headers.json.gz new file mode 100644 index 0000000..a6febe3 Binary files /dev/null and b/tests/fixtures/script_cpp_abi/basef864/headers.json.gz differ diff --git a/tests/fixtures/script_cpp_abi/basef864/manifest.json b/tests/fixtures/script_cpp_abi/basef864/manifest.json new file mode 100644 index 0000000..02ffb29 --- /dev/null +++ b/tests/fixtures/script_cpp_abi/basef864/manifest.json @@ -0,0 +1,61 @@ +{ + "source_commit": "f864be590931ba08c8df5af983b33b2c29be9c67", + "internal_namespace": "engine_script_run_v3", + "archive": "headers.json.gz", + "archive_sha256": "0aeded64e9b04de2ca743593f86ae80de01324af2407efd6c0c4afc92f24f019", + "generated_dependency": "pineforge/version.h is supplied by --generated-include.", + "files": { + "pineforge/bar.hpp": { + "sha256": "665c2528dca64ac9474de622dfd7ffdb540ff91316f70317e39aada158d1e0de", + "git_blob": "836f73daab382e9c8d5a6d03a942a921e39b756f" + }, + "pineforge/broker_events.hpp": { + "sha256": "4f3d9948a4d1604750901fa3982d6ea49bd50dd15a86c4e5d690b83d1f5f23c0", + "git_blob": "2972d53c7b039af266f4da161301c01eec54c16d" + }, + "pineforge/compat/pine/intraday_cap.hpp": { + "sha256": "0ac0eab34625ab9a974037ff8c0217603a01deb81599350a4b24f7bf48ac0cbd", + "git_blob": "0aba76b6a78251df33e92f19290473adecf5a61e" + }, + "pineforge/compat/pine/intraday_order_budget.hpp": { + "sha256": "43f7700f9c66c630ab8a9d8fd899eecb03c2fd9aabff11d2dc2b7fa85f92981b", + "git_blob": "730d941fffa3939e375489f40a50901ccea9ec87" + }, + "pineforge/engine.hpp": { + "sha256": "54b35fffaa163a31467f8ba883e44e02f013a37216b558dfe3a4f28fbbe84dc2", + "git_blob": "35af69a175cbc65f68cc58ddbfa52fe82f502781" + }, + "pineforge/magnifier.hpp": { + "sha256": "c0ed220b6df054b44596a434d4f48b1c3e960ca329bb8644a405a608175e06ff", + "git_blob": "1bbc910a869d162900d7610fa0bd9a88776b7b0f" + }, + "pineforge/na.hpp": { + "sha256": "0e8c256fcdeda94a0855458fa3b8c4ad4f9864dc2aecce969934f6bea82fd0a5", + "git_blob": "e98505aadc68a09cbe5a14aacf591a799a15ecd2" + }, + "pineforge/pending_order_mirror.hpp": { + "sha256": "cf4f756ce681dfec396918be6bc03d25e8ed5b1dbf9b2515c40af4c2b3df5236", + "git_blob": "ce672052f43add9d5965e7655c116f7874bbc8f7" + }, + "pineforge/pineforge.h": { + "sha256": "1e0def60e31f529dcb821502c0f7104b473c06be0fa1df713be0f408e04e9288", + "git_blob": "232cd6c66d3f6e6ffdcffdf246e6a9dcce3ef3f8" + }, + "pineforge/position_close_obligation.hpp": { + "sha256": "6f6ad0aaca952dda7a9386cb78e57705b3494364dbd8970b0a051fe99d05b777", + "git_blob": "09576ee0f2a1d49becc46f5701f4369cf8f87bf6" + }, + "pineforge/series.hpp": { + "sha256": "e7709f67df9046c9eba163c2dbe91f8161084a955265ccf6b163355856c0afde", + "git_blob": "61baaef2c351d37b0d6a5eb179e91ce670eb83c3" + }, + "pineforge/session_time.hpp": { + "sha256": "f58c94bfae948e08fa21e52f20eae3940b31e398d83b800baf3a31f3a5fde669", + "git_blob": "af3a4472d852e0965947c006d0b6ad85304539e7" + }, + "pineforge/timeframe.hpp": { + "sha256": "5e4c312da463fdcff55e0887188523c8bbf85a2ae62f3317a2b23f22fe0161e2", + "git_blob": "3d1bc9d495ac7e5b39ea5bae57c9d9afe580192b" + } + } +} diff --git a/tests/test_pending_order_identity.cpp b/tests/test_pending_order_identity.cpp index 704e744..b554897 100644 --- a/tests/test_pending_order_identity.cpp +++ b/tests/test_pending_order_identity.cpp @@ -12,6 +12,7 @@ using namespace pineforge; namespace { +bool pine_fixture_attachment = false; const double missing = std::numeric_limits::quiet_NaN(); #define REQUIRE(x) do { if (!(x)) throw std::runtime_error( \ std::string(__func__) + ":" + std::to_string(__LINE__) + ": " #x); } while (false) @@ -62,6 +63,11 @@ class Book final : public BacktestEngine { public: using BacktestEngine::open_trade_entry_id; Book() { +#if defined(PINEFORGE_HAS_EXPLICIT_PINE_EXECUTION_ADAPTER_V1) + if (pine_fixture_attachment) attach_pine_execution_adapter(); +#else + if (pine_fixture_attachment) throw std::runtime_error("Pine attachment unavailable"); +#endif initial_capital_ = 100000; pyramiding_ = 10; margin_long_ = margin_short_ = 0; @@ -408,6 +414,11 @@ class CallbackBook final : public BacktestEngine { int observed_close_callbacks = 0; uint64_t replaced = 0, fresh = 0; CallbackBook() { +#if defined(PINEFORGE_HAS_EXPLICIT_PINE_EXECUTION_ADAPTER_V1) + if (pine_fixture_attachment) attach_pine_execution_adapter(); +#else + if (pine_fixture_attachment) throw std::runtime_error("Pine attachment unavailable"); +#endif initial_capital_ = 100000; pyramiding_ = 10; margin_long_ = margin_short_ = 0; @@ -453,7 +464,8 @@ void actual_coof_callback() { int main(int argc, char** argv) { int failed = 0, passed = 0; - const std::string filter = argc > 1 ? argv[1] : ""; + pine_fixture_attachment = argc > 1 && std::string(argv[1]) == "--pine"; + const std::string filter = argc > 1 && !pine_fixture_attachment ? argv[1] : ""; auto check = [&](const std::string& name, const std::function& body) { if (!filter.empty() && name.find(filter) == std::string::npos) return; try { body(); ++passed; std::cout << "PASS " << name << '\n'; } diff --git a/tests/test_prearmed_exit_path_cursor.cpp b/tests/test_prearmed_exit_path_cursor.cpp index 4e6371c..977c7dc 100644 --- a/tests/test_prearmed_exit_path_cursor.cpp +++ b/tests/test_prearmed_exit_path_cursor.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include @@ -82,14 +83,21 @@ enum class BookVariant { PostCancelDoubleReissue, MissingParentCancel, InterleavedThird, + InterleavedFourth, + IncarnationGap, + ChildOca, + SharedOca, MultipleChildren, }; class FreshParentProbe final : public BacktestEngine { public: FreshParentProbe(Cell cell, int parent_first_factor, - BookVariant variant = BookVariant::ExactPair) + BookVariant variant = BookVariant::ExactPair, + bool pine_attachment = true) : cell_(cell), variant_(variant) { + // Legacy Pine numeric assertions require an explicit frontend opt-in. + if (pine_attachment) attach_pine_execution_adapter(); initial_capital_ = 1'000'000.0; default_qty_type_ = QtyType::FIXED; default_qty_value_ = 1.0; @@ -113,17 +121,24 @@ class FreshParentProbe final : public BacktestEngine { if (bar_index_ == 0) { strategy_entry("E", is_long, kNaN, is_long ? 130.0 : 70.0, - kNaN, "original parent"); + kNaN, "original parent", + variant_ == BookVariant::SharedOca ? "G" : "", + variant_ == BookVariant::SharedOca ? 1 : 0); if (variant_ != BookVariant::FreshChild && variant_ != BookVariant::PostCancelDoubleReissue) { strategy_exit("X", "E", child_limit, child_stop, kNaN, kNaN, kNaN, 100.0, "retained child"); } - if (variant_ == BookVariant::InterleavedThird) { + if (variant_ == BookVariant::InterleavedThird + || variant_ == BookVariant::InterleavedFourth) { strategy_entry("U", is_long, kNaN, is_long ? 1'000.0 : 1.0, 1.0, "unrelated resting parent"); + if (variant_ == BookVariant::InterleavedFourth) + strategy_entry("V", is_long, kNaN, + is_long ? 2'000.0 : 0.5, + 1.0, "fourth unrelated parent"); } else if (variant_ == BookVariant::MultipleChildren) { strategy_exit("Y", "E", child_limit, child_stop, kNaN, kNaN, kNaN, 100.0, @@ -177,10 +192,19 @@ class FreshParentProbe final : public BacktestEngine { } else { strategy_entry("E", is_long, kNaN, is_long ? 110.0 : 90.0, - kNaN, "fresh parent"); + kNaN, "fresh parent", + variant_ == BookVariant::SharedOca ? "G" : "", + variant_ == BookVariant::SharedOca ? 1 : 0); + if (variant_ == BookVariant::IncarnationGap) { + strategy_entry("U", is_long, kNaN, + is_long ? 1'000.0 : 1.0, 1.0); + strategy_cancel("U"); + } strategy_exit("X", "E", child_limit, child_stop, kNaN, kNaN, kNaN, 100.0, - "retained child"); + "retained child", kNaN, + variant_ == BookVariant::ChildOca + || variant_ == BookVariant::SharedOca ? "G" : ""); } if (variant_ == BookVariant::MultipleChildren) { strategy_exit("Y", "E", child_limit, child_stop, @@ -236,6 +260,13 @@ class FreshParentProbe final : public BacktestEngine { } } + bool priority_attached() const { return pine_order_priority_.attached(); } + bool priority_enabled() const { return pine_order_priority_.retained_parent_first(); } + bool cap_attached() const { + return max_intraday_filled_orders_.attachment() != compat::pine::CapAttachment::None; + } + uint64_t fills() const { return broker_fill_event_seq_; } + double position() const { return signed_position_size(); } bool fresh_parent_shape_seen = false; bool parent_cancel_provenance_seen = false; bool parent_cancel_token_exact = false; @@ -351,8 +382,9 @@ enum class SortMutation { }; static bool retained_child_predicate_accepts(SortMutation mutation) { - internal::RetainedChildFreshParentOrderContext context{ - true, // enabled + compat::pine::OrderPriority policy; + policy.attach(); + compat::pine::OrderPriorityContext context{ true, // broker flat true, // POOC false, // COOF option @@ -361,7 +393,6 @@ static bool retained_child_predicate_accepts(SortMutation mutation) { false, // stream warmup true, // stream idle 2, - 2, }; PendingOrder child{}; @@ -404,7 +435,7 @@ static bool retained_child_predicate_accepts(SortMutation mutation) { case SortMutation::ExactDefaultOn: break; case SortMutation::FactorOff: - context.enabled = false; + policy.metadata("flat_retained_child_fresh_parent_order", 0.0); break; case SortMutation::BrokerLive: context.broker_flat = false; @@ -525,8 +556,7 @@ static bool retained_child_predicate_accepts(SortMutation mutation) { parent.created_seq = 1; break; } - return internal::retained_child_fresh_parent_order_pair( - context, &parent, &child); + return policy.select(context, {parent, child}).has_value(); } static Bar bar(double o, double h, double l, double c, int64_t ts) { @@ -749,6 +779,100 @@ static void check_cancel_token_scope() { CHECK(probe.later_parent_has_no_token); } +// Public command controls distinguish policy extraction from native activation. +static void check_explicit_attachment_boundary() { + const Bar bars[] = { + bar(100,101,99,100,900000), bar(100,105,95,100,1800000), + bar(100,115,80,105,2700000), bar(105,108,85,95,3600000), + }; + const char* key = "flat_retained_child_fresh_parent_order"; + const double values[] = {0.0, -0.0, -1.0, kNaN, + std::numeric_limits::infinity(), + -std::numeric_limits::infinity(), 0.5, 1.0}; + for (double value : values) { + for (bool attached : {false, true}) { + FreshParentProbe probe(Cell::LongPost, -1, BookVariant::ExactPair, attached); + // Exercise the actual base/C transport, not a derived shadow setter. + strategy_set_syminfo_metadata(static_cast(&probe), key, value); + if (!attached) probe.enable_pine_intraday_cap(); // cap-only is not execution attachment + probe.run(bars, 4); + CHECK(probe.priority_attached() == attached); + CHECK(probe.cap_attached()); + const bool enabled = std::isfinite(value) && value > 0.0; + CHECK(probe.priority_enabled() == enabled); + CHECK(near(probe.position_seen_on_trigger_bar, attached && enabled ? 0 : 1)); + CHECK(probe.trade_count() == 1); + CHECK(probe.fills() == 2); + if (probe.trade_count() == 1) { + CHECK(near(probe.get_trade(0).entry_price, 110)); + CHECK(near(probe.get_trade(0).exit_price, 90)); + CHECK(probe.get_trade(0).exit_bar_index == 2); + } + } + } + FreshParentProbe bare(Cell::LongPost, -1, BookVariant::ExactPair, false); + CHECK(!bare.cap_attached()); + CHECK(!bare.priority_attached()); + bare.run(bars, 4); + CHECK(near(bare.position_seen_on_trigger_bar, 1)); + CHECK(bare.trade_count() == 1 && bare.fills() == 2); + + for (BookVariant variant : {BookVariant::InterleavedFourth, + BookVariant::IncarnationGap, BookVariant::ChildOca, BookVariant::SharedOca}) { + FreshParentProbe probe(Cell::LongPost, -1, variant); + probe.run(bars, 4); + CHECK(probe.priority_attached()); + CHECK(near(probe.position_seen_on_trigger_bar, 1)); + CHECK(probe.pending_book_size_on_reissue == + (variant == BookVariant::InterleavedFourth ? 4u : 2u)); + CHECK(probe.parent_then_child_incarnations == (variant != BookVariant::IncarnationGap)); + if (variant == BookVariant::SharedOca) { + CHECK(probe.trade_count() == 0 && probe.fills() == 1); + CHECK(near(probe.position(), 1)); + } else { + CHECK(probe.trade_count() == 1 && probe.fills() == 2); + if (probe.trade_count() == 1) { + CHECK(near(probe.get_trade(0).entry_price, 110)); + CHECK(near(probe.get_trade(0).exit_price, 90)); + } + } + } + + FreshParentProbe source(Cell::LongPost, -1, BookVariant::ExactPair, false); + const auto native_hash = source.broker_state_hash(); + source.enable_pine_intraday_cap(); + const auto cap_hash = source.broker_state_hash(); + source.attach_pine_execution_adapter(); + CHECK(native_hash != cap_hash); + CHECK(cap_hash != source.broker_state_hash()); // priority attachment has its own hash + const auto attached_hash = source.broker_state_hash(); + source.set_syminfo_metadata(key, 0.0); + CHECK(attached_hash != source.broker_state_hash()); + source.attach_pine_execution_adapter(); // idempotent, must preserve off + CHECK(!source.priority_enabled()); + source.run(bars, 4); + auto copy = source; + CHECK(copy.broker_state_hash() == source.broker_state_hash()); + copy.run(nullptr, 0); + CHECK(copy.priority_attached() && !copy.priority_enabled()); + copy.set_syminfo_metadata(key, 1.0); + CHECK(!source.priority_enabled()); + copy.run(bars, 4); + CHECK(near(copy.position_seen_on_trigger_bar, 0)); + CHECK(near(source.position_seen_on_trigger_bar, 1)); + + FreshParentProbe delayed(Cell::LongPost, 0, BookVariant::ExactPair, false); + delayed.attach_pine_execution_adapter(); // previously supplied off stays off + CHECK(!delayed.priority_enabled()); + delayed.run(bars, 4); + CHECK(near(delayed.position_seen_on_trigger_bar, 1)); + + const broker::OrderPriorityDecision decision{{{{11, 2}, {12, 3}}}}; + CHECK(decision.sequence(11, 3) == 2); + CHECK(decision.sequence(12, 2) == 3); + CHECK(decision.sequence(13, 7) == 7); // replacement cannot inherit by priority +} + int main() { std::printf("pre-armed from_entry bracket path cursor\n"); check_cell(Cell::LongPre, true, true); @@ -786,6 +910,7 @@ int main() { std::printf("exact scope guards and default-on behavior\n"); check_sort_scope_guards(); check_cancel_token_scope(); + check_explicit_attachment_boundary(); std::printf("\n%d passed, %d failed\n", tests_passed, tests_failed); return tests_failed == 0 ? 0 : 1;