15#include "../engine_internal.hpp"
16#include "../native_execution_consumer.hpp"
38int request_timeframe_seconds(
const std::string& tf,
bool lower_tf_array_requested) {
39 int requested_seconds = 0;
42 }
catch (
const std::invalid_argument&) {
44 }
catch (
const std::out_of_range&) {
49 const bool is_calendar_month = requested_seconds == -1 && !lower_tf_array_requested;
50 if (requested_seconds <= 0 && !is_calendar_month) {
51 const char* api = lower_tf_array_requested
52 ?
"request.security_lower_tf" :
"request.security";
55 std::string(api) +
": invalid timeframe literal '" + tf +
"'"
58 return requested_seconds;
67void ensure_supported_lower_tf_emulation_flags(
bool lookahead_on,
bool gaps_on) {
68 if (lookahead_on || gaps_on) {
71 "request.security lower TF emulation only supports lookahead=barmerge.lookahead_off and gaps=barmerge.gaps_off"
81 int sec_id,
const std::string& requested_tf,
const std::string& input_tf,
82 bool lookahead_on,
bool gaps_on,
bool heikinashi) {
83 const std::string& evaluator_input_tf =
84 security_input_tf_.empty() ? input_tf : security_input_tf_;
87 if (!evaluator_input_tf.empty() && !requested_tf.empty()) {
88 request_timeframe_seconds(requested_tf,
false);
90 const std::size_t before = security_eval_states_.size();
95 BacktestEngine::register_security_eval(sec_id, requested_tf, input_tf);
96 if (security_eval_states_.size() <= before)
return;
97 SecurityEvalState& state = security_eval_states_.back();
104 if (evaluator_input_tf.empty())
return;
106 int lower_seconds = 0;
107 if (supports_lower_tf_emulation(evaluator_input_tf, requested_tf,
108 &lower_ratio, &lower_seconds)) {
134 const std::string& requested_tf,
135 const std::string& input_tf
137 const std::string& evaluator_input_tf =
138 security_input_tf_.empty() ? input_tf : security_input_tf_;
141 if (!evaluator_input_tf.empty() && !requested_tf.empty()) {
142 request_timeframe_seconds(requested_tf,
true);
144 auto before = security_eval_states_.size();
146 if (security_eval_states_.size() > before) {
147 pine_security_state(sec_id).lower_tf_array_requested =
true;
156 for (
const auto& state : security_eval_states_) {
157 if (state.sec_id != sec_id) {
161 const bool lookahead_on =
163 return !lookahead_on || state.current_sub_bar_count <= 1;
170 for (
const auto& state : security_eval_states_) {
171 if (state.sec_id == sec_id) {
172 return pine_security_state(sec_id).lower_tf_sub_bar_index;
179void source::PineStrategyHost::prune_pine_security_states() {
180 for (
auto it = pine_security_states_.begin(); it != pine_security_states_.end();) {
181 bool registered =
false;
182 for (
const auto& state : security_eval_states_) {
183 if (state.sec_id == it->first) {
188 it = registered ? std::next(it) : pine_security_states_.erase(it);
206 if (input_tf.empty()) {
207 if (!security_eval_states_.empty()) {
210 "request.security cannot infer input timeframe from available input bars; pass input_tf explicitly"
222 int script_seconds = script_tf_seconds_;
223 for (
auto& state : security_eval_states_) {
238 if (state.tf.empty())
continue;
242 const int requested_seconds =
244 const bool is_calendar_month = requested_seconds == -1;
246 int lower_seconds = 0;
247 bool ltf_supported = supports_lower_tf_emulation(
248 input_tf, state.tf, &lower_ratio, &lower_seconds);
261 if (!is_calendar_month && requested_seconds < input_seconds) {
272 const bool intrabar_bars_supplied = input_seconds > 0
273 && (script_seconds == -1 || input_seconds < script_seconds);
274 if (intrabar_bars_supplied) {
280 {{
"reason",
"finer_than_input"}, {
"input_tf", input_tf}},
281 "request.security: requested timeframe '" + state.tf
282 +
"' is finer than input '" + input_tf
283 +
"'. Use request.security_lower_tf for sub-input timeframes."
287 if (input_seconds % requested_seconds != 0) {
290 {{
"reason",
"lower_tf_not_divisor_of_input"}, {
"input_tf", input_tf}},
291 "request.security_lower_tf: requested timeframe '" + state.tf
292 +
"' is not an integer divisor of input '" + input_tf
293 +
"' (ratio " + std::to_string(
294 static_cast<double>(input_seconds) / requested_seconds)
295 +
" is non-integer; chart bars cannot be evenly subdivided)"
304 "request.security_lower_tf: internal error - passed integer-ratio check but emulation support returned false (requested '"
305 + state.tf +
"', input '" + input_tf +
"')"
314 if (script_seconds <= 0) {
317 {{
"reason",
"lower_tf_script_unknown"}},
318 "request.security_lower_tf: script timeframe is unknown — cannot validate '"
319 + state.tf +
"' against script TF"
322 if (requested_seconds >= script_seconds) {
325 {{
"reason",
"lower_tf_not_finer_than_script"}, {
"script_tf", script_tf_}},
326 "request.security_lower_tf: requested timeframe '" + state.tf
327 +
"' must be finer than script timeframe '" + script_tf_
328 +
"'. Lower-TF API requires a strictly finer timeframe."
331 if (script_seconds % requested_seconds != 0) {
334 {{
"reason",
"lower_tf_not_divisor_of_script"}, {
"script_tf", script_tf_}},
335 "request.security_lower_tf: requested timeframe '" + state.tf
336 +
"' must evenly divide script timeframe '"
337 + script_tf_ +
"' (script_tf must be an integer multiple of requested TF)"
340 if (requested_seconds % input_seconds != 0) {
343 {{
"reason",
"lower_tf_not_multiple_of_input"}, {
"input_tf", input_tf}},
344 "request.security_lower_tf: requested timeframe '" + state.tf
345 +
"' is not an integer multiple of input '" + input_tf
346 +
"' (cannot aggregate raw input bars to requested TF)"
352 requested_seconds / input_seconds;
356 && script_seconds > 0
357 && requested_seconds < script_seconds
358 && script_seconds % requested_seconds == 0) {
384#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
392 if (aux_security_feed_enabled()) {
398#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
400 && aux_security_feed_enabled()
401 && script_seconds > 0
402 && requested_seconds < script_seconds) {
417void source::PineStrategyHost::publish_security_eval_state_at_calling_boundary(
418 SecurityEvalState& state) {
419 if (pine_security_state(state.sec_id).publish_gate_tf_seconds <= 0
420 || state.feed_count <= 0) {
427 explicit ReplayScope(
bool& flag)
428 : active(flag), previous(flag) { active =
true; }
429 ~ReplayScope() { active = previous; }
430 } replay_scope(security_history_publication_replay_);
438 dispatch_security_eval(state, state.current_bar,
true,
439 state.ta_bar_index >= 0 ? state.ta_bar_index
440 : state.eval_complete_count);
444bool source::PineStrategyHost::security_input_precedes_range_start(
445 const SecurityEvalState& state, int64_t input_ts)
const {
447 if (security_range_start_na_warmup_) {
460#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
483 if (aux_security_feed_enabled() && !pine.lower_tf_requested
484 && !pine.lower_tf_array_requested) {
488 &&
static_cast<int>(period) >
static_cast<int>(chart_period);
489 if (strictly_coarser) {
491 input_ts, syminfo_.timezone, syminfo_.session, period);
492 if (nominal_open < security_range_start_ms_
493 && aux_security_traded_between(nominal_open,
494 security_range_start_ms_)) {
500 return state.aggregator.bucket_open_ms(input_ts) < security_range_start_ms_;
502#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
510 if (!aux_security_feed_enabled() && native_security_feeds_.empty()
511 && security_first_chart_bar_ms_ > 0
512 && script_tf_seconds_ > 0 && script_tf_seconds_ < 86400
513 && (state.tf ==
"D" || state.tf ==
"1D")
514 && !pine.lower_tf_requested && !pine.lower_tf_array_requested
515 && (syminfo_.type ==
"forex" || syminfo_.type ==
"cfd")) {
521 stamp, syminfo_.timezone, syminfo_.session);
522 return trading_open < security_first_chart_bar_ms_;
541 if (security_first_chart_bar_ms_ <= 0 || !aux_security_feed_enabled()
542 || pine.lower_tf_requested || pine.lower_tf_array_requested
543 || script_tf_seconds_ <= 0) {
547 const bool coarser_or_chart = requested_seconds == -1
548 || requested_seconds >= script_tf_seconds_;
549 if (!coarser_or_chart) {
557 syminfo_.timezone, syminfo_.session);
558 if (nominal_open >= security_first_chart_bar_ms_) {
561 return aux_security_traded_between(nominal_open, security_first_chart_bar_ms_);
568#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
569bool source::PineStrategyHost::aux_security_traded_between(int64_t from_ms,
570 int64_t to_ms)
const {
571 const Bar* bars =
nullptr;
573 source_aux_security_input_view(bars, n);
574 if (bars ==
nullptr || n <= 0)
return false;
575 auto it = std::lower_bound(
576 bars, bars + n, from_ms,
577 [](
const Bar& bar, int64_t ts) {
return bar.timestamp < ts; });
578 return it != bars + n && it->timestamp < to_ms;
583void source::PineStrategyHost::pine_feed_security_eval_state(
584 SecurityEvalState& state,
const Bar& input_bar,
585 bool calling_bar_complete) {
601 if (security_input_precedes_range_start(state, input_bar.timestamp)) {
607 if (pine.no_loaded_bars)
return;
609 internal::AmbientEmaSeedingScope _na_warmup_scope(
610 NativeExecutionConsumer::bound(*this).pump_ambient(),
611 security_range_start_na_warmup_);
617 auto apply_ha = [&pine](
Bar&
b,
bool commit) {
618 double ha_close = (
b.open +
b.high +
b.low +
b.close) / 4.0;
619 double ha_open = pine.ha_seeded
620 ? (pine.ha_prev_open + pine.ha_prev_close) / 2.0
621 : (
b.open +
b.close) / 2.0;
622 double ha_high = std::max(
b.high, std::max(ha_open, ha_close));
623 double ha_low = std::min(
b.low, std::min(ha_open, ha_close));
629 pine.ha_prev_open = ha_open;
630 pine.ha_prev_close = ha_close;
631 pine.ha_seeded =
true;
635 if (pine.lower_tf_use_input) {
653 int script_seconds = script_tf_seconds_;
654 if (input_seconds <= 0 || script_seconds <= 0) {
657 pine.lower_tf_input_buffer.push_back(input_bar);
660 int chunk_size = script_seconds / input_seconds;
661 if (chunk_size <= 0) {
662 pine.lower_tf_input_buffer.push_back(input_bar);
668 pine.lower_tf_input_buffer.reserve(
static_cast<std::size_t
>(chunk_size));
669 int64_t bucket_ms =
static_cast<int64_t
>(script_seconds) * 1000;
670 int64_t this_bucket = input_bar.timestamp / bucket_ms;
671 bool boundary_crossed =
false;
672 if (!pine.lower_tf_input_buffer.empty()) {
673 int64_t buffer_bucket =
674 pine.lower_tf_input_buffer.front().timestamp / bucket_ms;
675 if (this_bucket != buffer_bucket) {
676 boundary_crossed =
true;
685 auto dispatch_and_clear = [&]() {
686 int agg_ratio = pine.lower_tf_input_aggregation_ratio;
687 if (agg_ratio < 1) agg_ratio = 1;
688 int buf_len =
static_cast<int>(pine.lower_tf_input_buffer.size());
689 std::vector<Bar> ltf_bars;
690 ltf_bars.reserve(
static_cast<std::size_t
>(buf_len / agg_ratio + 1));
691 if (agg_ratio == 1) {
692 for (
const Bar& b : pine.lower_tf_input_buffer) {
693 ltf_bars.push_back(b);
696 for (
int i = 0; i + agg_ratio <= buf_len; i += agg_ratio) {
697 Bar acc = pine.lower_tf_input_buffer[
698 static_cast<std::size_t
>(i)];
699 double vol = acc.volume;
700 for (
int j = 1; j < agg_ratio; ++j) {
701 const Bar& nxt = pine.lower_tf_input_buffer[
702 static_cast<std::size_t
>(i + j)];
703 if (nxt.high > acc.high) acc.high = nxt.high;
704 if (nxt.low < acc.low) acc.low = nxt.low;
705 acc.close = nxt.close;
709 ltf_bars.push_back(acc);
712 pine.lower_tf_sub_bar_index = 0;
713 for (
const Bar& b : ltf_bars) {
715 state.current_bar =
b;
716 state.current_sub_bar_count = 1;
717 state.eval_complete_count++;
718 dispatch_security_eval(state, b,
true,
719 state.eval_complete_count - 1);
720 pine.lower_tf_sub_bar_index++;
722 pine.lower_tf_input_buffer.clear();
725 if (boundary_crossed) {
728 dispatch_and_clear();
731 pine.lower_tf_input_buffer.push_back(input_bar);
737 if (
static_cast<int>(pine.lower_tf_input_buffer.size()) >= chunk_size) {
738 dispatch_and_clear();
742 if (pine.lower_tf_emulation) {
743 std::vector<Bar> synthetic_bars =
744 synthesize_lower_tf_bars(input_bar, pine.lower_tf_ratio, pine.lower_tf_seconds);
745 if (synthetic_bars.empty()) {
746 throw coded<std::runtime_error>(
748 {{
"reason",
"lower_tf_synthesis_failed"}, {
"input_tf", security_input_tf_}},
749 "request.security lower TF emulation could not synthesize bars for requested "
750 + state.tf +
" from input timeframe " + security_input_tf_
758 pine.lower_tf_sub_bar_index = 0;
759 for (
const auto& synthetic_bar : synthetic_bars) {
761 state.current_bar = synthetic_bar;
762 state.current_sub_bar_count = 1;
763 state.eval_complete_count++;
764 dispatch_security_eval(state, synthetic_bar,
true,
765 state.eval_complete_count - 1);
766 pine.lower_tf_sub_bar_index++;
771 if (historical_security_lookahead_projection_active_
772 && !pine.historical_projections.empty()) {
781 const int64_t input_ms = input_bar.timestamp;
782 while (pine.historical_projection_cursor + 1
783 < pine.historical_projections.size()
784 && pine.historical_projections[
785 pine.historical_projection_cursor + 1]
786 .first_child_ms <= input_ms) {
787 ++pine.historical_projection_cursor;
788 pine.historical_projection_dispatched =
false;
790 const auto& projection = pine.historical_projections[
791 pine.historical_projection_cursor];
793 if (pine.historical_projection_dispatched
794 || input_ms < projection.first_child_ms) {
800 pine.historical_projection_dispatched =
true;
802 Bar projected_bar = projection.bar;
812 substitute_native_security_bar(state, projected_bar,
813 projection.is_complete);
814 if (pine.heikinashi) {
815 apply_ha(projected_bar, projection.is_complete);
817 state.current_bar = projected_bar;
821 state.current_sub_bar_count = 1;
822 if (projection.is_complete) {
823 state.eval_complete_count++;
825 state.eval_partial_count++;
827 dispatch_security_eval(state, projected_bar, projection.is_complete,
828 projection.is_complete
829 ? state.eval_complete_count - 1
830 : state.eval_complete_count);
839 AggregatedBar ab = state.aggregator.feed(input_bar, security_next_input_ms_,
840 security_calling_close_ms_);
846 if (!ab.is_complete && minute_day && !pine.lookahead_on
847 && state.aggregator.has_pending_partial()) {
850 input_bar.timestamp, syminfo_.timezone, syminfo_.session,
852 const bool next_session = state.aggregator.early_close_completes()
853 && security_next_input_ms_ > 0
858 input_bar.timestamp, syminfo_.timezone, syminfo_.session,
860 const bool reaches_close = input_seconds > 0
861 && input_bar.timestamp +
static_cast<int64_t
>(input_seconds) * 1000
863 if (next_session || reaches_close) {
864 ab = state.aggregator.complete_pending_partial();
868 state.current_sub_bar_count = ab.sub_bar_count;
869 if (ab.is_complete) {
874 substitute_native_security_bar(state, ab.bar);
875 if (pine.heikinashi) apply_ha(ab.bar,
true);
876 state.current_bar = ab.bar;
877 state.eval_complete_count++;
893 if (pine.publish_gate_tf_seconds > 0 && script_tf_seconds_ > 0) {
900 publish = calling_bar_complete;
930 const bool boundary_emission = pine.lookahead_on
931 && state.aggregator.is_active()
932 && ab.bar.timestamp != state.aggregator.current().timestamp;
933 if (boundary_emission && state.current_sub_bar_count < 2) {
934 state.current_sub_bar_count = 2;
936 pine.last_published_label = ab.bar.timestamp;
937 dispatch_security_eval(state, ab.bar, publish,
938 state.eval_complete_count - 1);
939 if (boundary_emission) {
940 Bar fresh = state.aggregator.current();
941 if (pine.heikinashi) apply_ha(fresh,
false);
942 state.current_bar = fresh;
943 state.current_sub_bar_count = 1;
944 state.eval_partial_count++;
945 const bool peek_publish = pine.publish_gate_tf_seconds > 0
946 && calling_bar_complete;
947 dispatch_security_eval(state, fresh, peek_publish,
948 state.eval_complete_count);
950 }
else if (pine.lookahead_on) {
951 if (pine.heikinashi) apply_ha(ab.bar,
false);
952 state.current_bar = ab.bar;
953 state.eval_partial_count++;
957 const bool publish = pine.publish_gate_tf_seconds > 0
958 && calling_bar_complete;
960 dispatch_security_eval(state, ab.bar, publish,
961 state.eval_complete_count);
963 state.current_bar = ab.bar;
965 clear_security(state.sec_id);
984 if (calling_bar_complete && pine.calling_close_completes_partial
985 && state.aggregator.has_pending_partial()) {
986 AggregatedBar tail = state.aggregator.complete_pending_partial();
987 if (tail.is_complete) {
988 state.current_sub_bar_count = tail.sub_bar_count;
989 substitute_native_security_bar(state, tail.bar);
990 if (pine.heikinashi) apply_ha(tail.bar,
true);
991 state.current_bar = tail.bar;
992 state.eval_complete_count++;
993 pine.last_published_label = tail.bar.timestamp;
994 dispatch_security_eval(state, tail.bar,
true,
995 state.eval_complete_count - 1);
1018std::string canonical_symbol_timeframe(
const std::string& tf) {
1019 if (tf.size() == 1 && (tf[0] ==
'D' || tf[0] ==
'W' || tf[0] ==
'M' || tf[0] ==
'S'))
1025std::string symbol_ticker(
const std::string& key) {
1026 const auto colon =
key.rfind(
':');
1027 return colon == std::string::npos ?
key :
key.substr(colon + 1);
1030bool parse_fact_number(
const std::string& text,
double& value) {
1031 if (
text.empty())
return false;
1032 std::size_t
used = 0;
1034 value = std::stod(text, &used);
1043constexpr const char* kRunInProgress =
1044 "a run is in progress; request data is installed before a run begins";
1046bool run_in_progress(
const NativeStrategyHost& host) {
1047 return host.native_state().kind == NativeLifecycleKind::Running;
1053 int sec_id,
const std::string&
symbol,
const std::string& requested_tf,
1054 const std::string& input_tf,
bool lookahead_on,
bool gaps_on,
1055 bool ignore_invalid_symbol) {
1059 ForeignSecuritySite site;
1061 site.requested_tf = requested_tf;
1062 site.lookahead = lookahead_on;
1063 site.gaps = gaps_on;
1064 site.ignore_invalid = ignore_invalid_symbol;
1068std::int64_t source::PineStrategyHost::find_symbol_feed(
const std::string& key,
1069 const std::string& tf)
const {
1070 for (std::size_t i = 0; i < symbol_feeds_.size(); ++i) {
1071 if (symbol_feeds_[i].instrument == key && symbol_feeds_[i].tf == tf)
1072 return static_cast<std::int64_t
>(i);
1081const NativeInstrumentFeed& source::PineStrategyHost::installed_symbol_feed(
1082 std::size_t i)
const {
1083 if (!symbol_feeds_lent_)
return symbol_feeds_[i];
1084 const NativeRunSpec* spec = native_state().spec;
1085 if (spec ==
nullptr || i >= spec->instrument_feeds.size())
1086 throw coded<std::logic_error>(
1088 "request data: the run spec no longer holds the symbol feeds");
1089 return spec->instrument_feeds[i];
1092void source::PineStrategyHost::restore_symbol_feeds() {
1093 if (!symbol_feeds_lent_)
return;
1094 const NativeRunSpec* spec = native_state().spec;
1095 if (spec ==
nullptr || spec->instrument_feeds.size() != symbol_feeds_.size())
1096 throw coded<std::logic_error>(
1098 "request data: the run spec no longer holds the symbol feeds");
1101 for (std::size_t i = 0; i < symbol_feeds_.size(); ++i) {
1102 const NativeInstrumentFeed& held = spec->instrument_feeds[i];
1103 const NativeInstrumentFeed& feed = symbol_feeds_[i];
1104 bool same = held.instrument == feed.instrument && held.tf == feed.tf
1105 && held.columns.size() == feed.columns.size();
1106 for (std::size_t c = 0; same && c < feed.columns.size(); ++c)
1107 same = held.columns[c].name == feed.columns[c].name;
1109 throw coded<std::logic_error>(
1111 "request data: the run spec holds other symbol feeds");
1113 for (std::size_t i = 0; i < symbol_feeds_.size(); ++i) {
1114 const NativeInstrumentFeed& held = spec->instrument_feeds[i];
1115 NativeInstrumentFeed& feed = symbol_feeds_[i];
1116 const auto views = NativeExecutionConsumer::bound(*this).bar_views();
1117 const auto* slot = views.instrument_feed_count == 0
1118 ? nullptr : views.instrument_feeds[i];
1119 if (slot ==
nullptr) {
1120 feed.bars = held.bars;
1121 feed.close_ms = held.close_ms;
1123 for (std::size_t c = 0; c < feed.columns.size(); ++c)
1124 feed.columns[c].values = held.columns[c].values;
1126 symbol_feeds_lent_ =
false;
1131std::string source::PineStrategyHost::restore_symbol_feeds_reason() {
1133 restore_symbol_feeds();
1134 }
catch (
const std::exception& error) {
1135 return std::string(
"request data could not be restored: ") + error.what();
1140void source::PineStrategyHost::prepare_foreign_security_sites(
1141 std::vector<NativeTimeframeSubscription>& declared) {
1142 foreign_security_series_.assign(declared.size(), -1);
1143 if (foreign_security_sites_.empty())
return;
1148 const int ratio = input_tf_.empty() || script_tf_.empty() ? 0
1150 if (input_tf_.empty() || ratio > 1 || ratio == -1) {
1152 if (!input_tf_.empty()) aggregated.emplace_back(
"input_tf", input_tf_);
1153 if (!script_tf_.empty()) aggregated.emplace_back(
"script_tf", script_tf_);
1154 throw coded<std::runtime_error>(
1156 "request.security of another symbol needs the chart's own bars as input; input '"
1157 + input_tf_ +
"' aggregated to chart '" + script_tf_ +
"' is not supported");
1159 for (
auto& entry : foreign_security_sites_) {
1160 const int sec_id = entry.first;
1161 ForeignSecuritySite& site = entry.second;
1162 site.tf = canonical_symbol_timeframe(site.requested_tf.empty() ? script_tf_
1163 : site.requested_tf);
1164 site.invalid =
false;
1165 site.subscription = -1;
1169 site.last_input = -1;
1171 site.eval = SecurityEvalState{};
1172 site.eval.sec_id = sec_id;
1173 site.eval.tf = site.tf;
1174 const auto facts = symbol_facts_.find(site.symbol);
1176 if (known !=
nullptr && known->valid && !*known->valid) {
1179 if (!site.ignore_invalid) {
1181 "request.security: symbol '" + site.symbol
1184 site.invalid =
true;
1187 const std::int64_t feed = find_symbol_feed(site.symbol, site.tf);
1192 if (site.requested_tf.empty()) missing.emplace_back(
"timeframe", site.tf);
1194 "request.security: no feed is installed for symbol '"
1195 + site.symbol +
"' at timeframe '" + site.tf
1198 site.feed =
static_cast<std::size_t
>(feed);
1204 info.ticker = symbol_ticker(site.symbol);
1205 info.tickerid = site.symbol;
1206 info.currency.clear();
1207 info.basecurrency.clear();
1209 info.timezone.clear();
1210 info.session.clear();
1211 info.volumetype.clear();
1212 info.description.clear();
1213 info.mintick = std::numeric_limits<double>::quiet_NaN();
1214 info.pointvalue = std::numeric_limits<double>::quiet_NaN();
1215 info.qty_step = 0.0;
1216 if (known !=
nullptr) {
1217 info.type = known->type;
1218 info.timezone = known->timezone;
1219 info.session = known->session;
1220 info.currency = known->currency;
1221 info.mintick = known->mintick;
1223 site.syminfo = std::move(info);
1224 NativeTimeframeSubscription series;
1225 series.tf = site.tf;
1226 series.source = NativeSeriesSource::InstrumentFeed;
1227 series.instrument = site.symbol;
1228 series.lookahead = site.lookahead;
1229 series.gaps = site.gaps;
1230 site.subscription =
static_cast<std::int64_t
>(declared.size());
1231 declared.push_back(std::move(series));
1232 foreign_security_series_.push_back(sec_id);
1241 if (sec_id < 0)
return;
1244 ForeignSecuritySite& site = found->second;
1249 const std::int64_t index = site.handed;
1250 site.handed = index + 1;
1251 if (index < site.delivered)
return;
1255void source::PineStrategyHost::run_foreign_site_bar(ForeignSecuritySite& site,
const Bar& bar,
1256 std::int64_t close_ms) {
1257 const std::int64_t index = site.delivered;
1258 site.delivered = index + 1;
1259 site.close_ms = close_ms;
1260 site.last_input = foreign_input_index_;
1263 struct ContextScope {
1265 ForeignSecuritySite& site;
1266 const ForeignSecuritySite* previous_context;
1267 double previous_mintick;
1269 : host(h), site(s), previous_context(h.foreign_context_),
1270 previous_mintick(h.syminfo_mintick_) {
1271 std::swap(host.syminfo_, site.syminfo);
1272 host.syminfo_mintick_ = host.syminfo_.mintick;
1273 host.foreign_context_ = &site;
1276 host.foreign_context_ = previous_context;
1277 host.syminfo_mintick_ = previous_mintick;
1278 std::swap(host.syminfo_, site.syminfo);
1280 } scope(*
this, site);
1281 dispatch_security_eval(site.eval, bar,
true, index);
1303void source::PineStrategyHost::read_ahead_foreign_security_sites(
const Bar& chart_bar) {
1305 const std::int64_t chart_close = chart_bar_close_ms(chart_bar.timestamp);
1306 for (
auto& entry : foreign_security_sites_) {
1307 ForeignSecuritySite& site = entry.second;
1308 if (site.invalid || site.subscription < 0 || site.feed >= symbol_feeds_.size())
continue;
1310 :
is_na(chart_close)) {
1313 const auto views = NativeExecutionConsumer::bound(*this).bar_views();
1314 const auto* slot = views.instrument_feed_count == 0
1315 ? nullptr : views.instrument_feeds[site.feed];
1317 while (
static_cast<std::size_t
>(site.delivered) < feed.bars.size) {
1318 const auto at =
static_cast<std::size_t
>(site.delivered);
1319 const bool visible = site.lookahead ? feed.bars.data[at].timestamp <= chart_bar.timestamp
1320 : feed.close_ms[at] <= chart_close;
1321 if (!visible)
break;
1322 run_foreign_site_bar(site, feed.bars.data[at], feed.close_ms[at]);
1327void source::PineStrategyHost::clear_gapped_foreign_security_sites() {
1328 for (
const auto& entry : foreign_security_sites_) {
1329 const ForeignSecuritySite& site = entry.second;
1330 if (!site.gaps || site.subscription < 0)
continue;
1331 if (site.last_input != foreign_input_index_) clear_security(entry.first);
1336 const std::string& name)
const {
1339 const ForeignSecuritySite& site = found->second;
1340 if (site.subscription < 0 || site.delivered <= 0 || site.feed >=
symbol_feeds_.size())
1341 return std::numeric_limits<double>::quiet_NaN();
1343 const auto at =
static_cast<std::size_t
>(site.delivered - 1);
1344 for (
const auto& column : feed.
columns) {
1345 if (column.name != name)
continue;
1346 return at < column.values.size() ? column.values[at]
1347 : std::numeric_limits<double>::quiet_NaN();
1349 return std::numeric_limits<double>::quiet_NaN();
1356 "request data: no recorded series is installed for key '"
1360 std::size_t first = 0, last = series.
rows.size();
1361 while (first < last) {
1362 const auto middle = first + (last - first) / 2;
1363 if (series.
rows[middle].open_ms < current_bar_.timestamp) first = middle + 1;
1366 if (first == series.
rows.size())
1367 return std::numeric_limits<double>::quiet_NaN();
1369 return row.
open_ms == current_bar_.timestamp
1370 ? row.
value : std::numeric_limits<double>::quiet_NaN();
1377 const Bar* bars,
const int64_t* close_ms,
1379 guard_native_mutation(
"set_symbol_feed");
1380 const auto refuse = [
this](
const char* reason,
const std::string& why,
1381 const char* field =
nullptr) {
1383 if (field) args.emplace_back(
"field", field);
1388 if (run_in_progress(*
this))
return refuse(
"run_in_progress", kRunInProgress);
1389 if (key.empty())
return refuse(
"empty_key",
"empty symbol key");
1390 const std::string tf = canonical_symbol_timeframe(
timeframe);
1392 return refuse(
"timeframe_unparsable",
"timeframe '" +
timeframe +
"' does not parse");
1394 if (n < 0 || (n > 0 && (bars ==
nullptr || close_ms ==
nullptr)))
1395 return refuse(
"invalid_bars",
"invalid bar array");
1396 const std::int64_t existing = find_symbol_feed(key, tf);
1400 std::vector<NativeInstrumentFeed> judged_feeds(1);
1405 feed.
bars.assign(bars, bars + n);
1406 feed.
close_ms.assign(close_ms, close_ms + n);
1412 return refuse(
"kernel_refused",
"feed refused (NativeRunSpecError "
1413 + std::to_string(
static_cast<int>(judged.error)) +
", field "
1414 + std::to_string(
static_cast<int>(judged.field)) +
")",
1419 if (
const std::string why = restore_symbol_feeds_reason(); !why.empty())
1420 return refuse(
"restore_failed", why);
1421 if (existing >= 0) {
1422 const std::size_t at =
static_cast<std::size_t
>(existing);
1431 refresh_symbol_data_digest();
1438 const std::string& name,
1439 const double* values,
int n) {
1440 guard_native_mutation(
"set_symbol_feed_column");
1441 const auto refuse = [
this](
const char* reason,
const std::string& why,
1442 const char* field =
nullptr) {
1444 if (field) args.emplace_back(
"field", field);
1449 if (run_in_progress(*
this))
return refuse(
"run_in_progress", kRunInProgress);
1450 const std::int64_t found = find_symbol_feed(key, canonical_symbol_timeframe(
timeframe));
1452 return refuse(
"no_feed",
"no feed is installed for symbol '" + key +
"' at timeframe '"
1455 if (name.empty())
return refuse(
"empty_column",
"empty column name");
1458 if (
const std::string why = restore_symbol_feeds_reason(); !why.empty())
1459 return refuse(
"restore_failed", why);
1462 const std::size_t feed_count =
static_cast<std::size_t
>(found) <
symbol_feed_views_.size()
1465 if (n < 0 ||
static_cast<std::size_t
>(n) != feed_count || (n > 0 && values ==
nullptr))
1466 return refuse(
"column_length",
"column '" + name +
"' has " + std::to_string(n)
1467 +
" values for " + std::to_string(feed_count) +
" bars");
1476 return refuse(
"column_refused",
"column '" + name +
"' refused (NativeRunSpecError "
1477 + std::to_string(
static_cast<int>(judged.error)) +
", field "
1478 + std::to_string(
static_cast<int>(judged.field)) +
")",
1482 : std::vector<double>{}};
1483 bool replaced =
false;
1484 for (
auto& existing : feed.
columns) {
1485 if (existing.name != name)
continue;
1486 existing = std::move(column);
1490 if (!replaced) feed.
columns.push_back(std::move(column));
1491 refresh_symbol_data_digest();
1497 const std::string& field,
1498 const std::string& value) {
1499 guard_native_mutation(
"set_symbol_facts");
1500 const auto refuse = [
this](
const char* reason,
const std::string& why) {
1505 if (run_in_progress(*
this))
return refuse(
"run_in_progress", kRunInProgress);
1506 if (key.empty())
return refuse(
"empty_key",
"empty symbol key");
1508 if (field ==
"canonical") {
1510 }
else if (field ==
"valid") {
1511 if (value ==
"true") candidate.
valid =
true;
1512 else if (value ==
"false") candidate.
valid =
false;
1513 else return refuse(
"valid_not_boolean",
1514 "valid must be \"true\" or \"false\", got '" + value +
"'");
1515 }
else if (field ==
"type") {
1516 candidate.
type = value;
1517 }
else if (field ==
"timezone") {
1519 }
else if (field ==
"session") {
1521 }
else if (field ==
"currency") {
1523 }
else if (field ==
"mintick") {
1524 double parsed = 0.0;
1525 if (!parse_fact_number(value, parsed) || !std::isfinite(parsed) || parsed <= 0.0)
1526 return refuse(
"mintick_invalid",
1527 "mintick must be a positive decimal, got '" + value +
"'");
1530 return refuse(
"unknown_field",
"unknown field '" + field +
"'");
1534 if (
const std::string why = restore_symbol_feeds_reason(); !why.empty())
1535 return refuse(
"restore_failed", why);
1537 refresh_symbol_data_digest();
1543 const int64_t* chart_open_ms,
1544 const double* values,
int n) {
1545 return set_recorded_series_impl(key, chart_open_ms, values, n, std::nullopt);
1549 const int64_t* chart_open_ms,
1550 const double* values,
int n,
1551 std::size_t cache_capacity_bytes) {
1552 return set_recorded_series_impl(key, chart_open_ms, values, n, cache_capacity_bytes);
1555bool source::PineStrategyHost::set_recorded_series_impl(
1556 const std::string& key,
const int64_t* chart_open_ms,
const double* values,
int n,
1557 std::optional<std::size_t> cache_capacity_bytes) {
1558 guard_native_mutation(
"set_recorded_series");
1559 const auto refuse = [
this](
const char* reason,
const std::string& why) {
1564 if (run_in_progress(*
this))
return refuse(
"run_in_progress", kRunInProgress);
1565 if (
key.empty())
return refuse(
"empty_key",
"empty key");
1566 if (n < 0 || (n > 0 && (chart_open_ms ==
nullptr || values ==
nullptr)))
1567 return refuse(
"invalid_arrays",
"invalid arrays");
1568 for (
int i = 1; i < n; ++i) {
1569 if (chart_open_ms[i] <= chart_open_ms[i - 1])
1570 return refuse(
"times_not_increasing",
1571 "chart open times must be strictly increasing (row "
1572 + std::to_string(i) +
")");
1576 pineforge::detail::FeedProjectionBuilder<RecordedSeriesRow> builder(report_scratch_directory(),
true);
1577 for (
int i = 0; i < n; ++i)
1579 RecordedSeries series{builder.finish(cache_capacity_bytes.value_or(
1583 if (
const std::string why = restore_symbol_feeds_reason(); !why.empty())
1584 return refuse(
"restore_failed", why);
1585 const auto digest = calculate_symbol_data_digest(&key, &series);
1586 recorded_series_[
key] = std::move(series);
1587 symbol_data_digest_ = digest;
1594 return set_recorded_series_reader_impl(key, std::move(reader), std::nullopt);
1599 std::size_t cache_capacity_bytes) {
1600 return set_recorded_series_reader_impl(key, std::move(reader), cache_capacity_bytes);
1603bool source::PineStrategyHost::set_recorded_series_reader_impl(
1605 std::optional<std::size_t> cache_capacity_bytes) {
1606 guard_native_mutation(
"set_recorded_series_reader");
1607 const auto refuse = [
this](
const char* reason,
const std::string& why) {
1612 if (run_in_progress(*
this))
return refuse(
"run_in_progress", kRunInProgress);
1613 if (
key.empty())
return refuse(
"empty_key",
"empty key");
1614 if (!reader)
return refuse(
"invalid_arrays",
"missing reader");
1615 RecordedSeries series{pineforge::detail::FeedProjection<RecordedSeriesRow>(
1616 std::move(reader), cache_capacity_bytes.value_or(
1618 std::int64_t previous = 0;
1619 for (std::size_t i = 0; i < series.rows.size(); ++i) {
1620 const auto open = series.rows[i].open_ms;
1621 if (i && open <= previous)
1622 return refuse(
"times_not_increasing",
1623 "chart open times must be strictly increasing (row "
1624 + std::to_string(i) +
")");
1627 if (
const std::string why = restore_symbol_feeds_reason(); !why.empty())
1628 return refuse(
"restore_failed", why);
1629 const auto digest = calculate_symbol_data_digest(&key, &series);
1630 recorded_series_[
key] = std::move(series);
1631 symbol_data_digest_ = digest;
1639void source::PineStrategyHost::refresh_symbol_data_digest() {
1640 symbol_data_digest_ = calculate_symbol_data_digest();
1643std::uint64_t source::PineStrategyHost::calculate_symbol_data_digest(
1644 const std::string* replacement_key,
const RecordedSeries* replacement)
const {
1645 BrokerStateHashSink f;
1646 f.u(symbol_feeds_.size());
1647 for (
const auto& feed : symbol_feeds_) {
1648 f.s(feed.instrument);
1651 const std::size_t count = (&feed - symbol_feeds_.data()) < symbol_feed_views_.size()
1652 && symbol_feed_views_[
static_cast<std::size_t
>(&feed - symbol_feeds_.data())].present
1653 ? symbol_feed_views_[
static_cast<std::size_t
>(&feed - symbol_feeds_.data())].size
1656 f.u(feed.columns.size());
1657 for (
const auto& column : feed.columns) f.s(column.name);
1659 f.u(symbol_facts_.size());
1660 for (
const auto& [key, facts] : symbol_facts_) {
1662 f.s(
facts.canonical);
1663 f.b(
facts.valid.has_value());
1666 f.s(
facts.timezone);
1668 f.s(
facts.currency);
1671 const bool inserting = replacement_key && recorded_series_.find(*replacement_key)
1672 == recorded_series_.end();
1673 f.u(recorded_series_.size() + (inserting ? 1 : 0));
1674 const auto fold_series = [&f](
const std::string&
key,
const RecordedSeries& series) {
1676 f.u(series.rows.size());
1677 for (std::size_t i = 0; i < series.rows.size(); ++i) {
1685 bool emitted =
false;
1686 for (
const auto& [key, series] : recorded_series_) {
1687 if (replacement_key && !emitted && *replacement_key <= key) {
1688 fold_series(*replacement_key, *replacement);
1691 if (!replacement_key || key != *replacement_key) fold_series(key, series);
1693 if (replacement_key && !emitted) fold_series(*replacement_key, *replacement);
1699namespace pineforge {
1702 const std::string& key,
const std::string&
timeframe,
1703 const Bar* bars,
const int64_t* close_ms,
int n) {
1704 guard_native_mutation(
"set_symbol_feed_borrowed");
1705 const auto refuse = [
this](
const char* reason,
const std::string& why,
1706 const char* field =
nullptr) {
1708 if (field) args.emplace_back(
"field", field);
1713 if (run_in_progress(*
this))
return refuse(
"run_in_progress", kRunInProgress);
1714 if (key.empty())
return refuse(
"empty_key",
"empty symbol key");
1715 const std::string tf = canonical_symbol_timeframe(
timeframe);
1717 return refuse(
"timeframe_unparsable",
"timeframe '" +
timeframe +
"' does not parse");
1718 if (n < 0 || (n > 0 && (bars ==
nullptr || close_ms ==
nullptr)))
1719 return refuse(
"invalid_bars",
"invalid bar array");
1721 const std::size_t count =
static_cast<std::size_t
>(n);
1725 std::vector<NativeInstrumentFeed> judged_feeds(1);
1726 judged_feeds.front().instrument = key;
1727 judged_feeds.front().tf = tf;
1730 return refuse(
"kernel_refused",
"feed refused (NativeRunSpecError "
1731 + std::to_string(
static_cast<int>(judged.error)) +
", field "
1732 + std::to_string(
static_cast<int>(judged.field)) +
")",
1735 if (
const std::string why = restore_symbol_feeds_reason(); !why.empty())
1736 return refuse(
"restore_failed", why);
1738 const std::int64_t existing = find_symbol_feed(key, tf);
1740 if (existing >= 0) {
1741 const std::size_t at =
static_cast<std::size_t
>(existing);
1758 refresh_symbol_data_digest();
Base with a code: throw coded<std::invalid_argument>(code, {args}, text).
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)
std::map< std::string, RecordedSeries > recorded_series_
bool set_symbol_facts(const std::string &key, const std::string &field, const std::string &value) override
bool security_history_publication_replay_
bool set_recorded_series_reader(const std::string &key, std::shared_ptr< const FeedReader< RecordedSeriesRow > > reader)
bool set_symbol_feed_borrowed(const std::string &key, const std::string &timeframe, const Bar *bars, const int64_t *close_ms, int n) override
std::vector< BorrowedFeedSlot > symbol_feed_views_
bool set_symbol_feed_column(const std::string &key, const std::string &timeframe, const std::string &name, const double *values, int n) override
int security_lower_tf_sub_bar_index(int sec_id) const
bool set_symbol_feed(const std::string &key, const std::string &timeframe, const Bar *bars, const int64_t *close_ms, int n) override
std::map< std::string, SymbolFacts > symbol_facts_
double security_column_value(int sec_id, const std::string &name) 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::vector< NativeInstrumentFeed > symbol_feeds_
std::vector< int > foreign_security_series_
bool set_recorded_series(const std::string &key, const int64_t *chart_open_ms, const double *values, int n) override
std::map< int, PineSecurityEvalState > pine_security_states_
double recorded_series_value(const std::string &key) const
bool source_prepare_failed_
void on_native_timeframe_bar(const Bar &, const NativeTimeframeBarContext &) final
std::map< int, ForeignSecuritySite > foreign_security_sites_
constexpr std::size_t kFeedProjectionCacheBytes
NativeRunSpecValidation validate_native_instrument_feeds(const std::vector< NativeInstrumentFeed > &feeds, const NativeBarViews &views) noexcept
The same feed-only validation over effective data, retaining all existing refusal ordering and typed ...
NativeInstrumentFeedReadView native_instrument_feed_view(const NativeInstrumentFeed &owned, const NativeInstrumentFeedView *borrowed) noexcept
Common accessor for effective feed data.
std::optional< Timeframe > parse_timeframe(std::string_view text)
const char * instrument_feed_field_name(NativeRunSpecField field) 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.
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.
int64_t session_period_close_ms(int64_t ms, const std::string &tz, const std::string &session, CalendarPeriod period)
Exclusive close (Unix ms) of the symbol's D/W/M bar that contains ms: DAY -> session-day open + sessi...
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
@ other_symbol_feed_missing
@ recorded_request_refused
@ recorded_request_missing
@ request_timeframe_invalid
@ request_timeframe_unsupported
bool is_na(const Line &h)
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)
std::vector< RunFailureArg > RunFailureArgs
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...
One completed higher-timeframe bucket of a declared subscription (NativeRunSpec::subscriptions),...
native_calendar::NativeInterval interval
Explicit read context for feeds and lower paths.
Borrowed instrument feed.
int64_t last_traded_close_ms
One named column an instrument feed carries beside its OHLCV: one value per bar, in bar order.
Another instrument's own bars of one timeframe, installed as data.
std::vector< std::int64_t > close_ms
std::vector< NativeInstrumentColumn > columns
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
pineforge::detail::FeedProjection< RecordedSeriesRow > rows
std::optional< bool > valid