15#include "../engine_internal.hpp"
16#include "../native_execution_consumer.hpp"
42void ensure_supported_lower_tf_emulation_flags(
bool lookahead_on,
bool gaps_on) {
43 if (lookahead_on || gaps_on) {
46 "request.security lower TF emulation only supports lookahead=barmerge.lookahead_off and gaps=barmerge.gaps_off"
56 int sec_id,
const std::string& requested_tf,
const std::string& input_tf,
57 bool lookahead_on,
bool gaps_on,
bool heikinashi) {
58 const std::size_t before = security_eval_states_.size();
63 BacktestEngine::register_security_eval(sec_id, requested_tf, input_tf);
64 if (security_eval_states_.size() <= before)
return;
65 SecurityEvalState& state = security_eval_states_.back();
72 const std::string& evaluator_input_tf =
73 security_input_tf_.empty() ? input_tf : security_input_tf_;
74 if (evaluator_input_tf.empty())
return;
76 int lower_seconds = 0;
77 if (supports_lower_tf_emulation(evaluator_input_tf, requested_tf,
78 &lower_ratio, &lower_seconds)) {
104 const std::string& requested_tf,
105 const std::string& input_tf
107 auto before = security_eval_states_.size();
109 if (security_eval_states_.size() > before) {
110 pine_security_state(sec_id).lower_tf_array_requested =
true;
119 for (
const auto& state : security_eval_states_) {
120 if (state.sec_id != sec_id) {
124 const bool lookahead_on =
126 return !lookahead_on || state.current_sub_bar_count <= 1;
133 for (
const auto& state : security_eval_states_) {
134 if (state.sec_id == sec_id) {
135 return pine_security_state(sec_id).lower_tf_sub_bar_index;
142void source::PineStrategyHost::prune_pine_security_states() {
143 for (
auto it = pine_security_states_.begin(); it != pine_security_states_.end();) {
144 bool registered =
false;
145 for (
const auto& state : security_eval_states_) {
146 if (state.sec_id == it->first) {
151 it = registered ? std::next(it) : pine_security_states_.erase(it);
169 if (input_tf.empty()) {
170 if (!security_eval_states_.empty()) {
173 "request.security cannot infer input timeframe from available input bars; pass input_tf explicitly"
185 int script_seconds = script_tf_seconds_;
186 for (
auto& state : security_eval_states_) {
201 if (state.tf.empty())
continue;
204 int lower_seconds = 0;
205 bool ltf_supported = supports_lower_tf_emulation(
206 input_tf, state.tf, &lower_ratio, &lower_seconds);
230 if (requested_seconds <= 0 && !is_calendar_month) {
232 ?
"request.security_lower_tf" :
"request.security";
235 std::string(api) +
": invalid timeframe literal '" + state.tf +
"'"
239 if (!is_calendar_month && requested_seconds < input_seconds) {
250 const bool intrabar_bars_supplied = input_seconds > 0
251 && (script_seconds == -1 || input_seconds < script_seconds);
252 if (intrabar_bars_supplied) {
258 {{
"reason",
"finer_than_input"}, {
"input_tf", input_tf}},
259 "request.security: requested timeframe '" + state.tf
260 +
"' is finer than input '" + input_tf
261 +
"'. Use request.security_lower_tf for sub-input timeframes."
265 if (input_seconds % requested_seconds != 0) {
268 {{
"reason",
"lower_tf_not_divisor_of_input"}, {
"input_tf", input_tf}},
269 "request.security_lower_tf: requested timeframe '" + state.tf
270 +
"' is not an integer divisor of input '" + input_tf
271 +
"' (ratio " + std::to_string(
272 static_cast<double>(input_seconds) / requested_seconds)
273 +
" is non-integer; chart bars cannot be evenly subdivided)"
282 "request.security_lower_tf: internal error - passed integer-ratio check but emulation support returned false (requested '"
283 + state.tf +
"', input '" + input_tf +
"')"
292 if (script_seconds <= 0) {
295 {{
"reason",
"lower_tf_script_unknown"}},
296 "request.security_lower_tf: script timeframe is unknown — cannot validate '"
297 + state.tf +
"' against script TF"
300 if (requested_seconds >= script_seconds) {
303 {{
"reason",
"lower_tf_not_finer_than_script"}, {
"script_tf", script_tf_}},
304 "request.security_lower_tf: requested timeframe '" + state.tf
305 +
"' must be finer than script timeframe '" + script_tf_
306 +
"'. Lower-TF API requires a strictly finer timeframe."
309 if (script_seconds % requested_seconds != 0) {
312 {{
"reason",
"lower_tf_not_divisor_of_script"}, {
"script_tf", script_tf_}},
313 "request.security_lower_tf: requested timeframe '" + state.tf
314 +
"' must evenly divide script timeframe '"
315 + script_tf_ +
"' (script_tf must be an integer multiple of requested TF)"
318 if (requested_seconds % input_seconds != 0) {
321 {{
"reason",
"lower_tf_not_multiple_of_input"}, {
"input_tf", input_tf}},
322 "request.security_lower_tf: requested timeframe '" + state.tf
323 +
"' is not an integer multiple of input '" + input_tf
324 +
"' (cannot aggregate raw input bars to requested TF)"
330 requested_seconds / input_seconds;
334 && script_seconds > 0
335 && requested_seconds < script_seconds
336 && script_seconds % requested_seconds == 0) {
362#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
370 if (aux_security_feed_enabled()) {
376#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
378 && aux_security_feed_enabled()
379 && script_seconds > 0
380 && requested_seconds < script_seconds) {
395void source::PineStrategyHost::publish_security_eval_state_at_calling_boundary(
396 SecurityEvalState& state) {
397 if (pine_security_state(state.sec_id).publish_gate_tf_seconds <= 0
398 || state.feed_count <= 0) {
405 explicit ReplayScope(
bool& flag)
406 : active(flag), previous(flag) { active =
true; }
407 ~ReplayScope() { active = previous; }
408 } replay_scope(security_history_publication_replay_);
416 dispatch_security_eval(state, state.current_bar,
true,
417 state.ta_bar_index >= 0 ? state.ta_bar_index
418 : state.eval_complete_count);
422bool source::PineStrategyHost::security_input_precedes_range_start(
423 const SecurityEvalState& state, int64_t input_ts)
const {
425 if (security_range_start_na_warmup_) {
438#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
461 if (aux_security_feed_enabled() && !pine.lower_tf_requested
462 && !pine.lower_tf_array_requested) {
466 &&
static_cast<int>(period) >
static_cast<int>(chart_period);
467 if (strictly_coarser) {
469 input_ts, syminfo_.timezone, syminfo_.session, period);
470 if (nominal_open < security_range_start_ms_
471 && aux_security_traded_between(nominal_open,
472 security_range_start_ms_)) {
478 return state.aggregator.bucket_open_ms(input_ts) < security_range_start_ms_;
480#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
488 if (!aux_security_feed_enabled() && native_security_feeds_.empty()
489 && security_first_chart_bar_ms_ > 0
490 && script_tf_seconds_ > 0 && script_tf_seconds_ < 86400
491 && (state.tf ==
"D" || state.tf ==
"1D")
492 && !pine.lower_tf_requested && !pine.lower_tf_array_requested
493 && (syminfo_.type ==
"forex" || syminfo_.type ==
"cfd")) {
499 stamp, syminfo_.timezone, syminfo_.session);
500 return trading_open < security_first_chart_bar_ms_;
519 if (security_first_chart_bar_ms_ <= 0 || !aux_security_feed_enabled()
520 || pine.lower_tf_requested || pine.lower_tf_array_requested
521 || script_tf_seconds_ <= 0) {
525 const bool coarser_or_chart = requested_seconds == -1
526 || requested_seconds >= script_tf_seconds_;
527 if (!coarser_or_chart) {
535 syminfo_.timezone, syminfo_.session);
536 if (nominal_open >= security_first_chart_bar_ms_) {
539 return aux_security_traded_between(nominal_open, security_first_chart_bar_ms_);
546#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
547bool source::PineStrategyHost::aux_security_traded_between(int64_t from_ms,
548 int64_t to_ms)
const {
549 const Bar* bars =
nullptr;
551 source_aux_security_input_view(bars, n);
552 if (bars ==
nullptr || n <= 0)
return false;
553 auto it = std::lower_bound(
554 bars, bars + n, from_ms,
555 [](
const Bar& bar, int64_t ts) {
return bar.timestamp < ts; });
556 return it != bars + n && it->timestamp < to_ms;
561void source::PineStrategyHost::pine_feed_security_eval_state(
562 SecurityEvalState& state,
const Bar& input_bar,
563 bool calling_bar_complete) {
579 if (security_input_precedes_range_start(state, input_bar.timestamp)) {
585 if (pine.no_loaded_bars)
return;
587 internal::AmbientEmaSeedingScope _na_warmup_scope(
588 NativeExecutionConsumer::bound(*this).pump_ambient(),
589 security_range_start_na_warmup_);
595 auto apply_ha = [&pine](
Bar&
b,
bool commit) {
596 double ha_close = (
b.open +
b.high +
b.low +
b.close) / 4.0;
597 double ha_open = pine.ha_seeded
598 ? (pine.ha_prev_open + pine.ha_prev_close) / 2.0
599 : (
b.open +
b.close) / 2.0;
600 double ha_high = std::max(
b.high, std::max(ha_open, ha_close));
601 double ha_low = std::min(
b.low, std::min(ha_open, ha_close));
607 pine.ha_prev_open = ha_open;
608 pine.ha_prev_close = ha_close;
609 pine.ha_seeded =
true;
613 if (pine.lower_tf_use_input) {
631 int script_seconds = script_tf_seconds_;
632 if (input_seconds <= 0 || script_seconds <= 0) {
635 pine.lower_tf_input_buffer.push_back(input_bar);
638 int chunk_size = script_seconds / input_seconds;
639 if (chunk_size <= 0) {
640 pine.lower_tf_input_buffer.push_back(input_bar);
646 pine.lower_tf_input_buffer.reserve(
static_cast<std::size_t
>(chunk_size));
647 int64_t bucket_ms =
static_cast<int64_t
>(script_seconds) * 1000;
648 int64_t this_bucket = input_bar.timestamp / bucket_ms;
649 bool boundary_crossed =
false;
650 if (!pine.lower_tf_input_buffer.empty()) {
651 int64_t buffer_bucket =
652 pine.lower_tf_input_buffer.front().timestamp / bucket_ms;
653 if (this_bucket != buffer_bucket) {
654 boundary_crossed =
true;
663 auto dispatch_and_clear = [&]() {
664 int agg_ratio = pine.lower_tf_input_aggregation_ratio;
665 if (agg_ratio < 1) agg_ratio = 1;
666 int buf_len =
static_cast<int>(pine.lower_tf_input_buffer.size());
667 std::vector<Bar> ltf_bars;
668 ltf_bars.reserve(
static_cast<std::size_t
>(buf_len / agg_ratio + 1));
669 if (agg_ratio == 1) {
670 for (
const Bar& b : pine.lower_tf_input_buffer) {
671 ltf_bars.push_back(b);
674 for (
int i = 0; i + agg_ratio <= buf_len; i += agg_ratio) {
675 Bar acc = pine.lower_tf_input_buffer[
676 static_cast<std::size_t
>(i)];
677 double vol = acc.volume;
678 for (
int j = 1; j < agg_ratio; ++j) {
679 const Bar& nxt = pine.lower_tf_input_buffer[
680 static_cast<std::size_t
>(i + j)];
681 if (nxt.high > acc.high) acc.high = nxt.high;
682 if (nxt.low < acc.low) acc.low = nxt.low;
683 acc.close = nxt.close;
687 ltf_bars.push_back(acc);
690 pine.lower_tf_sub_bar_index = 0;
691 for (
const Bar& b : ltf_bars) {
693 state.current_bar =
b;
694 state.current_sub_bar_count = 1;
695 state.eval_complete_count++;
696 dispatch_security_eval(state, b,
true,
697 state.eval_complete_count - 1);
698 pine.lower_tf_sub_bar_index++;
700 pine.lower_tf_input_buffer.clear();
703 if (boundary_crossed) {
706 dispatch_and_clear();
709 pine.lower_tf_input_buffer.push_back(input_bar);
715 if (
static_cast<int>(pine.lower_tf_input_buffer.size()) >= chunk_size) {
716 dispatch_and_clear();
720 if (pine.lower_tf_emulation) {
721 std::vector<Bar> synthetic_bars =
722 synthesize_lower_tf_bars(input_bar, pine.lower_tf_ratio, pine.lower_tf_seconds);
723 if (synthetic_bars.empty()) {
724 throw coded<std::runtime_error>(
726 {{
"reason",
"lower_tf_synthesis_failed"}, {
"input_tf", security_input_tf_}},
727 "request.security lower TF emulation could not synthesize bars for requested "
728 + state.tf +
" from input timeframe " + security_input_tf_
736 pine.lower_tf_sub_bar_index = 0;
737 for (
const auto& synthetic_bar : synthetic_bars) {
739 state.current_bar = synthetic_bar;
740 state.current_sub_bar_count = 1;
741 state.eval_complete_count++;
742 dispatch_security_eval(state, synthetic_bar,
true,
743 state.eval_complete_count - 1);
744 pine.lower_tf_sub_bar_index++;
749 if (historical_security_lookahead_projection_active_
750 && !pine.historical_projections.empty()) {
759 const int64_t input_ms = input_bar.timestamp;
760 while (pine.historical_projection_cursor + 1
761 < pine.historical_projections.size()
762 && pine.historical_projections[
763 pine.historical_projection_cursor + 1]
764 .first_child_ms <= input_ms) {
765 ++pine.historical_projection_cursor;
766 pine.historical_projection_dispatched =
false;
768 const auto& projection = pine.historical_projections[
769 pine.historical_projection_cursor];
771 if (pine.historical_projection_dispatched
772 || input_ms < projection.first_child_ms) {
778 pine.historical_projection_dispatched =
true;
780 Bar projected_bar = projection.bar;
790 substitute_native_security_bar(state, projected_bar,
791 projection.is_complete);
792 if (pine.heikinashi) {
793 apply_ha(projected_bar, projection.is_complete);
795 state.current_bar = projected_bar;
799 state.current_sub_bar_count = 1;
800 if (projection.is_complete) {
801 state.eval_complete_count++;
803 state.eval_partial_count++;
805 dispatch_security_eval(state, projected_bar, projection.is_complete,
806 projection.is_complete
807 ? state.eval_complete_count - 1
808 : state.eval_complete_count);
817 AggregatedBar ab = state.aggregator.feed(input_bar, security_next_input_ms_,
818 security_calling_close_ms_);
824 if (!ab.is_complete && minute_day && !pine.lookahead_on
825 && state.aggregator.has_pending_partial()) {
828 input_bar.timestamp, syminfo_.timezone, syminfo_.session,
830 const bool next_session = state.aggregator.early_close_completes()
831 && security_next_input_ms_ > 0
836 input_bar.timestamp, syminfo_.timezone, syminfo_.session,
838 const bool reaches_close = input_seconds > 0
839 && input_bar.timestamp +
static_cast<int64_t
>(input_seconds) * 1000
841 if (next_session || reaches_close) {
842 ab = state.aggregator.complete_pending_partial();
846 state.current_sub_bar_count = ab.sub_bar_count;
847 if (ab.is_complete) {
852 substitute_native_security_bar(state, ab.bar);
853 if (pine.heikinashi) apply_ha(ab.bar,
true);
854 state.current_bar = ab.bar;
855 state.eval_complete_count++;
871 if (pine.publish_gate_tf_seconds > 0 && script_tf_seconds_ > 0) {
878 publish = calling_bar_complete;
908 const bool boundary_emission = pine.lookahead_on
909 && state.aggregator.is_active()
910 && ab.bar.timestamp != state.aggregator.current().timestamp;
911 if (boundary_emission && state.current_sub_bar_count < 2) {
912 state.current_sub_bar_count = 2;
914 pine.last_published_label = ab.bar.timestamp;
915 dispatch_security_eval(state, ab.bar, publish,
916 state.eval_complete_count - 1);
917 if (boundary_emission) {
918 Bar fresh = state.aggregator.current();
919 if (pine.heikinashi) apply_ha(fresh,
false);
920 state.current_bar = fresh;
921 state.current_sub_bar_count = 1;
922 state.eval_partial_count++;
923 const bool peek_publish = pine.publish_gate_tf_seconds > 0
924 && calling_bar_complete;
925 dispatch_security_eval(state, fresh, peek_publish,
926 state.eval_complete_count);
928 }
else if (pine.lookahead_on) {
929 if (pine.heikinashi) apply_ha(ab.bar,
false);
930 state.current_bar = ab.bar;
931 state.eval_partial_count++;
935 const bool publish = pine.publish_gate_tf_seconds > 0
936 && calling_bar_complete;
938 dispatch_security_eval(state, ab.bar, publish,
939 state.eval_complete_count);
941 state.current_bar = ab.bar;
943 clear_security(state.sec_id);
962 if (calling_bar_complete && pine.calling_close_completes_partial
963 && state.aggregator.has_pending_partial()) {
964 AggregatedBar tail = state.aggregator.complete_pending_partial();
965 if (tail.is_complete) {
966 state.current_sub_bar_count = tail.sub_bar_count;
967 substitute_native_security_bar(state, tail.bar);
968 if (pine.heikinashi) apply_ha(tail.bar,
true);
969 state.current_bar = tail.bar;
970 state.eval_complete_count++;
971 pine.last_published_label = tail.bar.timestamp;
972 dispatch_security_eval(state, tail.bar,
true,
973 state.eval_complete_count - 1);
996std::string canonical_symbol_timeframe(
const std::string& tf) {
997 if (tf.size() == 1 && (tf[0] ==
'D' || tf[0] ==
'W' || tf[0] ==
'M' || tf[0] ==
'S'))
1003std::string symbol_ticker(
const std::string& key) {
1004 const auto colon =
key.rfind(
':');
1005 return colon == std::string::npos ?
key :
key.substr(colon + 1);
1008bool parse_fact_number(
const std::string& text,
double& value) {
1009 if (
text.empty())
return false;
1010 std::size_t
used = 0;
1012 value = std::stod(text, &used);
1021constexpr const char* kRunInProgress =
1022 "a run is in progress; request data is installed before a run begins";
1024bool run_in_progress(
const NativeStrategyHost& host) {
1025 return host.native_state().kind == NativeLifecycleKind::Running;
1031 int sec_id,
const std::string&
symbol,
const std::string& requested_tf,
1032 const std::string& input_tf,
bool lookahead_on,
bool gaps_on,
1033 bool ignore_invalid_symbol) {
1037 ForeignSecuritySite site;
1039 site.requested_tf = requested_tf;
1040 site.lookahead = lookahead_on;
1041 site.gaps = gaps_on;
1042 site.ignore_invalid = ignore_invalid_symbol;
1046std::int64_t source::PineStrategyHost::find_symbol_feed(
const std::string& key,
1047 const std::string& tf)
const {
1048 for (std::size_t i = 0; i < symbol_feeds_.size(); ++i) {
1049 if (symbol_feeds_[i].instrument == key && symbol_feeds_[i].tf == tf)
1050 return static_cast<std::int64_t
>(i);
1059const NativeInstrumentFeed& source::PineStrategyHost::installed_symbol_feed(
1060 std::size_t i)
const {
1061 if (!symbol_feeds_lent_)
return symbol_feeds_[i];
1062 const NativeRunSpec* spec = native_state().spec;
1063 if (spec ==
nullptr || i >= spec->instrument_feeds.size())
1064 throw coded<std::logic_error>(
1066 "request data: the run spec no longer holds the symbol feeds");
1067 return spec->instrument_feeds[i];
1070void source::PineStrategyHost::restore_symbol_feeds() {
1071 if (!symbol_feeds_lent_)
return;
1072 const NativeRunSpec* spec = native_state().spec;
1073 if (spec ==
nullptr || spec->instrument_feeds.size() != symbol_feeds_.size())
1074 throw coded<std::logic_error>(
1076 "request data: the run spec no longer holds the symbol feeds");
1079 for (std::size_t i = 0; i < symbol_feeds_.size(); ++i) {
1080 const NativeInstrumentFeed& held = spec->instrument_feeds[i];
1081 const NativeInstrumentFeed& feed = symbol_feeds_[i];
1082 bool same = held.instrument == feed.instrument && held.tf == feed.tf
1083 && held.columns.size() == feed.columns.size();
1084 for (std::size_t c = 0; same && c < feed.columns.size(); ++c)
1085 same = held.columns[c].name == feed.columns[c].name;
1087 throw coded<std::logic_error>(
1089 "request data: the run spec holds other symbol feeds");
1091 for (std::size_t i = 0; i < symbol_feeds_.size(); ++i) {
1092 const NativeInstrumentFeed& held = spec->instrument_feeds[i];
1093 NativeInstrumentFeed& feed = symbol_feeds_[i];
1094 const auto views = NativeExecutionConsumer::bound(*this).bar_views();
1095 const auto* slot = views.instrument_feed_count == 0
1096 ? nullptr : views.instrument_feeds[i];
1097 if (slot ==
nullptr) {
1098 feed.bars = held.bars;
1099 feed.close_ms = held.close_ms;
1101 for (std::size_t c = 0; c < feed.columns.size(); ++c)
1102 feed.columns[c].values = held.columns[c].values;
1104 symbol_feeds_lent_ =
false;
1109std::string source::PineStrategyHost::restore_symbol_feeds_reason() {
1111 restore_symbol_feeds();
1112 }
catch (
const std::exception& error) {
1113 return std::string(
"request data could not be restored: ") + error.what();
1118void source::PineStrategyHost::prepare_foreign_security_sites(
1119 std::vector<NativeTimeframeSubscription>& declared) {
1120 foreign_security_series_.assign(declared.size(), -1);
1121 if (foreign_security_sites_.empty())
return;
1126 const int ratio = input_tf_.empty() || script_tf_.empty() ? 0
1128 if (input_tf_.empty() || ratio > 1 || ratio == -1) {
1130 if (!input_tf_.empty()) aggregated.emplace_back(
"input_tf", input_tf_);
1131 if (!script_tf_.empty()) aggregated.emplace_back(
"script_tf", script_tf_);
1132 throw coded<std::runtime_error>(
1134 "request.security of another symbol needs the chart's own bars as input; input '"
1135 + input_tf_ +
"' aggregated to chart '" + script_tf_ +
"' is not supported");
1137 for (
auto& entry : foreign_security_sites_) {
1138 const int sec_id = entry.first;
1139 ForeignSecuritySite& site = entry.second;
1140 site.tf = canonical_symbol_timeframe(site.requested_tf.empty() ? script_tf_
1141 : site.requested_tf);
1142 site.invalid =
false;
1143 site.subscription = -1;
1147 site.last_input = -1;
1149 site.eval = SecurityEvalState{};
1150 site.eval.sec_id = sec_id;
1151 site.eval.tf = site.tf;
1152 const auto facts = symbol_facts_.find(site.symbol);
1154 if (known !=
nullptr && known->valid && !*known->valid) {
1157 if (!site.ignore_invalid) {
1159 "request.security: symbol '" + site.symbol
1162 site.invalid =
true;
1165 const std::int64_t feed = find_symbol_feed(site.symbol, site.tf);
1170 if (site.requested_tf.empty()) missing.emplace_back(
"timeframe", site.tf);
1172 "request.security: no feed is installed for symbol '"
1173 + site.symbol +
"' at timeframe '" + site.tf
1176 site.feed =
static_cast<std::size_t
>(feed);
1182 info.ticker = symbol_ticker(site.symbol);
1183 info.tickerid = site.symbol;
1184 info.currency.clear();
1185 info.basecurrency.clear();
1187 info.timezone.clear();
1188 info.session.clear();
1189 info.volumetype.clear();
1190 info.description.clear();
1191 info.mintick = std::numeric_limits<double>::quiet_NaN();
1192 info.pointvalue = std::numeric_limits<double>::quiet_NaN();
1193 info.qty_step = 0.0;
1194 if (known !=
nullptr) {
1195 info.type = known->type;
1196 info.timezone = known->timezone;
1197 info.session = known->session;
1198 info.currency = known->currency;
1199 info.mintick = known->mintick;
1201 site.syminfo = std::move(info);
1202 NativeTimeframeSubscription series;
1203 series.tf = site.tf;
1204 series.source = NativeSeriesSource::InstrumentFeed;
1205 series.instrument = site.symbol;
1206 series.lookahead = site.lookahead;
1207 series.gaps = site.gaps;
1208 site.subscription =
static_cast<std::int64_t
>(declared.size());
1209 declared.push_back(std::move(series));
1210 foreign_security_series_.push_back(sec_id);
1219 if (sec_id < 0)
return;
1222 ForeignSecuritySite& site = found->second;
1227 const std::int64_t index = site.handed;
1228 site.handed = index + 1;
1229 if (index < site.delivered)
return;
1233void source::PineStrategyHost::run_foreign_site_bar(ForeignSecuritySite& site,
const Bar& bar,
1234 std::int64_t close_ms) {
1235 const std::int64_t index = site.delivered;
1236 site.delivered = index + 1;
1237 site.close_ms = close_ms;
1238 site.last_input = foreign_input_index_;
1241 struct ContextScope {
1243 ForeignSecuritySite& site;
1244 const ForeignSecuritySite* previous_context;
1245 double previous_mintick;
1247 : host(h), site(s), previous_context(h.foreign_context_),
1248 previous_mintick(h.syminfo_mintick_) {
1249 std::swap(host.syminfo_, site.syminfo);
1250 host.syminfo_mintick_ = host.syminfo_.mintick;
1251 host.foreign_context_ = &site;
1254 host.foreign_context_ = previous_context;
1255 host.syminfo_mintick_ = previous_mintick;
1256 std::swap(host.syminfo_, site.syminfo);
1258 } scope(*
this, site);
1259 dispatch_security_eval(site.eval, bar,
true, index);
1281void source::PineStrategyHost::read_ahead_foreign_security_sites(
const Bar& chart_bar) {
1283 const std::int64_t chart_close = chart_bar_close_ms(chart_bar.timestamp);
1284 for (
auto& entry : foreign_security_sites_) {
1285 ForeignSecuritySite& site = entry.second;
1286 if (site.invalid || site.subscription < 0 || site.feed >= symbol_feeds_.size())
continue;
1288 :
is_na(chart_close)) {
1291 const auto views = NativeExecutionConsumer::bound(*this).bar_views();
1292 const auto* slot = views.instrument_feed_count == 0
1293 ? nullptr : views.instrument_feeds[site.feed];
1295 while (
static_cast<std::size_t
>(site.delivered) < feed.bars.size) {
1296 const auto at =
static_cast<std::size_t
>(site.delivered);
1297 const bool visible = site.lookahead ? feed.bars.data[at].timestamp <= chart_bar.timestamp
1298 : feed.close_ms[at] <= chart_close;
1299 if (!visible)
break;
1300 run_foreign_site_bar(site, feed.bars.data[at], feed.close_ms[at]);
1305void source::PineStrategyHost::clear_gapped_foreign_security_sites() {
1306 for (
const auto& entry : foreign_security_sites_) {
1307 const ForeignSecuritySite& site = entry.second;
1308 if (!site.gaps || site.subscription < 0)
continue;
1309 if (site.last_input != foreign_input_index_) clear_security(entry.first);
1314 const std::string& name)
const {
1317 const ForeignSecuritySite& site = found->second;
1318 if (site.subscription < 0 || site.delivered <= 0 || site.feed >=
symbol_feeds_.size())
1319 return std::numeric_limits<double>::quiet_NaN();
1321 const auto at =
static_cast<std::size_t
>(site.delivered - 1);
1322 for (
const auto& column : feed.
columns) {
1323 if (column.name != name)
continue;
1324 return at < column.values.size() ? column.values[at]
1325 : std::numeric_limits<double>::quiet_NaN();
1327 return std::numeric_limits<double>::quiet_NaN();
1334 "request data: no recorded series is installed for key '"
1338 std::size_t first = 0, last = series.
rows.size();
1339 while (first < last) {
1340 const auto middle = first + (last - first) / 2;
1341 if (series.
rows[middle].open_ms < current_bar_.timestamp) first = middle + 1;
1344 if (first == series.
rows.size())
1345 return std::numeric_limits<double>::quiet_NaN();
1347 return row.
open_ms == current_bar_.timestamp
1348 ? row.
value : std::numeric_limits<double>::quiet_NaN();
1355 const Bar* bars,
const int64_t* close_ms,
1357 guard_native_mutation(
"set_symbol_feed");
1358 const auto refuse = [
this](
const char* reason,
const std::string& why,
1359 const char* field =
nullptr) {
1361 if (field) args.emplace_back(
"field", field);
1366 if (run_in_progress(*
this))
return refuse(
"run_in_progress", kRunInProgress);
1367 if (key.empty())
return refuse(
"empty_key",
"empty symbol key");
1368 const std::string tf = canonical_symbol_timeframe(
timeframe);
1370 return refuse(
"timeframe_unparsable",
"timeframe '" +
timeframe +
"' does not parse");
1372 if (n < 0 || (n > 0 && (bars ==
nullptr || close_ms ==
nullptr)))
1373 return refuse(
"invalid_bars",
"invalid bar array");
1374 const std::int64_t existing = find_symbol_feed(key, tf);
1378 std::vector<NativeInstrumentFeed> judged_feeds(1);
1383 feed.
bars.assign(bars, bars + n);
1384 feed.
close_ms.assign(close_ms, close_ms + n);
1390 return refuse(
"kernel_refused",
"feed refused (NativeRunSpecError "
1391 + std::to_string(
static_cast<int>(judged.error)) +
", field "
1392 + std::to_string(
static_cast<int>(judged.field)) +
")",
1397 if (
const std::string why = restore_symbol_feeds_reason(); !why.empty())
1398 return refuse(
"restore_failed", why);
1399 if (existing >= 0) {
1400 const std::size_t at =
static_cast<std::size_t
>(existing);
1409 refresh_symbol_data_digest();
1416 const std::string& name,
1417 const double* values,
int n) {
1418 guard_native_mutation(
"set_symbol_feed_column");
1419 const auto refuse = [
this](
const char* reason,
const std::string& why,
1420 const char* field =
nullptr) {
1422 if (field) args.emplace_back(
"field", field);
1427 if (run_in_progress(*
this))
return refuse(
"run_in_progress", kRunInProgress);
1428 const std::int64_t found = find_symbol_feed(key, canonical_symbol_timeframe(
timeframe));
1430 return refuse(
"no_feed",
"no feed is installed for symbol '" + key +
"' at timeframe '"
1433 if (name.empty())
return refuse(
"empty_column",
"empty column name");
1436 if (
const std::string why = restore_symbol_feeds_reason(); !why.empty())
1437 return refuse(
"restore_failed", why);
1440 const std::size_t feed_count =
static_cast<std::size_t
>(found) <
symbol_feed_views_.size()
1443 if (n < 0 ||
static_cast<std::size_t
>(n) != feed_count || (n > 0 && values ==
nullptr))
1444 return refuse(
"column_length",
"column '" + name +
"' has " + std::to_string(n)
1445 +
" values for " + std::to_string(feed_count) +
" bars");
1454 return refuse(
"column_refused",
"column '" + name +
"' refused (NativeRunSpecError "
1455 + std::to_string(
static_cast<int>(judged.error)) +
", field "
1456 + std::to_string(
static_cast<int>(judged.field)) +
")",
1460 : std::vector<double>{}};
1461 bool replaced =
false;
1462 for (
auto& existing : feed.
columns) {
1463 if (existing.name != name)
continue;
1464 existing = std::move(column);
1468 if (!replaced) feed.
columns.push_back(std::move(column));
1469 refresh_symbol_data_digest();
1475 const std::string& field,
1476 const std::string& value) {
1477 guard_native_mutation(
"set_symbol_facts");
1478 const auto refuse = [
this](
const char* reason,
const std::string& why) {
1483 if (run_in_progress(*
this))
return refuse(
"run_in_progress", kRunInProgress);
1484 if (key.empty())
return refuse(
"empty_key",
"empty symbol key");
1486 if (field ==
"canonical") {
1488 }
else if (field ==
"valid") {
1489 if (value ==
"true") candidate.
valid =
true;
1490 else if (value ==
"false") candidate.
valid =
false;
1491 else return refuse(
"valid_not_boolean",
1492 "valid must be \"true\" or \"false\", got '" + value +
"'");
1493 }
else if (field ==
"type") {
1494 candidate.
type = value;
1495 }
else if (field ==
"timezone") {
1497 }
else if (field ==
"session") {
1499 }
else if (field ==
"currency") {
1501 }
else if (field ==
"mintick") {
1502 double parsed = 0.0;
1503 if (!parse_fact_number(value, parsed) || !std::isfinite(parsed) || parsed <= 0.0)
1504 return refuse(
"mintick_invalid",
1505 "mintick must be a positive decimal, got '" + value +
"'");
1508 return refuse(
"unknown_field",
"unknown field '" + field +
"'");
1512 if (
const std::string why = restore_symbol_feeds_reason(); !why.empty())
1513 return refuse(
"restore_failed", why);
1515 refresh_symbol_data_digest();
1521 const int64_t* chart_open_ms,
1522 const double* values,
int n) {
1523 return set_recorded_series_impl(key, chart_open_ms, values, n, std::nullopt);
1527 const int64_t* chart_open_ms,
1528 const double* values,
int n,
1529 std::size_t cache_capacity_bytes) {
1530 return set_recorded_series_impl(key, chart_open_ms, values, n, cache_capacity_bytes);
1533bool source::PineStrategyHost::set_recorded_series_impl(
1534 const std::string& key,
const int64_t* chart_open_ms,
const double* values,
int n,
1535 std::optional<std::size_t> cache_capacity_bytes) {
1536 guard_native_mutation(
"set_recorded_series");
1537 const auto refuse = [
this](
const char* reason,
const std::string& why) {
1542 if (run_in_progress(*
this))
return refuse(
"run_in_progress", kRunInProgress);
1543 if (
key.empty())
return refuse(
"empty_key",
"empty key");
1544 if (n < 0 || (n > 0 && (chart_open_ms ==
nullptr || values ==
nullptr)))
1545 return refuse(
"invalid_arrays",
"invalid arrays");
1546 for (
int i = 1; i < n; ++i) {
1547 if (chart_open_ms[i] <= chart_open_ms[i - 1])
1548 return refuse(
"times_not_increasing",
1549 "chart open times must be strictly increasing (row "
1550 + std::to_string(i) +
")");
1554 pineforge::detail::FeedProjectionBuilder<RecordedSeriesRow> builder(report_scratch_directory(),
true);
1555 for (
int i = 0; i < n; ++i)
1557 RecordedSeries series{builder.finish(cache_capacity_bytes.value_or(
1561 if (
const std::string why = restore_symbol_feeds_reason(); !why.empty())
1562 return refuse(
"restore_failed", why);
1563 const auto digest = calculate_symbol_data_digest(&key, &series);
1564 recorded_series_[
key] = std::move(series);
1565 symbol_data_digest_ = digest;
1572 return set_recorded_series_reader_impl(key, std::move(reader), std::nullopt);
1577 std::size_t cache_capacity_bytes) {
1578 return set_recorded_series_reader_impl(key, std::move(reader), cache_capacity_bytes);
1581bool source::PineStrategyHost::set_recorded_series_reader_impl(
1583 std::optional<std::size_t> cache_capacity_bytes) {
1584 guard_native_mutation(
"set_recorded_series_reader");
1585 const auto refuse = [
this](
const char* reason,
const std::string& why) {
1590 if (run_in_progress(*
this))
return refuse(
"run_in_progress", kRunInProgress);
1591 if (
key.empty())
return refuse(
"empty_key",
"empty key");
1592 if (!reader)
return refuse(
"invalid_arrays",
"missing reader");
1593 RecordedSeries series{pineforge::detail::FeedProjection<RecordedSeriesRow>(
1594 std::move(reader), cache_capacity_bytes.value_or(
1596 std::int64_t previous = 0;
1597 for (std::size_t i = 0; i < series.rows.size(); ++i) {
1598 const auto open = series.rows[i].open_ms;
1599 if (i && open <= previous)
1600 return refuse(
"times_not_increasing",
1601 "chart open times must be strictly increasing (row "
1602 + std::to_string(i) +
")");
1605 if (
const std::string why = restore_symbol_feeds_reason(); !why.empty())
1606 return refuse(
"restore_failed", why);
1607 const auto digest = calculate_symbol_data_digest(&key, &series);
1608 recorded_series_[
key] = std::move(series);
1609 symbol_data_digest_ = digest;
1617void source::PineStrategyHost::refresh_symbol_data_digest() {
1618 symbol_data_digest_ = calculate_symbol_data_digest();
1621std::uint64_t source::PineStrategyHost::calculate_symbol_data_digest(
1622 const std::string* replacement_key,
const RecordedSeries* replacement)
const {
1623 BrokerStateHashSink f;
1624 f.u(symbol_feeds_.size());
1625 for (
const auto& feed : symbol_feeds_) {
1626 f.s(feed.instrument);
1629 const std::size_t count = (&feed - symbol_feeds_.data()) < symbol_feed_views_.size()
1630 && symbol_feed_views_[
static_cast<std::size_t
>(&feed - symbol_feeds_.data())].present
1631 ? symbol_feed_views_[
static_cast<std::size_t
>(&feed - symbol_feeds_.data())].size
1634 f.u(feed.columns.size());
1635 for (
const auto& column : feed.columns) f.s(column.name);
1637 f.u(symbol_facts_.size());
1638 for (
const auto& [key, facts] : symbol_facts_) {
1640 f.s(
facts.canonical);
1641 f.b(
facts.valid.has_value());
1644 f.s(
facts.timezone);
1646 f.s(
facts.currency);
1649 const bool inserting = replacement_key && recorded_series_.find(*replacement_key)
1650 == recorded_series_.end();
1651 f.u(recorded_series_.size() + (inserting ? 1 : 0));
1652 const auto fold_series = [&f](
const std::string&
key,
const RecordedSeries& series) {
1654 f.u(series.rows.size());
1655 for (std::size_t i = 0; i < series.rows.size(); ++i) {
1663 bool emitted =
false;
1664 for (
const auto& [key, series] : recorded_series_) {
1665 if (replacement_key && !emitted && *replacement_key <= key) {
1666 fold_series(*replacement_key, *replacement);
1669 if (!replacement_key || key != *replacement_key) fold_series(key, series);
1671 if (replacement_key && !emitted) fold_series(*replacement_key, *replacement);
1677namespace pineforge {
1680 const std::string& key,
const std::string&
timeframe,
1681 const Bar* bars,
const int64_t* close_ms,
int n) {
1682 guard_native_mutation(
"set_symbol_feed_borrowed");
1683 const auto refuse = [
this](
const char* reason,
const std::string& why,
1684 const char* field =
nullptr) {
1686 if (field) args.emplace_back(
"field", field);
1691 if (run_in_progress(*
this))
return refuse(
"run_in_progress", kRunInProgress);
1692 if (key.empty())
return refuse(
"empty_key",
"empty symbol key");
1693 const std::string tf = canonical_symbol_timeframe(
timeframe);
1695 return refuse(
"timeframe_unparsable",
"timeframe '" +
timeframe +
"' does not parse");
1696 if (n < 0 || (n > 0 && (bars ==
nullptr || close_ms ==
nullptr)))
1697 return refuse(
"invalid_bars",
"invalid bar array");
1699 const std::size_t count =
static_cast<std::size_t
>(n);
1703 std::vector<NativeInstrumentFeed> judged_feeds(1);
1704 judged_feeds.front().instrument = key;
1705 judged_feeds.front().tf = tf;
1708 return refuse(
"kernel_refused",
"feed refused (NativeRunSpecError "
1709 + std::to_string(
static_cast<int>(judged.error)) +
", field "
1710 + std::to_string(
static_cast<int>(judged.field)) +
")",
1713 if (
const std::string why = restore_symbol_feeds_reason(); !why.empty())
1714 return refuse(
"restore_failed", why);
1716 const std::int64_t existing = find_symbol_feed(key, tf);
1718 if (existing >= 0) {
1719 const std::size_t at =
static_cast<std::size_t
>(existing);
1736 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