PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
pine_aux_security.cpp
Go to the documentation of this file.
2/*
3 * engine_aux_security.cpp — native-chart / request.security feed separation
4 */
5
6#include "../engine_internal.hpp"
7#include "../native_execution_consumer.hpp"
9
11#include <pineforge/ta.hpp>
12
13#include <algorithm>
14#include <limits>
15#include <stdexcept>
16#include <vector>
17
18namespace pineforge {
19using namespace source;
20
21#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
22
24 const std::string& input_tf) {
25 guard_native_mutation("set_aux_security_feed");
26 if (n == 0) {
27 aux_security_bars_.clear();
30 return true;
31 }
32 if (n < 0 || bars == nullptr || input_tf.empty()) {
34 *this, "auxiliary request.security feed requires bars, a positive count, and input_tf",
35 RunFailureCode::security_feed_refused, {{"reason", "aux_feed_shape"}});
36 return false;
37 }
38 int seconds = 0;
39 try {
40 seconds = tf_to_seconds(input_tf);
41 } catch (...) {
42 seconds = 0;
43 }
44 if (seconds <= 0) {
45 note_run_failure(*this, "auxiliary request.security feed requires a fixed positive input_tf",
47 {{"reason", "aux_feed_timeframe"}});
48 return false;
49 }
50 for (int i = 1; i < n; ++i) {
51 if (bars[i].timestamp <= bars[i - 1].timestamp) {
53 *this, "auxiliary request.security feed timestamps must be strictly increasing",
54 RunFailureCode::security_feed_refused, {{"reason", "aux_feed_order"}});
55 return false;
56 }
57 }
58 aux_security_bars_.assign(bars, bars + n);
59 aux_security_input_tf_ = input_tf;
60 clear_aux_security_chart_ranges();
61 clear_run_failure(*this);
62 return true;
63}
64
67 f.u(aux_security_bars_.size());
68 for (const auto& bar : aux_security_bars_) {
69 f.d(bar.open); f.d(bar.high); f.d(bar.low); f.d(bar.close);
70 f.d(bar.volume); f.i(bar.timestamp);
71 }
74 for (const auto value : aux_security_chart_begin_) f.u(value);
75 f.u(aux_security_chart_end_.size());
76 for (const auto value : aux_security_chart_end_) f.u(value);
78}
79
83
85 const Bar*& bars, int& n) const {
86 bars = aux_security_bars_.data();
87 n = static_cast<int>(aux_security_bars_.size());
88}
89
90void source::PineStrategyHost::clear_aux_security_chart_ranges() {
91 aux_security_chart_begin_.clear();
92 aux_security_chart_end_.clear();
93 refresh_aux_security_digest();
94}
95
96void source::PineStrategyHost::prepare_aux_security_chart_ranges(
97 const Bar* chart_bars, int n_chart, const std::string& chart_tf) {
98 // However it leaves -- ranges prepared, none needed, or a refused feed --
99 // the digest follows what it left.
100 struct Refresh {
101 PineStrategyHost& host;
102 ~Refresh() { host.refresh_aux_security_digest(); }
103 } refresh{*this};
104 clear_aux_security_chart_ranges();
105 if (!aux_security_feed_enabled()) return;
106 if (chart_bars == nullptr || n_chart <= 0) {
107 throw coded<std::runtime_error>(
108 RunFailureCode::security_feed_refused, {{"reason", "aux_feed_no_chart"}},
109 "auxiliary request.security feed requires at least one native chart bar");
110 }
111
112 int chart_seconds = 0;
113 int aux_seconds = 0;
114 try {
115 chart_seconds = tf_to_seconds(chart_tf);
116 aux_seconds = tf_to_seconds(aux_security_input_tf_);
117 } catch (...) {
118 chart_seconds = 0;
119 aux_seconds = 0;
120 }
121 if (chart_seconds <= 0 || aux_seconds <= 0
122 || aux_seconds >= chart_seconds) {
123 throw coded<std::runtime_error>(
124 RunFailureCode::security_feed_refused, {{"reason", "aux_feed_not_finer"}},
125 "auxiliary request.security feed input_tf must be fixed and strictly finer than the native chart timeframe");
126 }
127 if (chart_seconds % aux_seconds != 0) {
128 throw coded<std::runtime_error>(
129 RunFailureCode::security_feed_refused, {{"reason", "aux_feed_not_divisor"}},
130 "auxiliary request.security feed input_tf must evenly divide the native chart timeframe");
131 }
132 for (int i = 1; i < n_chart; ++i) {
133 if (chart_bars[i].timestamp <= chart_bars[i - 1].timestamp) {
134 throw coded<std::runtime_error>(
135 RunFailureCode::security_feed_refused, {{"reason", "aux_chart_order"}},
136 "native chart feed timestamps must be strictly increasing with an auxiliary security feed");
137 }
138 }
139
140 const std::size_t missing = std::numeric_limits<std::size_t>::max();
141 aux_security_chart_begin_.assign(static_cast<std::size_t>(n_chart), missing);
142 aux_security_chart_end_.assign(static_cast<std::size_t>(n_chart), missing);
143 TimeframeAggregator chart_router(chart_tf, aux_security_input_tf_,
144 syminfo_.timezone, syminfo_.session);
145 const CalendarPeriod chart_period = calendar_period_for(chart_tf);
146 const bool calendar_chart = chart_period != CalendarPeriod::NONE;
147 // Calendar chart timestamps are the ACTUAL exchange bar opens and define
148 // the authoritative partition. A native daily bar may begin at a shifted
149 // special-session open (NSE Muhurat), or may coalesce more than one
150 // nominal session key (the CME Labor-Day Sunday/Monday sessions). Route
151 // each auxiliary bar through [chart_ts[i], chart_ts[i + 1]) while retaining
152 // the native label for chart and broker semantics. Nominal calendar keys
153 // remain useful only for ignoring wider leading/trailing feed coverage.
154 std::vector<int64_t> chart_route_keys;
155 chart_route_keys.reserve(static_cast<std::size_t>(n_chart));
156 for (int i = 0; i < n_chart; ++i) {
157 // Key each native bar by the session it COVERS: OANDA stamps daily
158 // bars at the 17:00 ET break, one hour before the 1800-1700 session
159 // the bar actually carries, and the raw stamp would key one session
160 // too early -- the tail prefilter below would then drop the last
161 // bar's entire slice as beyond last_chart_key.
162 const int64_t key = calendar_chart
164 session_covered_instant_ms(chart_bars[i].timestamp,
165 syminfo_.timezone,
166 syminfo_.session),
167 syminfo_.timezone, syminfo_.session, chart_period)
168 : chart_bars[i].timestamp;
169 // A calendar bar may share its covered-session key with the native
170 // bar after it: an NSE Muhurat session held after the regular close
171 // (Sunday 2023-11-12 18:15 IST, Monday 2022-10-24 18:15 IST on the
172 // nifty-1d lane) covers the NEXT regular session. The calendar route
173 // below partitions by the native timestamps, which are strictly
174 // increasing (checked above), so the two keep their own slices; only
175 // a key that runs backwards is refused there. Intraday keys are the
176 // routing labels themselves and stay strictly increasing.
177 if (!chart_route_keys.empty()
178 && (calendar_chart ? key < chart_route_keys.back()
179 : key <= chart_route_keys.back())) {
180 throw coded<std::runtime_error>(
181 RunFailureCode::security_feed_refused, {{"reason", "aux_chart_period_identity"}},
182 "native chart feed trading-period identities must be unique and strictly increasing with an auxiliary security feed");
183 }
184 chart_route_keys.push_back(key);
185 }
186 const int64_t first_chart_key = chart_route_keys.front();
187 const int64_t last_chart_key = chart_route_keys.back();
188 std::size_t chart_index = 0;
189 auto record_aux = [&](std::size_t aux_index) {
190 if (aux_security_chart_begin_[chart_index] == missing) {
191 aux_security_chart_begin_[chart_index] = aux_index;
192 }
193 aux_security_chart_end_[chart_index] = aux_index + 1;
194 };
195 if (calendar_chart) {
196 const int64_t first_chart_timestamp = chart_bars[0].timestamp;
197 for (std::size_t aux_index = 0;
198 aux_index < aux_security_bars_.size(); ++aux_index) {
199 const int64_t aux_timestamp =
200 aux_security_bars_[aux_index].timestamp;
201 const int64_t period_key = session_period_open_ms(
202 aux_timestamp, syminfo_.timezone, syminfo_.session,
203 chart_period);
204 if (period_key < first_chart_key || period_key > last_chart_key
205 || aux_timestamp < first_chart_timestamp) {
206 continue;
207 }
208 while (chart_index + 1 < static_cast<std::size_t>(n_chart)
209 && chart_bars[chart_index + 1].timestamp <= aux_timestamp) {
210 ++chart_index;
211 }
212 record_aux(aux_index);
213 }
214 } else {
215 for (std::size_t aux_index = 0;
216 aux_index < aux_security_bars_.size(); ++aux_index) {
217 const int64_t label = chart_router.bar_label_ms(
218 aux_security_bars_[aux_index].timestamp);
219 // Intraday native bars remain exact session-grid labels. Wider
220 // leading/trailing coverage is inert, but an interior grid hole
221 // must fail closed rather than attach to a neighbouring bar.
222 if (label < first_chart_key || label > last_chart_key) {
223 continue;
224 }
225 while (chart_index + 1 < static_cast<std::size_t>(n_chart)
226 && chart_route_keys[chart_index] < label) {
227 ++chart_index;
228 }
229 if (chart_route_keys[chart_index] != label) {
230 throw coded<std::runtime_error>(
232 "auxiliary request.security bar does not map to a native chart bar");
233 }
234 record_aux(aux_index);
235 }
236 }
237 // An exchange chart can retain a short/early-close chart slot for which
238 // the finer export has no bar. Retain the sentinel so the native-hook
239 // route supplies no auxiliary evaluator input at that slot and holds the
240 // existing request.security value instead of rejecting the chart run.
241}
242
243int64_t source::PineStrategyHost::aux_security_calling_close_ms() const {
244 const CalendarPeriod chart_period = calendar_period_for(input_tf_);
245 if (chart_period != CalendarPeriod::NONE) {
247 session_covered_instant_ms(current_bar_.timestamp,
248 syminfo_.timezone, syminfo_.session),
249 syminfo_.timezone, syminfo_.session, chart_period);
250 }
251 int chart_seconds = 0;
252 try {
253 chart_seconds = tf_to_seconds(input_tf_);
254 } catch (...) {
255 chart_seconds = 0;
256 }
257 return chart_seconds > 0
258 ? current_bar_.timestamp + static_cast<int64_t>(chart_seconds) * 1000
259 : 0;
260}
261
262void source::PineStrategyHost::feed_aux_security_for_chart_bar(int chart_index) {
263 const std::size_t idx = static_cast<std::size_t>(chart_index);
264 if (idx >= aux_security_chart_begin_.size()
265 || idx >= aux_security_chart_end_.size()) {
266 throw coded<std::runtime_error>(
268 "auxiliary request.security chart routing is not initialized");
269 }
270 const std::size_t begin = aux_security_chart_begin_[idx];
271 const std::size_t end = aux_security_chart_end_[idx];
272 const Bar* calling_bar = scheduler_.current_script_bar();
273 if (calling_bar == nullptr) {
274 throw coded<std::runtime_error>(
276 "auxiliary request.security routing requires a calling chart bar");
277 }
278 const auto chart_timeframe = native_calendar::parse_timeframe(script_tf_);
279 const auto input_timeframe = native_calendar::parse_timeframe(input_tf_);
280 const bool native_calendar_input = chart_timeframe && input_timeframe
281 && same_calendar_timeframe(*input_timeframe, *chart_timeframe);
282 std::vector<bool> uses_native_chart;
283 uses_native_chart.reserve(security_eval_states_.size());
284 for (const auto& state : security_eval_states_) {
285 const auto& pine = pine_security_state(state.sec_id);
286 const auto requested_timeframe = native_calendar::parse_timeframe(state.tf);
287 uses_native_chart.push_back(native_calendar_input && requested_timeframe
288 && same_calendar_timeframe(*requested_timeframe, *chart_timeframe)
289 && !pine.heikinashi && !pine.lower_tf_array_requested);
290 }
291 for (std::size_t state_index = 0; state_index < security_eval_states_.size();
292 ++state_index) {
293 auto& state = security_eval_states_[state_index];
294 if (!uses_native_chart[state_index]
295 || security_input_precedes_range_start(state, calling_bar->timestamp)) {
296 continue;
297 }
298 auto& pine = pine_security_state(state.sec_id);
299 if (pine.no_loaded_bars
300 || (state.eval_complete_count > 0
301 && state.current_bar.timestamp == calling_bar->timestamp)) {
302 continue;
303 }
304 state.current_bar = *calling_bar;
305 state.current_sub_bar_count = 1;
306 ++state.feed_count;
307 ++state.eval_complete_count;
308 pine.last_published_label = calling_bar->timestamp;
309 internal::AmbientEmaSeedingScope warmup_scope(
310 NativeExecutionConsumer::bound(*this).pump_ambient(),
311 security_range_start_na_warmup_);
312 dispatch_security_eval(state, *calling_bar, true,
313 state.eval_complete_count - 1);
314 }
315 const std::size_t missing = std::numeric_limits<std::size_t>::max();
316 if (begin == missing || end == missing) return;
317
318 security_calling_close_ms_ = aux_security_calling_close_ms();
319 // The calling chart bar's time, where TradingView merges a finer
320 // lookahead_on request (calling_open_latches_first).
321 const int64_t calling_open_ms = calling_bar->timestamp;
322
323 // A first-bucket-latched evaluator (calling_open_latches_first) starts
324 // every chart bar's slice live and is deferred once its first bucket of
325 // the slice has been published.
326 for (auto& state : security_eval_states_) {
327 PineSecurityEvalState& pine = pine_security_state(state.sec_id);
328 pine.first_bucket_published = false;
329 pine.deferred_aux.clear();
330 pine.slice_open_label = begin < end
331 ? state.aggregator.bucket_open_ms(aux_security_bars_[begin].timestamp)
332 : 0;
333 }
334
335 // Lower-TF arrays belong to the native chart slice, not a raw UTC bucket.
336 // Accumulate the entire symbol-clock-aligned slice here and publish it only
337 // after its final auxiliary bar. This keeps an overnight 17:00-17:00 daily
338 // bar as one array even though its raw timestamps cross UTC midnight.
339 for (std::size_t i = begin; i < end; ++i) {
340 const Bar& aux_bar = aux_security_bars_[i];
341 const bool calling_bar_complete = (i + 1 == end);
342 // The auxiliary bar after this one, across chart bars (0 at the
343 // feed's end): a calendar bucket completes on the period's actual
344 // last bar (security_next_input_ms_).
345 security_next_input_ms_ = (i + 1 < aux_security_bars_.size())
346 ? aux_security_bars_[i + 1].timestamp : 0;
347 for (std::size_t state_index = 0; state_index < security_eval_states_.size();
348 ++state_index) {
349 auto& state = security_eval_states_[state_index];
350 if (uses_native_chart[state_index]) continue;
351 PineSecurityEvalState& pine = pine_security_state(state.sec_id);
352 if (!pine.lower_tf_array_requested) {
353 // TradingView merges the requested bar opening at or before
354 // the calling bar's time. When none does -- OANDA stamps its
355 // daily bars at 17:00 ET, inside the break before the 18:00
356 // session they carry -- it reads the one before, the previous
357 // slice's last bucket, so the chart body runs before this
358 // slice is fed (lab tv w8c-ltfon-xau1d: 278/278 daily bars
359 // read the 16:45 ET bucket, or Friday's on the Sunday stamp).
360 // A last bucket no rule completed (the Thanksgiving "3"
361 // singleton) is that bar too (lab tv w8c-ltfon-3m-xau1d, the
362 // Black Friday bar): finalize and publish it first; lookahead_on
363 // evaluated it on its sub-bars, so it rewrites that slot.
364 // Heikin-Ashi sites keep the path below.
365 if (pine.calling_open_latches_first && !pine.heikinashi
366 && !pine.first_bucket_published
367 && state.aggregator.bucket_open_ms(aux_bar.timestamp)
368 > calling_open_ms) {
369 if (state.aggregator.has_pending_partial()) {
370 AggregatedBar tail = state.aggregator.complete_pending_partial();
371 if (tail.is_complete) {
372 state.current_sub_bar_count =
373 std::max(tail.sub_bar_count, 2);
374 substitute_native_security_bar(state, tail.bar);
375 state.current_bar = tail.bar;
376 state.eval_complete_count++;
377 pine.last_published_label = tail.bar.timestamp;
378 internal::AmbientEmaSeedingScope tail_warmup_scope(
379 NativeExecutionConsumer::bound(*this).pump_ambient(),
380 security_range_start_na_warmup_);
381 dispatch_security_eval(state, tail.bar, true,
382 state.eval_complete_count - 1);
383 }
384 }
385 pine.first_bucket_published = true;
386 }
387 if (pine.calling_open_latches_first
388 && pine.first_bucket_published) {
389 // The latch is closed (the slice's first bucket is
390 // published, or none opens at or before the calling
391 // bar's time): the rest of the slice follows the chart
392 // body (feed_deferred_aux_security_for_chart_bar).
393 pine.deferred_aux.push_back(
394 {aux_bar, security_next_input_ms_,
395 calling_bar_complete});
396 continue;
397 }
398 const int64_t published_before = state.eval_complete_count;
399 pine_feed_security_eval_state(
400 state, aux_bar, calling_bar_complete);
401 // The slice's first bucket is published: a completion
402 // labelled at or after the slice's first bucket open. An
403 // older label is the previous slice's pending tail emitted
404 // on this boundary (the Thanksgiving 21:57 singleton on
405 // Black Friday's first minute): it precedes this bar's
406 // first bucket in the requested series and does not latch.
407 if (pine.calling_open_latches_first
408 && state.eval_complete_count > published_before
409 && pine.last_published_label >= pine.slice_open_label) {
410 pine.first_bucket_published = true;
411 }
412 continue;
413 }
414 if (security_input_precedes_range_start(state, aux_bar.timestamp)) {
415 continue;
416 }
417 if (pine.lower_tf_use_input) {
418 pine.lower_tf_input_buffer.push_back(aux_bar);
419 } else if (pine.lower_tf_emulation) {
420 std::vector<Bar> synthetic = internal::synthesize_lower_tf_bars(
421 aux_bar, pine.lower_tf_ratio, pine.lower_tf_seconds);
422 if (synthetic.empty()) {
423 throw coded<std::runtime_error>(
425 "request.security_lower_tf could not synthesize auxiliary sub-bars");
426 }
427 pine.lower_tf_input_buffer.insert(
428 pine.lower_tf_input_buffer.end(),
429 synthetic.begin(), synthetic.end());
430 } else {
431 throw coded<std::runtime_error>(
433 "request.security_lower_tf auxiliary routing is not initialized");
434 }
435 }
436 }
437
438 security_calling_close_ms_ = 0;
439
440 // On the pump's runtime block when it has one (R5 lane D2-C).
441 internal::AmbientEmaSeedingScope warmup_scope(
442 NativeExecutionConsumer::bound(*this).pump_ambient(),
443 security_range_start_na_warmup_);
444
445 for (auto& state : security_eval_states_) {
446 PineSecurityEvalState& pine = pine_security_state(state.sec_id);
447 if (!pine.lower_tf_array_requested) continue;
448 int aggregate_ratio = pine.lower_tf_emulation
449 ? 1 : pine.lower_tf_input_aggregation_ratio;
450 if (aggregate_ratio < 1) aggregate_ratio = 1;
451 const int count = static_cast<int>(pine.lower_tf_input_buffer.size());
452 std::vector<Bar> requested_bars;
453 requested_bars.reserve(
454 static_cast<std::size_t>(count / aggregate_ratio + 1));
455 if (aggregate_ratio == 1) {
456 requested_bars.assign(pine.lower_tf_input_buffer.begin(),
457 pine.lower_tf_input_buffer.end());
458 } else {
459 for (int i = 0; i + aggregate_ratio <= count;
460 i += aggregate_ratio) {
461 Bar aggregate = pine.lower_tf_input_buffer[
462 static_cast<std::size_t>(i)];
463 double volume = aggregate.volume;
464 for (int j = 1; j < aggregate_ratio; ++j) {
465 const Bar& next = pine.lower_tf_input_buffer[
466 static_cast<std::size_t>(i + j)];
467 aggregate.high = std::max(aggregate.high, next.high);
468 aggregate.low = std::min(aggregate.low, next.low);
469 aggregate.close = next.close;
470 volume += next.volume;
471 }
472 aggregate.volume = volume;
473 requested_bars.push_back(aggregate);
474 }
475 }
476
477 pine.lower_tf_sub_bar_index = 0;
478 for (const Bar& bar : requested_bars) {
479 state.feed_count++;
480 state.current_bar = bar;
481 state.current_sub_bar_count = 1;
482 state.eval_complete_count++;
483 dispatch_security_eval(state, bar, true,
484 state.eval_complete_count - 1);
485 pine.lower_tf_sub_bar_index++;
486 }
487 pine.lower_tf_input_buffer.clear();
488 }
489}
490
491void source::PineStrategyHost::feed_deferred_aux_security_for_chart_bar(int chart_index) {
492 (void)chart_index;
493 bool any = false;
494 for (const auto& entry : pine_security_states_) {
495 if (entry.second.calling_open_latches_first && !entry.second.deferred_aux.empty()) {
496 any = true;
497 break;
498 }
499 }
500 if (!any) return;
501 security_calling_close_ms_ = aux_security_calling_close_ms();
502 for (auto& state : security_eval_states_) {
503 PineSecurityEvalState& pine = pine_security_state(state.sec_id);
504 if (!pine.calling_open_latches_first || pine.deferred_aux.empty()) {
505 continue;
506 }
507 // Move the slice out first: feeding never re-enters the deferral
508 // (first_bucket_published stays set until the next chart bar's
509 // slice resets it), but the buffer must not be appended to while
510 // it is walked.
511 std::vector<DeferredAuxBar> held;
512 held.swap(pine.deferred_aux);
513 for (const auto& d : held) {
514 security_next_input_ms_ = d.next_input_ms;
515 pine_feed_security_eval_state(state, d.bar,
516 d.calling_bar_complete);
517 }
518 }
519 security_calling_close_ms_ = 0;
520}
521
523 clear_aux_security_chart_ranges();
524 std::vector<std::size_t>().swap(aux_security_chart_begin_);
525 std::vector<std::size_t>().swap(aux_security_chart_end_);
526}
527
528#endif // PINEFORGE_HAS_AUX_SECURITY_FEED_V1
529
530} // namespace pineforge
bool set_aux_security_feed(const Bar *bars, int n, const std::string &input_tf) override
std::vector< std::size_t > aux_security_chart_begin_
bool source_aux_security_feed_enabled() const override
void source_aux_security_input_view(const Bar *&, int &) const override
std::vector< std::size_t > aux_security_chart_end_
std::optional< Timeframe > parse_timeframe(std::string_view text)
std::optional< int64_t > period_key(const SessionCalendar &calendar, const Timeframe &tf, int64_t ms)
bool same_calendar_timeframe(const native_calendar::Timeframe &requested, const native_calendar::Timeframe &chart) noexcept
int64_t session_covered_instant_ms(int64_t ms, const std::string &tz, const std::string &session)
The session instant a native CALENDAR chart stamp covers.
int64_t session_period_last_traded_close_ms(int64_t ms, const std::string &tz, const std::string &session, CalendarPeriod period)
Exclusive close (Unix ms) of the LAST TRADED session-day of the D/W/M bar that contains ms: DAY is se...
int tf_to_seconds(const std::string &tf)
Convert a TradingView timeframe string to seconds.
CalendarPeriod calendar_period_for(const std::string &tf)
Determine the calendar period for a target TF string.
void note_run_failure(BacktestEngine &engine, std::string text, RunFailureCode code, const RunFailureArgs &args={})
void clear_run_failure(BacktestEngine &engine) noexcept
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...
int64_t timestamp
Definition bar.hpp:8