PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
native_market_strategy.cpp
Go to the documentation of this file.
1// Pine-free native example: one market entry, one market flatten.
2//
3// This single file is both artefacts a native strategy usually ships as:
4//
5// * a loadable module — PINEFORGE_EXPORT_NATIVE_STRATEGY below defines the
6// `extern "C"` surface `pineforge-live` and the C ABI tests dlopen;
7// * a standalone program — `main()` runs the same host over embedded bars,
8// first in batch, then through the streaming lifecycle.
9//
10// Nothing here is PineScript: no codegen, no `src/source`, no
11// `src/compat/pine`. `scripts/check_native_include_independence.py` compiles
12// this file against the installed headers with the source trees deleted.
13
15
16#include <iostream>
17#include <variant>
18
19namespace {
20
21// A native host overrides callbacks and submits requests. `on_native_bar` is
22// the only pure virtual: it is the close-of-bar calculation point.
23// Not `final`: PINEFORGE_EXPORT_NATIVE_STRATEGY derives the module class from
24// this one.
25class NativeMarketExample : public pineforge::NativeStrategyHost {
26 int bars_ = 0;
27
28 void on_native_run_begin() override { bars_ = 0; }
29
30 void on_native_bar(const pineforge::Bar&,
31 const pineforge::NativeDecisionContext&) override {
32 ++bars_;
33 if (bars_ == 1 && physical_position().signed_units == 0.0) {
34 submit_market({pineforge::order_action::Transact{1.0}, "native-open", ""});
35 } else if (bars_ == 4 && physical_position().signed_units != 0.0) {
36 submit_market({pineforge::execution::Flatten{}, "native-flat", ""});
37 }
38 }
39};
40
41// Every required NativeRunSpec field, spelled out: validation refuses an
42// incomplete value, and nothing is inferred from the bars.
45 spec.identity.session_key = "native-market-example";
46 spec.identity.run_number = 1;
47 // This example reads its whole event record once the run has ended, so
48 // it keeps it all: the default, Window, keeps only what a host has not
49 // yet acknowledged (native_acknowledge_events).
50 spec.event_retention = pineforge::NativeEventRetention::Full;
51 spec.input_tf = "5";
52 spec.script_tf = "5";
53 spec.ticker = "MOCK";
54 spec.tickerid = "TEST:MOCK";
55 spec.type = "crypto";
56 spec.currency = "USDT";
57 spec.basecurrency = "ETH";
58 spec.description = "";
59 spec.volumetype = "";
60 spec.timezone = "UTC";
61 spec.session = "24x7";
62 spec.chart_timezone = "";
63 spec.initial_capital = 10000.0;
64 spec.point_value = 1.0;
65 spec.account_fx = 1.0;
66 spec.price_tick = 0.01;
67 spec.slippage_ticks = 0;
68 spec.fee_kind = pineforge::NativeFeeKind::CashPerExecution;
69 spec.fee_value = 6.0;
70 spec.close_execution = pineforge::NativeCloseExecution::NextEligiblePoint;
71 spec.allowed_open_directions = pineforge::NativeOpenDirections::Both;
72 return spec;
73}
74
75// Five UTC 24x7 five-minute bars. Unix milliseconds, positive finite OHLC.
76const pineforge::Bar kBars[] = {
77 {100.0, 102.0, 99.0, 101.0, 4.0, 0},
78 {102.0, 103.0, 101.0, 102.0, 4.0, 300000},
79 {103.0, 104.0, 102.0, 103.5, 4.0, 600000},
80 {103.5, 104.0, 103.0, 103.5, 4.0, 900000},
81 {104.0, 105.0, 103.5, 104.0, 4.0, 1200000},
82};
83constexpr int kBarCount = 5;
84
85bool failed(const pineforge::NativeStrategyHost& host, const char* where) {
86 const auto state = host.native_state();
87 if (state.kind == pineforge::NativeLifecycleKind::Failed) {
88 std::cerr << where << ": Failed code="
89 << static_cast<int>(state.failure.code)
90 << " op=" << static_cast<int>(state.failure.operation)
91 << " " << host.last_error() << '\n';
92 return true;
93 }
94 return false;
95}
96
97} // namespace
98
99// The whole C ABI for this host, generated. Compare the eight functions it
100// replaces in git history: they were hand-written per example.
102
103extern "C" {
104
105// Example-specific probe kept outside the macro: the C ABI tests read it to
106// confirm the module and the test binary agree on the engine ABI version.
108
109} // extern "C"
110
111int main() {
112 const auto spec = make_spec();
113
114 NativeMarketExample batch;
115 const auto setup = batch.configure_native(spec);
116 if (setup.status != pineforge::NativeSetupStatus::Applied) {
117 std::cerr << "configure: " << batch.last_error() << '\n';
118 return 1;
119 }
120 batch.run(kBars, kBarCount);
121 if (failed(batch, "run()")) return 1;
122 if (batch.native_state().kind != pineforge::NativeLifecycleKind::Completed) {
123 std::cerr << "run() not completed: " << batch.last_error() << '\n';
124 return 1;
125 }
126
127 NativeMarketExample batch_tf;
128 if (batch_tf.configure_native(spec).status != pineforge::NativeSetupStatus::Applied)
129 return 1;
130 batch_tf.run(kBars, kBarCount, "5", "5");
131 if (failed(batch_tf, "run(tf)") ||
132 batch_tf.native_state().kind != pineforge::NativeLifecycleKind::Completed) {
133 std::cerr << "run(tf) " << batch_tf.last_error() << '\n';
134 return 1;
135 }
136
137 // Acceptance and fill are separate rows in the event history: the request
138 // is accepted on bar 1 and filled at the next modeled open.
139 bool accepted = false;
140 bool filled = false;
141 for (const auto& event : batch.native_events(0)) {
142 if (!event.command) continue;
143 if (std::holds_alternative<pineforge::native_order::AcceptedEvent>(*event.command))
144 accepted = true;
145 if (std::holds_alternative<pineforge::native_order::ExecutionAppliedEvent>(*event.command))
146 filled = true;
147 }
148 if (!accepted || !filled) {
149 std::cerr << "expected AcceptedEvent and ExecutionAppliedEvent\n";
150 return 1;
151 }
152 if (batch.physical_position().signed_units != 0.0) {
153 std::cerr << "expected flat book after flatten fill\n";
154 return 1;
155 }
156
157 // The same host, driven forward: one begin with a warmup prefix, then
158 // bar-by-bar. No second public run() and no reset at the handoff.
159 NativeMarketExample stream;
160 if (stream.configure_native(spec).status != pineforge::NativeSetupStatus::Applied)
161 return 1;
162 if (!stream.stream_begin(kBars, 1, "5", "5")) {
163 std::cerr << "stream_begin: " << stream.last_error() << '\n';
164 return 1;
165 }
166 for (int i = 1; i < kBarCount; ++i) {
167 if (!stream.stream_push_bar(kBars[i])) {
168 std::cerr << "stream_push_bar: " << stream.last_error() << '\n';
169 return 1;
170 }
171 }
172 if (!stream.stream_end()) {
173 std::cerr << "stream_end: " << stream.last_error() << '\n';
174 return 1;
175 }
176 if (failed(stream, "stream") ||
177 stream.native_state().kind != pineforge::NativeLifecycleKind::Completed) {
178 std::cerr << "stream not completed: " << stream.last_error() << '\n';
179 return 1;
180 }
181
182 std::cout << "batch closed trades: " << batch.trade_count()
183 << " stream closed trades: " << stream.trade_count() << '\n';
184 return 0;
185}
The public native host: an abstract subclass of BacktestEngine with no PineScript on it.
int pf_abi_version(void)
static const pf_bar_t kBars[]
static pf_native_run_spec_v1 make_spec(void)
@ kBarCount
int native_market_example_abi_version()
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.
#define PF_API
Definition pineforge.h:83
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.