PineForge v1.4.0-4-g873b25da
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
native_broker_hash_strategy.cpp
Go to the documentation of this file.
1// Pine-free native example: per-bar broker-state hashes, and the host's own
2// state folded into them (hash_host_extension).
3//
4// With report_policy = KernelRecorded and set_broker_state_hash_recording(true)
5// the kernel records one broker-state hash per script bar into the report,
6// beside the equity point of that bar:
7//
8// broker_state_hash_len == equity_curve_len == script_bars_processed
9//
10// in a batch and across a stream's warmup and realtime legs alike. A row is
11// the run's CONTINUATION IDENTITY at that bar -- the kernel's broker state (the
12// lots, the realized sums, the extremes, the closed rows) and, ahead of it,
13// the state a resume would continue from -- so the array is a replay check
14// within one driving mode:
15// * the same drive twice records the same rows;
16// * a run that ends at bar k recorded, as its last row, row k-1 of the
17// longer run (prefix closure).
18// Across driving modes it is deliberately NOT equal: the phase (Batch /
19// Warmup / Realtime) is folded into the continuation, so a batch and a stream
20// over the same bars share no row, and two streams share exactly the bars
21// both are still warming up on, then converge one bar past the later warmup
22// boundary. What IS the same across drivings is the length identity, the
23// outcome -- every closed row -- and the broker half alone: factor the
24// continuation out with broker_state_hash_from_execution_hash(fixed), a
25// protected member every host inherits, and the rest of the fold is identical
26// at every bar in every driving.
27//
28// This host's next decision depends on state the kernel does not own: a
29// streak of rising closes. It folds that state into every hash through
30// hash_host_extension, under its own domain tag, so a replay that diverges
31// there diverges in the hash. The extension is input to the hash only: it
32// moves every per-bar row and the scalar broker_state_hash(), and no fill and
33// no continuation -- the same host with the extension off books the same
34// trades under the same native_continuation_hash().
35//
36// c++ -std=c++17 native_broker_hash_strategy.cpp -lpineforge_kernel -o broker_hash
37//
38// Nothing here is PineScript: no codegen, no `src/source`, no `src/compat`.
39
41
42#include <algorithm>
43#include <cassert>
44#include <cstdint>
45#include <cstdio>
46#include <iostream>
47#include <vector>
48
49namespace {
50
51namespace no = pineforge::native_order;
52
53constexpr std::int64_t kFiveMinutes = 5LL * 60LL * 1000LL;
54constexpr int kBarCount = 24;
55constexpr int kEarlyWarmup = 6; // one stream warms up on bars [0, 6) ...
56constexpr int kLateWarmup = 12; // ... the other on bars [0, 12)
57constexpr int kPrefix = 12; // a batch that stops after bar 11
58// Any fixed execution hash factors the continuation out of the fold.
59constexpr std::uint64_t kFixedExecutionHash = 0;
60
61// Closes chosen so the streak rule below trades four round trips: in after
62// bars 2, 7, 12 and 18, out after bars 4, 10, 14 and 20.
63const double kCloses[kBarCount] = {
64 100.0, 101.0, 102.0, 103.0, 102.0, 101.0, 102.0, 103.0,
65 104.0, 105.0, 103.0, 104.0, 105.0, 106.0, 104.0, 103.0,
66 102.0, 103.0, 104.0, 105.0, 104.0, 103.0, 104.0, 103.5,
67};
68
69std::vector<pineforge::Bar> make_bars() {
70 std::vector<pineforge::Bar> bars;
71 double previous = kCloses[0];
72 for (int i = 0; i < kBarCount; ++i) {
73 const double close = kCloses[i];
74 const double open = previous;
75 bars.push_back(pineforge::Bar{open, std::max(open, close) + 0.5,
76 std::min(open, close) - 0.5, close, 10.0,
77 i * kFiveMinutes});
78 previous = close;
79 }
80 return bars;
81}
82
83class BrokerHashExample : public pineforge::NativeStrategyHost {
84public:
85 explicit BrokerHashExample(bool fold) : fold_(fold) {}
86
87 // The broker half of the hash at every calculation, continuation factored out.
88 std::vector<std::uint64_t> broker_half;
89
90private:
91 bool fold_ = true;
92 // The host's own durable state: its next decision reads it.
93 std::int64_t streak_ = 0;
94 double previous_close_ = 0.0;
95 std::int64_t entries_ = 0;
96
97 void on_native_run_begin() override {
98 streak_ = 0;
99 previous_close_ = 0.0;
100 entries_ = 0;
101 broker_half.clear();
102 }
103
104 void on_native_bar(const pineforge::Bar& bar,
105 const pineforge::NativeDecisionContext&) override {
106 streak_ = (previous_close_ > 0.0 && bar.close > previous_close_) ? streak_ + 1 : 0;
107 previous_close_ = bar.close;
108 const double held = physical_position().signed_units;
109 if (held == 0.0 && streak_ >= 2) {
110 submit({no::Transact{1.0}, "long", "streak"});
111 ++entries_;
112 } else if (held > 0.0 && streak_ == 0) {
113 submit({no::Flatten{}, "flat", "streak"});
114 }
115 broker_half.push_back(broker_state_hash_from_execution_hash(kFixedExecutionHash));
116 }
117
118 // The fold's tail: a domain tag of this host's own, then each durable
119 // value in a fixed order. With the extension off this host folds exactly
120 // what a host that overrides nothing folds.
121 void hash_host_extension(pineforge::BrokerStateHashSink& sink) const override {
122 if (!fold_) {
124 return;
125 }
126 sink.s("streak-host/v1");
127 sink.i(streak_);
128 sink.d(previous_close_);
129 sink.i(entries_);
130 }
131};
132
135 spec.identity.session_key = "native-broker-hash-example";
136 spec.identity.run_number = 1;
137 spec.input_tf = "5";
138 spec.script_tf = "5";
139 spec.ticker = "MOCK";
140 spec.tickerid = "TEST:MOCK";
141 spec.type = "crypto";
142 spec.currency = "USDT";
143 spec.basecurrency = "ETH";
144 spec.timezone = "UTC";
145 spec.session = "24x7";
146 spec.initial_capital = 10000.0;
147 spec.point_value = 1.0;
148 spec.account_fx = 1.0;
149 spec.price_tick = 0.01;
150 spec.fee_kind = pineforge::NativeFeeKind::Percent;
151 spec.fee_value = 0.1;
152 // The kernel records the report, one point -- and, with the recording
153 // switch on, one broker-state hash -- per script bar.
154 spec.report_policy = pineforge::NativeReportPolicy::KernelRecorded;
155 return spec;
156}
157
158// What one drive leaves behind.
159struct Drive {
160 std::vector<std::uint64_t> rows; // the recorded per-bar hashes
161 std::vector<std::uint64_t> broker_half;
162 std::vector<pineforge::Trade> trades;
163 std::int64_t script_bars = 0;
164 std::int64_t curve_points = 0;
165 std::uint64_t scalar = 0; // broker_state_hash() after the run
166 std::uint64_t continuation = 0; // native_continuation_hash()
167};
168
169Drive harvest(BrokerHashExample& host) {
170 assert(host.native_state().kind == pineforge::NativeLifecycleKind::Completed);
171 pineforge::ReportC report{};
172 host.fill_report(&report);
173 Drive drive;
174 drive.rows.assign(report.broker_state_hash,
175 report.broker_state_hash + report.broker_state_hash_len);
176 drive.script_bars = report.script_bars_processed;
177 drive.curve_points = report.equity_curve_len;
179 drive.broker_half = host.broker_half;
180 for (int i = 0; i < host.trade_count(); ++i) drive.trades.push_back(host.get_trade(i));
181 drive.scalar = host.broker_state_hash();
182 drive.continuation = host.native_continuation_hash();
183 // One row per script bar, whatever the driving.
184 assert(static_cast<std::int64_t>(drive.rows.size()) == drive.script_bars);
185 assert(drive.curve_points == drive.script_bars);
186 return drive;
187}
188
189// run() over the first `bars` bars.
190Drive batch(bool fold, int bars) {
191 const auto feed = make_bars();
192 BrokerHashExample host(fold);
193 host.set_broker_state_hash_recording(true);
194 const auto setup = host.configure_native(make_spec());
195 assert(setup.status == pineforge::NativeSetupStatus::Applied);
196 host.run(feed.data(), bars);
197 return harvest(host);
198}
199
200// stream_begin over the first `warmup` bars, then every other bar pushed live.
201Drive stream(bool fold, int warmup) {
202 const auto feed = make_bars();
203 BrokerHashExample host(fold);
204 // Persistent configuration: set it before stream_begin so the warmup leg
205 // records too.
206 host.set_broker_state_hash_recording(true);
207 const auto setup = host.configure_native(make_spec());
208 assert(setup.status == pineforge::NativeSetupStatus::Applied);
209 const bool begun = host.stream_begin(feed.data(), warmup, "5", "5");
210 assert(begun);
211 for (int i = warmup; i < kBarCount; ++i) {
212 const bool pushed = host.stream_push_bar(feed[static_cast<std::size_t>(i)]);
213 assert(pushed);
214 }
215 const bool ended = host.stream_end(false);
216 assert(ended);
217 return harvest(host);
218}
219
220std::size_t shared_rows(const std::vector<std::uint64_t>& a, const std::vector<std::uint64_t>& b) {
221 assert(a.size() == b.size());
222 std::size_t same = 0;
223 for (std::size_t i = 0; i < a.size(); ++i) same += a[i] == b[i];
224 return same;
225}
226
227bool same_trades(const Drive& a, const Drive& b) {
228 if (a.trades.size() != b.trades.size()) return false;
229 for (std::size_t i = 0; i < a.trades.size(); ++i) {
230 const auto& x = a.trades[i];
231 const auto& y = b.trades[i];
232 if (x.entry_time != y.entry_time || x.exit_time != y.exit_time
233 || x.entry_price != y.entry_price || x.exit_price != y.exit_price
234 || x.qty != y.qty || x.pnl != y.pnl || x.commission != y.commission
235 || x.entry_id != y.entry_id || x.exit_id != y.exit_id) {
236 return false;
237 }
238 }
239 return true;
240}
241
242} // namespace
243
244int main() {
245 const Drive folded = batch(true, kBarCount);
246 const Drive plain = batch(false, kBarCount);
247 std::printf("batch: %zu rows for %lld script bars, %lld equity points, %zu closed trades\n",
248 folded.rows.size(), static_cast<long long>(folded.script_bars),
249 static_cast<long long>(folded.curve_points), folded.trades.size());
250 assert(folded.script_bars == kBarCount && folded.trades.size() == 4);
251
252 // --- hash_host_extension: every row moves, nothing else does -------------
253 std::printf("extension on vs off: %zu of %d rows shared; scalar %016llx vs %016llx; "
254 "continuation %s\n",
255 shared_rows(folded.rows, plain.rows), kBarCount,
256 static_cast<unsigned long long>(folded.scalar),
257 static_cast<unsigned long long>(plain.scalar),
258 folded.continuation == plain.continuation ? "identical" : "DIFFERS");
259 assert(shared_rows(folded.rows, plain.rows) == 0);
260 assert(folded.scalar != plain.scalar);
261 assert(folded.continuation == plain.continuation);
262 assert(same_trades(folded, plain));
263
264 // --- within one driving mode: a replay check -----------------------------
265 assert(batch(true, kBarCount).rows == folded.rows);
266 const Drive prefix = batch(true, kPrefix);
267 std::printf("prefix: a batch of %d bars ends on row %016llx; the full batch's row %d is "
268 "%016llx\n",
269 kPrefix, static_cast<unsigned long long>(prefix.rows.back()), kPrefix - 1,
270 static_cast<unsigned long long>(folded.rows[kPrefix - 1]));
271 assert(prefix.rows.size() == static_cast<std::size_t>(kPrefix));
272 assert(std::equal(prefix.rows.begin(), prefix.rows.end(), folded.rows.begin()));
273
274 // --- across driving modes ------------------------------------------------
275 const Drive early = stream(true, kEarlyWarmup);
276 const Drive late = stream(true, kLateWarmup);
277 const Drive early_plain = stream(false, kEarlyWarmup);
278 for (const Drive* drive : {&early, &late, &early_plain}) {
279 assert(drive->script_bars == kBarCount && drive->rows.size() == folded.rows.size());
280 }
281 // The outcome is the batch's, closed row for closed row...
282 assert(same_trades(early, folded) && same_trades(late, folded));
283 // ...and so is the broker half, at every bar.
284 assert(shared_rows(early.broker_half, folded.broker_half) == kBarCount);
285 assert(shared_rows(late.broker_half, folded.broker_half) == kBarCount);
286 // The rows are not: a batch shares none with a stream.
287 std::printf("batch vs stream (warmup %d): %zu of %d rows shared; broker half %zu of %d\n",
288 kEarlyWarmup, shared_rows(folded.rows, early.rows), kBarCount,
289 shared_rows(folded.broker_half, early.broker_half), kBarCount);
290 assert(shared_rows(folded.rows, early.rows) == 0);
291 assert(shared_rows(folded.rows, late.rows) == 0);
292 // Two streams: equal on the bars both warm up on, different from the
293 // earlier boundary through the later one, equal again past it.
294 for (int i = 0; i < kBarCount; ++i) {
295 const bool same = early.rows[static_cast<std::size_t>(i)]
296 == late.rows[static_cast<std::size_t>(i)];
297 assert(same == (i < kEarlyWarmup || i > kLateWarmup));
298 }
299 std::printf("stream warmup %d vs %d: rows equal on [0,%d) and [%d,%d), different on [%d,%d]\n",
300 kEarlyWarmup, kLateWarmup, kEarlyWarmup, kLateWarmup + 1, kBarCount,
301 kEarlyWarmup, kLateWarmup);
302 // The extension moves every stream row as well.
303 assert(shared_rows(early.rows, early_plain.rows) == 0);
304 assert(same_trades(early, early_plain));
305
306 // The summary line, printed only once every check above has passed.
307 std::printf("per-bar rows %zu/%lld batch, %zu/%lld stream extension moved %zu/%zu "
308 "batch vs stream shared %zu/%zu rows, %zu/%zu broker half closed trades: %zu\n",
309 folded.rows.size(), static_cast<long long>(folded.script_bars),
310 early.rows.size(), static_cast<long long>(early.script_bars),
311 folded.rows.size() - shared_rows(folded.rows, plain.rows), folded.rows.size(),
312 shared_rows(folded.rows, early.rows), folded.rows.size(),
313 shared_rows(folded.broker_half, early.broker_half), folded.broker_half.size(),
314 folded.trades.size());
315 return 0;
316}
virtual void hash_host_extension(BrokerStateHashSink &) const
The public native host: an abstract subclass of BacktestEngine with no PineScript on it.
static pf_native_run_spec_v1 make_spec(void)
@ kBarCount
int b(int64_t c)
Definition color.hpp:44
static void submit(struct host_state *state, uint32_t intent, double value, uint32_t trigger, double price, const char *label)
double close
Definition bar.hpp:7
One complete setup value, staged/copied by NativeStrategyHost before it is applied at begin.