PineForge v0.13.1-379-g9b50973
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 spec.input_tf = "5";
48 spec.script_tf = "5";
49 spec.ticker = "MOCK";
50 spec.tickerid = "TEST:MOCK";
51 spec.type = "crypto";
52 spec.currency = "USDT";
53 spec.basecurrency = "ETH";
54 spec.description = "";
55 spec.volumetype = "";
56 spec.timezone = "UTC";
57 spec.session = "24x7";
58 spec.chart_timezone = "";
59 spec.initial_capital = 10000.0;
60 spec.point_value = 1.0;
61 spec.account_fx = 1.0;
62 spec.price_tick = 0.01;
63 spec.slippage_ticks = 0;
64 spec.fee_kind = pineforge::NativeFeeKind::CashPerExecution;
65 spec.fee_value = 6.0;
66 spec.close_execution = pineforge::NativeCloseExecution::NextEligiblePoint;
67 spec.allowed_open_directions = pineforge::NativeOpenDirections::Both;
68 return spec;
69}
70
71// Five UTC 24x7 five-minute bars. Unix milliseconds, positive finite OHLC.
72const pineforge::Bar kBars[] = {
73 {100.0, 102.0, 99.0, 101.0, 4.0, 0},
74 {102.0, 103.0, 101.0, 102.0, 4.0, 300000},
75 {103.0, 104.0, 102.0, 103.5, 4.0, 600000},
76 {103.5, 104.0, 103.0, 103.5, 4.0, 900000},
77 {104.0, 105.0, 103.5, 104.0, 4.0, 1200000},
78};
79constexpr int kBarCount = 5;
80
81bool failed(const pineforge::NativeStrategyHost& host, const char* where) {
82 const auto state = host.native_state();
83 if (state.kind == pineforge::NativeLifecycleKind::Failed) {
84 std::cerr << where << ": Failed code="
85 << static_cast<int>(state.failure.code)
86 << " op=" << static_cast<int>(state.failure.operation)
87 << " " << host.last_error() << '\n';
88 return true;
89 }
90 return false;
91}
92
93} // namespace
94
95// The whole C ABI for this host, generated. Compare the eight functions it
96// replaces in git history: they were hand-written per example.
98
99extern "C" {
100
101// Example-specific probe kept outside the macro: the C ABI tests read it to
102// confirm the module and the test binary agree on the engine ABI version.
104
105} // extern "C"
106
107int main() {
108 const auto spec = make_spec();
109
110 NativeMarketExample batch;
111 const auto setup = batch.configure_native(spec);
112 if (setup.status != pineforge::NativeSetupStatus::Applied) {
113 std::cerr << "configure: " << batch.last_error() << '\n';
114 return 1;
115 }
116 batch.run(kBars, kBarCount);
117 if (failed(batch, "run()")) return 1;
118 if (batch.native_state().kind != pineforge::NativeLifecycleKind::Completed) {
119 std::cerr << "run() not completed: " << batch.last_error() << '\n';
120 return 1;
121 }
122
123 NativeMarketExample batch_tf;
124 if (batch_tf.configure_native(spec).status != pineforge::NativeSetupStatus::Applied)
125 return 1;
126 batch_tf.run(kBars, kBarCount, "5", "5");
127 if (failed(batch_tf, "run(tf)") ||
128 batch_tf.native_state().kind != pineforge::NativeLifecycleKind::Completed) {
129 std::cerr << "run(tf) " << batch_tf.last_error() << '\n';
130 return 1;
131 }
132
133 // Acceptance and fill are separate rows in the event history: the request
134 // is accepted on bar 1 and filled at the next modeled open.
135 bool accepted = false;
136 bool filled = false;
137 for (const auto& event : batch.native_events(0)) {
138 if (!event.command) continue;
139 if (std::holds_alternative<pineforge::native_order::AcceptedEvent>(*event.command))
140 accepted = true;
141 if (std::holds_alternative<pineforge::native_order::ExecutionAppliedEvent>(*event.command))
142 filled = true;
143 }
144 if (!accepted || !filled) {
145 std::cerr << "expected AcceptedEvent and ExecutionAppliedEvent\n";
146 return 1;
147 }
148 if (batch.physical_position().signed_units != 0.0) {
149 std::cerr << "expected flat book after flatten fill\n";
150 return 1;
151 }
152
153 // The same host, driven forward: one begin with a warmup prefix, then
154 // bar-by-bar. No second public run() and no reset at the handoff.
155 NativeMarketExample stream;
156 if (stream.configure_native(spec).status != pineforge::NativeSetupStatus::Applied)
157 return 1;
158 if (!stream.stream_begin(kBars, 1, "5", "5")) {
159 std::cerr << "stream_begin: " << stream.last_error() << '\n';
160 return 1;
161 }
162 for (int i = 1; i < kBarCount; ++i) {
163 if (!stream.stream_push_bar(kBars[i])) {
164 std::cerr << "stream_push_bar: " << stream.last_error() << '\n';
165 return 1;
166 }
167 }
168 if (!stream.stream_end()) {
169 std::cerr << "stream_end: " << stream.last_error() << '\n';
170 return 1;
171 }
172 if (failed(stream, "stream") ||
173 stream.native_state().kind != pineforge::NativeLifecycleKind::Completed) {
174 std::cerr << "stream not completed: " << stream.last_error() << '\n';
175 return 1;
176 }
177
178 std::cout << "batch closed trades: " << batch.trade_count()
179 << " stream closed trades: " << stream.trade_count() << '\n';
180 return 0;
181}
The public native host: an abstract subclass of BacktestEngine with no PineScript on it.
NativeStateView native_state() const
The whole run state as one owning read.
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:70
One complete setup value, staged/copied by NativeStrategyHost before it is applied at begin.