PineForge v0.13.1-379-g9b50973
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
native_module.hpp
Go to the documentation of this file.
1#pragma once
2
3/// @file native_module.hpp
4/// One-line C ABI export for a hand-written `NativeStrategyHost`.
5///
6/// A loadable PineForge strategy is a shared object that exports a small,
7/// fixed set of `extern "C"` entry points (`strategy_create`, `run_backtest`,
8/// …) declared in `<pineforge/pineforge.h>`. Codegen emits them for a
9/// PineScript strategy; a native C++ host had to hand-write them. This header
10/// generates exactly that surface from one macro:
11///
12/// @code
13/// #include <pineforge/native_module.hpp>
14///
15/// class MyHost : public pineforge::NativeStrategyHost {
16/// void on_native_bar(const pineforge::Bar&,
17/// const pineforge::NativeDecisionContext&) override { … }
18/// };
19///
20/// PINEFORGE_EXPORT_NATIVE_STRATEGY(MyHost);
21/// @endcode
22///
23/// The macro adds no new C symbol: every name it defines is already declared
24/// in `<pineforge/pineforge.h>`. The remaining runtime exports
25/// (`strategy_configure_native_v1`, the `strategy_stream_*` family,
26/// `strategy_get_last_error`, …) come from the engine's own `c_abi.cpp`; the
27/// generated `strategy_create` references `pf_abi_version()` so the linker
28/// keeps that object when the module links `libpineforge.a` statically.
29///
30/// The host class must be a `NativeStrategyHost` subclass and must not be
31/// `final`: the module wraps it in one derived class, which is what makes the
32/// engine's protected presentation-error string (read back through
33/// `strategy_get_last_error`) writable from the C boundary.
34
36#include <pineforge/pineforge.h>
37
38#include <cstddef>
39#include <cstring>
40#include <exception>
41#include <type_traits>
42#include <vector>
43
44namespace pineforge {
45namespace native_module {
46
47// The C mirrors are copied field by field (bars) and memcpy'd (report), so
48// their layouts must agree exactly.
49static_assert(sizeof(pf_bar_t) == sizeof(Bar),
50 "pf_bar_t / pineforge::Bar size mismatch");
51static_assert(offsetof(pf_bar_t, open) == offsetof(Bar, open),
52 "pf_bar_t::open offset mismatch");
53static_assert(offsetof(pf_bar_t, high) == offsetof(Bar, high),
54 "pf_bar_t::high offset mismatch");
55static_assert(offsetof(pf_bar_t, low) == offsetof(Bar, low),
56 "pf_bar_t::low offset mismatch");
57static_assert(offsetof(pf_bar_t, close) == offsetof(Bar, close),
58 "pf_bar_t::close offset mismatch");
59static_assert(offsetof(pf_bar_t, volume) == offsetof(Bar, volume),
60 "pf_bar_t::volume offset mismatch");
61static_assert(offsetof(pf_bar_t, timestamp) == offsetof(Bar, timestamp),
62 "pf_bar_t::timestamp offset mismatch");
63
64static_assert(sizeof(pf_report_t) == sizeof(ReportC),
65 "pf_report_t / pineforge::ReportC size mismatch");
66static_assert(offsetof(pf_report_t, total_trades) == offsetof(ReportC, total_trades),
67 "pf_report_t::total_trades offset mismatch");
68static_assert(offsetof(pf_report_t, trades) == offsetof(ReportC, trades),
69 "pf_report_t::trades offset mismatch");
70static_assert(offsetof(pf_report_t, net_profit) == offsetof(ReportC, net_profit),
71 "pf_report_t::net_profit offset mismatch");
72static_assert(offsetof(pf_report_t, security_diag) == offsetof(ReportC, security_diag),
73 "pf_report_t::security_diag offset mismatch");
74static_assert(offsetof(pf_report_t, metrics) == offsetof(ReportC, metrics),
75 "pf_report_t::metrics offset mismatch");
76static_assert(offsetof(pf_report_t, equity_curve) == offsetof(ReportC, equity_curve),
77 "pf_report_t::equity_curve offset mismatch");
78static_assert(offsetof(pf_report_t, equity_curve_len) == offsetof(ReportC, equity_curve_len),
79 "pf_report_t::equity_curve_len offset mismatch");
80static_assert(offsetof(pf_report_t, broker_state_hash) == offsetof(ReportC, broker_state_hash),
81 "pf_report_t::broker_state_hash offset mismatch");
82
83/// The one class the macro instantiates: the host plus the module boundary.
84/// Deriving is deliberate — `last_error_` is protected engine state, and only
85/// a derived class may write it.
86template <typename Host>
87class Module final : public Host {
88 static_assert(std::is_base_of<NativeStrategyHost, Host>::value,
89 "PINEFORGE_EXPORT_NATIVE_STRATEGY requires a NativeStrategyHost subclass");
90
91public:
92 /// Presentation text for `strategy_get_last_error`. Never throws: it runs
93 /// on the C boundary, often from a catch handler.
94 void note_error(const char* text) noexcept {
95 try {
96 this->last_error_ = text ? text : "";
97 } catch (...) {
98 }
99 }
100};
101
103 return static_cast<BacktestEngine*>(strategy);
104}
105
106template <typename Host>
108 return strategy ? dynamic_cast<Module<Host>*>(as_engine(strategy)) : nullptr;
109}
110
111template <typename Host>
112void note_error(pf_strategy_t strategy, const char* text) {
113 if (auto* module = as_module<Host>(strategy)) module->note_error(text);
114}
115
116/// Runs `fn` and converts any escaping exception into presentation text. C
117/// callers have no exception channel, so nothing may propagate past here.
118template <typename Host, typename Fn>
119void guarded(pf_strategy_t strategy, Fn&& fn) {
120 if (!strategy) return;
121 try {
122 fn();
123 } catch (const std::exception& error) {
124 note_error<Host>(strategy, error.what());
125 } catch (...) {
126 note_error<Host>(strategy, "native host refuses source mutation");
127 }
128}
129
130inline Bar copy_bar(const pf_bar_t& in) {
131 Bar out;
132 out.open = in.open;
133 out.high = in.high;
134 out.low = in.low;
135 out.close = in.close;
136 out.volume = in.volume;
137 out.timestamp = in.timestamp;
138 return out;
139}
140
141/// A native host owns its own intrabar path through `NativeRunSpec::intrabar`,
142/// so the C magnifier arguments have no meaning here. Only the defaults are
143/// accepted; anything else is refused before the run starts.
144inline bool magnifier_unsupported(int bar_magnifier, int magnifier_samples,
145 pf_magnifier_distribution_t distribution) {
146 return bar_magnifier != 0 || magnifier_samples != 4
147 || distribution != PF_MAGNIFIER_ENDPOINTS;
148}
149
150/// Fills a local report first: the caller's struct is written only once the
151/// engine has produced a complete, owned value.
152inline void publish_report(BacktestEngine* engine, pf_report_t* out) {
153 if (!engine || !out) return;
154 ReportC local{};
155 try {
156 engine->fill_report(&local);
157 std::memcpy(out, &local, sizeof(local));
158 } catch (...) {
159 BacktestEngine::free_report(&local);
160 throw;
161 }
162}
163
164template <typename Host>
166 try {
167 // Also the link-time anchor: referencing one PF_API symbol pulls the
168 // engine's C ABI object (and with it every other runtime export) out
169 // of libpineforge.a.
170 (void)pf_abi_version();
171 BacktestEngine* engine = new Module<Host>();
172 return static_cast<pf_strategy_t>(engine);
173 } catch (...) {
174 return nullptr;
175 }
176}
177
178inline void destroy(pf_strategy_t strategy) {
179 delete as_engine(strategy);
180}
181
182template <typename Host>
183void set_input(pf_strategy_t strategy, const char* key, const char* value) {
184 guarded<Host>(strategy, [&] {
185 as_engine(strategy)->set_input(key ? key : "", value ? value : "");
186 });
187}
188
189template <typename Host>
190void set_override(pf_strategy_t strategy, const char* key, const char* value) {
191 guarded<Host>(strategy, [&] {
192 // No BacktestEngine override mutator exists. Forward to the public
193 // guarded source-configuration entry; a host that refuses source
194 // mutation latches before inputs_ is written.
195 as_engine(strategy)->set_input(key ? key : "strategy_set_override",
196 value ? value : "");
197 });
198}
199
200template <typename Host>
202 guarded<Host>(strategy, [&] {
203 as_engine(strategy)->set_magnifier_volume_weighted(on != 0);
204 });
205}
206
207template <typename Host>
208void run_batch(pf_strategy_t strategy, pf_bar_t* bars, int count,
209 const char* input_tf, const char* script_tf, bool with_timeframes,
210 pf_report_t* out) {
211 if (!strategy) return;
212 auto* engine = as_engine(strategy);
213 try {
214 std::vector<Bar> copied;
215 const Bar* source = nullptr;
216 if (count > 0 && bars != nullptr) {
217 copied.resize(static_cast<std::size_t>(count));
218 for (int i = 0; i < count; ++i) {
219 copied[static_cast<std::size_t>(i)] = copy_bar(bars[i]);
220 }
221 source = copied.data();
222 }
223 if (with_timeframes) {
224 engine->run(source, count, input_tf ? input_tf : "",
225 script_tf ? script_tf : "");
226 } else {
227 engine->run(source, count);
228 }
229 publish_report(engine, out);
230 } catch (const std::exception& error) {
231 note_error<Host>(strategy, error.what());
232 } catch (...) {
233 note_error<Host>(strategy, "native module C batch failed");
234 }
235}
236
237template <typename Host>
238void run_backtest(pf_strategy_t strategy, pf_bar_t* bars, int count, pf_report_t* out) {
239 run_batch<Host>(strategy, bars, count, nullptr, nullptr, false, out);
240}
241
242template <typename Host>
243void run_backtest_full(pf_strategy_t strategy, pf_bar_t* bars, int count,
244 const char* input_tf, const char* script_tf,
245 int bar_magnifier, int magnifier_samples,
246 pf_magnifier_distribution_t magnifier_distribution,
247 pf_report_t* out) {
248 if (!strategy) return;
249 if (magnifier_unsupported(bar_magnifier, magnifier_samples, magnifier_distribution)) {
250 // Wrapper preflight only: the output report is left untouched and the
251 // host stays Ready, so the caller may retry with supported arguments.
252 note_error<Host>(strategy, "native module refuses unsupported magnifier arguments");
253 return;
254 }
255 run_batch<Host>(strategy, bars, count, input_tf, script_tf, true, out);
256}
257
258inline void report_free(pf_report_t* report) {
259 if (!report) return;
260 ReportC local{};
261 std::memcpy(&local, report, sizeof(local));
262 BacktestEngine::free_report(&local);
263 std::memcpy(report, &local, sizeof(local));
264}
265
266} // namespace native_module
267} // namespace pineforge
268
269/// Define the loadable-module C ABI for one `NativeStrategyHost` subclass.
270/// Use it once, at namespace scope, in the module's translation unit.
271#define PINEFORGE_EXPORT_NATIVE_STRATEGY(Class) \
272 extern "C" { \
273 PF_API pf_strategy_t strategy_create(const char*) { \
274 return ::pineforge::native_module::create<Class>(); \
275 } \
276 PF_API void strategy_free(pf_strategy_t s) { \
277 ::pineforge::native_module::destroy(s); \
278 } \
279 PF_API void strategy_set_input(pf_strategy_t s, const char* key, \
280 const char* value) { \
281 ::pineforge::native_module::set_input<Class>(s, key, value); \
282 } \
283 PF_API void strategy_set_override(pf_strategy_t s, const char* key, \
284 const char* value) { \
285 ::pineforge::native_module::set_override<Class>(s, key, value); \
286 } \
287 PF_API void strategy_set_magnifier_volume_weighted(pf_strategy_t s, int on) { \
288 ::pineforge::native_module::set_magnifier_volume_weighted<Class>(s, on); \
289 } \
290 PF_API void run_backtest(pf_strategy_t s, pf_bar_t* bars, int n, \
291 pf_report_t* out) { \
292 ::pineforge::native_module::run_backtest<Class>(s, bars, n, out); \
293 } \
294 PF_API void run_backtest_full(pf_strategy_t s, pf_bar_t* bars, int n, \
295 const char* input_tf, const char* script_tf, \
296 int bar_magnifier, int magnifier_samples, \
297 pf_magnifier_distribution_t magnifier_dist, \
298 pf_report_t* out) { \
299 ::pineforge::native_module::run_backtest_full<Class>( \
300 s, bars, n, input_tf, script_tf, bar_magnifier, magnifier_samples, \
301 magnifier_dist, out); \
302 } \
303 PF_API void report_free(pf_report_t* report) { \
304 ::pineforge::native_module::report_free(report); \
305 } \
306 } \
307 static_assert(true, "PINEFORGE_EXPORT_NATIVE_STRATEGY expects a trailing ';'")
void set_input(const std::string &key, const std::string &value)
Definition engine.hpp:2400
The one class the macro instantiates: the host plus the module boundary.
void note_error(const char *text) noexcept
Presentation text for strategy_get_last_error.
pf_magnifier_distribution_t
Bar-magnifier sub-bar sampling distribution.
Definition pineforge.h:115
@ PF_MAGNIFIER_ENDPOINTS
Default — exact O,H,L,C points plus uniform fill between.
Definition pineforge.h:119
int pf_abi_version(void)
void run_backtest_full(pf_strategy_t strategy, pf_bar_t *bars, int count, const char *input_tf, const char *script_tf, int bar_magnifier, int magnifier_samples, pf_magnifier_distribution_t magnifier_distribution, pf_report_t *out)
Bar copy_bar(const pf_bar_t &in)
BacktestEngine * as_engine(pf_strategy_t strategy)
void publish_report(BacktestEngine *engine, pf_report_t *out)
Fills a local report first: the caller's struct is written only once the engine has produced a comple...
void set_override(pf_strategy_t strategy, const char *key, const char *value)
void set_input(pf_strategy_t strategy, const char *key, const char *value)
void note_error(pf_strategy_t strategy, const char *text)
void guarded(pf_strategy_t strategy, Fn &&fn)
Runs fn and converts any escaping exception into presentation text.
void destroy(pf_strategy_t strategy)
void report_free(pf_report_t *report)
void run_backtest(pf_strategy_t strategy, pf_bar_t *bars, int count, pf_report_t *out)
void run_batch(pf_strategy_t strategy, pf_bar_t *bars, int count, const char *input_tf, const char *script_tf, bool with_timeframes, pf_report_t *out)
void set_magnifier_volume_weighted(pf_strategy_t strategy, int on)
Module< Host > * as_module(pf_strategy_t strategy)
bool magnifier_unsupported(int bar_magnifier, int magnifier_samples, pf_magnifier_distribution_t distribution)
A native host owns its own intrabar path through NativeRunSpec::intrabar, so the C magnifier argument...
void * pf_strategy_t
Opaque handle to a compiled strategy instance.
Definition pineforge.h:433
Single OHLCV bar pushed into the engine.
Definition pineforge.h:127
double volume
Bar volume.
Definition pineforge.h:132
double high
High price.
Definition pineforge.h:129
double low
Low price.
Definition pineforge.h:130
double close
Close price.
Definition pineforge.h:131
double open
Open price.
Definition pineforge.h:128
int64_t timestamp
Bar open time, Unix milliseconds.
Definition pineforge.h:133
Backtest report filled by run_backtest / run_backtest_full.
Definition pineforge.h:365
double open
Definition bar.hpp:7
double close
Definition bar.hpp:7
double low
Definition bar.hpp:7
double volume
Definition bar.hpp:7
int64_t timestamp
Definition bar.hpp:8
double high
Definition bar.hpp:7