Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ endif()
add_library(pineforge STATIC
src/c_abi.cpp
src/compat/pine/order_birth.cpp
src/compat/pine/exit_activation.cpp
src/compat/pine/order_priority.cpp
src/engine_aux_security.cpp
src/engine_fills.cpp
Expand Down
26 changes: 14 additions & 12 deletions docs/pages/abi-stability.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,32 +112,34 @@ 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 integrated quantity, predecessor and order-birth representation
changes `PendingOrder` and uses internal `engine_script_run_v5`. Exact frozen
c45/v4 headers are compiled before link tests: both native and generated-style
v4 callers must fail to link to the v5 archive, while matched v5 callers link.
Earlier exact base38/v2 and f864/v3 mismatch controls remain. A compile failure
cannot masquerade as mismatch protection; no pairing executable is run.
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<PendingOrder>` 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.

`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 108 existing field names/types/offsets and its full old prefix;
mirror keeps all 128 shipped149 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 v5 module.
its own matching runtime; this check does not turn it into a v6 module.

The integrated representation advances the broker fingerprint domain to
`pineforge-broker-state/v5` and stream fingerprint version to 5. These identify
changed serialized quantity/reservation, predecessor and birth facts, alongside
existing Pine policy state. The Pine component schema remains 1; it is
independent of the aggregate fingerprint version. Prior v2/v3/v4 fingerprints are
`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
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
Expand Down
60 changes: 60 additions & 0 deletions docs/pages/exit-leg-activation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
Exit stop and limit matching now consumes concrete activation bounds attached
to a position-cycle identity. Each bound names the earliest script bar where
that leg may be evaluated for that owner. A mismatch cannot reuse another
cycle's constraint; an unbound value imposes no lower-bound constraint.
This is one exposure-cycle scope, not a physical lot or `from_entry` claim.
Existing quantity/entry ownership and other path/trailing rules remain separate.

The lower bounds also apply when a dormant bracket is revived after a margin
partial and when a prearmed bracket selects a stop or limit at the open.
Revival can restore the bracket without executing an unready stop. Open-gap
selection checks each leg before applying stop/limit precedence, so an eligible
sibling retains its own execution and slippage semantics. Risk liquidation
itself is independent of a bracket's activation time.
The historical chart-extreme rounding fallback checks the same per-leg bounds;
a normal no-fill result cannot become an early fill through that fallback.

The transitions are connected to the current book. New or replaced EXIT
construction binds against live exposure. Both fresh-position producers bind
retained exits after establishing the new cycle, before later matching or
callbacks. Going flat clears bindings while retained policy evidence stays
with the order. Partial reductions and same-cycle adds do not refresh a
deadline. Order replacement creates fresh placement evidence and activation;
cancellation and OCA erasure remove the values with their owning order.

For Pine compatibility, immutable `ExitPlacementEvidence` retains the original
callback marketability inputs: placement cycle/entry bar, physical exposure
direction, cursor price, raw stop/limit levels and any named limit continuation.
Current trigger fields can be neutralized or modified without rewriting those
facts. `LaterSameOpen` and `FirstHighRecross` continuations remain explicit
Pine exceptions; their existing predicates are evaluated in the adapter.
Their recorded observed fill sequence is the sequence visible at policy
selection; the separate `OrderBirth` still owns the actual callback interval.

At binding, the adapter resolves the retained original decision to concrete
bounds for the new owner. A held leg receives `owner_entry_bar + 1`; the other
leg receives `owner_entry_bar`. The arithmetic uses a wider bar coordinate.
The matcher never refreshes these numbers from the current position. In
particular a surviving same-ID exit can bind a new cycle and receive another
hold, even though its original birth-bar deadline has already passed. New
marketability is not recalculated from the replacement owner's opposite side.

The two native `coof_suppress_*_on_entry_bar` booleans are removed. Their old
C mirror fields are read-only projections from the Pine evidence; no native
decision reads those outputs. All 128 pre-existing mirror fields and the full
old byte prefix remain unchanged, and new activation/evidence fields append.
Every new owner, coordinate, evidence value and optional-presence discriminator
is hashed and mirrored. There is no side map or second pending-order book.

This slice moves the two decisions out of the matching masks, but does not
eliminate the Pine policies or detach their producer from existing native
callback use. The Pine adapter is still invoked by the current exit producer.
`PineHistoricalBirthReach`, cascade leg/gap permissions, dormant-leg revival and
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
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.
95 changes: 95 additions & 0 deletions include/pineforge/compat/pine/exit_activation.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#pragma once
#include "../../bar.hpp"
#include "../../leg_activation.hpp"
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <string>

namespace pineforge {
enum class PositionSide;
inline namespace engine_script_run_v6 { struct PendingOrder; }
}
namespace pineforge::compat::pine {

enum class LimitContinuationCause : int32_t { LaterSameOpen, FirstHighRecross };
struct LimitContinuation {
LimitContinuationCause cause;
// Sequence observed when policy was selected; callback cause remains the
// independent immutable OrderBirth interval on the order.
uint64_t observed_fill_sequence;
};

// Immutable original policy evidence. Rebinding must not recompute original
// marketability from the new owner's side or a later-mutated trigger level.
struct ExitPlacementEvidence {
int64_t position_cycle;
int entry_bar;
int direction;
double cursor_price;
double stop_level;
double limit_level;
std::optional<LimitContinuation> limit_continuation;
};

class ExitActivationPolicy {
public:
ExitActivationPolicy() = default;
explicit ExitActivationPolicy(ExitPlacementEvidence evidence) : evidence_(evidence) {
if (evidence.position_cycle <= 0 || evidence.entry_bar < 0
|| (evidence.direction != 1 && evidence.direction != -1)
|| !std::isfinite(evidence.cursor_price))
throw std::invalid_argument("invalid Pine exit placement evidence");
}
const std::optional<ExitPlacementEvidence>& evidence() const { return evidence_; }
bool holds_stop() const;
bool holds_limit() const;
bool continues_at_later_open() const;
ExitLegActivationBounds resolve(int64_t owner_cycle, int owner_entry_bar) const;
private:
std::optional<ExitPlacementEvidence> evidence_;
};

// Transient producer facts, never retained as a parallel mutable mode bag.
struct ExitActivationContext {
const Bar& bar;
PositionSide side;
int64_t cycle;
int bar_index;
int position_open_bar;
int position_entry_count;
double position_quantity;
int pyramiding;
std::size_t lot_count;
const std::string& first_lot_id;
uint64_t first_lot_incarnation;
bool fill_recalc;
bool scheduler;
double cursor_price;
bool after_first_open_fill;
int recalc_leg;
bool historical_segment;
bool at_extreme;
int historical_point;
uint64_t market_recalc_incarnation;
uint64_t market_recalc_fill;
uint64_t current_fill;
bool magnifier;
bool process_on_close;
bool warmup;
bool stream_idle;
bool pending_empty;
int slippage;
double pointvalue;
double account_fx;
bool fx_series_empty;
double tick_high;
};

ExitActivationPolicy select_exit_activation(const PendingOrder& order,
double requested_stop, double requested_limit, const ExitActivationContext& context);

} // namespace pineforge::compat::pine

namespace pineforge { using PineExitActivationPolicy = compat::pine::ExitActivationPolicy; }
2 changes: 1 addition & 1 deletion include/pineforge/compat/pine/order_birth.hpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#pragma once
#include "../../order_birth.hpp"

namespace pineforge { inline namespace engine_script_run_v5 { struct PendingOrder; } }
namespace pineforge { inline namespace engine_script_run_v6 { struct PendingOrder; } }
namespace pineforge::compat::pine {

// Historical Pine permissions remain policy, not physical birth facts.
Expand Down
2 changes: 1 addition & 1 deletion include/pineforge/compat/pine/order_priority.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
#include <string>
#include <vector>

namespace pineforge { inline namespace engine_script_run_v5 { struct PendingOrder; } }
namespace pineforge { inline namespace engine_script_run_v6 { struct PendingOrder; } }
namespace pineforge::compat::pine {

struct OrderPriorityContext {
Expand Down
27 changes: 14 additions & 13 deletions include/pineforge/engine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
#include "bar.hpp"
#include "broker_events.hpp"
#include "quantity_intent.hpp"
#include "leg_activation.hpp"
#include "compat/pine/exit_activation.hpp"
#include "order_birth.hpp"
#include "compat/pine/order_birth.hpp"
#include "compat/pine/intraday_cap.hpp"
Expand Down Expand Up @@ -404,7 +406,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_v5 {
inline namespace engine_script_run_v6 {
struct PendingOrder {
std::string id;
std::string from_entry; // for exit orders
Expand Down Expand Up @@ -459,14 +461,10 @@ struct PendingOrder {
// fires, later bars—and later COOF scheduler segments on the same bar—
// evaluate only the live limit leg until the order fills or is replaced.
bool stop_limit_activated = false;
// A stop/limit leg emitted by a COOF recalc on the position's entry bar
// cannot consume the fill cursor that caused that recalc when the leg is
// already marketable there. Suppression is deliberately per-leg: the
// other, correctly-sided bracket leg remains live on the remaining path.
// Both bits expire automatically once bar_index_ advances, so an unfilled
// suppressed leg carries into the next bar as an ordinary order.
bool coof_suppress_stop_on_entry_bar = false;
bool coof_suppress_limit_on_entry_bar = false;
// Concrete activation bounds belong to the currently bound exposure cycle.
// Pine placement evidence is retained separately for explicit rebinding.
ExitLegActivation leg_activation;
PineExitActivationPolicy pine_exit_activation;
// Immutable evaluation/fill origin, captured once for this incarnation.
// Historical extreme-only/trailing permissions live in compat::pine.
OrderBirth birth;
Expand Down Expand Up @@ -1035,7 +1033,7 @@ struct PendingOrder {
ShortSeedCollisionRole::NONE;
};

} // inline namespace engine_script_run_v5 (PendingOrder)
} // inline namespace engine_script_run_v6 (PendingOrder)

// default_qty_type constants (matches TradingView)
enum class QtyType { FIXED = 0, PERCENT_OF_EQUITY = 1, CASH = 2 };
Expand Down Expand Up @@ -1090,10 +1088,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.
// v5 integrates typed quantity, replacement identity and causal order birth.
// 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_v5 {
inline namespace engine_script_run_v6 {
class BacktestEngine {
protected:
// --- Position state ---
Expand Down Expand Up @@ -4321,6 +4319,9 @@ class BacktestEngine {
double fill_price, double explicit_qty,
int explicit_qty_type,
uint64_t entry_incarnation);
void bind_exit_activation(PendingOrder& order);
void bind_retained_exit_activations();
void unbind_exit_activations();
void open_fresh_position(PositionSide requested, double fill_price,
double qty, const std::string& id,
uint64_t entry_incarnation);
Expand Down Expand Up @@ -5128,5 +5129,5 @@ class BacktestEngine {
void trace(const std::string& name, int value) { trace(name, static_cast<double>(value)); }
};

} // inline namespace engine_script_run_v5
} // inline namespace engine_script_run_v6
} // namespace pineforge
37 changes: 37 additions & 0 deletions include/pineforge/leg_activation.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#pragma once
#include <cstdint>
#include <optional>
#include <stdexcept>

namespace pineforge {

// Concrete lower bounds resolved when an exit binds an exposure cycle. Other
// path/price/lifetime constraints remain independent of this lower bound.
struct ExitLegActivationBounds {
int64_t position_cycle;
int64_t stop_first_bar;
int64_t limit_first_bar;
};

class ExitLegActivation {
public:
void bind(ExitLegActivationBounds bounds) {
if (bounds.position_cycle <= 0 || bounds.stop_first_bar < 0 || bounds.limit_first_bar < 0)
throw std::invalid_argument("invalid exit-leg activation bounds");
bounds_ = bounds;
}
void unbind() { bounds_.reset(); }
const std::optional<ExitLegActivationBounds>& bounds() const { return bounds_; }
// An unbound value imposes no lower-bound constraint. Production EXIT
// producers bind on live exposure; exposure creation binds retained exits.
bool stop_ready(int64_t cycle, int64_t bar) const {
return !bounds_ || (bounds_->position_cycle == cycle && bar >= bounds_->stop_first_bar);
}
bool limit_ready(int64_t cycle, int64_t bar) const {
return !bounds_ || (bounds_->position_cycle == cycle && bar >= bounds_->limit_first_bar);
}
private:
std::optional<ExitLegActivationBounds> bounds_;
};

} // namespace pineforge
20 changes: 17 additions & 3 deletions include/pineforge/pending_order_mirror.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// GENERATED by scripts/gen_pending_order_mirror.py from include/pineforge/engine.hpp -- do not edit.
// 94 PendingOrder members mirrored (128 POD fields incl. struct_version/size).
// 94 PendingOrder members mirrored (142 POD fields incl. struct_version/size).
#pragma once
#include <stdint.h>

Expand Down Expand Up @@ -46,8 +46,8 @@ typedef struct pf_pending_order_v1_s {
uint64_t recreated_after_named_cancelled_entry_incarnation;
uint64_t named_cancel_surviving_exit_incarnation;
uint8_t stop_limit_activated;
uint8_t coof_suppress_stop_on_entry_bar;
uint8_t coof_suppress_limit_on_entry_bar;
uint8_t 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
Expand Down Expand Up @@ -140,6 +140,20 @@ typedef struct pf_pending_order_v1_s {
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
Expand Down
Loading
Loading