Skip to content
Closed
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
651 changes: 651 additions & 0 deletions docs/design/native-feature-parity.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions docs/pages/abi-stability.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ order type; `pineforge-source-adapter/v2` hashes adapter and scheduler state
instead. Native request/core/event values are `native_order_v5`, the private
consumer identity is
`native-consumer/v7`, driver types are `native_driver_v5`, and run specs are
`native_run_spec_v2`.
`native_run_spec_v3`.

| Matrix role | Internal identity |
| --- | --- |
Expand Down Expand Up @@ -192,7 +192,7 @@ The current integrated representation uses generic broker fingerprint domain
`pineforge-broker-state/v17` and stream fingerprint version 17; the source
extension begins with `pineforge-source-adapter/v2`. Native consumer identity
is `native-consumer/v7`, driver values own `native_driver_v5`, and run specs own
`native_run_spec_v2`. Stable `RunIdentity` / `RequestHandle` / `Birth` remain
`native_run_spec_v3`. Stable `RunIdentity` / `RequestHandle` / `Birth` remain
`native_order_v1`; request, core, and event values own `native_order_v5`.
Terms receipts, attempted terms, deferred
remaining/allowance state, and a staged FX-curve digest contribute through the
Expand Down
42 changes: 42 additions & 0 deletions docs/pages/native-engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,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.

Optional, absent unless set:

Expand Down Expand Up @@ -319,6 +322,45 @@ ticket per execution). Slippage is applied to the raw observed/modeled price
as described above. Native account rows follow lots, remaining entry costs,
and realized balance at the matching coordinate.

## Reporting for native hosts

`fill_report` publishes closed trades, diagnostics, an equity curve and the
metrics derived from it. The curve is **host-owned by default**: nothing in
the kernel records a point, so a bare `NativeStrategyHost` that leaves
`report_policy` at `HostRecorded` reports `equity_curve_len == 0`, and every
equity metric (drawdown, run-up, Sharpe/Sortino, CAGR, time in market)
degenerates over that empty series. A host that marks its own equity keeps
this default and owns the whole series.

`NativeReportPolicy::KernelRecorded` asks the consumer to record instead. Once
per script calculation — after the callback returns, and after the
`AfterCalculation` close when that mode is on — it folds the equity extremes
and appends one point labelled with the **script interval's open**, so the
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.

