PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
pine_scheduler.hpp
Go to the documentation of this file.
1#pragma once
2
5
6#include <algorithm>
7#include <array>
8#include <cstdint>
9#include <limits>
10#include <memory>
11#include <optional>
12#include <string>
13#include <vector>
14
15namespace pineforge {
16inline namespace engine_script_run_v19 { class ScriptBucketCursor; }
17}
18
19namespace pineforge::source {
20
22
23// Source cadence over generic native callbacks. The driver remains the sole
24// owner of matching; this class owns only language publication, retained begin
25// data and the Pine calc-on-order-fills callback cadence.
27public:
30
31 // C++17 read-only span. Empty iterators use one sentinel so standard
32 // pointer-range algorithms never subtract two null pointers; data()
33 // still exposes the released span as exactly {nullptr, 0}.
34 class InputView {
35 public:
36 InputView() noexcept = default;
37 InputView(const Bar* bars, std::size_t n) noexcept : data_(bars), size_(n) {}
38 const Bar* data() const noexcept { return data_; }
39 std::size_t size() const noexcept { return size_; }
40 bool empty() const noexcept { return size_ == 0; }
41 const Bar& operator[](std::size_t i) const noexcept { return data_[i]; }
42 const Bar& front() const noexcept { return data_[0]; }
43 const Bar& back() const noexcept { return data_[size_ - 1]; }
44 const Bar* begin() const noexcept { return data_ ? data_ : &empty_bar_; }
45 const Bar* end() const noexcept { return begin() + size_; }
46 private:
47 const Bar* data_ = nullptr;
48 std::size_t size_ = 0;
49 inline static constexpr Bar empty_bar_{};
50 };
51
52 void capture_begin(const NativeBeginArgs&);
53 // Fold the consumed prefix while caller memory is alive, then detach it.
54 // A stream keeps its owned warmup until the next capture/destruction.
55 void release_batch_input() noexcept;
57 void input(const Bar&, const NativeInputContext&, PineStrategyHost&);
58 void tick(const Bar&, const NativeTickContext&, PineStrategyHost&);
60 void bar(const Bar&, const NativeDecisionContext&, PineStrategyHost&);
61 // Applied-notification bookkeeping only. The calc_on_order_fills
62 // recalculation itself is the kernel's cadence now
63 // (NativeCalculationTrigger::BarCloseAndFills): the consumer drives it
64 // from the same drain iteration, right after this callback returns, and
65 // delivers it as recalculate() below.
66 void applied(const native_order::ExecutionAppliedEvent&);
67 // Pine's own admission of a fill recalculation, re-checked at the
68 // kernel's OrderFill cursor. Pure: it reads the freshly advanced applied
69 // cursor, the source config and adapter facts, and mutates nothing.
70 bool coof_recalculation_due(const native_order::ExecutionAppliedEvent&,
72 void recalculate(const native_order::ExecutionAppliedEvent&, const NativeDecisionContext&,
74
75 PineLanguageState& language() noexcept { return language_; }
76 bool is_first_tick() const noexcept { return language_.is_first_tick_; }
77 bool is_last_tick() const noexcept { return language_.is_last_tick_; }
78 bool bar_magnifier_enabled() const noexcept { return retained_.bar_magnifier; }
79 bool history_advances_new_bar() const noexcept {
80 return language_.is_first_tick_ && language_.history_slot_is_new_;
81 }
82 bool security_series_slot_is_new(int) const noexcept {
83 return language_.history_slot_is_new_;
84 }
85 double previous_chart_close() const noexcept { return language_.prev_chart_close_; }
86 int bar_index_offset() const noexcept { return language_.bar_index_offset_; }
87 void set_bar_index_offset(int value) noexcept { language_.bar_index_offset_ = value; }
88 void set_source_series_active(bool value) noexcept { language_._src_series_active_ = value; }
89 double script_position_view(int bar_index, PositionSide side, double quantity) const noexcept;
90 void freeze_script_position_view(int bar_index, PositionSide side, double quantity,
91 const std::vector<PyramidEntry>& lots);
92 void clear_script_position_view() noexcept;
93 const Series<double>& source_series(const std::string&) const;
94 void fixture_publish_source_series(const Bar&, bool new_history_slot);
95 int source_bar_count() const noexcept { return source_bar_count_; }
96 std::optional<Bar> next_source_bar(int interval_index) const {
97 if (retained_.is_stream || retained_.bar_magnifier
98 || (!retained_.input_tf.empty() && !retained_.script_tf.empty()
99 && retained_.input_tf != retained_.script_tf)
100 || interval_index < 0
101 || interval_index + 1 >= static_cast<int>(retained_.bars.size())) {
102 return std::nullopt;
103 }
104 return retained_.bars[static_cast<std::size_t>(interval_index + 1)];
105 }
106 // The first input the run retains -- a batch's, or a stream's warmup --
107 // after input `input_index` that opens at or after `at_or_after`, or
108 // nullopt when the retained input ends first.
109 std::optional<Bar> retained_input_from(int input_index, std::int64_t at_or_after) const {
110 if (input_index < 0) return std::nullopt;
111 for (std::size_t i = static_cast<std::size_t>(input_index) + 1; i < retained_.bars.size(); ++i) {
112 if (retained_.bars[i].timestamp >= at_or_after) return retained_.bars[i];
113 }
114 return std::nullopt;
115 }
116 bool retains_stream() const noexcept { return retained_.is_stream; }
117 bool input_is_observed_ticks() const noexcept { return input_is_observed_ticks_; }
118 // broker_bar is used only by observe_intraday_cap_fill/noop: their context
119 // names the current sub-bar, or the prior one while its close is dispatched.
120 // Thus two recent inputs suffice; eight retain conservative headroom, not
121 // arbitrary history. Two separate opening anchors cover the current and
122 // preceding script intervals even when either contains more than eight inputs.
123 void retain_confirmed_input(const Bar& bar, std::int64_t script_open_ms =
124 std::numeric_limits<std::int64_t>::min()) noexcept {
125 if (!retained_.is_stream) return;
126 if (bar.timestamp == script_open_ms) {
127 bool replaced = false;
128 for (std::size_t index = 0; index < confirmed_open_input_count_; ++index) {
129 if (confirmed_open_input_bars_[index].timestamp == bar.timestamp) {
130 confirmed_open_input_bars_[index] = bar;
131 replaced = true;
132 break;
133 }
134 }
135 if (!replaced) {
136 confirmed_open_input_bars_[confirmed_open_input_next_] = bar;
137 confirmed_open_input_next_ =
138 (confirmed_open_input_next_ + 1) % confirmed_open_input_bars_.size();
139 confirmed_open_input_count_ =
140 std::min(confirmed_open_input_count_ + 1, confirmed_open_input_bars_.size());
141 }
142 }
143 for (std::size_t index = 0; index < confirmed_input_count_; ++index) {
144 auto& existing = confirmed_input_bars_[index];
145 if (existing.timestamp == bar.timestamp) {
146 existing = bar;
147 return;
148 }
149 }
150 confirmed_input_bars_[confirmed_input_next_] = bar;
151 confirmed_input_next_ = (confirmed_input_next_ + 1) % confirmed_input_bars_.size();
152 confirmed_input_count_ = std::min(confirmed_input_count_ + 1, confirmed_input_bars_.size());
153 }
154 std::size_t confirmed_input_count() const noexcept { return confirmed_input_count_; }
155 std::optional<Bar> confirmed_input_bar_at(std::int64_t timestamp) const noexcept {
156 for (std::size_t index = 0; index < confirmed_input_count_; ++index) {
157 if (confirmed_input_bars_[index].timestamp == timestamp)
158 return confirmed_input_bars_[index];
159 }
160 for (std::size_t index = 0; index < confirmed_open_input_count_; ++index) {
161 if (confirmed_open_input_bars_[index].timestamp == timestamp)
162 return confirmed_open_input_bars_[index];
163 }
164 return std::nullopt;
165 }
166 static bool confirmed_script_interval_complete(const NativeInputContext& context) noexcept {
167 return context.script_interval.last_traded_close_ms > context.script_interval.open_ms
168 && context.input_interval.last_traded_close_ms
169 >= context.script_interval.last_traded_close_ms;
170 }
171 // The input the run retains -- a batch's, or a stream's warmup -- in
172 // time order, and whether each of its bars is a chart bar (no
173 // aggregation to a coarser script timeframe).
174 InputView retained_input() const noexcept { return retained_.bars; }
175 bool retained_input_is_chart() const noexcept {
176 return retained_.input_tf.empty() || retained_.script_tf.empty()
177 || retained_.input_tf == retained_.script_tf;
178 }
179 // The open of the script bar published last, before the run's first none.
180 std::optional<std::int64_t> last_published_script_open() const noexcept {
181 if (last_published_script_open_ms_ == std::numeric_limits<std::int64_t>::min())
182 return std::nullopt;
183 return last_published_script_open_ms_;
184 }
185 bool terminal_source_bar() const noexcept {
186 return expected_source_bars_ > 0 && source_bar_count_ >= expected_source_bars_;
187 }
188 int source_bar_index_for(const NativeDecisionContext& context) const noexcept;
189 // The next fill point, for a fill recalculation's market order, of the
190 // lower-path bar being walked: a price that bar still reaches, or
191 // (next_open) the next lower-path bar's opening -- NaN when there is
192 // none -- which a plain market order reaches by itself, since the kernel
193 // fills a newborn market request at the path's next discrete point. The
194 // leg order is the kernel's answer for that bar
195 // (PineExecutionAdapter::source_path_uses_high_first), not a copy of it.
197 double price;
199 };
200 std::optional<InputWaypoint> next_input_waypoint(
201 const PineStrategyHost&, const NativeDecisionContext&) const noexcept;
202 const Bar* current_script_bar() const noexcept {
203 return current_script_bar_valid_ ? &current_script_bar_ : nullptr;
204 }
205 // The bar of the run's lower intrabar path the context is walking (its
206 // sub_bar_open_ms), or nullptr when the run declares no lower path or no
207 // bar of it carries that stamp. A magnified run's sub-bars are
208 // TradingView's intrabars built from the input (R5 lane MAG-INTRABAR,
209 // source::tradingview_magnifier_bars), not input bars.
210 const Bar* lower_path_bar_at(const PineStrategyHost& host,
211 const NativeDecisionContext& context) const noexcept;
212 // One intrabar of a magnified run's path is done. TradingView's magnified
213 // broker admits the open fill and one refill at EVERY intrabar's open,
214 // not only at the chart bar's first one (R5 lane MAG-INTRABAR,
215 // tests/fixtures/magnifier_intrabars/mi-coof-refill*), so the open-point
216 // count starts over with the next intrabar.
217 void sub_bar_complete() noexcept;
218 std::optional<Bar> broker_bar(const PineStrategyHost& host,
219 const NativeDecisionContext& context) const {
220 if (const Bar* sub = lower_path_bar_at(host, context)) return *sub;
221 if (const auto observed = confirmed_input_bar_at(context.sub_bar_open_ms)) return observed;
222 const auto& bars = retained_.bars;
223 const std::size_t n = bars.size();
224 if (n > 0) {
225 if (broker_bar_cursor < n && bars[broker_bar_cursor].timestamp == context.sub_bar_open_ms) {
226 return bars[broker_bar_cursor];
227 }
228 if (broker_bar_cursor >= n || bars[broker_bar_cursor].timestamp > context.sub_bar_open_ms) {
229 broker_bar_cursor = 0;
230 }
231 while (broker_bar_cursor < n && bars[broker_bar_cursor].timestamp < context.sub_bar_open_ms) {
232 ++broker_bar_cursor;
233 }
234 if (broker_bar_cursor < n && bars[broker_bar_cursor].timestamp == context.sub_bar_open_ms) {
235 return bars[broker_bar_cursor];
236 }
237 const auto found = std::find_if(bars.begin(), bars.end(),
238 [&](const Bar& bar) { return bar.timestamp == context.sub_bar_open_ms; });
239 if (found != bars.end()) {
240 broker_bar_cursor = static_cast<std::size_t>(std::distance(bars.begin(), found));
241 return *found;
242 }
243 }
244 if (retained_.is_stream && !input_is_observed_ticks_) return std::nullopt;
245 return current_script_bar_valid_ ? std::optional<Bar>{current_script_bar_}
246 : std::nullopt;
247 }
248
249 void hash_state(BrokerStateHashSink&) const;
250
251private:
252 struct RetainedBegin {
253 InputView bars;
254 std::vector<Bar> owned;
255 std::string input_tf;
256 std::string script_tf;
257 bool bar_magnifier = false;
258 int magnifier_samples = 4;
260 bool volume_weighted = false;
261 int volume_weighted_min_samples = 2;
262 int volume_weighted_max_samples = 64;
263 bool is_stream = false;
264 int warmup_n = 0;
265 bool simple_run = false;
266 };
267
268 void publish_series(const Bar&, PineStrategyHost&);
269 void update_source_series(const Bar&);
270 void reset_language();
271 // Restarts the consumed-prefix digests below (the retained input changed).
272 void reset_consumed_digests() noexcept;
273 void fold_consumed_digests() const noexcept;
274 void snapshot_coof_script_state(PineStrategyHost&);
275 void restore_coof_script_state(PineStrategyHost&);
276 void commit_coof_script_state(PineStrategyHost&);
277
278 struct DeferredBoundaryInput {
279 Bar bar{};
280 std::int64_t next_input_ms = 0;
281 std::int64_t prior_script_open_ms = 0;
282 bool calling_bar_complete = false;
283 bool all_security_states = false;
284 bool active = false;
285 };
286
287 // Derived lookup cursor over retained_.bars (reset-and-rescan in
288 // broker_bar()); waived from the state hash by
289 // scripts/check_broker_state_hash_coverage.py::SCHEDULER_CACHE_WAIVERS.
290 mutable std::size_t broker_bar_cursor = 0;
291
292 // Derived completion lookup over retained_.bars. Repeated or backward
293 // input indices are answered by the cursor's last result or a rescan.
294 // Waived as a cache; the independent consumed-prefix cursor folds below.
295 mutable std::unique_ptr<ScriptBucketCursor> input_script_cursor;
296
297 // @source-state begin
298 PineLanguageState language_;
299 RetainedBegin retained_;
300 // The hash retains its old consumed count after bars becomes {nullptr,0}.
301 // Unconsumed future input remains transport, as in the v3 hash domain.
302 std::size_t retained_n_ = 0;
303 std::int64_t current_script_open_ms_ = 0;
304 Bar current_script_bar_{};
305 bool current_script_bar_valid_ = false;
306 bool saw_open_fill_ = false;
307 int open_point_fills_ = 0;
308 int source_bar_count_ = 0;
309 int expected_source_bars_ = 0;
310 std::uint64_t applied_cursor_ = 0;
311 std::int64_t coof_callback_script_open_ = std::numeric_limits<std::int64_t>::min();
312 std::int64_t last_published_script_open_ms_ =
313 std::numeric_limits<std::int64_t>::min();
314 std::int64_t prior_input_script_open_ms_ = std::numeric_limits<std::int64_t>::min();
315 std::int64_t awaiting_legacy_script_open_ms_ = std::numeric_limits<std::int64_t>::min();
316 std::int64_t last_stream_input_open_ms_ = std::numeric_limits<std::int64_t>::min();
317 // The run-begin preview's extent, including unconsumed transport. Only
318 // its consumed prefix is hashed, just as for the former flag vectors.
319 std::size_t input_script_size_ = 0;
320 bool uses_aux_security_feed_ = false;
321 bool input_is_observed_ticks_ = false;
322 std::array<Bar, 8> confirmed_input_bars_{};
323 std::size_t confirmed_input_next_ = 0;
324 std::size_t confirmed_input_count_ = 0;
325 std::array<Bar, 2> confirmed_open_input_bars_{};
326 std::size_t confirmed_open_input_next_ = 0;
327 std::size_t confirmed_open_input_count_ = 0;
328 DeferredBoundaryInput deferred_boundary_input_{};
329 // R5 lane V19-E: running digests of the consumed input prefix -- the bars,
330 // their script completions and their boundary completions -- each element
331 // folded once, the first time the state hash reads past it (the v2
332 // scheduler fold re-read the whole prefix at every read).
333 mutable std::size_t consumed_bars_folded_ = 0;
334 mutable std::uint64_t consumed_bars_digest_ = 1469598103934665603ULL;
335 mutable std::size_t consumed_completes_folded_ = 0;
336 mutable std::uint64_t consumed_completes_digest_ = 1469598103934665603ULL;
337 mutable std::size_t consumed_boundaries_folded_ = 0;
338 mutable std::uint64_t consumed_boundaries_digest_ = 1469598103934665603ULL;
339 mutable std::unique_ptr<ScriptBucketCursor> consumed_script_cursor_;
340 // @source-state end
341};
342
343} // namespace pineforge::source
const Bar & operator[](std::size_t i) const noexcept
void tick(const Bar &, const NativeTickContext &, PineStrategyHost &)
void retain_confirmed_input(const Bar &bar, std::int64_t script_open_ms=std::numeric_limits< std::int64_t >::min()) noexcept
int source_bar_count() const noexcept
std::optional< Bar > retained_input_from(int input_index, std::int64_t at_or_after) const
std::optional< Bar > broker_bar(const PineStrategyHost &host, const NativeDecisionContext &context) const
std::optional< Bar > confirmed_input_bar_at(std::int64_t timestamp) const noexcept
double script_position_view(int bar_index, PositionSide side, double quantity) const noexcept
bool coof_recalculation_due(const native_order::ExecutionAppliedEvent &, const NativeDecisionContext &, PineStrategyHost &) const
bool is_first_tick() const noexcept
bool bar_magnifier_enabled() const noexcept
void fixture_publish_source_series(const Bar &, bool new_history_slot)
const Bar * current_script_bar() const noexcept
void recalculate(const native_order::ExecutionAppliedEvent &, const NativeDecisionContext &, PineStrategyHost &)
std::optional< std::int64_t > last_published_script_open() const noexcept
std::optional< Bar > next_source_bar(int interval_index) const
int bar_index_offset() const noexcept
bool retains_stream() const noexcept
static bool confirmed_script_interval_complete(const NativeInputContext &context) noexcept
const Series< double > & source_series(const std::string &) const
bool terminal_source_bar() const noexcept
bool input_is_observed_ticks() const noexcept
bool retained_input_is_chart() const noexcept
int source_bar_index_for(const NativeDecisionContext &context) const noexcept
void bar_open(const Bar &, const NativeDecisionContext &, PineStrategyHost &)
PineLanguageState & language() noexcept
void set_source_series_active(bool value) noexcept
std::optional< InputWaypoint > next_input_waypoint(const PineStrategyHost &, const NativeDecisionContext &) const noexcept
void hash_state(BrokerStateHashSink &) const
InputView retained_input() const noexcept
std::size_t confirmed_input_count() const noexcept
void capture_begin(const NativeBeginArgs &)
void freeze_script_position_view(int bar_index, PositionSide side, double quantity, const std::vector< PyramidEntry > &lots)
void applied(const native_order::ExecutionAppliedEvent &)
void bar(const Bar &, const NativeDecisionContext &, PineStrategyHost &)
const Bar * lower_path_bar_at(const PineStrategyHost &host, const NativeDecisionContext &context) const noexcept
void set_bar_index_offset(int value) noexcept
bool is_last_tick() const noexcept
bool security_series_slot_is_new(int) const noexcept
double previous_chart_close() const noexcept
bool history_advances_new_bar() const noexcept
MagnifierDistribution
Definition magnifier.hpp:7
Borrowed begin-call facts.
Accepted input facts presented before the generic consumer aggregates the bar into its script interva...
One accepted realtime print before native matching at its current decision point.