PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
native_selected_strategy.cpp
Go to the documentation of this file.
1// Pine-free native example: the ownership and execution seams.
2//
3// Where native_market_strategy.cpp is the minimum host, this one exercises the
4// public request vocabulary a Pine strategy would reach through
5// `strategy.entry` / `strategy.exit` / `strategy.close`:
6//
7// * a host-sized opening behind a limit trigger (`HostSized` +
8// `resolve_execution_terms`);
9// * a child stop bound to that opening's cycle (`BindOpening`), the native
10// spelling of a bracket leg;
11// * a close selected against exactly one opening (`BindOpenings`), executed
12// at the current point after a readiness preview;
13// * an exact reversal to a signed target (`ReverseTo`).
14//
15// Like its sibling it is both a loadable module and a standalone program, and
16// contains no Pine command call, formula, or protected engine write.
17
19
20#include <cstdint>
21#include <iostream>
22#include <optional>
23#include <variant>
24
25namespace {
26
27// Keep C++ capability/layout checks with the module that links the runtime.
29 "selected example requires the v19 native host");
30static_assert(sizeof(pf_bar_t) == sizeof(pineforge::Bar),
31 "C bar mirror must match the native host bar");
32static_assert(sizeof(pf_report_t) == sizeof(pineforge::ReportC),
33 "C report mirror must match the native host report");
34static_assert(static_cast<int>(pineforge::NativeLifecycleKind::Completed)
36 "C lifecycle mirror must match the native host lifecycle");
37
38namespace no = pineforge::native_order;
39
40// Not `final`: PINEFORGE_EXPORT_NATIVE_STRATEGY derives the module class from
41// this one.
42class NativeSelectedExample : public pineforge::NativeStrategyHost {
43 std::optional<no::RequestHandle> opening_;
44 std::optional<no::RequestHandle> reversal_;
45 std::optional<no::RequestHandle> selected_;
46 std::optional<std::int64_t> opening_cycle_;
47 int bars_ = 0;
48 bool child_submitted_ = false;
49
50 struct AppliedMetrics {
51 int host_sized_applied = 0;
52 int reversal_applied = 0;
53 int selected_applied = 0;
54 double host_sized_opened_units = 0.0;
55 double reversal_opened_units = 0.0;
56 double selected_ticket = 0.0;
57 };
58
59 // The sizing seam. An unresolved HostSized request asks the host for its
60 // units; returning none leaves it unresolved and fails the run.
61 no::ExecutionTerms resolve_execution_terms(
62 const pineforge::NativeExecutionTermsFacts& facts) const override {
63 if (std::holds_alternative<no::RemainingDeferred>(facts.remaining)) {
64 return {facts.default_resolved_price, 2.0, no::OpeningShape::Transact};
65 }
66 return {facts.default_resolved_price, std::nullopt, no::OpeningShape::Transact};
67 }
68
69 pineforge::NativePrecommitVerdict validate_execution_precommit(
70 const pineforge::NativePrecommitView&) const override {
71 return pineforge::NativePrecommitVerdict::Proceed;
72 }
73
74 void on_native_run_begin() override {
75 opening_.reset();
76 reversal_.reset();
77 selected_.reset();
78 opening_cycle_.reset();
79 bars_ = 0;
80 child_submitted_ = false;
81 }
82
83 void on_native_bar(const pineforge::Bar&,
84 const pineforge::NativeDecisionContext&) override {
85 ++bars_;
86 if (bars_ == 1) {
87 submit(no::Request{no::Transact{1.0}, "baseline-open", ""});
88 no::Request open;
89 open.intent = no::HostSized{no::HostSizedKind::Open, no::Side::Long};
90 open.label = "sized-limit";
91 open.trigger = no::Limit{101.0};
92 opening_ = submit(open).handle;
93 return;
94 }
95
96 if (!child_submitted_ && opening_ && opening_cycle_) {
97 no::Request child;
98 child.intent = no::Reduce{no::OwnerOpenedUnits{}};
99 child.label = "bound-stop";
100 child.trigger = no::Stop{99.0};
101 child.owner = no::BindOpening{*opening_, *opening_cycle_};
102 submit(child);
103 child_submitted_ = true;
104 }
105
106 if (bars_ == 3 && opening_ && opening_cycle_ && !selected_) {
107 no::Request selected{no::Flatten{}, "selected-flatten", ""};
108 selected.owner = no::BindOpenings{{*opening_}, *opening_cycle_};
109 const auto accepted = submit(selected);
110 selected_ = accepted.handle;
111 if (selected_) {
112 const pineforge::NativeCurrentExecution current{
113 *selected_, pineforge::NativeCurrentPriceRule::NearestTick};
114 const auto preview = inspect_current_execution(current);
115 if (!preview.refusal
116 && preview.settlement_readiness == pineforge::execution::Status::Applied) {
117 execute_current(current);
118 }
119 }
120 }
121
122 if (bars_ == 4 && !reversal_) {
123 no::Request reverse;
124 reverse.intent = no::ReverseTo{-1.0};
125 reverse.label = "exact-reverse";
126 reversal_ = submit(reverse).handle;
127 }
128 }
129
130 void on_native_applied(const no::ExecutionAppliedEvent& event,
131 const pineforge::NativeDecisionContext&) override {
132 if (opening_ && event.handle() == *opening_) {
133 opening_cycle_ = event.cycle_after;
134 }
135 }
136
137 AppliedMetrics applied_metrics() const {
138 AppliedMetrics result;
139 for (const auto& event : native_events(0)) {
140 if (!event.command) continue;
141 const auto* applied = std::get_if<no::ExecutionAppliedEvent>(&*event.command);
142 if (!applied) continue;
143 if (opening_ && applied->handle() == *opening_) {
144 ++result.host_sized_applied;
145 result.host_sized_opened_units = applied->opened_units;
146 }
147 if (reversal_ && applied->handle() == *reversal_) {
148 ++result.reversal_applied;
149 result.reversal_opened_units = applied->opened_units;
150 }
151 if (selected_ && applied->handle() == *selected_) {
152 ++result.selected_applied;
153 result.selected_ticket = applied->current_ticket;
154 }
155 }
156 return result;
157 }
158
159public:
160 int host_sized_applied() const { return applied_metrics().host_sized_applied; }
161 int reversal_applied() const { return applied_metrics().reversal_applied; }
162 int selected_applied() const { return applied_metrics().selected_applied; }
163 double host_sized_opened_units() const { return applied_metrics().host_sized_opened_units; }
164 double reversal_opened_units() const { return applied_metrics().reversal_opened_units; }
165 double selected_ticket() const { return applied_metrics().selected_ticket; }
166
167 int reversal_terminal_kind() const {
168 if (!reversal_) return -1;
169 int observed = 0;
170 for (const auto& event : native_events(0)) {
171 if (!event.command) continue;
172 const auto* command = &*event.command;
173 if (const auto* applied = std::get_if<no::ExecutionAppliedEvent>(command)) {
174 if (applied->handle() == *reversal_) return 1;
175 }
176 if (const auto* rejected = std::get_if<no::MatchRejectedEvent>(command)) {
177 if (rejected->handle() == *reversal_) {
178 return 100 + static_cast<int>(rejected->reason);
179 }
180 }
181 if (const auto* cancelled = std::get_if<no::CancelledEvent>(command)) {
182 if (cancelled->handle() == *reversal_) return 200 + static_cast<int>(cancelled->reason);
183 }
184 if (const auto* no_effect = std::get_if<no::NoEffectEvent>(command)) {
185 if (no_effect->handle() == *reversal_) return 300;
186 }
187 if (const auto* accepted = std::get_if<no::AcceptedEvent>(command)) {
188 if (accepted->handle() == *reversal_) observed = 400;
189 }
190 }
191 return observed;
192 }
193};
194
195NativeSelectedExample* as_example(pf_strategy_t strategy) {
196 return strategy
197 ? dynamic_cast<NativeSelectedExample*>(
199 : nullptr;
200}
201
202int lifecycle_kind(pf_strategy_t strategy) {
203 const auto* example = as_example(strategy);
204 return example ? static_cast<int>(example->native_state().kind) : -1;
205}
206
207int failure_code(pf_strategy_t strategy) {
208 const auto* example = as_example(strategy);
209 return example ? static_cast<int>(example->native_state().failure.code) : -1;
210}
211
212std::uint32_t failure_discriminator(pf_strategy_t strategy) {
213 const auto* example = as_example(strategy);
214 return example ? example->native_state().failure.discriminator : 0;
215}
216
219 spec.identity.session_key = "native-selected-example";
220 spec.identity.run_number = 1;
221 // This example reads its whole event record once the run has ended, so
222 // it keeps it all: the default, Window, keeps only what a host has not
223 // yet acknowledged (native_acknowledge_events).
224 spec.event_retention = pineforge::NativeEventRetention::Full;
225 spec.input_tf = "5";
226 spec.script_tf = "5";
227 spec.ticker = "SELECTED";
228 spec.tickerid = "EXCHANGE:SELECTED";
229 spec.type = "crypto";
230 spec.currency = "USD";
231 spec.basecurrency = "USD";
232 spec.description = "native-selected-example";
233 spec.volumetype = "base";
234 spec.timezone = "UTC";
235 spec.session = "24x7";
236 spec.chart_timezone = "UTC";
237 spec.initial_capital = 10000.0;
238 spec.point_value = 1.0;
239 spec.account_fx = 1.0;
240 spec.price_tick = 0.01;
241 spec.slippage_ticks = 0;
242 spec.fee_kind = pineforge::NativeFeeKind::CashPerExecution;
243 spec.fee_value = 1.0;
244 spec.close_execution = pineforge::NativeCloseExecution::NextEligiblePoint;
245 spec.allowed_open_directions = pineforge::NativeOpenDirections::Both;
246 return spec;
247}
248
249const pineforge::Bar kBars[] = {
250 {100.0, 102.0, 100.0, 101.0, 1.0, 0},
251 {102.0, 103.0, 101.0, 102.5, 1.0, 300000},
252 {103.0, 104.0, 102.0, 103.5, 1.0, 600000},
253 {104.0, 105.0, 103.0, 104.5, 1.0, 900000},
254 {105.0, 106.0, 104.0, 105.5, 1.0, 1200000},
255};
256constexpr int kBarCount = 5;
257
258bool completed(const NativeSelectedExample& host, const char* where) {
259 if (host.native_state().kind == pineforge::NativeLifecycleKind::Completed) return true;
260 std::cerr << where << ": lifecycle="
261 << static_cast<int>(host.native_state().kind)
262 << " code=" << static_cast<int>(host.native_state().failure.code)
263 << " " << host.last_error() << '\n';
264 return false;
265}
266
267} // namespace
268
269PINEFORGE_EXPORT_NATIVE_STRATEGY(NativeSelectedExample);
270
271extern "C" {
272
273// Example-specific observation symbols kept outside the macro: the C ABI test
274// reads the host's own counters through them.
276 (void)pf_abi_version();
277 return 15;
278}
279
281 return lifecycle_kind(strategy);
282}
283
285 return failure_code(strategy);
286}
287
289 return failure_discriminator(strategy);
290}
291
293 const auto* example = as_example(strategy);
294 return example ? example->host_sized_applied() : -1;
295}
296
298 const auto* example = as_example(strategy);
299 return example ? example->host_sized_opened_units() : 0.0;
300}
301
303 const auto* example = as_example(strategy);
304 return example ? example->reversal_applied() : -1;
305}
306
308 const auto* example = as_example(strategy);
309 return example ? example->reversal_opened_units() : 0.0;
310}
311
313 const auto* example = as_example(strategy);
314 return example ? example->reversal_terminal_kind() : -1;
315}
316
318 const auto* example = as_example(strategy);
319 return example ? example->selected_applied() : -1;
320}
321
323 const auto* example = as_example(strategy);
324 return example ? example->selected_ticket() : 0.0;
325}
326
327} // extern "C"
328
329int main() {
330 const auto spec = make_spec();
331
332 NativeSelectedExample batch;
333 if (batch.configure_native(spec).status != pineforge::NativeSetupStatus::Applied) {
334 std::cerr << "configure: " << batch.last_error() << '\n';
335 return 1;
336 }
337 batch.run(kBars, kBarCount, "5", "5");
338 if (!completed(batch, "run(tf)")) return 1;
339
340 NativeSelectedExample stream;
341 if (stream.configure_native(spec).status != pineforge::NativeSetupStatus::Applied) {
342 std::cerr << "configure(stream): " << stream.last_error() << '\n';
343 return 1;
344 }
345 if (!stream.stream_begin(kBars, 1, "5", "5")) {
346 std::cerr << "stream_begin: " << stream.last_error() << '\n';
347 return 1;
348 }
349 for (int i = 1; i < kBarCount; ++i) {
350 if (!stream.stream_push_bar(kBars[i])) {
351 std::cerr << "stream_push_bar: " << stream.last_error() << '\n';
352 return 1;
353 }
354 }
355 if (!stream.stream_end()) {
356 std::cerr << "stream_end: " << stream.last_error() << '\n';
357 return 1;
358 }
359 if (!completed(stream, "stream")) return 1;
360
361 std::cout << "host-sized applied: " << batch.host_sized_applied()
362 << " units=" << batch.host_sized_opened_units()
363 << " selected-close applied: " << batch.selected_applied()
364 << " closed trades: " << batch.trade_count() << '\n';
365 return 0;
366}
The public native host: an abstract subclass of BacktestEngine with no PineScript on it.
@ PF_NATIVE_LIFECYCLE_COMPLETED
int pf_abi_version(void)
static const pf_bar_t kBars[]
static pf_native_run_spec_v1 make_spec(void)
@ kBarCount
NativePrecommitVerdict
The host is consulted before generic opening-margin admission.
BacktestEngine * as_engine(pf_strategy_t strategy)
#define PINEFORGE_HAS_NATIVE_STRATEGY_HOST_V19
One-line C ABI export for a hand-written NativeStrategyHost.
#define PINEFORGE_EXPORT_NATIVE_STRATEGY(Class)
Define the loadable-module C ABI for one NativeStrategyHost subclass.
static void submit(struct host_state *state, uint32_t intent, double value, uint32_t trigger, double price, const char *label)
int native_selected_example_host_epoch()
int native_selected_example_lifecycle(pf_strategy_t strategy)
int native_selected_example_host_sized_applied(pf_strategy_t strategy)
int native_selected_example_selected_applied(pf_strategy_t strategy)
int native_selected_example_failure_code(pf_strategy_t strategy)
double native_selected_example_reversal_opened_units(pf_strategy_t strategy)
double native_selected_example_selected_ticket(pf_strategy_t strategy)
int native_selected_example_reversal_applied(pf_strategy_t strategy)
double native_selected_example_host_sized_opened_units(pf_strategy_t strategy)
std::uint32_t native_selected_example_failure_discriminator(pf_strategy_t strategy)
int native_selected_example_reversal_terminal_kind(pf_strategy_t strategy)
void * pf_strategy_t
Opaque handle to a compiled strategy instance.
Definition pineforge.h:441
#define PF_API
Definition pineforge.h:83
Single OHLCV bar pushed into the engine.
Definition pineforge.h:140
Backtest report filled by run_backtest / run_backtest_full.
Definition pineforge.h:366
One complete setup value, staged/copied by NativeStrategyHost before it is applied at begin.
NativeEventRetention event_retention
What native_events() can still return; see NativeEventRetention.