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
33 changes: 25 additions & 8 deletions docs/pages/native-engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,9 @@ Always set, with documented defaults in the header:
Cash kinds are account currency per unit or per execution.
- `close_execution`: `NextEligiblePoint` (default) or `AfterCalculation`
- `allowed_open_directions`: `None`, `Long`, `Short`, `Both` (default)
- `report_policy`: `HostRecorded` (default) or `KernelRecorded`;
`report_open_position_at_end`: `false` (default). See *Reporting for native
hosts* below.
- `report_policy`: `HostRecorded` (default), `KernelRecorded` or
`KernelRecordedAtHostMarks`; `report_open_position_at_end`: `false`
(default). See *Reporting for native hosts* below.
- `calculation`: `BarClose` (default), `BarCloseAndFills` or
`EveryModeledPoint`; `max_recalculations_per_point`: `8` (default, any
value including 0 is legal); `open_bar_view`: `Complete` (default) or
Expand Down Expand Up @@ -731,6 +731,21 @@ curve is identical with and without an intrabar path. The result is one point
per script bar, a finite drawdown/run-up walk, and metrics computed over a
real series.

`NativeReportPolicy::KernelRecordedAtHostMarks` records the very same series
at the points the host marks, for a host whose report cadence is not one point
per calculation. The consumer never records on its own initiative under it:
the host calls the kernel's report mark from inside its own callback, naming
the label the point carries, and the kernel performs the extremes fold and the
curve append. The Pine adapter is that host — its report has one point per
published *source* slot, which is not the same series of instants: a
`calc_on_order_fills` re-entry marks the slot it opened at the fill and the
bar's ordinary close calculation then marks nothing, and a suppressed probe
tail advances source history, and marks, without running generated code at
all. The point also has to land inside the callback, before the adapter takes
that bar's broker-state hash and before its range-end rows re-mark the curve's
last point. Under this policy the kernel owns what a report point is and the
host owns only when.

`report_open_position_at_end` (`KernelRecorded` only) adds the rows a close of
the still-open position at the last bar's close would record — one per
physical lot, through the same row builder every full close uses, with
Expand All @@ -742,11 +757,13 @@ exactly as the run left them; the rows appear in `fill_report` and in
`report_trade_count()` / `get_report_trade()`, never in `closed_trade_count()`
/ `closed_trade()`.

Both fields are opt-in and fold into the continuation hash only once
`report_policy` is non-default, so a spec that does not ask for kernel
recording keeps the continuation identity it had before these fields existed.
Recording does move the broker-state hash, because the equity extremes it
folds are durable engine state.
Both fields are opt-in and fold into the continuation hash only under
`KernelRecorded` — the one policy under which the consumer decides, on its
own, to act between two points of a run. `HostRecorded` and
`KernelRecordedAtHostMarks` fold nothing, so a spec that does not hand the
kernel its cadence keeps the continuation identity it had before these fields
existed. Recording does move the broker-state hash, because the equity
extremes it folds are durable engine state.

