9#include "../engine_internal.hpp"
10#include "../report_replay.hpp"
11#include "../timezone.hpp"
12#include "../native_execution_consumer.hpp"
13#include "../scratch_table_binders.hpp"
38#include <system_error>
48::pineforge::detail::ScratchStoreConfig command_scratch_config(
49 std::size_t page_size, ::pineforge::detail::ScratchKeyOrder order)
noexcept {
50 return {2, page_size, 1, 1, order,
54std::size_t command_scratch_page_size() {
56 const long size = ::sysconf(_SC_PAGESIZE);
59 std::system_error(errno ? errno : EINVAL, std::generic_category(),
60 "command scratch platform page size"));
61 return static_cast<std::size_t
>(size);
64struct SourceExitScratchRun {
68 const std::size_t page_size = command_scratch_page_size();
69 std::unique_ptr<::pineforge::detail::ScratchBacking> exit_backing;
70 ::pineforge::detail::CommandScratchStore exit{
73 source::detail::ExitScratchContext exit_context{exit, {}};
74 source::detail::ExitScratchVersionsProduction exit_versions{exit};
76 explicit SourceExitScratchRun(CacheCapacity capacity)
77 : exit_backing(source::detail::make_command_scratch_backing(page_size, capacity)) {
79 if (exit.status() == ::pineforge::detail::ScratchStatus::io_error)
80 ::pineforge::detail::rethrow_carried_scratch_failure(exit.carried_failure() ? exit.carried_failure() : exit_backing->carried_failure());
81 const auto code = exit.status() == ::pineforge::detail::ScratchStatus::io_error
82 ? RunFailureCode::scratch_io_failed
83 : RunFailureCode::engine_invariant;
84 throw coded<std::logic_error>(code, {},
85 "run command scratch store failed to open");
89 exit_context.versions = &exit_versions;
90 exit_context.lots = [](
const NativeStrategyHost&
host,
double mark,
void* context,
91 bool (*visit)(
void*,
const NativeOpenLot&)) {
92 for (
const auto& lot :
host.native_open_lots(mark))
93 if (!visit(context, lot)) return ::pineforge::detail::ScratchStatus::cancelled;
94 return ::pineforge::detail::ScratchStatus::ok;
96 exit_context.working = [](
const NativeStrategyHost&
host,
void* context,
97 bool (*visit)(
void*,
const NativeWorkingRequest&)) {
98 for (
const auto& request :
host.native_working_requests())
99 if (!visit(context, request)) return ::pineforge::detail::ScratchStatus::cancelled;
100 return ::pineforge::detail::ScratchStatus::ok;
105using TradeScratchKey = std::array<std::uint64_t, 4>;
106using TradeScratchHeader = std::array<std::uint64_t, 23>;
107enum : std::uint64_t {
108 scratch_incarnation, scratch_row, scratch_sort, scratch_permutation
111::pineforge::detail::ScratchBytes
scratch_bytes(
const void* data, std::size_t size)
noexcept {
112 return {
static_cast<const std::byte*
>(
data), size};
115::pineforge::detail::ScratchBuffer scratch_buffer(
void* data, std::size_t size)
noexcept {
116 return {
static_cast<std::byte*
>(
data), size};
120 void*, const ::pineforge::detail::ScratchByteSource& a, const ::pineforge::detail::ScratchByteSource& b,
121 int& order)
noexcept {
122 TradeScratchKey ka{}, kb{};
123 if (a.size() !=
sizeof ka ||
b.size() !=
sizeof kb)
124 return ::pineforge::detail::ScratchStatus::programming_error;
125 auto status = a.read(0, scratch_buffer(ka.data(),
sizeof ka));
127 status =
b.read(0, scratch_buffer(kb.data(),
sizeof kb));
129 order = ka < kb ? -1 : kb < ka ? 1 : 0;
130 return ::pineforge::detail::ScratchStatus::ok;
134 const ::pineforge::detail::ScratchReadback& store) {
142 "trade scratch contract operation failed");
147class TradeScratchTransaction {
149 explicit TradeScratchTransaction(::pineforge::detail::CommandScratchStore& store) : store_(store) {
150 require_trade_scratch(store_.begin(), store_);
152 ~TradeScratchTransaction() {
153 if (store_.transaction_active()) (void)store_.rollback();
155 void finish() { require_trade_scratch(store_.rollback(), store_); }
157 ::pineforge::detail::CommandScratchStore& store_;
161 const TradeScratchKey& key) {
162 ::pineforge::detail::ScratchSpanSource probe(
scratch_bytes(
key.data(),
sizeof key));
164 const auto status = store.lower_bound(probe, handle);
169TradeScratchKey scratch_key(const ::pineforge::detail::ScratchReadback& store,
171 TradeScratchKey
key{};
173 scratch_buffer(
key.data(),
sizeof key)), store);
178 const TradeScratchKey& key) {
179 const auto handle = scratch_seek(store, key);
184void scratch_put(::pineforge::detail::CommandScratchStore& store,
const TradeScratchKey& key,
185 const ::pineforge::detail::ScratchByteSource& value) {
188 require_trade_scratch(store.
put(
source, value, handle), store);
191void scratch_put(::pineforge::detail::CommandScratchStore& store,
const TradeScratchKey& key,
192 ::pineforge::detail::ScratchBytes value = {}) {
193 ::pineforge::detail::ScratchSpanSource
source(value);
194 scratch_put(store, key,
source);
198void scratch_walk(const ::pineforge::detail::CommandScratchStore& store, std::uint64_t kind, Visit&& visit) {
199 auto handle = scratch_seek(store, {kind, 0, 0, 0});
201 const auto key = scratch_key(store, handle);
202 if (key[0] != kind)
break;
205 const auto status = store.next(handle, next);
212std::uint64_t scratch_word(
const T& value)
noexcept {
213 static_assert(
sizeof(T) <=
sizeof(std::uint64_t),
"trade scratch scalar");
214 std::uint64_t word = 0;
215 std::memcpy(&word, &value,
sizeof value);
220void scratch_scalar(std::uint64_t word, T& value)
noexcept {
221 static_assert(
sizeof(T) <=
sizeof word,
"trade scratch scalar");
222 std::memcpy(&value, &word,
sizeof value);
225TradeScratchHeader scratch_trade_header(
const Trade& row, std::size_t
source,
226 std::uint8_t phase)
noexcept {
227 return {
source, phase, scratch_word(row.entry_time), scratch_word(row.exit_time),
228 scratch_word(row.entry_price), scratch_word(row.exit_price), scratch_word(row.qty),
229 scratch_word(row.pnl), scratch_word(row.pnl_pct), scratch_word(row.is_long),
230 scratch_word(row.entry_bar_index), scratch_word(row.exit_bar_index),
231 scratch_word(row.exit_from_bracket), scratch_word(row.max_runup),
232 scratch_word(row.max_drawdown), scratch_word(row.commission),
233 row.entry_incarnation, scratch_word(row.open_at_end), scratch_word(row.close_cause),
234 row.entry_id.size(), row.entry_comment.size(), row.exit_comment.size(),
238void scratch_trade_scalars(
const TradeScratchHeader& header,
Trade& row)
noexcept {
239 scratch_scalar(header[2], row.entry_time);
240 scratch_scalar(header[3], row.exit_time);
241 scratch_scalar(header[4], row.entry_price);
242 scratch_scalar(header[5], row.exit_price);
243 scratch_scalar(header[6], row.qty);
244 scratch_scalar(header[7], row.pnl);
245 scratch_scalar(header[8], row.pnl_pct);
246 scratch_scalar(header[9], row.is_long);
247 scratch_scalar(header[10], row.entry_bar_index);
248 scratch_scalar(header[11], row.exit_bar_index);
249 scratch_scalar(header[12], row.exit_from_bracket);
250 scratch_scalar(header[13], row.max_runup);
251 scratch_scalar(header[14], row.max_drawdown);
252 scratch_scalar(header[15], row.commission);
253 row.entry_incarnation = header[16];
254 scratch_scalar(header[17], row.open_at_end);
255 scratch_scalar(header[18], row.close_cause);
260class TradeScratchSource final :
public ::pineforge::detail::ScratchByteSource {
262 TradeScratchSource(TradeScratchHeader header,
const Trade& text) noexcept
263 : header_(header), parts_{
scratch_bytes(header_.data(),
sizeof header_),
268 std::uint64_t size() const noexcept
override {
269 std::uint64_t result = 0;
270 for (
const auto part : parts_) result += part.size;
274 ::pineforge::detail::ScratchBuffer destination)
const noexcept override {
275 if (offset > size() || destination.size > size() - offset
276 || (destination.size && !destination.data))
277 return ::pineforge::detail::ScratchStatus::programming_error;
278 if (!destination.size) return ::pineforge::detail::ScratchStatus::ok;
279 for (
const auto part : parts_) {
280 if (offset >= part.size) { offset -= part.size;
continue; }
281 const auto count = std::min(destination.size,
282 part.size -
static_cast<std::size_t
>(offset));
283 if (count) std::memcpy(destination.data, part.data + offset, count);
284 destination.data += count;
285 destination.size -= count;
287 if (!destination.size)
break;
289 return ::pineforge::detail::ScratchStatus::ok;
292 const TradeScratchHeader header_;
293 const std::array<::pineforge::detail::ScratchBytes, 5> parts_;
296TradeScratchHeader scratch_read_trade(const ::pineforge::detail::ScratchReadback& store,
298 TradeScratchHeader header{};
300 scratch_buffer(header.data(),
sizeof header)), store);
305 const TradeScratchHeader& header,
Trade& row) {
306 const std::array<std::string*, 4>
text{
307 &row.entry_id, &row.entry_comment, &row.exit_comment, &row.exit_id};
308 std::uint64_t offset =
sizeof header;
309 for (std::size_t index = 0; index <
text.size(); ++index) {
310 text[index]->resize(
static_cast<std::size_t
>(header[19 + index]));
312 scratch_buffer(text[index]->
data(), text[index]->size())), store);
313 offset +=
text[index]->size();
317[[noreturn]]
void reject_begin_bar(
int index,
const char* field,
const char*
detail,
318 const char* reason) {
321 {{
"index", index}, {
"field", field}, {
"reason", reason}},
322 "bar[" + std::to_string(index) +
"]." + field + (
detail ?
detail :
""));
332 "bar count must be non-negative");
334 if (args.n > 0 && args.bars ==
nullptr) {
336 "bars must be non-null for a nonempty array");
338 for (
int i = 0; i < args.n; ++i) {
339 const Bar& bar = args.bars[i];
340 if (!std::isfinite(bar.open)) reject_begin_bar(i,
"open",
" must be finite",
"not_finite");
341 if (!std::isfinite(bar.high)) reject_begin_bar(i,
"high",
" must be finite",
"not_finite");
342 if (!std::isfinite(bar.low)) reject_begin_bar(i,
"low",
" must be finite",
"not_finite");
343 if (!std::isfinite(bar.close)) reject_begin_bar(i,
"close",
" must be finite",
"not_finite");
344 if (args.is_stream) {
345 if (bar.timestamp < 0)
346 reject_begin_bar(i,
"timestamp",
" must be non-negative",
"negative");
347 if (bar.open < 0.0) reject_begin_bar(i,
"open",
" must be non-negative",
"negative");
348 if (bar.high < 0.0) reject_begin_bar(i,
"high",
" must be non-negative",
"negative");
349 if (bar.low < 0.0) reject_begin_bar(i,
"low",
" must be non-negative",
"negative");
350 if (bar.close < 0.0) reject_begin_bar(i,
"close",
" must be non-negative",
"negative");
351 if (!std::isfinite(bar.volume) || bar.volume < 0.0)
352 reject_begin_bar(i,
"volume",
" must be non-negative finite",
"volume_invalid");
353 }
else if (!std::isnan(bar.volume)
354 && (!std::isfinite(bar.volume) || bar.volume < 0.0)) {
355 reject_begin_bar(i,
"volume",
" must be non-negative finite or NaN (unavailable)",
358 if (bar.low > std::min(bar.open, bar.close))
359 reject_begin_bar(i,
"low",
" must not exceed open or close",
"low_above_body");
360 if (bar.high < std::max(bar.open, bar.close))
361 reject_begin_bar(i,
"high",
" must not be below open or close",
"high_below_body");
363 const std::int64_t previous = args.bars[i - 1].timestamp;
364 if (bar.timestamp <= previous)
365 reject_begin_bar(i,
"timestamp",
" must be strictly increasing",
"not_increasing");
367 && bar.timestamp > std::numeric_limits<std::int64_t>::max() + previous) {
368 reject_begin_bar(i,
"timestamp",
" delta exceeds int64 range",
"delta_overflow");
372 if (args.is_stream && args.n > 0
373 && (!std::isfinite(args.bars[args.n - 1].close)
374 || args.bars[args.n - 1].close <= 0.0)) {
376 "stream warmup final close must be finite and positive");
384std::vector<NativeInstrumentFeed> lend_feed_bytes(std::vector<NativeInstrumentFeed>& store) {
385 std::vector<NativeInstrumentFeed> lent(store.size());
386 for (std::size_t i = 0; i < store.size(); ++i) {
387 lent[i].instrument = store[i].instrument;
388 lent[i].tf = store[i].tf;
389 lent[i].columns.resize(store[i].columns.size());
390 for (std::size_t c = 0; c < store[i].columns.size(); ++c)
391 lent[i].columns[c].name = store[i].columns[c].name;
393 for (std::size_t i = 0; i < store.size(); ++i) {
394 lent[i].bars.swap(store[i].bars);
395 lent[i].close_ms.swap(store[i].close_ms);
396 for (std::size_t c = 0; c < store[i].columns.size(); ++c)
397 lent[i].columns[c].values.swap(store[i].columns[c].values);
403void return_feed_bytes(std::vector<NativeInstrumentFeed>& store,
404 std::vector<NativeInstrumentFeed>& lent)
noexcept {
405 for (std::size_t i = 0; i < store.size() && i < lent.size(); ++i) {
406 store[i].bars.swap(lent[i].bars);
407 store[i].close_ms.swap(lent[i].close_ms);
408 for (std::size_t c = 0; c < store[i].columns.size() && c < lent[i].columns.size(); ++c)
409 store[i].columns[c].values.swap(lent[i].columns[c].values);
417 if (
dynamic_cast<const RunFailureInfo*
>(&error) !=
nullptr)
throw;
425 return {0x5830365830370001ULL,
nullptr, compare_trade_scratch};
429 require_trade_scratch(store.
status(), store);
431 || (trade_scratch_ && trade_scratch_->transaction_active()))
433 "trade scratch must be exclusively owned and empty");
434 trade_scratch_ = &store;
440 "run owner did not provision trade scratch");
441 return *trade_scratch_;
459 host_mutation_guard_inert_ =
true;
460 NativeExecutionConsumer::bound(*this).set_batch_input_release(
461 this, [](
void* owner)
noexcept {
462 static_cast<PineStrategyHost*>(owner)->scheduler_.release_batch_input();
464#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
475 if (last_script_continuation_valid_) {
476 return broker_state_hash_from_execution_hash(last_script_continuation_hash_);
478 const std::uint64_t
execution = execution_consumer().continuation_hash();
482 as_native_consumer(execution_consumer()).request_core().rethrow_book_read_failure();
483 return broker_state_hash_from_execution_hash(
execution);
493 const std::optional<double> level = native_liquidation_price();
495 double liquidation = *level;
496 if (syminfo_mintick_ > 0.0) {
498 ? std::ceil(liquidation / syminfo_mintick_) * syminfo_mintick_
499 : std::floor(liquidation / syminfo_mintick_) * syminfo_mintick_;
509 {{
"entrypoint",
"strategy_set_override"},
510 {
"reason",
"number_out_of_finite_range"}},
511 "default_qty_value: number out of finite range");
532 staged.syminfo.
mintick = syminfo_mintick_;
533 staged.inputs = inputs_;
534 staged.chart_timezone = chart_timezone_;
535 staged.account_fx = account_currency_fx_;
536 staged.account_fx_effective_from_ms = account_currency_fx_timestamps_;
537 staged.account_fx_per_quote = account_currency_fx_rates_;
538 if (std::isfinite(qty_step_) && qty_step_ > 0.0) staged.quantity_grid = qty_step_;
539 staged.instrument_feeds.reserve(symbol_feeds_.size());
540 for (std::size_t i = 0; i < symbol_feeds_.size(); ++i) {
541 const NativeInstrumentFeed& feed = installed_symbol_feed(i);
542 const auto views = NativeExecutionConsumer::bound(*this).bar_views();
543 const auto* slot = views.instrument_feed_count == 0
544 ? nullptr : views.instrument_feeds[i];
546 const std::size_t count = i < symbol_feed_views_.size() && symbol_feed_views_[i].present
547 ? symbol_feed_views_[i].size :
data.bars.size;
548 staged.instrument_feeds.push_back(
549 {feed.instrument, feed.tf, count, feed.columns.size()});
558 abort_requested_.store(
false, std::memory_order_relaxed);
559 validate_source_begin_bars(args);
562 if (native_state().kind == NativeLifecycleKind::Running)
return;
565 syminfo_mintick_ = syminfo_.
mintick;
566 if (std::isfinite(syminfo_.qty_step) && syminfo_.qty_step > 0.0)
567 qty_step_ = syminfo_.qty_step;
571 if (args.
is_stream && native_security_feed_enabled()) {
574 "native request.security feed supports historical runs only");
582 "request.security symbol feeds and recorded request series support historical "
587 std::string effective_input = args.
input_tf;
588 if (effective_input.empty() && args.
n >= 2 && args.
bars !=
nullptr)
590 const std::string effective_script = args.
script_tf.empty()
593 if (!effective_input.empty() && !effective_script.empty()
594 &&
tf_ratio(effective_input, effective_script) == -2) {
597 {{
"option",
"script_timeframe"}, {
"script_tf", effective_script},
598 {
"input_tf", effective_input}},
599 "script timeframe must be coarser than or equal to input timeframe: requested script_tf "
600 + effective_script +
" from input timeframe " + effective_input);
602 }
catch (
const std::runtime_error&) {
612 "native stream requires close-only calculation; calc_on_order_fills is unsupported");
617 "native stream cannot use historical probe/tail overrides");
622 effective = apply_overrides(effective, *overrides);
628 "timestamped account-currency FX does not support calc_on_order_fills");
632 "timestamped account-currency FX is not supported with bar magnifier");
635 adapter_.set_cache_policy_for_run(cache_policy_for_run());
637 tape.second.rows.set_capacity_bytes(tape.second.cache_capacity_override.value_or(
638 cache_policy_for_run().recorded_series_pages.capacity_bytes));
639 const auto& scratch = scratch_tables_for_run();
642 auto command_scratch = std::make_shared<SourceExitScratchRun>(cache_policy_for_run().exit_pages);
643 auto& consumer = NativeExecutionConsumer::bound(*
this);
644 using ScratchBinding = NativeExecutionConsumer::CommandScratchBinding;
647 if (args.
n > 0 &&
static_cast<std::size_t
>(args.
n)
648 <=
adapter_.placement_.max_size() / 2U) {
649 adapter_.placement_.reserve(
static_cast<std::size_t
>(args.
n) * 2U);
651 adapter_.set_configuration(effective);
652 adapter_.set_staged_configuration(staged);
658 ? NativePathOrder::HighFirst
659 : (path_order_mode_ == 2 ? NativePathOrder::LowFirst
660 : NativePathOrder::Auto);
666 restore_symbol_feeds();
667 std::vector<NativeInstrumentFeed> lent = lend_feed_bytes(
symbol_feeds_);
670 const bool has_borrowed_feed = std::any_of(
673 if (has_borrowed_feed) {
674 stage_symbol_feed_views();
676 NativeExecutionConsumer::bound(*this).stage_instrument_feed_views(
nullptr, 0);
678 return adapter_.project(effective, staged, args, path_order, &lent);
684 const bool failed_before = native_state().kind == NativeLifecycleKind::Failed;
691 command_scratch->exit_context.run = spec.
identity;
692 adapter_.bind_exit_scratch(command_scratch->exit_context);
693 consumer.retain_command_scratch_owner(ScratchBinding::Exit, command_scratch);
694 setup = NativeExecutionConsumer::bound(*this).configure(*
this, std::move(spec));
699 if (setup.
status == NativeSetupStatus::Applied) {
704 if (setup.
status != NativeSetupStatus::Applied) {
711 try { prepare_script_run(
nullptr, 0,
false); }
713 rethrow_latched_setting(error);
718 "Pine native adapter failed to configure projected run spec");
725 NativeExecutionConsumer::bound(*this).provision_run_command_scratch(
726 NativeExecutionConsumer::CommandScratchBinding::Trade,
732 native_acknowledge_events(0);
759 rethrow_latched_setting(error);
760 }
catch (
const std::exception& error) {
767 "unknown error during Pine script preparation");
780 const std::int64_t step = daily ? 0 :
static_cast<std::int64_t
>(script_tf_seconds_) * 1000;
782 const auto it = std::lower_bound(
784 [](
const std::pair<std::int64_t, std::int64_t>& day, std::int64_t value) {
785 return day.first < value;
787 const auto& day = *std::prev(it);
788 const std::int64_t last = std::min(before, day.second) - 1;
789 if (step <= 0)
return day.first;
790 return day.first + (last - day.first) / step * step;
809 std::vector<std::pair<std::int64_t, std::int64_t>> sessions;
810 for (
long long i = 0; i < count; ++i) {
811 const auto open = opens.find(i);
812 const auto close = closes.find(i);
813 if (open == opens.end() || close == closes.end()
814 || !std::isfinite(open->second) || !std::isfinite(close->second)) {
817 "symbol calendar: session day " + std::to_string(i) +
" of "
818 + std::to_string(count) +
" is missing");
820 sessions.emplace_back(std::llround(open->second), std::llround(close->second));
824 {{
"field",
"session_calendar"}},
825 "symbol calendar: session days out of order");
830 std::vector<std::pair<std::int64_t, std::int64_t>> sessions) {
831 for (std::size_t i = 0; i < sessions.size(); ++i) {
832 if (sessions[i].first >= sessions[i].second)
return false;
833 if (i + 1 < sessions.size() && sessions[i].second > sessions[i + 1].first)
return false;
840 std::int64_t ms)
const {
841 const auto it = std::upper_bound(
843 [](std::int64_t value,
const std::pair<std::int64_t, std::int64_t>& day) {
844 return value < day.first;
847 const auto& day = *std::prev(it);
848 return ms < day.second ? &day :
nullptr;
855 const std::int64_t step = daily ? 0 :
static_cast<std::int64_t
>(script_tf_seconds_) * 1000;
856 auto it = std::upper_bound(
858 [](std::int64_t value,
const std::pair<std::int64_t, std::int64_t>& day) {
859 return value < day.second;
862 if (at <= it->first)
return it->first;
863 if (step <= 0)
continue;
864 const std::int64_t slot = it->first + (at - it->first + step - 1) / step * step;
865 if (slot < it->second)
return slot;
870void source::PineStrategyHost::capture_script_continuation_hash() {
873 const std::uint64_t
execution = execution_consumer().continuation_hash();
877 as_native_consumer(execution_consumer()).request_core().rethrow_book_read_failure();
878 last_script_continuation_hash_ =
execution;
879 last_script_continuation_valid_ =
true;
880 if (broker_state_hash_recording_ && !broker_state_hashes_.empty()) {
881 broker_state_hashes_.back() = broker_state_hash();
889 stream_warmup_mode_ =
false;
895 if (
scheduler_.terminal_source_bar()) capture_script_continuation_hash();
902 stream_warmup_mode_ =
false;
917 read_ahead_foreign_security_sites(bar);
918 clear_gapped_foreign_security_sites();
920 bar_magnifier_enabled_ =
scheduler_.bar_magnifier_enabled();
921 diag_magnifier_sub_bars_processed_ = bar_magnifier_enabled_
923 diag_magnifier_sample_ticks_processed_ = bar_magnifier_enabled_
928 selected_replay_at_open(bar, context);
942 bar_magnifier_enabled_ =
scheduler_.bar_magnifier_enabled();
943 diag_magnifier_sub_bars_processed_ = bar_magnifier_enabled_
945 diag_magnifier_sample_ticks_processed_ = bar_magnifier_enabled_
947 adapter_.observe_terminal_receipts();
948 adapter_.on_bar_close_before_script(bar, context);
950 adapter_.on_bar_close(bar, context);
953 scheduler_record_range_end(bar);
955 const bool recording = broker_state_hash_recording_ && !broker_state_hashes_.empty();
959 && stream_phase_ == StreamPhase::REALTIME;
960 if (recording || last_batch || stream_script) {
966 capture_script_continuation_hash();
978 for (std::size_t i = 0; i <
event.closed_trade_count; ++i) {
979 const std::size_t index =
event.first_trade_index + i;
980 if (index < trades_.size()
982 trades_.modify(index, [](
Trade& row) {
1012 const bool modeled_point = provenance == NativePriceProvenance::Confirmed
1013 || provenance == NativePriceProvenance::ModeledOHLCOpen
1014 || provenance == NativePriceProvenance::ModeledOHLCClose
1015 || provenance == NativePriceProvenance::Calculation
1016 || provenance == NativePriceProvenance::AfterCalculationClose;
1017 if (aggregated && modeled_point) {
1020 for (
auto& lot : pyramid_entries_) {
1022 lot.time = script_open;
1030 index < trades_.size(); ++index) {
1032 {
adapter_.invalidate_exit_group(index); trades_.modify(index, [&](
Trade& row) { row.
entry_time = script_open; }); }
1035 for (std::size_t i = 0; i <
event.closed_trade_count; ++i) {
1036 const std::size_t index =
event.first_trade_index + i;
1037 if (index >= trades_.size())
continue;
1038 trades_.modify(index, [&](
Trade& row) { row.
exit_time = script_open; });
1042 const bool pine_priced = p !=
adapter_.placement_.end()
1043 && ((std::isfinite(p->second.exit_levels.stop) && p->second.exit_levels.stop > 0.0)
1044 || (std::isfinite(p->second.exit_levels.limit) && p->second.exit_levels.limit > 0.0));
1049 const bool market_add = !pine_priced &&
event.closed_units == 0.0
1053 && std::any_of(pyramid_entries_.begin(), pyramid_entries_.end(),
1055 return lot.entry_incarnation != event.handle().incarnation;
1067 if (broker_fill_event_seq_ == std::numeric_limits<std::uint64_t>::max()) {
1069 "source broker fill sequence exhausted");
1071 ++broker_fill_event_seq_;
1074 bool changed =
false;
1075 for (
auto it = pyramid_entries_.begin(); it != pyramid_entries_.end(); ) {
1076 if (it->qty <= internal::kQtyEpsilon) {
1077 it = pyramid_entries_.erase(it);
1083 if (pyramid_entries_.empty() || position_qty_ <= internal::kQtyEpsilon) {
1084 reset_position_state_to_flat();
1085 }
else if (changed) {
1086 double total_qty = 0.0;
1087 double weighted_price = 0.0;
1088 for (
const auto& pe : pyramid_entries_) {
1089 total_qty += pe.qty;
1090 weighted_price += pe.price * pe.qty;
1092 position_qty_ = total_qty;
1093 position_entry_price_ = weighted_price / total_qty;
1094 position_entry_count_ =
static_cast<int>(pyramid_entries_.size());
1098 bool other_exit_here =
false;
1101 other_exit_here = own ==
adapter_.placement_.end()
1104 own->second.source_id, event.
handle());
1106 adapter_.on_applied(event, context);
1109 for (std::size_t i = 0; i <
event.closed_trade_count; ++i) {
1110 const std::size_t index =
event.first_trade_index + i;
1111 if (index >= trades_.size())
continue;
1112 trades_.modify(index, [](
Trade& row) {
1124 for (std::size_t i = 0; i <
event.closed_trade_count; ++i) {
1125 const std::size_t index =
event.first_trade_index + i;
1126 if (index >= trades_.size())
continue;
1130 }
else if (row.
exit_id.empty()) {
1136 adapter_.invalidate_exit_group(index);
if (row.
close_cause != previous_cause || index < trades_.base()) trades_.replace(index, row);
1138 project_short_seed_report_rows(event);
1139 if (!other_exit_here) order_global_exit_children(event);
1152void source::PineStrategyHost::record_applied_range_end() {
1153 if (scheduler_.terminal_source_bar() || barstate_islast_) {
1154 const Bar terminal = scheduler_.current_script_bar()
1155 ? *scheduler_.current_script_bar() : current_bar_;
1156 scheduler_record_range_end(terminal);
1164 if (reason != NativeCalculationReason::OrderFill) {
1174 if (!
scheduler_.coof_recalculation_due(*cause, context, *
this))
return;
1175 scheduler_.recalculate(*cause, context, *
this);
1176 record_applied_range_end();
1181 return adapter_.resolve_terms(facts);
1186 return adapter_.margin_check_allowed(point);
1191 return adapter_.resolve_margin_requirement(view);
1196 return adapter_.resolve_margin_call_units(view);
1201 return adapter_.resolve_anchored_level(view);
1206 const auto verdict =
adapter_.validate_precommit(view);
1212 guard_native_mutation(
"configure_pine_strategy");
1221 guard_native_mutation(
"set_strategy_override");
1230 BacktestEngine::set_syminfo_session(session);
1240 adapter_.set_risk_direction(direction);
1245 adapter_.set_risk_max_cons_loss_days(value);
1251 adapter_.set_risk_max_drawdown(value, percent);
1257 adapter_.set_risk_max_intraday_loss(value, percent);
1262 if (limit > 0)
adapter_.cap.adopt_script_count();
1269 adapter_.set_risk_max_position_size(value);
1292 if (
const auto point = current_execution_point()) {
1293 context = point->decision;
1301 syminfo_.session.empty() ?
"24x7" : syminfo_.session,
1302 syminfo_.timezone.empty() ?
"UTC" : syminfo_.timezone,
1303 time.dayofmonth, time.month};
1308 if (
const auto point = current_execution_point()) {
1309 context = point->decision;
1315 return adapter_.cap_calculation(context);
1324 std::int64_t timestamp_ms)
const {
1325 const std::time_t seconds =
static_cast<std::time_t
>(timestamp_ms / 1000);
1327 const auto utc = [&]() {
1328 return ::gmtime_r(&seconds, &tm) !=
nullptr;
1330 if (chart_timezone_.empty() || chart_timezone_ ==
"UTC"
1331 || chart_timezone_ ==
"Etc/UTC") {
1335 tz_util::ScopedTimezone guard(chart_timezone_);
1336 if (::localtime_r(&seconds, &tm) ==
nullptr) (void)utc();
1342 result.year = tm.tm_year + 1900;
1343 result.month = tm.tm_mon + 1;
1344 result.dayofmonth = tm.tm_mday;
1345 result.hour = tm.tm_hour;
1346 result.minute = tm.tm_min;
1347 result.second = tm.tm_sec;
1348 result.dayofweek = tm.tm_wday + 1;
1349 result.weekofyear = (tm.tm_yday + 7 - ((tm.tm_wday + 6) % 7)) / 7;
1354 as_native_consumer(execution_consumer()).set_retention_override(NativeEventRetention::Full);
1362 return as_native_consumer(execution_consumer()).applied_event_count();
1366 return scheduler_.history_advances_new_bar();
1375 return adapter_.pending_intent_view().last_bar_dual_entry_path();
1381 for (
const auto& lot : pyramid_entries_) {
1382 if (lot.qty > internal::kQtyEpsilon) paid += open_entry_commission(lot);
1388 return scheduler_.script_position_view(bar_index_, position_side_, position_qty_);
1392 const double tick = syminfo_mintick_;
1394 || !std::isfinite(tick) || !(tick > 0.0)) {
1397 const double ticks =
price / tick;
1398 const double nearest = std::floor(ticks + 0.5);
1400 if (std::abs(ticks - nearest) <= 1e-9 * std::max(1.0, std::abs(ticks)))
return price;
1401 return nearest * tick;
1406 bar_index_, position_side_, position_qty_, pyramid_entries_);
1415 if (position_side_ !=
PositionSide::FLAT || !std::isfinite(average) || !(average > 0.0))
1417 position_entry_price_ = average;
1435 const auto found = inputs_.find(key);
1436 if (found == inputs_.end() || found->second.empty())
return fallback;
1438 return scheduler_.source_series(found->second);
1439 }
catch (
const std::invalid_argument&) {
1461 std::vector<admission::Field> fields;
1463 fields.push_back(field);
1469 int index,
double fill_price,
double* qty,
int* close_only,
int* partition)
const {
1470 return pending_intent_view().probe_fill_qty(index, fill_price, qty, close_only, partition);
1478 int index,
double* stop,
double* limit,
double* trail_activation)
const {
1483 return adapter_.pending_intent_view();
1488 return adapter_.short_seed_collision_role_v1(std::move(handle));
1496 adapter_.attach_execution_adapter();
1505 const std::string& key,
double value) {
1506 if (key ==
"report_terminal_quote_time_ms") {
1507 if (NativeExecutionConsumer::selected_query_closed(*
this)) {
1510 report_quote_time_ms_ = std::isfinite(value) && value >= 0.0
1511 && value <= 9007199254740991.0 && std::floor(value) == value
1512 ?
static_cast<std::int64_t
>(value) : -1;
1515 if (key ==
"report_terminal_quote_close") {
1516 if (NativeExecutionConsumer::selected_query_closed(*
this)) {
1519 report_quote_close_ = std::isfinite(value) && value > 0.0
1520 ? value : std::numeric_limits<double>::quiet_NaN();
1523 BacktestEngine::set_syminfo_metadata(key, value);
1529 if (key.compare(0, 16,
"symbol_calendar_") == 0) {
1530 if (key ==
"symbol_calendar_days") {
1531 if (std::isfinite(value) && value >= 0.0 && value <= 1e9) {
1540 for (
const auto& [prefix, days] : {std::pair<const char*, std::map<long long, double>*>{
1543 const std::size_t n = std::char_traits<char>::length(prefix);
1544 if (key.size() <= n || key.size() - n > 9 || key.compare(0, n, prefix) != 0)
continue;
1545 long long index = 0;
1547 for (std::size_t i = n; i < key.size(); ++i) {
1548 if (key[i] <
'0' || key[i] >
'9') {
1552 index = index * 10 + (key[i] -
'0');
1554 if (digits) (*days)[index] = value;
1558 if (key ==
"bar_index_offset") {
1559 scheduler_.set_bar_index_offset(std::isfinite(value)
1560 ?
static_cast<int>(std::llround(value)) : 0);
1562 if (key ==
"security_range_start_na_warmup") {
1563 if (std::isfinite(value) && value > 0.0) {
1571 if (key ==
"chart_ema_na_warmup")
1573 if (key ==
"historical_security_lookahead_projection")
1575 if (key ==
"margin_long" &&
config_.margin_long == 100.0)
1576 config_.margin_long = (std::isfinite(value) && value > 0.0) ? value : 100.0;
1577 if (key ==
"margin_short" &&
config_.margin_short == 100.0)
1578 config_.margin_short = (std::isfinite(value) && value > 0.0) ? value : 100.0;
1580 adapter_.priority.metadata(key, value);
1585 if (NativeExecutionConsumer::selected_query_closed(*
this) || time_ms < 0
1586 || time_ms > 9007199254740991LL || !std::isfinite(close) || close <= 0.0)
return false;
1587 report_quote_time_ms_ = time_ms;
1588 report_quote_close_ = close;
1593 if (NativeExecutionConsumer::selected_query_closed(*
this)) {
1596 report_quote_time_ms_ = -1;
1597 report_quote_close_ = std::numeric_limits<double>::quiet_NaN();
1614 int index,
double fill_price,
double* qty,
int* close_only,
int* partition)
const {
1615 return pending_intent_view().probe_fill_qty(index, fill_price, qty, close_only, partition);
1623 int index,
double* stop,
double* limit,
double* trail_activation)
const {
1628 return adapter_.pending_intent_view().trail_best_price();
1631void source::PineStrategyHost::adapter_label_bracket_trades(
1636 for (std::size_t offset = 0; offset <
event.closed_trade_count; ++offset) {
1637 const std::size_t index =
event.first_trade_index + offset;
1638 if (index >= trades_.size())
continue;
1639 adapter_.invalidate_exit_group(index); trades_.modify(index, [&](
Trade& trade) { trade.
exit_from_bracket = from_bracket; });
1643bool source::PineStrategyHost::adapter_has_open_entry_id(
1644 const std::string&
id)
const {
1645 return std::any_of(pyramid_entries_.begin(), pyramid_entries_.end(),
1646 [&](
const PyramidEntry& row) { return row.entry_id == id && row.qty > 0.0; });
1649const std::vector<source::PineStrategyHost::FixtureIntentRow>&
1654 +
adapter_.pending_coof_requests_.size()
1655 +
adapter_.delayed_market_orders_.size()
1657 const auto append = [&](
const PlacementSnapshot& snapshot,
const std::string& label) {
1659 switch (snapshot.family) {
1667 type = FixtureIntentKind::EXIT;
1670 type = FixtureIntentKind::RAW_ORDER;
1677 ?
"__close__" + snapshot.source_id
1678 : (snapshot.frozen_market_targeted_close ? label : snapshot.source_id);
1681 && !std::isfinite(snapshot.exit_levels.limit)
1682 && std::isfinite(snapshot.exit_levels.stop)
1683 && std::isnan(snapshot.requested_qty)
1685 &&
config_.default_qty_value <= 100.0;
1686 const double absent = std::numeric_limits<double>::quiet_NaN();
1692 row.
is_long = snapshot.is_long;
1694 ? snapshot.requested_qty
1695 : (std::isfinite(snapshot.projection_remaining_qty)
1696 ? snapshot.projection_remaining_qty : snapshot.requested_qty);
1698 row.
created_bar = snapshot.projection_created_bar;
1699 row.
created_seq =
static_cast<std::int64_t
>(snapshot.source_sequence);
1706 ? snapshot.projection_default_stop_equity : absent;
1708 ? snapshot.projection_default_stop_signal_close : absent;
1713 && snapshot.frozen_market_instruction
1714 && std::isfinite(snapshot.requested_qty)) {
1715 auto observation = std::make_shared<admission::CommandObservation>();
1716 observation->command = snapshot.command_ordinal;
1717 observation->kind = admission::CommandKind::Entry;
1721 observation->id = snapshot.source_id;
1722 observation->requested_quantity = snapshot.requested_qty;
1723 observation->quantity_type = snapshot.qty_type;
1724 observation->buy = snapshot.is_long;
1725 observation->prices = {snapshot.exit_levels.limit, snapshot.exit_levels.stop};
1726 observation->oca_name = snapshot.oca_name;
1727 observation->oca_type = snapshot.oca_type;
1728 auto& configuration = observation->configuration;
1729 configuration.process_on_close =
config_.process_orders_on_close;
1730 configuration.calc_on_fills =
config_.calc_on_order_fills;
1731 configuration.slippage =
config_.slippage;
1732 configuration.pyramiding =
config_.pyramiding;
1733 configuration.default_quantity_type =
config_.default_qty_type;
1734 configuration.default_quantity_value =
config_.default_qty_value;
1735 configuration.long_margin =
config_.margin_long;
1736 configuration.short_margin =
config_.margin_short;
1737 configuration.commission_value =
config_.commission_value;
1738 configuration.commission_type =
config_.commission_type;
1739 configuration.pointvalue = staged_configuration().syminfo.pointvalue;
1740 configuration.fx = snapshot.sizing.fx;
1741 configuration.quantity_step = staged_configuration().quantity_grid
1742 ? *staged_configuration().quantity_grid : 0.0;
1743 configuration.mintick = staged_configuration().syminfo.mintick;
1746 observation->placement_cycle = snapshot.placement_cycle;
1747 observation->held_quantity = 0.0;
1748 observation->held_entries = 0;
1749 observation->realized_equity = snapshot.sizing.equity;
1750 observation->placement_equity = snapshot.sizing.equity;
1751 observation->signal_close = snapshot.sizing.price;
1752 observation->quantized_fixed_quantity =
1753 snapshot.frozen_market_own_units;
1755 snapshot.requested_qty, snapshot.sizing.equity,
1756 snapshot.sizing.price, snapshot.sizing.mark, snapshot.sizing.fx};
1761 for (
const auto& command :
adapter_.pending_same_bar_commands_) {
1766 if (
config_.process_orders_on_close
1768 && !command.snapshot.birth.at_terminal_fill()) {
1771 append(command.snapshot, command.request.label);
1773 for (
const auto& pending :
adapter_.pending_entries_)
1774 append(pending.snapshot, pending.request.label);
1775 for (
const auto& delayed :
adapter_.delayed_market_orders_)
1776 append(delayed.snapshot, delayed.request.label);
1777 for (
const auto& leg :
adapter_.pending_bracket_legs_)
1778 append(leg.snapshot, leg.request.label);
1779 for (
const auto& pending :
adapter_.pending_coof_requests_)
1780 append(pending.snapshot, pending.request.label);
1781 for (
const auto& shadow :
adapter_.source_shadow_pending_)
1782 append(shadow.snapshot, shadow.label);
1783 for (
const auto& handle :
adapter_.live_handles_) {
1784 const auto found =
adapter_.placement_.find(handle.incarnation);
1785 if (found ==
adapter_.placement_.end())
continue;
1786 if (
config_.process_orders_on_close
1789 && !found->second.birth.at_terminal_fill()) {
1792 append(found->second, found->second.source_id);
1800void source::PineStrategyHost::project_short_seed_report_rows(
1804 || event.
handle() == plan.final_short) {
1807 std::optional<PlacementSnapshot> placement_snapshot;
1808 if (
const auto placement = adapter_.placement_.find(event.
handle().
incarnation);
1809 placement != adapter_.placement_.end()) {
1810 placement_snapshot = placement->second;
1813 || (placement_snapshot->from_entry != plan.seed_id
1814 && placement_snapshot->source_id != plan.seed_id)) {
1817 for (std::size_t index = trades_.base(); index < trades_.size(); ++index) {
1818 Trade trade = trades_[index];
1819 if (trade.entry_incarnation ==
plan.materialize_long.incarnation
1820 && trade.entry_id ==
plan.materialize_label) {
1821 trade.entry_incarnation =
plan.final_short.incarnation;
1822 adapter_.invalidate_exit_group(index); trades_.replace(index, trade);
1825 const std::size_t begin =
event.first_trade_index;
1826 const std::size_t
end = begin +
event.closed_trade_count;
1827 for (std::size_t index = begin; index <
end && index < trades_.size(); ++index) {
1828 if (trades_[index].entry_incarnation ==
plan.final_short.incarnation
1829 && trades_[index].entry_id ==
plan.final_short_id) {
1830 adapter_.invalidate_exit_group(index); trades_.modify(index, [&](Trade& row) {
1831 row.entry_incarnation =
plan.materialize_long.incarnation;
1835 adapter_.short_seed_.report_swap_pending =
false;
1838void source::PineStrategyHost::scheduler_prepare_script_run(
1840 int expected_script_bars,
bool script_bar_geometry) {
1841 if (
const auto state = native_state(); state.spec) {
1842 input_tf_ = state.spec->timeframe_undetected ?
"" : state.spec->input_tf;
1843 script_tf_ = state.spec->timeframe_undetected ?
"" : state.spec->script_tf;
1846 prepare_script_run(bars.empty() ?
nullptr : bars.data(),
static_cast<int>(bars.size()),
1848 last_bar_index_ = expected_script_bars - 1;
1849 last_bar_time_ = bars.empty() ? 0 : bars.back().timestamp;
1850 apply_realtime_tail_horizon(
1851 bars.empty() ?
nullptr : bars.data(),
static_cast<int>(bars.size()),
1852 script_bar_geometry);
1853 source_last_bar_index_ = last_bar_index_;
1860 bool script_bar_geometry) {
1863 last_bar_index_ = horizon - 1;
1864 const int64_t script_tf_ms =
1865 static_cast<int64_t
>(script_tf_seconds_ > 0 ? script_tf_seconds_ : 0) * 1000;
1866 if (script_bar_geometry) {
1868 last_bar_time_ = bars[horizon - 1].
timestamp;
1871 +
static_cast<int64_t
>(horizon - n) * script_tf_ms;
1875 +
static_cast<int64_t
>(horizon - 1) * script_tf_ms;
1879void source::PineStrategyHost::scheduler_configure_security_evaluators() {
1880 configure_security_evaluators();
1881 prune_pine_security_states();
1884bool source::PineStrategyHost::scheduler_uses_aux_security_feed() const noexcept {
1885#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
1886 return aux_security_feed_enabled();
1895int multi_day_count(
const std::string& tf) {
1897 if (!parsed || parsed->unit() != native_calendar::TimeframeUnit::Day)
return 0;
1898 return parsed->count() >= 2 ? parsed->count() : 0;
1903void source::PineStrategyHost::scheduler_prepare_security_sequence(
1905 security_input_tf_ = input_tf_;
1906#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
1907 if (aux_security_feed_enabled()) security_input_tf_ = aux_security_input_tf_;
1909 validate_security_timeframes(security_input_tf_);
1910 security_first_chart_bar_ms_ = bars.empty() ? 0 : bars.front().timestamp;
1911 if (declare_security_sites_to_kernel()) {
1915 security_next_input_ms_ = 0;
1916 security_calling_close_ms_ = 0;
1917 clear_historical_security_lookahead_projections();
1918 prepare_chart_day_partition(
1919 bars.empty() ?
nullptr : bars.data(),
static_cast<int>(bars.size()));
1922 init_security_eval_states_for_run(security_input_tf_);
1923 prepare_native_security_feeds(
1924 bars.empty() ?
nullptr : bars.data(),
static_cast<int>(bars.size()));
1930 for (
auto& state : security_eval_states_) {
1931 const int days = multi_day_count(state.tf);
1933 || state.aggregator.has_native_periods()) {
1938 feeds.chart = bars.data();
1939 feeds.n_chart =
static_cast<int>(bars.size());
1941#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
1942 if (aux_security_feed_enabled()) {
1943 feeds.aux = aux_security_bars_.data();
1944 feeds.n_aux = aux_security_bars_.size();
1947 feeds.tz = syminfo_.timezone;
1948 feeds.session = syminfo_.session;
1949 feeds.exchange_calendar = session_template_knows_early_close();
1951 state.aggregator.set_native_period_source(std::move(partition));
1953#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
1954 if (aux_security_feed_enabled()) {
1955 prepare_aux_security_chart_ranges(
1956 bars.empty() ?
nullptr : bars.data(),
static_cast<int>(bars.size()), script_tf_);
1959 prepare_historical_security_lookahead_projections(
1960 bars.empty() ?
nullptr : bars.data(),
static_cast<int>(bars.size()), input_tf_);
1961 prepare_chart_day_partition(
1962 bars.empty() ?
nullptr : bars.data(),
static_cast<int>(bars.size()));
1965bool source::PineStrategyHost::security_sites_kernel_routed() const noexcept {
1970 if (spec ==
nullptr)
return false;
1971 for (
const auto& series : spec->subscriptions) {
1972 if (series.source != NativeSeriesSource::InstrumentFeed)
return true;
1977bool source::PineStrategyHost::declare_security_sites_to_kernel() {
1978 std::vector<NativeTimeframeSubscription> declared;
1979 bool routed = same_symbol_sites_routable(declared);
1980 const std::size_t same_symbol = declared.size();
1983 prepare_foreign_security_sites(declared);
1984 if (declared.empty())
return false;
1985 if (declared.size() == same_symbol) {
1986 if (!declare_timeframe_subscriptions(std::move(declared)))
return false;
1988 auto result = declare_timeframe_subscriptions_result(declared);
1989 if (result.status != NativeSetupStatus::Applied && routed) {
1993 declared.erase(declared.begin(),
1994 declared.begin() +
static_cast<std::ptrdiff_t
>(same_symbol));
1995 foreign_security_series_.erase(
1996 foreign_security_series_.begin(),
1997 foreign_security_series_.begin() +
static_cast<std::ptrdiff_t
>(same_symbol));
1998 for (
auto& entry : foreign_security_sites_) {
1999 if (entry.second.subscription >= 0)
2000 entry.second.subscription -=
static_cast<std::int64_t
>(same_symbol);
2003 result = declare_timeframe_subscriptions_result(declared);
2005 if (result.status != NativeSetupStatus::Applied) {
2008 refused.emplace_back(
"field", field);
2009 throw coded<std::runtime_error>(
2011 "request.security: the kernel refused the symbol feed series (NativeRunSpecError "
2012 + std::to_string(
static_cast<int>(result.validation.error)) +
", field "
2013 + std::to_string(
static_cast<int>(result.validation.field)) +
")");
2018 if (routed) security_eval_states_.clear();
2022bool source::PineStrategyHost::same_symbol_sites_routable(
2023 std::vector<NativeTimeframeSubscription>& out) {
2024 if (security_eval_states_.empty())
return false;
2032 if (stream_warmup_mode_ || scheduler_.bar_magnifier_enabled())
return false;
2033 if (security_range_start_na_warmup_ || historical_security_lookahead_projection_)
2035#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
2036 if (aux_security_feed_enabled())
return false;
2038 if (input_tf_.empty() || input_tf_ != script_tf_ || security_input_tf_ != input_tf_)
2040 const auto view = native_state();
2041 if (view.spec ==
nullptr || view.spec->timeframe_undetected
2042 || view.spec->input_tf != input_tf_) {
2045 const bool otc_daily_pins = (syminfo_.type ==
"forex" || syminfo_.type ==
"cfd")
2046 && native_security_feeds_.empty() && script_tf_seconds_ > 0
2047 && script_tf_seconds_ < 86400;
2048 std::vector<NativeTimeframeSubscription> declared;
2049 declared.reserve(security_eval_states_.size());
2050 for (std::size_t i = 0; i < security_eval_states_.size(); ++i) {
2051 const SecurityEvalState& state = security_eval_states_[i];
2054 if (minute_day && !pine_security_state(state.sec_id).lookahead_on
2055 && script_tf_seconds_ > 0 && script_tf_seconds_ < 86400) {
2060 if (state.sec_id !=
static_cast<int>(i) || state.tf.empty())
return false;
2065 if (pine.lookahead_on || pine.heikinashi || pine.lower_tf_requested
2066 || pine.lower_tf_emulation || pine.lower_tf_use_input
2067 || pine.lower_tf_array_requested || pine.publish_gate_tf_seconds > 0
2068 || pine.calling_close_completes_partial || pine.calling_open_latches_first) {
2071 if (otc_daily_pins && (state.tf ==
"D" || state.tf ==
"1D"))
return false;
2075 if (multi_day_count(state.tf) != 0)
return false;
2076 NativeTimeframeSubscription subscription;
2077 subscription.tf = state.tf;
2078 subscription.gaps = pine.gaps_on;
2079 declared.push_back(std::move(subscription));
2081 out = std::move(declared);
2085bool source::PineStrategyHost::scheduler_feed_security_input(
2086 const Bar& bar, std::int64_t next_input_ms,
bool calling_bar_complete,
2087 bool defer_boundary_gate) {
2088 security_next_input_ms_ = next_input_ms;
2089 security_calling_close_ms_ = 0;
2090 bool deferred =
false;
2091 for (
auto& state : security_eval_states_) {
2092 if (defer_boundary_gate
2093 && pine_security_state(state.sec_id).publish_gate_tf_seconds > 0) {
2097 pine_feed_security_eval_state(state, bar, calling_bar_complete);
2102void source::PineStrategyHost::scheduler_publish_security_boundary() {
2103 for (
auto& state : security_eval_states_) {
2104 if (pine_security_state(state.sec_id).publish_gate_tf_seconds > 0)
2105 publish_security_eval_state_at_calling_boundary(state);
2109void source::PineStrategyHost::scheduler_feed_deferred_security_input(
2110 const Bar& bar, std::int64_t next_input_ms) {
2111 security_next_input_ms_ = next_input_ms;
2112 security_calling_close_ms_ = 0;
2113 for (
auto& state : security_eval_states_) {
2114 if (pine_security_state(state.sec_id).publish_gate_tf_seconds > 0)
2115 pine_feed_security_eval_state(state, bar,
false);
2119void source::PineStrategyHost::scheduler_feed_aux_security(
int chart_index) {
2120#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
2121 if (aux_security_feed_enabled() && scheduler_coof_enabled()) {
2129 restore_script_state();
2130 feed_aux_security_for_chart_bar(chart_index);
2131 snapshot_script_state();
2134 if (aux_security_feed_enabled()) feed_aux_security_for_chart_bar(chart_index);
2140void source::PineStrategyHost::scheduler_feed_deferred_aux_security(
int chart_index) {
2141#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
2142 if (aux_security_feed_enabled()) feed_deferred_aux_security_for_chart_bar(chart_index);
2148void source::PineStrategyHost::scheduler_finish_security_sequence() {
2149 clear_historical_security_lookahead_projections();
2150#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
2151 release_aux_security_chart_ranges();
2162#if PINEFORGE_PINE_QUIET_BAR_GATES
2163 if (trades.
size() < 2)
return false;
2164 const auto last_index = trades.
size() - 1;
2165 const auto previous_index = last_index - 1;
2173 const auto last_entry_id = trades.
entry_id_at(last_index);
2174 return trades.
entry_id_at(previous_index) == last_entry_id;
2181bool source::PineStrategyHost::report_terminal_quote_eligible()
const {
2182 if (!last_equity_point() || range_end_trades_.empty() || !native_bound())
return false;
2183 const auto state = as_native_consumer(execution_consumer()).view();
2184 const auto interval =
static_cast<std::int64_t
>(script_tf_seconds_) * 1000;
2185 const auto last_time = last_equity_point()->time_ms;
2186 return state.spec && state.spec->report_policy == NativeReportPolicy::KernelRecordedAtHostMarks
2187 && state.kind == NativeLifecycleKind::Completed && bar_magnifier_enabled_
2188 && script_tf_seconds_ > 60 && script_tf_seconds_ < 86400
2189 && report_quote_time_ms_ >= last_time && report_quote_time_ms_ - last_time <= interval
2190 && (report_quote_time_ms_ - last_time) % interval == 0
2191 && std::isfinite(report_quote_close_)
2192 && range_end_trades_.
size() == pyramid_entries_.size();
2196 if (!report_terminal_quote_eligible() ||
report.equity_curve_len == 0
2197 ||
report.equity_curve[
report.equity_curve_len - 1].time_ms != report_quote_time_ms_)
return false;
2198 std::size_t rows = 0;
2199 for (
int index = 0; index <
report.trades_len; ++index) {
2200 const auto& row =
report.trades[index];
2201 if (!row.open_at_end)
continue;
2202 if (row.exit_time != report_quote_time_ms_ || row.exit_price != bar_fill_price(report_quote_close_))
2206 return rows == pyramid_entries_.size();
2209void source::PineStrategyHost::present_report(
ReportC* out)
const {
2211 const auto state = as_native_consumer(execution_consumer()).view();
2213 || state.spec->report_policy != NativeReportPolicy::KernelRecordedAtHostMarks)
return;
2214 const auto position = as_native_consumer(execution_consumer()).position(*
this);
2216 if ((stream_phase_ == StreamPhase::REALTIME
2217 || state.completion == NativeCompletion::StreamEnded)
2218 && (!range_end_trades_.empty()
2219 || (position.signed_units == 0.0 && position.lot_count == 0
2220 && last.open_profit != 0.0))) {
2221 last.open_profit = 0.0;
2222 if (!range_end_trades_.empty()) {
2223 double range_end_pnl = 0.0;
2224 for (
const auto& row : range_end_trades_) range_end_pnl += row.pnl;
2225 last.equity = initial_capital_ + net_profit_sum_ + range_end_pnl;
2226 amend_report_replay_point(last.time_ms, report::EquityBasis::range_end, range_end_pnl);
2229 if (!report_terminal_quote_eligible())
return;
2230 const double price = bar_fill_price(report_quote_close_);
2231 const double fx = account_currency_fx_at(report_quote_time_ms_);
2232 std::size_t lot_index = 0;
2233 for (
int index = 0; index < out->
trades_len; ++index) {
2234 auto& row = out->
trades[index];
2235 if (!row.open_at_end)
continue;
2236 const auto& lot = pyramid_entries_[lot_index++];
2237 execution::PhysicalExecutionContext context;
2238 context.effective_time_ms = report_quote_time_ms_;
2240 context.account_fx = fx;
2241 const auto mark = build_close_trade_with_costs(lot, row.qty,
price, row.is_long != 0,
2242 allocated_entry_commission(lot, row.qty), calc_commission_at(
price, row.qty, fx), context);
2246 if (!internal::closed_row_all_finite(mark) || !std::isfinite(
price))
2247 throw std::logic_error(
"non-finite terminal-quote report row");
2248 row.exit_time = report_quote_time_ms_;
2249 row.exit_price =
price;
2251 row.pnl_pct = mark.pnl_pct;
2252 row.commission = mark.commission;
2256 last.time_ms = report_quote_time_ms_;
2257 last.equity = initial_capital_ + out->
net_profit;
2258 amend_report_replay_point(last.time_ms, report::EquityBasis::terminal_quote, out->
net_profit);
2261void source::PineStrategyHost::scheduler_record_range_end(
const Bar& terminal_bar) {
2262 range_end_trades_.clear();
2263 if (stream_warmup_mode_ || realtime_tail_
2265 || !std::isfinite(terminal_bar.close))
return;
2266 const double fill_price = bar_fill_price(terminal_bar.close);
2275 const std::int64_t mark_time = last_equity_point()->time_ms;
2276 const double range_end_pnl = as_native_consumer(execution_consumer())
2277 .append_open_position_report_rows(*
this, fill_price, mark_time, bar_index_,
2278 account_currency_fx_at(mark_time));
2279 if (stream_phase_ != StreamPhase::REALTIME) {
2280 auto& last = *last_equity_point();
2281 last.open_profit = 0.0;
2282 last.equity = initial_capital_ + net_profit_sum_ + range_end_pnl;
2283 amend_report_replay_point(last.time_ms, report::EquityBasis::range_end, range_end_pnl);
2288 refold_equity_tail();
2291 if (
const auto moved = sort_same_bar_exit_trades(trades_, adapter_))
2292 native_closed_rows_amended(*moved);
2321void source::PineStrategyHost::order_global_exit_children(
2322 const native_order::ExecutionAppliedEvent& event) {
2323 const std::size_t first =
event.first_trade_index;
2324 const std::size_t count =
event.closed_trade_count;
2325 if (count < 2 || event.opened_units != 0.0 || first + count != trades_.size()
2326 || !pyramid_entries_.empty()) {
2329 const auto& run = adapter_.config_;
2330 if (run.calc_on_order_fills || run.process_orders_on_close || bar_magnifier_enabled_
2331 || active_account_currency_fx() != 1.0 || commission_value_ < 0.0
2335 const auto placement = adapter_.placement_.find(event.handle().incarnation);
2336 if (placement == adapter_.placement_.end())
return;
2339 || !cause.from_entry.empty()) {
2346 Trade row = trades_[first];
2347 head.exit_time = row.exit_time;
2348 head.exit_price = row.exit_price;
2349 head.exit_id = std::move(row.exit_id);
2350 head.is_long = row.is_long;
2351 head.entry_id = std::move(row.entry_id);
2353 auto& scratch = trade_scratch();
2354 TradeScratchTransaction transaction(scratch);
2355 bool different_ids =
false;
2356 for (std::size_t index = first; index < first + count; ++index) {
2357 const Trade& row = trades_[index];
2358 if (row.exit_time != head.exit_time || row.exit_price != head.exit_price
2359 || row.exit_id != head.exit_id || row.is_long != head.is_long
2360 || !(row.qty > 0.0) || row.open_at_end) {
2363 const TradeScratchKey incarnation{scratch_incarnation, row.entry_incarnation, 0, 0};
2365 scratch_put(scratch, incarnation);
2366 different_ids = different_ids || row.entry_id != head.entry_id;
2368 if (!different_ids)
return;
2369 if (first > 0 && trades_[first - 1].exit_time == head.exit_time
2370 && trades_[first - 1].exit_price == head.exit_price) {
2378 std::array<std::size_t, 27> ages{};
2379 std::array<std::uint32_t, 27> ranks{};
2380 std::size_t age_count = 0;
2381 const auto age_of = [&](std::size_t lot) {
2382 const auto& keys = adapter_.exit_rank_keys_;
2383 const auto found = keys.find(trades_[first + lot].entry_id);
2384 return found ?
static_cast<std::size_t
>(keys.get(*found))
2385 : static_cast<std::size_t>(keys.size());
2387 const auto key_of = [&](std::size_t age) {
2389 adapter_.exit_rank_keys_.for_each_inserted([&](
SourceId key, std::uint32_t key_age) {
2390 if (key_age == age)
id = std::move(key);
2394 std::vector<SourceId> ids;
2395 for (std::size_t lot = 0; lot < count; ++lot) {
2396 const auto age = age_of(lot);
2397 if (age == adapter_.exit_rank_keys_.size())
return;
2398 if (std::find(ages.begin(), ages.begin() + age_count, age) != ages.begin() + age_count)
2400 ids.assign(1, trades_[first + lot].entry_id);
2401 const auto rank = adapter_.global_exit_child_ranks(ids);
2403 ranks[age] = rank->front();
2404 for (std::size_t earlier = 0; earlier < age_count; ++earlier) {
2406 ids.push_back(key_of(ages[earlier]));
2407 if (!adapter_.global_exit_child_ranks(ids))
return;
2409 ages[age_count++] = age;
2411 const auto rank_of = [&](std::size_t lot) {
2412 return ranks[age_of(lot)];
2415 bool ordered =
true;
2416 auto previous_rank = rank_of(0);
2417 for (std::size_t lot = 1; lot < count; ++lot) {
2418 const auto rank = rank_of(lot);
2419 if (rank < previous_rank) ordered =
false;
2420 previous_rank = rank;
2422 if (ordered)
return;
2423 auto*
const replay = report_replay_state();
2424 std::unique_ptr<::pineforge::detail::ScratchSequence<report::ReplayTrade>> replay_rows;
2426 replay_rows = std::make_unique<::pineforge::detail::ScratchSequence<report::ReplayTrade>>();
2427 replay_rows->set_directory(replay->scratch_directory());
2429 const auto visit_children = [&](
const auto& visit) {
2430 for (std::size_t rank = 0; rank < adapter_.exit_rank_keys_.size(); ++rank)
2431 for (std::size_t child = 0; child < count; ++child)
2432 if (rank_of(child) == rank && !visit(child))
return false;
2435 std::size_t row_count = 0;
2436 const auto emit_row = [&](TradeScratchHeader header,
const Trade&
text) {
2437 const TradeScratchSource row(header, text);
2438 scratch_put(scratch, {scratch_row, row_count++, 0, 0}, row);
2440 const auto header_for = [&](std::size_t source,
const Trade& row) {
2441 const auto phase = adapter_.exit_phase_value(source);
2442 return scratch_trade_header(row, source, phase);
2444 const auto annotate = [&](std::size_t lot, std::size_t child,
double transaction_qty,
2445 bool fifo,
bool fees,
double exit_fee,
double transaction_fee) {
2446 if (!replay)
return;
2447 const Trade whole = trades_.numeric_at(first + lot);
2448 auto row = replay->trade(first + lot);
2449 row.parent =
event.ordinal;
2450 row.slice = row_count - 1;
2452 row.transaction_qty = transaction_qty;
2453 row.whole_qty = whole.qty;
2454 row.whole_pnl = whole.pnl;
2455 row.whole_commission = whole.commission;
2456 row.whole_runup = whole.max_runup;
2457 row.whole_drawdown = whole.max_drawdown;
2458 row.whole_exit_fee = exit_fee;
2459 row.transaction_fee = transaction_fee;
2463 replay_rows->push_back(row);
2466 if (run.close_entries_rule_any) {
2467 visit_children([&](std::size_t lot) {
2468 const Trade row = trades_[first + lot];
2469 emit_row(header_for(first + lot, row), row);
2470 if (replay) annotate(lot, lot, row.qty,
false,
false, 0.0, 0.0);
2474 const bool fees = commission_value_ != 0.0;
2475 for (std::size_t lot = 0; lot < count; ++lot) {
2476 const double qty = trades_[first + lot].qty;
2479 const auto visit_slices = [&](
const auto& visit) {
2480 std::size_t lot = 0;
2481 double left = trades_[first].qty;
2482 if (!visit_children([&](std::size_t child) {
2483 const double transaction = trades_[first + child].qty;
2484 double wanted = transaction;
2485 while (wanted > internal::kQtyEpsilon && lot < count) {
2486 const double taken = std::min(left, wanted);
2487 visit(lot, child, taken, transaction);
2490 if (left <= internal::kQtyEpsilon) {
2492 if (lot < count) left = trades_[first + lot].qty;
2495 return !(wanted > internal::kQtyEpsilon);
2497 return lot == count;
2501 if (!visit_slices([](std::size_t, std::size_t,
double,
double) {}))
return;
2502 visit_slices([&](std::size_t lot, std::size_t child,
double qty,
double transaction) {
2503 const Trade& whole = trades_[first + lot];
2504 const double share = qty / whole.qty;
2505 double commission = 0.0;
2506 double replay_exit_fee = 0.0, replay_transaction_fee = 0.0;
2508 const double exit_fee = calc_commission_at(whole.exit_price, whole.qty, 1.0);
2509 const double entry_part =
2510 tv_money_round(std::max(0.0, whole.commission - exit_fee) * share);
2511 const double transaction_fee =
2512 tv_money_round(calc_commission_at(whole.exit_price, transaction, 1.0));
2513 const double exit_part =
tv_money_round(transaction_fee * qty / transaction);
2516 replay_exit_fee = exit_fee;
2517 replay_transaction_fee = transaction_fee;
2520 auto header = header_for(first + lot, whole);
2522 scratch_trade_scalars(header, row);
2524 row.commission = commission;
2525 row.pnl =
tv_money_round((whole.pnl + whole.commission) * share) - commission;
2526 const double cost = whole.entry_price * qty * syminfo_.pointvalue;
2527 row.pnl_pct = cost > 0.0 ? row.pnl / cost * 100.0 : 0.0;
2530 row.max_runup = whole.max_runup * share;
2531 row.max_drawdown = whole.max_drawdown * share;
2532 header[6] = scratch_word(row.qty);
2533 header[7] = scratch_word(row.pnl);
2534 header[8] = scratch_word(row.pnl_pct);
2535 header[13] = scratch_word(row.max_runup);
2536 header[14] = scratch_word(row.max_drawdown);
2537 header[15] = scratch_word(row.commission);
2538 emit_row(header, whole);
2539 if (replay) annotate(lot, child, transaction,
true, fees,
2540 replay_exit_fee, replay_transaction_fee);
2547 scratch_trade_scalars(scratch_read_trade(scratch, handle), row);
2548 if (!internal::closed_row_all_finite(row))
2549 throw std::logic_error(
"non-finite amended trade row");
2557 const auto tally = [
this](
double pnl,
int sign) {
2558 if (pnl > 0.0) { gross_profit_sum_ += sign * pnl; win_trades_count_ += sign; }
2559 else if (pnl < 0.0) { gross_loss_sum_ += sign * pnl; loss_trades_count_ += sign; }
2560 else { eventrades_count_ += sign; }
2562 for (std::size_t index = first; index < first + count; ++index) tally(trades_[index].pnl, -1);
2565 scratch_trade_scalars(scratch_read_trade(scratch, handle), row);
2569 if (replay_rows->size() != row_count)
2571 "recut replay rows differ from replacement rows");
2572 replay->resize_trades(first + row_count);
2573 for (std::size_t i = 0; i < row_count; ++i) {
2574 auto annotation = replay_rows->at(i);
2575 annotation.slices = row_count;
2576 replay->replace(first + i, annotation);
2579 adapter_.invalidate_exit_group(first, trades_.size() - first); trades_.resize(first);
2581 const auto header = scratch_read_trade(scratch, handle);
2583 scratch_trade_scalars(header, row);
2584 scratch_trade_text(scratch, handle, header, row);
2585 trades_.push_back(std::move(row));
2587 const auto none =
static_cast<std::uint8_t
>(NativePathPhase::None);
2589 const auto header = scratch_read_trade(scratch, handle);
2590 const auto target = first +
static_cast<std::size_t
>(
key[1]);
2591 const auto phase =
static_cast<std::uint8_t
>(header[1]);
2592 if (phase ==
none && adapter_.exit_phase_value(target) ==
none)
return;
2593 adapter_.set_exit_phase(target, phase);
2595 transaction.finish();
2596 native_closed_rows_amended(first);
2604std::optional<std::size_t> source::PineStrategyHost::sort_same_bar_exit_trades(
2605 TradeLedger& trades, source::PineExecutionAdapter& adapter) {
2606 ++adapter.exit_group_work_.publications;
if (adapter.exit_group_certified(trades) || trades.size() < 2)
return std::nullopt;
2607 const std::size_t
end = trades.size();
2608 std::size_t start =
end - 1;
2610 && trades[start - 1].exit_time == trades[end - 1].exit_time
2611 && trades[start - 1].entry_time == trades[end - 1].entry_time
2612 && trades[start - 1].entry_id == trades[end - 1].entry_id
2613 && trades[start - 1].exit_from_bracket
2614 && trades[end - 1].exit_from_bracket) {
2615 --start; ++adapter.exit_group_work_.tail_rows;
2617 if (end - start > 1) {
2622 const auto less = [&](std::size_t ia, std::size_t ib) {
2623 ++adapter.exit_group_work_.comparisons;
const bool open_a = adapter.is_open_phase_exit(ia);
2624 const bool open_b = adapter.is_open_phase_exit(ib);
2625 if (open_a != open_b)
return open_a;
2626 const auto sa = adapter.command_sequence_for_exit(trades[ia].exit_id, trades[ia].entry_id);
2627 const auto sb = adapter.command_sequence_for_exit(trades[ib].exit_id, trades[ib].entry_id);
2630 const auto none =
static_cast<std::uint8_t
>(NativePathPhase::None);
2631 bool ordered =
true;
2632 for (std::size_t index = start + 1; index <
end; ++index)
2633 if (less(index, index - 1)) { ordered =
false;
break; }
2634 if (ordered) { adapter.certify_exit_group(trades, start);
return std::nullopt; }
2635 auto& scratch = trade_scratch(); adapter.certify_exit_group(trades, start,
false);
2636 const auto scratch_before = scratch.stats(); TradeScratchTransaction transaction(scratch);
2637 for (std::size_t source = start; source <
end; ++source) {
2638 const auto phase = adapter.exit_phase_value(source);
2639 const auto sequence = adapter.command_sequence_for_exit(
2640 trades[source].exit_id, trades[source].entry_id);
2641 scratch_put(scratch,
2642 {scratch_sort, adapter.is_open_phase_exit(source) ? 0U : 1U,
sequence, source},
2645 std::size_t target = start;
2649 scratch_walk(scratch, scratch_sort,
2651 std::uint8_t phase =
none;
2653 scratch_buffer(&phase,
sizeof phase)), scratch);
2654 const std::array<std::uint64_t, 2> mapping{
key[3], phase};
2655 scratch_put(scratch, {scratch_permutation, target++, 0, 0},
2660 for (std::size_t cycle = start; cycle <
end; ++cycle) {
2661 auto handle = scratch_find(scratch, {scratch_permutation, cycle, 0, 0});
2663 const Trade row = trades[cycle];
2664 auto*
const replay = report_replay_state();
2665 const auto saved_replay = replay ? replay->trade(cycle) : report::ReplayTrade{};
2666 std::size_t current = cycle;
2668 std::array<std::uint64_t, 2> mapping{};
2670 scratch_buffer(mapping.data(),
sizeof mapping)), scratch);
2671 const auto source =
static_cast<std::size_t
>(mapping[0]);
2672 const auto phase =
static_cast<std::uint8_t
>(mapping[1]);
2676 if (phase !=
none || adapter.exit_phase_value(current) !=
none)
2677 adapter.set_exit_phase(current, phase);
2678 ++adapter.exit_group_work_.replacements;
if (source == cycle) trades.replace(current, row);
2679 else trades.replace(current, trades[source]);
2681 replay->replace(current, source == cycle ? saved_replay : replay->trade(source));
2682 require_trade_scratch(scratch.erase(handle), scratch);
2683 if (source == cycle)
break;
2685 handle = scratch_find(scratch, {scratch_permutation, current, 0, 0});
2688 "incomplete trade scratch permutation");
2691 transaction.finish(); adapter.finish_exit_group_sort(scratch, scratch_before);
2694 return std::nullopt;
2697void source::PineStrategyHost::scheduler_update_session_state() {
2723 const NativeDecisionContext&
facts = point->decision;
2724 session_ismarket_ =
facts.in_session;
2725 session_isfirstbar_regular_ =
facts.opens_session_day;
2726 session_islastbar_regular_ = realtime_tail_ ?
facts.closes_session_day_open_ended
2727 :
facts.closes_session_day;
2728 session_isfirstbar_ = session_isfirstbar_regular_;
2729 session_islastbar_ = session_islastbar_regular_;
2738 && script_tf_seconds_ > 0) {
2739 if (
const auto* day = symbol_calendar_day(
facts.script_bar_open_ms)) {
2740 const bool last =
facts.script_bar_open_ms
2741 +
static_cast<std::int64_t
>(script_tf_seconds_) * 1000 >= day->second;
2742 session_islastbar_regular_ = last;
2743 session_islastbar_ = last;
2746 update_extended_session_day(facts);
2771void source::PineStrategyHost::update_extended_session_day(
const NativeDecisionContext& facts) {
2772 const std::string& session = syminfo_.session;
2773 const std::string& tz = syminfo_.timezone;
2774 const std::int64_t at =
facts.script_bar_open_ms;
2775 const bool regular =
facts.in_session;
2778 if (regular && !
facts.opens_session_day && !session_islastbar_regular_)
return;
2779 const auto pre = [&](std::int64_t ms) {
2782 const auto post = [&](std::int64_t ms) {
2785 const bool premarket = !regular && pre(at);
2786 const bool postmarket = !regular && !premarket && post(at);
2787 if (!regular && !premarket && !postmarket)
return;
2790 const auto same_day = [&](std::int64_t ms) {
2793 const auto in_chart_day = [&](std::int64_t ms) {
2797 const std::optional<std::int64_t> before = scheduler_.last_published_script_open();
2801 const std::int64_t next_open = std::max(at + 1,
facts.script_interval.next_period_open_ms);
2802 std::optional<std::int64_t> after;
2803 bool known_after =
false;
2804 if (
const auto bar = scheduler_.retained_input_from(
facts.coordinate.input_interval_index,
2806 after = bar->timestamp;
2809 const bool run_end = !known_after && !scheduler_.retains_stream() && !realtime_tail_;
2810 if (!known_after && !run_end && !regular && script_tf_seconds_ > 0)
2811 after = at +
static_cast<std::int64_t
>(script_tf_seconds_) * 1000;
2817 if (
facts.opens_session_day)
2818 session_isfirstbar_ = !(before && pre(*before) && same_day(*before));
2819 if (session_islastbar_regular_)
2820 session_islastbar_ = !(known_after && post(*after) && same_day(*after));
2824 session_isfirstbar_ = !(before && pre(*before) && same_day(*before));
2825 session_islastbar_ = after ? !in_chart_day(*after) : run_end;
2828 session_isfirstbar_ = !(before && in_chart_day(*before));
2829 session_islastbar_ = after ? !(post(*after) && same_day(*after)) : run_end;
2832void source::PineStrategyHost::scheduler_publish_source_bar(
2833 const Bar& bar,
bool,
bool advance_source_index) {
2835 const bool temporary_index = !advance_source_index
2836 && (!scheduler_.current_script_bar()
2837 || scheduler_.current_script_bar()->timestamp != bar.timestamp);
2838 const int previous_bar_index = bar_index_;
2839 const bool previous_barstate_islast = barstate_islast_;
2840 if (advance_source_index || temporary_index) ++source_bar_index_;
2841 ++source_callback_count_;
2842 bar_index_ = source_bar_index_;
2845 if (lifecycle == NativeLifecycleKind::Running && phase == NativeRunPhase::Warmup) {
2846 barstate_islast_ =
false;
2847 }
else if (lifecycle == NativeLifecycleKind::Running
2848 && phase == NativeRunPhase::Realtime) {
2849 barstate_islast_ =
true;
2851 barstate_islast_ = source_bar_index_ == source_last_bar_index_;
2862 internal::RuntimeAmbient*
const ambient = NativeExecutionConsumer::bound(*this).pump_ambient();
2863 internal::AmbientDayPartitionScope chart_day_partition(
2864 ambient, chart_day_partition_.empty() ?
nullptr : &chart_day_partition_);
2867 adapter_.begin_source_evaluation();
2872 const bool capturing = selected_begin_evaluation(bar.timestamp);
2877 if (
const auto moved = sort_same_bar_exit_trades(trades_, adapter_))
2878 native_closed_rows_amended(*moved);
2884 if (consumer.streams_closed_rows(*
this) && consumer.begin_closed_row_seal(bar.timestamp)) {
2885 const std::size_t base = trades_.base();
2886 auto prefix = consumer.closed_trade_prefix();
2887 if (prefix.base != base)
2889 "source trade seal has a mismatched base");
2890 const bool can_swap = adapter_.short_seed_.final_short.incarnation != 0
2891 && adapter_.config_.default_qty_type !=
static_cast<int>(
QtyType::FIXED);
2892 const std::size_t next = consumer.closed_rows_sealable(trades_,
2893 adapter_.exit_phase_final_, adapter_.source_money_folded_, bar.timestamp,
2894 can_swap ? std::optional<std::uint64_t>(adapter_.short_seed_.materialize_long.incarnation)
2896 if (next - base > adapter_.trade_exit_phase_.size())
2898 "source trade seal exceeds resident exit phases");
2899 for (std::size_t index = base; index < next; ++index) {
2900 const Trade& row = trades_[index];
2902 prefix.exit_phase_digest, adapter_.trade_exit_phase_[index - base]);
2903 if (row.pnl > 0.0) prefix.source_gains += row.pnl;
2904 else prefix.source_losses += row.pnl;
2911 adapter_.preserve_position_tail_before_seal(
2912 next, consumer.report_scratch_directory(*
this));
2913 consumer.seal_closed_rows(*
this, next, prefix.exit_phase_digest,
2914 prefix.source_gains, prefix.source_losses);
2915 for (std::size_t offset = 0; offset < next - base; ++offset)
2916 consumer.fold_sealed_exit_phase(adapter_.trade_exit_phase_[offset]);
2917 adapter_.trade_exit_phase_.erase(adapter_.trade_exit_phase_.begin(),
2918 adapter_.trade_exit_phase_.begin() + (next - base));
2921 adapter_.observe_terminal_receipts();
2934 if (!
skip_quiet(QuietHook::BracketLegs, adapter_.pending_bracket_legs_.empty())) {
2935 adapter_.flush_pending_bracket_legs({},
false,
2938 internal::AmbientEmaSeedingScope ema_scope(ambient, chart_ema_na_warmup_);
2939 internal::AmbientBarContextScope bar_scope(ambient, pine_bar_index(),
2940 scheduler_.bar_index_offset());
2942 ? 0 : adapter_.source_entry_slot_count();
2946 release_script_average_price();
2951 adapter_.set_configuration(config_);
2952 if (capturing) selected_finish_evaluation();
2953 if (temporary_index) {
2954 --source_bar_index_;
2955 bar_index_ = previous_bar_index;
2956 barstate_islast_ = previous_barstate_islast;
2958 finish_adapter_evaluation();
2959 if (advance_source_index) {
2960 scheduler_mark_report_point(bar.timestamp);
2961 prev_bar_timestamp_ = bar.timestamp;
2968void source::PineStrategyHost::finish_adapter_evaluation() {
2972 if (!
skip_quiet(QuietHook::PendingCloses, adapter_.close_batch_callsites_.empty()))
2973 adapter_.flush_pending_closes();
2974#if PINEFORGE_PINE_QUIET_BAR_GATES
2979 if (adapter_.host_ !=
nullptr && adapter_.delayed_market_orders_.empty()
2980 && adapter_.pending_same_bar_commands_.empty() && adapter_.pending_entries_.empty()) {
2981 (void)
skip_quiet(QuietHook::PendingEntries,
true);
2982 adapter_.pending_same_bar_close_qty_ = 0.0;
2984 adapter_.flush_pending_entries();
2987 adapter_.flush_pending_entries();
2989 if (!
skip_quiet(QuietHook::BracketLegs, adapter_.pending_bracket_legs_.empty()))
2990 adapter_.flush_pending_bracket_legs();
2994 if (!
skip_quiet(QuietHook::RelativeExits, adapter_.anchored_relative_legs_.empty()
2995 && adapter_.pending_relative_exits_.empty())) {
2996 adapter_.anchor_relative_exits();
2998 adapter_.order_same_point_entries();
3001void source::PineStrategyHost::scheduler_publish_suppressed_tail(
const Bar& bar) {
3006 ++source_bar_index_;
3007 bar_index_ = source_bar_index_;
3008 barstate_islast_ =
false;
3009 internal::AmbientDayPartitionScope chart_day_partition(
3010 NativeExecutionConsumer::bound(*this).pump_ambient(),
3011 chart_day_partition_.empty() ?
nullptr : &chart_day_partition_);
3012 adapter_.begin_source_evaluation();
3015 selected_clear_suppressed_evaluation(bar.timestamp);
3016 adapter_.observe_terminal_receipts();
3017 scheduler_mark_report_point(bar.timestamp);
3018 prev_bar_timestamp_ = bar.timestamp;
3022bool source::PineStrategyHost::selected_begin_evaluation(std::int64_t label_ms) {
3024 if (pre_roll.state ==
nullptr)
return false;
3026 adapter_.capture_replay_risk_config(), *pre_roll.point);
3027 pre_roll.state->capturing =
true;
3031void source::PineStrategyHost::selected_finish_evaluation() noexcept {
3033 if (state ==
nullptr || !state->capturing)
return;
3034 state->end_config = config_;
3035 state->end_risk = adapter_.capture_replay_risk_config();
3037 state->terminal_point = *point;
3038 state->capturing =
false;
3041void source::PineStrategyHost::selected_clear_suppressed_evaluation(std::int64_t label_ms) {
3043 if (pre_roll.state ==
nullptr)
return;
3045 adapter_.capture_replay_risk_config(), *pre_roll.point);
3055void replay_intent(source::PineExecutionAdapter& adapter,
3056 const source::detail::IntentEntry& row) {
3058 row.qty, row.comment, row.oca_name, row.oca_type, row.qty_type);
3059 adapter.entry(row.id, row.is_long, row.limit_price, row.stop_price, row.qty, row.comment,
3060 row.oca_name, row.oca_type, row.qty_type);
3062void replay_intent(source::PineExecutionAdapter& adapter,
3063 const source::detail::IntentClose& row) {
3065 adapter.close(row.id, row.comment, row.qty, row.qty_percent, row.immediately);
3067void replay_intent(source::PineExecutionAdapter& adapter,
3068 const source::detail::IntentCloseToken& row) {
3070 row.immediately, row.callsite_token);
3071 adapter.close(row.id, row.comment, row.qty, row.qty_percent, row.immediately,
3072 row.callsite_token);
3074void replay_intent(source::PineExecutionAdapter& adapter,
const source::detail::IntentCloseAll&) {
3076 adapter.close_all();
3078void replay_intent(source::PineExecutionAdapter& adapter,
3079 const source::detail::IntentExit& row) {
3081 row.trail_points, row.trail_offset, row.trail_price,
3082 row.qty_percent, row.comment, row.qty, row.oca_name,
3083 row.profit_ticks, row.loss_ticks);
3084 adapter.exit(row.id, row.from_entry, row.limit_price, row.stop_price, row.trail_points,
3085 row.trail_offset, row.trail_price, row.qty_percent, row.comment, row.qty,
3086 row.oca_name, row.profit_ticks, row.loss_ticks);
3088void replay_intent(source::PineExecutionAdapter& adapter,
3089 const source::detail::IntentExitCancelBracket& row) {
3091 adapter.exit_cancel_bracket(row.exit_id, row.from_entry, row.comment);
3093void replay_intent(source::PineExecutionAdapter& adapter,
3094 const source::detail::IntentCancel& row) {
3096 adapter.cancel(row.id);
3098void replay_intent(source::PineExecutionAdapter& adapter,
3099 const source::detail::IntentCancelAll&) {
3101 adapter.cancel_all();
3103void replay_intent(source::PineExecutionAdapter& adapter,
3104 const source::detail::IntentOrder& row) {
3106 row.oca_name, row.oca_type);
3107 adapter.order(row.id, row.is_long, row.qty, row.limit_price, row.stop_price, row.oca_name,
3110void replay_intent(source::PineExecutionAdapter& adapter,
3111 const source::detail::IntentRiskDirection& row) {
3113 adapter.set_risk_direction(row.direction);
3115void replay_intent(source::PineExecutionAdapter& adapter,
3116 const source::detail::IntentRiskMaxConsLossDays& row) {
3117 adapter.set_risk_max_cons_loss_days(row.value);
3119void replay_intent(source::PineExecutionAdapter& adapter,
3120 const source::detail::IntentRiskMaxDrawdown& row) {
3122 adapter.set_risk_max_drawdown(row.value, row.percent);
3124void replay_intent(source::PineExecutionAdapter& adapter,
3125 const source::detail::IntentRiskMaxIntradayLoss& row) {
3127 adapter.set_risk_max_intraday_loss(row.value, row.percent);
3130void replay_intent(source::PineExecutionAdapter& adapter,
3131 const source::detail::IntentRiskMaxIntradayFilledOrders& row) {
3132 if (row.limit > 0) adapter.cap.adopt_script_count();
3133 adapter.cap = row.limit;
3135void replay_intent(source::PineExecutionAdapter& adapter,
3136 const source::detail::IntentRiskMaxPositionSize& row) {
3138 adapter.set_risk_max_position_size(row.value);
3145[[noreturn]]
void replay_invariant(
const char* what) {
3154class ReplayBracket {
3156 ReplayBracket(source::detail::PineIntentState& state, NativeExecutionConsumer& consumer,
3157 source::PineExecutionAdapter& adapter,
3158 const source::PineStrategyConfig& config) noexcept
3159 : state_(&state), consumer_(&consumer), adapter_(&adapter), config_(&config) {}
3160 ReplayBracket(
const ReplayBracket&) =
delete;
3161 ReplayBracket& operator=(
const ReplayBracket&) =
delete;
3162 ~ReplayBracket() noexcept {
3163 state_->replay_point =
nullptr;
3164 consumer_->end_open_activation();
3165 adapter_->set_configuration(*config_);
3169 source::detail::PineIntentState* state_;
3170 NativeExecutionConsumer* consumer_;
3171 source::PineExecutionAdapter* adapter_;
3172 const source::PineStrategyConfig* config_;
3183void source::PineStrategyHost::selected_replay_at_open(
const Bar& bar,
3184 const NativeDecisionContext&) {
3185 pf_selected_window_config_v1 window{};
3186 if (!NativeExecutionConsumer::selected_window_admitted(*
this, &window)
3187 || bar.timestamp < window.start_ms) {
3191 if (state ==
nullptr) replay_invariant(
"selected replay without its intent state");
3192 if (state->replayed)
return;
3193 if (!state->has_evaluation) {
3194 state->replayed =
true;
3199 if (state->capturing) replay_invariant(
"selected replay inside a pre-roll capture bracket");
3200 if (state->label_ms >= window.start_ms)
3201 replay_invariant(
"selected replay of an evaluation labelled inside the window");
3203 replay_invariant(
"selected replay over a held position");
3204 if (adapter_.coof_recalc_active_)
3205 replay_invariant(
"selected replay inside a calc_on_order_fills recalculation");
3206 if (!adapter_.live_handles_.empty() || !adapter_.delayed_market_orders_.empty()
3207 || !adapter_.pending_same_bar_commands_.empty() || !adapter_.pending_entries_.empty()
3208 || !adapter_.close_batch_callsites_.empty() || !adapter_.pending_bracket_legs_.empty()
3209 || !adapter_.anchored_relative_legs_.empty()
3210 || !adapter_.pending_relative_exits_.empty()) {
3211 replay_invariant(
"selected replay over live requests or queued source commands");
3214 state->replayed =
true;
3215 NativeExecutionConsumer& consumer = NativeExecutionConsumer::bound(*
this);
3216 if (!consumer.begin_open_activation(*
this))
3217 replay_invariant(
"selected replay without an open activation scope");
3218 const ReplayBracket bracket(*state, consumer, adapter_, config_);
3219 const auto stop_if_failed = [
this] {
3221 replay_invariant(
"selected replay stopped by a native failure");
3229 state->replay_point = &state->terminal_point;
3231 adapter_.restore_replay_risk_config(state->start_risk);
3232 adapter_.set_configuration(state->start_config);
3233 adapter_.begin_source_evaluation();
3234 adapter_.observe_terminal_receipts();
3236 if (!
skip_quiet(QuietHook::BracketLegs, adapter_.pending_bracket_legs_.empty())) {
3237 adapter_.flush_pending_bracket_legs({},
false,
3244 state->replay_point = &row.point;
3245 adapter_.set_configuration(row.config);
3246 std::visit([
this](
const auto& payload) { replay_intent(adapter_, payload); },
3253 state->replay_point = &state->terminal_point;
3254 adapter_.set_configuration(state->end_config);
3256 adapter_.restore_replay_risk_config(state->end_risk);
3257 finish_adapter_evaluation();
3265 const NativeDecisionContext& terminal = state->terminal_point.decision;
3266 const bool bound = adapter_.host_ !=
nullptr;
3267 const int bar_index = adapter_.projection_bar_index(terminal);
3268 std::size_t explicit_entries = 0;
3269 if (adapter_.config_.process_orders_on_close && adapter_.config_.pyramiding == 0
3270 && adapter_.modeled_input()) {
3271 for (
const auto& handle : adapter_.live_handles_) {
3272 const auto found = adapter_.placement_.find(handle.incarnation);
3273 if (found != adapter_.placement_.end() && found->second.opening
3275 && found->second.projection_created_bar ==
bar_index
3276 && std::isfinite(found->second.requested_qty)
3277 && found->second.requested_qty > 0.0) {
3282 if (!
skip_quiet(QuietHook::TerminalExplicitMarket, bound && explicit_entries < 2))
3283 adapter_.apply_terminal_explicit_market_policy(terminal,
false);
3288 state->rows.clear();
3301void source::PineStrategyHost::scheduler_mark_report_point(std::int64_t script_bar_ts) {
3305void source::PineStrategyHost::scheduler_record_broker_hash() {
3306 if (!broker_state_hash_recording_)
return;
3307 const std::uint64_t
execution = execution_consumer().continuation_hash();
3310 as_native_consumer(execution_consumer()).request_core().rethrow_book_read_failure();
3311 last_script_continuation_hash_ =
execution;
3312 last_script_continuation_valid_ =
true;
3313 broker_state_hashes_.push_back(broker_state_hash());
3317 return adapter_.core_sizes_default_opening(is_long);
3334 syminfo_.timezone, syminfo_.session);
3337 syminfo_.timezone, script_tf_);
3358constexpr std::int64_t kTimeOffsetMinuteMs = 60'000;
3361constexpr int kTimeOffsetCalendarMinutes = 16 * 24 * 60;
3363constexpr int kTimeOffsetBarsPerStep = 100'000;
3368 const std::string& tf,
3369 const std::string& session,
3370 const std::string& tz,
3371 int timeframe_bars_back,
3373 if (
is_na(bar_open_ms) ||
is_na(bars_back) ||
is_na(timeframe_bars_back))
3375 const std::string& sym_tz = syminfo_.timezone;
3376 const std::string& sym_session = syminfo_.session;
3378 const bool input_is_chart =
scheduler_.retained_input_is_chart();
3382 const auto chart_open_of = [&](std::int64_t input_ms) -> std::int64_t {
3383 return input_is_chart ? input_ms
3384 :
pine_time(input_ms, script_tf_,
"",
"", script_tf_, sym_tz,
3395 const std::int64_t chart_step_ms =
static_cast<std::int64_t
>(script_tf_seconds_) * 1000;
3398 && script_tf_seconds_ == 86400));
3399 const auto chart_close_of = [&](std::int64_t open_ms) -> std::int64_t {
3401 return daily_chart || chart_step_ms <= 0 ? day->second
3402 : std::min(open_ms + chart_step_ms, day->second);
3404 return daily_chart ?
pine_time_close(open_ms, script_tf_,
"",
"", script_tf_, sym_tz,
3407 open_ms, script_tf_, sym_session, sym_tz, script_tf_);
3409 const auto chart_slot_of = [&](std::int64_t ms) -> std::int64_t {
3410 return pine_time(ms, script_tf_,
"",
"", script_tf_, sym_tz, sym_session);
3414 const auto chart_open_from = [&](std::int64_t at) -> std::int64_t {
3415 if (calendar_chart) {
3418 auto it = std::lower_bound(
input.begin(),
input.end(), at,
3419 [](
const Bar& bar, std::int64_t value) {
3420 return bar.timestamp < value;
3422 while (!input_is_chart && it !=
input.end() && chart_open_of(it->timestamp) < at)
3424 if (it !=
input.end())
return chart_open_of(it->timestamp);
3425 std::int64_t t = at;
3426 for (
int k = 0; k < kTimeOffsetCalendarMinutes; ++k, t += kTimeOffsetMinuteMs) {
3428 const std::int64_t slot = chart_slot_of(t);
3429 return is_na(slot) || slot < at ? t : slot;
3437 const auto chart_open_before = [&](std::int64_t before) -> std::int64_t {
3438 if (calendar_chart) {
3442 auto it = std::lower_bound(
input.begin(),
input.end(), before,
3443 [](
const Bar& bar, std::int64_t value) {
3444 return bar.timestamp < value;
3447 if (held && it !=
input.begin())
return chart_open_of(std::prev(it)->timestamp);
3448 std::int64_t t = before - kTimeOffsetMinuteMs;
3449 for (
int k = 0; k < kTimeOffsetCalendarMinutes; ++k, t -= kTimeOffsetMinuteMs) {
3451 const std::int64_t slot = chart_slot_of(t);
3452 return is_na(slot) || slot >= before ? t : slot;
3459 const auto next_chart_open = [&](std::int64_t open_ms) -> std::int64_t {
3464 auto it = std::upper_bound(
input.begin(),
input.end(), open_ms,
3465 [](std::int64_t value,
const Bar& bar) {
3466 return value < bar.timestamp;
3468 while (!input_is_chart && it !=
input.end() && chart_open_of(it->timestamp) <= open_ms)
3470 if (it !=
input.end())
return chart_open_of(it->timestamp);
3471 const std::int64_t end = chart_close_of(open_ms);
3472 return chart_open_from(
is_na(end) || end <= open_ms ? open_ms + 1 : end);
3478 const auto prior_chart_open = [&](std::int64_t open_ms) -> std::int64_t {
3479 auto it = std::lower_bound(
input.begin(),
input.end(), open_ms,
3480 [](
const Bar& bar, std::int64_t value) {
3481 return bar.timestamp < value;
3484 return chart_open_before(open_ms);
3485 while (it !=
input.begin()) {
3487 const std::int64_t prior = chart_open_of(it->timestamp);
3488 if (prior < open_ms)
return prior;
3493 std::int64_t chart_open = bar_open_ms;
3494 for (
int k = 0; k < -bars_back && !
is_na(chart_open); ++k)
3495 chart_open = next_chart_open(chart_open);
3496 for (
int k = 0; k < bars_back && !
is_na(chart_open); ++k)
3497 chart_open = prior_chart_open(chart_open);
3501 if ((tf.empty() || tf == script_tf_) && session.empty()) {
3502 for (
int k = 0; k < -timeframe_bars_back && !
is_na(chart_open); ++k)
3503 chart_open = next_chart_open(chart_open);
3504 for (
int k = 0; k < timeframe_bars_back && !
is_na(chart_open); ++k)
3505 chart_open = chart_open_before(chart_open);
3507 return close ? chart_close_of(chart_open) : chart_open;
3519 const auto calendar_open_of = [&](std::int64_t ms) -> std::int64_t {
3520 if (calendar_days) {
3523 return pine_time(ms, tf, session, tz, script_tf_, sym_tz, sym_session);
3525 const auto tf_close_of = [&](std::int64_t open_ms) -> std::int64_t {
3526 if (calendar_days) {
3529 return pine_time_close(open_ms, tf, session, tz, script_tf_, sym_tz, sym_session);
3531 const auto tf_open_of = [&](std::int64_t ms) -> std::int64_t {
3532 const std::int64_t open = calendar_open_of(ms);
3533 if (
is_na(open) || !session.empty()
3537 const std::int64_t first = chart_open_from(open);
3538 if (
is_na(first) || calendar_open_of(first) != open)
return open;
3539 const std::int64_t day =
pine_time(first,
"D",
"",
"", script_tf_, sym_tz, sym_session);
3540 return !
is_na(day) && day > open ? day : open;
3542 std::int64_t open = tf_open_of(chart_open);
3548 const int tf_seconds =
tf_to_seconds(tf.empty() ? script_tf_ : tf);
3550 if (timeframe_bars_back != 0 && tf_seconds > 0 && chart_seconds > 0
3551 && tf_seconds < chart_seconds) {
3552 const std::int64_t at = open -
static_cast<std::int64_t
>(timeframe_bars_back)
3553 * tf_seconds * 1000;
3554 const std::int64_t held = chart_open_before(at + 1);
3556 return close ? chart_close_of(held) : held;
3558 for (
int k = 0; k < -timeframe_bars_back && !
is_na(open); ++k) {
3560 const std::int64_t end = tf_close_of(open);
3561 std::int64_t at = chart_open_from(
is_na(end) || end <= open ? open + 1 : end);
3563 for (
int guard = 0; guard < kTimeOffsetBarsPerStep && !
is_na(at); ++guard) {
3564 next = tf_open_of(at);
3565 if (!
is_na(next) && next > open)
break;
3567 at = next_chart_open(at);
3571 for (
int k = 0; k < timeframe_bars_back && !
is_na(open); ++k) {
3573 std::int64_t at = chart_open_before(open);
3575 for (
int guard = 0; guard < kTimeOffsetBarsPerStep && !
is_na(at); ++guard) {
3576 prev = tf_open_of(at);
3577 if (!
is_na(prev) && prev < open)
break;
3579 at = chart_open_before(at);
3584 return close ? tf_close_of(open) : open;
3590 const bool recalculation_due =
scheduler_.coof_recalculation_due(event, context, *
this);
3591 if (!recalculation_due) record_applied_range_end();
3599 && !
adapter_.admission_journal.has_unfinished_events()) {
3608void source::PineStrategyHost::stage_symbol_feed_views() {
3611 "request data: symbol feed view table is out of sync");
3613 std::vector<const NativeInstrumentFeedView*> slots(
symbol_feed_views_.size(),
nullptr);
3616 if (!slot.present)
continue;
3617 descriptors[i] = {{slot.bars, slot.size}, slot.close_ms};
3620 NativeExecutionConsumer::bound(*this).stage_instrument_feed_views(
static OrderBirth chart_evaluation(int bar, int64_t timestamp)
The code carried beside a thrown exception.
The refusal of a read-only state query, or of a report-only write, that may no longer be answered.
const std::shared_ptr< const CommandObservation > & observation() const
void bind(std::shared_ptr< const CommandObservation > observation)
A thrown legacy setter's original message, propagated to native begin.
Base with a code: throw coded<std::invalid_argument>(code, {args}, text).
ScratchStatus status() const noexcept
std::uint64_t record_count() const noexcept override
ScratchStatus put(const ScratchByteSource &key, const ScratchByteSource &value, ScratchHandle &result) noexcept
bool transaction_active() const noexcept
The public native host: an abstract subclass of BacktestEngine with no PineScript on it.
Trade numeric_at(size_type global) const
std::string entry_id_at(size_type global) const
static std::uint64_t fold_exit_phase(std::uint64_t digest, std::uint8_t phase) noexcept
int observe_pending_count_v1() const override
compat::pine::Calculation fixture_cap_calculation() const
std::map< long long, double > symbol_calendar_opens_
std::map< std::string, RecordedSeries > recorded_series_
void clear_report_terminal_quote()
Series< double > & _src_volume_
std::int64_t foreign_input_index_
void attach_pine_execution_adapter()
int64_t chart_time_close() const
void set_syminfo_session(const std::string &)
PineExecutionAdapter adapter_
void hold_script_average_price(double average)
bool session_islastbar_regular_
std::optional< NativeMarginDecision > resolve_margin_requirement(const NativeMarginRequirementView &) const final
const std::pair< std::int64_t, std::int64_t > * symbol_calendar_day(std::int64_t ms) const
std::optional< double > resolve_margin_call_units(const NativeMarginCallView &) const final
void set_pine_risk_direction(int)
void prepare_native_begin(const NativeBeginArgs &) final
BarTime fixture_chart_time(std::int64_t timestamp_ms) const
bool & _src_series_active_
compat::pine::CapClock fixture_cap_clock() const
std::uint64_t source_callback_count_
void enable_pine_intraday_cap()
Series< double > & _src_hlcc4_
const PendingIntentView & pending_intent_view() const noexcept
const std::vector< FixtureIntentRow > & source_pending_view() const
void finish_applied_admission(const native_order::ExecutionAppliedEvent &, const NativeDecisionContext &)
void set_pine_risk_max_intraday_filled_orders(int)
long long symbol_calendar_days_
int pending_order_count() const
void on_native_tick(const Bar &, const NativeTickContext &) final
Series< double > & _src_open_
const Series< double > & source_input_series(const std::string &key, const Series< double > &fallback) const
Series< double > & _src_hl2_
int pending_order_effective_levels(int index, double *stop, double *limit, double *trail_activation) const
int observe_pending_effective_levels(int, double *, double *, double *) const override
double observe_trail_best_price_v1() const override
bool margin_call_enabled_
Series< double > & _src_low_
bool fixture_intraday_cap_latched()
void configure_pine_strategy(const PineStrategyConfig &)
void set_pine_risk_max_position_size(double)
bool is_last_tick() const noexcept
void set_pine_risk_max_cons_loss_days(int)
int observe_pending_level_resolved(int) const override
bool set_symbol_calendar(std::vector< std::pair< std::int64_t, std::int64_t > > sessions)
bool session_isfirstbar_regular_
virtual void on_native_applied_event(const native_order::ExecutionAppliedEvent &, const NativeDecisionContext &)
std::vector< BorrowedFeedSlot > symbol_feed_views_
int64_t security_range_start_ms_
const Series< double > & source_series(const std::string &) const
void install_symbol_calendar_metadata()
void apply_realtime_tail_horizon(const Bar *bars, int n, bool script_bar_geometry)
int64_t chart_bar_close_ms(int64_t stamp) const
std::vector< std::pair< std::int64_t, std::int64_t > > symbol_calendar_
void on_native_applied(const native_order::ExecutionAppliedEvent &, const NativeDecisionContext &) final
std::vector< admission::Field > market_admission_fields() const
PineStrategyHost(compat::pine::CapAttachment cap=compat::pine::CapAttachment::None)
int pending_order_level_resolved(int index) const
void refresh_aux_security_digest() noexcept
int observe_last_bar_dual_entry_path_v1() const override
int pine_last_bar_index() const
void set_trade_scratch(detail::CommandScratchStore &)
void on_native_run_begin() final
void release_script_average_price() noexcept
std::uint64_t broker_state_hash_projection() const override
StrategyOverrides override_
Series< double > & _src_close_
void fixture_retain_all_events()
bool security_range_start_na_warmup_
PineStrategyConfig config_
std::vector< FixtureIntentRow > source_pending_view_cache_
bool margin_check_allowed(const NativeMarginCheckPoint &) const final
std::optional< double > resolve_anchored_level(const NativeAnchoredLevelView &) const final
void freeze_script_position_view()
std::uint64_t fixture_applied_receipt_count() const
Series< double > & _src_hlc3_
bool chart_ema_na_warmup_
bool report_terminal_quote_applied(const ReportC &) const
std::vector< NativeInstrumentFeed > symbol_feeds_
int last_bar_dual_entry_path() const
std::int64_t symbol_calendar_open_before(std::int64_t before) const
double prev_chart_close() const
int source_last_bar_index_
void on_native_input(const Bar &, const NativeInputContext &) final
double live_position_size() const override
std::vector< int > foreign_security_series_
std::map< long long, double > symbol_calendar_closes_
void set_pine_risk_max_intraday_loss(double, bool)
bool script_average_held_
int short_seed_collision_role_v1(native_order::RequestHandle) const noexcept
bool history_advances_new_bar() const noexcept
int pine_bar_index() const
void on_native_bar_open(const Bar &, const NativeDecisionContext &) final
bool set_report_terminal_quote(std::int64_t time_ms, double close)
bool source_prepare_failed_
bool historical_security_lookahead_projection_
bool is_first_tick() const noexcept
void set_pine_risk_max_drawdown(double, bool)
double open_entry_fees_paid() const
double margin_liquidation_price() const
int realtime_tail_horizon_bars_
bool probe_suppress_tail_logic_
MarketAdmissionJournal & market_admission_journal()
double precommit_held_units_
Series< double > & _src_ohlc4_
static detail::ScratchKeyOrder trade_scratch_order() noexcept
NativePrecommitVerdict validate_execution_precommit(const NativePrecommitView &) const final
void clear_script_position_view()
int64_t pine_time_offset(int64_t bar_open_ms, int bars_back, const std::string &tf, const std::string &session, const std::string &tz, int timeframe_bars_back, bool close) const
const bool & is_last_tick_
double script_mark_price(double price) const
void on_native_bar(const Bar &, const NativeDecisionContext &) final
int observe_probe_fill_qty(int, double, double *, int *, int *) const override
bool source_configuration_captured_
void set_syminfo_metadata(const std::string &, double) override
void set_strategy_override(const StrategyOverrides &)
Series< double > & _src_high_
int observe_pending_copy_v1(int, pf_pending_order_v1_t *) const override
bool adapter_core_sizes_default_opening(bool is_long) const
native_order::ExecutionTerms resolve_execution_terms(const NativeExecutionTermsFacts &) const final
void on_native_sub_bar(const Bar &, const NativeDecisionContext &) final
int probe_fill_qty(int index, double fill_price, double *qty, int *close_only, int *partition) const
double signed_position_size() const
void on_native_recalculate(const Bar &, const NativeDecisionContext &, NativeCalculationReason, const native_order::ExecutionAppliedEvent *) final
std::int64_t symbol_calendar_open_from(std::int64_t at) const
const ForeignSecuritySite * foreign_context_
std::map< int, ForeignSecuritySite > foreign_security_sites_
void source_stream_entry_comment(const PyramidEntry &, std::string &) const override
uint64_t sequence(const Event &event)
ScratchStatus finish(CommandScratchStore &store, ScratchStatus status) noexcept
std::uint64_t slots(std::uint64_t keys)
void bind_id_pair_scratch_tables(source::PineExecutionAdapter &, const std::shared_ptr< ScratchSlab > &)
std::uint64_t ScratchHandle
constexpr ScratchHandle scratch_no_handle
int64_t symbol_session_time_close(int64_t bar_ms, const std::string &tf, const std::string &session, const std::string &tz, const std::string &chart_tf)
void bind_tree_scratch_tables(source::PineExecutionAdapter &, const std::shared_ptr< ScratchSlab > &)
void rethrow_carried_scratch_failure(const ReportScratchIoError *failure)
NativeRunPhase
Which driving is Running: a batch run(), a stream's internal warmup, or its realtime leg.
NativeCalculationReason
Why the kernel is asking the host to calculate.
NativePrecommitVerdict
The host is consulted before generic opening-margin admission.
NativeLifecycleKind
Where a host stands.
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)
NativePathOrder
Generic ordering for a modeled OHLC path.
std::optional< Plan > plan(double signed_position, Reduce request) noexcept
NativePhysicalPosition run_position(const NativeStrategyHost &host)
void take_run_facts(NativeExecutionConsumer &consumer, const PineExecutionAdapter &adapter, std::uint64_t run)
const NativeCurrentPointView * callback_point(const NativeStrategyHost &host)
ScriptRuleSwitches & script_rule_switches() noexcept
PineIntentState * selected_intents(const NativeStrategyHost &host) noexcept
ScratchKeyOrder exit_scratch_key_order(ExitScratchOrderStats *stats=nullptr) noexcept
void reset_intent_evaluation(PineIntentState &state, std::int64_t label_ms, const PineStrategyConfig &config, const PineReplayRiskConfig &risk, const NativeCurrentPointView &point) noexcept
bool run_aggregates_input_bars(const NativeExecutionConsumer &consumer, const void *owner, std::uint64_t run)
const NativeRunSpec * run_spec(const NativeStrategyHost &host)
NativeExecutionConsumer & run_consumer(const NativeStrategyHost &host)
bool skip_quiet(QuietHook hook, bool quiet) noexcept
NativeRunPhase run_phase(const NativeStrategyHost &host)
PreRollEvaluation selected_pre_roll_evaluation(const NativeStrategyHost &host, std::int64_t label_ms)
void adopt_selected_intents(NativeStrategyHost &host)
const NativeCurrentPointView * intent_live_point(const NativeStrategyHost &host) noexcept
void capture_risk_row(const NativeStrategyHost &host, const PineStrategyConfig &config, Payload &&payload)
NativeLifecycleKind run_kind(const NativeStrategyHost &host)
void admit_strategy_declaration(const PineStrategyConfig &config)
void admit_strategy_exit(const std::string &id, const std::string &from_entry, double limit_price, double, double, double trail_offset, double trail_price, double, const std::string &comment, double, const std::string &oca_name, double profit_ticks, double loss_ticks)
void admit_strategy_close_token(const std::string &id, const std::string &comment, double qty, double qty_percent, bool immediately, std::uint64_t)
constexpr char kMarginCallLabel[]
double tv_money_round(double value)
constexpr char kIntradayLossComment[]
void admit_strategy_cancel(const std::string &id)
void admit_pine_risk_max_drawdown(double value, bool)
void assert_admitted_pine_replay_risk_config(const PineReplayRiskConfig &snapshot)
void admit_strategy_close_all() noexcept
void admit_pine_risk_max_intraday_loss(double value, bool)
const char * instrument_feed_field_name(NativeRunSpecField field) noexcept
constexpr char kFillCapCommentPrefix[]
std::shared_ptr< TimeframeAggregator::PeriodSource > make_multi_day_period_source(const MultiDayFeeds &feeds)
The N-day partition of feeds, or null where TradingView's rule yields none: fewer than two days,...
void admit_strategy_entry(const std::string &id, bool, double limit_price, double stop_price, double, const std::string &comment, const std::string &oca_name, int, int)
void admit_pine_risk_max_position_size(double value)
void admit_strategy_exit_cancel_bracket(const std::string &exit_id, const std::string &from_entry, const std::string &comment)
void admit_strategy_close(const std::string &id, const std::string &comment, double, double, bool)
void admit_strategy_cancel_all() noexcept
void admit_strategy_order(const std::string &id, bool, double, double limit_price, double stop_price, const std::string &oca_name, int)
int64_t pine_time(int64_t bar_ms, const std::string &tf, const std::string &session, const std::string &tz, const std::string &chart_tf, const std::string &syminfo_tz=std::string())
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.
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={})
bool session_in_market(const std::string &session, const std::string &tz, int64_t bar_ms)
@ engine_unclassified_error
@ symbol_metadata_rejected
RunFailureValue run_failure_value_of(const BacktestEngine &engine) noexcept
The recorded value under the same rule (none when there is no failure).
bool session_trades_at(const std::string &session, const std::string &tz, int64_t bar_ms)
bool is_na(const Line &h)
RunFailureValue classify_run_failure(const std::exception &error) noexcept
The code an exception carries: a coded exception's own; else the safety net by type (std::bad_alloc -...
std::string detect_timeframe(const Bar *bars, int n, int max_samples=100)
Detect the timeframe string from an array of bars by computing the median timestamp delta and mapping...
int64_t calendar_day_open_local_ms(int64_t bar_ms, const std::string &tz)
bool session_in_premarket(const std::string &session, const std::string &tz, int64_t bar_ms)
std::vector< RunFailureArg > RunFailureArgs
admission::Journal MarketAdmissionJournal
static bool same_bar_exit_group_possible(const TradeLedger &trades)
bool session_in_postmarket(const std::string &session, const std::string &tz, int64_t bar_ms)
int64_t pine_time_close(int64_t bar_ms, const std::string &tf, const std::string &session, const std::string &tz, const std::string &chart_tf, const std::string &syminfo_tz=std::string())
pf_equity_point_t * equity_curve
A validated code and its canonical arguments: the JSON object with its keys sorted and no whitespace,...
execution::CloseCause close_cause
Ephemeral read-only facts of one anchored-leg materialization (L7b), offered to the host exactly once...
Borrowed begin-call facts.
const void * overrides_opaque
const SymInfo * syminfo
The rich run overload's symbol metadata is borrowed only for this callback.
Read-only owning-value facts for one candidate.
Accepted input facts presented before the generic consumer aggregates the bar into its script interva...
Ephemeral factual view of one kernel-issued liquidation before its units are fixed.
Ephemeral factual view of one kernel check point, offered to the host before the check runs.
Ephemeral factual view of the numbers the kernel is about to compare, at one check point,...
Ephemeral factual view of one prepared execution before any physical effect.
What configure_native answers: the status and, on refusal, the first error field validate_native_run_...
One accepted realtime print before native matching at its current decision point.
NativePriceProvenance provenance
NativeDriverStatistics driver_statistics
int64_t script_bar_open_ms
NativeCoordinate coordinate
uint64_t sub_bars_processed
uint64_t sample_ticks_processed
One committed execution: the definition, the cursor, the resolved price and units,...
std::size_t closed_trade_count
const RequestHandle & handle() const noexcept
std::size_t first_trade_index
One complete setup value, staged/copied by NativeStrategyHost before it is applied at begin.
std::vector< NativeInstrumentFeed > instrument_feeds
Opt-in feeds of other instruments.
native_order::RunIdentity identity
What a run holds of the symbol the N-day request reads.
int days
N of the request, at least 2.
bool process_orders_on_close
bool close_entries_rule_any
double frozen_market_transaction_units
double paired_flat_market_transaction_qty
double affordability_placement_equity
std::uint64_t incarnation
double default_stop_placement_equity
double default_stop_sizing_price
double frozen_market_own_units
MarketAdmissionDraft market_admission
std::int64_t paired_flat_market_peer_seq
double frozen_default_qty
double default_stop_placement_qty
bool over_pyramiding_cap_at_placement
double default_stop_placement_signal_close
std::vector< std::int64_t > account_fx_effective_from_ms
int process_orders_on_close