15#include "../engine_internal.hpp"
37void ensure_supported_lower_tf_emulation_flags(
bool lookahead_on,
bool gaps_on) {
38 if (lookahead_on || gaps_on) {
39 throw std::runtime_error(
40 "request.security lower TF emulation only supports lookahead=barmerge.lookahead_off and gaps=barmerge.gaps_off"
50 int sec_id,
const std::string& requested_tf,
const std::string& input_tf,
51 bool lookahead_on,
bool gaps_on,
bool heikinashi) {
52 const std::size_t before = security_eval_states_.size();
57 BacktestEngine::register_security_eval(sec_id, requested_tf, input_tf);
58 if (security_eval_states_.size() <= before)
return;
59 SecurityEvalState& state = security_eval_states_.back();
66 const std::string& evaluator_input_tf =
67 security_input_tf_.empty() ? input_tf : security_input_tf_;
68 if (evaluator_input_tf.empty())
return;
70 int lower_seconds = 0;
71 if (supports_lower_tf_emulation(evaluator_input_tf, requested_tf,
72 &lower_ratio, &lower_seconds)) {
98 const std::string& requested_tf,
99 const std::string& input_tf
101 auto before = security_eval_states_.size();
103 if (security_eval_states_.size() > before) {
104 pine_security_state(sec_id).lower_tf_array_requested =
true;
113 for (
const auto& state : security_eval_states_) {
114 if (state.sec_id != sec_id) {
118 const bool lookahead_on =
120 return !lookahead_on || state.current_sub_bar_count <= 1;
127 for (
const auto& state : security_eval_states_) {
128 if (state.sec_id == sec_id) {
129 return pine_security_state(sec_id).lower_tf_sub_bar_index;
136void source::PineStrategyHost::prune_pine_security_states() {
137 for (
auto it = pine_security_states_.begin(); it != pine_security_states_.end();) {
138 bool registered =
false;
139 for (
const auto& state : security_eval_states_) {
140 if (state.sec_id == it->first) {
145 it = registered ? std::next(it) : pine_security_states_.erase(it);
163 if (input_tf.empty()) {
164 if (!security_eval_states_.empty()) {
165 throw std::runtime_error(
166 "request.security cannot infer input timeframe from available input bars; pass input_tf explicitly"
178 int script_seconds = script_tf_seconds_;
179 for (
auto& state : security_eval_states_) {
193 if (state.tf.empty())
continue;
196 int lower_seconds = 0;
197 bool ltf_supported = supports_lower_tf_emulation(
198 input_tf, state.tf, &lower_ratio, &lower_seconds);
222 if (requested_seconds <= 0 && !is_calendar_month) {
224 ?
"request.security_lower_tf" :
"request.security";
225 throw std::runtime_error(
226 std::string(api) +
": invalid timeframe literal '" + state.tf +
"'"
230 if (!is_calendar_month && requested_seconds < input_seconds) {
234 throw std::runtime_error(
235 "request.security: requested timeframe '" + state.tf
236 +
"' is finer than input '" + input_tf
237 +
"'. Use request.security_lower_tf for sub-input timeframes."
241 if (input_seconds % requested_seconds != 0) {
242 throw std::runtime_error(
243 "request.security_lower_tf: requested timeframe '" + state.tf
244 +
"' is not an integer divisor of input '" + input_tf
245 +
"' (ratio " + std::to_string(
246 static_cast<double>(input_seconds) / requested_seconds)
247 +
" is non-integer; chart bars cannot be evenly subdivided)"
254 throw std::runtime_error(
255 "request.security_lower_tf: internal error - passed integer-ratio check but emulation support returned false (requested '"
256 + state.tf +
"', input '" + input_tf +
"')"
265 if (script_seconds <= 0) {
266 throw std::runtime_error(
267 "request.security_lower_tf: script timeframe is unknown — cannot validate '"
268 + state.tf +
"' against script TF"
271 if (requested_seconds >= script_seconds) {
272 throw std::runtime_error(
273 "request.security_lower_tf: requested timeframe '" + state.tf
274 +
"' must be finer than script timeframe '" + script_tf_
275 +
"'. Lower-TF API requires a strictly finer timeframe."
278 if (script_seconds % requested_seconds != 0) {
279 throw std::runtime_error(
280 "request.security_lower_tf: requested timeframe '" + state.tf
281 +
"' must evenly divide script timeframe '"
282 + script_tf_ +
"' (script_tf must be an integer multiple of requested TF)"
285 if (requested_seconds % input_seconds != 0) {
286 throw std::runtime_error(
287 "request.security_lower_tf: requested timeframe '" + state.tf
288 +
"' is not an integer multiple of input '" + input_tf
289 +
"' (cannot aggregate raw input bars to requested TF)"
295 requested_seconds / input_seconds;
299 && script_seconds > 0
300 && requested_seconds < script_seconds
301 && script_seconds % requested_seconds == 0) {
327#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
335 if (aux_security_feed_enabled()) {
341#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
343 && aux_security_feed_enabled()
344 && script_seconds > 0
345 && requested_seconds < script_seconds) {
360void source::PineStrategyHost::publish_security_eval_state_at_calling_boundary(
361 SecurityEvalState& state) {
362 if (pine_security_state(state.sec_id).publish_gate_tf_seconds <= 0
363 || state.feed_count <= 0) {
370 explicit ReplayScope(
bool& flag)
371 : active(flag), previous(flag) { active =
true; }
372 ~ReplayScope() { active = previous; }
373 } replay_scope(security_history_publication_replay_);
381 dispatch_security_eval(state, state.current_bar,
true,
382 state.ta_bar_index >= 0 ? state.ta_bar_index
383 : state.eval_complete_count);
387bool source::PineStrategyHost::security_input_precedes_range_start(
388 const SecurityEvalState& state, int64_t input_ts)
const {
390 if (security_range_start_na_warmup_) {
403#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
426 if (aux_security_feed_enabled() && !pine.lower_tf_requested
427 && !pine.lower_tf_array_requested) {
431 &&
static_cast<int>(period) >
static_cast<int>(chart_period);
432 if (strictly_coarser) {
434 input_ts, syminfo_.timezone, syminfo_.session, period);
435 if (nominal_open < security_range_start_ms_
436 && aux_security_traded_between(nominal_open,
437 security_range_start_ms_)) {
443 return state.aggregator.bucket_open_ms(input_ts) < security_range_start_ms_;
445#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
453 if (!aux_security_feed_enabled() && native_security_feeds_.empty()
454 && security_first_chart_bar_ms_ > 0
455 && script_tf_seconds_ > 0 && script_tf_seconds_ < 86400
456 && (state.tf ==
"D" || state.tf ==
"1D")
457 && !pine.lower_tf_requested && !pine.lower_tf_array_requested
458 && (syminfo_.type ==
"forex" || syminfo_.type ==
"cfd")
459 && !stream_warmup_mode_ && stream_phase_ == StreamPhase::IDLE) {
465 stamp, syminfo_.timezone, syminfo_.session);
466 return trading_open < security_first_chart_bar_ms_;
485 if (security_first_chart_bar_ms_ <= 0 || !aux_security_feed_enabled()
486 || pine.lower_tf_requested || pine.lower_tf_array_requested
487 || script_tf_seconds_ <= 0) {
491 const bool coarser_or_chart = requested_seconds == -1
492 || requested_seconds >= script_tf_seconds_;
493 if (!coarser_or_chart) {
501 syminfo_.timezone, syminfo_.session);
502 if (nominal_open >= security_first_chart_bar_ms_) {
505 return aux_security_traded_between(nominal_open, security_first_chart_bar_ms_);
512#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
513bool source::PineStrategyHost::aux_security_traded_between(int64_t from_ms,
514 int64_t to_ms)
const {
515 const Bar* bars =
nullptr;
517 source_aux_security_input_view(bars, n);
518 if (bars ==
nullptr || n <= 0)
return false;
519 auto it = std::lower_bound(
520 bars, bars + n, from_ms,
521 [](
const Bar& bar, int64_t ts) {
return bar.timestamp < ts; });
522 return it != bars + n && it->timestamp < to_ms;
527void source::PineStrategyHost::pine_feed_security_eval_state(
528 SecurityEvalState& state,
const Bar& input_bar,
529 bool calling_bar_complete) {
545 if (security_input_precedes_range_start(state, input_bar.timestamp)) {
549 struct SecurityNaWarmupScope {
551 explicit SecurityNaWarmupScope(
bool on)
553 ~SecurityNaWarmupScope() { ta::ema_na_warmup_flag() = prev_; }
554 } _na_warmup_scope(security_range_start_na_warmup_);
560 auto apply_ha = [&pine](Bar&
b,
bool commit) {
561 double ha_close = (
b.open +
b.high +
b.low +
b.close) / 4.0;
562 double ha_open = pine.ha_seeded
563 ? (pine.ha_prev_open + pine.ha_prev_close) / 2.0
564 : (
b.open +
b.close) / 2.0;
565 double ha_high = std::max(
b.high, std::max(ha_open, ha_close));
566 double ha_low = std::min(
b.low, std::min(ha_open, ha_close));
572 pine.ha_prev_open = ha_open;
573 pine.ha_prev_close = ha_close;
574 pine.ha_seeded =
true;
578 if (pine.lower_tf_use_input) {
596 int script_seconds = script_tf_seconds_;
597 if (input_seconds <= 0 || script_seconds <= 0) {
600 pine.lower_tf_input_buffer.push_back(input_bar);
603 int chunk_size = script_seconds / input_seconds;
604 if (chunk_size <= 0) {
605 pine.lower_tf_input_buffer.push_back(input_bar);
611 pine.lower_tf_input_buffer.reserve(
static_cast<std::size_t
>(chunk_size));
612 int64_t bucket_ms =
static_cast<int64_t
>(script_seconds) * 1000;
613 int64_t this_bucket = input_bar.timestamp / bucket_ms;
614 bool boundary_crossed =
false;
615 if (!pine.lower_tf_input_buffer.empty()) {
616 int64_t buffer_bucket =
617 pine.lower_tf_input_buffer.front().timestamp / bucket_ms;
618 if (this_bucket != buffer_bucket) {
619 boundary_crossed =
true;
628 auto dispatch_and_clear = [&]() {
629 int agg_ratio = pine.lower_tf_input_aggregation_ratio;
630 if (agg_ratio < 1) agg_ratio = 1;
631 int buf_len =
static_cast<int>(pine.lower_tf_input_buffer.size());
632 std::vector<Bar> ltf_bars;
633 ltf_bars.reserve(
static_cast<std::size_t
>(buf_len / agg_ratio + 1));
634 if (agg_ratio == 1) {
635 for (
const Bar& b : pine.lower_tf_input_buffer) {
636 ltf_bars.push_back(b);
639 for (
int i = 0; i + agg_ratio <= buf_len; i += agg_ratio) {
640 Bar acc = pine.lower_tf_input_buffer[
641 static_cast<std::size_t
>(i)];
642 double vol = acc.volume;
643 for (
int j = 1; j < agg_ratio; ++j) {
644 const Bar& nxt = pine.lower_tf_input_buffer[
645 static_cast<std::size_t
>(i + j)];
646 if (nxt.high > acc.high) acc.high = nxt.high;
647 if (nxt.low < acc.low) acc.low = nxt.low;
648 acc.close = nxt.close;
652 ltf_bars.push_back(acc);
655 pine.lower_tf_sub_bar_index = 0;
656 for (
const Bar& b : ltf_bars) {
658 state.current_bar =
b;
659 state.current_sub_bar_count = 1;
660 state.eval_complete_count++;
661 dispatch_security_eval(state, b,
true,
662 state.eval_complete_count - 1);
663 pine.lower_tf_sub_bar_index++;
665 pine.lower_tf_input_buffer.clear();
668 if (boundary_crossed) {
671 dispatch_and_clear();
674 pine.lower_tf_input_buffer.push_back(input_bar);
680 if (
static_cast<int>(pine.lower_tf_input_buffer.size()) >= chunk_size) {
681 dispatch_and_clear();
685 if (pine.lower_tf_emulation) {
686 std::vector<Bar> synthetic_bars =
687 synthesize_lower_tf_bars(input_bar, pine.lower_tf_ratio, pine.lower_tf_seconds);
688 if (synthetic_bars.empty()) {
689 throw std::runtime_error(
690 "request.security lower TF emulation could not synthesize bars for requested "
691 + state.tf +
" from input timeframe " + security_input_tf_
699 pine.lower_tf_sub_bar_index = 0;
700 for (
const auto& synthetic_bar : synthetic_bars) {
702 state.current_bar = synthetic_bar;
703 state.current_sub_bar_count = 1;
704 state.eval_complete_count++;
705 dispatch_security_eval(state, synthetic_bar,
true,
706 state.eval_complete_count - 1);
707 pine.lower_tf_sub_bar_index++;
712 if (historical_security_lookahead_projection_active_
713 && !pine.historical_projections.empty()) {
722 const int64_t input_ms = input_bar.timestamp;
723 while (pine.historical_projection_cursor + 1
724 < pine.historical_projections.size()
725 && pine.historical_projections[
726 pine.historical_projection_cursor + 1]
727 .first_child_ms <= input_ms) {
728 ++pine.historical_projection_cursor;
729 pine.historical_projection_dispatched =
false;
731 const auto& projection = pine.historical_projections[
732 pine.historical_projection_cursor];
734 if (pine.historical_projection_dispatched
735 || input_ms < projection.first_child_ms) {
741 pine.historical_projection_dispatched =
true;
743 Bar projected_bar = projection.bar;
753 substitute_native_security_bar(state, projected_bar,
754 projection.is_complete);
755 if (pine.heikinashi) {
756 apply_ha(projected_bar, projection.is_complete);
758 state.current_bar = projected_bar;
762 state.current_sub_bar_count = 1;
763 if (projection.is_complete) {
764 state.eval_complete_count++;
766 state.eval_partial_count++;
768 dispatch_security_eval(state, projected_bar, projection.is_complete,
769 projection.is_complete
770 ? state.eval_complete_count - 1
771 : state.eval_complete_count);
780 AggregatedBar ab = state.aggregator.feed(input_bar, security_next_input_ms_,
781 security_calling_close_ms_);
783 state.current_sub_bar_count = ab.sub_bar_count;
784 if (ab.is_complete) {
789 substitute_native_security_bar(state, ab.bar);
790 if (pine.heikinashi) apply_ha(ab.bar,
true);
791 state.current_bar = ab.bar;
792 state.eval_complete_count++;
808 if (pine.publish_gate_tf_seconds > 0 && script_tf_seconds_ > 0) {
815 publish = calling_bar_complete;
845 const bool boundary_emission = pine.lookahead_on
846 && state.aggregator.is_active()
847 && ab.bar.timestamp != state.aggregator.current().timestamp;
848 if (boundary_emission && state.current_sub_bar_count < 2) {
849 state.current_sub_bar_count = 2;
851 pine.last_published_label = ab.bar.timestamp;
852 dispatch_security_eval(state, ab.bar, publish,
853 state.eval_complete_count - 1);
854 if (boundary_emission) {
855 Bar fresh = state.aggregator.current();
856 if (pine.heikinashi) apply_ha(fresh,
false);
857 state.current_bar = fresh;
858 state.current_sub_bar_count = 1;
859 state.eval_partial_count++;
860 const bool peek_publish = pine.publish_gate_tf_seconds > 0
861 && calling_bar_complete;
862 dispatch_security_eval(state, fresh, peek_publish,
863 state.eval_complete_count);
865 }
else if (pine.lookahead_on) {
866 if (pine.heikinashi) apply_ha(ab.bar,
false);
867 state.current_bar = ab.bar;
868 state.eval_partial_count++;
872 const bool publish = pine.publish_gate_tf_seconds > 0
873 && calling_bar_complete;
875 dispatch_security_eval(state, ab.bar, publish,
876 state.eval_complete_count);
878 state.current_bar = ab.bar;
880 clear_security(state.sec_id);
899 if (calling_bar_complete && pine.calling_close_completes_partial
900 && state.aggregator.has_pending_partial()) {
901 AggregatedBar tail = state.aggregator.complete_pending_partial();
902 if (tail.is_complete) {
903 state.current_sub_bar_count = tail.sub_bar_count;
904 substitute_native_security_bar(state, tail.bar);
905 if (pine.heikinashi) apply_ha(tail.bar,
true);
906 state.current_bar = tail.bar;
907 state.eval_complete_count++;
908 pine.last_published_label = tail.bar.timestamp;
909 dispatch_security_eval(state, tail.bar,
true,
910 state.eval_complete_count - 1);
void validate_security_timeframes(const std::string &input_tf)
void register_security_eval(int sec_id, const std::string &requested_tf, const std::string &input_tf, bool lookahead_on, bool gaps_on=false, bool heikinashi=false)
bool security_history_publication_replay_
int security_lower_tf_sub_bar_index(int sec_id) const
void register_security_lower_tf_eval(int sec_id, const std::string &requested_tf, const std::string &input_tf)
bool security_series_slot_is_new(int) const noexcept
std::map< int, PineSecurityEvalState > pine_security_states_
bool & ema_na_warmup_flag()
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.
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.
int64_t session_intraday_bucket_open_ms(int64_t ms, int64_t bucket_sec, const std::string &tz, const std::string &session)
Open (Unix ms) of the bucket_sec-wide intraday bucket that contains ms on the symbol's day-stamp-anch...
static int safe_tf_to_seconds(const std::string &tf)
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...
int lower_tf_input_aggregation_ratio
bool calling_close_completes_partial
std::vector< DeferredAuxBar > deferred_aux
bool calling_open_latches_first
std::vector< Bar > lower_tf_input_buffer
bool first_bucket_published
int publish_gate_tf_seconds
bool lower_tf_array_requested