PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
pine_scheduler_native.cpp
Go to the documentation of this file.
2
5
6#include "pine_host_reads.hpp"
10
11#include <algorithm>
12#include <cstdint>
13#include <limits>
14#include <stdexcept>
15#include <utility>
16
17namespace pineforge::source {
18
21
23 RetainedBegin next;
24 if (args.bars && args.n > 0) {
25 if (args.is_stream) next.owned.assign(args.bars, args.bars + args.n);
26 next.bars = InputView(args.is_stream ? next.owned.data() : args.bars,
27 static_cast<std::size_t>(args.n));
28 }
29 next.input_tf = args.input_tf; next.script_tf = args.script_tf;
30 next.bar_magnifier = args.bar_magnifier; next.magnifier_samples = args.magnifier_samples;
31 next.distribution = args.magnifier_distribution;
32 next.volume_weighted = args.magnifier_volume_weighted;
33 next.volume_weighted_min_samples = args.magnifier_volume_weighted_min_samples;
34 next.volume_weighted_max_samples = args.magnifier_volume_weighted_max_samples;
35 next.is_stream = args.is_stream; next.warmup_n = args.warmup_n;
36 next.simple_run = args.simple_run;
37 retained_ = std::move(next);
38 retained_n_ = retained_.bars.size();
39 confirmed_input_bars_ = {};
40 confirmed_input_next_ = 0;
41 confirmed_input_count_ = 0;
42 confirmed_open_input_bars_ = {};
43 confirmed_open_input_next_ = 0;
44 confirmed_open_input_count_ = 0;
45 reset_consumed_digests();
46}
47
49 if (retained_.is_stream) return;
50 fold_consumed_digests();
51 retained_.bars = {};
52}
53
54void PineScheduler::reset_consumed_digests() noexcept {
55 consumed_script_cursor_.reset();
56 consumed_bars_folded_ = 0;
57 consumed_bars_digest_ = 1469598103934665603ULL;
58 consumed_completes_folded_ = 0;
59 consumed_completes_digest_ = 1469598103934665603ULL;
60 consumed_boundaries_folded_ = 0;
61 consumed_boundaries_digest_ = 1469598103934665603ULL;
62}
63
64void PineScheduler::reset_language() {
65 language_.reset_for_run();
66 language_.pos_view_freeze_bar_ = -1;
68 language_.pos_view_frozen_qty_ = 0.0;
69 language_.pos_view_frozen_entry_qty_.clear();
70 language_.is_first_tick_ = true; language_.is_last_tick_ = true;
71 language_.history_slot_is_new_ = true; language_.coof_checkpoint_contains_current_bar_ = false;
77 current_script_open_ms_ = 0; saw_open_fill_ = false; open_point_fills_ = 0;
78 current_script_bar_ = {}; current_script_bar_valid_ = false;
79 source_bar_count_ = 0; expected_source_bars_ = 0; applied_cursor_ = 0;
80 coof_callback_script_open_ = std::numeric_limits<std::int64_t>::min();
81 last_published_script_open_ms_ = std::numeric_limits<std::int64_t>::min();
82 prior_input_script_open_ms_ = std::numeric_limits<std::int64_t>::min();
83 awaiting_legacy_script_open_ms_ = std::numeric_limits<std::int64_t>::min();
84 last_stream_input_open_ms_ = std::numeric_limits<std::int64_t>::min();
85 input_script_cursor.reset();
86 input_script_size_ = 0;
87 uses_aux_security_feed_ = false;
88 input_is_observed_ticks_ = false;
89 confirmed_input_bars_ = {};
90 confirmed_input_next_ = 0;
91 confirmed_input_count_ = 0;
92 confirmed_open_input_bars_ = {};
93 confirmed_open_input_next_ = 0;
94 confirmed_open_input_count_ = 0;
95 deferred_boundary_input_ = {};
96 reset_consumed_digests();
97}
98
99void PineScheduler::snapshot_coof_script_state(PineStrategyHost& host) {
100 if (language_._src_series_active_) {
101 language_.coof_checkpoint_src_open_ = language_._src_open_;
102 language_.coof_checkpoint_src_high_ = language_._src_high_;
103 language_.coof_checkpoint_src_low_ = language_._src_low_;
104 language_.coof_checkpoint_src_close_ = language_._src_close_;
105 language_.coof_checkpoint_src_volume_ = language_._src_volume_;
106 language_.coof_checkpoint_src_hl2_ = language_._src_hl2_;
107 language_.coof_checkpoint_src_hlc3_ = language_._src_hlc3_;
108 language_.coof_checkpoint_src_ohlc4_ = language_._src_ohlc4_;
109 language_.coof_checkpoint_src_hlcc4_ = language_._src_hlcc4_;
110 }
111 language_.coof_checkpoint_prev_chart_close_ = language_.prev_chart_close_;
112 language_.coof_checkpoint_last_chart_close_ = language_.last_chart_close_;
113 language_.coof_checkpoint_contains_current_bar_ = false;
114 host.snapshot_script_state();
115}
116
117void PineScheduler::restore_coof_script_state(PineStrategyHost& host) {
118 if (language_._src_series_active_) {
119 language_._src_open_ = language_.coof_checkpoint_src_open_;
120 language_._src_high_ = language_.coof_checkpoint_src_high_;
121 language_._src_low_ = language_.coof_checkpoint_src_low_;
122 language_._src_close_ = language_.coof_checkpoint_src_close_;
123 language_._src_volume_ = language_.coof_checkpoint_src_volume_;
124 language_._src_hl2_ = language_.coof_checkpoint_src_hl2_;
125 language_._src_hlc3_ = language_.coof_checkpoint_src_hlc3_;
126 language_._src_ohlc4_ = language_.coof_checkpoint_src_ohlc4_;
127 language_._src_hlcc4_ = language_.coof_checkpoint_src_hlcc4_;
128 }
129 language_.prev_chart_close_ = language_.coof_checkpoint_prev_chart_close_;
130 language_.last_chart_close_ = language_.coof_checkpoint_last_chart_close_;
131 host.restore_script_state();
132}
133
134void PineScheduler::commit_coof_script_state(PineStrategyHost& host) {
135 if (language_._src_series_active_) {
136 language_.coof_checkpoint_src_open_ = language_._src_open_;
137 language_.coof_checkpoint_src_high_ = language_._src_high_;
138 language_.coof_checkpoint_src_low_ = language_._src_low_;
139 language_.coof_checkpoint_src_close_ = language_._src_close_;
140 language_.coof_checkpoint_src_volume_ = language_._src_volume_;
141 language_.coof_checkpoint_src_hl2_ = language_._src_hl2_;
142 language_.coof_checkpoint_src_hlc3_ = language_._src_hlc3_;
143 language_.coof_checkpoint_src_ohlc4_ = language_._src_ohlc4_;
144 language_.coof_checkpoint_src_hlcc4_ = language_._src_hlcc4_;
145 }
146 language_.coof_checkpoint_prev_chart_close_ = language_.prev_chart_close_;
147 language_.coof_checkpoint_last_chart_close_ = language_.last_chart_close_;
148 language_.coof_checkpoint_contains_current_bar_ = true;
149 host.commit_script_state();
150}
151
152namespace {
153// The script bars a SELECTED admitted batch run executes over its retained
154// primary rows: the kernel's count_selected_executed_primary, which walks them as
155// the native consumer does (the groups that open before T, the last pending one
156// completed at the accepted T, and the window groups its seal rules execute; an
157// unsealed window tail adds none). The request is the run's own: its window (E
158// unchecked: it bounds no such group), its spec's timeframes, timezone and session,
159// the host's retained chart timezone and its slot-label policy. A refusal of the
160// count after admission, or a count the scheduler's int cannot hold, is the
161// engine's fault and fails as engine_invariant; no count is guessed.
162int selected_executed_source_bars(const PineStrategyHost& host, const NativeRunSpec* spec,
163 const pf_selected_window_config_v1& window,
164 PineScheduler::InputView retained) {
165 if (spec == nullptr) {
166 throw coded<std::logic_error>(RunFailureCode::engine_invariant, {},
167 "selected run begin without a running spec for its "
168 "source tail count");
169 }
170 SelectedRetainedRequest request;
171 request.start_ms = window.start_ms;
172 request.end_ms = window.end_ms;
173 request.input_tf = spec->input_tf;
174 request.script_tf = spec->script_tf;
175 request.chart_timezone = host.chart_timezone();
176 request.engine_timezone = spec->timezone;
177 request.session = spec->session;
178 request.feed_tolerant = spec->slot_label_policy == NativeSlotLabelPolicy::FeedTolerant;
179 const SelectedRetainedCount counted =
180 count_selected_executed_primary(retained.data(), retained.size(), request);
181 if (counted.status != SelectedPlanStatus::Ok
182 || counted.script_bars > static_cast<std::uint64_t>(std::numeric_limits<int>::max())) {
183 throw coded<std::logic_error>(RunFailureCode::engine_invariant, {},
184 "selected run source tail count refused or out of range "
185 "after admission");
186 }
187 return static_cast<int>(counted.script_bars);
188}
189} // namespace
190
192 reset_language();
193 const bool static_eligible = !retained_.is_stream && !retained_.bar_magnifier
194 && retained_.input_tf.empty() && retained_.script_tf.empty();
195 const auto state = host.native_state();
196 const bool undetected = state.spec && state.spec->timeframe_undetected;
197 const int ratio = undetected || !state.spec ? 1
198 : tf_ratio(state.spec->input_tf, state.spec->script_tf);
199 const bool needs_aggregation = ratio > 1 || ratio == -1;
200 host.diag_script_tf_ratio_ = ratio;
201 host.diag_needs_aggregation_ = needs_aggregation;
202 input_script_size_ = retained_.bars.size();
203 expected_source_bars_ = static_cast<int>(input_script_size_);
204 // Count once without retaining flags. Input delivery and consumed-prefix
205 // hashing each get their own walk of the very same kernel producer.
206 // Ratio 1 uses complete=1/boundary=0 without constructing any cursor.
207 if (needs_aggregation && state.spec) {
208 ScriptBucketCursor counting(*state.spec);
209 expected_source_bars_ = 0;
210 for (std::size_t i = 0; i < input_script_size_; ++i)
211 expected_source_bars_ += counting.next(retained_.bars[i]).complete != 0U ? 1 : 0;
212 input_script_cursor = std::make_unique<ScriptBucketCursor>(*state.spec);
213 consumed_script_cursor_ = std::make_unique<ScriptBucketCursor>(*state.spec);
214 }
215 // A selected admitted batch run executes the script bars of the consumer's
216 // own bucket walk, which the aggregator's per-input hint above does not
217 // model: the last pending pre-T group is completed at the accepted T and an
218 // unsealed window tail is never executed. The tail expectations
219 // (terminal_source_bar, the probe tail) read the executed count. An OFF run
220 // and a stream never reach this and keep the hint; completion cursors are
221 // unchanged either way.
222 if (!retained_.is_stream && state.phase == NativeRunPhase::Batch) {
224 if (NativeExecutionConsumer::selected_window_admitted(host, &window)) {
225 expected_source_bars_ =
226 selected_executed_source_bars(host, state.spec, window, retained_.bars);
227 }
228 }
229 host.stream_warmup_mode_ = retained_.is_stream;
230 host.scheduler_prepare_script_run(retained_.bars, static_eligible,
231 expected_source_bars_, !needs_aggregation);
232 // ab9714be pine_scheduler.cpp:717-804 (legacy_run_simple) never configures
233 // the request.security evaluator surface; :1249-1323 (run_tf_impl, reached
234 // by every timeframe-aware, magnified or stream overload, including the
235 // generated wrapper's run_backtest / run_backtest_full with EMPTY
236 // timeframes) configures it unconditionally after prepare_script_run()
237 // received the static-eligibility flag. Empty timeframes only request
238 // auto-detection; only the bare run(bars, n) overload skips the surface,
239 // and the kernel reports that overload as NativeBeginArgs::simple_run.
240 // (R4-D L10o: gating on static eligibility left every request.security
241 // series na whenever a caller auto-detected the timeframe.)
242 if (!retained_.simple_run) host.scheduler_configure_security_evaluators();
243 uses_aux_security_feed_ = host.scheduler_uses_aux_security_feed();
244 host.scheduler_prepare_security_sequence(retained_.bars);
245}
246
248 // Matching at a new script-bar open precedes the terminal source callback;
249 // all fills after that callback (including COOF/POOC notifications) belong
250 // to the already-published source index. This is the same cadence the
251 // legacy aggregation loop used for Trade.entry_bar_index/exit_bar_index.
252 // A stream's quiet carried interval is matched at its own open before it
253 // joins a script bar, while the context still names the bar before it:
254 // that fill belongs to the bar the carried interval opens into (lane F1).
255 const std::int64_t script_open_ms =
256 context.coordinate.provenance == NativePriceProvenance::CarriedOpen
257 ? context.coordinate.open_ms : context.script_bar_open_ms;
258 const bool published = last_published_script_open_ms_ == script_open_ms;
259 const bool coof_published = coof_callback_script_open_ == script_open_ms;
260 if (published || coof_published)
261 return std::max(0, source_bar_count_ - 1);
262 return source_bar_count_;
263}
264
266 const PineStrategyHost& host, const NativeDecisionContext& context) const noexcept {
267 const NativeRunSpec* spec = detail::run_spec(host);
268 const auto* lower = spec ? spec->intrabar.lower() : nullptr;
269 if (!lower) return nullptr;
270 const auto views = detail::run_consumer(host).bar_views();
271 if (views.lower_path != nullptr) {
272 const auto segment = views.lower_segment;
273 if (segment.size == 0) return nullptr;
274 const auto found = std::lower_bound(
275 segment.data, segment.data + segment.size, context.sub_bar_open_ms,
276 [](const Bar& bar, std::int64_t time) { return bar.timestamp < time; });
277 return found != segment.data + segment.size && found->timestamp == context.sub_bar_open_ms
278 ? found : nullptr;
279 }
280 // preflight_intrabar_path admitted the path only with strictly
281 // increasing stamps.
282 const auto found = std::lower_bound(
283 lower->bars.begin(), lower->bars.end(), context.sub_bar_open_ms,
284 [](const Bar& bar, std::int64_t time) { return bar.timestamp < time; });
285 if (found == lower->bars.end() || found->timestamp != context.sub_bar_open_ms)
286 return nullptr;
287 return &*found;
288}
289
291 if (!retained_.bar_magnifier) return;
292 saw_open_fill_ = false;
293 open_point_fills_ = 0;
294}
295
296namespace {
297// A one-price bar that traded nothing: a print of a Pine magnified path (the
298// chart bar's own open or close, source::tradingview_magnifier_bars).
299bool single_print(const Bar& bar) noexcept {
300 return bar.volume == 0.0 && bar.high == bar.open && bar.low == bar.open
301 && bar.close == bar.open;
302}
303} // namespace
304
305std::optional<PineScheduler::InputWaypoint> PineScheduler::next_input_waypoint(
306 const PineStrategyHost& host, const NativeDecisionContext& context) const noexcept {
307 // The sub-bar being walked is a bar of the run's lower path -- one of
308 // TradingView's intrabars since R5 lane MAG-INTRABAR, no longer always an
309 // input bar -- so it is looked up there.
310 const Bar* found = lower_path_bar_at(host, context);
311 if (!found) return std::nullopt;
312 // TradingView's magnified broker fills a fill recalculation's order at
313 // the next of the path's fill points: the chart bar's own open (a print,
314 // walked once), then each intrabar's open, first extreme and second
315 // extreme -- an intrabar's close is not one, the next intrabar's open
316 // follows its second extreme (tests/fixtures/magnifier_intrabars/
317 // mi-coof-refill*, 72 of 72 cascades; R5 lane MAG-INTRABAR).
318 const auto* spec = detail::run_spec(host);
319 const auto views = detail::run_consumer(host).bar_views();
320 const Bar* successor = nullptr;
321 if (views.lower_path != nullptr) {
322 successor = found + 1 < views.lower_segment.data + views.lower_segment.size
323 ? found + 1 : views.lower_path->next_after_segment();
324 } else {
325 const auto& path = spec->intrabar.lower()->bars;
326 if (found + 1 < path.data() + path.size()) successor = found + 1;
327 }
328 const InputWaypoint next_open{successor ? successor->open
329 : std::numeric_limits<double>::quiet_NaN(), true};
330 if (single_print(*found)) return next_open;
331 const bool high_first = host.adapter_.source_path_uses_high_first(*found);
332 const NativePathPhase phase[] = {
333 NativePathPhase::Open,
334 high_first ? NativePathPhase::High : NativePathPhase::Low,
335 high_first ? NativePathPhase::Low : NativePathPhase::High,
336 NativePathPhase::Close,
337 };
338 const double price[] = {
339 found->open,
340 high_first ? found->high : found->low,
341 high_first ? found->low : found->high,
342 found->close,
343 };
344 for (int index = 0; index < 4; ++index) {
345 if (phase[index] != context.coordinate.path_phase) continue;
346 // A fill AT an extreme books that print's tick, which can sit a
347 // binary64 ulp off the print (coof_fill_at_path_point): it is at the
348 // waypoint, not short of it.
349 if (index > 0 && index < 3 && !host.adapter_.coof_fill_at_path_point(price[index]))
350 return InputWaypoint{price[index], false};
351 // The matcher's fill of a resting request at its own level AT the
352 // intrabar's second extreme ends the leg there, and the
353 // recalculation's market order fills at that extreme, as on a chart
354 // bar (PineExecutionAdapter::coof_fill_at_second_extreme, H-MEASURE
355 // Finding 6d): TradingView's hm-mag-diff-v2 / -v3 bar 19 book ML at
356 // 11.72, where PX's limit filled on the 2-minute intrabar's high. A
357 // fill the adapter forced there -- a cascade's market order -- still
358 // takes the next intrabar's open (R5 lane INT27).
359 if (index == 2 && !host.adapter_.coof_fill_forced_)
360 return InputWaypoint{price[index], false};
361 if (index < 2) return InputWaypoint{price[index + 1], false};
362 return next_open;
363 }
364 return std::nullopt;
365}
366
367void PineScheduler::publish_series(const Bar& bar, PineStrategyHost& host) {
368 (void)host;
369 update_source_series(bar);
370}
371
372void PineScheduler::update_source_series(const Bar& bar) {
373 if (language_.history_slot_is_new_)
374 language_.prev_chart_close_ = language_.last_chart_close_;
375 language_.last_chart_close_ = bar.close;
376 if (!language_._src_series_active_) return;
377 const double hl2 = (bar.high + bar.low) / 2.0;
378 const double hlc3 = (bar.high + bar.low + bar.close) / 3.0;
379 const double ohlc4 = (bar.open + bar.high + bar.low + bar.close) / 4.0;
380 const double hlcc4 = (bar.high + bar.low + bar.close + bar.close) / 4.0;
381 if (language_.history_slot_is_new_) {
382 language_._src_open_.push(bar.open);
383 language_._src_high_.push(bar.high);
384 language_._src_low_.push(bar.low);
385 language_._src_close_.push(bar.close);
386 language_._src_volume_.push(bar.volume);
387 language_._src_hl2_.push(hl2);
388 language_._src_hlc3_.push(hlc3);
389 language_._src_ohlc4_.push(ohlc4);
390 language_._src_hlcc4_.push(hlcc4);
391 return;
392 }
393 language_._src_open_.update(bar.open);
394 language_._src_high_.update(bar.high);
395 language_._src_low_.update(bar.low);
396 language_._src_close_.update(bar.close);
397 language_._src_volume_.update(bar.volume);
398 language_._src_hl2_.update(hl2);
399 language_._src_hlc3_.update(hlc3);
400 language_._src_ohlc4_.update(ohlc4);
401 language_._src_hlcc4_.update(hlcc4);
402}
403
405 int bar_index, PositionSide side, double quantity) const noexcept {
406 if (language_.pos_view_freeze_bar_ == bar_index) {
407 if (language_.pos_view_frozen_side_ == PositionSide::LONG)
408 return language_.pos_view_frozen_qty_;
409 if (language_.pos_view_frozen_side_ == PositionSide::SHORT)
410 return -language_.pos_view_frozen_qty_;
411 return 0.0;
412 }
413 if (side == PositionSide::LONG) return quantity;
414 if (side == PositionSide::SHORT) return -quantity;
415 return 0.0;
416}
417
419 int bar_index, PositionSide side, double quantity,
420 const std::vector<PyramidEntry>& lots) {
421 if (language_.pos_view_freeze_bar_ == bar_index) return;
422 language_.pos_view_freeze_bar_ = bar_index;
423 language_.pos_view_frozen_side_ = side;
424 language_.pos_view_frozen_qty_ = quantity;
425 language_.pos_view_frozen_entry_qty_.clear();
426 for (const auto& lot : lots)
427 language_.pos_view_frozen_entry_qty_[lot.entry_id] += lot.qty;
428}
429
431 language_.pos_view_freeze_bar_ = -1;
432}
433
434const Series<double>& PineScheduler::source_series(const std::string& key) const {
435 if (key == "open") return language_._src_open_;
436 if (key == "high") return language_._src_high_;
437 if (key == "low") return language_._src_low_;
438 if (key == "close") return language_._src_close_;
439 if (key == "volume") return language_._src_volume_;
440 if (key == "hl2") return language_._src_hl2_;
441 if (key == "hlc3") return language_._src_hlc3_;
442 if (key == "ohlc4") return language_._src_ohlc4_;
443 if (key == "hlcc4") return language_._src_hlcc4_;
445 "unknown source series");
446}
447
448void PineScheduler::fixture_publish_source_series(const Bar& bar, bool new_history_slot) {
449 language_.history_slot_is_new_ = new_history_slot;
450 language_.is_first_tick_ = new_history_slot;
451 update_source_series(bar);
452}
453
455 const Bar& bar, const NativeInputContext& context, PineStrategyHost& host) {
456 input_is_observed_ticks_ = false;
458 const bool retained_input = context.input_index >= 0
459 && static_cast<std::size_t>(context.input_index) < input_script_size_;
460 ScriptBucketCursor::Flags flags{};
461 if (retained_input && input_script_cursor) {
462 const auto index = static_cast<std::size_t>(context.input_index);
463 auto& cursor = *input_script_cursor;
464 if (cursor.position() > index + 1) cursor.reset();
465 while (cursor.position() <= index)
466 (void)cursor.next(retained_.bars[cursor.position()]);
467 flags = cursor.last();
468 }
469 // Advance before routed, auxiliary and deferred-input early returns too.
470 struct InputBarIndexScope {
471 PineStrategyHost& host;
472 int previous;
473 explicit InputBarIndexScope(PineStrategyHost& value, int index)
474 : host(value), previous(value.bar_index_) { host.bar_index_ = index; }
475 ~InputBarIndexScope() { host.bar_index_ = previous; }
476 } input_bar_index(host, context.input_index);
477 // The auxiliary slice is fed per chart bar (scheduler_feed_aux_security);
478 // sites the kernel steps (declare_security_sites_to_kernel) are fed by
479 // its own pump, right after this callback returns.
480 if (uses_aux_security_feed_ || host.security_sites_kernel_routed()) {
481 prior_input_script_open_ms_ = context.script_interval.open_ms;
482 return;
483 }
484 std::int64_t next_input_ms = 0;
485 if (context.input_index >= 0
486 && context.input_index + 1 < static_cast<int>(retained_.bars.size())) {
487 next_input_ms = retained_.bars[static_cast<std::size_t>(context.input_index + 1)].timestamp;
488 }
489 if (retained_.bar_magnifier && deferred_boundary_input_.active
490 && deferred_boundary_input_.prior_script_open_ms != context.script_interval.open_ms) {
491 // A sparse lower feed reveals the completed caller only when the next
492 // child arrives. Feed the retained final child first, then let the
493 // new caller's child proceed in timestamp order.
494 const std::int64_t completed_script_open =
495 deferred_boundary_input_.prior_script_open_ms;
496 (void)host.scheduler_feed_security_input(
497 deferred_boundary_input_.bar, deferred_boundary_input_.next_input_ms,
498 true, false);
499 // Keep the new caller's first child out of request.security until the
500 // completed caller's chart callback has observed the published value.
501 deferred_boundary_input_.bar = bar;
502 deferred_boundary_input_.next_input_ms = next_input_ms;
503 deferred_boundary_input_.prior_script_open_ms = completed_script_open;
504 deferred_boundary_input_.calling_bar_complete = false;
505 deferred_boundary_input_.all_security_states = true;
506 deferred_boundary_input_.active = true;
507 prior_input_script_open_ms_ = context.script_interval.open_ms;
508 return;
509 }
510 // The generic calendar may wait for a later tradable opening before it
511 // seals a script interval. The source chart aggregator can have already
512 // completed that interval on the prior raw bar. Keep the new raw input
513 // out of request.security until the pending script callback observes the
514 // same legacy point; then feed it immediately after that callback.
515 if (!retained_.bar_magnifier && awaiting_legacy_script_open_ms_
516 != std::numeric_limits<std::int64_t>::min()) {
517 deferred_boundary_input_.bar = bar;
518 deferred_boundary_input_.next_input_ms = next_input_ms;
519 deferred_boundary_input_.prior_script_open_ms = awaiting_legacy_script_open_ms_;
520 deferred_boundary_input_.calling_bar_complete = false;
521 deferred_boundary_input_.all_security_states = true;
522 deferred_boundary_input_.active = true;
523 prior_input_script_open_ms_ = context.script_interval.open_ms;
524 return;
525 }
526
527 bool calling_bar_complete = context.completes_script_interval;
528 bool boundary = prior_input_script_open_ms_
529 != std::numeric_limits<std::int64_t>::min()
530 && prior_input_script_open_ms_ != context.script_interval.open_ms;
531 if (retained_input) {
532 calling_bar_complete = flags.complete != 0U;
533 boundary = flags.boundary != 0U;
534 } else if (retained_.is_stream && context.input_index >= 0) {
535 calling_bar_complete = confirmed_script_interval_complete(context);
536 boundary = false;
537 }
538 // The final sparse magnifier child is a partial requested bucket. The
539 // legacy lower-TF pump does not promote that tail to a completed
540 // request.security value merely because the input array ended.
541 if (retained_.bar_magnifier && next_input_ms == 0)
542 calling_bar_complete = false;
543 bool security_boundary_ahead = false;
544 if (retained_.bar_magnifier && next_input_ms != 0) {
545 const bool caller_boundary = tf_change(
546 bar.timestamp, next_input_ms, retained_.script_tf,
547 host.syminfo_.timezone, host.syminfo_.session);
548 if (caller_boundary) {
549 for (const auto& state : host.security_eval_states_) {
550 if (host.pine_security_state(state.sec_id).publish_gate_tf_seconds > 0) {
551 security_boundary_ahead = true;
552 break;
553 }
554 }
555 }
556 }
557 if (security_boundary_ahead) {
558 deferred_boundary_input_.bar = bar;
559 deferred_boundary_input_.next_input_ms = next_input_ms;
560 deferred_boundary_input_.prior_script_open_ms = context.script_interval.open_ms;
561 deferred_boundary_input_.calling_bar_complete = true;
562 deferred_boundary_input_.all_security_states = false;
563 deferred_boundary_input_.active = true;
564 prior_input_script_open_ms_ = context.script_interval.open_ms;
565 return;
566 }
567 const bool deferred_gate = host.scheduler_feed_security_input(
568 bar, next_input_ms, calling_bar_complete,
569 boundary && !retained_.bar_magnifier);
570 if (deferred_gate) {
571 deferred_boundary_input_.bar = bar;
572 deferred_boundary_input_.next_input_ms = next_input_ms;
573 deferred_boundary_input_.prior_script_open_ms = prior_input_script_open_ms_;
574 deferred_boundary_input_.calling_bar_complete = false;
575 deferred_boundary_input_.all_security_states = false;
576 deferred_boundary_input_.active = true;
577 }
578 if (calling_bar_complete && !retained_.bar_magnifier) {
579 awaiting_legacy_script_open_ms_ = boundary
580 ? prior_input_script_open_ms_ : context.script_interval.open_ms;
581 }
582 prior_input_script_open_ms_ = context.script_interval.open_ms;
583}
584
585void PineScheduler::tick(const Bar& bar, const NativeTickContext& context,
586 PineStrategyHost& host) {
587 if (!retained_.is_stream) return;
588 input_is_observed_ticks_ = true;
589 const auto& interval = context.decision.input_interval;
590 if (last_stream_input_open_ms_ == interval.open_ms) return;
591 if (prior_input_script_open_ms_ != std::numeric_limits<std::int64_t>::min()
592 && prior_input_script_open_ms_ != context.decision.script_interval.open_ms) {
593 // A realtime tick can be the first child of the next caller after a
594 // sparse warmup. The legacy stream replays the retained final child
595 // at this boundary (without advancing the requested-context slot).
596 host.scheduler_publish_security_boundary();
597 }
598 last_stream_input_open_ms_ = interval.open_ms;
599 const auto& script = context.decision.script_interval;
600 const bool complete = interval.next_period_open_ms >= script.next_period_open_ms;
601 (void)host.scheduler_feed_security_input(
602 bar, interval.next_input_open_ms, complete, false);
603 prior_input_script_open_ms_ = script.open_ms;
604}
605
606void PineScheduler::bar_open(const Bar& value, const NativeDecisionContext& context,
607 PineStrategyHost& host) {
608 if (context.script_bar_open_ms != current_script_open_ms_) {
609 current_script_open_ms_ = context.script_bar_open_ms;
610 saw_open_fill_ = false;
611 open_point_fills_ = 0;
612 coof_callback_script_open_ = std::numeric_limits<std::int64_t>::min();
613 if (host.scheduler_coof_enabled()) snapshot_coof_script_state(host);
614 }
615 if (!retained_.is_stream) {
616 current_script_bar_ = value;
617 current_script_bar_.timestamp = context.script_bar_open_ms;
618 current_script_bar_valid_ = true;
619 }
620}
621
622void PineScheduler::bar(const Bar& value, const NativeDecisionContext& context, PineStrategyHost& host) {
623 // P7c: matching advances over every sub-bar; the language callback occurs
624 // only at the terminal sub-bar with the script bar timestamp restored.
625 language_.is_first_tick_ = context.is_terminal_sub_bar;
626 language_.is_last_tick_ = context.is_terminal_sub_bar;
627 language_.history_slot_is_new_ = context.is_terminal_sub_bar;
628 if (!context.is_terminal_sub_bar) return;
629 // A FeedTolerant native stream can emit a quiet carried callback for
630 // the calendar-aligned slot immediately preceding the last raw-label
631 // warmup bar. The retired source stream starts its realtime cadence from
632 // that warmup bar's next source slot, so it never published this stale
633 // callback to generated code.
634 if (retained_.is_stream && current_script_bar_valid_
635 && context.script_bar_open_ms <= current_script_bar_.timestamp) {
636 // The generic driver still dispatched a script point. Preserve the
637 // public one-hash-per-dispatch accounting without exposing it to the
638 // source callback cadence.
639 host.scheduler_record_broker_hash();
640 return;
641 }
642 const bool coof = host.scheduler_coof_enabled();
643 const bool had_coof_recalc = coof_callback_script_open_
644 == context.script_bar_open_ms;
645 Bar script_bar = value;
646 script_bar.timestamp = context.script_bar_open_ms;
647 if (retained_.is_stream && input_is_observed_ticks_
648 && !retained_.bars.empty() && calendar_period_for(retained_.script_tf) == CalendarPeriod::DAY
649 && tf_to_seconds(retained_.input_tf) == 86400
650 && host.syminfo_.session == "1800-1700"
651 && host.syminfo_.timezone == "America/New_York") {
652 const auto stamp = session_period_open_ms(script_bar.timestamp,
653 host.syminfo_.timezone, host.syminfo_.session, CalendarPeriod::DAY);
654 const auto next_stamp = session_period_open_ms(retained_.bars.back().timestamp + 86400000LL,
655 host.syminfo_.timezone, host.syminfo_.session, CalendarPeriod::DAY);
656 script_bar.timestamp = std::max(stamp, next_stamp);
657 }
658 current_script_bar_ = script_bar;
659 current_script_bar_valid_ = true;
660 const bool completes_awaiting_legacy_script = awaiting_legacy_script_open_ms_
661 == context.script_bar_open_ms;
662 if (deferred_boundary_input_.active
663 && !deferred_boundary_input_.all_security_states
664 && deferred_boundary_input_.prior_script_open_ms == context.script_bar_open_ms) {
665 // The legacy magnifier feeds the boundary-triggering lower bar after
666 // the completed script callback; it does not replay the caller at the
667 // callback boundary. The ordinary batch loop does replay it.
668 if (!retained_.bar_magnifier)
669 host.scheduler_publish_security_boundary();
670 }
671 const int chart_index = context.coordinate.input_interval_index;
672 // A calc_on_order_fills recalculation of this bar before this callback
673 // fed its slice already (recalculate).
674 if (uses_aux_security_feed_ && !had_coof_recalc) host.scheduler_feed_aux_security(chart_index);
675 if (coof) {
676 restore_coof_script_state(host);
677 language_.is_first_tick_ = true;
678 language_.is_last_tick_ = true;
679 language_.history_slot_is_new_ =
680 !language_.coof_checkpoint_contains_current_bar_;
681 }
682 publish_series(script_bar, host);
683 // The session flags belong to the script bar, not to a callback: a
684 // calc_on_order_fills recalculation that already published this bar set
685 // them (recalculate below) and its close reads the same answer (lane F1).
686 // They are the kernel's session-day facts of the bar (lane F5), which
687 // every callback of the bar carries alike.
688 if (const NativeRunSpec* spec = detail::run_spec(host); spec
689 && !spec->timeframe_undetected && !had_coof_recalc) {
690 host.scheduler_update_session_state();
691 }
692 const bool suppress_probe_tail = host.probe_suppress_tail_logic()
693 && expected_source_bars_ > 0
694 && source_bar_count_ + 1 >= expected_source_bars_;
695 if (suppress_probe_tail) host.scheduler_publish_suppressed_tail(script_bar);
696 else {
697 host.scheduler_publish_source_bar(script_bar, true, !had_coof_recalc);
698 // The bar a recalculation published early becomes the previous bar
699 // of timeframe.change() only now, after its own close (recalculate).
700 if (had_coof_recalc) host.prev_bar_timestamp_ = script_bar.timestamp;
701 last_published_script_open_ms_ = context.script_bar_open_ms;
702 }
703 if (coof) commit_coof_script_state(host);
704 if (uses_aux_security_feed_) host.scheduler_feed_deferred_aux_security(chart_index);
705 if (deferred_boundary_input_.active
706 && deferred_boundary_input_.prior_script_open_ms == context.script_bar_open_ms) {
707 if (deferred_boundary_input_.all_security_states) {
708 (void)host.scheduler_feed_security_input(
709 deferred_boundary_input_.bar, deferred_boundary_input_.next_input_ms,
710 deferred_boundary_input_.calling_bar_complete, false);
711 } else {
712 host.scheduler_feed_deferred_security_input(
713 deferred_boundary_input_.bar, deferred_boundary_input_.next_input_ms);
714 }
715 deferred_boundary_input_ = {};
716 }
717 if (completes_awaiting_legacy_script) {
718 awaiting_legacy_script_open_ms_ = std::numeric_limits<std::int64_t>::min();
719 }
720 if (!had_coof_recalc) ++source_bar_count_;
721 if (terminal_source_bar()) {
722 if (!suppress_probe_tail) host.scheduler_record_range_end(current_script_bar_);
723 if (!retained_.is_stream) host.scheduler_finish_security_sequence();
724 }
725 host.scheduler_record_broker_hash();
726}
727
729 if (event.ordinal <= applied_cursor_) return;
730 applied_cursor_ = event.ordinal;
731}
732
735 const NativeDecisionContext& context, PineStrategyHost& host) const {
736 // applied() advanced the cursor onto this event a moment ago, in the same
737 // drain iteration. A replayed ordinal never scheduled a source callback.
738 if (event.ordinal != applied_cursor_) return false;
739 if (!host.scheduler_coof_enabled()) return false;
740 if (host.config_.process_orders_on_close
741 && (context.coordinate.provenance == NativePriceProvenance::Calculation
742 || context.coordinate.provenance == NativePriceProvenance::AfterCalculationClose
743 || context.coordinate.path_phase == NativePathPhase::None)) {
744 // ab9714be pine_scheduler.cpp terminal POOC dispatch: a fill at the
745 // already-consumed close is final for that script bar and does not
746 // schedule a calc_on_order_fills source callback.
747 return false;
748 }
749 // So is a strategy.close / close_all with immediately = true that the
750 // bar's own calculation executed: TradingView recalculates after it
751 // neither -- an entry sent before it while the pyramiding limit held
752 // never fills, one sent after it fills once at the next open (lab tv
753 // tapes te-coof-immediate-reentry-*, tests/fixtures/coof_immediate_close;
754 // lane TAIL-E). A recalculation there re-sent the entry, which the kernel
755 // filled at the same close -- and, when it also closed again, looped
756 // without end.
757 if (host.adapter_.immediate_calculation_close(event, context)) return false;
758 if (host.adapter_.suppress_grouped_stop_recalc(event, context)) return false;
759 return true;
760}
761
762// One calc_on_order_fills recalculation at the kernel's OrderFill cursor
763// (NativeCalculationTrigger::BarCloseAndFills): the consumer has already
764// delivered this event's on_native_applied and re-entered the same point
765// frame, so everything below is Pine's own language-state rollback,
766// publication and first-open execution chain.
768 const NativeDecisionContext& context, PineStrategyHost& host) {
769 // ab9714be pine_scheduler.cpp:531-537: the historical O point admits the
770 // carried order's open fill and one refill; every later fill advances
771 // along the path. The native matcher may book such a fill at the start
772 // (t == 0) of the O->W1 segment instead of at the Open phase; both are
773 // the same O point, so its recalculation carries Open provenance. Once
774 // the O point is spent, a further fill at that price lies on the O->W1
775 // leg (pine_scheduler.cpp:571-606) and its recalculation says so.
776 const bool bar_known = current_script_bar_valid_
777 && current_script_bar_.timestamp == context.script_bar_open_ms;
778 const NativePathPhase first_extreme = bar_known
779 && host.adapter_.source_path_uses_high_first(current_script_bar_)
780 ? NativePathPhase::High : NativePathPhase::Low;
781 // A print of a magnified path (the chart bar's own open) admits the
782 // carried fill alone: TradingView's refill waits for the first intrabar's
783 // open (sub_bar_complete, next_input_waypoint).
784 // On a lower path a point's phase is its sub-bar's, so the open point is
785 // that sub-bar's own Open: the chart bar's first extreme says nothing
786 // about it.
787 const Bar* path_bar = lower_path_bar_at(host, context);
788 const bool open_point = path_bar
789 ? !single_print(*path_bar) && context.coordinate.path_phase == NativePathPhase::Open
790 : context.coordinate.path_phase == NativePathPhase::Open
791 || (bar_known && context.coordinate.path_phase == first_extreme
792 && event.cursor.t == 0.0);
793 const bool at_open = open_point && open_point_fills_ < 2;
794 open_point_fills_ = at_open ? open_point_fills_ + 1 : 2;
795 const bool first_open = at_open && !saw_open_fill_;
796 if (at_open) saw_open_fill_ = true;
797 const bool first_callback = coof_callback_script_open_
798 != context.script_bar_open_ms;
799 const bool callback_advances_source_bar = first_callback
800 && !language_.coof_checkpoint_contains_current_bar_;
801 // The bar's first recalculation before its close callback feeds the
802 // bar's auxiliary slice. TradingView runs a recalculation of a historical
803 // bar on the whole bar, its finer request.security reads as well as its
804 // close, high and low: the 5m close read after the open fill, and after a
805 // later fill of the bar, is the chart bar's close, a lookahead_on 5m site
806 // reads the bar's first 5m bar and a 5m lower_tf array all three (lab tv
807 // int28fix-rc, -rc2, -rc3; lane INT28-FIX rule RC). The slice waited for
808 // the close callback, so the recalculation read the previous bar's. A
809 // recalculation after the close callback (a fill the close booked) finds
810 // the slice that callback fed.
811 if (uses_aux_security_feed_ && callback_advances_source_bar)
812 host.scheduler_feed_aux_security(context.coordinate.input_interval_index);
813 // COOF re-evaluates the source script against the full script bar while
814 // the native current-execution coordinate still supplies the fill price
815 // for sizing/placement. A one-price synthetic callback erases high/low,
816 // volume and barstate facts that the legacy scheduler retained.
817 Bar callback_bar = current_script_bar_valid_
818 && current_script_bar_.timestamp == context.script_bar_open_ms
819 ? current_script_bar_ : host.current_bar_;
820 callback_bar.timestamp = context.script_bar_open_ms;
821 restore_coof_script_state(host);
822 language_.is_first_tick_ = true;
823 language_.is_last_tick_ = true;
824 language_.history_slot_is_new_ =
825 !language_.coof_checkpoint_contains_current_bar_;
826 publish_series(callback_bar, host);
827 // A fill recalculation runs the script ON the bar the fill is on, so it
828 // reads that bar's session flags, as ab9714be's loop set them before any
829 // of the bar's fills. The first publication of the bar asks; the close
830 // callback and later recalculations of the bar read the same answer
831 // (lane F1).
832 if (const NativeRunSpec* spec = detail::run_spec(host); spec
833 && !spec->timeframe_undetected
834 && last_published_script_open_ms_ != context.script_bar_open_ms) {
835 host.scheduler_update_session_state();
836 }
837 NativeDecisionContext coof_context = context;
838 if (at_open) coof_context.coordinate.path_phase = NativePathPhase::Open;
839 else if (open_point && bar_known) coof_context.coordinate.path_phase = first_extreme;
840 // timeframe.change() compares the script bar with the bar before it. A
841 // recalculation publishes its bar ahead of the bar's close, and every
842 // later calculation of the bar still compares with that same previous
843 // bar: TradingView resets a day's counter on the close of a day's first
844 // bar that a fill recalculated (lab tv tape
845 // tests/fixtures/coof_timeframe_change td-m3a; lane TAIL-D).
846 const std::int64_t previous_bar_timestamp = host.prev_bar_timestamp_;
847 host.adapter_.begin_coof_recalc(
848 event, coof_context, first_open, host.broker_fill_event_seq_);
849 try {
850 host.scheduler_publish_source_bar(
851 callback_bar, true, callback_advances_source_bar);
852 // Low/high/close recalculations stage source requests until the
853 // callback has finished so their statement order is complete. Drain
854 // that source queue while the fill coordinate is still current; the
855 // accepted MARKET newborns below then execute at this same broker
856 // point, matching calc_on_order_fills chronology.
857 host.adapter_.flush_coof_tail(/*openings_only=*/true);
858 host.adapter_.flush_coof_tail();
859 } catch (...) {
860 host.prev_bar_timestamp_ = previous_bar_timestamp;
861 host.adapter_.end_coof_recalc();
862 throw;
863 }
864 host.prev_bar_timestamp_ = previous_bar_timestamp;
865 host.adapter_.end_coof_recalc();
866 restore_coof_script_state(host);
867 coof_callback_script_open_ = context.script_bar_open_ms;
868 last_published_script_open_ms_ = context.script_bar_open_ms;
869 if (callback_advances_source_bar) ++source_bar_count_;
870 if (!first_open) {
871 auto newborns = host.adapter_.take_first_open_newborns();
872 for (const auto& handle : newborns) {
873 (void)host.execute_current({handle, NativeCurrentPriceRule::NearestTick});
874 }
875 return;
876 }
877 constexpr std::uint64_t kNoFillEventBudget = std::numeric_limits<std::uint64_t>::max();
878 // The same TradingView literal project() hands the kernel as
879 // max_recalculations_per_point; here it still bounds Pine's own first-open
880 // execution chain inside one recalculation.
881 constexpr std::size_t kFirstOpenLoopGuard = kCoofLoopGuard;
882 std::uint64_t budget = kNoFillEventBudget;
883 std::size_t executed = 0;
884 for (;;) {
885 auto newborns = host.adapter_.take_first_open_newborns();
886 if (newborns.empty()) break;
887 for (const auto& handle : newborns) {
888 if (budget == 0 || executed == kFirstOpenLoopGuard) {
890 "Pine COOF first-open loop guard exhausted");
891 }
892 --budget;
893 ++executed;
894 (void)host.execute_current({handle, NativeCurrentPriceRule::NearestTick});
895 }
896 }
897}
898
899} // namespace pineforge::source
void push(T value)
Definition series.hpp:430
void update(T value)
Definition series.hpp:434
Base with a code: throw coded<std::invalid_argument>(code, {args}, text).
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
void input(const Bar &, const NativeInputContext &, PineStrategyHost &)
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
void fixture_publish_source_series(const Bar &, bool new_history_slot)
void recalculate(const native_order::ExecutionAppliedEvent &, const NativeDecisionContext &, PineStrategyHost &)
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
int source_bar_index_for(const NativeDecisionContext &context) const noexcept
void bar_open(const Bar &, const NativeDecisionContext &, PineStrategyHost &)
std::optional< InputWaypoint > next_input_waypoint(const PineStrategyHost &, const NativeDecisionContext &) const noexcept
InputView retained_input() 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
NativePathPhase
Which leg of a modeled OHLC walk a point sits on.
SelectedRetainedCount count_selected_executed_primary(const Bar *rows, std::size_t count, const SelectedRetainedRequest &request)
const NativeRunSpec * run_spec(const NativeStrategyHost &host)
NativeExecutionConsumer & run_consumer(const NativeStrategyHost &host)
constexpr std::uint32_t kCoofLoopGuard
int tf_to_seconds(const std::string &tf)
Convert a TradingView timeframe string to seconds.
int tf_ratio(const std::string &input_tf, const std::string &target_tf)
Compute how many input bars fit into one target bar.
CalendarPeriod calendar_period_for(const std::string &tf)
Determine the calendar period for a target TF string.
bool tf_change(int64_t prev_ms, int64_t curr_ms, const std::string &tf)
Check if prev/curr timestamps cross a timeframe boundary.
int64_t session_period_open_ms(int64_t ms, const std::string &tz, const std::string &session, CalendarPeriod period)
Open (Unix ms) of the symbol's D/W/M bar that contains ms: the day stamp of the period's first sessio...
Selected-window configuration, handed to strategy_set_selected_window_v1().
double open
Definition bar.hpp:7
double close
Definition bar.hpp:7
double low
Definition bar.hpp:7
int64_t timestamp
Definition bar.hpp:8
double high
Definition bar.hpp:7
Borrowed begin-call facts.
bool simple_run
Which public overload began the run: the bare run(bars, n) lifecycle (true) or a timeframe-aware / ma...
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.
One committed execution: the definition, the cursor, the resolved price and units,...
const lower_tf * lower() const noexcept
The retained finer feed, or nullptr when this is not the lower_tf alternative.
One complete setup value, staged/copied by NativeStrategyHost before it is applied at begin.
std::unordered_map< std::string, double > pos_view_frozen_entry_qty_