Per-trade reads: `closed_trade_count()` / `closed_trade(i)` return the closed
rows this run booked; `report_trade_count()` / `get_report_trade(i)` span those
Expand Down
8 changes: 8 additions & 0 deletions include/pineforge/native_run_spec.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,17 @@ enum class NativeAbortReporting : std::uint32_t {
// calculation, so a bare host gets a truthful curve, finite drawdown/run-up
// metrics and a report whose walk is not degenerate. Recording is reporting:
// it books no cash and places no order.
// KernelRecordedAtHostMarks records the very same series, at the points the
// host marks: a host whose report cadence is not one point per calculation —
// a source adapter that re-enters its script on a fill, or publishes a bar
// its script never calculates — keeps that cadence and still stops owning
// what a report point is. The consumer never records on its own initiative
// under it, so it leaves the continuation identity exactly where
// HostRecorded leaves it (see hash_spec in native_execution_consumer.cpp).
enum class NativeReportPolicy : std::uint32_t {
HostRecorded = 0,
KernelRecorded = 1,
KernelRecordedAtHostMarks = 2,
};

enum class NativeOpenDirections : std::uint32_t {
Expand Down
4 changes: 4 additions & 0 deletions include/pineforge/source/pine_strategy_host.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,10 @@ class PineStrategyHost : public NativeStrategyHost, public BrokerStateHashProvid
void scheduler_feed_deferred_aux_security(int chart_index);
void scheduler_finish_security_sequence();
void scheduler_record_range_end(const Bar&);
// One report point per published source slot. The kernel records it
// (NativeReportPolicy::KernelRecordedAtHostMarks); this host owns only
// the Pine cadence that says where the points fall.
void scheduler_mark_report_point(std::int64_t script_bar_ts);
void scheduler_record_broker_hash();
void capture_script_continuation_hash();
void scheduler_update_session_state(
Expand Down
36 changes: 30 additions & 6 deletions src/native_execution_consumer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,14 @@ void hash_spec(Fnv& f, const NativeRunSpec& spec) noexcept {
f.b(spec.initial_margin_fraction.has_value());
if (spec.initial_margin_fraction) f.d(*spec.initial_margin_fraction);
f.u(native_intrabar_path_digest(spec.intrabar));
// Report recording is opt-in, so it folds only when it is on: a spec that
// leaves the kernel out of its report keeps the continuation identity it
// had before the policy existed (same conditional shape as the precommit
// digest below).
if (spec.report_policy != NativeReportPolicy::HostRecorded) {
// Report recording folds only when the consumer records of its own
// initiative: a spec that leaves the kernel out of its report keeps the
// continuation identity it had before the policy existed (same
// conditional shape as the precommit digest below). A host-marked report
// (KernelRecordedAtHostMarks) is the same case: the consumer decides
// nothing and does nothing between two points of the run, so it folds
// nothing, exactly as HostRecorded folds nothing.
if (spec.report_policy == NativeReportPolicy::KernelRecorded) {
f.u(static_cast<uint64_t>(spec.report_policy));
f.b(spec.report_open_position_at_end);
}
Expand Down Expand Up @@ -5806,8 +5809,29 @@ void NativeExecutionConsumer::record_script_report_point(
BacktestEngine& engine, int64_t script_open_ms) const {
const auto* spec = spec_ptr();
if (!spec || spec->report_policy != NativeReportPolicy::KernelRecorded) return;
record_report_point(engine, script_open_ms);
}

// The same fold and the same append, at an instant only the host can name
// (NativeReportPolicy::KernelRecordedAtHostMarks). A host whose report
// cadence is its own — a source adapter that publishes a script bar to its
// generated code, re-enters that script on a fill, or advances its source
// history over a bar the script never calculates — marks the point inside
// its own callback, where its broker-state hash and its report rows already
// read the curve. Recording is still the kernel's: the host names when, not
// what. Inert under every other policy, so a host that records its own
// report, or one that asked for the per-calculation cadence, is unaffected.
void NativeExecutionConsumer::mark_script_report_point(
BacktestEngine& engine, int64_t script_bar_ts) const {
const auto* spec = spec_ptr();
if (!spec || spec->report_policy != NativeReportPolicy::KernelRecordedAtHostMarks) return;
record_report_point(engine, script_bar_ts);
}

void NativeExecutionConsumer::record_report_point(
BacktestEngine& engine, int64_t report_ts) const {
engine.update_equity_extremes();
engine.record_equity_point(script_open_ms);
engine.record_equity_point(report_ts);
}

// A position still open when the feed ends is reported as the rows a close at
Expand Down
6 changes: 6 additions & 0 deletions src/native_execution_consumer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ class NativeExecutionConsumer final : public IExecutionConsumer {
}
uint64_t high_water() const noexcept { return consumed_high_water_; }
void reject_inherited_on_bar(BacktestEngine& engine);
// Report truth at the host's own cadence
// (NativeReportPolicy::KernelRecordedAtHostMarks): the host marks the
// script bar it has just published and the kernel records the point.
// Reporting only, and inert under every other policy.
void mark_script_report_point(BacktestEngine& engine, int64_t script_bar_ts) const;
std::optional<Bar> series_bar(std::size_t subscription) const;
// L5 calculation timing readbacks. The partial bar is the lookahead-free
// bar so far at the current cursor; the two counters are observation of
Expand Down Expand Up @@ -335,6 +340,7 @@ class NativeExecutionConsumer final : public IExecutionConsumer {
// are reporting-only: they mark equity and synthesize report rows, and
// never book cash, place an order or move the broker book.
void record_script_report_point(BacktestEngine& engine, int64_t script_open_ms) const;
void record_report_point(BacktestEngine& engine, int64_t report_ts) const;
void record_open_position_report_rows(BacktestEngine& engine) const;
void match_point(BacktestEngine& engine, const NativeDriverPoint& point);
void match_discrete(BacktestEngine& engine, const NativeDriverPoint& point);
Expand Down
1 change: 1 addition & 0 deletions src/native_run_spec.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ bool valid_report_policy(NativeReportPolicy policy) noexcept {
switch (policy) {
case NativeReportPolicy::HostRecorded:
case NativeReportPolicy::KernelRecorded:
case NativeReportPolicy::KernelRecordedAtHostMarks:
return true;
}
return false;
Expand Down
8 changes: 8 additions & 0 deletions src/source/pine_adapter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1465,6 +1465,14 @@ NativeRunSpec PineExecutionAdapter::project(const PineStrategyConfig& config,
// Pine's request_abort surface reports a cooperative cancellation through
// status, not through last_error(). Native-only hosts retain Error.
spec.abort_reporting = NativeAbortReporting::Quiet;
// RP3/RP9 (L2): the kernel records the equity curve and its extremes; the
// source host keeps only the Pine cadence that marks where the points
// fall (pine_strategy_host.cpp scheduler_mark_report_point). TradingView's
// range-end report — which re-marks the curve's last point and re-folds
// every extreme from it (pine_strategy_host.cpp scheduler_record_range_end)
// — is report shape, not a mark-to-market row, so the kernel's own
// range-end producer stays off and report_open_position_at_end with it.
spec.report_policy = NativeReportPolicy::KernelRecordedAtHostMarks;
// Contract P6: Pine pyramiding is adapter command policy. A resting source
// entry must not consume a generic physical-lot cap before it fills, so
// the projected native spec deliberately leaves max_open_lots unbounded.
Expand Down
21 changes: 17 additions & 4 deletions src/source/pine_strategy_host.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1579,8 +1579,7 @@ void source::PineStrategyHost::scheduler_publish_source_bar(
adapter_.flush_pending_entries();
adapter_.flush_pending_bracket_legs();
if (advance_source_index) {
update_equity_extremes();
record_equity_point(bar.timestamp);
scheduler_mark_report_point(bar.timestamp);
prev_bar_timestamp_ = bar.timestamp;
}
}
Expand All @@ -1597,11 +1596,25 @@ void source::PineStrategyHost::scheduler_publish_suppressed_tail(const Bar& bar)
chart_day_partition_.empty() ? nullptr : &chart_day_partition_);
adapter_.begin_source_evaluation();
adapter_.observe_terminal_receipts();
update_equity_extremes();
record_equity_point(bar.timestamp);
scheduler_mark_report_point(bar.timestamp);
prev_bar_timestamp_ = bar.timestamp;
}

// The Pine report series has one point per SOURCE slot this host published,
// which is not the kernel's per-calculation cadence: a calc_on_order_fills
// re-entry marks the slot it opened at the fill, the ordinary close
// calculation then marks nothing, and the probe's suppressed tail marks a
// slot generated code never calculated. The point also has to land inside
// this callback, before scheduler_record_broker_hash() folds the extremes it
// just moved and before scheduler_record_range_end() re-marks the curve's
// last point. So the cadence stays here and the recording does not: the
// kernel owns what a report point is — the extremes fold and the curve
// append in engine.hpp — reached through the run spec's report policy.
void source::PineStrategyHost::scheduler_mark_report_point(std::int64_t script_bar_ts) {
as_native_consumer(execution_consumer())
.mark_script_report_point(*this, script_bar_ts);
}

void source::PineStrategyHost::scheduler_record_broker_hash() {
if (!broker_state_hash_recording_) return;
last_script_continuation_hash_ = execution_consumer().continuation_hash();
Expand Down
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ set(TEST_SOURCES
test_native_execution_terms
test_native_report_truth
test_native_margin_model
test_adapter_report_relower
test_native_queued_reverse_to
test_native_precommit_view
test_native_fx_activation
Expand Down
Loading
Loading