PineForge v0.13.1-379-g9b50973
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 <algorithm>
7#include <stdexcept>
8#include <utility>
9
10namespace pineforge::source {
11
13 RetainedBegin next;
14 if (args.bars && args.n > 0) next.bars.assign(args.bars, args.bars + args.n);
15 next.input_tf = args.input_tf; next.script_tf = args.script_tf;
16 next.bar_magnifier = args.bar_magnifier; next.magnifier_samples = args.magnifier_samples;
17 next.distribution = args.magnifier_distribution;
18 next.volume_weighted = args.magnifier_volume_weighted;
19 next.volume_weighted_min_samples = args.magnifier_volume_weighted_min_samples;
20 next.volume_weighted_max_samples = args.magnifier_volume_weighted_max_samples;
21 next.is_stream = args.is_stream; next.warmup_n = args.warmup_n;
22 next.simple_run = args.simple_run;
23 retained_ = std::move(next);
24}
25
26void PineScheduler::reset_language() {
27 language_.reset_for_run();
28 language_.pos_view_freeze_bar_ = -1;
30 language_.pos_view_frozen_qty_ = 0.0;
31 language_.pos_view_frozen_entry_qty_.clear();
32 language_.is_first_tick_ = true; language_.is_last_tick_ = true;
33 language_.history_slot_is_new_ = true; language_.coof_checkpoint_contains_current_bar_ = false;
39 current_script_open_ms_ = 0; saw_open_fill_ = false; open_point_fills_ = 0;
40 current_script_bar_ = {}; current_script_bar_valid_ = false;
41 source_bar_count_ = 0; expected_source_bars_ = 0; applied_cursor_ = 0;
42 coof_callback_script_open_ = std::numeric_limits<std::int64_t>::min();
43 last_published_script_open_ms_ = std::numeric_limits<std::int64_t>::min();
44 prior_input_script_open_ms_ = std::numeric_limits<std::int64_t>::min();
45 awaiting_legacy_script_open_ms_ = std::numeric_limits<std::int64_t>::min();
46 last_stream_input_open_ms_ = std::numeric_limits<std::int64_t>::min();
47 input_script_completes_.clear();
48 input_script_boundary_completes_.clear();
49 uses_aux_security_feed_ = false;
50 deferred_boundary_input_ = {};
51}
52
53void PineScheduler::snapshot_coof_script_state(PineStrategyHost& host) {
54 if (language_._src_series_active_) {
55 language_.coof_checkpoint_src_open_ = language_._src_open_;
56 language_.coof_checkpoint_src_high_ = language_._src_high_;
57 language_.coof_checkpoint_src_low_ = language_._src_low_;
58 language_.coof_checkpoint_src_close_ = language_._src_close_;
59 language_.coof_checkpoint_src_volume_ = language_._src_volume_;
60 language_.coof_checkpoint_src_hl2_ = language_._src_hl2_;
61 language_.coof_checkpoint_src_hlc3_ = language_._src_hlc3_;
62 language_.coof_checkpoint_src_ohlc4_ = language_._src_ohlc4_;
63 language_.coof_checkpoint_src_hlcc4_ = language_._src_hlcc4_;
64 }
65 language_.coof_checkpoint_prev_chart_close_ = language_.prev_chart_close_;
66 language_.coof_checkpoint_last_chart_close_ = language_.last_chart_close_;
67 language_.coof_checkpoint_contains_current_bar_ = false;
68 host.snapshot_script_state();
69}
70
71void PineScheduler::restore_coof_script_state(PineStrategyHost& host) {
72 if (language_._src_series_active_) {
73 language_._src_open_ = language_.coof_checkpoint_src_open_;
74 language_._src_high_ = language_.coof_checkpoint_src_high_;
75 language_._src_low_ = language_.coof_checkpoint_src_low_;
76 language_._src_close_ = language_.coof_checkpoint_src_close_;
77 language_._src_volume_ = language_.coof_checkpoint_src_volume_;
78 language_._src_hl2_ = language_.coof_checkpoint_src_hl2_;
79 language_._src_hlc3_ = language_.coof_checkpoint_src_hlc3_;
80 language_._src_ohlc4_ = language_.coof_checkpoint_src_ohlc4_;
81 language_._src_hlcc4_ = language_.coof_checkpoint_src_hlcc4_;
82 }
83 language_.prev_chart_close_ = language_.coof_checkpoint_prev_chart_close_;
84 language_.last_chart_close_ = language_.coof_checkpoint_last_chart_close_;
85 host.restore_script_state();
86}
87
88void PineScheduler::commit_coof_script_state(PineStrategyHost& host) {
89 if (language_._src_series_active_) {
90 language_.coof_checkpoint_src_open_ = language_._src_open_;
91 language_.coof_checkpoint_src_high_ = language_._src_high_;
92 language_.coof_checkpoint_src_low_ = language_._src_low_;
93 language_.coof_checkpoint_src_close_ = language_._src_close_;
94 language_.coof_checkpoint_src_volume_ = language_._src_volume_;
95 language_.coof_checkpoint_src_hl2_ = language_._src_hl2_;
96 language_.coof_checkpoint_src_hlc3_ = language_._src_hlc3_;
97 language_.coof_checkpoint_src_ohlc4_ = language_._src_ohlc4_;
98 language_.coof_checkpoint_src_hlcc4_ = language_._src_hlcc4_;
99 }
100 language_.coof_checkpoint_prev_chart_close_ = language_.prev_chart_close_;
101 language_.coof_checkpoint_last_chart_close_ = language_.last_chart_close_;
102 language_.coof_checkpoint_contains_current_bar_ = true;
103 host.commit_script_state();
104}
105
107 reset_language();
108 const bool static_eligible = !retained_.is_stream && !retained_.bar_magnifier
109 && retained_.input_tf.empty() && retained_.script_tf.empty();
110 const auto state = host.native_state();
111 const bool undetected = state.spec && state.spec->timeframe_undetected;
112 const int ratio = undetected || !state.spec ? 1
113 : tf_ratio(state.spec->input_tf, state.spec->script_tf);
114 const bool needs_aggregation = ratio > 1 || ratio == -1;
115 input_script_completes_.assign(retained_.bars.size(), 1U);
116 input_script_boundary_completes_.assign(retained_.bars.size(), 0U);
117 if (needs_aggregation && state.spec) {
118 TimeframeAggregator preview(state.spec->script_tf, state.spec->input_tf,
119 state.spec->timezone, state.spec->session);
120 for (std::size_t i = 0; i < retained_.bars.size(); ++i) {
121 const AggregatedBar aggregate = preview.feed(retained_.bars[i]);
122 input_script_completes_[i] = aggregate.is_complete ? 1U : 0U;
123 input_script_boundary_completes_[i] = aggregate.is_complete
124 && tf_change(aggregate.bar.timestamp, retained_.bars[i].timestamp,
125 state.spec->script_tf, state.spec->timezone,
126 state.spec->session) ? 1U : 0U;
127 }
128 }
129 expected_source_bars_ = 0;
130 for (const auto complete : input_script_completes_) {
131 expected_source_bars_ += complete != 0U ? 1 : 0;
132 }
133 host.stream_warmup_mode_ = retained_.is_stream;
134 host.scheduler_prepare_script_run(retained_.bars, static_eligible,
135 expected_source_bars_, !needs_aggregation);
136 // ab9714be pine_scheduler.cpp:717-804 (legacy_run_simple) never configures
137 // the request.security evaluator surface; :1249-1323 (run_tf_impl, reached
138 // by every timeframe-aware, magnified or stream overload, including the
139 // generated wrapper's run_backtest / run_backtest_full with EMPTY
140 // timeframes) configures it unconditionally after prepare_script_run()
141 // received the static-eligibility flag. Empty timeframes only request
142 // auto-detection; only the bare run(bars, n) overload skips the surface,
143 // and the kernel reports that overload as NativeBeginArgs::simple_run.
144 // (R4-D L10o: gating on static eligibility left every request.security
145 // series na whenever a caller auto-detected the timeframe.)
146 if (!retained_.simple_run) host.scheduler_configure_security_evaluators();
147 uses_aux_security_feed_ = host.scheduler_uses_aux_security_feed();
148 host.scheduler_prepare_security_sequence(retained_.bars);
149}
150
152 // Matching at a new script-bar open precedes the terminal source callback;
153 // all fills after that callback (including COOF/POOC notifications) belong
154 // to the already-published source index. This is the same cadence the
155 // legacy aggregation loop used for Trade.entry_bar_index/exit_bar_index.
156 const bool published = last_published_script_open_ms_
157 == context.script_bar_open_ms;
158 const bool coof_published = coof_callback_script_open_ == context.script_bar_open_ms;
159 if (published || coof_published)
160 return std::max(0, source_bar_count_ - 1);
161 return source_bar_count_;
162}
163
165 const NativeDecisionContext& context, double current_price,
166 NativePathOrder order) const noexcept {
167 const auto found = std::find_if(retained_.bars.begin(), retained_.bars.end(),
168 [&](const Bar& bar) { return bar.timestamp == context.sub_bar_open_ms; });
169 if (found == retained_.bars.end()) return std::nullopt;
170 bool high_first = std::abs(found->high - found->open)
171 < std::abs(found->open - found->low);
172 if (order == NativePathOrder::HighFirst) high_first = true;
173 else if (order == NativePathOrder::LowFirst) high_first = false;
174 const NativePathPhase phase[] = {
175 NativePathPhase::Open,
176 high_first ? NativePathPhase::High : NativePathPhase::Low,
177 high_first ? NativePathPhase::Low : NativePathPhase::High,
178 NativePathPhase::Close,
179 };
180 const double price[] = {
181 found->open,
182 high_first ? found->high : found->low,
183 high_first ? found->low : found->high,
184 found->close,
185 };
186 for (int index = 0; index < 4; ++index) {
187 if (phase[index] != context.coordinate.path_phase) continue;
188 if (index > 0 && current_price != price[index]) return price[index];
189 if (index < 3) return price[index + 1];
190 return std::nullopt;
191 }
192 return std::nullopt;
193}
194
195void PineScheduler::publish_series(const Bar& bar, PineStrategyHost& host) {
196 (void)host;
197 update_source_series(bar);
198}
199
200void PineScheduler::update_source_series(const Bar& bar) {
201 if (language_.history_slot_is_new_)
202 language_.prev_chart_close_ = language_.last_chart_close_;
203 language_.last_chart_close_ = bar.close;
204 if (!language_._src_series_active_) return;
205 const double hl2 = (bar.high + bar.low) / 2.0;
206 const double hlc3 = (bar.high + bar.low + bar.close) / 3.0;
207 const double ohlc4 = (bar.open + bar.high + bar.low + bar.close) / 4.0;
208 const double hlcc4 = (bar.high + bar.low + bar.close + bar.close) / 4.0;
209 if (language_.history_slot_is_new_) {
210 language_._src_open_.push(bar.open);
211 language_._src_high_.push(bar.high);
212 language_._src_low_.push(bar.low);
213 language_._src_close_.push(bar.close);
214 language_._src_volume_.push(bar.volume);
215 language_._src_hl2_.push(hl2);
216 language_._src_hlc3_.push(hlc3);
217 language_._src_ohlc4_.push(ohlc4);
218 language_._src_hlcc4_.push(hlcc4);
219 return;
220 }
221 language_._src_open_.update(bar.open);
222 language_._src_high_.update(bar.high);
223 language_._src_low_.update(bar.low);
224 language_._src_close_.update(bar.close);
225 language_._src_volume_.update(bar.volume);
226 language_._src_hl2_.update(hl2);
227 language_._src_hlc3_.update(hlc3);
228 language_._src_ohlc4_.update(ohlc4);
229 language_._src_hlcc4_.update(hlcc4);
230}
231
233 int bar_index, PositionSide side, double quantity) const noexcept {
234 if (language_.pos_view_freeze_bar_ == bar_index) {
235 if (language_.pos_view_frozen_side_ == PositionSide::LONG)
236 return language_.pos_view_frozen_qty_;
237 if (language_.pos_view_frozen_side_ == PositionSide::SHORT)
238 return -language_.pos_view_frozen_qty_;
239 return 0.0;
240 }
241 if (side == PositionSide::LONG) return quantity;
242 if (side == PositionSide::SHORT) return -quantity;
243 return 0.0;
244}
245
247 int bar_index, PositionSide side, double quantity,
248 const std::vector<PyramidEntry>& lots) {
249 if (language_.pos_view_freeze_bar_ == bar_index) return;
250 language_.pos_view_freeze_bar_ = bar_index;
251 language_.pos_view_frozen_side_ = side;
252 language_.pos_view_frozen_qty_ = quantity;
253 language_.pos_view_frozen_entry_qty_.clear();
254 for (const auto& lot : lots)
255 language_.pos_view_frozen_entry_qty_[lot.entry_id] += lot.qty;
256}
257
259 language_.pos_view_freeze_bar_ = -1;
260}
261
262const Series<double>& PineScheduler::source_series(const std::string& key) const {
263 if (key == "open") return language_._src_open_;
264 if (key == "high") return language_._src_high_;
265 if (key == "low") return language_._src_low_;
266 if (key == "close") return language_._src_close_;
267 if (key == "volume") return language_._src_volume_;
268 if (key == "hl2") return language_._src_hl2_;
269 if (key == "hlc3") return language_._src_hlc3_;
270 if (key == "ohlc4") return language_._src_ohlc4_;
271 if (key == "hlcc4") return language_._src_hlcc4_;
272 throw std::invalid_argument("unknown source series");
273}
274
275void PineScheduler::fixture_publish_source_series(const Bar& bar, bool new_history_slot) {
276 language_.history_slot_is_new_ = new_history_slot;
277 language_.is_first_tick_ = new_history_slot;
278 update_source_series(bar);
279}
280
282 const Bar& bar, const NativeInputContext& context, PineStrategyHost& host) {
283 struct InputBarIndexScope {
284 PineStrategyHost& host;
285 int previous;
286 explicit InputBarIndexScope(PineStrategyHost& value, int index)
287 : host(value), previous(value.bar_index_) { host.bar_index_ = index; }
288 ~InputBarIndexScope() { host.bar_index_ = previous; }
289 } input_bar_index(host, context.input_index);
290 // The auxiliary slice is fed per chart bar (scheduler_feed_aux_security);
291 // sites the kernel steps (declare_security_sites_to_kernel) are fed by
292 // its own pump, right after this callback returns.
293 if (uses_aux_security_feed_ || host.security_sites_kernel_routed()) {
294 prior_input_script_open_ms_ = context.script_interval.open_ms;
295 return;
296 }
297 std::int64_t next_input_ms = 0;
298 if (context.input_index >= 0
299 && context.input_index + 1 < static_cast<int>(retained_.bars.size())) {
300 next_input_ms = retained_.bars[static_cast<std::size_t>(context.input_index + 1)].timestamp;
301 }
302 if (retained_.bar_magnifier && deferred_boundary_input_.active
303 && deferred_boundary_input_.prior_script_open_ms != context.script_interval.open_ms) {
304 // A sparse lower feed reveals the completed caller only when the next
305 // child arrives. Feed the retained final child first, then let the
306 // new caller's child proceed in timestamp order.
307 const std::int64_t completed_script_open =
308 deferred_boundary_input_.prior_script_open_ms;
309 (void)host.scheduler_feed_security_input(
310 deferred_boundary_input_.bar, deferred_boundary_input_.next_input_ms,
311 true, false);
312 // Keep the new caller's first child out of request.security until the
313 // completed caller's chart callback has observed the published value.
314 deferred_boundary_input_.bar = bar;
315 deferred_boundary_input_.next_input_ms = next_input_ms;
316 deferred_boundary_input_.prior_script_open_ms = completed_script_open;
317 deferred_boundary_input_.calling_bar_complete = false;
318 deferred_boundary_input_.all_security_states = true;
319 deferred_boundary_input_.active = true;
320 prior_input_script_open_ms_ = context.script_interval.open_ms;
321 return;
322 }
323 // The generic calendar may wait for a later tradable opening before it
324 // seals a script interval. The source chart aggregator can have already
325 // completed that interval on the prior raw bar. Keep the new raw input
326 // out of request.security until the pending script callback observes the
327 // same legacy point; then feed it immediately after that callback.
328 if (!retained_.bar_magnifier && awaiting_legacy_script_open_ms_
329 != std::numeric_limits<std::int64_t>::min()) {
330 deferred_boundary_input_.bar = bar;
331 deferred_boundary_input_.next_input_ms = next_input_ms;
332 deferred_boundary_input_.prior_script_open_ms = awaiting_legacy_script_open_ms_;
333 deferred_boundary_input_.calling_bar_complete = false;
334 deferred_boundary_input_.all_security_states = true;
335 deferred_boundary_input_.active = true;
336 prior_input_script_open_ms_ = context.script_interval.open_ms;
337 return;
338 }
339
340 bool calling_bar_complete = context.completes_script_interval;
341 bool boundary = prior_input_script_open_ms_
342 != std::numeric_limits<std::int64_t>::min()
343 && prior_input_script_open_ms_ != context.script_interval.open_ms;
344 if (context.input_index >= 0
345 && context.input_index < static_cast<int>(input_script_completes_.size())) {
346 calling_bar_complete = input_script_completes_[
347 static_cast<std::size_t>(context.input_index)] != 0U;
348 boundary = input_script_boundary_completes_[
349 static_cast<std::size_t>(context.input_index)] != 0U;
350 }
351 // The final sparse magnifier child is a partial requested bucket. The
352 // legacy lower-TF pump does not promote that tail to a completed
353 // request.security value merely because the input array ended.
354 if (retained_.bar_magnifier && next_input_ms == 0)
355 calling_bar_complete = false;
356 bool security_boundary_ahead = false;
357 if (retained_.bar_magnifier && next_input_ms != 0) {
358 const bool caller_boundary = tf_change(
359 bar.timestamp, next_input_ms, retained_.script_tf,
360 host.syminfo_.timezone, host.syminfo_.session);
361 if (caller_boundary) {
362 for (const auto& state : host.security_eval_states_) {
363 if (host.pine_security_state(state.sec_id).publish_gate_tf_seconds > 0) {
364 security_boundary_ahead = true;
365 break;
366 }
367 }
368 }
369 }
370 if (security_boundary_ahead) {
371 deferred_boundary_input_.bar = bar;
372 deferred_boundary_input_.next_input_ms = next_input_ms;
373 deferred_boundary_input_.prior_script_open_ms = context.script_interval.open_ms;
374 deferred_boundary_input_.calling_bar_complete = true;
375 deferred_boundary_input_.all_security_states = false;
376 deferred_boundary_input_.active = true;
377 prior_input_script_open_ms_ = context.script_interval.open_ms;
378 return;
379 }
380 const bool deferred_gate = host.scheduler_feed_security_input(
381 bar, next_input_ms, calling_bar_complete,
382 boundary && !retained_.bar_magnifier);
383 if (deferred_gate) {
384 deferred_boundary_input_.bar = bar;
385 deferred_boundary_input_.next_input_ms = next_input_ms;
386 deferred_boundary_input_.prior_script_open_ms = prior_input_script_open_ms_;
387 deferred_boundary_input_.calling_bar_complete = false;
388 deferred_boundary_input_.all_security_states = false;
389 deferred_boundary_input_.active = true;
390 }
391 if (calling_bar_complete && !retained_.bar_magnifier) {
392 awaiting_legacy_script_open_ms_ = boundary
393 ? prior_input_script_open_ms_ : context.script_interval.open_ms;
394 }
395 prior_input_script_open_ms_ = context.script_interval.open_ms;
396}
397
398void PineScheduler::tick(const Bar& bar, const NativeTickContext& context,
399 PineStrategyHost& host) {
400 if (!retained_.is_stream) return;
401 const auto& interval = context.decision.input_interval;
402 if (last_stream_input_open_ms_ == interval.open_ms) return;
403 if (prior_input_script_open_ms_ != std::numeric_limits<std::int64_t>::min()
404 && prior_input_script_open_ms_ != context.decision.script_interval.open_ms) {
405 // A realtime tick can be the first child of the next caller after a
406 // sparse warmup. The legacy stream replays the retained final child
407 // at this boundary (without advancing the requested-context slot).
408 host.scheduler_publish_security_boundary();
409 }
410 last_stream_input_open_ms_ = interval.open_ms;
411 const auto& script = context.decision.script_interval;
412 const bool complete = interval.next_period_open_ms >= script.next_period_open_ms;
413 (void)host.scheduler_feed_security_input(
414 bar, interval.next_input_open_ms, complete, false);
415 prior_input_script_open_ms_ = script.open_ms;
416}
417
418void PineScheduler::bar_open(const Bar& value, const NativeDecisionContext& context,
419 PineStrategyHost& host) {
420 if (context.script_bar_open_ms != current_script_open_ms_) {
421 current_script_open_ms_ = context.script_bar_open_ms;
422 saw_open_fill_ = false;
423 open_point_fills_ = 0;
424 coof_callback_script_open_ = std::numeric_limits<std::int64_t>::min();
425 if (host.scheduler_coof_enabled()) snapshot_coof_script_state(host);
426 }
427 if (!retained_.is_stream) {
428 current_script_bar_ = value;
429 current_script_bar_.timestamp = context.script_bar_open_ms;
430 current_script_bar_valid_ = true;
431 }
432}
433
434void PineScheduler::bar(const Bar& value, const NativeDecisionContext& context, PineStrategyHost& host) {
435 // P7c: matching advances over every sub-bar; the language callback occurs
436 // only at the terminal sub-bar with the script bar timestamp restored.
437 language_.is_first_tick_ = context.is_terminal_sub_bar;
438 language_.is_last_tick_ = context.is_terminal_sub_bar;
439 language_.history_slot_is_new_ = context.is_terminal_sub_bar;
440 if (!context.is_terminal_sub_bar) return;
441 // A FeedTolerant native stream can emit a quiet carried callback for
442 // the calendar-aligned slot immediately preceding the last raw-label
443 // warmup bar. The retired source stream starts its realtime cadence from
444 // that warmup bar's next source slot, so it never published this stale
445 // callback to generated code.
446 if (retained_.is_stream && current_script_bar_valid_
447 && context.script_bar_open_ms <= current_script_bar_.timestamp) {
448 // The generic driver still dispatched a script point. Preserve the
449 // public one-hash-per-dispatch accounting without exposing it to the
450 // source callback cadence.
451 host.scheduler_record_broker_hash();
452 return;
453 }
454 const bool coof = host.scheduler_coof_enabled();
455 const bool had_coof_recalc = coof_callback_script_open_
456 == context.script_bar_open_ms;
457 Bar script_bar = value;
458 script_bar.timestamp = context.script_bar_open_ms;
459 if (retained_.is_stream && !retained_.bars.empty()
460 && source_bar_count_ >= static_cast<int>(retained_.bars.size())) {
461 const int input_seconds = tf_to_seconds(retained_.input_tf);
462 const std::int64_t expected_open = retained_.bars.back().timestamp
463 + static_cast<std::int64_t>(std::max(input_seconds, 0)) * 1000;
464 if (input_seconds > 0 && script_bar.timestamp < expected_open) {
465 // ab9714be pine_stream.cpp:112-125 labels the first realtime
466 // source bar at last_warmup + input_duration. A tolerant native
467 // calendar can report its aligned interval label instead; retain
468 // the source-visible raw stream label without changing matching.
469 script_bar.timestamp = expected_open;
470 }
471 }
472 current_script_bar_ = script_bar;
473 current_script_bar_valid_ = true;
474 const bool completes_awaiting_legacy_script = awaiting_legacy_script_open_ms_
475 == context.script_bar_open_ms;
476 if (deferred_boundary_input_.active
477 && !deferred_boundary_input_.all_security_states
478 && deferred_boundary_input_.prior_script_open_ms == context.script_bar_open_ms) {
479 // The legacy magnifier feeds the boundary-triggering lower bar after
480 // the completed script callback; it does not replay the caller at the
481 // callback boundary. The ordinary batch loop does replay it.
482 if (!retained_.bar_magnifier)
483 host.scheduler_publish_security_boundary();
484 }
485 const int chart_index = context.coordinate.interval_index;
486 if (uses_aux_security_feed_) host.scheduler_feed_aux_security(chart_index);
487 if (coof) {
488 restore_coof_script_state(host);
489 language_.is_first_tick_ = true;
490 language_.is_last_tick_ = true;
491 language_.history_slot_is_new_ =
492 !language_.coof_checkpoint_contains_current_bar_;
493 }
494 publish_series(script_bar, host);
495 std::optional<std::int64_t> next_script_open_ms;
496 if (const auto state = host.native_state(); state.spec
497 && !state.spec->timeframe_undetected) {
498 if (tf_ratio(state.spec->input_tf, state.spec->script_tf) == 1
499 && source_bar_count_ + 1 < static_cast<int>(retained_.bars.size())) {
500 next_script_open_ms = retained_.bars[
501 static_cast<std::size_t>(source_bar_count_ + 1)].timestamp;
502 }
503 host.scheduler_update_session_state(script_bar, next_script_open_ms);
504 }
505 const bool suppress_probe_tail = host.probe_suppress_tail_logic()
506 && expected_source_bars_ > 0
507 && source_bar_count_ + 1 >= expected_source_bars_;
508 if (suppress_probe_tail) host.scheduler_publish_suppressed_tail(script_bar);
509 else {
510 host.scheduler_publish_source_bar(script_bar, true, !had_coof_recalc);
511 last_published_script_open_ms_ = context.script_bar_open_ms;
512 }
513 if (coof) commit_coof_script_state(host);
514 if (uses_aux_security_feed_) host.scheduler_feed_deferred_aux_security(chart_index);
515 if (deferred_boundary_input_.active
516 && deferred_boundary_input_.prior_script_open_ms == context.script_bar_open_ms) {
517 if (deferred_boundary_input_.all_security_states) {
518 (void)host.scheduler_feed_security_input(
519 deferred_boundary_input_.bar, deferred_boundary_input_.next_input_ms,
520 deferred_boundary_input_.calling_bar_complete, false);
521 } else {
522 host.scheduler_feed_deferred_security_input(
523 deferred_boundary_input_.bar, deferred_boundary_input_.next_input_ms);
524 }
525 deferred_boundary_input_ = {};
526 }
527 if (completes_awaiting_legacy_script) {
528 awaiting_legacy_script_open_ms_ = std::numeric_limits<std::int64_t>::min();
529 }
530 if (!had_coof_recalc) ++source_bar_count_;
531 if (terminal_source_bar()) {
532 if (!suppress_probe_tail) host.scheduler_record_range_end(current_script_bar_);
533 if (!retained_.is_stream) host.scheduler_finish_security_sequence();
534 }
535 host.scheduler_record_broker_hash();
536}
537
539 if (event.ordinal <= applied_cursor_) return;
540 applied_cursor_ = event.ordinal;
541}
542
545 const NativeDecisionContext& context, PineStrategyHost& host) const {
546 // applied() advanced the cursor onto this event a moment ago, in the same
547 // drain iteration. A replayed ordinal never scheduled a source callback.
548 if (event.ordinal != applied_cursor_) return false;
549 if (!host.scheduler_coof_enabled()) return false;
551 && (context.coordinate.provenance == NativePriceProvenance::Calculation
552 || context.coordinate.provenance == NativePriceProvenance::AfterCalculationClose
553 || context.coordinate.path_phase == NativePathPhase::None)) {
554 // ab9714be pine_scheduler.cpp terminal POOC dispatch: a fill at the
555 // already-consumed close is final for that script bar and does not
556 // schedule a calc_on_order_fills source callback.
557 return false;
558 }
559 if (host.adapter_.suppress_grouped_stop_recalc(event, context)) return false;
560 return true;
561}
562
563// One calc_on_order_fills recalculation at the kernel's OrderFill cursor
564// (NativeCalculationTrigger::BarCloseAndFills): the consumer has already
565// delivered this event's on_native_applied and re-entered the same point
566// frame, so everything below is Pine's own language-state rollback,
567// publication and first-open execution chain.
569 const NativeDecisionContext& context, PineStrategyHost& host) {
570 // ab9714be pine_scheduler.cpp:531-537: the historical O point admits the
571 // carried order's open fill and one refill; every later fill advances
572 // along the path. The native matcher may book such a fill at the start
573 // (t == 0) of the O->W1 segment instead of at the Open phase; both are
574 // the same O point, so its recalculation carries Open provenance. Once
575 // the O point is spent, a further fill at that price lies on the O->W1
576 // leg (pine_scheduler.cpp:571-606) and its recalculation says so.
577 const bool bar_known = current_script_bar_valid_
578 && current_script_bar_.timestamp == context.script_bar_open_ms;
579 const NativePathPhase first_extreme = bar_known
580 && host.adapter_.source_path_uses_high_first(current_script_bar_)
581 ? NativePathPhase::High : NativePathPhase::Low;
582 const bool open_point = context.coordinate.path_phase == NativePathPhase::Open
583 || (bar_known && context.coordinate.path_phase == first_extreme
584 && event.cursor.t == 0.0);
585 const bool at_open = open_point && open_point_fills_ < 2;
586 open_point_fills_ = at_open ? open_point_fills_ + 1 : 2;
587 const bool first_open = at_open && !saw_open_fill_;
588 if (at_open) saw_open_fill_ = true;
589 const bool first_callback = coof_callback_script_open_
590 != context.script_bar_open_ms;
591 const bool callback_advances_source_bar = first_callback
592 && !language_.coof_checkpoint_contains_current_bar_;
593 // COOF re-evaluates the source script against the full script bar while
594 // the native current-execution coordinate still supplies the fill price
595 // for sizing/placement. A one-price synthetic callback erases high/low,
596 // volume and barstate facts that the legacy scheduler retained.
597 Bar callback_bar = current_script_bar_valid_
598 && current_script_bar_.timestamp == context.script_bar_open_ms
599 ? current_script_bar_ : host.current_bar_;
600 callback_bar.timestamp = context.script_bar_open_ms;
601 restore_coof_script_state(host);
602 language_.is_first_tick_ = true;
603 language_.is_last_tick_ = true;
604 language_.history_slot_is_new_ =
605 !language_.coof_checkpoint_contains_current_bar_;
606 publish_series(callback_bar, host);
607 NativeDecisionContext coof_context = context;
608 if (at_open) coof_context.coordinate.path_phase = NativePathPhase::Open;
609 else if (open_point && bar_known) coof_context.coordinate.path_phase = first_extreme;
611 event, coof_context, first_open, host.broker_fill_event_seq_);
612 // ab9714be pine_scheduler.cpp:357-366: under COOF every fill recalculation
613 // invokes update_per_trade_extremes() against the full script bar, except
614 // the carried positive-slip POOC opening-money chain at O
615 // (pine_scheduler.cpp:494-511), whose exits see only the open.
616 Bar extremes_bar = callback_bar;
617 if (host.config_.process_orders_on_close && host.config_.slippage > 0 && open_point) {
618 extremes_bar.high = extremes_bar.low = extremes_bar.close = extremes_bar.open;
619 }
621 host.pyramid_entries_, host.position_side_, host.bar_index_, extremes_bar);
622 try {
623 host.scheduler_publish_source_bar(
624 callback_bar, true, callback_advances_source_bar);
625 // Low/high/close recalculations stage source requests until the
626 // callback has finished so their statement order is complete. Drain
627 // that source queue while the fill coordinate is still current; the
628 // accepted MARKET newborns below then execute at this same broker
629 // point, matching calc_on_order_fills chronology.
630 host.adapter_.flush_coof_tail(/*openings_only=*/true);
631 host.adapter_.flush_coof_tail();
632 } catch (...) {
634 throw;
635 }
637 restore_coof_script_state(host);
638 coof_callback_script_open_ = context.script_bar_open_ms;
639 last_published_script_open_ms_ = context.script_bar_open_ms;
640 if (callback_advances_source_bar) ++source_bar_count_;
641 if (!first_open) {
642 auto newborns = host.adapter_.take_first_open_newborns();
643 for (const auto& handle : newborns) {
644 (void)host.execute_current({handle, NativeCurrentPriceRule::NearestTick});
645 }
646 return;
647 }
648 constexpr std::uint64_t kNoFillEventBudget = std::numeric_limits<std::uint64_t>::max();
649 // The same TradingView literal project() hands the kernel as
650 // max_recalculations_per_point; here it still bounds Pine's own first-open
651 // execution chain inside one recalculation.
652 constexpr std::size_t kFirstOpenLoopGuard = kCoofLoopGuard;
653 std::uint64_t budget = kNoFillEventBudget;
654 std::size_t executed = 0;
655 for (;;) {
656 auto newborns = host.adapter_.take_first_open_newborns();
657 if (newborns.empty()) break;
658 for (const auto& handle : newborns) {
659 if (budget == 0 || executed == kFirstOpenLoopGuard)
660 throw std::overflow_error("Pine COOF first-open loop guard exhausted");
661 --budget;
662 ++executed;
663 (void)host.execute_current({handle, NativeCurrentPriceRule::NearestTick});
664 }
665 }
666}
667
668} // namespace pineforge::source
void push(T value)
Definition series.hpp:100
void update(T value)
Definition series.hpp:104
AggregatedBar feed(const Bar &input_bar)
Feed one input bar. Returns aggregation state.
void begin_coof_recalc(const native_order::ExecutionAppliedEvent &, const NativeDecisionContext &, bool first_open, std::uint64_t source_fill_sequence)
std::vector< native_order::RequestHandle > take_first_open_newborns()
bool suppress_grouped_stop_recalc(const native_order::ExecutionAppliedEvent &, const NativeDecisionContext &) const noexcept
std::optional< double > next_input_waypoint(const NativeDecisionContext &, double current_price, NativePathOrder) const noexcept
void tick(const Bar &, const NativeTickContext &, PineStrategyHost &)
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 &)
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 &)
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 &)
NativePathPhase
Which leg of a modeled OHLC walk a point sits on.
NativePathOrder
Generic ordering for a modeled OHLC path.
constexpr std::uint32_t kCoofLoopGuard
void sample_open_trade_extremes(std::vector< PyramidEntry > &lots, PositionSide side, int bar_index, const Bar &bar)
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.
bool tf_change(int64_t prev_ms, int64_t curr_ms, const std::string &tf)
Check if prev/curr timestamps cross a timeframe boundary.
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,...
std::unordered_map< std::string, double > pos_view_frozen_entry_qty_