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
14 changes: 12 additions & 2 deletions docs/pages/abi-stability.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,14 +119,24 @@ Rebuild generated and native C++ objects against matching engine headers and
runtime. R4-D L1 advanced `BacktestEngine`, `NativeStrategyHost`, and the
private consumer to `engine_script_run_v17`; R5 L6 advances the same three to
`engine_script_run_v18` for the native higher-timeframe host surface
(`on_native_timeframe_bar`, `native_series_bar`), and the host capability macro
(`on_native_timeframe_bar`, `native_series_bar`), joined by R5 L4's margin
surface (`resolve_margin_call_units`, `on_native_margin_call`,
`native_liquidation_price`) and R5 L5's calculation-timing surface
(`on_native_recalculate`, `on_native_sub_bar`, `current_partial_bar`), and the
host capability macro
is `PINEFORGE_HAS_NATIVE_STRATEGY_HOST_V18`. L3b removes the source compatibility
order type; `pineforge-source-adapter/v2` hashes adapter and scheduler state
instead. Native request/core/event values are `native_order_v6`, the private
consumer identity is
`native-consumer/v7`, driver types are `native_driver_v5`, and run specs are
`native_run_spec_v3` (R5 L6 adds `NativeRunSpec::subscriptions`, folded into
the continuation hash only when it is non-empty).
the continuation hash only when it is non-empty; R5 L4 adds
`NativeRunSpec::margin`, folded only when it is set; R5 L5 adds `calculation`,
`max_recalculations_per_point` and `open_bar_view`, folded only once the
trigger or the open-bar view is non-default). R5 L4 also adds
`RequestDefinition::origin` and the `MarginCallEvent` alternative to
`native_order_v6`; `RequestOrigin::Host` — every host request — folds nothing,
so no established continuation hash moves.

| Matrix role | Internal identity |
| --- | --- |
Expand Down
279 changes: 269 additions & 10 deletions docs/pages/native-engine.md

Large diffs are not rendered by default.

100 changes: 100 additions & 0 deletions include/pineforge/native_host.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,19 @@ enum class NativePrecommitVerdict : std::uint8_t {
AdmitWithHostMargin = 2,
};

// Ephemeral factual view of one kernel-issued liquidation before its units
// are fixed. `position` is the physical book being liquidated, `mark` the
// sizing price the kernel measured the breach at, `equity` the marked equity
// there and `required` the maintenance requirement of the whole position at
// that same mark. A host that answers with a value owns the slice quantity.
struct NativeMarginCallView {
NativePhysicalPosition position;
double mark = 0.0;
double equity = 0.0;
double required = 0.0;
native_order::MatchCursor cursor;
};

