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();
1524 if (key ==
"series_origin_ms" && (!std::isfinite(value) || value <= 0.0))
1526 BacktestEngine::set_syminfo_metadata(key, value);
1532 if (key.compare(0, 16,
"symbol_calendar_") == 0) {
1533 if (key ==
"symbol_calendar_days") {
1534 if (std::isfinite(value) && value >= 0.0 && value <= 1e9) {
1543 for (
const auto& [prefix, days] : {std::pair<const char*, std::map<long long, double>*>{
1546 const std::size_t n = std::char_traits<char>::length(prefix);
1547 if (key.size() <= n || key.size() - n > 9 || key.compare(0, n, prefix) != 0)
continue;
1548 long long index = 0;
1550 for (std::size_t i = n; i < key.size(); ++i) {
1551 if (key[i] <
'0' || key[i] >
'9') {
1555 index = index * 10 + (key[i] -
'0');
1557 if (digits) (*days)[index] = value;
1561 if (key ==
"bar_index_offset") {
1562 scheduler_.set_bar_index_offset(std::isfinite(value)
1563 ?
static_cast<int>(std::llround(value)) : 0);
1567 if (key ==
"series_origin_ms" && std::isfinite(value) && value > 0.0) {
1572 if (key ==
"security_range_start_na_warmup") {
1573 if (std::isfinite(value) && value > 0.0) {
1581 if (key ==
"chart_ema_na_warmup")
1583 if (key ==
"historical_security_lookahead_projection")
1585 if (key ==
"margin_long" &&
config_.margin_long == 100.0)
1586 config_.margin_long = (std::isfinite(value) && value > 0.0) ? value : 100.0;
1587 if (key ==
"margin_short" &&
config_.margin_short == 100.0)
1588 config_.margin_short = (std::isfinite(value) && value > 0.0) ? value : 100.0;
1590 adapter_.priority.metadata(key, value);
1595 if (NativeExecutionConsumer::selected_query_closed(*
this) || time_ms < 0
1596 || time_ms > 9007199254740991LL || !std::isfinite(close) || close <= 0.0)
return false;
1597 report_quote_time_ms_ = time_ms;
1598 report_quote_close_ = close;
1603 if (NativeExecutionConsumer::selected_query_closed(*
this)) {
1606 report_quote_time_ms_ = -1;
1607 report_quote_close_ = std::numeric_limits<double>::quiet_NaN();
1624 int index,
double fill_price,
double* qty,
int* close_only,
int* partition)
const {
1625 return pending_intent_view().probe_fill_qty(index, fill_price, qty, close_only, partition);
1633 int index,
double* stop,
double* limit,
double* trail_activation)
const {
1638 return adapter_.pending_intent_view().trail_best_price();
1641void source::PineStrategyHost::adapter_label_bracket_trades(
1646 for (std::size_t offset = 0; offset <
event.closed_trade_count; ++offset) {
1647 const std::size_t index =
event.first_trade_index + offset;
1648 if (index >= trades_.size())
continue;
1649 adapter_.invalidate_exit_group(index); trades_.modify(index, [&](
Trade& trade) { trade.
exit_from_bracket = from_bracket; });
1653bool source::PineStrategyHost::adapter_has_open_entry_id(
1654 const std::string&
id)
const {
1655 return std::any_of(pyramid_entries_.begin(), pyramid_entries_.end(),
1656 [&](
const PyramidEntry& row) { return row.entry_id == id && row.qty > 0.0; });
1659const std::vector<source::PineStrategyHost::FixtureIntentRow>&
1664 +
adapter_.pending_coof_requests_.size()
1665 +
adapter_.delayed_market_orders_.size()
1669 switch (snapshot.family) {
1677 type = FixtureIntentKind::EXIT;
1680 type = FixtureIntentKind::RAW_ORDER;
1687 ?
"__close__" + snapshot.source_id
1688 : (snapshot.frozen_market_targeted_close ?
label : snapshot.source_id);
1691 && !std::isfinite(snapshot.exit_levels.limit)
1692 && std::isfinite(snapshot.exit_levels.stop)
1693 && std::isnan(snapshot.requested_qty)
1695 &&
config_.default_qty_value <= 100.0;
1696 const double absent = std::numeric_limits<double>::quiet_NaN();
1702 row.
is_long = snapshot.is_long;
1704 ? snapshot.requested_qty
1705 : (std::isfinite(snapshot.projection_remaining_qty)
1706 ? snapshot.projection_remaining_qty : snapshot.requested_qty);
1708 row.
created_bar = snapshot.projection_created_bar;
1709 row.
created_seq =
static_cast<std::int64_t
>(snapshot.source_sequence);
1716 ? snapshot.projection_default_stop_equity : absent;
1718 ? snapshot.projection_default_stop_signal_close : absent;
1723 && snapshot.frozen_market_instruction
1724 && std::isfinite(snapshot.requested_qty)) {
1725 auto observation = std::make_shared<admission::CommandObservation>();
1726 observation->command = snapshot.command_ordinal;
1727 observation->kind = admission::CommandKind::Entry;
1731 observation->id = snapshot.source_id;
1732 observation->requested_quantity = snapshot.requested_qty;
1733 observation->quantity_type = snapshot.qty_type;
1734 observation->buy = snapshot.is_long;
1735 observation->prices = {snapshot.exit_levels.limit, snapshot.exit_levels.stop};
1736 observation->oca_name = snapshot.oca_name;
1737 observation->oca_type = snapshot.oca_type;
1738 auto& configuration = observation->configuration;
1739 configuration.process_on_close =
config_.process_orders_on_close;
1740 configuration.calc_on_fills =
config_.calc_on_order_fills;
1741 configuration.slippage =
config_.slippage;
1742 configuration.pyramiding =
config_.pyramiding;
1743 configuration.default_quantity_type =
config_.default_qty_type;
1744 configuration.default_quantity_value =
config_.default_qty_value;
1745 configuration.long_margin =
config_.margin_long;
1746 configuration.short_margin =
config_.margin_short;
1747 configuration.commission_value =
config_.commission_value;
1748 configuration.commission_type =
config_.commission_type;
1749 configuration.pointvalue = staged_configuration().syminfo.pointvalue;
1750 configuration.fx = snapshot.sizing.fx;
1751 configuration.quantity_step = staged_configuration().quantity_grid
1752 ? *staged_configuration().quantity_grid : 0.0;
1753 configuration.mintick = staged_configuration().syminfo.mintick;
1756 observation->placement_cycle = snapshot.placement_cycle;
1757 observation->held_quantity = 0.0;
1758 observation->held_entries = 0;
1759 observation->realized_equity = snapshot.sizing.equity;
1760 observation->placement_equity = snapshot.sizing.equity;
1761 observation->signal_close = snapshot.sizing.price;
1762 observation->quantized_fixed_quantity =
1763 snapshot.frozen_market_own_units;
1765 snapshot.requested_qty, snapshot.sizing.equity,
1766 snapshot.sizing.price, snapshot.sizing.mark, snapshot.sizing.fx};
1771 for (
const auto& command :
adapter_.pending_same_bar_commands_) {
1776 if (
config_.process_orders_on_close
1778 && !command.snapshot.birth.at_terminal_fill()) {
1781 append(command.snapshot, command.request.label);
1783 for (
const auto& pending :
adapter_.pending_entries_)
1784 append(pending.snapshot, pending.request.label);
1785 for (
const auto& delayed :
adapter_.delayed_market_orders_)
1786 append(delayed.snapshot, delayed.request.label);
1787 for (
const auto& leg :
adapter_.pending_bracket_legs_)
1788 append(leg.snapshot, leg.request.label);
1789 for (
const auto& pending :
adapter_.pending_coof_requests_)
1790 append(pending.snapshot, pending.request.label);
1791 for (
const auto& shadow :
adapter_.source_shadow_pending_)
1792 append(shadow.snapshot, shadow.label);
1793 for (
const auto& handle :
adapter_.live_handles_) {
1794 const auto found =
adapter_.placement_.find(handle.incarnation);
1795 if (found ==
adapter_.placement_.end())
continue;
1796 if (
config_.process_orders_on_close
1799 && !found->second.birth.at_terminal_fill()) {
1802 append(found->second, found->second.source_id);
1810void source::PineStrategyHost::project_short_seed_report_rows(
1814 || event.
handle() == plan.final_short) {
1817 std::optional<PlacementSnapshot> placement_snapshot;
1818 if (
const auto placement = adapter_.placement_.find(event.
handle().
incarnation);
1819 placement != adapter_.placement_.end()) {
1820 placement_snapshot = placement->second;
1823 || (placement_snapshot->from_entry != plan.seed_id
1824 && placement_snapshot->source_id != plan.seed_id)) {
1827 for (std::size_t index = trades_.base(); index < trades_.size(); ++index) {
1828 Trade trade = trades_[index];
1829 if (trade.entry_incarnation ==
plan.materialize_long.incarnation
1830 && trade.entry_id ==
plan.materialize_label) {
1831 trade.entry_incarnation =
plan.final_short.incarnation;
1832 adapter_.invalidate_exit_group(index); trades_.replace(index, trade);
1835 const std::size_t begin =
event.first_trade_index;
1836 const std::size_t
end = begin +
event.closed_trade_count;
1837 for (std::size_t index = begin; index <
end && index < trades_.size(); ++index) {
1838 if (trades_[index].entry_incarnation ==
plan.final_short.incarnation
1839 && trades_[index].entry_id ==
plan.final_short_id) {
1840 adapter_.invalidate_exit_group(index); trades_.modify(index, [&](Trade& row) {
1841 row.entry_incarnation =
plan.materialize_long.incarnation;
1845 adapter_.short_seed_.report_swap_pending =
false;
1848void source::PineStrategyHost::scheduler_prepare_script_run(
1850 int expected_script_bars,
bool script_bar_geometry) {
1851 if (
const auto state = native_state(); state.spec) {
1852 input_tf_ = state.spec->timeframe_undetected ?
"" : state.spec->input_tf;
1853 script_tf_ = state.spec->timeframe_undetected ?
"" : state.spec->script_tf;
1856 prepare_script_run(bars.empty() ?
nullptr : bars.data(),
static_cast<int>(bars.size()),
1858 last_bar_index_ = expected_script_bars - 1;
1859 last_bar_time_ = bars.empty() ? 0 : bars.back().timestamp;
1860 apply_realtime_tail_horizon(
1861 bars.empty() ?
nullptr : bars.data(),
static_cast<int>(bars.size()),
1862 script_bar_geometry);
1863 source_last_bar_index_ = last_bar_index_;
1866thread_local const source::PineStrategyHost* source::PineRuntimeSecurityScope::owner_ =
nullptr;
1867thread_local source::PineRuntimeSecurityCursor* source::PineRuntimeSecurityScope::cursor_ =
nullptr;
1871 : previous_owner_(owner_), previous_cursor_(cursor_) {
1872 owner_ = &owner; cursor_ = &cursor;
1875 owner_ = previous_owner_; cursor_ = previous_cursor_;
1879 return owner_ == &owner ? cursor_ :
nullptr;
1882 const std::string& selected)
const {
1883 if (!selected.empty())
return selected;
1885 return parent ? parent->timeframe : script_tf_;
1889 const std::string& selected,
bool lookahead,
bool gaps,
bool lower_array,
1890 bool heikinashi)
const {
1891 if (input_tf_.empty())
1894 "request.security cannot infer input timeframe; pass input_tf explicitly");
1897 const char* api = lower_array ?
"request.security_lower_tf" :
"request.security";
1900 std::string(api) +
": invalid timeframe literal '" +
timeframe +
"'");
1906 cursor.
timezone = syminfo_.timezone;
1907 cursor.
session = syminfo_.session;
1915 if (lower_array && chart_seconds <= 0)
1918 {{
"reason",
"lower_tf_script_unknown"}},
1919 "request.security_lower_tf: script timeframe is unknown");
1920 if (lower_array && (requested_seconds <= 0 || requested_seconds >= chart_seconds))
1923 {{
"reason",
"lower_tf_not_finer_than_script"}, {
"script_tf", script_tf_}},
1924 "request.security_lower_tf: requested timeframe must be finer than the chart timeframe");
1925 if (lower_array && chart_seconds % requested_seconds != 0)
1928 {{
"reason",
"lower_tf_not_divisor_of_script"}, {
"script_tf", script_tf_}},
1929 "request.security_lower_tf: requested timeframe must evenly divide the chart timeframe");
1930 if (requested_seconds > 0 && input_seconds > 0 && requested_seconds < input_seconds) {
1936 {{
"reason",
"lower_tf_not_divisor_of_input"}, {
"input_tf", input_tf_}},
1937 "request.security_lower_tf: requested timeframe is not an integer divisor of input");
1938 }
else if (chart_seconds == -1 || input_seconds < chart_seconds) {
1943 {{
"reason",
"finer_than_input"}, {
"input_tf", input_tf_}},
1944 "request.security: requested timeframe is finer than input; supply finer bars");
1954 const auto& service = runtime_contexts();
1955 if (service.active_context().serial)
return service.callback_frontier();
1963 const std::function<
void()>& begin_chart) {
1964 if (
prior_input == std::numeric_limits<std::int64_t>::min()
1977 deliver(sub_bar,
true);
1988 const double close = (aggregate.bar.open + aggregate.bar.high
1989 + aggregate.bar.low + aggregate.bar.close) / 4.0;
1991 : (aggregate.bar.open + aggregate.bar.close) / 2.0;
1992 aggregate.bar.high = std::max(aggregate.bar.high, std::max(open, close));
1993 aggregate.bar.low = std::min(aggregate.bar.low, std::min(open, close));
1994 aggregate.bar.open = open; aggregate.bar.close = close;
1998 deliver(aggregate.bar, aggregate.is_complete);
2006 bool script_bar_geometry) {
2009 last_bar_index_ = horizon - 1;
2010 const int64_t script_tf_ms =
2011 static_cast<int64_t
>(script_tf_seconds_ > 0 ? script_tf_seconds_ : 0) * 1000;
2012 if (script_bar_geometry) {
2014 last_bar_time_ = bars[horizon - 1].
timestamp;
2017 +
static_cast<int64_t
>(horizon - n) * script_tf_ms;
2021 +
static_cast<int64_t
>(horizon - 1) * script_tf_ms;
2025void source::PineStrategyHost::scheduler_configure_security_evaluators() {
2026 configure_security_evaluators();
2027 prune_pine_security_states();
2030bool source::PineStrategyHost::scheduler_uses_aux_security_feed() const noexcept {
2031#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
2032 return aux_security_feed_enabled();
2041int multi_day_count(
const std::string& tf) {
2043 if (!parsed || parsed->unit() != native_calendar::TimeframeUnit::Day)
return 0;
2044 return parsed->count() >= 2 ? parsed->count() : 0;
2049void source::PineStrategyHost::scheduler_prepare_security_sequence(
2051 security_input_tf_ = input_tf_;
2052#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
2053 if (aux_security_feed_enabled()) security_input_tf_ = aux_security_input_tf_;
2055 validate_security_timeframes(security_input_tf_);
2056 security_first_chart_bar_ms_ = bars.empty() ? 0 : bars.front().timestamp;
2057 if (declare_security_sites_to_kernel()) {
2061 security_next_input_ms_ = 0;
2062 security_calling_close_ms_ = 0;
2063 clear_historical_security_lookahead_projections();
2064 prepare_chart_day_partition(
2065 bars.empty() ?
nullptr : bars.data(),
static_cast<int>(bars.size()));
2068 init_security_eval_states_for_run(security_input_tf_);
2069 prepare_native_security_feeds(
2070 bars.empty() ?
nullptr : bars.data(),
static_cast<int>(bars.size()));
2076 for (
auto& state : security_eval_states_) {
2077 const int days = multi_day_count(state.tf);
2079 || state.aggregator.has_native_periods()) {
2084 feeds.chart = bars.data();
2085 feeds.n_chart =
static_cast<int>(bars.size());
2087#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
2088 if (aux_security_feed_enabled()) {
2089 feeds.aux = aux_security_bars_.data();
2090 feeds.n_aux = aux_security_bars_.size();
2093 feeds.tz = syminfo_.timezone;
2094 feeds.session = syminfo_.session;
2095 feeds.exchange_calendar = session_template_knows_early_close();
2097 state.aggregator.set_native_period_source(std::move(partition));
2099#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
2100 if (aux_security_feed_enabled()) {
2101 prepare_aux_security_chart_ranges(
2102 bars.empty() ?
nullptr : bars.data(),
static_cast<int>(bars.size()), script_tf_);
2105 prepare_historical_security_lookahead_projections(
2106 bars.empty() ?
nullptr : bars.data(),
static_cast<int>(bars.size()), input_tf_);
2107 prepare_chart_day_partition(
2108 bars.empty() ?
nullptr : bars.data(),
static_cast<int>(bars.size()));
2111bool source::PineStrategyHost::security_sites_kernel_routed() const noexcept {
2116 if (spec ==
nullptr)
return false;
2117 for (
const auto& series : spec->subscriptions) {
2118 if (series.source != NativeSeriesSource::InstrumentFeed)
return true;
2123bool source::PineStrategyHost::declare_security_sites_to_kernel() {
2124 std::vector<NativeTimeframeSubscription> declared;
2125 bool routed = same_symbol_sites_routable(declared);
2126 const std::size_t same_symbol = declared.size();
2129 prepare_foreign_security_sites(declared);
2130 if (declared.empty())
return false;
2131 if (declared.size() == same_symbol) {
2132 if (!declare_timeframe_subscriptions(std::move(declared)))
return false;
2134 auto result = declare_timeframe_subscriptions_result(declared);
2135 if (result.status != NativeSetupStatus::Applied && routed) {
2139 declared.erase(declared.begin(),
2140 declared.begin() +
static_cast<std::ptrdiff_t
>(same_symbol));
2141 foreign_security_series_.erase(
2142 foreign_security_series_.begin(),
2143 foreign_security_series_.begin() +
static_cast<std::ptrdiff_t
>(same_symbol));
2144 for (
auto& entry : foreign_security_sites_) {
2145 if (entry.second.subscription >= 0)
2146 entry.second.subscription -=
static_cast<std::int64_t
>(same_symbol);
2149 result = declare_timeframe_subscriptions_result(declared);
2151 if (result.status != NativeSetupStatus::Applied) {
2154 refused.emplace_back(
"field", field);
2155 throw coded<std::runtime_error>(
2157 "request.security: the kernel refused the symbol feed series (NativeRunSpecError "
2158 + std::to_string(
static_cast<int>(result.validation.error)) +
", field "
2159 + std::to_string(
static_cast<int>(result.validation.field)) +
")");
2164 if (routed) security_eval_states_.clear();
2168bool source::PineStrategyHost::same_symbol_sites_routable(
2169 std::vector<NativeTimeframeSubscription>& out) {
2170 if (security_eval_states_.empty())
return false;
2178 if (stream_warmup_mode_ || scheduler_.bar_magnifier_enabled())
return false;
2179 if (security_range_start_na_warmup_ || historical_security_lookahead_projection_)
2181#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
2182 if (aux_security_feed_enabled())
return false;
2184 if (input_tf_.empty() || input_tf_ != script_tf_ || security_input_tf_ != input_tf_)
2186 const auto view = native_state();
2187 if (view.spec ==
nullptr || view.spec->timeframe_undetected
2188 || view.spec->input_tf != input_tf_) {
2191 const bool otc_daily_pins = (syminfo_.type ==
"forex" || syminfo_.type ==
"cfd")
2192 && native_security_feeds_.empty() && script_tf_seconds_ > 0
2193 && script_tf_seconds_ < 86400;
2194 std::vector<NativeTimeframeSubscription> declared;
2195 declared.reserve(security_eval_states_.size());
2196 for (std::size_t i = 0; i < security_eval_states_.size(); ++i) {
2197 const SecurityEvalState& state = security_eval_states_[i];
2200 if (minute_day && !pine_security_state(state.sec_id).lookahead_on
2201 && script_tf_seconds_ > 0 && script_tf_seconds_ < 86400) {
2206 if (state.sec_id !=
static_cast<int>(i) || state.tf.empty())
return false;
2211 if (pine.lookahead_on || pine.heikinashi || pine.lower_tf_requested
2212 || pine.lower_tf_emulation || pine.lower_tf_use_input
2213 || pine.lower_tf_array_requested || pine.publish_gate_tf_seconds > 0
2214 || pine.calling_close_completes_partial || pine.calling_open_latches_first) {
2217 if (otc_daily_pins && (state.tf ==
"D" || state.tf ==
"1D"))
return false;
2221 if (multi_day_count(state.tf) != 0)
return false;
2222 NativeTimeframeSubscription subscription;
2223 subscription.tf = state.tf;
2224 subscription.gaps = pine.gaps_on;
2225 declared.push_back(std::move(subscription));
2227 out = std::move(declared);
2231bool source::PineStrategyHost::scheduler_feed_security_input(
2232 const Bar& bar, std::int64_t next_input_ms,
bool calling_bar_complete,
2233 bool defer_boundary_gate) {
2234 security_next_input_ms_ = next_input_ms;
2235 security_calling_close_ms_ = 0;
2236 bool deferred =
false;
2237 for (
auto& state : security_eval_states_) {
2238 if (defer_boundary_gate
2239 && pine_security_state(state.sec_id).publish_gate_tf_seconds > 0) {
2243 pine_feed_security_eval_state(state, bar, calling_bar_complete);
2248void source::PineStrategyHost::scheduler_publish_security_boundary() {
2249 for (
auto& state : security_eval_states_) {
2250 if (pine_security_state(state.sec_id).publish_gate_tf_seconds > 0)
2251 publish_security_eval_state_at_calling_boundary(state);
2255void source::PineStrategyHost::scheduler_feed_deferred_security_input(
2256 const Bar& bar, std::int64_t next_input_ms) {
2257 security_next_input_ms_ = next_input_ms;
2258 security_calling_close_ms_ = 0;
2259 for (
auto& state : security_eval_states_) {
2260 if (pine_security_state(state.sec_id).publish_gate_tf_seconds > 0)
2261 pine_feed_security_eval_state(state, bar,
false);
2265void source::PineStrategyHost::scheduler_feed_aux_security(
int chart_index) {
2266#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
2267 if (aux_security_feed_enabled() && scheduler_coof_enabled()) {
2275 restore_script_state();
2276 feed_aux_security_for_chart_bar(chart_index);
2277 snapshot_script_state();
2280 if (aux_security_feed_enabled()) feed_aux_security_for_chart_bar(chart_index);
2286void source::PineStrategyHost::scheduler_feed_deferred_aux_security(
int chart_index) {
2287#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
2288 if (aux_security_feed_enabled()) feed_deferred_aux_security_for_chart_bar(chart_index);
2294void source::PineStrategyHost::scheduler_finish_security_sequence() {
2295 clear_historical_security_lookahead_projections();
2296#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
2297 release_aux_security_chart_ranges();
2308#if PINEFORGE_PINE_QUIET_BAR_GATES
2309 if (trades.
size() < 2)
return false;
2310 const auto last_index = trades.
size() - 1;
2311 const auto previous_index = last_index - 1;
2319 const auto last_entry_id = trades.
entry_id_at(last_index);
2320 return trades.
entry_id_at(previous_index) == last_entry_id;
2327bool source::PineStrategyHost::report_terminal_quote_eligible()
const {
2328 if (!last_equity_point() || range_end_trades_.empty() || !native_bound())
return false;
2329 const auto state = as_native_consumer(execution_consumer()).view();
2330 const auto interval =
static_cast<std::int64_t
>(script_tf_seconds_) * 1000;
2331 const auto last_time = last_equity_point()->time_ms;
2332 return state.spec && state.spec->report_policy == NativeReportPolicy::KernelRecordedAtHostMarks
2333 && state.kind == NativeLifecycleKind::Completed && bar_magnifier_enabled_
2334 && script_tf_seconds_ > 60 && script_tf_seconds_ < 86400
2335 && report_quote_time_ms_ >= last_time && report_quote_time_ms_ - last_time <= interval
2336 && (report_quote_time_ms_ - last_time) % interval == 0
2337 && std::isfinite(report_quote_close_)
2338 && range_end_trades_.
size() == pyramid_entries_.size();
2342 if (!report_terminal_quote_eligible() ||
report.equity_curve_len == 0
2343 ||
report.equity_curve[
report.equity_curve_len - 1].time_ms != report_quote_time_ms_)
return false;
2344 std::size_t rows = 0;
2345 for (
int index = 0; index <
report.trades_len; ++index) {
2346 const auto& row =
report.trades[index];
2347 if (!row.open_at_end)
continue;
2348 if (row.exit_time != report_quote_time_ms_ || row.exit_price != bar_fill_price(report_quote_close_))
2352 return rows == pyramid_entries_.size();
2355void source::PineStrategyHost::present_report(
ReportC* out)
const {
2357 const auto state = as_native_consumer(execution_consumer()).view();
2359 || state.spec->report_policy != NativeReportPolicy::KernelRecordedAtHostMarks)
return;
2360 const auto position = as_native_consumer(execution_consumer()).position(*
this);
2362 if ((stream_phase_ == StreamPhase::REALTIME
2363 || state.completion == NativeCompletion::StreamEnded)
2364 && (!range_end_trades_.empty()
2365 || (position.signed_units == 0.0 && position.lot_count == 0
2366 && last.open_profit != 0.0))) {
2367 last.open_profit = 0.0;
2368 if (!range_end_trades_.empty()) {
2369 double range_end_pnl = 0.0;
2370 for (
const auto& row : range_end_trades_) range_end_pnl += row.pnl;
2371 last.equity = initial_capital_ + net_profit_sum_ + range_end_pnl;
2372 amend_report_replay_point(last.time_ms, report::EquityBasis::range_end, range_end_pnl);
2375 if (!report_terminal_quote_eligible())
return;
2376 const double price = bar_fill_price(report_quote_close_);
2377 const double fx = account_currency_fx_at(report_quote_time_ms_);
2378 std::size_t lot_index = 0;
2379 for (
int index = 0; index < out->
trades_len; ++index) {
2380 auto& row = out->
trades[index];
2381 if (!row.open_at_end)
continue;
2382 const auto& lot = pyramid_entries_[lot_index++];
2383 execution::PhysicalExecutionContext context;
2384 context.effective_time_ms = report_quote_time_ms_;
2386 context.account_fx = fx;
2387 const auto mark = build_close_trade_with_costs(lot, row.qty,
price, row.is_long != 0,
2388 allocated_entry_commission(lot, row.qty), calc_commission_at(
price, row.qty, fx), context);
2392 if (!internal::closed_row_all_finite(mark) || !std::isfinite(
price))
2393 throw std::logic_error(
"non-finite terminal-quote report row");
2394 row.exit_time = report_quote_time_ms_;
2395 row.exit_price =
price;
2397 row.pnl_pct = mark.pnl_pct;
2398 row.commission = mark.commission;
2402 last.time_ms = report_quote_time_ms_;
2403 last.equity = initial_capital_ + out->
net_profit;
2404 amend_report_replay_point(last.time_ms, report::EquityBasis::terminal_quote, out->
net_profit);
2407void source::PineStrategyHost::scheduler_record_range_end(
const Bar& terminal_bar) {
2408 range_end_trades_.clear();
2409 if (stream_warmup_mode_ || realtime_tail_
2411 || !std::isfinite(terminal_bar.close))
return;
2412 const double fill_price = bar_fill_price(terminal_bar.close);
2421 const std::int64_t mark_time = last_equity_point()->time_ms;
2422 const double range_end_pnl = as_native_consumer(execution_consumer())
2423 .append_open_position_report_rows(*
this, fill_price, mark_time, bar_index_,
2424 account_currency_fx_at(mark_time));
2425 if (stream_phase_ != StreamPhase::REALTIME) {
2426 auto& last = *last_equity_point();
2427 last.open_profit = 0.0;
2428 last.equity = initial_capital_ + net_profit_sum_ + range_end_pnl;
2429 amend_report_replay_point(last.time_ms, report::EquityBasis::range_end, range_end_pnl);
2434 refold_equity_tail();
2437 if (
const auto moved = sort_same_bar_exit_trades(trades_, adapter_))
2438 native_closed_rows_amended(*moved);
2467void source::PineStrategyHost::order_global_exit_children(
2468 const native_order::ExecutionAppliedEvent& event) {
2469 const std::size_t first =
event.first_trade_index;
2470 const std::size_t count =
event.closed_trade_count;
2471 if (count < 2 || event.opened_units != 0.0 || first + count != trades_.size()
2472 || !pyramid_entries_.empty()) {
2475 const auto& run = adapter_.config_;
2476 if (run.calc_on_order_fills || run.process_orders_on_close || bar_magnifier_enabled_
2477 || active_account_currency_fx() != 1.0 || commission_value_ < 0.0
2481 const auto placement = adapter_.placement_.find(event.handle().incarnation);
2482 if (placement == adapter_.placement_.end())
return;
2485 || !cause.from_entry.empty()) {
2492 Trade row = trades_[first];
2493 head.exit_time = row.exit_time;
2494 head.exit_price = row.exit_price;
2495 head.exit_id = std::move(row.exit_id);
2496 head.is_long = row.is_long;
2497 head.entry_id = std::move(row.entry_id);
2499 auto& scratch = trade_scratch();
2500 TradeScratchTransaction transaction(scratch);
2501 bool different_ids =
false;
2502 for (std::size_t index = first; index < first + count; ++index) {
2503 const Trade& row = trades_[index];
2504 if (row.exit_time != head.exit_time || row.exit_price != head.exit_price
2505 || row.exit_id != head.exit_id || row.is_long != head.is_long
2506 || !(row.qty > 0.0) || row.open_at_end) {
2509 const TradeScratchKey incarnation{scratch_incarnation, row.entry_incarnation, 0, 0};
2511 scratch_put(scratch, incarnation);
2512 different_ids = different_ids || row.entry_id != head.entry_id;
2514 if (!different_ids)
return;
2515 if (first > 0 && trades_[first - 1].exit_time == head.exit_time
2516 && trades_[first - 1].exit_price == head.exit_price) {
2524 std::array<std::size_t, 27> ages{};
2525 std::array<std::uint32_t, 27> ranks{};
2526 std::size_t age_count = 0;
2527 const auto age_of = [&](std::size_t lot) {
2528 const auto& keys = adapter_.exit_rank_keys_;
2529 const auto found = keys.find(trades_[first + lot].entry_id);
2530 return found ?
static_cast<std::size_t
>(keys.get(*found))
2531 : static_cast<std::size_t>(keys.size());
2533 const auto key_of = [&](std::size_t age) {
2535 adapter_.exit_rank_keys_.for_each_inserted([&](
SourceId key, std::uint32_t key_age) {
2536 if (key_age == age)
id = std::move(key);
2540 std::vector<SourceId> ids;
2541 for (std::size_t lot = 0; lot < count; ++lot) {
2542 const auto age = age_of(lot);
2543 if (age == adapter_.exit_rank_keys_.size())
return;
2544 if (std::find(ages.begin(), ages.begin() + age_count, age) != ages.begin() + age_count)
2546 ids.assign(1, trades_[first + lot].entry_id);
2547 const auto rank = adapter_.global_exit_child_ranks(ids);
2549 ranks[age] = rank->front();
2550 for (std::size_t earlier = 0; earlier < age_count; ++earlier) {
2552 ids.push_back(key_of(ages[earlier]));
2553 if (!adapter_.global_exit_child_ranks(ids))
return;
2555 ages[age_count++] = age;
2557 const auto rank_of = [&](std::size_t lot) {
2558 return ranks[age_of(lot)];
2561 bool ordered =
true;
2562 auto previous_rank = rank_of(0);
2563 for (std::size_t lot = 1; lot < count; ++lot) {
2564 const auto rank = rank_of(lot);
2565 if (rank < previous_rank) ordered =
false;
2566 previous_rank = rank;
2568 if (ordered)
return;
2569 auto*
const replay = report_replay_state();
2570 std::unique_ptr<::pineforge::detail::ScratchSequence<report::ReplayTrade>> replay_rows;
2572 replay_rows = std::make_unique<::pineforge::detail::ScratchSequence<report::ReplayTrade>>();
2573 replay_rows->set_directory(replay->scratch_directory());
2575 const auto visit_children = [&](
const auto& visit) {
2576 for (std::size_t rank = 0; rank < adapter_.exit_rank_keys_.size(); ++rank)
2577 for (std::size_t child = 0; child < count; ++child)
2578 if (rank_of(child) == rank && !visit(child))
return false;
2581 std::size_t row_count = 0;
2582 const auto emit_row = [&](TradeScratchHeader header,
const Trade&
text) {
2583 const TradeScratchSource row(header, text);
2584 scratch_put(scratch, {scratch_row, row_count++, 0, 0}, row);
2586 const auto header_for = [&](std::size_t source,
const Trade& row) {
2587 const auto phase = adapter_.exit_phase_value(source);
2588 return scratch_trade_header(row, source, phase);
2590 const auto annotate = [&](std::size_t lot, std::size_t child,
double transaction_qty,
2591 bool fifo,
bool fees,
double exit_fee,
double transaction_fee) {
2592 if (!replay)
return;
2593 const Trade whole = trades_.numeric_at(first + lot);
2594 auto row = replay->trade(first + lot);
2595 row.parent =
event.ordinal;
2596 row.slice = row_count - 1;
2598 row.transaction_qty = transaction_qty;
2599 row.whole_qty = whole.qty;
2600 row.whole_pnl = whole.pnl;
2601 row.whole_commission = whole.commission;
2602 row.whole_runup = whole.max_runup;
2603 row.whole_drawdown = whole.max_drawdown;
2604 row.whole_exit_fee = exit_fee;
2605 row.transaction_fee = transaction_fee;
2609 replay_rows->push_back(row);
2612 if (run.close_entries_rule_any) {
2613 visit_children([&](std::size_t lot) {
2614 const Trade row = trades_[first + lot];
2615 emit_row(header_for(first + lot, row), row);
2616 if (replay) annotate(lot, lot, row.qty,
false,
false, 0.0, 0.0);
2620 const bool fees = commission_value_ != 0.0;
2621 for (std::size_t lot = 0; lot < count; ++lot) {
2622 const double qty = trades_[first + lot].qty;
2625 const auto visit_slices = [&](
const auto& visit) {
2626 std::size_t lot = 0;
2627 double left = trades_[first].qty;
2628 if (!visit_children([&](std::size_t child) {
2629 const double transaction = trades_[first + child].qty;
2630 double wanted = transaction;
2631 while (wanted > internal::kQtyEpsilon && lot < count) {
2632 const double taken = std::min(left, wanted);
2633 visit(lot, child, taken, transaction);
2636 if (left <= internal::kQtyEpsilon) {
2638 if (lot < count) left = trades_[first + lot].qty;
2641 return !(wanted > internal::kQtyEpsilon);
2643 return lot == count;
2647 if (!visit_slices([](std::size_t, std::size_t,
double,
double) {}))
return;
2648 visit_slices([&](std::size_t lot, std::size_t child,
double qty,
double transaction) {
2649 const Trade& whole = trades_[first + lot];
2650 const double share = qty / whole.qty;
2651 double commission = 0.0;
2652 double replay_exit_fee = 0.0, replay_transaction_fee = 0.0;
2654 const double exit_fee = calc_commission_at(whole.exit_price, whole.qty, 1.0);
2655 const double entry_part =
2656 tv_money_round(std::max(0.0, whole.commission - exit_fee) * share);
2657 const double transaction_fee =
2658 tv_money_round(calc_commission_at(whole.exit_price, transaction, 1.0));
2659 const double exit_part =
tv_money_round(transaction_fee * qty / transaction);
2662 replay_exit_fee = exit_fee;
2663 replay_transaction_fee = transaction_fee;
2666 auto header = header_for(first + lot, whole);
2668 scratch_trade_scalars(header, row);
2670 row.commission = commission;
2671 row.pnl =
tv_money_round((whole.pnl + whole.commission) * share) - commission;
2672 const double cost = whole.entry_price * qty * syminfo_.pointvalue;
2673 row.pnl_pct = cost > 0.0 ? row.pnl / cost * 100.0 : 0.0;
2676 row.max_runup = whole.max_runup * share;
2677 row.max_drawdown = whole.max_drawdown * share;
2678 header[6] = scratch_word(row.qty);
2679 header[7] = scratch_word(row.pnl);
2680 header[8] = scratch_word(row.pnl_pct);
2681 header[13] = scratch_word(row.max_runup);
2682 header[14] = scratch_word(row.max_drawdown);
2683 header[15] = scratch_word(row.commission);
2684 emit_row(header, whole);
2685 if (replay) annotate(lot, child, transaction,
true, fees,
2686 replay_exit_fee, replay_transaction_fee);
2693 scratch_trade_scalars(scratch_read_trade(scratch, handle), row);
2694 if (!internal::closed_row_all_finite(row))
2695 throw std::logic_error(
"non-finite amended trade row");
2703 const auto tally = [
this](
double pnl,
int sign) {
2704 if (pnl > 0.0) { gross_profit_sum_ += sign * pnl; win_trades_count_ += sign; }
2705 else if (pnl < 0.0) { gross_loss_sum_ += sign * pnl; loss_trades_count_ += sign; }
2706 else { eventrades_count_ += sign; }
2708 for (std::size_t index = first; index < first + count; ++index) tally(trades_[index].pnl, -1);
2711 scratch_trade_scalars(scratch_read_trade(scratch, handle), row);
2715 if (replay_rows->size() != row_count)
2717 "recut replay rows differ from replacement rows");
2718 replay->resize_trades(first + row_count);
2719 for (std::size_t i = 0; i < row_count; ++i) {
2720 auto annotation = replay_rows->at(i);
2721 annotation.slices = row_count;
2722 replay->replace(first + i, annotation);
2725 adapter_.invalidate_exit_group(first, trades_.size() - first); trades_.resize(first);
2727 const auto header = scratch_read_trade(scratch, handle);
2729 scratch_trade_scalars(header, row);
2730 scratch_trade_text(scratch, handle, header, row);
2731 trades_.push_back(std::move(row));
2733 const auto none =
static_cast<std::uint8_t
>(NativePathPhase::None);
2735 const auto header = scratch_read_trade(scratch, handle);
2736 const auto target = first +
static_cast<std::size_t
>(
key[1]);
2737 const auto phase =
static_cast<std::uint8_t
>(header[1]);
2738 if (phase ==
none && adapter_.exit_phase_value(target) ==
none)
return;
2739 adapter_.set_exit_phase(target, phase);
2741 transaction.finish();
2742 native_closed_rows_amended(first);
2750std::optional<std::size_t> source::PineStrategyHost::sort_same_bar_exit_trades(
2751 TradeLedger& trades, source::PineExecutionAdapter& adapter) {
2752 ++adapter.exit_group_work_.publications;
if (adapter.exit_group_certified(trades) || trades.size() < 2)
return std::nullopt;
2753 const std::size_t
end = trades.size();
2754 std::size_t start =
end - 1;
2756 && trades[start - 1].exit_time == trades[end - 1].exit_time
2757 && trades[start - 1].entry_time == trades[end - 1].entry_time
2758 && trades[start - 1].entry_id == trades[end - 1].entry_id
2759 && trades[start - 1].exit_from_bracket
2760 && trades[end - 1].exit_from_bracket) {
2761 --start; ++adapter.exit_group_work_.tail_rows;
2763 if (end - start > 1) {
2768 const auto less = [&](std::size_t ia, std::size_t ib) {
2769 ++adapter.exit_group_work_.comparisons;
const bool open_a = adapter.is_open_phase_exit(ia);
2770 const bool open_b = adapter.is_open_phase_exit(ib);
2771 if (open_a != open_b)
return open_a;
2772 const auto sa = adapter.command_sequence_for_exit(trades[ia].exit_id, trades[ia].entry_id);
2773 const auto sb = adapter.command_sequence_for_exit(trades[ib].exit_id, trades[ib].entry_id);
2776 const auto none =
static_cast<std::uint8_t
>(NativePathPhase::None);
2777 bool ordered =
true;
2778 for (std::size_t index = start + 1; index <
end; ++index)
2779 if (less(index, index - 1)) { ordered =
false;
break; }
2780 if (ordered) { adapter.certify_exit_group(trades, start);
return std::nullopt; }
2781 auto& scratch = trade_scratch(); adapter.certify_exit_group(trades, start,
false);
2782 const auto scratch_before = scratch.stats(); TradeScratchTransaction transaction(scratch);
2783 for (std::size_t source = start; source <
end; ++source) {
2784 const auto phase = adapter.exit_phase_value(source);
2785 const auto sequence = adapter.command_sequence_for_exit(
2786 trades[source].exit_id, trades[source].entry_id);
2787 scratch_put(scratch,
2788 {scratch_sort, adapter.is_open_phase_exit(source) ? 0U : 1U,
sequence, source},
2791 std::size_t target = start;
2795 scratch_walk(scratch, scratch_sort,
2797 std::uint8_t phase =
none;
2799 scratch_buffer(&phase,
sizeof phase)), scratch);
2800 const std::array<std::uint64_t, 2> mapping{
key[3], phase};
2801 scratch_put(scratch, {scratch_permutation, target++, 0, 0},
2806 for (std::size_t cycle = start; cycle <
end; ++cycle) {
2807 auto handle = scratch_find(scratch, {scratch_permutation, cycle, 0, 0});
2809 const Trade row = trades[cycle];
2810 auto*
const replay = report_replay_state();
2811 const auto saved_replay = replay ? replay->trade(cycle) : report::ReplayTrade{};
2812 std::size_t current = cycle;
2814 std::array<std::uint64_t, 2> mapping{};
2816 scratch_buffer(mapping.data(),
sizeof mapping)), scratch);
2817 const auto source =
static_cast<std::size_t
>(mapping[0]);
2818 const auto phase =
static_cast<std::uint8_t
>(mapping[1]);
2822 if (phase !=
none || adapter.exit_phase_value(current) !=
none)
2823 adapter.set_exit_phase(current, phase);
2824 ++adapter.exit_group_work_.replacements;
if (source == cycle) trades.replace(current, row);
2825 else trades.replace(current, trades[source]);
2827 replay->replace(current, source == cycle ? saved_replay : replay->trade(source));
2828 require_trade_scratch(scratch.erase(handle), scratch);
2829 if (source == cycle)
break;
2831 handle = scratch_find(scratch, {scratch_permutation, current, 0, 0});
2834 "incomplete trade scratch permutation");
2837 transaction.finish(); adapter.finish_exit_group_sort(scratch, scratch_before);
2840 return std::nullopt;
2843void source::PineStrategyHost::scheduler_update_session_state() {
2869 const NativeDecisionContext&
facts = point->decision;
2870 session_ismarket_ =
facts.in_session;
2871 session_isfirstbar_regular_ =
facts.opens_session_day;
2872 session_islastbar_regular_ = realtime_tail_ ?
facts.closes_session_day_open_ended
2873 :
facts.closes_session_day;
2874 session_isfirstbar_ = session_isfirstbar_regular_;
2875 session_islastbar_ = session_islastbar_regular_;
2884 && script_tf_seconds_ > 0) {
2885 if (
const auto* day = symbol_calendar_day(
facts.script_bar_open_ms)) {
2886 const bool last =
facts.script_bar_open_ms
2887 +
static_cast<std::int64_t
>(script_tf_seconds_) * 1000 >= day->second;
2888 session_islastbar_regular_ = last;
2889 session_islastbar_ = last;
2892 update_extended_session_day(facts);
2917void source::PineStrategyHost::update_extended_session_day(
const NativeDecisionContext& facts) {
2918 const std::string& session = syminfo_.session;
2919 const std::string& tz = syminfo_.timezone;
2920 const std::int64_t at =
facts.script_bar_open_ms;
2921 const bool regular =
facts.in_session;
2924 if (regular && !
facts.opens_session_day && !session_islastbar_regular_)
return;
2925 const auto pre = [&](std::int64_t ms) {
2928 const auto post = [&](std::int64_t ms) {
2931 const bool premarket = !regular && pre(at);
2932 const bool postmarket = !regular && !premarket && post(at);
2933 if (!regular && !premarket && !postmarket)
return;
2936 const auto same_day = [&](std::int64_t ms) {
2939 const auto in_chart_day = [&](std::int64_t ms) {
2943 const std::optional<std::int64_t> before = scheduler_.last_published_script_open();
2947 const std::int64_t next_open = std::max(at + 1,
facts.script_interval.next_period_open_ms);
2948 std::optional<std::int64_t> after;
2949 bool known_after =
false;
2950 if (
const auto bar = scheduler_.retained_input_from(
facts.coordinate.input_interval_index,
2952 after = bar->timestamp;
2955 const bool run_end = !known_after && !scheduler_.retains_stream() && !realtime_tail_;
2956 if (!known_after && !run_end && !regular && script_tf_seconds_ > 0)
2957 after = at +
static_cast<std::int64_t
>(script_tf_seconds_) * 1000;
2963 if (
facts.opens_session_day)
2964 session_isfirstbar_ = !(before && pre(*before) && same_day(*before));
2965 if (session_islastbar_regular_)
2966 session_islastbar_ = !(known_after && post(*after) && same_day(*after));
2970 session_isfirstbar_ = !(before && pre(*before) && same_day(*before));
2971 session_islastbar_ = after ? !in_chart_day(*after) : run_end;
2974 session_isfirstbar_ = !(before && in_chart_day(*before));
2975 session_islastbar_ = after ? !(post(*after) && same_day(*after)) : run_end;
2978void source::PineStrategyHost::scheduler_publish_source_bar(
2979 const Bar& bar,
bool,
bool advance_source_index) {
2981 const bool temporary_index = !advance_source_index
2982 && (!scheduler_.current_script_bar()
2983 || scheduler_.current_script_bar()->timestamp != bar.timestamp);
2984 const int previous_bar_index = bar_index_;
2985 const bool previous_barstate_islast = barstate_islast_;
2986 if (advance_source_index || temporary_index) ++source_bar_index_;
2987 ++source_callback_count_;
2988 bar_index_ = source_bar_index_;
2991 if (lifecycle == NativeLifecycleKind::Running && phase == NativeRunPhase::Warmup) {
2992 barstate_islast_ =
false;
2993 }
else if (lifecycle == NativeLifecycleKind::Running
2994 && phase == NativeRunPhase::Realtime) {
2995 barstate_islast_ =
true;
2997 barstate_islast_ = source_bar_index_ == source_last_bar_index_;
3008 internal::RuntimeAmbient*
const ambient = NativeExecutionConsumer::bound(*this).pump_ambient();
3009 internal::AmbientDayPartitionScope chart_day_partition(
3010 ambient, chart_day_partition_.empty() ?
nullptr : &chart_day_partition_);
3013 adapter_.begin_source_evaluation();
3018 const bool capturing = selected_begin_evaluation(bar.timestamp);
3023 if (
const auto moved = sort_same_bar_exit_trades(trades_, adapter_))
3024 native_closed_rows_amended(*moved);
3030 if (consumer.streams_closed_rows(*
this) && consumer.begin_closed_row_seal(bar.timestamp)) {
3031 const std::size_t base = trades_.base();
3032 auto prefix = consumer.closed_trade_prefix();
3033 if (prefix.base != base)
3035 "source trade seal has a mismatched base");
3036 const bool can_swap = adapter_.short_seed_.final_short.incarnation != 0
3037 && adapter_.config_.default_qty_type !=
static_cast<int>(
QtyType::FIXED);
3038 const std::size_t next = consumer.closed_rows_sealable(trades_,
3039 adapter_.exit_phase_final_, adapter_.source_money_folded_, bar.timestamp,
3040 can_swap ? std::optional<std::uint64_t>(adapter_.short_seed_.materialize_long.incarnation)
3042 if (next - base > adapter_.trade_exit_phase_.size())
3044 "source trade seal exceeds resident exit phases");
3045 for (std::size_t index = base; index < next; ++index) {
3046 const Trade& row = trades_[index];
3048 prefix.exit_phase_digest, adapter_.trade_exit_phase_[index - base]);
3049 if (row.pnl > 0.0) prefix.source_gains += row.pnl;
3050 else prefix.source_losses += row.pnl;
3057 adapter_.preserve_position_tail_before_seal(
3058 next, consumer.report_scratch_directory(*
this));
3059 consumer.seal_closed_rows(*
this, next, prefix.exit_phase_digest,
3060 prefix.source_gains, prefix.source_losses);
3061 for (std::size_t offset = 0; offset < next - base; ++offset)
3062 consumer.fold_sealed_exit_phase(adapter_.trade_exit_phase_[offset]);
3063 adapter_.trade_exit_phase_.erase(adapter_.trade_exit_phase_.begin(),
3064 adapter_.trade_exit_phase_.begin() + (next - base));
3067 adapter_.observe_terminal_receipts();
3080 if (!
skip_quiet(QuietHook::BracketLegs, adapter_.pending_bracket_legs_.empty())) {
3081 adapter_.flush_pending_bracket_legs({},
false,
3084 internal::AmbientEmaSeedingScope ema_scope(ambient, chart_ema_na_warmup_);
3085 internal::AmbientBarContextScope bar_scope(ambient, pine_bar_index(),
3086 scheduler_.bar_index_offset());
3088 ? 0 : adapter_.source_entry_slot_count();
3092 release_script_average_price();
3097 adapter_.set_configuration(config_);
3098 if (capturing) selected_finish_evaluation();
3099 if (temporary_index) {
3100 --source_bar_index_;
3101 bar_index_ = previous_bar_index;
3102 barstate_islast_ = previous_barstate_islast;
3104 finish_adapter_evaluation();
3105 if (advance_source_index) {
3106 scheduler_mark_report_point(bar.timestamp);
3107 prev_bar_timestamp_ = bar.timestamp;
3114void source::PineStrategyHost::finish_adapter_evaluation() {
3118 if (!
skip_quiet(QuietHook::PendingCloses, adapter_.close_batch_callsites_.empty()))
3119 adapter_.flush_pending_closes();
3120#if PINEFORGE_PINE_QUIET_BAR_GATES
3125 if (adapter_.host_ !=
nullptr && adapter_.delayed_market_orders_.empty()
3126 && adapter_.pending_same_bar_commands_.empty() && adapter_.pending_entries_.empty()) {
3127 (void)
skip_quiet(QuietHook::PendingEntries,
true);
3128 adapter_.pending_same_bar_close_qty_ = 0.0;
3130 adapter_.flush_pending_entries();
3133 adapter_.flush_pending_entries();
3135 if (!
skip_quiet(QuietHook::BracketLegs, adapter_.pending_bracket_legs_.empty()))
3136 adapter_.flush_pending_bracket_legs();
3140 if (!
skip_quiet(QuietHook::RelativeExits, adapter_.anchored_relative_legs_.empty()
3141 && adapter_.pending_relative_exits_.empty())) {
3142 adapter_.anchor_relative_exits();
3144 adapter_.order_same_point_entries();
3147void source::PineStrategyHost::scheduler_publish_suppressed_tail(
const Bar& bar) {
3152 ++source_bar_index_;
3153 bar_index_ = source_bar_index_;
3154 barstate_islast_ =
false;
3155 internal::AmbientDayPartitionScope chart_day_partition(
3156 NativeExecutionConsumer::bound(*this).pump_ambient(),
3157 chart_day_partition_.empty() ?
nullptr : &chart_day_partition_);
3158 adapter_.begin_source_evaluation();
3161 selected_clear_suppressed_evaluation(bar.timestamp);
3162 adapter_.observe_terminal_receipts();
3163 scheduler_mark_report_point(bar.timestamp);
3164 prev_bar_timestamp_ = bar.timestamp;
3168bool source::PineStrategyHost::selected_begin_evaluation(std::int64_t label_ms) {
3170 if (pre_roll.state ==
nullptr)
return false;
3172 adapter_.capture_replay_risk_config(), *pre_roll.point);
3173 pre_roll.state->capturing =
true;
3177void source::PineStrategyHost::selected_finish_evaluation() noexcept {
3179 if (state ==
nullptr || !state->capturing)
return;
3180 state->end_config = config_;
3181 state->end_risk = adapter_.capture_replay_risk_config();
3183 state->terminal_point = *point;
3184 state->capturing =
false;
3187void source::PineStrategyHost::selected_clear_suppressed_evaluation(std::int64_t label_ms) {
3189 if (pre_roll.state ==
nullptr)
return;
3191 adapter_.capture_replay_risk_config(), *pre_roll.point);
3201void replay_intent(source::PineExecutionAdapter& adapter,
3202 const source::detail::IntentEntry& row) {
3204 row.qty, row.comment, row.oca_name, row.oca_type, row.qty_type);
3205 adapter.entry(row.id, row.is_long, row.limit_price, row.stop_price, row.qty, row.comment,
3206 row.oca_name, row.oca_type, row.qty_type);
3208void replay_intent(source::PineExecutionAdapter& adapter,
3209 const source::detail::IntentClose& row) {
3211 adapter.close(row.id, row.comment, row.qty, row.qty_percent, row.immediately);
3213void replay_intent(source::PineExecutionAdapter& adapter,
3214 const source::detail::IntentCloseToken& row) {
3216 row.immediately, row.callsite_token);
3217 adapter.close(row.id, row.comment, row.qty, row.qty_percent, row.immediately,
3218 row.callsite_token);
3220void replay_intent(source::PineExecutionAdapter& adapter,
const source::detail::IntentCloseAll&) {
3222 adapter.close_all();
3224void replay_intent(source::PineExecutionAdapter& adapter,
3225 const source::detail::IntentExit& row) {
3227 row.trail_points, row.trail_offset, row.trail_price,
3228 row.qty_percent, row.comment, row.qty, row.oca_name,
3229 row.profit_ticks, row.loss_ticks);
3230 adapter.exit(row.id, row.from_entry, row.limit_price, row.stop_price, row.trail_points,
3231 row.trail_offset, row.trail_price, row.qty_percent, row.comment, row.qty,
3232 row.oca_name, row.profit_ticks, row.loss_ticks);
3234void replay_intent(source::PineExecutionAdapter& adapter,
3235 const source::detail::IntentExitCancelBracket& row) {
3237 adapter.exit_cancel_bracket(row.exit_id, row.from_entry, row.comment);
3239void replay_intent(source::PineExecutionAdapter& adapter,
3240 const source::detail::IntentCancel& row) {
3242 adapter.cancel(row.id);
3244void replay_intent(source::PineExecutionAdapter& adapter,
3245 const source::detail::IntentCancelAll&) {
3247 adapter.cancel_all();
3249void replay_intent(source::PineExecutionAdapter& adapter,
3250 const source::detail::IntentOrder& row) {
3252 row.oca_name, row.oca_type);
3253 adapter.order(row.id, row.is_long, row.qty, row.limit_price, row.stop_price, row.oca_name,
3256void replay_intent(source::PineExecutionAdapter& adapter,
3257 const source::detail::IntentRiskDirection& row) {
3259 adapter.set_risk_direction(row.direction);
3261void replay_intent(source::PineExecutionAdapter& adapter,
3262 const source::detail::IntentRiskMaxConsLossDays& row) {
3263 adapter.set_risk_max_cons_loss_days(row.value);
3265void replay_intent(source::PineExecutionAdapter& adapter,
3266 const source::detail::IntentRiskMaxDrawdown& row) {
3268 adapter.set_risk_max_drawdown(row.value, row.percent);
3270void replay_intent(source::PineExecutionAdapter& adapter,
3271 const source::detail::IntentRiskMaxIntradayLoss& row) {
3273 adapter.set_risk_max_intraday_loss(row.value, row.percent);
3276void replay_intent(source::PineExecutionAdapter& adapter,
3277 const source::detail::IntentRiskMaxIntradayFilledOrders& row) {
3278 if (row.limit > 0) adapter.cap.adopt_script_count();
3279 adapter.cap = row.limit;
3281void replay_intent(source::PineExecutionAdapter& adapter,
3282 const source::detail::IntentRiskMaxPositionSize& row) {
3284 adapter.set_risk_max_position_size(row.value);
3291[[noreturn]]
void replay_invariant(
const char* what) {
3300class ReplayBracket {
3302 ReplayBracket(source::detail::PineIntentState& state, NativeExecutionConsumer& consumer,
3303 source::PineExecutionAdapter& adapter,
3304 const source::PineStrategyConfig& config) noexcept
3305 : state_(&state), consumer_(&consumer), adapter_(&adapter), config_(&config) {}
3306 ReplayBracket(
const ReplayBracket&) =
delete;
3307 ReplayBracket& operator=(
const ReplayBracket&) =
delete;
3308 ~ReplayBracket() noexcept {
3309 state_->replay_point =
nullptr;
3310 consumer_->end_open_activation();
3311 adapter_->set_configuration(*config_);
3315 source::detail::PineIntentState* state_;
3316 NativeExecutionConsumer* consumer_;
3317 source::PineExecutionAdapter* adapter_;
3318 const source::PineStrategyConfig* config_;
3329void source::PineStrategyHost::selected_replay_at_open(
const Bar& bar,
3330 const NativeDecisionContext&) {
3331 pf_selected_window_config_v1 window{};
3332 if (!NativeExecutionConsumer::selected_window_admitted(*
this, &window)
3333 || bar.timestamp < window.start_ms) {
3337 if (state ==
nullptr) replay_invariant(
"selected replay without its intent state");
3338 if (state->replayed)
return;
3339 if (!state->has_evaluation) {
3340 state->replayed =
true;
3345 if (state->capturing) replay_invariant(
"selected replay inside a pre-roll capture bracket");
3346 if (state->label_ms >= window.start_ms)
3347 replay_invariant(
"selected replay of an evaluation labelled inside the window");
3349 replay_invariant(
"selected replay over a held position");
3350 if (adapter_.coof_recalc_active_)
3351 replay_invariant(
"selected replay inside a calc_on_order_fills recalculation");
3352 if (!adapter_.live_handles_.empty() || !adapter_.delayed_market_orders_.empty()
3353 || !adapter_.pending_same_bar_commands_.empty() || !adapter_.pending_entries_.empty()
3354 || !adapter_.close_batch_callsites_.empty() || !adapter_.pending_bracket_legs_.empty()
3355 || !adapter_.anchored_relative_legs_.empty()
3356 || !adapter_.pending_relative_exits_.empty()) {
3357 replay_invariant(
"selected replay over live requests or queued source commands");
3360 state->replayed =
true;
3361 NativeExecutionConsumer& consumer = NativeExecutionConsumer::bound(*
this);
3362 if (!consumer.begin_open_activation(*
this))
3363 replay_invariant(
"selected replay without an open activation scope");
3364 const ReplayBracket bracket(*state, consumer, adapter_, config_);
3365 const auto stop_if_failed = [
this] {
3367 replay_invariant(
"selected replay stopped by a native failure");
3375 state->replay_point = &state->terminal_point;
3377 adapter_.restore_replay_risk_config(state->start_risk);
3378 adapter_.set_configuration(state->start_config);
3379 adapter_.begin_source_evaluation();
3380 adapter_.observe_terminal_receipts();
3382 if (!
skip_quiet(QuietHook::BracketLegs, adapter_.pending_bracket_legs_.empty())) {
3383 adapter_.flush_pending_bracket_legs({},
false,
3390 state->replay_point = &row.point;
3391 adapter_.set_configuration(row.config);
3392 std::visit([
this](
const auto& payload) { replay_intent(adapter_, payload); },
3399 state->replay_point = &state->terminal_point;
3400 adapter_.set_configuration(state->end_config);
3402 adapter_.restore_replay_risk_config(state->end_risk);
3403 finish_adapter_evaluation();
3411 const NativeDecisionContext& terminal = state->terminal_point.decision;
3412 const bool bound = adapter_.host_ !=
nullptr;
3413 const int bar_index = adapter_.projection_bar_index(terminal);
3414 std::size_t explicit_entries = 0;
3415 if (adapter_.config_.process_orders_on_close && adapter_.config_.pyramiding == 0
3416 && adapter_.modeled_input()) {
3417 for (
const auto& handle : adapter_.live_handles_) {
3418 const auto found = adapter_.placement_.find(handle.incarnation);
3419 if (found != adapter_.placement_.end() && found->second.opening
3421 && found->second.projection_created_bar ==
bar_index
3422 && std::isfinite(found->second.requested_qty)
3423 && found->second.requested_qty > 0.0) {
3428 if (!
skip_quiet(QuietHook::TerminalExplicitMarket, bound && explicit_entries < 2))
3429 adapter_.apply_terminal_explicit_market_policy(terminal,
false);
3434 state->rows.clear();
3447void source::PineStrategyHost::scheduler_mark_report_point(std::int64_t script_bar_ts) {
3451void source::PineStrategyHost::scheduler_record_broker_hash() {
3452 if (!broker_state_hash_recording_)
return;
3453 const std::uint64_t
execution = execution_consumer().continuation_hash();
3456 as_native_consumer(execution_consumer()).request_core().rethrow_book_read_failure();
3457 last_script_continuation_hash_ =
execution;
3458 last_script_continuation_valid_ =
true;
3459 broker_state_hashes_.push_back(broker_state_hash());
3463 return adapter_.core_sizes_default_opening(is_long);
3480 syminfo_.timezone, syminfo_.session);
3483 syminfo_.timezone, script_tf_);
3504constexpr std::int64_t kTimeOffsetMinuteMs = 60'000;
3507constexpr int kTimeOffsetCalendarMinutes = 16 * 24 * 60;
3509constexpr int kTimeOffsetBarsPerStep = 100'000;
3514 const std::string& tf,
3515 const std::string& session,
3516 const std::string& tz,
3517 int timeframe_bars_back,
3519 if (
is_na(bar_open_ms) ||
is_na(bars_back) ||
is_na(timeframe_bars_back))
3521 const std::string& sym_tz = syminfo_.timezone;
3522 const std::string& sym_session = syminfo_.session;
3524 const bool input_is_chart =
scheduler_.retained_input_is_chart();
3528 const auto chart_open_of = [&](std::int64_t input_ms) -> std::int64_t {
3529 return input_is_chart ? input_ms
3530 :
pine_time(input_ms, script_tf_,
"",
"", script_tf_, sym_tz,
3541 const std::int64_t chart_step_ms =
static_cast<std::int64_t
>(script_tf_seconds_) * 1000;
3544 && script_tf_seconds_ == 86400));
3545 const auto chart_close_of = [&](std::int64_t open_ms) -> std::int64_t {
3547 return daily_chart || chart_step_ms <= 0 ? day->second
3548 : std::min(open_ms + chart_step_ms, day->second);
3550 return daily_chart ?
pine_time_close(open_ms, script_tf_,
"",
"", script_tf_, sym_tz,
3553 open_ms, script_tf_, sym_session, sym_tz, script_tf_);
3555 const auto chart_slot_of = [&](std::int64_t ms) -> std::int64_t {
3556 return pine_time(ms, script_tf_,
"",
"", script_tf_, sym_tz, sym_session);
3560 const auto chart_open_from = [&](std::int64_t at) -> std::int64_t {
3561 if (calendar_chart) {
3564 auto it = std::lower_bound(
input.begin(),
input.end(), at,
3565 [](
const Bar& bar, std::int64_t value) {
3566 return bar.timestamp < value;
3568 while (!input_is_chart && it !=
input.end() && chart_open_of(it->timestamp) < at)
3570 if (it !=
input.end())
return chart_open_of(it->timestamp);
3571 std::int64_t t = at;
3572 for (
int k = 0; k < kTimeOffsetCalendarMinutes; ++k, t += kTimeOffsetMinuteMs) {
3574 const std::int64_t slot = chart_slot_of(t);
3575 return is_na(slot) || slot < at ? t : slot;
3583 const auto chart_open_before = [&](std::int64_t before) -> std::int64_t {
3584 if (calendar_chart) {
3588 auto it = std::lower_bound(
input.begin(),
input.end(), before,
3589 [](
const Bar& bar, std::int64_t value) {
3590 return bar.timestamp < value;
3593 if (held && it !=
input.begin())
return chart_open_of(std::prev(it)->timestamp);
3594 std::int64_t t = before - kTimeOffsetMinuteMs;
3595 for (
int k = 0; k < kTimeOffsetCalendarMinutes; ++k, t -= kTimeOffsetMinuteMs) {
3597 const std::int64_t slot = chart_slot_of(t);
3598 return is_na(slot) || slot >= before ? t : slot;
3605 const auto next_chart_open = [&](std::int64_t open_ms) -> std::int64_t {
3610 auto it = std::upper_bound(
input.begin(),
input.end(), open_ms,
3611 [](std::int64_t value,
const Bar& bar) {
3612 return value < bar.timestamp;
3614 while (!input_is_chart && it !=
input.end() && chart_open_of(it->timestamp) <= open_ms)
3616 if (it !=
input.end())
return chart_open_of(it->timestamp);
3617 const std::int64_t end = chart_close_of(open_ms);
3618 return chart_open_from(
is_na(end) || end <= open_ms ? open_ms + 1 : end);
3624 const auto prior_chart_open = [&](std::int64_t open_ms) -> std::int64_t {
3625 auto it = std::lower_bound(
input.begin(),
input.end(), open_ms,
3626 [](
const Bar& bar, std::int64_t value) {
3627 return bar.timestamp < value;
3630 return chart_open_before(open_ms);
3631 while (it !=
input.begin()) {
3633 const std::int64_t prior = chart_open_of(it->timestamp);
3634 if (prior < open_ms)
return prior;
3639 std::int64_t chart_open = bar_open_ms;
3640 for (
int k = 0; k < -bars_back && !
is_na(chart_open); ++k)
3641 chart_open = next_chart_open(chart_open);
3642 for (
int k = 0; k < bars_back && !
is_na(chart_open); ++k)
3643 chart_open = prior_chart_open(chart_open);
3647 if ((tf.empty() || tf == script_tf_) && session.empty()) {
3648 for (
int k = 0; k < -timeframe_bars_back && !
is_na(chart_open); ++k)
3649 chart_open = next_chart_open(chart_open);
3650 for (
int k = 0; k < timeframe_bars_back && !
is_na(chart_open); ++k)
3651 chart_open = chart_open_before(chart_open);
3653 return close ? chart_close_of(chart_open) : chart_open;
3665 const auto calendar_open_of = [&](std::int64_t ms) -> std::int64_t {
3666 if (calendar_days) {
3669 return pine_time(ms, tf, session, tz, script_tf_, sym_tz, sym_session);
3671 const auto tf_close_of = [&](std::int64_t open_ms) -> std::int64_t {
3672 if (calendar_days) {
3675 return pine_time_close(open_ms, tf, session, tz, script_tf_, sym_tz, sym_session);
3677 const auto tf_open_of = [&](std::int64_t ms) -> std::int64_t {
3678 const std::int64_t open = calendar_open_of(ms);
3679 if (
is_na(open) || !session.empty()
3683 const std::int64_t first = chart_open_from(open);
3684 if (
is_na(first) || calendar_open_of(first) != open)
return open;
3685 const std::int64_t day =
pine_time(first,
"D",
"",
"", script_tf_, sym_tz, sym_session);
3686 return !
is_na(day) && day > open ? day : open;
3688 std::int64_t open = tf_open_of(chart_open);
3694 const int tf_seconds =
tf_to_seconds(tf.empty() ? script_tf_ : tf);
3696 if (timeframe_bars_back != 0 && tf_seconds > 0 && chart_seconds > 0
3697 && tf_seconds < chart_seconds) {
3698 const std::int64_t at = open -
static_cast<std::int64_t
>(timeframe_bars_back)
3699 * tf_seconds * 1000;
3700 const std::int64_t held = chart_open_before(at + 1);
3702 return close ? chart_close_of(held) : held;
3704 for (
int k = 0; k < -timeframe_bars_back && !
is_na(open); ++k) {
3706 const std::int64_t end = tf_close_of(open);
3707 std::int64_t at = chart_open_from(
is_na(end) || end <= open ? open + 1 : end);
3709 for (
int guard = 0; guard < kTimeOffsetBarsPerStep && !
is_na(at); ++guard) {
3710 next = tf_open_of(at);
3711 if (!
is_na(next) && next > open)
break;
3713 at = next_chart_open(at);
3717 for (
int k = 0; k < timeframe_bars_back && !
is_na(open); ++k) {
3719 std::int64_t at = chart_open_before(open);
3721 for (
int guard = 0; guard < kTimeOffsetBarsPerStep && !
is_na(at); ++guard) {
3722 prev = tf_open_of(at);
3723 if (!
is_na(prev) && prev < open)
break;
3725 at = chart_open_before(at);
3730 return close ? tf_close_of(open) : open;
3736 const bool recalculation_due =
scheduler_.coof_recalculation_due(event, context, *
this);
3737 if (!recalculation_due) record_applied_range_end();
3745 && !
adapter_.admission_journal.has_unfinished_events()) {
3754void source::PineStrategyHost::stage_symbol_feed_views() {
3757 "request data: symbol feed view table is out of sync");
3759 std::vector<const NativeInstrumentFeedView*> slots(
symbol_feed_views_.size(),
nullptr);
3762 if (!slot.present)
continue;
3763 descriptors[i] = {{slot.bars, slot.size}, slot.close_ms};
3766 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.
void set_early_close_completes(bool on)
Whether a session ending BEFORE its nominal close completes the running D/W/M bucket on that session'...
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.
std::string entry_id_at(size_type global) const
Trade numeric_at(size_type global) const
static std::uint64_t fold_exit_phase(std::uint64_t digest, std::uint8_t phase) noexcept
~PineRuntimeSecurityScope()
PineRuntimeSecurityScope(const PineStrategyHost &, PineRuntimeSecurityCursor &) noexcept
static const PineRuntimeSecurityCursor * current(const PineStrategyHost &) 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_
PineRuntimeSecurityCursor make_runtime_security_cursor(const std::string &timeframe, bool lookahead, bool gaps, bool lower_array, bool heikinashi=false) const
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
std::string runtime_security_timeframe(const std::string &selected) const
void freeze_script_position_view()
std::uint64_t fixture_applied_receipt_count() const
Series< double > & _src_hlc3_
bool chart_ema_na_warmup_
std::uint64_t runtime_security_frontier() const
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)
NativeCalculationReason
Why the kernel is asking the host to calculate.
NativeLifecycleKind
Where a host stands.
NativeRunPhase
Which driving is Running: a batch run(), a stream's internal warmup, or its realtime leg.
NativePrecommitVerdict
The host is consulted before generic opening-margin admission.
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.
std::optional< RuntimeSubdivision > runtime_subdivision(const std::string &input_timeframe, const std::string &requested_timeframe)
void note_run_failure(BacktestEngine &engine, std::string text, RunFailureCode code, const RunFailureArgs &args={})
bool tf_change(int64_t prev_ms, int64_t curr_ms, const std::string &tf)
Check if prev/curr timestamps cross a timeframe boundary.
bool session_in_market(const std::string &session, const std::string &tz, int64_t bar_ms)
@ request_timeframe_invalid
@ engine_unclassified_error
@ request_timeframe_unsupported
@ symbol_metadata_rejected
std::vector< Bar > runtime_subdivide(const Bar &, RuntimeSubdivision)
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.
std::int64_t requested_index
std::optional< RuntimeSubdivision > subdivision
std::string chart_timeframe
void feed(const RuntimeContextEvent &, const Delivery &, const std::function< void()> &begin_chart)
TimeframeAggregator aggregator
std::function< void(const Bar &, bool)> Delivery
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