`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
`open_at_end` set. The mark is the raw close on the price grid with **no
slippage**: slippage models a market order's fill uncertainty, and this row is
a mark, not an order. It is reporting only. The live position, the pending
orders, the realized sums, the equity curve and the broker state are left
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.

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
rows followed by the range-end rows, in the order `fill_report` lays them out.

## Calendar, session, timeframes, warmup

Timestamps are **Unix milliseconds**. Confirmed bars require positive finite
Expand Down
9 changes: 9 additions & 0 deletions include/pineforge/engine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3124,6 +3124,15 @@ class BacktestEngine {

int trade_count() const { return (int)trades_.size(); }
const Trade& get_trade(int i) const { return trades_[i]; }
// The same closed rows under the `closed_trade_*` name a host reads them
// by (RP6). The per-field `closed_trade_*` family below stays protected
// Pine plumbing: one whole row by reference answers all of it, and a host
// that owns its report needs the row, not twenty wrappers. Range-end
// report rows are NOT here — report_trade_count() / get_report_trade()
// below span both spaces. Unchecked, like get_trade above: index against
// closed_trade_count().
std::size_t closed_trade_count() const noexcept { return trades_.size(); }
const Trade& closed_trade(std::size_t i) const { return trades_[i]; }
// The REPORT's row space: trades_ followed by range_end_trades_, in the
// order fill_trades_section lays pf_report_t::trades out. trade_count()
// / get_trade() stay the Pine-visible closed trades (strategy.closedtrades
Expand Down
2 changes: 1 addition & 1 deletion include/pineforge/market_driver.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
#include <string>

namespace pineforge {
inline namespace native_run_spec_v2 { struct NativeRunSpec; }
inline namespace native_run_spec_v3 { struct NativeRunSpec; }
inline namespace native_driver_v5 {

// Semantic versions hashed into native continuation identity.
Expand Down
24 changes: 22 additions & 2 deletions include/pineforge/native_run_spec.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
#include <vector>

namespace pineforge {
inline namespace native_run_spec_v2 {
inline namespace native_run_spec_v3 {

// Encodings coincide with the versioned native-v1 C transport. These values
// describe native execution; they do not configure source strategy policies.
Expand All @@ -35,6 +35,18 @@ enum class NativeAbortReporting : std::uint32_t {
Quiet = 1,
};

// Who records the per-script-bar report series. HostRecorded leaves the
// equity curve, its metrics and any range-end row entirely to the host, which
// is what every host that drives its own recording already does.
// KernelRecorded asks the consumer to mark one equity point per script
// 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.
enum class NativeReportPolicy : std::uint32_t {
HostRecorded = 0,
KernelRecorded = 1,
};

enum class NativeOpenDirections : std::uint32_t {
None = 0,
Long = 1,
Expand Down Expand Up @@ -172,6 +184,12 @@ struct NativeRunSpec {
NativeOpenDirections allowed_open_directions = NativeOpenDirections::Both;
std::optional<double> initial_margin_fraction; // Positive fraction, not percent;
// no maintenance liquidation.
NativeReportPolicy report_policy = NativeReportPolicy::HostRecorded;
// Report a position still open at run end as a mark-to-market closed row
// at the last close. KernelRecorded only, and reporting only: the live
// book, the realized sums and every hash are left exactly as the run left
// them. Inert under HostRecorded, whose host owns the whole report series.
bool report_open_position_at_end = false;
IntrabarPath intrabar{};
};

Expand All @@ -188,6 +206,7 @@ enum class NativeRunSpecField : std::uint8_t {
TimeframeUndetected,
SlotLabelPolicy, LegacyTolerance,
PathOrder,
ReportPolicy,
};

enum class NativeRunSpecError : std::uint8_t {
Expand Down Expand Up @@ -216,6 +235,7 @@ enum class NativeRunSpecError : std::uint8_t {
UnknownSlotLabelPolicy,
UnknownLegacyTolerance,
UnknownPathOrder,
UnknownReportPolicy,
};

// Allocation-free facts suitable for the host's durable failure variant.
Expand Down Expand Up @@ -258,5 +278,5 @@ static_assert(std::is_trivially_copyable_v<NativeRunSpecValidation>);
static_assert(std::is_nothrow_move_constructible_v<NativeRunSpec>);
static_assert(std::is_nothrow_move_assignable_v<NativeRunSpec>);

} // inline namespace native_run_spec_v2
} // inline namespace native_run_spec_v3
} // namespace pineforge
6 changes: 3 additions & 3 deletions scripts/check_native_cpp_abi.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,7 @@ def main() -> int:
("pineforge::native_order::native_order_v1::WorkingRequestCore::submit(",
current_order_submit),
("pineforge::validate_native_run_spec(",
"pineforge::native_run_spec_v2::validate_native_run_spec("),
"pineforge::native_run_spec_v3::validate_native_run_spec("),
("pineforge::native_bar_structurally_valid(",
"pineforge::native_driver_v5::native_bar_structurally_valid("),
("abi_accept_coordinate(pineforge::NativeCoordinate",
Expand All @@ -663,7 +663,7 @@ def main() -> int:
"native_order_identity": "pineforge::native_order::native_order_v1",
"native_order": "pineforge::native_order::" + order_namespace,
"native_calendar": "pineforge::native_calendar::native_calendar_v2",
"native_run_spec": "pineforge::native_run_spec_v2",
"native_run_spec": "pineforge::native_run_spec_v3",
"native_driver": "pineforge::native_driver_v5",
"native_host": "pineforge::engine_script_run_v17",
},
Expand Down Expand Up @@ -1087,7 +1087,7 @@ def link(name, objects, runtime, missing=None):
link("current_descriptor_to_old_calendar", [current_descriptor, old_timezone_obj], old_calendar_obj,
"pineforge::native_calendar::native_calendar_v2::timezone_identity_descriptor(")
link("current_spec_to_old_symbol_control", [current_spec], old_spec_symbols,
"pineforge::native_run_spec_v2::validate_native_run_spec(")
"pineforge::native_run_spec_v3::validate_native_run_spec(")
link("current_host_events_to_old_symbols", [current_host_events], old_host_events_symbols,
"pineforge::engine_script_run_v17::NativeStrategyHost::native_events(")
link("current_bar_to_old_object", [current_bar], old_bar_obj,
Expand Down
40 changes: 22 additions & 18 deletions scripts/check_native_cpp_versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
NATIVE_FX_CURVE_NAMESPACE = "native_fx_curve_v1"

DRIVER_FORWARD = (
"inline namespace native_run_spec_v2 { struct NativeRunSpec; }"
"inline namespace native_run_spec_v3 { struct NativeRunSpec; }"
)

TYPE_DEF = r'\b(?:enum\s+class|class|struct)\s+(\w+)\s*(?::[^;{]+)?\{'
Expand Down Expand Up @@ -291,53 +291,57 @@ def check_texts(files):
require(calendar_src, ("TimezoneIdentityDescriptor::valid",),
"native_calendar_v2", r'\bNAME\s*\(')

spec = versioned(files[FILES[4]], "pineforge", "native_run_spec_v2")
spec = versioned(files[FILES[4]], "pineforge", "native_run_spec_v3")
require(spec, ("NativeRunSpec", "NativeRunSpecValidation", "NativeRunSpecError",
"NativeRunSpecField", "IntrabarPath", "SampleEligibility", "synthesized",
"NativeSlotLabelPolicy", "NativePathOrder",
"NativeLegacyTolerance"),
"native_run_spec_v2",
"NativeLegacyTolerance", "NativeReportPolicy"),
"native_run_spec_v3",
r'\b(?:enum\s+class|struct)\s+NAME\s*(?::[^;{]+)?\{')
require_namespace_functions(
spec, ("validate_native_run_spec", "normalize_native_run_spec", "native_intrabar_path_digest"),
"native_run_spec_v2")
spec_src = versioned(files[FILES[5]], "pineforge", "native_run_spec_v2")
"native_run_spec_v3")
spec_src = versioned(files[FILES[5]], "pineforge", "native_run_spec_v3")
require_namespace_functions(
spec_src, ("validate_native_run_spec", "normalize_native_run_spec", "native_intrabar_path_digest"),
"native_run_spec_v2")
"native_run_spec_v3")
run_spec = body(spec, r'struct\s+NativeRunSpec\s*\{', 'native run spec')
if ('std::stringinput_tf;std::stringscript_tf;booltimeframe_undetected=false;'
not in re.sub(r'\s+', '', run_spec)):
raise ValueError('native_run_spec_v2 requires its explicit undetected-timeframe field')
raise ValueError('native_run_spec_v3 requires its explicit undetected-timeframe field')
compact_spec = re.sub(r'\s+', '', run_spec)
if not re.search(r'\benum\s+class\s+NativeAbortReporting\s*:', spec):
raise ValueError('native_run_spec_v2 omits NativeAbortReporting')
raise ValueError('native_run_spec_v3 omits NativeAbortReporting')
for member in (
'NativeSlotLabelPolicyslot_label_policy=NativeSlotLabelPolicy::Canonical;',
'NativeLegacyTolerancelegacy_tolerance=NativeLegacyTolerance::None;',
'NativePathOrderpath_order=NativePathOrder::Auto;',
'NativeAbortReportingabort_reporting=NativeAbortReporting::Error;'):
'NativeAbortReportingabort_reporting=NativeAbortReporting::Error;',
'NativeReportPolicyreport_policy=NativeReportPolicy::HostRecorded;',
'boolreport_open_position_at_end=false;'):
if member not in compact_spec:
raise ValueError('native_run_spec_v2 omits required policy member: ' + member)
raise ValueError('native_run_spec_v3 omits required policy member: ' + member)
fields = body(spec, r'enum\s+class\s+NativeRunSpecField\s*:\s*std::uint8_t\s*\{',
'native run spec fields')
for field in ('TimeframeUndetected', 'SlotLabelPolicy', 'LegacyTolerance', 'AbortReporting',
'PathOrder'):
'PathOrder', 'ReportPolicy'):
if not re.search(r'\b' + field + r'\b', fields):
raise ValueError('native_run_spec_v2 omits the field tag: ' + field)
raise ValueError('native_run_spec_v3 omits the field tag: ' + field)
errors = body(spec, r'enum\s+class\s+NativeRunSpecError\s*:\s*std::uint8_t\s*\{',
'native run spec errors')
for error in ('InvalidUndetectedTimeframe', 'UnknownSlotLabelPolicy',
'UnknownLegacyTolerance', 'UnknownAbortReporting',
'UnknownIntrabarSampleEligibility', 'UnknownPathOrder'):
'UnknownIntrabarSampleEligibility', 'UnknownPathOrder',
'UnknownReportPolicy'):
if not re.search(r'\b' + error + r'\b', errors):
raise ValueError('native_run_spec_v2 omits the validation error: ' + error)
raise ValueError('native_run_spec_v3 omits the validation error: ' + error)
if ('spec.timeframe_undetected' not in spec_src
or 'InvalidUndetectedTimeframe' not in spec_src
or 'spec.slot_label_policy' not in spec_src
or 'spec.legacy_tolerance' not in spec_src
or 'spec.path_order' not in spec_src
or 'spec.abort_reporting' not in spec_src
or 'spec.report_policy' not in spec_src
or 'lower->sample_eligibility' not in spec_src):
raise ValueError('native run-spec validation omits an explicit compatibility rule')
intrabar = body(spec, r'struct\s+IntrabarPath\s*\{', 'intrabar path')
Expand Down Expand Up @@ -379,12 +383,12 @@ def check_texts(files):
driver_text = files[FILES[6]]
if driver_text.count(DRIVER_FORWARD) != 1:
raise ValueError("market_driver.hpp must forward-declare NativeRunSpec "
"in native_run_spec_v2 outside native_driver_v5")
"in native_run_spec_v3 outside native_driver_v5")
driver_clean = clean(driver_text)
driver_owner = body(driver_clean, r'namespace\s+pineforge\s*\{', "pineforge")
driver = body(driver_owner, r'inline\s+namespace\s+native_driver_v5\s*\{',
"native_driver_v5")
if DRIVER_FORWARD in driver or "native_run_spec_v2" in driver:
if DRIVER_FORWARD in driver or "native_run_spec_v3" in driver:
raise ValueError("NativeRunSpec forward declaration must stay outside native_driver_v5")
if re.search(r'\bstruct\s+NativeRunSpec\s*\{', driver_clean):
raise ValueError("NativeRunSpec definition does not belong to native_driver_v5")
Expand Down Expand Up @@ -638,5 +642,5 @@ def check(root=ROOT):

if __name__ == "__main__":
check()
print("native_order identity v1 / values v5, native_calendar_v2, native_run_spec_v2, "
print("native_order identity v1 / values v5, native_calendar_v2, native_run_spec_v3, "
"native_driver_v5, native_fx_curve_v1 and host engine_script_run_v17 ownership verified")
14 changes: 7 additions & 7 deletions scripts/test_native_cpp_versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,8 @@ def test_stale_wrapper(self):
(FILES[11], "native_order_v1", "native_order_v5"),
(FILES[2], "native_calendar_v2", "native_calendar_v1"),
(FILES[3], "native_calendar_v2", "native_calendar_v3"),
(FILES[4], "native_run_spec_v2", "native_run_spec_v1"),
(FILES[5], "native_run_spec_v2", "native_run_spec_v1"),
(FILES[4], "native_run_spec_v3", "native_run_spec_v1"),
(FILES[5], "native_run_spec_v3", "native_run_spec_v1"),
(FILES[6], "native_driver_v5", "native_driver_v2"),
(FILES[7], "native_driver_v5", "native_driver_v3"),
(FILES[8], "engine_script_run_v17", "engine_script_run_v12"),
Expand All @@ -201,7 +201,7 @@ def test_duplicate_wrapper(self):
for path, namespace in (
(FILES[0], "native_order_v5"),
(FILES[2], "native_calendar_v2"),
(FILES[4], "native_run_spec_v2"),
(FILES[4], "native_run_spec_v3"),
(FILES[6], "native_driver_v5"),
(FILES[8], "engine_script_run_v17"),
(FILES[11], "native_order_v1"),
Expand All @@ -214,7 +214,7 @@ def test_empty_namespace_is_not_ownership(self):
for path, namespace in (
(FILES[0], "native_order_v5"),
(FILES[2], "native_calendar_v2"),
(FILES[4], "native_run_spec_v2"),
(FILES[4], "native_run_spec_v3"),
(FILES[6], "native_driver_v5"),
(FILES[8], "engine_script_run_v17"),
(FILES[11], "native_order_v1"),
Expand Down Expand Up @@ -302,8 +302,8 @@ def test_return_only_function_escape(self):
decl = "NativeRunSpecValidation validate_native_run_spec(const NativeRunSpec& spec) noexcept;"
text = changed[spec].replace(decl, "", 1)
text = text.replace(
"} // inline namespace native_run_spec_v2",
"} // inline namespace native_run_spec_v2\n" + decl,
"} // inline namespace native_run_spec_v3",
"} // inline namespace native_run_spec_v3\n" + decl,
1)
changed[spec] = text
with self.assertRaises(ValueError):
Expand Down Expand Up @@ -353,7 +353,7 @@ def test_driver_native_run_spec_forward_must_stay_outside(self):
self.reject(
FILES[6],
DRIVER_FORWARD,
"inline namespace native_run_spec_v2 { struct NativeRunSpec {}; }")
"inline namespace native_run_spec_v3 { struct NativeRunSpec {}; }")

def test_host_public_values_cannot_leave_v17(self):
self.reject(FILES[8], "struct NativeStateView {", "} struct NativeStateView {")
Expand Down
Loading
Loading