struct NativeCurrentPointView {
NativeDecisionContext decision;
double price = 0.0;
Expand Down Expand Up @@ -447,6 +460,24 @@ struct NativeTimeframeBarContext {
std::int64_t delivered_at_ms = 0;
};

// Why the kernel is asking the host to calculate. BarClose is the script
// bar's own calculation and is delivered for every run, whatever the spec's
// NativeCalculationTrigger is: every calculation is routed through
// on_native_recalculate, whose default forwards to on_native_bar, so a host
// that only implements on_native_bar sees exactly what it saw before.
// OrderFill is one recalculation at the cursor of an applied execution
// (NativeCalculationTrigger::BarCloseAndFills and above) and carries that
// event as its cause. Tick is one recalculation at a modeled path point or
// an observed print (NativeCalculationTrigger::EveryModeledPoint). SubBar is
// reserved: a lower-timeframe sub-bar has its own hook, on_native_sub_bar,
// and is never delivered through on_native_recalculate.
enum class NativeCalculationReason : std::uint8_t {
BarClose = 0,
OrderFill = 1,
Tick = 2,
SubBar = 3,
};

// Most-derived native strategy host. Binds NativeExecutionConsumer in the
// protected engine constructor. Noncopyable and nonmovable. Lives in the
// same inline engine epoch as BacktestEngine so old-header/new-library
Expand Down Expand Up @@ -485,6 +516,43 @@ class NativeStrategyHost : public BacktestEngine {
// returns, before the consumer advances beyond this calculation point.
virtual void on_native_bar(const Bar& bar, const NativeDecisionContext& context) = 0;

// EVERY calculation of the run arrives here first, including the script
// bar's own close calculation (reason BarClose, cause nullptr), whose
// default forwarding keeps on_native_bar the complete contract for a host
// that never opts into another cadence.
//
// reason OrderFill: one recalculation at an applied execution's cursor,
// driven from the applied-notification drain after that event's
// on_native_applied and bounded by
// NativeRunSpec::max_recalculations_per_point. `cause` is that event and
// is valid only for this call. reason Tick: one recalculation at a
// modeled path point or an observed print, with a null cause.
//
// `bar` is the bar the calculation is about: the script bar under
// delivery in batch, the print's value bar for a stream Tick. It is the
// COMPLETE script bar even mid-path; current_partial_bar() is the
// lookahead-free bar so far at this cursor. Commands and
// execute_current are legal here exactly as in on_native_applied.
virtual void on_native_recalculate(const Bar& bar, const NativeDecisionContext& ctx,
NativeCalculationReason reason,
const native_order::ExecutionAppliedEvent* cause) {
(void)reason;
(void)cause;
on_native_bar(bar, ctx);
}

// One completed lower-timeframe sub-bar of an IntrabarPath::lower_tf
// path, delivered after that sub-bar's whole matching path and before the
// next sub-bar's. Never called for a run without a retained lower feed:
// a synthesized path and a plain confirmed bar have no sub-bars of their
// own. The decision point is the sub-bar's last modeled point, so
// commands and execute_current are legal and a request born here follows
// the ordinary birth rule.
virtual void on_native_sub_bar(const Bar& sub, const NativeDecisionContext& ctx) {
(void)sub;
(void)ctx;
}

virtual void on_native_applied(const native_order::ExecutionAppliedEvent&,
const NativeDecisionContext&) {}

Expand All @@ -498,6 +566,17 @@ class NativeStrategyHost : public BacktestEngine {
return NativePrecommitVerdict::Admit;
}

// The kernel's own liquidation sizing, offered to the host before the
// reduction rests. Returning nullopt keeps the run spec's sizing policy;
// a returned value is clamped into (0, held] and wins over it.
virtual std::optional<double> resolve_margin_call_units(
const NativeMarginCallView&) const {
return std::nullopt;
}
// A kernel-issued liquidation that filled. It is delivered after the
// ordinary on_native_applied for the same fill, with the same cursor.
virtual void on_native_margin_call(const native_order::MarginCallEvent&) {}

// RULING A48 — the ONE generic per-lot excursion capability. A host that
// returns true here takes ownership of every open lot's favorable/adverse
// excursion: the consumer stops sampling excursion at matched trigger
Expand All @@ -510,6 +589,22 @@ class NativeStrategyHost : public BacktestEngine {
return {};
}

// The bar so far at the current cursor, folded from the modeled points
// this script bar has already presented: open of its first point,
// running high/low, close at the cursor. Volume is the activity actually
// consumed so far — the completed lower-timeframe sub-bars of an
// intrabar path, or the prints of an observed stream — and stays 0 for a
// modeled path with no intrabar volume of its own. Valid in the bar-open,
// applied, tick, sub-bar and recalculation callbacks; nullopt outside a
// path walk, including in the bar's own close calculation, where the host
// already holds the complete bar.
std::optional<Bar> current_partial_bar() const;
// How many recalculations the kernel has driven this run, and how many it
// suppressed because a point had already spent its
// max_recalculations_per_point budget. Observation only.
std::uint64_t native_recalculation_count() const;
std::uint64_t native_recalculations_skipped() const;

std::optional<NativeCurrentPointView> current_execution_point() const;
std::optional<NativeTrailState> trail_state(
const native_order::RequestHandle& target) const;
Expand Down Expand Up @@ -548,6 +643,11 @@ class NativeStrategyHost : public BacktestEngine {

NativePhysicalPosition physical_position() const;
double native_marked_equity(double mark) const;
// The price at which the marked equity falls below the run's maintenance
// requirement for the live position's side. nullopt when the run declares
// no margin model, the side has no maintenance fraction, the book is flat,
// or no finite price solves the breach (a long at full maintenance).
std::optional<double> native_liquidation_price() const;
// Owning snapshots copied at query time. Later commands/reset do not
// invalidate already returned values.
std::vector<NativeMarketEvent> native_events(uint64_t after_ordinal) const;
Expand Down
58 changes: 54 additions & 4 deletions include/pineforge/native_order.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -416,11 +416,23 @@ struct TargetObservation {
std::vector<OpeningObservation> openings;
};

// Who authored a request. Host is every request a host submits, replaces or
// cancels, and it is the whole existing population: it folds nothing into the
// continuation digest, so no established hash moves. KernelLiquidation marks
// the margin model's own Reduce; KernelRisk is reserved for the risk lane.
enum class RequestOrigin : std::uint8_t {
Host = 0,
KernelLiquidation = 1,
KernelRisk = 2,
};

struct RequestDefinition {
RequestHandle handle;
Request request;
Birth birth;
std::optional<RequestHandle> predecessor;
// Appended last so every existing aggregate initializer keeps its meaning.
RequestOrigin origin = RequestOrigin::Host;
};
using DefinitionRef = std::shared_ptr<const RequestDefinition>;

Expand Down Expand Up @@ -554,6 +566,9 @@ enum class CancelReason : std::uint8_t {
Group = 1,
OwnerGone = 2,
UnsupportedRelation = 3,
// A kernel-originated request the kernel itself withdrew: the liquidation
// level or its units moved, or the requirement is no longer breached.
Superseded = 4,
};

enum class AppliedTerminalReason : std::uint8_t {
Expand Down Expand Up @@ -799,6 +814,30 @@ struct ArmedEvent {
std::optional<EventId> quantity_resolution;
};

// A kernel-issued liquidation that actually filled. It carries the margin
// facts of that fill, so a host reconstructs the outcome without recomputing
// the account: `mark` is the booked resolved price, `equity` and `required`
// are the marked equity and the maintenance requirement of the SURVIVING book
// at that price, `liquidation_price` is the level re-solved for what is left,
// and `position_before` / `position_after` are the signed book on either side
// of the reduction. `applied` names the ExecutionAppliedEvent that booked it.
struct MarginCallEvent {
uint64_t ordinal = 0;
DefinitionRef definition;
EventId applied;
MatchCursor cursor{};
Side side = Side::Long;
double mark = 0.0;
double equity = 0.0;
double required = 0.0;
double liquidation_price = 0.0;
double units = 0.0;
double position_before = 0.0;
double position_after = 0.0;
const RequestHandle& handle() const noexcept { return definition->handle; }
const Request& request() const noexcept { return definition->request; }
};

using CommandEvent = std::variant<AcceptedEvent,
RejectedEvent,
ReplacedEvent,
Expand All @@ -815,7 +854,8 @@ using CommandEvent = std::variant<AcceptedEvent,
DeferredGroupAdjustmentEvent,
QuantityBoundEvent,
ArmedEvent,
TermsResolvedEvent>;
TermsResolvedEvent,
MarginCallEvent>;

// Almost every prepared command yields one history event. Keep that ordinary
// transactional payload inline; the overflow vector preserves the existing
Expand Down Expand Up @@ -1132,14 +1172,18 @@ class WorkingRequestCore {
PreparedSubmit prepare_submit(const Request& request,
const CommandContext& context,
uint64_t& next_order_incarnation,
uint64_t& next_timeline_ordinal);
uint64_t& next_timeline_ordinal,
RequestOrigin origin = RequestOrigin::Host);
PreparedReplace prepare_replace(const RequestHandle& target,
const Request& request,
const CommandContext& context,
uint64_t& next_order_incarnation,
uint64_t& next_timeline_ordinal,
ReplaceOptions options = {});
PreparedCancel prepare_cancel(const RequestHandle& target, uint64_t& next_timeline_ordinal);
// `reason` lets the kernel withdraw its own request under the durable
// Superseded receipt. Host cancels keep the default User reason.
PreparedCancel prepare_cancel(const RequestHandle& target, uint64_t& next_timeline_ordinal,
CancelReason reason = CancelReason::User);

InstalledCommand<SubmitResult> install_submit(PreparedSubmit&& prepared) noexcept;
InstalledCommand<ReplaceResult> install_replace(PreparedReplace&& prepared) noexcept;
Expand Down Expand Up @@ -1179,6 +1223,12 @@ class WorkingRequestCore {
const TermsResolvedInput& input,
uint64_t& next_timeline_ordinal);

// Append one MarginCallEvent to the immutable history. The target is the
// kernel-originated request that filled, which is already terminal by the
// time its margin facts are recorded, so no live row moves.
Preparation<PreparedMutation> prepare_margin_call(const MarginCallEvent& event,
uint64_t& next_timeline_ordinal);

// The allowance that prepare_evaluation would install for this point.
static Allowance evaluated_allowance(const LiveRequest& live, uint64_t point) noexcept;
// Consumer-only no-event form of the ordinary allowance
Expand Down Expand Up @@ -1439,7 +1489,7 @@ static_assert(std::variant_size_v<SizeBasis> == 2);
static_assert(std::variant_size_v<Remaining> == 5);
static_assert(std::variant_size_v<RemainingProjection> == 5);
static_assert(std::variant_size_v<Allowance> == 4);
static_assert(std::variant_size_v<CommandEvent> == 17);
static_assert(std::variant_size_v<CommandEvent> == 18);
static_assert(std::variant_size_v<ExecutionPlan> == 4);
static_assert(std::variant_size_v<ExecutionScope> == 3);
static_assert(std::variant_size_v<TriggerState> == 9);
Expand Down
Loading
Loading