PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
pine_strategy_host.cpp
Go to the documentation of this file.
6#include <pineforge/ta.hpp>
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"
14#include "pine_host_reads.hpp"
15#include "pine_intent_state.hpp"
16#include "pine_quiet_bar.hpp"
18#include "pine_run_failure.hpp"
23
24#include <algorithm>
25#include <array>
26#include <cerrno>
27#include <cmath>
28#include <cstring>
29#include <cstdlib>
30#include <ctime>
31#include <iterator>
32#include <limits>
33#include <memory>
34#include <numeric>
35#include <optional>
36#include <stdexcept>
37#include <string>
38#include <system_error>
39#include <utility>
40#include <variant>
41#include <unistd.h>
42
43namespace pineforge {
44using namespace source;
45
46namespace {
47
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,
52}
53
54std::size_t command_scratch_page_size() {
55 errno = 0;
56 const long size = ::sysconf(_SC_PAGESIZE);
57 if (size <= 0)
59 std::system_error(errno ? errno : EINVAL, std::generic_category(),
60 "command scratch platform page size"));
61 return static_cast<std::size_t>(size);
62}
63
64struct SourceExitScratchRun {
65 // Backings precede their stores/context, so borrowers die first.
66 // The native consumer retains this run through source-adapter destruction.
67 // Geometry is fixed independently of the bar and order populations.
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{
71 command_scratch_config(page_size, source::detail::exit_scratch_key_order()),
72 exit_backing.get()};
73 source::detail::ExitScratchContext exit_context{exit, {}};
74 source::detail::ExitScratchVersionsProduction exit_versions{exit};
75
76 explicit SourceExitScratchRun(CacheCapacity capacity)
77 : exit_backing(source::detail::make_command_scratch_backing(page_size, capacity)) {
78 if (exit.status() != ::pineforge::detail::ScratchStatus::ok) {
79 if (exit.status() == ::pineforge::detail::ScratchStatus::io_error) // the store carries a refused workspace allocation
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");
86 }
87 // This head exposes owning NativeOpenLot rows and snapshot readers,
88 // rather than the reference tree's NativeOpenLotView visitor API.
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;
95 };
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;
101 };
102 }
103};
104
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
109};
110
111::pineforge::detail::ScratchBytes scratch_bytes(const void* data, std::size_t size) noexcept {
112 return {static_cast<const std::byte*>(data), size};
113}
114
115::pineforge::detail::ScratchBuffer scratch_buffer(void* data, std::size_t size) noexcept {
116 return {static_cast<std::byte*>(data), size};
117}
118
119::pineforge::detail::ScratchStatus compare_trade_scratch(
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;
131}
132
133void require_trade_scratch(::pineforge::detail::ScratchStatus status,
134 const ::pineforge::detail::ScratchReadback& store) {
135 if (status == ::pineforge::detail::ScratchStatus::ok) return;
141 throw coded<std::logic_error>(code, {},
142 "trade scratch contract operation failed");
143}
144
145// These records are temporary, including on success. Rollback restores the
146// empty store and its handle counter; no scratch state survives a callback.
147class TradeScratchTransaction {
148public:
149 explicit TradeScratchTransaction(::pineforge::detail::CommandScratchStore& store) : store_(store) {
150 require_trade_scratch(store_.begin(), store_);
151 }
152 ~TradeScratchTransaction() {
153 if (store_.transaction_active()) (void)store_.rollback();
154 }
155 void finish() { require_trade_scratch(store_.rollback(), store_); }
156private:
157 ::pineforge::detail::CommandScratchStore& store_;
158};
159
160::pineforge::detail::ScratchHandle scratch_seek(const ::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);
165 if (status != ::pineforge::detail::ScratchStatus::end) require_trade_scratch(status, store);
166 return handle;
167}
168
169TradeScratchKey scratch_key(const ::pineforge::detail::ScratchReadback& store,
171 TradeScratchKey key{};
172 require_trade_scratch(store.read(handle, ::pineforge::detail::ScratchField::key, 0,
173 scratch_buffer(key.data(), sizeof key)), store);
174 return key;
175}
176
177::pineforge::detail::ScratchHandle scratch_find(const ::pineforge::detail::CommandScratchStore& store,
178 const TradeScratchKey& key) {
179 const auto handle = scratch_seek(store, key);
180 return handle != ::pineforge::detail::scratch_no_handle && scratch_key(store, handle) == key
182}
183
184void scratch_put(::pineforge::detail::CommandScratchStore& store, const TradeScratchKey& key,
185 const ::pineforge::detail::ScratchByteSource& value) {
186 ::pineforge::detail::ScratchSpanSource source(scratch_bytes(key.data(), sizeof key));
188 require_trade_scratch(store.put(source, value, handle), store);
189}
190
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);
195}
196
197template<class Visit>
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;
203 visit(handle, key);
205 const auto status = store.next(handle, next);
206 if (status != ::pineforge::detail::ScratchStatus::end) require_trade_scratch(status, store);
207 handle = next;
208 }
209}
210
211template<class T>
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);
216 return word;
217}
218
219template<class T>
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);
223}
224
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(),
235 row.exit_id.size()};
236}
237
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);
256}
257
258// Scalar amendments plus text borrowed from one ledger value. put streams
259// all five spans without retaining the replacement population in RAM.
260class TradeScratchSource final : public ::pineforge::detail::ScratchByteSource {
261public:
262 TradeScratchSource(TradeScratchHeader header, const Trade& text) noexcept
263 : header_(header), parts_{scratch_bytes(header_.data(), sizeof header_),
264 scratch_bytes(text.entry_id.data(), text.entry_id.size()),
265 scratch_bytes(text.entry_comment.data(), text.entry_comment.size()),
266 scratch_bytes(text.exit_comment.data(), text.exit_comment.size()),
267 scratch_bytes(text.exit_id.data(), text.exit_id.size())} {}
268 std::uint64_t size() const noexcept override {
269 std::uint64_t result = 0;
270 for (const auto part : parts_) result += part.size;
271 return result;
272 }
273 ::pineforge::detail::ScratchStatus read(std::uint64_t offset,
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;
286 offset = 0;
287 if (!destination.size) break;
288 }
289 return ::pineforge::detail::ScratchStatus::ok;
290 }
291private:
292 const TradeScratchHeader header_;
293 const std::array<::pineforge::detail::ScratchBytes, 5> parts_;
294};
295
296TradeScratchHeader scratch_read_trade(const ::pineforge::detail::ScratchReadback& store,
298 TradeScratchHeader header{};
299 require_trade_scratch(store.read(handle, ::pineforge::detail::ScratchField::value, 0,
300 scratch_buffer(header.data(), sizeof header)), store);
301 return header;
302}
303
304void scratch_trade_text(const ::pineforge::detail::ScratchReadback& store, ::pineforge::detail::ScratchHandle handle,
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]));
311 require_trade_scratch(store.read(handle, ::pineforge::detail::ScratchField::value, offset,
312 scratch_buffer(text[index]->data(), text[index]->size())), store);
313 offset += text[index]->size();
314 }
315}
316
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 : ""));
323}
324
325// Validate the borrowed public begin array before the source provider stages
326// syminfo, inputs, adapter state, or a native run spec. This mirrors the
327// legacy chart/stream shape checks and deliberately does not impose native
328// calendar or slot-label policy; those remain the generic preflight's job.
329void validate_source_begin_bars(const NativeBeginArgs& args) {
330 if (args.n < 0) {
332 "bar count must be non-negative");
333 }
334 if (args.n > 0 && args.bars == nullptr) {
336 "bars must be non-null for a nonempty array");
337 }
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)",
356 "volume_invalid");
357 }
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");
362 if (i > 0) {
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");
366 if (previous < 0
367 && bar.timestamp > std::numeric_limits<std::int64_t>::max() + previous) {
368 reject_begin_bar(i, "timestamp", " delta exceeds int64 range", "delta_overflow");
369 }
370 }
371 }
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");
377 }
378}
379
380// AR-R4: each installed feed lent to a projected spec. The lent entry holds
381// the bars, closes and column values; the store keeps the instrument, tf and
382// column names. Every allocation happens before the first byte moves, and the
383// moves are swaps, so a throw leaves the store whole.
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;
392 }
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);
398 }
399 return lent;
400}
401
402// The bytes of a configure that did not apply go back where they came from.
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);
410 }
411}
412
413// A legacy setter the generated strategy latched (a request's input or
414// override it could not take) surfaces at the run's begin: the same type and
415// text, coded setting_rejected unless it already carries a code.
416[[noreturn]] void rethrow_latched_setting(const checked_settings::LatchedSettingsFailure& error) {
417 if (dynamic_cast<const RunFailureInfo*>(&error) != nullptr) throw;
419 error.what());
420}
421
422} // namespace
423
425 return {0x5830365830370001ULL, nullptr, compare_trade_scratch};
426}
427
429 require_trade_scratch(store.status(), store);
430 if (store.transaction_active() || store.record_count() != 0
431 || (trade_scratch_ && trade_scratch_->transaction_active()))
433 "trade scratch must be exclusively owned and empty");
434 trade_scratch_ = &store;
435}
436
437::pineforge::detail::CommandScratchStore& source::PineStrategyHost::trade_scratch() {
438 if (!trade_scratch_)
440 "run owner did not provision trade scratch");
441 return *trade_scratch_;
442}
443
446 adapter_(*this, cap),
448 _src_open_(scheduler_.language()._src_open_),
449 _src_high_(scheduler_.language()._src_high_),
450 _src_low_(scheduler_.language()._src_low_),
453 _src_hl2_(scheduler_.language()._src_hl2_),
454 _src_hlc3_(scheduler_.language()._src_hlc3_),
458 // ab9714be LegacyCompatibilityConsumer::refuse was a no-op on this handle.
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();
463 });
464#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
465 // A fresh host folds its empty feed as one cleared at a run's end does.
467#endif
468}
469
471 // Fold current source/generic state with the last script-point
472 // continuation. Recording only controls whether the per-bar array is
473 // retained; the continuation snapshot keeps the scalar independent of
474 // that switch and of NativeCompleted teardown.
475 if (last_script_continuation_valid_) {
476 return broker_state_hash_from_execution_hash(last_script_continuation_hash_);
477 }
478 const std::uint64_t execution = execution_consumer().continuation_hash();
479 // The fold cannot throw: a read of the spilled book that failed inside it
480 // was carried by the core and is thrown here, never folded as a hash it
481 // cut short.
482 as_native_consumer(execution_consumer()).request_core().rethrow_book_read_failure();
483 return broker_state_hash_from_execution_hash(execution);
484}
485
487 // The level is the kernel's (native_liquidation_price, solved from closed
488 // money alone: project() declares NativeLiquidationLevelBase::RealizedOnly),
489 // and na where it has none -- a flat book, a run without a margin model.
490 // TradingView's spelling is the one thing kept here (design row MG8): the
491 // level rounded onto the tick grid away from the position, down for a
492 // long, up for a short.
493 const std::optional<double> level = native_liquidation_price();
494 if (!level) return na<double>();
495 double liquidation = *level;
496 if (syminfo_mintick_ > 0.0) {
497 liquidation = position_side_ == PositionSide::SHORT
498 ? std::ceil(liquidation / syminfo_mintick_) * syminfo_mintick_
499 : std::floor(liquidation / syminfo_mintick_) * syminfo_mintick_;
500 }
501 return liquidation;
502}
503
504PineStrategyConfig source::PineStrategyHost::apply_overrides(
505 PineStrategyConfig config, const StrategyOverrides& overrides) {
506 if (std::isinf(overrides.default_qty_value)) {
509 {{"entrypoint", "strategy_set_override"},
510 {"reason", "number_out_of_finite_range"}},
511 "default_qty_value: number out of finite range");
512 }
513 if (!std::isnan(overrides.initial_capital)) config.initial_capital = overrides.initial_capital;
514 if (!std::isnan(overrides.commission_value)) config.commission_value = overrides.commission_value;
515 if (!std::isnan(overrides.default_qty_value)) config.default_qty_value = overrides.default_qty_value;
516 if (overrides.pyramiding >= 0) config.pyramiding = overrides.pyramiding;
517 if (overrides.slippage >= 0) config.slippage = overrides.slippage;
518 if (overrides.commission_type >= 0) config.commission_type = overrides.commission_type;
519 if (overrides.default_qty_type >= 0) config.default_qty_type = overrides.default_qty_type;
520 if (overrides.process_orders_on_close >= 0)
521 config.process_orders_on_close = overrides.process_orders_on_close != 0;
522 if (overrides.calc_on_order_fills >= 0)
523 config.calc_on_order_fills = overrides.calc_on_order_fills != 0;
524 if (overrides.close_entries_rule >= 0)
525 config.close_entries_rule_any = overrides.close_entries_rule != 0;
526 return config;
527}
528
529StagedConfiguration source::PineStrategyHost::staged_configuration() const {
530 StagedConfiguration staged;
531 staged.syminfo = syminfo_;
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];
545 const auto data = native_instrument_feed_view(feed, slot);
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()});
550 }
551 return staged;
552}
553
555 // Idle abort requests are consumed by the public begin entry even when
556 // input validation refuses before a run starts. Crucially, validation
557 // runs before any provider-owned state is changed.
558 abort_requested_.store(false, std::memory_order_relaxed);
559 validate_source_begin_bars(args);
560 // Admission owns a reentrant begin's refusal. Keep the running caller
561 // span and source state intact until the outer public call unwinds.
562 if (native_state().kind == NativeLifecycleKind::Running) return;
563 if (args.syminfo) {
564 syminfo_ = *args.syminfo;
565 syminfo_mintick_ = syminfo_.mintick;
566 if (std::isfinite(syminfo_.qty_step) && syminfo_.qty_step > 0.0)
567 qty_step_ = syminfo_.qty_step;
568 }
569 if (args.inputs) inputs_ = *args.inputs;
570
571 if (args.is_stream && native_security_feed_enabled()) {
573 RunFailureCode::run_mode_unsupported, {{"feature", "stream_native_security_feed"}},
574 "native request.security feed supports historical runs only");
575 }
576 // Lane XSYM-D: another symbol's feeds and the recorded request series are
577 // installed whole before a run and have no realtime ingress, exactly as
578 // the native feed above.
579 if (args.is_stream && (!symbol_feeds_.empty() || !recorded_series_.empty())) {
581 RunFailureCode::run_mode_unsupported, {{"feature", "stream_symbol_feeds"}},
582 "request.security symbol feeds and recorded request series support historical "
583 "runs only");
584 }
585
586 if (!(args.n < 2 && !args.is_stream)) {
587 std::string effective_input = args.input_tf;
588 if (effective_input.empty() && args.n >= 2 && args.bars != nullptr)
589 effective_input = detect_timeframe(args.bars, args.n);
590 const std::string effective_script = args.script_tf.empty()
591 ? effective_input : args.script_tf;
592 try {
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);
601 }
602 } catch (const std::runtime_error&) {
603 throw;
604 } catch (...) {
605 // The native specification validator owns malformed literals.
606 }
607 }
608
609 if (args.is_stream && config_.calc_on_order_fills) {
611 RunFailureCode::run_mode_unsupported, {{"feature", "stream_calc_on_order_fills"}},
612 "native stream requires close-only calculation; calc_on_order_fills is unsupported");
613 }
616 RunFailureCode::run_mode_unsupported, {{"feature", "stream_probe_overrides"}},
617 "native stream cannot use historical probe/tail overrides");
618 }
619 PineStrategyConfig effective = config_;
620 if (args.overrides_opaque) {
621 const auto* overrides = static_cast<const StrategyOverrides*>(args.overrides_opaque);
622 effective = apply_overrides(effective, *overrides);
623 }
624 const StagedConfiguration staged = staged_configuration();
625 if (!staged.account_fx_effective_from_ms.empty() && effective.calc_on_order_fills)
627 RunFailureCode::run_mode_unsupported, {{"feature", "fx_series_calc_on_order_fills"}},
628 "timestamped account-currency FX does not support calc_on_order_fills");
629 if (!staged.account_fx_effective_from_ms.empty() && args.bar_magnifier)
631 RunFailureCode::run_mode_unsupported, {{"feature", "fx_series_bar_magnifier"}},
632 "timestamped account-currency FX is not supported with bar magnifier");
633
634 adapter_.reset_for_run();
635 adapter_.set_cache_policy_for_run(cache_policy_for_run());
636 for (auto& tape : recorded_series_)
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;
645 // Reserve placement evidence by incarnation once for the disclosed batch:
646 // two legs per bar, including re-issued brackets' historical projections.
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);
650 }
651 adapter_.set_configuration(effective);
652 adapter_.set_staged_configuration(staged);
653 adapter_.set_begin_mode(args.is_stream, args.bar_magnifier);
654 adapter_.set_margin_call_enabled(margin_call_enabled_);
655 scheduler_.capture_begin(args);
656 scheduler_.set_source_series_active(effective.src_series_active);
657 const NativePathOrder path_order = path_order_mode_ == 1
658 ? NativePathOrder::HighFirst
659 : (path_order_mode_ == 2 ? NativePathOrder::LowFirst
660 : NativePathOrder::Auto);
661 // The installed feeds' bytes move into the projected spec (project()
662 // moves them in, and back out if it throws), which the kernel keeps as its
663 // own when it applies it: one copy, read through installed_symbol_feed()
664 // while it is lent (AR-R4). A refusal, or an exception, hands them back.
665 // Nothing of this host or of the script runs inside configure.
666 restore_symbol_feeds();
667 std::vector<NativeInstrumentFeed> lent = lend_feed_bytes(symbol_feeds_);
668 NativeRunSpec spec = [&] {
669 try {
670 const bool has_borrowed_feed = std::any_of(
672 [](const BorrowedFeedSlot& slot) { return slot.present; });
673 if (has_borrowed_feed) {
674 stage_symbol_feed_views();
675 } else {
676 NativeExecutionConsumer::bound(*this).stage_instrument_feed_views(nullptr, 0);
677 }
678 return adapter_.project(effective, staged, args, path_order, &lent);
679 } catch (...) {
680 return_feed_bytes(symbol_feeds_, lent);
681 throw;
682 }
683 }();
684 const bool failed_before = native_state().kind == NativeLifecycleKind::Failed;
685 NativeSetupResult setup;
686 try {
687 // project() establishes the actual run identity and installs a
688 // standalone store. Replace it with the host's service afterwards,
689 // before configure or any run/bar callback can read the adapter.
690 // Keeping this in the feed-return guard also covers binding failure.
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));
695 } catch (...) {
696 return_feed_bytes(symbol_feeds_, spec.instrument_feeds);
697 throw;
698 }
699 if (setup.status == NativeSetupStatus::Applied) {
701 } else {
702 return_feed_bytes(symbol_feeds_, spec.instrument_feeds);
703 }
704 if (setup.status != NativeSetupStatus::Applied) {
705 // configure() recorded the cause (its own text and code): the refusal
706 // below keeps its text and carries that code.
708 if (cause.code == RunFailureCode::none)
710 if (failed_before) {
711 try { prepare_script_run(nullptr, 0, false); }
712 catch (const checked_settings::LatchedSettingsFailure& error) {
713 rethrow_latched_setting(error);
714 }
715 catch (...) {}
716 }
717 throw coded<std::logic_error>(std::move(cause),
718 "Pine native adapter failed to configure projected run spec");
719 }
720 config_ = effective;
722}
723
725 NativeExecutionConsumer::bound(*this).provision_run_command_scratch(
726 NativeExecutionConsumer::CommandScratchBinding::Trade,
727 trade_scratch_order(), trade_scratch_);
728 // The adapter reads the command journal with its receipt cursor and
729 // acknowledges it as it moves (observe_terminal_receipts); acknowledging
730 // "nothing read yet" here marks this host as one that polls, so the
731 // kernel keeps every event above the cursor until the adapter has seen it.
732 native_acknowledge_events(0);
733 // The spec facts the per-bar callbacks read, once for the run (R5 lane
734 // D2-C, pine_host_reads.hpp).
736 // A selected window's pre-roll capture state is parked for this run alone
737 // (pine_intent_state.hpp). It sits outside the try below on purpose: an
738 // allocation failure fails the begin instead of becoming a script preparation
739 // failure, and nothing is dropped silently.
744 session_ismarket_ = false;
745 session_isfirstbar_ = false;
746 session_islastbar_ = false;
750 // Generated configure_security_evaluators() registers this run's foreign
751 // sites again inside run_begin; a run that configures none has none.
755 try {
757 scheduler_.run_begin(*this);
758 } catch (const checked_settings::LatchedSettingsFailure& error) {
759 rethrow_latched_setting(error);
760 } catch (const std::exception& error) {
761 if (adapter_.stream_mode_) throw;
763 note_run_failure(*this, std::string(error.what()), classify_run_failure(error));
764 } catch (...) {
765 if (adapter_.stream_mode_)
767 "unknown error during Pine script preparation");
769 note_run_failure(*this, "unknown error during Pine script preparation",
771 }
772}
773
774std::int64_t source::PineStrategyHost::symbol_calendar_open_before(std::int64_t before) const {
775 if (symbol_calendar_.empty() || before <= symbol_calendar_.front().first
776 || before > symbol_calendar_.back().second) {
777 return na<std::int64_t>();
778 }
779 const bool daily = calendar_period_for(script_tf_) != CalendarPeriod::NONE;
780 const std::int64_t step = daily ? 0 : static_cast<std::int64_t>(script_tf_seconds_) * 1000;
781 // The last day opening before `before`, and its last instant before it.
782 const auto it = std::lower_bound(
783 symbol_calendar_.begin(), symbol_calendar_.end(), before,
784 [](const std::pair<std::int64_t, std::int64_t>& day, std::int64_t value) {
785 return day.first < value;
786 });
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;
791}
792
793// The symbol calendar the C ABI named day by day (set_syminfo_metadata): every
794// day of the count it announced, or the run fails naming what is missing.
796 if (symbol_calendar_days_ < 0) return;
797 // The announced days are this run's: taken whole, so a later run's
798 // announcement starts from none (a day it leaves out is missing, never
799 // an earlier run's).
800 const long long count = symbol_calendar_days_;
801 const std::map<long long, double> opens = std::move(symbol_calendar_opens_);
802 const std::map<long long, double> closes = std::move(symbol_calendar_closes_);
806 // The announcement replaces the calendar: one it cannot install leaves
807 // none behind for a later run to read.
808 symbol_calendar_.clear();
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)) {
816 RunFailureCode::symbol_metadata_rejected, {{"field", "session_calendar"}},
817 "symbol calendar: session day " + std::to_string(i) + " of "
818 + std::to_string(count) + " is missing");
819 }
820 sessions.emplace_back(std::llround(open->second), std::llround(close->second));
821 }
822 if (!set_symbol_calendar(std::move(sessions))) {
824 {{"field", "session_calendar"}},
825 "symbol calendar: session days out of order");
826 }
827}
828
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;
834 }
835 symbol_calendar_ = std::move(sessions);
836 return true;
837}
838
839const std::pair<std::int64_t, std::int64_t>* source::PineStrategyHost::symbol_calendar_day(
840 std::int64_t ms) const {
841 const auto it = std::upper_bound(
842 symbol_calendar_.begin(), symbol_calendar_.end(), ms,
843 [](std::int64_t value, const std::pair<std::int64_t, std::int64_t>& day) {
844 return value < day.first;
845 });
846 if (it == symbol_calendar_.begin()) return nullptr;
847 const auto& day = *std::prev(it);
848 return ms < day.second ? &day : nullptr;
849}
850
851std::int64_t source::PineStrategyHost::symbol_calendar_open_from(std::int64_t at) const {
852 if (symbol_calendar_.empty() || at < symbol_calendar_.front().first)
853 return na<std::int64_t>();
854 const bool daily = calendar_period_for(script_tf_) != CalendarPeriod::NONE;
855 const std::int64_t step = daily ? 0 : static_cast<std::int64_t>(script_tf_seconds_) * 1000;
856 auto it = std::upper_bound(
857 symbol_calendar_.begin(), symbol_calendar_.end(), at,
858 [](std::int64_t value, const std::pair<std::int64_t, std::int64_t>& day) {
859 return value < day.second;
860 });
861 for (; it != symbol_calendar_.end(); ++it) {
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;
866 }
867 return na<std::int64_t>();
868}
869
870void source::PineStrategyHost::capture_script_continuation_hash() {
871 // Taken at once: since v19 the fold is the live state alone, which is what
872 // a deferred view would have recorded word for word (R5 lane V19-A).
873 const std::uint64_t execution = execution_consumer().continuation_hash();
874 // A read of the spilled book that failed inside the noexcept fold is
875 // thrown here, before the hash it cut short is latched as valid; the
876 // latch keeps what it held.
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();
882 }
883}
884
886 const Bar& bar, const NativeInputContext& context) {
887 if (source_prepare_failed_) return;
888 if (detail::run_phase(*this) == NativeRunPhase::Realtime)
889 stream_warmup_mode_ = false;
891 scheduler_.input(bar, context, *this);
892 // Aggregation can deliver leftover input after the last script callback.
893 // Refresh the last recorded row (and the continuation snapshot) so the
894 // scalar stays the same fold with or without recording.
895 if (scheduler_.terminal_source_bar()) capture_script_continuation_hash();
896}
897
899 const Bar& tick, const NativeTickContext& context) {
900 if (source_prepare_failed_) return;
901 if (detail::run_phase(*this) == NativeRunPhase::Realtime)
902 stream_warmup_mode_ = false;
903 scheduler_.tick(tick, context, *this);
904 adapter_.on_tick(tick, context);
905}
906
908 const Bar& bar, const NativeDecisionContext& context) {
909 if (source_prepare_failed_) return;
910 // Foreign sites, after the input's deliveries and before the bar's
911 // calc_on_order_fills checkpoint (PineScheduler::bar_open), so every
912 // calculation of the bar reads the same values: on a calendar chart the
913 // bars TradingView's merge rule makes visible at this bar that the kernel
914 // has not handed over yet run first; then a gaps_on site that ran on no
915 // new bar reads na.
916 if (!foreign_security_sites_.empty()) {
917 read_ahead_foreign_security_sites(bar);
918 clear_gapped_foreign_security_sites();
919 }
920 bar_magnifier_enabled_ = scheduler_.bar_magnifier_enabled();
921 diag_magnifier_sub_bars_processed_ = bar_magnifier_enabled_
922 ? static_cast<std::int64_t>(context.driver_statistics.sub_bars_processed) : 0;
923 diag_magnifier_sample_ticks_processed_ = bar_magnifier_enabled_
924 ? static_cast<std::int64_t>(context.driver_statistics.sample_ticks_processed) : 0;
925 // A selected window's pre-roll rows are placed here, once, before the adapter
926 // reads the bar's open: it must see them as the prior bar's orders
927 // (selected_replay_at_open). Nothing happens for any other run.
928 selected_replay_at_open(bar, context);
929 adapter_.on_bar_open(bar, context);
930 scheduler_.bar_open(bar, context, *this);
931}
932
934 const Bar&, const NativeDecisionContext&) {
935 if (source_prepare_failed_) return;
936 scheduler_.sub_bar_complete();
937}
938
940 const Bar& bar, const NativeDecisionContext& context) {
941 if (source_prepare_failed_) return;
942 bar_magnifier_enabled_ = scheduler_.bar_magnifier_enabled();
943 diag_magnifier_sub_bars_processed_ = bar_magnifier_enabled_
944 ? static_cast<std::int64_t>(context.driver_statistics.sub_bars_processed) : 0;
945 diag_magnifier_sample_ticks_processed_ = bar_magnifier_enabled_
946 ? static_cast<std::int64_t>(context.driver_statistics.sample_ticks_processed) : 0;
947 adapter_.observe_terminal_receipts();
948 adapter_.on_bar_close_before_script(bar, context);
949 scheduler_.bar(bar, context, *this);
950 adapter_.on_bar_close(bar, context);
951 if (context.is_terminal_sub_bar
953 scheduler_record_range_end(bar);
954 }
955 const bool recording = broker_state_hash_recording_ && !broker_state_hashes_.empty();
956 const bool last_batch = context.is_terminal_sub_bar
958 const bool stream_script = context.is_terminal_sub_bar
959 && stream_phase_ == StreamPhase::REALTIME;
960 if (recording || last_batch || stream_script) {
961 // ab9714be pine_scheduler.cpp:1753/:1875 records after dispatch_bar,
962 // including the terminal source policy updates. The native hook
963 // returns through adapter_.on_bar_close after the scheduler callback,
964 // so refresh the continuation snapshot (and the just-appended row)
965 // at that boundary.
966 capture_script_continuation_hash();
967 }
968}
969
972 const NativeDecisionContext& context) { adapter_.invalidate_exit_group(event.first_trade_index, event.closed_trade_count); // Covers later close_cause, exit_time and exit_id writes; no exit-group sort intervenes.
973 // R5 lane F3: the kernel records CloseCause::Bracket for a close its
974 // owner's fill armed (WaitForApplied). A Pine row keeps the cause this host
975 // has always given it -- a strategy.exit leg by its order family
976 // (exit_from_bracket), a strategy.close armed on a reversal parent as a
977 // script close -- so the kernel's reading is not taken on these rows.
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()
981 && trades_[index].close_cause == execution::CloseCause::Bracket) {
982 trades_.modify(index, [](Trade& row) {
984 });
985 }
986 }
987 if (source_prepare_failed_) return;
988 const bool aggregated = detail::run_aggregates_input_bars(
989 detail::run_consumer(*this), &adapter_, adapter_.run_counter_);
990 // TradingView dates every fill of a chart bar at the bar's open, a
991 // process_orders_on_close fill included (the e25-f-islastbar tape, lane
992 // E27). The chart timeframe's zero-width interval gives exactly that:
993 // every point of a confirmed bar's modeled path (its open, the segments,
994 // the calculation, the close after it) sits at the bar's label. On an
995 // aggregated chart the kernel dates a close point at the bucket's close,
996 // and books a fill the host executes at such a point (a
997 // calc_on_order_fills first-open chain, a same-bar cover) at its decision
998 // floor, which the bucket's inputs have already carried to the close on
999 // the no-path route. The adapter dates the rows it presents at the chart
1000 // bar's open: every modeled point of an aggregated chart, as ab9714be's
1001 // aggregation loop did, and under the magnifier too -- each sub-bar's
1002 // open, path and close (ModeledOHLCClose, which only a magnified run
1003 // presents). TradingView dates every magnified fill at its chart bar's
1004 // open, 218 of 218 on H-MEASURE's sixteen tapes; ab9714be kept a sub-bar
1005 // fill's own instant, and so did this adapter for all but the close
1006 // points until R5 lane PAR-ORDERS. A realtime print keeps its own
1007 // instant, as it does on the chart timeframe, and so does a stream's
1008 // quiet carried interval, whose context still names the bar before it.
1009 // The kernel's own instant, which its FX lookup, decision floor and
1010 // ordering read, is untouched (lane F1).
1011 const auto provenance = context.coordinate.provenance;
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) {
1018 const std::int64_t script_open = context.script_bar_open_ms;
1019 if (event.opened_units != 0.0) {
1020 for (auto& lot : pyramid_entries_) {
1021 if (lot.entry_incarnation == event.handle().incarnation)
1022 lot.time = script_open;
1023 }
1024 // A later execution of the same pass can close this fill's lot
1025 // before this notification arrives (the reversing second call
1026 // of a process_orders_on_close pair): the rows it booked after
1027 // this event's own still carry the kernel's instant (lane
1028 // FIX-E1E2, tests/fixtures/aggregated_admission_receipt).
1029 for (std::size_t index = event.first_trade_index + event.closed_trade_count;
1030 index < trades_.size(); ++index) {
1031 if (trades_[index].entry_incarnation == event.handle().incarnation)
1032 { adapter_.invalidate_exit_group(index); trades_.modify(index, [&](Trade& row) { row.entry_time = script_open; }); }
1033 }
1034 }
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; });
1039 }
1040 }
1041 const auto p = adapter_.placement_.find(event.handle().incarnation);
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));
1045 if (event.opened_units != 0.0) {
1046 // ab9714be pine_orders.cpp:750-753 and pine_fills.cpp:7189-7193
1047 // A MARKET entry that adds to a live same-side position is
1048 // flagged so a same-bar from_entry bracket exit covers it.
1049 const bool market_add = !pine_priced && event.closed_units == 0.0
1050 && p != adapter_.placement_.end()
1051 && (p->second.family == PineOrderFamily::Entry
1052 || p->second.family == PineOrderFamily::Order)
1053 && std::any_of(pyramid_entries_.begin(), pyramid_entries_.end(),
1054 [&](const PyramidEntry& lot) {
1055 return lot.entry_incarnation != event.handle().incarnation;
1056 });
1057 if (market_add) adapter_.mark_market_pyramid_add(event.handle().incarnation);
1058 }
1059 // The legacy source observer counted one broker fill for every committed
1060 // execution event. The native consumer owns those events now; mirror the
1061 // count at its notification boundary so restored source tests and public
1062 // source-side policy reads see the same monotone value.
1063 // ab9714be pine_fills.cpp:5954/:6300 and the margin/FX sites: one source
1064 // broker fill sequence is consumed per applied broker instruction, not
1065 // per closed trade row. Native ordinals remain the execution authority;
1066 // this is the generated/source-visible diagnostic projection.
1067 if (broker_fill_event_seq_ == std::numeric_limits<std::uint64_t>::max()) {
1069 "source broker fill sequence exhausted");
1070 }
1071 ++broker_fill_event_seq_;
1072 if (position_side_ != PositionSide::FLAT) {
1073 // ab9714be engine_orders.cpp:531-541: settle_position_after_partial_exit resets to flat when position_qty_ <= kQtyEpsilon or empty
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);
1078 changed = true;
1079 } else {
1080 ++it;
1081 }
1082 }
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;
1091 }
1092 position_qty_ = total_qty;
1093 position_entry_price_ = weighted_price / total_qty;
1094 position_entry_count_ = static_cast<int>(pyramid_entries_.size());
1095 }
1096 }
1097 // Read before the adapter retires the legs this fill leaves without a lot.
1098 bool other_exit_here = false;
1099 if (event.closed_trade_count >= 2 && event.first_trade_index < trades_.size()) {
1100 const auto own = adapter_.placement_.find(event.handle().incarnation);
1101 other_exit_here = own == adapter_.placement_.end()
1102 || adapter_.other_exit_reaches(trades_[event.first_trade_index].exit_price,
1103 trades_[event.first_trade_index].is_long,
1104 own->second.source_id, event.handle());
1105 }
1106 adapter_.on_applied(event, context);
1107 precommit_held_units_ = std::numeric_limits<double>::quiet_NaN();
1108 if (adapter_.take_intraday_loss_relabel(event.ordinal)) {
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) {
1113 row.exit_id.clear();
1115 });
1116 }
1117 }
1118 // R5 lane L12 (2.ii l): the kernel used to read these strings back out of
1119 // the row to answer strategy_closed_trade_close_cause. They are the
1120 // adapter's, so the adapter's host states the cause instead, on the rows
1121 // this event produced and after the relabel above has settled them. The
1122 // predicates are the retired kernel branches verbatim, including the
1123 // empty-exit-id gate and the filled-orders PREFIX match.
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;
1127 Trade row = trades_[index]; const auto previous_cause = row.close_cause;
1128 if (row.exit_id == source::kMarginCallLabel) {
1130 } else if (row.exit_id.empty()) {
1131 if (row.exit_comment.rfind(source::kFillCapCommentPrefix, 0) == 0)
1133 else if (row.exit_comment.rfind(source::kIntradayLossComment, 0) == 0)
1135 }
1136 adapter_.invalidate_exit_group(index); if (row.close_cause != previous_cause || index < trades_.base()) trades_.replace(index, row);
1137 }
1138 project_short_seed_report_rows(event);
1139 if (!other_exit_here) order_global_exit_children(event);
1140 scheduler_.applied(event);
1141 // R6: with calc_on_order_fills the kernel drives this event's
1142 // recalculation next, in the same drain iteration and at the same cursor
1143 // (NativeCalculationTrigger::BarCloseAndFills). The range-end row stays
1144 // after it, exactly where the adapter's own cascade used to put it.
1145 // Finish the same range-end mark, then any post-calculation journal tail.
1146 finish_applied_admission(event, context);
1147 // The observer seam: once per applied event, after every row this event
1148 // produced has settled. Empty unless a host overrides it.
1149 on_native_applied_event(event, context);
1150}
1151
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);
1157 }
1158}
1159
1161 const Bar& bar, const NativeDecisionContext& context,
1164 if (reason != NativeCalculationReason::OrderFill) {
1165 // BarClose is the script bar's own calculation; Tick and SubBar are
1166 // cadences the Pine spec never selects.
1167 on_native_bar(bar, context);
1168 return;
1169 }
1170 if (source_prepare_failed_ || cause == nullptr) return;
1171 // A fill Pine does not recalculate on (POOC's terminal close fill, a
1172 // grouped-stop sibling, a replayed ordinal) still spends a kernel
1173 // recalculation slot; it publishes nothing and books nothing.
1174 if (!scheduler_.coof_recalculation_due(*cause, context, *this)) return;
1175 scheduler_.recalculate(*cause, context, *this);
1176 record_applied_range_end();
1177}
1178
1183
1185 const NativeMarginCheckPoint& point) const {
1186 return adapter_.margin_check_allowed(point);
1187}
1188
1190 const NativeMarginRequirementView& view) const {
1191 return adapter_.resolve_margin_requirement(view);
1192}
1193
1195 const NativeMarginCallView& view) const {
1196 return adapter_.resolve_margin_call_units(view);
1197}
1198
1200 const NativeAnchoredLevelView& view) const {
1201 return adapter_.resolve_anchored_level(view);
1202}
1203
1205 const NativePrecommitView& view) const {
1206 const auto verdict = adapter_.validate_precommit(view);
1207 precommit_held_units_ = std::abs(detail::run_position(*this).signed_units);
1208 return verdict;
1209}
1210
1212 guard_native_mutation("configure_pine_strategy");
1214 config_ = config;
1215 adapter_.set_configuration(config_);
1216 scheduler_.set_source_series_active(config_.src_series_active);
1218}
1219
1221 guard_native_mutation("set_strategy_override");
1222 override_ = overrides;
1223 config_ = apply_overrides(config_, override_);
1224 adapter_.set_configuration(config_);
1225 scheduler_.set_source_series_active(config_.src_series_active);
1227}
1228
1229void source::PineStrategyHost::set_syminfo_session(const std::string& session) {
1230 BacktestEngine::set_syminfo_session(session);
1231}
1232
1233// The six risk setters apply live as they always did. While a selected window's
1234// pre-roll evaluation captures they also append an ordered row, so the replay
1235// can set them again in the script's own order against the saved configuration.
1236// The three int arguments cannot represent NaN/infinity; their existing
1237// direction/count/cap behavior needs no numeric admission.
1240 adapter_.set_risk_direction(direction);
1241}
1242
1245 adapter_.set_risk_max_cons_loss_days(value);
1246}
1247
1249 admit_pine_risk_max_drawdown(value, percent);
1251 adapter_.set_risk_max_drawdown(value, percent);
1252}
1253
1255 admit_pine_risk_max_intraday_loss(value, percent);
1257 adapter_.set_risk_max_intraday_loss(value, percent);
1258}
1259
1262 if (limit > 0) adapter_.cap.adopt_script_count();
1263 adapter_.cap = limit;
1264}
1265
1271
1273 // Inside a foreign site's payload, bar_index is the requested context's.
1274 if (foreign_context_ != nullptr) return static_cast<int>(foreign_context_->delivered - 1);
1275 return source_bar_index_ + scheduler_.bar_index_offset();
1276}
1277
1279 return source_last_bar_index_ + scheduler_.bar_index_offset();
1280}
1281
1283 return scheduler_.is_first_tick();
1284}
1285
1287 return scheduler_.is_last_tick();
1288}
1289
1291 NativeDecisionContext context;
1292 if (const auto point = current_execution_point()) {
1293 context = point->decision;
1294 } else {
1295 context.coordinate.interval_index = bar_index_;
1296 context.sub_bar_open_ms = current_bar_.timestamp;
1297 context.script_bar_open_ms = current_bar_.timestamp;
1298 }
1299 const BarTime time = fixture_chart_time(context.sub_bar_open_ms);
1300 return {context.sub_bar_open_ms,
1301 syminfo_.session.empty() ? "24x7" : syminfo_.session,
1302 syminfo_.timezone.empty() ? "UTC" : syminfo_.timezone,
1303 time.dayofmonth, time.month};
1304}
1305
1307 NativeDecisionContext context;
1308 if (const auto point = current_execution_point()) {
1309 context = point->decision;
1310 } else {
1311 context.coordinate.interval_index = bar_index_;
1312 context.sub_bar_open_ms = current_bar_.timestamp;
1313 context.script_bar_open_ms = current_bar_.timestamp;
1314 }
1315 return adapter_.cap_calculation(context);
1316}
1317
1322
1323source::PineStrategyHost::BarTime source::PineStrategyHost::fixture_chart_time(
1324 std::int64_t timestamp_ms) const {
1325 const std::time_t seconds = static_cast<std::time_t>(timestamp_ms / 1000);
1326 std::tm tm{};
1327 const auto utc = [&]() {
1328 return ::gmtime_r(&seconds, &tm) != nullptr;
1329 };
1330 if (chart_timezone_.empty() || chart_timezone_ == "UTC"
1331 || chart_timezone_ == "Etc/UTC") {
1332 (void)utc();
1333 } else {
1334 try {
1335 tz_util::ScopedTimezone guard(chart_timezone_);
1336 if (::localtime_r(&seconds, &tm) == nullptr) (void)utc();
1337 } catch (...) {
1338 (void)utc();
1339 }
1340 }
1341 BarTime result;
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;
1350 return result;
1351}
1352
1354 as_native_consumer(execution_consumer()).set_retention_override(NativeEventRetention::Full);
1355}
1356
1358 // Every ExecutionAppliedEvent the run committed. The adapter acknowledges
1359 // the journal as it reads it, so the window no longer holds the whole
1360 // run; the kernel's own count of the events it committed is the same
1361 // number the journal scan made when it did.
1362 return as_native_consumer(execution_consumer()).applied_event_count();
1363}
1364
1366 return scheduler_.history_advances_new_bar();
1367}
1368
1369
1371 return scheduler_.previous_chart_close();
1372}
1373
1375 return adapter_.pending_intent_view().last_bar_dual_entry_path();
1376}
1377
1379 if (position_side_ == PositionSide::FLAT) return 0.0;
1380 double paid = 0.0;
1381 for (const auto& lot : pyramid_entries_) {
1382 if (lot.qty > internal::kQtyEpsilon) paid += open_entry_commission(lot);
1383 }
1384 return paid;
1385}
1386
1388 return scheduler_.script_position_view(bar_index_, position_side_, position_qty_);
1389}
1390
1392 const double tick = syminfo_mintick_;
1393 if (!source::detail::script_rule_switches().equity_tick_mark || !std::isfinite(price)
1394 || !std::isfinite(tick) || !(tick > 0.0)) {
1395 return price;
1396 }
1397 const double ticks = price / tick;
1398 const double nearest = std::floor(ticks + 0.5);
1399 // On the grid up to the quotient's own rounding: the close itself.
1400 if (std::abs(ticks - nearest) <= 1e-9 * std::max(1.0, std::abs(ticks))) return price;
1401 return nearest * tick;
1402}
1403
1405 scheduler_.freeze_script_position_view(
1406 bar_index_, position_side_, position_qty_, pyramid_entries_);
1407}
1408
1410 scheduler_.clear_script_position_view();
1411}
1412
1414 // Only a book the call left flat: any open lot keeps its own average.
1415 if (position_side_ != PositionSide::FLAT || !std::isfinite(average) || !(average > 0.0))
1416 return;
1417 position_entry_price_ = average;
1418 script_average_held_ = true;
1419}
1420
1422 if (!script_average_held_) return;
1423 script_average_held_ = false;
1424 // reset_position_state_to_flat's value; a fill since the hold that
1425 // opened a position set its own average.
1426 if (position_side_ == PositionSide::FLAT) position_entry_price_ = 0.0;
1427}
1428
1429const Series<double>& source::PineStrategyHost::source_series(const std::string& key) const {
1430 return scheduler_.source_series(key);
1431}
1432
1434 const std::string& key, const Series<double>& fallback) const {
1435 const auto found = inputs_.find(key);
1436 if (found == inputs_.end() || found->second.empty()) return fallback;
1437 try {
1438 return scheduler_.source_series(found->second);
1439 } catch (const std::invalid_argument&) {
1440 return fallback;
1441 }
1442}
1443
1447
1451
1455
1457 return adapter_.admission_journal;
1458}
1459
1460std::vector<admission::Field> source::PineStrategyHost::market_admission_fields() const {
1461 std::vector<admission::Field> fields;
1462 adapter_.admission_journal.reflect("journal", [&](const admission::Field& field) {
1463 fields.push_back(field);
1464 });
1465 return fields;
1466}
1467
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);
1471}
1472
1474 return pending_intent_view().level_resolved(index);
1475}
1476
1478 int index, double* stop, double* limit, double* trail_activation) const {
1479 return pending_intent_view().effective_levels(index, stop, limit, trail_activation);
1480}
1481
1483 return adapter_.pending_intent_view();
1484}
1485
1487 native_order::RequestHandle handle) const noexcept {
1488 return adapter_.short_seed_collision_role_v1(std::move(handle));
1489}
1490
1492 adapter_.enable_intraday_cap();
1493}
1494
1496 adapter_.attach_execution_adapter();
1497 // Generated constructors attach the source execution bridge before their
1498 // risk statements and metadata arrive. The intraday-cap configuration is
1499 // part of that same source-policy attachment; leaving it detached makes a
1500 // later max_intraday_filled_orders declaration silently inert.
1501 adapter_.enable_intraday_cap();
1502}
1503
1505 const std::string& key, double value) {
1506 if (key == "report_terminal_quote_time_ms") {
1507 if (NativeExecutionConsumer::selected_query_closed(*this)) {
1509 }
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;
1513 return;
1514 }
1515 if (key == "report_terminal_quote_close") {
1516 if (NativeExecutionConsumer::selected_query_closed(*this)) {
1518 }
1519 report_quote_close_ = std::isfinite(value) && value > 0.0
1520 ? value : std::numeric_limits<double>::quiet_NaN();
1521 return;
1522 }
1523 BacktestEngine::set_syminfo_metadata(key, value);
1524 // TradingView's symbol calendar (set_symbol_calendar) over the C ABI:
1525 // "symbol_calendar_days" = N, then "symbol_calendar_open:<i>" and
1526 // "symbol_calendar_close:<i>" for i < N, installed (and the days consumed)
1527 // when the run begins. A count that is not a number >= 0 removes the
1528 // calendar; a day key whose index is not a decimal number is no day.
1529 if (key.compare(0, 16, "symbol_calendar_") == 0) {
1530 if (key == "symbol_calendar_days") {
1531 if (std::isfinite(value) && value >= 0.0 && value <= 1e9) {
1532 symbol_calendar_days_ = std::llround(value);
1533 } else {
1535 symbol_calendar_opens_.clear();
1537 symbol_calendar_.clear();
1538 }
1539 }
1540 for (const auto& [prefix, days] : {std::pair<const char*, std::map<long long, double>*>{
1541 "symbol_calendar_open:", &symbol_calendar_opens_},
1542 {"symbol_calendar_close:", &symbol_calendar_closes_}}) {
1543 const std::size_t n = std::char_traits<char>::length(prefix);
1544 if (key.size() <= n || key.size() - n > 9 || key.compare(0, n, prefix) != 0) continue;
1545 long long index = 0;
1546 bool digits = true;
1547 for (std::size_t i = n; i < key.size(); ++i) {
1548 if (key[i] < '0' || key[i] > '9') {
1549 digits = false;
1550 break;
1551 }
1552 index = index * 10 + (key[i] - '0');
1553 }
1554 if (digits) (*days)[index] = value;
1555 }
1556 return;
1557 }
1558 if (key == "bar_index_offset") {
1559 scheduler_.set_bar_index_offset(std::isfinite(value)
1560 ? static_cast<int>(std::llround(value)) : 0);
1561 }
1562 if (key == "security_range_start_na_warmup") {
1563 if (std::isfinite(value) && value > 0.0) {
1565 security_range_start_ms_ = static_cast<int64_t>(std::llround(value));
1566 } else {
1569 }
1570 }
1571 if (key == "chart_ema_na_warmup")
1572 chart_ema_na_warmup_ = std::isfinite(value) && value > 0.0;
1573 if (key == "historical_security_lookahead_projection")
1574 historical_security_lookahead_projection_ = std::isfinite(value) && value > 0.0;
1575 if (key == "margin_long" && config_.margin_long == 100.0)
1576 config_.margin_long = (std::isfinite(value) && value > 0.0) ? value : 100.0;
1577 if (key == "margin_short" && config_.margin_short == 100.0)
1578 config_.margin_short = (std::isfinite(value) && value > 0.0) ? value : 100.0;
1579 adapter_.set_configuration(config_);
1580 adapter_.priority.metadata(key, value);
1581 adapter_.cap.metadata(key, value);
1582}
1583
1584bool source::PineStrategyHost::set_report_terminal_quote(std::int64_t time_ms, double close) {
1585 if (NativeExecutionConsumer::selected_query_closed(*this) || time_ms < 0
1586 || time_ms > 9007199254740991LL || !std::isfinite(close) || close <= 0.0) return false;
1587 report_quote_time_ms_ = time_ms;
1588 report_quote_close_ = close;
1589 return true;
1590}
1591
1593 if (NativeExecutionConsumer::selected_query_closed(*this)) {
1595 }
1596 report_quote_time_ms_ = -1;
1597 report_quote_close_ = std::numeric_limits<double>::quiet_NaN();
1598}
1599
1601 return pending_intent_view().last_bar_dual_entry_path();
1602}
1603
1607
1609 int index, pf_pending_order_v1_t* out) const {
1610 return pending_intent_view().copy_v1(index, out);
1611}
1612
1614 int index, double fill_price, double* qty, int* close_only, int* partition) const {
1615 return pending_intent_view().probe_fill_qty(index, fill_price, qty, close_only, partition);
1616}
1617
1619 return pending_intent_view().level_resolved(index);
1620}
1621
1623 int index, double* stop, double* limit, double* trail_activation) const {
1624 return pending_intent_view().effective_levels(index, stop, limit, trail_activation);
1625}
1626
1628 return adapter_.pending_intent_view().trail_best_price();
1629}
1630
1631void source::PineStrategyHost::adapter_label_bracket_trades(
1632 const native_order::ExecutionAppliedEvent& event, bool from_bracket) {
1633 // ab9714be pine_fills.cpp:6232-6252: every trade row emitted by a real
1634 // strategy.exit leg carries the bracket cause; strategy.close and
1635 // close_all requests remain script closes.
1636 for (std::size_t offset = 0; offset < event.closed_trade_count; ++offset) {
1637 const std::size_t index = event.first_trade_index + offset;
1638 if (index >= trades_.size()) continue;
1639 adapter_.invalidate_exit_group(index); trades_.modify(index, [&](Trade& trade) { trade.exit_from_bracket = from_bracket; });
1640 }
1641}
1642
1643bool source::PineStrategyHost::adapter_has_open_entry_id(
1644 const std::string& id) const {
1645 return std::any_of(pyramid_entries_.begin(), pyramid_entries_.end(),
1646 [&](const PyramidEntry& row) { return row.entry_id == id && row.qty > 0.0; });
1647}
1648
1649const std::vector<source::PineStrategyHost::FixtureIntentRow>&
1652 source_pending_view_cache_.reserve(adapter_.pending_same_bar_commands_.size()
1653 + adapter_.pending_entries_.size() + adapter_.pending_bracket_legs_.size()
1654 + adapter_.pending_coof_requests_.size()
1655 + adapter_.delayed_market_orders_.size()
1656 + adapter_.source_shadow_pending_.size() + adapter_.live_handles_.size());
1657 const auto append = [&](const PlacementSnapshot& snapshot, const std::string& label) {
1658 FixtureIntentKind type = FixtureIntentKind::MARKET;
1659 switch (snapshot.family) {
1667 type = FixtureIntentKind::EXIT;
1668 break;
1670 type = FixtureIntentKind::RAW_ORDER;
1671 break;
1673 break;
1674 }
1675 FixtureIntentRow row;
1676 row.id = snapshot.family == PineOrderFamily::Close
1677 ? "__close__" + snapshot.source_id
1678 : (snapshot.frozen_market_targeted_close ? label : snapshot.source_id);
1679 row.type = type;
1680 const bool default_stop = snapshot.family == PineOrderFamily::Entry
1681 && !std::isfinite(snapshot.exit_levels.limit)
1682 && std::isfinite(snapshot.exit_levels.stop)
1683 && std::isnan(snapshot.requested_qty)
1684 && config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
1685 && config_.default_qty_value <= 100.0;
1686 const double absent = std::numeric_limits<double>::quiet_NaN();
1687 row.default_stop_placement_qty = default_stop ? snapshot.sizing.frozen_units : absent;
1688 row.default_stop_sizing_price = snapshot.sizing.price;
1689 row.frozen_market_own_units = snapshot.frozen_market_own_units;
1690 row.frozen_market_transaction_units = snapshot.frozen_market_transaction_units;
1691 row.from_entry = snapshot.from_entry;
1692 row.is_long = snapshot.is_long;
1693 row.qty = snapshot.family == PineOrderFamily::Close
1694 ? snapshot.requested_qty
1695 : (std::isfinite(snapshot.projection_remaining_qty)
1696 ? snapshot.projection_remaining_qty : snapshot.requested_qty);
1697 row.qty_percent = snapshot.qty_percent;
1698 row.created_bar = snapshot.projection_created_bar;
1699 row.created_seq = static_cast<std::int64_t>(snapshot.source_sequence);
1700 row.incarnation = snapshot.source_sequence;
1701 row.over_pyramiding_cap_at_placement = snapshot.projection_over_pyramiding;
1703 row.paired_flat_market_transaction_qty = std::numeric_limits<double>::quiet_NaN();
1704 row.frozen_default_qty = default_stop ? absent : snapshot.sizing.frozen_units;
1705 row.default_stop_placement_equity = default_stop
1706 ? snapshot.projection_default_stop_equity : absent;
1707 row.default_stop_placement_signal_close = default_stop
1708 ? snapshot.projection_default_stop_signal_close : absent;
1709 row.affordability_placement_equity = snapshot.projection_affordability_equity;
1710 row.market_admission = snapshot.market_admission;
1711 if (!row.market_admission.observation()
1712 && snapshot.family == PineOrderFamily::Entry
1713 && snapshot.frozen_market_instruction
1714 && std::isfinite(snapshot.requested_qty)) {
1715 auto observation = std::make_shared<admission::CommandObservation>();
1716 observation->command = snapshot.command_ordinal;
1717 observation->kind = admission::CommandKind::Entry;
1718 observation->birth = snapshot.birth.cause() == OrderBirthCause::Unattributed
1719 ? OrderBirth::chart_evaluation(source_bar_index_, current_bar_.timestamp)
1720 : snapshot.birth;
1721 observation->id = snapshot.source_id;
1722 observation->requested_quantity = snapshot.requested_qty;
1723 observation->quantity_type = snapshot.qty_type;
1724 observation->buy = snapshot.is_long;
1725 observation->prices = {snapshot.exit_levels.limit, snapshot.exit_levels.stop};
1726 observation->oca_name = snapshot.oca_name;
1727 observation->oca_type = snapshot.oca_type;
1728 auto& configuration = observation->configuration;
1729 configuration.process_on_close = config_.process_orders_on_close;
1730 configuration.calc_on_fills = config_.calc_on_order_fills;
1731 configuration.slippage = config_.slippage;
1732 configuration.pyramiding = config_.pyramiding;
1733 configuration.default_quantity_type = config_.default_qty_type;
1734 configuration.default_quantity_value = config_.default_qty_value;
1735 configuration.long_margin = config_.margin_long;
1736 configuration.short_margin = config_.margin_short;
1737 configuration.commission_value = config_.commission_value;
1738 configuration.commission_type = config_.commission_type;
1739 configuration.pointvalue = staged_configuration().syminfo.pointvalue;
1740 configuration.fx = snapshot.sizing.fx;
1741 configuration.quantity_step = staged_configuration().quantity_grid
1742 ? *staged_configuration().quantity_grid : 0.0;
1743 configuration.mintick = staged_configuration().syminfo.mintick;
1744 observation->bar = source_bar_index_;
1745 observation->placement_side = static_cast<int>(PositionSide::FLAT);
1746 observation->placement_cycle = snapshot.placement_cycle;
1747 observation->held_quantity = 0.0;
1748 observation->held_entries = 0;
1749 observation->realized_equity = snapshot.sizing.equity;
1750 observation->placement_equity = snapshot.sizing.equity;
1751 observation->signal_close = snapshot.sizing.price;
1752 observation->quantized_fixed_quantity =
1753 snapshot.frozen_market_own_units;
1754 observation->original_sizing = admission::SizingObservation{
1755 snapshot.requested_qty, snapshot.sizing.equity,
1756 snapshot.sizing.price, snapshot.sizing.mark, snapshot.sizing.fx};
1757 row.market_admission.bind(std::move(observation));
1758 }
1759 source_pending_view_cache_.push_back(std::move(row));
1760 };
1761 for (const auto& command : adapter_.pending_same_bar_commands_) {
1762 // ab9714be strategy.close under process_orders_on_close is held in the
1763 // same-bar close accumulator until the callback returns; the legacy
1764 // pending_orders_ observer therefore sees the two entry commands but
1765 // not that staged close during the source body.
1766 if (config_.process_orders_on_close
1767 && command.snapshot.family == PineOrderFamily::Close
1768 && !command.snapshot.birth.at_terminal_fill()) {
1769 continue;
1770 }
1771 append(command.snapshot, command.request.label);
1772 }
1773 for (const auto& pending : adapter_.pending_entries_)
1774 append(pending.snapshot, pending.request.label);
1775 for (const auto& delayed : adapter_.delayed_market_orders_)
1776 append(delayed.snapshot, delayed.request.label);
1777 for (const auto& leg : adapter_.pending_bracket_legs_)
1778 append(leg.snapshot, leg.request.label);
1779 for (const auto& pending : adapter_.pending_coof_requests_)
1780 append(pending.snapshot, pending.request.label);
1781 for (const auto& shadow : adapter_.source_shadow_pending_)
1782 append(shadow.snapshot, shadow.label);
1783 for (const auto& handle : adapter_.live_handles_) {
1784 const auto found = adapter_.placement_.find(handle.incarnation);
1785 if (found == adapter_.placement_.end()) continue;
1786 if (config_.process_orders_on_close
1787 && found->second.family == PineOrderFamily::Close
1788 && found->second.projection_created_bar == source_bar_index_
1789 && !found->second.birth.at_terminal_fill()) {
1790 continue;
1791 }
1792 append(found->second, found->second.source_id);
1793 }
1795}
1796
1799
1800void source::PineStrategyHost::project_short_seed_report_rows(
1802 const ShortSeedPlan plan = adapter_.short_seed_;
1803 if (!plan.report_swap_pending || event.closed_trade_count == 0
1804 || event.handle() == plan.final_short) {
1805 return;
1806 }
1807 std::optional<PlacementSnapshot> placement_snapshot;
1808 if (const auto placement = adapter_.placement_.find(event.handle().incarnation);
1809 placement != adapter_.placement_.end()) {
1810 placement_snapshot = placement->second;
1811 }
1812 if (!placement_snapshot || placement_snapshot->family != PineOrderFamily::Close
1813 || (placement_snapshot->from_entry != plan.seed_id
1814 && placement_snapshot->source_id != plan.seed_id)) {
1815 return;
1816 }
1817 for (std::size_t index = trades_.base(); index < trades_.size(); ++index) {
1818 Trade trade = trades_[index];
1819 if (trade.entry_incarnation == plan.materialize_long.incarnation
1820 && trade.entry_id == plan.materialize_label) {
1821 trade.entry_incarnation = plan.final_short.incarnation;
1822 adapter_.invalidate_exit_group(index); trades_.replace(index, trade);
1823 }
1824 }
1825 const std::size_t begin = event.first_trade_index;
1826 const std::size_t end = begin + event.closed_trade_count;
1827 for (std::size_t index = begin; index < end && index < trades_.size(); ++index) {
1828 if (trades_[index].entry_incarnation == plan.final_short.incarnation
1829 && trades_[index].entry_id == plan.final_short_id) {
1830 adapter_.invalidate_exit_group(index); trades_.modify(index, [&](Trade& row) {
1831 row.entry_incarnation = plan.materialize_long.incarnation;
1832 });
1833 }
1834 }
1835 adapter_.short_seed_.report_swap_pending = false;
1836}
1837
1838void source::PineStrategyHost::scheduler_prepare_script_run(
1839 PineScheduler::InputView bars, bool static_eligible,
1840 int expected_script_bars, bool script_bar_geometry) {
1841 if (const auto state = native_state(); state.spec) {
1842 input_tf_ = state.spec->timeframe_undetected ? "" : state.spec->input_tf;
1843 script_tf_ = state.spec->timeframe_undetected ? "" : state.spec->script_tf;
1844 script_tf_seconds_ = tf_to_seconds(script_tf_);
1845 }
1846 prepare_script_run(bars.empty() ? nullptr : bars.data(), static_cast<int>(bars.size()),
1847 static_eligible);
1848 last_bar_index_ = expected_script_bars - 1;
1849 last_bar_time_ = bars.empty() ? 0 : bars.back().timestamp;
1850 apply_realtime_tail_horizon(
1851 bars.empty() ? nullptr : bars.data(), static_cast<int>(bars.size()),
1852 script_bar_geometry);
1853 source_last_bar_index_ = last_bar_index_;
1854}
1855
1856// Live-runtime tail (spec §3.1): the horizon freeze of last_bar_index_ /
1857// last_bar_time_. Moved verbatim from src/engine_run.cpp (R5 lane N14); the
1858// geometry rules are documented on the declaration.
1860 bool script_bar_geometry) {
1861 if (!realtime_tail_ || realtime_tail_horizon_bars_ <= 0 || n <= 0 || bars == nullptr) return;
1862 const int horizon = realtime_tail_horizon_bars_;
1863 last_bar_index_ = horizon - 1;
1864 const int64_t script_tf_ms =
1865 static_cast<int64_t>(script_tf_seconds_ > 0 ? script_tf_seconds_ : 0) * 1000;
1866 if (script_bar_geometry) {
1867 if (horizon <= n) {
1868 last_bar_time_ = bars[horizon - 1].timestamp;
1869 } else {
1870 last_bar_time_ = bars[n - 1].timestamp
1871 + static_cast<int64_t>(horizon - n) * script_tf_ms;
1872 }
1873 } else {
1874 last_bar_time_ = bars[0].timestamp
1875 + static_cast<int64_t>(horizon - 1) * script_tf_ms;
1876 }
1877}
1878
1879void source::PineStrategyHost::scheduler_configure_security_evaluators() {
1880 configure_security_evaluators();
1881 prune_pine_security_states();
1882}
1883
1884bool source::PineStrategyHost::scheduler_uses_aux_security_feed() const noexcept {
1885#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
1886 return aux_security_feed_enabled();
1887#else
1888 return false;
1889#endif
1890}
1891
1892namespace {
1893
1894// N of an N-day timeframe ("2D", "8D"), 0 for any other.
1895int multi_day_count(const std::string& tf) {
1896 const auto parsed = native_calendar::parse_timeframe(tf);
1897 if (!parsed || parsed->unit() != native_calendar::TimeframeUnit::Day) return 0;
1898 return parsed->count() >= 2 ? parsed->count() : 0;
1899}
1900
1901} // namespace
1902
1903void source::PineStrategyHost::scheduler_prepare_security_sequence(
1905 security_input_tf_ = input_tf_;
1906#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
1907 if (aux_security_feed_enabled()) security_input_tf_ = aux_security_input_tf_;
1908#endif
1909 validate_security_timeframes(security_input_tf_);
1910 security_first_chart_bar_ms_ = bars.empty() ? 0 : bars.front().timestamp;
1911 if (declare_security_sites_to_kernel()) {
1912 // The kernel registers the declared sites after this callback returns
1913 // and prepares their feeds itself (begin_timeframe_subscriptions).
1914 // The chart's day partition is the chart's, not a site's, and stays.
1915 security_next_input_ms_ = 0;
1916 security_calling_close_ms_ = 0;
1917 clear_historical_security_lookahead_projections();
1918 prepare_chart_day_partition(
1919 bars.empty() ? nullptr : bars.data(), static_cast<int>(bars.size()));
1920 return;
1921 }
1922 init_security_eval_states_for_run(security_input_tf_);
1923 prepare_native_security_feeds(
1924 bars.empty() ? nullptr : bars.data(), static_cast<int>(bars.size()));
1925 // An N-day request's bars are TradingView's (make_multi_day_period_source),
1926 // its aggregator's period partition: the calendar aggregator split them
1927 // into single days. The partition is generated from the chart's and the
1928 // auxiliary feed's bars as the run asks for it, never stored: it reads
1929 // them until the next prepare rebuilds this aggregator.
1930 for (auto& state : security_eval_states_) {
1931 const int days = multi_day_count(state.tf);
1932 if (days == 0 || state.aggregator.calendar_period() != CalendarPeriod::DAY
1933 || state.aggregator.has_native_periods()) {
1934 continue;
1935 }
1936 MultiDayFeeds feeds;
1937 feeds.days = days;
1938 feeds.chart = bars.data();
1939 feeds.n_chart = static_cast<int>(bars.size());
1940 feeds.daily_chart = tf_to_seconds(input_tf_) == 86400;
1941#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
1942 if (aux_security_feed_enabled()) {
1943 feeds.aux = aux_security_bars_.data();
1944 feeds.n_aux = aux_security_bars_.size();
1945 }
1946#endif
1947 feeds.tz = syminfo_.timezone;
1948 feeds.session = syminfo_.session;
1949 feeds.exchange_calendar = session_template_knows_early_close();
1950 if (auto partition = make_multi_day_period_source(feeds))
1951 state.aggregator.set_native_period_source(std::move(partition));
1952 }
1953#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
1954 if (aux_security_feed_enabled()) {
1955 prepare_aux_security_chart_ranges(
1956 bars.empty() ? nullptr : bars.data(), static_cast<int>(bars.size()), script_tf_);
1957 }
1958#endif
1959 prepare_historical_security_lookahead_projections(
1960 bars.empty() ? nullptr : bars.data(), static_cast<int>(bars.size()), input_tf_);
1961 prepare_chart_day_partition(
1962 bars.empty() ? nullptr : bars.data(), static_cast<int>(bars.size()));
1963}
1964
1965bool source::PineStrategyHost::security_sites_kernel_routed() const noexcept {
1966 // A foreign site's InstrumentFeed series is always the kernel's; the
1967 // same-symbol sites are routed exactly when the running spec names one of
1968 // theirs.
1969 const NativeRunSpec* spec = detail::run_spec(*this);
1970 if (spec == nullptr) return false;
1971 for (const auto& series : spec->subscriptions) {
1972 if (series.source != NativeSeriesSource::InstrumentFeed) return true;
1973 }
1974 return false;
1975}
1976
1977bool source::PineStrategyHost::declare_security_sites_to_kernel() {
1978 std::vector<NativeTimeframeSubscription> declared;
1979 bool routed = same_symbol_sites_routable(declared);
1980 const std::size_t same_symbol = declared.size();
1981 // Lane XSYM-D: every foreign site that reads a feed is the kernel's,
1982 // declared after the same-symbol ones so theirs keep sec_id = index.
1983 prepare_foreign_security_sites(declared);
1984 if (declared.empty()) return false;
1985 if (declared.size() == same_symbol) {
1986 if (!declare_timeframe_subscriptions(std::move(declared))) return false;
1987 } else {
1988 auto result = declare_timeframe_subscriptions_result(declared);
1989 if (result.status != NativeSetupStatus::Applied && routed) {
1990 // The same-symbol sites stay on this host's own drive; the
1991 // foreign series have no other drive, so only their refusal is
1992 // final.
1993 declared.erase(declared.begin(),
1994 declared.begin() + static_cast<std::ptrdiff_t>(same_symbol));
1995 foreign_security_series_.erase(
1996 foreign_security_series_.begin(),
1997 foreign_security_series_.begin() + static_cast<std::ptrdiff_t>(same_symbol));
1998 for (auto& entry : foreign_security_sites_) {
1999 if (entry.second.subscription >= 0)
2000 entry.second.subscription -= static_cast<std::int64_t>(same_symbol);
2001 }
2002 routed = false;
2003 result = declare_timeframe_subscriptions_result(declared);
2004 }
2005 if (result.status != NativeSetupStatus::Applied) {
2006 RunFailureArgs refused{{"reason", "kernel_refused_series"}};
2007 if (const char* field = instrument_feed_field_name(result.validation.field))
2008 refused.emplace_back("field", field);
2009 throw coded<std::runtime_error>(
2011 "request.security: the kernel refused the symbol feed series (NativeRunSpecError "
2012 + std::to_string(static_cast<int>(result.validation.error)) + ", field "
2013 + std::to_string(static_cast<int>(result.validation.field)) + ")");
2014 }
2015 }
2016 // The kernel registers the routed sites, sec_id by index, after this
2017 // callback returns; the per-site table keeps their Pine semantics.
2018 if (routed) security_eval_states_.clear();
2019 return routed;
2020}
2021
2022bool source::PineStrategyHost::same_symbol_sites_routable(
2023 std::vector<NativeTimeframeSubscription>& out) {
2024 if (security_eval_states_.empty()) return false;
2025 // Run shapes with a Pine-only rule around the step: a stream feeds its
2026 // sites from realtime prints (the kernel takes confirmed bars only); an
2027 // aggregated chart or the bar magnifier can hold an input back until the
2028 // calling bar's callback has run (PineScheduler::input's deferrals); the
2029 // auxiliary slice is fed per chart bar; the range-start cut, the
2030 // historical lookahead projection and the OTC-daily pins veto or
2031 // re-shape inputs before the aggregator sees them.
2032 if (stream_warmup_mode_ || scheduler_.bar_magnifier_enabled()) return false;
2033 if (security_range_start_na_warmup_ || historical_security_lookahead_projection_)
2034 return false;
2035#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
2036 if (aux_security_feed_enabled()) return false;
2037#endif
2038 if (input_tf_.empty() || input_tf_ != script_tf_ || security_input_tf_ != input_tf_)
2039 return false;
2040 const auto view = native_state();
2041 if (view.spec == nullptr || view.spec->timeframe_undetected
2042 || view.spec->input_tf != input_tf_) {
2043 return false;
2044 }
2045 const bool otc_daily_pins = (syminfo_.type == "forex" || syminfo_.type == "cfd")
2046 && native_security_feeds_.empty() && script_tf_seconds_ > 0
2047 && script_tf_seconds_ < 86400;
2048 std::vector<NativeTimeframeSubscription> declared;
2049 declared.reserve(security_eval_states_.size());
2050 for (std::size_t i = 0; i < security_eval_states_.size(); ++i) {
2051 const SecurityEvalState& state = security_eval_states_[i];
2052 const bool minute_day = calendar_period_for(state.tf) == CalendarPeriod::NONE
2053 && tf_to_seconds(state.tf) == 86400;
2054 if (minute_day && !pine_security_state(state.sec_id).lookahead_on
2055 && script_tf_seconds_ > 0 && script_tf_seconds_ < 86400) {
2056 return false;
2057 }
2058 // The kernel registers sec_id = index, which is what generated code
2059 // dispatches on.
2060 if (state.sec_id != static_cast<int>(i) || state.tf.empty()) return false;
2061 const PineSecurityEvalState& pine = pine_security_state(state.sec_id);
2062 // Site rules inside the step: the lookahead_on peeks and merge latch
2063 // (and the publication gate they arm), ticker.heikinashi, both
2064 // lower-timeframe paths, the auxiliary-slice completions.
2065 if (pine.lookahead_on || pine.heikinashi || pine.lower_tf_requested
2066 || pine.lower_tf_emulation || pine.lower_tf_use_input
2067 || pine.lower_tf_array_requested || pine.publish_gate_tf_seconds > 0
2068 || pine.calling_close_completes_partial || pine.calling_open_latches_first) {
2069 return false;
2070 }
2071 if (otc_daily_pins && (state.tf == "D" || state.tf == "1D")) return false;
2072 // TradingView's N-day bars restart every year and count the symbol's
2073 // trading dates (make_multi_day_period_source); the kernel's are a
2074 // fixed grid of calendar days.
2075 if (multi_day_count(state.tf) != 0) return false;
2076 NativeTimeframeSubscription subscription;
2077 subscription.tf = state.tf;
2078 subscription.gaps = pine.gaps_on;
2079 declared.push_back(std::move(subscription));
2080 }
2081 out = std::move(declared);
2082 return true;
2083}
2084
2085bool source::PineStrategyHost::scheduler_feed_security_input(
2086 const Bar& bar, std::int64_t next_input_ms, bool calling_bar_complete,
2087 bool defer_boundary_gate) {
2088 security_next_input_ms_ = next_input_ms;
2089 security_calling_close_ms_ = 0;
2090 bool deferred = false;
2091 for (auto& state : security_eval_states_) {
2092 if (defer_boundary_gate
2093 && pine_security_state(state.sec_id).publish_gate_tf_seconds > 0) {
2094 deferred = true;
2095 continue;
2096 }
2097 pine_feed_security_eval_state(state, bar, calling_bar_complete);
2098 }
2099 return deferred;
2100}
2101
2102void source::PineStrategyHost::scheduler_publish_security_boundary() {
2103 for (auto& state : security_eval_states_) {
2104 if (pine_security_state(state.sec_id).publish_gate_tf_seconds > 0)
2105 publish_security_eval_state_at_calling_boundary(state);
2106 }
2107}
2108
2109void source::PineStrategyHost::scheduler_feed_deferred_security_input(
2110 const Bar& bar, std::int64_t next_input_ms) {
2111 security_next_input_ms_ = next_input_ms;
2112 security_calling_close_ms_ = 0;
2113 for (auto& state : security_eval_states_) {
2114 if (pine_security_state(state.sec_id).publish_gate_tf_seconds > 0)
2115 pine_feed_security_eval_state(state, bar, false);
2116 }
2117}
2118
2119void source::PineStrategyHost::scheduler_feed_aux_security(int chart_index) {
2120#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
2121 if (aux_security_feed_enabled() && scheduler_coof_enabled()) {
2122 // ab9714be pine_scheduler.cpp:1705-1708 feeds the auxiliary slice
2123 // before dispatch_bar, whose COOF bar takes its script checkpoint
2124 // only afterwards (pine_scheduler.cpp:467). The native scheduler
2125 // checkpoints at the script-bar open and restores that checkpoint
2126 // after this feed, which would discard the fed request.security
2127 // values every bar. Rebase the checkpoint on the open state plus
2128 // this feed so the restore keeps them.
2129 restore_script_state();
2130 feed_aux_security_for_chart_bar(chart_index);
2131 snapshot_script_state();
2132 return;
2133 }
2134 if (aux_security_feed_enabled()) feed_aux_security_for_chart_bar(chart_index);
2135#else
2136 (void)chart_index;
2137#endif
2138}
2139
2140void source::PineStrategyHost::scheduler_feed_deferred_aux_security(int chart_index) {
2141#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
2142 if (aux_security_feed_enabled()) feed_deferred_aux_security_for_chart_bar(chart_index);
2143#else
2144 (void)chart_index;
2145#endif
2146}
2147
2148void source::PineStrategyHost::scheduler_finish_security_sequence() {
2149 clear_historical_security_lookahead_projections();
2150#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
2151 release_aux_security_chart_ranges();
2152#endif
2153}
2154
2155// sort_same_bar_exit_trades' first test, asked before the call (R5 lane D2-D):
2156// its group grows past the last row only when the row before it is a bracket
2157// exit of the same entry at the same exit time, as the last row is. Otherwise
2158// the group is one row, the sort moves nothing and the call answers nullopt.
2159// A quiet-bar gate (pine_quiet_bar.hpp): the ungated reference calls the sort
2160// at every publication.
2161static bool same_bar_exit_group_possible(const TradeLedger& trades) {
2162#if PINEFORGE_PINE_QUIET_BAR_GATES
2163 if (trades.size() < 2) return false;
2164 const auto last_index = trades.size() - 1;
2165 const auto previous_index = last_index - 1;
2166 const Trade last = trades.numeric_at(last_index);
2167 const Trade previous = trades.numeric_at(previous_index);
2168 if (!previous.exit_from_bracket || !last.exit_from_bracket
2169 || previous.exit_time != last.exit_time || previous.entry_time != last.entry_time)
2170 return false;
2171 // Entry identity is still exact, but comments and exit IDs are irrelevant
2172 // here. Do not decode even the entry IDs unless the numeric test matches.
2173 const auto last_entry_id = trades.entry_id_at(last_index);
2174 return trades.entry_id_at(previous_index) == last_entry_id;
2175#else
2176 (void)trades;
2177 return true;
2178#endif
2179}
2180
2181bool source::PineStrategyHost::report_terminal_quote_eligible() const {
2182 if (!last_equity_point() || range_end_trades_.empty() || !native_bound()) return false;
2183 const auto state = as_native_consumer(execution_consumer()).view();
2184 const auto interval = static_cast<std::int64_t>(script_tf_seconds_) * 1000;
2185 const auto last_time = last_equity_point()->time_ms;
2186 return state.spec && state.spec->report_policy == NativeReportPolicy::KernelRecordedAtHostMarks
2187 && state.kind == NativeLifecycleKind::Completed && bar_magnifier_enabled_
2188 && script_tf_seconds_ > 60 && script_tf_seconds_ < 86400
2189 && report_quote_time_ms_ >= last_time && report_quote_time_ms_ - last_time <= interval
2190 && (report_quote_time_ms_ - last_time) % interval == 0
2191 && std::isfinite(report_quote_close_)
2192 && range_end_trades_.size() == pyramid_entries_.size();
2193}
2194
2196 if (!report_terminal_quote_eligible() || report.equity_curve_len == 0
2197 || report.equity_curve[report.equity_curve_len - 1].time_ms != report_quote_time_ms_) return false;
2198 std::size_t rows = 0;
2199 for (int index = 0; index < report.trades_len; ++index) {
2200 const auto& row = report.trades[index];
2201 if (!row.open_at_end) continue;
2202 if (row.exit_time != report_quote_time_ms_ || row.exit_price != bar_fill_price(report_quote_close_))
2203 return false;
2204 ++rows;
2205 }
2206 return rows == pyramid_entries_.size();
2207}
2208
2209void source::PineStrategyHost::present_report(ReportC* out) const {
2210 if (out->equity_curve_len == 0 || !native_bound()) return;
2211 const auto state = as_native_consumer(execution_consumer()).view();
2212 if (!state.spec
2213 || state.spec->report_policy != NativeReportPolicy::KernelRecordedAtHostMarks) return;
2214 const auto position = as_native_consumer(execution_consumer()).position(*this);
2215 auto& last = out->equity_curve[out->equity_curve_len - 1];
2216 if ((stream_phase_ == StreamPhase::REALTIME
2217 || state.completion == NativeCompletion::StreamEnded)
2218 && (!range_end_trades_.empty()
2219 || (position.signed_units == 0.0 && position.lot_count == 0
2220 && last.open_profit != 0.0))) {
2221 last.open_profit = 0.0;
2222 if (!range_end_trades_.empty()) {
2223 double range_end_pnl = 0.0;
2224 for (const auto& row : range_end_trades_) range_end_pnl += row.pnl;
2225 last.equity = initial_capital_ + net_profit_sum_ + range_end_pnl;
2226 amend_report_replay_point(last.time_ms, report::EquityBasis::range_end, range_end_pnl);
2227 }
2228 }
2229 if (!report_terminal_quote_eligible()) return;
2230 const double price = bar_fill_price(report_quote_close_);
2231 const double fx = account_currency_fx_at(report_quote_time_ms_);
2232 std::size_t lot_index = 0;
2233 for (int index = 0; index < out->trades_len; ++index) {
2234 auto& row = out->trades[index];
2235 if (!row.open_at_end) continue;
2236 const auto& lot = pyramid_entries_[lot_index++];
2237 execution::PhysicalExecutionContext context;
2238 context.effective_time_ms = report_quote_time_ms_;
2239 context.interval_index = row.exit_bar_index;
2240 context.account_fx = fx;
2241 const auto mark = build_close_trade_with_costs(lot, row.qty, price, row.is_long != 0,
2242 allocated_entry_commission(lot, row.qty), calc_commission_at(price, row.qty, fx), context);
2243 // settlement safety guard (an invariant), not Pine parity. The price
2244 // test is implied by the predicate (the mark carries it as exit_price);
2245 // no mutant can kill it alone.
2246 if (!internal::closed_row_all_finite(mark) || !std::isfinite(price))
2247 throw std::logic_error("non-finite terminal-quote report row");
2248 row.exit_time = report_quote_time_ms_;
2249 row.exit_price = price;
2250 row.pnl = mark.pnl;
2251 row.pnl_pct = mark.pnl_pct;
2252 row.commission = mark.commission;
2253 }
2254 out->net_profit = out->total_trades == out->trades_len ? 0.0 : report_closed_net_profit();
2255 for (int index = 0; index < out->trades_len; ++index) out->net_profit += out->trades[index].pnl;
2256 last.time_ms = report_quote_time_ms_;
2257 last.equity = initial_capital_ + out->net_profit;
2258 amend_report_replay_point(last.time_ms, report::EquityBasis::terminal_quote, out->net_profit);
2259}
2260
2261void source::PineStrategyHost::scheduler_record_range_end(const Bar& terminal_bar) {
2262 range_end_trades_.clear();
2263 if (stream_warmup_mode_ || realtime_tail_
2264 || position_side_ == PositionSide::FLAT || !last_equity_point()
2265 || !std::isfinite(terminal_bar.close)) return;
2266 const double fill_price = bar_fill_price(terminal_bar.close);
2267 // R5 audit lane Q6 (duplicate D5): the row loop itself is the kernel's
2268 // generic producer, called here at TradingView's own mark instead of
2269 // being restated. What stays below is the part that is report SHAPE and
2270 // not a mark-to-market row — the reason the kernel's run-end producer is
2271 // gated out of every Pine run (pine_adapter.cpp project()). The rows are
2272 // dated on the curve's last point and convert at that instant's rate,
2273 // handed to the producer: the presented clock is never rewritten (R5
2274 // lane B-ADAPTER; E21's rule -- thread the rate, never the clock).
2275 const std::int64_t mark_time = last_equity_point()->time_ms;
2276 const double range_end_pnl = as_native_consumer(execution_consumer())
2277 .append_open_position_report_rows(*this, fill_price, mark_time, bar_index_,
2278 account_currency_fx_at(mark_time));
2279 if (stream_phase_ != StreamPhase::REALTIME) {
2280 auto& last = *last_equity_point();
2281 last.open_profit = 0.0;
2282 last.equity = initial_capital_ + net_profit_sum_ + range_end_pnl;
2283 amend_report_replay_point(last.time_ms, report::EquityBasis::range_end, range_end_pnl);
2284 // Restore the fold before the held point, then fold its replacement.
2285 // Debug checks all four resulting doubles against a complete walk.
2286 // The sink-off helper keeps the existing walk and initial seed.
2287 // Presentation later receives its own copy of this editable tail.
2288 refold_equity_tail();
2289 }
2290 if (same_bar_exit_group_possible(trades_)) {
2291 if (const auto moved = sort_same_bar_exit_trades(trades_, adapter_))
2292 native_closed_rows_amended(*moved);
2293 }
2294}
2295
2296// TradingView keeps one child of a strategy.exit that names no entry for every
2297// open entry. When one path point triggers several, it fills them one at a
2298// time: the children of the entry id that comes first in its entry-id table
2299// first (PineExecutionAdapter::global_exit_child_ranks), and an older entry's
2300// child before a newer one's of the same id. Each child is a transaction of
2301// its entry's quantity, settled against its own entry under
2302// close_entries_rule = "ANY" and against the oldest lots first otherwise. Each
2303// settled slice is a row of its own, and its fee is its share of its lot's
2304// entry fee plus its share of its transaction's exit fee, each on
2305// TradingView's ten-significant-digit money (127 TradingView tapes,
2306// tests/fixtures/global_exit_children).
2307// The kernel fills such a leg once for the whole position, oldest lots first,
2308// one row per lot. Its rows are still the last ones and not final until this
2309// notification returns, so they are re-cut here into the children's rows.
2310// Every shape the pin does not reach keeps the kernel's rows:
2311// - a fill that leaves part of the position open;
2312// - a trailing leg;
2313// - an exit filled at the same point before it, or a leg of any other
2314// exit live at the fill that its price also reaches (the order of a
2315// named child, or another exit's, against these; on_native_applied reads
2316// it before the adapter retires that leg);
2317// - a table the pin does not size (global_exit_child_ranks);
2318// - calc_on_order_fills, process_orders_on_close or an intrabar feed;
2319// - a non-unit account rate, a cash-per-order fee or a negative fee;
2320// - a lot quantity off TradingView's ten significant digits.
2321void source::PineStrategyHost::order_global_exit_children(
2322 const native_order::ExecutionAppliedEvent& event) {
2323 const std::size_t first = event.first_trade_index;
2324 const std::size_t count = event.closed_trade_count;
2325 if (count < 2 || event.opened_units != 0.0 || first + count != trades_.size()
2326 || !pyramid_entries_.empty()) {
2327 return;
2328 }
2329 const auto& run = adapter_.config_;
2330 if (run.calc_on_order_fills || run.process_orders_on_close || bar_magnifier_enabled_
2331 || active_account_currency_fx() != 1.0 || commission_value_ < 0.0
2332 || (commission_value_ != 0.0 && commission_type_ == CommissionType::CASH_PER_ORDER)) {
2333 return;
2334 }
2335 const auto placement = adapter_.placement_.find(event.handle().incarnation);
2336 if (placement == adapter_.placement_.end()) return;
2337 const auto& cause = placement->second;
2338 if ((cause.family != PineOrderFamily::ExitLimit && cause.family != PineOrderFamily::ExitStop)
2339 || !cause.from_entry.empty()) {
2340 return;
2341 }
2342 // Ledger reads own their strings. Retain only the group keys so a wide
2343 // comment does not stay resident while another row is being read.
2344 Trade head{};
2345 {
2346 Trade row = trades_[first];
2347 head.exit_time = row.exit_time;
2348 head.exit_price = row.exit_price;
2349 head.exit_id = std::move(row.exit_id);
2350 head.is_long = row.is_long;
2351 head.entry_id = std::move(row.entry_id);
2352 }
2353 auto& scratch = trade_scratch();
2354 TradeScratchTransaction transaction(scratch);
2355 bool different_ids = false;
2356 for (std::size_t index = first; index < first + count; ++index) {
2357 const Trade& row = trades_[index];
2358 if (row.exit_time != head.exit_time || row.exit_price != head.exit_price
2359 || row.exit_id != head.exit_id || row.is_long != head.is_long
2360 || !(row.qty > 0.0) || row.open_at_end) {
2361 return;
2362 }
2363 const TradeScratchKey incarnation{scratch_incarnation, row.entry_incarnation, 0, 0};
2364 if (scratch_find(scratch, incarnation) != ::pineforge::detail::scratch_no_handle) return;
2365 scratch_put(scratch, incarnation);
2366 different_ids = different_ids || row.entry_id != head.entry_id;
2367 }
2368 if (!different_ids) return;
2369 if (first > 0 && trades_[first - 1].exit_time == head.exit_time
2370 && trades_[first - 1].exit_price == head.exit_price) {
2371 return;
2372 }
2373 // Validate one id and each distinct pair through the existing table rule:
2374 // a grown table rejects shared buckets, while the original table does
2375 // not. Each query owns at most two ids, irrespective of the lot count.
2376 // note_exit_rank_key retains at most 27 keys (the existing parity pin),
2377 // so distinct-id work is fixed even when one id owns arbitrarily many lots.
2378 std::array<std::size_t, 27> ages{};
2379 std::array<std::uint32_t, 27> ranks{};
2380 std::size_t age_count = 0;
2381 const auto age_of = [&](std::size_t lot) {
2382 const auto& keys = adapter_.exit_rank_keys_;
2383 const auto found = keys.find(trades_[first + lot].entry_id);
2384 return found ? static_cast<std::size_t>(keys.get(*found))
2385 : static_cast<std::size_t>(keys.size());
2386 };
2387 const auto key_of = [&](std::size_t age) {
2388 SourceId id;
2389 adapter_.exit_rank_keys_.for_each_inserted([&](SourceId key, std::uint32_t key_age) {
2390 if (key_age == age) id = std::move(key);
2391 });
2392 return id;
2393 };
2394 std::vector<SourceId> ids;
2395 for (std::size_t lot = 0; lot < count; ++lot) {
2396 const auto age = age_of(lot);
2397 if (age == adapter_.exit_rank_keys_.size()) return;
2398 if (std::find(ages.begin(), ages.begin() + age_count, age) != ages.begin() + age_count)
2399 continue;
2400 ids.assign(1, trades_[first + lot].entry_id);
2401 const auto rank = adapter_.global_exit_child_ranks(ids);
2402 if (!rank) return;
2403 ranks[age] = rank->front();
2404 for (std::size_t earlier = 0; earlier < age_count; ++earlier) {
2405 ids.resize(1);
2406 ids.push_back(key_of(ages[earlier]));
2407 if (!adapter_.global_exit_child_ranks(ids)) return;
2408 }
2409 ages[age_count++] = age;
2410 }
2411 const auto rank_of = [&](std::size_t lot) {
2412 return ranks[age_of(lot)];
2413 };
2414 // Children in lot order settle exactly as the kernel's one fill did.
2415 bool ordered = true;
2416 auto previous_rank = rank_of(0);
2417 for (std::size_t lot = 1; lot < count; ++lot) {
2418 const auto rank = rank_of(lot);
2419 if (rank < previous_rank) ordered = false;
2420 previous_rank = rank;
2421 }
2422 if (ordered) return;
2423 auto* const replay = report_replay_state();
2424 std::unique_ptr<::pineforge::detail::ScratchSequence<report::ReplayTrade>> replay_rows;
2425 if (replay) {
2426 replay_rows = std::make_unique<::pineforge::detail::ScratchSequence<report::ReplayTrade>>();
2427 replay_rows->set_directory(replay->scratch_directory());
2428 }
2429 const auto visit_children = [&](const auto& visit) {
2430 for (std::size_t rank = 0; rank < adapter_.exit_rank_keys_.size(); ++rank)
2431 for (std::size_t child = 0; child < count; ++child)
2432 if (rank_of(child) == rank && !visit(child)) return false;
2433 return true;
2434 };
2435 std::size_t row_count = 0;
2436 const auto emit_row = [&](TradeScratchHeader header, const Trade& text) {
2437 const TradeScratchSource row(header, text);
2438 scratch_put(scratch, {scratch_row, row_count++, 0, 0}, row);
2439 };
2440 const auto header_for = [&](std::size_t source, const Trade& row) {
2441 const auto phase = adapter_.exit_phase_value(source);
2442 return scratch_trade_header(row, source, phase);
2443 };
2444 const auto annotate = [&](std::size_t lot, std::size_t child, double transaction_qty,
2445 bool fifo, bool fees, double exit_fee, double transaction_fee) {
2446 if (!replay) return;
2447 const Trade whole = trades_.numeric_at(first + lot);
2448 auto row = replay->trade(first + lot);
2449 row.parent = event.ordinal;
2450 row.slice = row_count - 1;
2451 row.child = child;
2452 row.transaction_qty = transaction_qty;
2453 row.whole_qty = whole.qty;
2454 row.whole_pnl = whole.pnl;
2455 row.whole_commission = whole.commission;
2456 row.whole_runup = whole.max_runup;
2457 row.whole_drawdown = whole.max_drawdown;
2458 row.whole_exit_fee = exit_fee;
2459 row.transaction_fee = transaction_fee;
2460 row.marked = true;
2461 row.fifo = fifo;
2462 row.fees = fees;
2463 replay_rows->push_back(row);
2464 };
2465 // The rule the adapter runs under, not the host's live copy a body writes.
2466 if (run.close_entries_rule_any) {
2467 visit_children([&](std::size_t lot) {
2468 const Trade row = trades_[first + lot];
2469 emit_row(header_for(first + lot, row), row);
2470 if (replay) annotate(lot, lot, row.qty, false, false, 0.0, 0.0);
2471 return true;
2472 });
2473 } else {
2474 const bool fees = commission_value_ != 0.0;
2475 for (std::size_t lot = 0; lot < count; ++lot) {
2476 const double qty = trades_[first + lot].qty;
2477 if (tv_money_round(qty) != qty) return;
2478 }
2479 const auto visit_slices = [&](const auto& visit) {
2480 std::size_t lot = 0;
2481 double left = trades_[first].qty;
2482 if (!visit_children([&](std::size_t child) {
2483 const double transaction = trades_[first + child].qty;
2484 double wanted = transaction;
2485 while (wanted > internal::kQtyEpsilon && lot < count) {
2486 const double taken = std::min(left, wanted);
2487 visit(lot, child, taken, transaction);
2488 left = tv_money_round(left - taken);
2489 wanted = tv_money_round(wanted - taken);
2490 if (left <= internal::kQtyEpsilon) {
2491 ++lot;
2492 if (lot < count) left = trades_[first + lot].qty;
2493 }
2494 }
2495 return !(wanted > internal::kQtyEpsilon);
2496 })) return false;
2497 return lot == count;
2498 };
2499 // Complete the exact quantity pass before calculating any replacement
2500 // money, as the old slice-vector pass did.
2501 if (!visit_slices([](std::size_t, std::size_t, double, double) {})) return;
2502 visit_slices([&](std::size_t lot, std::size_t child, double qty, double transaction) {
2503 const Trade& whole = trades_[first + lot];
2504 const double share = qty / whole.qty;
2505 double commission = 0.0;
2506 double replay_exit_fee = 0.0, replay_transaction_fee = 0.0;
2507 if (fees) {
2508 const double exit_fee = calc_commission_at(whole.exit_price, whole.qty, 1.0);
2509 const double entry_part =
2510 tv_money_round(std::max(0.0, whole.commission - exit_fee) * share);
2511 const double transaction_fee =
2512 tv_money_round(calc_commission_at(whole.exit_price, transaction, 1.0));
2513 const double exit_part = tv_money_round(transaction_fee * qty / transaction);
2514 commission = tv_money_round(entry_part + exit_part);
2515 if (replay) {
2516 replay_exit_fee = exit_fee;
2517 replay_transaction_fee = transaction_fee;
2518 }
2519 }
2520 auto header = header_for(first + lot, whole);
2521 Trade row{};
2522 scratch_trade_scalars(header, row);
2523 row.qty = qty;
2524 row.commission = commission;
2525 row.pnl = tv_money_round((whole.pnl + whole.commission) * share) - commission;
2526 const double cost = whole.entry_price * qty * syminfo_.pointvalue;
2527 row.pnl_pct = cost > 0.0 ? row.pnl / cost * 100.0 : 0.0;
2528 // The lot's excursions taken by share: an approximation where the
2529 // slice's lot is not its child's own (excursions are not graded).
2530 row.max_runup = whole.max_runup * share;
2531 row.max_drawdown = whole.max_drawdown * share;
2532 header[6] = scratch_word(row.qty);
2533 header[7] = scratch_word(row.pnl);
2534 header[8] = scratch_word(row.pnl_pct);
2535 header[13] = scratch_word(row.max_runup);
2536 header[14] = scratch_word(row.max_drawdown);
2537 header[15] = scratch_word(row.commission);
2538 emit_row(header, whole);
2539 if (replay) annotate(lot, child, transaction, true, fees,
2540 replay_exit_fee, replay_transaction_fee);
2541 });
2542 }
2543 // settlement safety guard (an invariant), not Pine parity. Validate all
2544 // replacements before changing the committed rows or their tallies.
2545 scratch_walk(scratch, scratch_row, [&](::pineforge::detail::ScratchHandle handle, const TradeScratchKey&) {
2546 Trade row{};
2547 scratch_trade_scalars(scratch_read_trade(scratch, handle), row);
2548 if (!internal::closed_row_all_finite(row))
2549 throw std::logic_error("non-finite amended trade row");
2550 });
2551 // strategy.wintrades / losstrades / eventrades and the gross sums count
2552 // rows: count the children's instead of the kernel's. net_profit_sum_ (the
2553 // equity) stays the kernel's whole-lot sum: the slices' PnLs can differ
2554 // from it by at most half a unit in the tenth significant digit of each
2555 // slice's gross and fee, which no tape has shown in strategy.netprofit or
2556 // in an equity-sized quantity.
2557 const auto tally = [this](double pnl, int sign) {
2558 if (pnl > 0.0) { gross_profit_sum_ += sign * pnl; win_trades_count_ += sign; }
2559 else if (pnl < 0.0) { gross_loss_sum_ += sign * pnl; loss_trades_count_ += sign; }
2560 else { eventrades_count_ += sign; }
2561 };
2562 for (std::size_t index = first; index < first + count; ++index) tally(trades_[index].pnl, -1);
2563 scratch_walk(scratch, scratch_row, [&](::pineforge::detail::ScratchHandle handle, const TradeScratchKey&) {
2564 Trade row{};
2565 scratch_trade_scalars(scratch_read_trade(scratch, handle), row);
2566 tally(row.pnl, 1);
2567 });
2568 if (replay) {
2569 if (replay_rows->size() != row_count)
2570 throw coded<std::logic_error>(RunFailureCode::engine_invariant, {},
2571 "recut replay rows differ from replacement rows");
2572 replay->resize_trades(first + row_count);
2573 for (std::size_t i = 0; i < row_count; ++i) {
2574 auto annotation = replay_rows->at(i);
2575 annotation.slices = row_count;
2576 replay->replace(first + i, annotation);
2577 }
2578 }
2579 adapter_.invalidate_exit_group(first, trades_.size() - first); trades_.resize(first);
2580 scratch_walk(scratch, scratch_row, [&](::pineforge::detail::ScratchHandle handle, const TradeScratchKey&) {
2581 const auto header = scratch_read_trade(scratch, handle);
2582 Trade row{};
2583 scratch_trade_scalars(header, row);
2584 scratch_trade_text(scratch, handle, header, row);
2585 trades_.push_back(std::move(row));
2586 });
2587 const auto none = static_cast<std::uint8_t>(NativePathPhase::None);
2588 scratch_walk(scratch, scratch_row, [&](::pineforge::detail::ScratchHandle handle, const TradeScratchKey& key) {
2589 const auto header = scratch_read_trade(scratch, handle);
2590 const auto target = first + static_cast<std::size_t>(key[1]);
2591 const auto phase = static_cast<std::uint8_t>(header[1]);
2592 if (phase == none && adapter_.exit_phase_value(target) == none) return;
2593 adapter_.set_exit_phase(target, phase);
2594 });
2595 transaction.finish();
2596 native_closed_rows_amended(first);
2597}
2598
2599// ab9714be pine_fills.cpp:664-670: same-bar bracket exit trades sort by script command sequence created_seq
2600// The rows are final kernel rows by now (their applied notifications have
2601// returned), so a sort that moves one answers the first row it moved, which
2602// the caller hands to the kernel before the next hash read
2603// (NativeStrategyHost::native_closed_rows_amended).
2604std::optional<std::size_t> source::PineStrategyHost::sort_same_bar_exit_trades(
2605 TradeLedger& trades, source::PineExecutionAdapter& adapter) {
2606 ++adapter.exit_group_work_.publications; if (adapter.exit_group_certified(trades) || trades.size() < 2) return std::nullopt;
2607 const std::size_t end = trades.size();
2608 std::size_t start = end - 1;
2609 while (start > 0
2610 && trades[start - 1].exit_time == trades[end - 1].exit_time
2611 && trades[start - 1].entry_time == trades[end - 1].entry_time
2612 && trades[start - 1].entry_id == trades[end - 1].entry_id
2613 && trades[start - 1].exit_from_bracket
2614 && trades[end - 1].exit_from_bracket) {
2615 --start; ++adapter.exit_group_work_.tail_rows;
2616 }
2617 if (end - start > 1) {
2618 // ab9714be pine_scheduler.cpp:262-283 and pine_fills.cpp:3710-3730:
2619 // fills on the bar OPEN precede intrabar fills chronologically. Sibling
2620 // bracket exits that fill within the same phase tie-break by command
2621 // sequence (test_l10m_corpus_parity.cpp).
2622 const auto less = [&](std::size_t ia, std::size_t ib) {
2623 ++adapter.exit_group_work_.comparisons; const bool open_a = adapter.is_open_phase_exit(ia);
2624 const bool open_b = adapter.is_open_phase_exit(ib);
2625 if (open_a != open_b) return open_a;
2626 const auto sa = adapter.command_sequence_for_exit(trades[ia].exit_id, trades[ia].entry_id);
2627 const auto sb = adapter.command_sequence_for_exit(trades[ib].exit_id, trades[ib].entry_id);
2628 return sa < sb;
2629 };
2630 const auto none = static_cast<std::uint8_t>(NativePathPhase::None);
2631 bool ordered = true;
2632 for (std::size_t index = start + 1; index < end; ++index)
2633 if (less(index, index - 1)) { ordered = false; break; }
2634 if (ordered) { adapter.certify_exit_group(trades, start); return std::nullopt; }
2635 auto& scratch = trade_scratch(); adapter.certify_exit_group(trades, start, false);
2636 const auto scratch_before = scratch.stats(); TradeScratchTransaction transaction(scratch);
2637 for (std::size_t source = start; source < end; ++source) {
2638 const auto phase = adapter.exit_phase_value(source);
2639 const auto sequence = adapter.command_sequence_for_exit(
2640 trades[source].exit_id, trades[source].entry_id);
2641 scratch_put(scratch,
2642 {scratch_sort, adapter.is_open_phase_exit(source) ? 0U : 1U, sequence, source},
2643 scratch_bytes(&phase, sizeof phase));
2644 }
2645 std::size_t target = start;
2646 // source is the last key word, so ties retain their original order.
2647 // This is a seek walk, not a store callback: the fixed mapping value
2648 // is read out before put, and its source never reads the same store.
2649 scratch_walk(scratch, scratch_sort,
2650 [&](::pineforge::detail::ScratchHandle handle, const TradeScratchKey& key) {
2651 std::uint8_t phase = none;
2652 require_trade_scratch(scratch.read(handle, ::pineforge::detail::ScratchField::value, 0,
2653 scratch_buffer(&phase, sizeof phase)), scratch);
2654 const std::array<std::uint64_t, 2> mapping{key[3], phase};
2655 scratch_put(scratch, {scratch_permutation, target++, 0, 0},
2656 scratch_bytes(mapping.data(), sizeof mapping));
2657 });
2658 // Apply disjoint permutation cycles with one saved Trade. Erasing a
2659 // mapping marks that target visited in backing, without a RAM bitmap.
2660 for (std::size_t cycle = start; cycle < end; ++cycle) {
2661 auto handle = scratch_find(scratch, {scratch_permutation, cycle, 0, 0});
2662 if (handle == ::pineforge::detail::scratch_no_handle) continue;
2663 const Trade row = trades[cycle];
2664 auto* const replay = report_replay_state();
2665 const auto saved_replay = replay ? replay->trade(cycle) : report::ReplayTrade{};
2666 std::size_t current = cycle;
2667 for (;;) {
2668 std::array<std::uint64_t, 2> mapping{};
2669 require_trade_scratch(scratch.read(handle, ::pineforge::detail::ScratchField::value, 0,
2670 scratch_buffer(mapping.data(), sizeof mapping)), scratch);
2671 const auto source = static_cast<std::size_t>(mapping[0]);
2672 const auto phase = static_cast<std::uint8_t>(mapping[1]);
2673 // Both indices are global; only the adapter translates the
2674 // sealed base. Clear an existing phase even when None moves
2675 // here, without extending an absent all-None tail.
2676 if (phase != none || adapter.exit_phase_value(current) != none)
2677 adapter.set_exit_phase(current, phase);
2678 ++adapter.exit_group_work_.replacements; if (source == cycle) trades.replace(current, row);
2679 else trades.replace(current, trades[source]);
2680 if (replay)
2681 replay->replace(current, source == cycle ? saved_replay : replay->trade(source));
2682 require_trade_scratch(scratch.erase(handle), scratch);
2683 if (source == cycle) break;
2684 current = source;
2685 handle = scratch_find(scratch, {scratch_permutation, current, 0, 0});
2687 throw coded<std::logic_error>(RunFailureCode::engine_invariant, {},
2688 "incomplete trade scratch permutation");
2689 }
2690 }
2691 transaction.finish(); adapter.finish_exit_group_sort(scratch, scratch_before);
2692 return start;
2693 }
2694 return std::nullopt;
2695}
2696
2697void source::PineStrategyHost::scheduler_update_session_state() {
2698 // session_ismarket_ and the _regular pair are the kernel's session-day
2699 // facts of this script bar, selected before the source callback reads
2700 // them (R5 lane F5). The rule lane E26 established here -- TradingView
2701 // ends a session at the session DAY, the flag belonging to the last chart
2702 // bar whose successor is out of session or on another session day,
2703 // isfirstbar its dual; the `lab tv` tapes in tests/fixtures/session_islastbar
2704 // give it bar for bar -- is the kernel's generic reading of the run's
2705 // calendar and input (NativeDecisionContext, market_driver.hpp), so every
2706 // driving path reads the same regular day by construction. The plain pair
2707 // starts from it; the chart's day an extended-hours chart widens it to is
2708 // Pine's own, below (update_extended_session_day).
2709 //
2710 // Pine's live-probe protocol (set_realtime_tail) stays too: its batch's
2711 // final bar is the still-forming realtime bar, so that bar closes its
2712 // session day where the calendar does, not by the batch's run-end
2713 // convention -- the kernel's open-ended reading, which equals the ordinary
2714 // one on every other bar.
2715 //
2716 // The scheduler's old lookahead indexed its retained input by the count
2717 // of source bars published, which under calc_on_order_fills read two bars
2718 // ahead on a bar a fill recalculation had already published
2719 // (tests/test_session_day_facts_adapter.cpp); the chart day's lookahead
2720 // starts from the point's own input and its script bar's end instead.
2721 const NativeCurrentPointView* point = detail::callback_point(*this);
2722 if (!point) return;
2723 const NativeDecisionContext& facts = point->decision;
2724 session_ismarket_ = facts.in_session;
2725 session_isfirstbar_regular_ = facts.opens_session_day;
2726 session_islastbar_regular_ = realtime_tail_ ? facts.closes_session_day_open_ended
2727 : facts.closes_session_day;
2728 session_isfirstbar_ = session_isfirstbar_regular_;
2729 session_islastbar_ = session_islastbar_regular_;
2730 // TradingView's symbol calendar, where the run holds one: a regular
2731 // intraday bar ends its session day when the calendar's next slot does not
2732 // lie in it -- OANDA:XAUUSD's 16:45 ET bar (the next slot, 17:00, opens the
2733 // next day), not its 12:45 ET bar of 2025-07-04, where the feed stops and
2734 // the calendar goes on (te_session_calendar_xau15-jul4). The first bar of a
2735 // day stays the one the run holds (isfirstbar is the bar after a bar of
2736 // another day, on the calendar and off it).
2737 if (facts.in_session && calendar_period_for(script_tf_) == CalendarPeriod::NONE
2738 && script_tf_seconds_ > 0) {
2739 if (const auto* day = symbol_calendar_day(facts.script_bar_open_ms)) {
2740 const bool last = facts.script_bar_open_ms
2741 + static_cast<std::int64_t>(script_tf_seconds_) * 1000 >= day->second;
2742 session_islastbar_regular_ = last;
2743 session_islastbar_ = last;
2744 }
2745 }
2746 update_extended_session_day(facts);
2747}
2748
2749// session.isfirstbar / session.islastbar mark the CHART's session day (R5
2750// lane K-SESSION-WINDOWS F2). With extended hours TradingView's chart holds
2751// the regular day's pre- and post-market bars too, and the plain pair spans
2752// them while the _regular pair keeps the regular day: on NASDAQ:AAPL 60 with
2753// extended hours isfirstbar is the 04:00 bar and islastbar the 19:00 one,
2754// isfirstbar_regular 10:00 and islastbar_regular 15:00
2755// (tests/fixtures/session_windows). A chart without extended hours holds no
2756// pre- or post-market bar, so there the pairs are one. The regular day is the
2757// kernel's session-day facts; pre- and post-market are session.ispremarket /
2758// ispostmarket's windows (session_time.hpp), which exist only on a session
2759// day that opens and closes on one date, the date the chart's day is keyed
2760// to. A bar neither in session nor pre- or post-market is in no chart day.
2761//
2762// The bar before is the script bar published last. The bar after is the
2763// first input the run holds -- a batch's, or a stream's warmup
2764// (PineScheduler::retained_input_from) -- past this script bar's interval,
2765// so the chart, magnified and aggregated paths read one answer for the same
2766// bars; a batch's final bar closes its day as the kernel's does unless the
2767// live probe tail is still forming it. Without such a bar a pre- or
2768// post-market bar reads the chart timeframe's next slot, while a regular bar
2769// keeps the kernel's close: nothing says whether the post-market bars that
2770// slot names will come.
2771void source::PineStrategyHost::update_extended_session_day(const NativeDecisionContext& facts) {
2772 const std::string& session = syminfo_.session;
2773 const std::string& tz = syminfo_.timezone;
2774 const std::int64_t at = facts.script_bar_open_ms;
2775 const bool regular = facts.in_session;
2776 // A regular bar that neither opens nor closes the regular day is inside
2777 // it, and so inside the chart's: the kernel's answer stands.
2778 if (regular && !facts.opens_session_day && !session_islastbar_regular_) return;
2779 const auto pre = [&](std::int64_t ms) {
2780 return pineforge::session_in_premarket(session, tz, ms, script_tf_);
2781 };
2782 const auto post = [&](std::int64_t ms) {
2783 return pineforge::session_in_postmarket(session, tz, ms, script_tf_);
2784 };
2785 const bool premarket = !regular && pre(at);
2786 const bool postmarket = !regular && !premarket && post(at);
2787 if (!regular && !premarket && !postmarket) return;
2788
2789 const std::int64_t day = pineforge::calendar_day_open_local_ms(at, tz);
2790 const auto same_day = [&](std::int64_t ms) {
2791 return pineforge::calendar_day_open_local_ms(ms, tz) == day;
2792 };
2793 const auto in_chart_day = [&](std::int64_t ms) {
2794 return (pineforge::session_in_market(session, tz, ms) || pre(ms) || post(ms))
2795 && same_day(ms);
2796 };
2797 const std::optional<std::int64_t> before = scheduler_.last_published_script_open();
2798 // The next script bar opens at or after this one's interval end: an
2799 // aggregated bucket's, or -- a FeedTolerant chart bar's interval being its
2800 // label alone -- the instant after the label.
2801 const std::int64_t next_open = std::max(at + 1, facts.script_interval.next_period_open_ms);
2802 std::optional<std::int64_t> after;
2803 bool known_after = false;
2804 if (const auto bar = scheduler_.retained_input_from(facts.coordinate.input_interval_index,
2805 next_open)) {
2806 after = bar->timestamp;
2807 known_after = true;
2808 }
2809 const bool run_end = !known_after && !scheduler_.retains_stream() && !realtime_tail_;
2810 if (!known_after && !run_end && !regular && script_tf_seconds_ > 0)
2811 after = at + static_cast<std::int64_t>(script_tf_seconds_) * 1000;
2812
2813 if (regular) {
2814 // The regular day's first bar opens the chart's unless pre-market
2815 // bars of its date precede it; its last closes the chart's unless
2816 // post-market bars of its date follow.
2817 if (facts.opens_session_day)
2818 session_isfirstbar_ = !(before && pre(*before) && same_day(*before));
2819 if (session_islastbar_regular_)
2820 session_islastbar_ = !(known_after && post(*after) && same_day(*after));
2821 return;
2822 }
2823 if (premarket) {
2824 session_isfirstbar_ = !(before && pre(*before) && same_day(*before));
2825 session_islastbar_ = after ? !in_chart_day(*after) : run_end;
2826 return;
2827 }
2828 session_isfirstbar_ = !(before && in_chart_day(*before));
2829 session_islastbar_ = after ? !(post(*after) && same_day(*after)) : run_end;
2830}
2831
2832void source::PineStrategyHost::scheduler_publish_source_bar(
2833 const Bar& bar, bool, bool advance_source_index) {
2834 current_bar_ = bar;
2835 const bool temporary_index = !advance_source_index
2836 && (!scheduler_.current_script_bar()
2837 || scheduler_.current_script_bar()->timestamp != bar.timestamp);
2838 const int previous_bar_index = bar_index_;
2839 const bool previous_barstate_islast = barstate_islast_;
2840 if (advance_source_index || temporary_index) ++source_bar_index_;
2841 ++source_callback_count_;
2842 bar_index_ = source_bar_index_;
2843 const NativeLifecycleKind lifecycle = detail::run_kind(*this);
2844 const NativeRunPhase phase = detail::run_phase(*this);
2845 if (lifecycle == NativeLifecycleKind::Running && phase == NativeRunPhase::Warmup) {
2846 barstate_islast_ = false;
2847 } else if (lifecycle == NativeLifecycleKind::Running
2848 && phase == NativeRunPhase::Realtime) {
2849 barstate_islast_ = true;
2850 } else {
2851 barstate_islast_ = source_bar_index_ == source_last_bar_index_;
2852 }
2853 // The chart day partition, the EMA seeding default and the TA bar context
2854 // this publication runs under, written on the pump's runtime block (R5
2855 // lane D2-C: no thread-local WRITE per bar; runtime_ambient.hpp). The
2856 // readers still reach the block through the thread's pointer, one
2857 // thread-local access per call: every extremum ring behind ta.highest /
2858 // lowest / highestbars / lowestbars / stoch / wpr / range, and the day
2859 // partition behind the default-anchored ta.vwap, crosses_boundary(DAY) and
2860 // the session-period helpers (docs/native-refactor-progress.md, "Deferred
2861 // performance work").
2862 internal::RuntimeAmbient* const ambient = NativeExecutionConsumer::bound(*this).pump_ambient();
2863 internal::AmbientDayPartitionScope chart_day_partition(
2864 ambient, chart_day_partition_.empty() ? nullptr : &chart_day_partition_);
2865 // A named-entry cancellation token has source-evaluation scope. Clear a
2866 // prior callback before publishing receipts and entering this body.
2867 adapter_.begin_source_evaluation();
2868 // A selected window's pre-roll evaluation records its commands and risk
2869 // statements instead of placing them: the buffer is this evaluation's alone,
2870 // reset here even if the body calls nothing, and the scope closes the bracket
2871 // on every exit (pine_intent_state.hpp).
2872 const bool capturing = selected_begin_evaluation(bar.timestamp);
2873 const detail::IntentCaptureScope capture_scope(*this, capturing);
2874 // Publish terminal and group-adjustment receipts before the source body
2875 // reads its public pending projection at this decision boundary.
2876 if (same_bar_exit_group_possible(trades_)) {
2877 if (const auto moved = sort_same_bar_exit_trades(trades_, adapter_))
2878 native_closed_rows_amended(*moved);
2879 }
2880 // Publish only the immutable prefix, once at this script-bar
2881 // boundary. Keep the last exit-time block AND its predecessor: sorting
2882 // and global-child re-cutting both inspect that predecessor.
2883 auto& consumer = detail::run_consumer(*this);
2884 if (consumer.streams_closed_rows(*this) && consumer.begin_closed_row_seal(bar.timestamp)) {
2885 const std::size_t base = trades_.base();
2886 auto prefix = consumer.closed_trade_prefix();
2887 if (prefix.base != base)
2888 throw coded<std::logic_error>(RunFailureCode::engine_invariant, {},
2889 "source trade seal has a mismatched base");
2890 const bool can_swap = adapter_.short_seed_.final_short.incarnation != 0
2891 && adapter_.config_.default_qty_type != static_cast<int>(QtyType::FIXED);
2892 const std::size_t next = consumer.closed_rows_sealable(trades_,
2893 adapter_.exit_phase_final_, adapter_.source_money_folded_, bar.timestamp,
2894 can_swap ? std::optional<std::uint64_t>(adapter_.short_seed_.materialize_long.incarnation)
2895 : std::nullopt);
2896 if (next - base > adapter_.trade_exit_phase_.size())
2897 throw coded<std::logic_error>(RunFailureCode::engine_invariant, {},
2898 "source trade seal exceeds resident exit phases");
2899 for (std::size_t index = base; index < next; ++index) {
2900 const Trade& row = trades_[index];
2901 prefix.exit_phase_digest = PineExecutionAdapter::fold_exit_phase(
2902 prefix.exit_phase_digest, adapter_.trade_exit_phase_[index - base]);
2903 if (row.pnl > 0.0) prefix.source_gains += row.pnl;
2904 else prefix.source_losses += row.pnl;
2905 // L18's position-tail spill consumes this row here, before the
2906 // seal below retires storage, including older-time barriers.
2907 }
2908 if (next > base) {
2909 // L18: spill the position tail of the whole prefix, in forward row
2910 // order, before the seal retires its storage; a throw seals nothing.
2911 adapter_.preserve_position_tail_before_seal(
2912 next, consumer.report_scratch_directory(*this));
2913 consumer.seal_closed_rows(*this, next, prefix.exit_phase_digest,
2914 prefix.source_gains, prefix.source_losses);
2915 for (std::size_t offset = 0; offset < next - base; ++offset)
2916 consumer.fold_sealed_exit_phase(adapter_.trade_exit_phase_[offset]);
2917 adapter_.trade_exit_phase_.erase(adapter_.trade_exit_phase_.begin(),
2918 adapter_.trade_exit_phase_.begin() + (next - base));
2919 }
2920 }
2921 adapter_.observe_terminal_receipts();
2922 // ab9714be src/source/pine_scheduler.cpp:242,258: under
2923 // process_orders_on_close the orders that were already resting fill at step 1
2924 // (process_pending_orders(before_pooc_script=true)) BEFORE the strategy body
2925 // runs at step 3, so a bracket leg this route parked while its parent entry
2926 // was pending settles on the touch bar ahead of this bar's source
2927 // evaluation. Draining it here keeps the body's position state, and the
2928 // levels it re-prices, on the same side of the fill as the owner; a leg that
2929 // carries a predecessor receipt stays staged for the flush below the body.
2930 // Quiet-bar gates (pine_quiet_bar.hpp): a drain with nothing staged or
2931 // queued is not called.
2932 using detail::QuietHook;
2933 using detail::skip_quiet;
2934 if (!skip_quiet(QuietHook::BracketLegs, adapter_.pending_bracket_legs_.empty())) {
2935 adapter_.flush_pending_bracket_legs({}, /*post_calculation=*/false,
2936 /*pre_script_drain=*/true);
2937 }
2938 internal::AmbientEmaSeedingScope ema_scope(ambient, chart_ema_na_warmup_);
2939 internal::AmbientBarContextScope bar_scope(ambient, pine_bar_index(),
2940 scheduler_.bar_index_offset());
2941 position_entry_count_ = detail::run_position(*this).signed_units == 0.0
2942 ? 0 : adapter_.source_entry_slot_count();
2943 on_source_bar(bar);
2944 // The pass is over: a pre-fill average it held for the script goes
2945 // before the orders it placed settle (hold_script_average_price).
2946 release_script_average_price();
2947 // Handwritten/source-generated callbacks historically read and could
2948 // update the live Pine configuration fields directly. Keep the adapter's
2949 // source policy view synchronized at the callback boundary; the generic
2950 // NativeRunSpec remains immutable for the run.
2951 adapter_.set_configuration(config_);
2952 if (capturing) selected_finish_evaluation();
2953 if (temporary_index) {
2954 --source_bar_index_;
2955 bar_index_ = previous_bar_index;
2956 barstate_islast_ = previous_barstate_islast;
2957 }
2958 finish_adapter_evaluation();
2959 if (advance_source_index) {
2960 scheduler_mark_report_point(bar.timestamp);
2961 prev_bar_timestamp_ = bar.timestamp;
2962 }
2963}
2964
2965// The drains that close one source evaluation once its body has run, in the
2966// order they always ran in. The replay of a selected window's pre-roll rows ends
2967// its evaluation with this same list (selected_replay_at_open).
2968void source::PineStrategyHost::finish_adapter_evaluation() {
2969 // Quiet-bar gates (pine_quiet_bar.hpp), as at the publication.
2970 using detail::QuietHook;
2971 using detail::skip_quiet;
2972 if (!skip_quiet(QuietHook::PendingCloses, adapter_.close_batch_callsites_.empty()))
2973 adapter_.flush_pending_closes();
2974#if PINEFORGE_PINE_QUIET_BAR_GATES
2975 // flush_pending_entries' own quiet gate, asked here instead of behind the
2976 // call (R5 lane D2-D): with nothing delayed, batched or held, the flush
2977 // only resets the batch's close total. The gate is asked once either way,
2978 // so the probe counts what it always counted.
2979 if (adapter_.host_ != nullptr && adapter_.delayed_market_orders_.empty()
2980 && adapter_.pending_same_bar_commands_.empty() && adapter_.pending_entries_.empty()) {
2981 (void)skip_quiet(QuietHook::PendingEntries, true);
2982 adapter_.pending_same_bar_close_qty_ = 0.0;
2983 } else {
2984 adapter_.flush_pending_entries();
2985 }
2986#else
2987 adapter_.flush_pending_entries();
2988#endif
2989 if (!skip_quiet(QuietHook::BracketLegs, adapter_.pending_bracket_legs_.empty()))
2990 adapter_.flush_pending_bracket_legs();
2991 // After every command of this evaluation is in the kernel: a queued
2992 // relative strategy.exit whose parent entry is now a live request becomes
2993 // that parent's anchored bracket child.
2994 if (!skip_quiet(QuietHook::RelativeExits, adapter_.anchored_relative_legs_.empty()
2995 && adapter_.pending_relative_exits_.empty())) {
2996 adapter_.anchor_relative_exits();
2997 }
2998 adapter_.order_same_point_entries();
2999}
3000
3001void source::PineStrategyHost::scheduler_publish_suppressed_tail(const Bar& bar) {
3002 // ab9714be pine_scheduler.cpp:222-231: the forming probe tail advances
3003 // source history and settles the already-matched broker book, but does
3004 // not invoke generated code or synthesize a range-end close.
3005 current_bar_ = bar;
3006 ++source_bar_index_;
3007 bar_index_ = source_bar_index_;
3008 barstate_islast_ = false;
3009 internal::AmbientDayPartitionScope chart_day_partition(
3010 NativeExecutionConsumer::bound(*this).pump_ambient(),
3011 chart_day_partition_.empty() ? nullptr : &chart_day_partition_);
3012 adapter_.begin_source_evaluation();
3013 // A selected pre-roll tail runs no body: it leaves an empty last-present
3014 // evaluation, so no earlier bar's signal outlives it.
3015 selected_clear_suppressed_evaluation(bar.timestamp);
3016 adapter_.observe_terminal_receipts();
3017 scheduler_mark_report_point(bar.timestamp);
3018 prev_bar_timestamp_ = bar.timestamp;
3019}
3020
3021// The selected window's pre-roll capture hooks (pine_intent_state.hpp).
3022bool source::PineStrategyHost::selected_begin_evaluation(std::int64_t label_ms) {
3023 const detail::PreRollEvaluation pre_roll = detail::selected_pre_roll_evaluation(*this, label_ms);
3024 if (pre_roll.state == nullptr) return false;
3025 detail::reset_intent_evaluation(*pre_roll.state, label_ms, config_,
3026 adapter_.capture_replay_risk_config(), *pre_roll.point);
3027 pre_roll.state->capturing = true;
3028 return true;
3029}
3030
3031void source::PineStrategyHost::selected_finish_evaluation() noexcept {
3033 if (state == nullptr || !state->capturing) return;
3034 state->end_config = config_;
3035 state->end_risk = adapter_.capture_replay_risk_config();
3036 if (const NativeCurrentPointView* const point = detail::intent_live_point(*this))
3037 state->terminal_point = *point;
3038 state->capturing = false;
3039}
3040
3041void source::PineStrategyHost::selected_clear_suppressed_evaluation(std::int64_t label_ms) {
3042 const detail::PreRollEvaluation pre_roll = detail::selected_pre_roll_evaluation(*this, label_ms);
3043 if (pre_roll.state == nullptr) return;
3044 detail::reset_intent_evaluation(*pre_roll.state, label_ms, config_,
3045 adapter_.capture_replay_risk_config(), *pre_roll.point);
3046}
3047
3048namespace {
3049
3050// The selected window's replay (selected_replay_at_open) hands each retained
3051// row to the adapter's own door with exactly the arguments the script passed.
3052// The host's wrappers are never used: they would take the trading-window gate
3053// again and copy the live configuration over the row's. The nine command doors
3054// first, then the six risk setter statements.
3055void replay_intent(source::PineExecutionAdapter& adapter,
3056 const source::detail::IntentEntry& row) {
3057 source::admit_strategy_entry(row.id, row.is_long, row.limit_price, row.stop_price,
3058 row.qty, row.comment, row.oca_name, row.oca_type, row.qty_type);
3059 adapter.entry(row.id, row.is_long, row.limit_price, row.stop_price, row.qty, row.comment,
3060 row.oca_name, row.oca_type, row.qty_type);
3061}
3062void replay_intent(source::PineExecutionAdapter& adapter,
3063 const source::detail::IntentClose& row) {
3064 source::admit_strategy_close(row.id, row.comment, row.qty, row.qty_percent, row.immediately);
3065 adapter.close(row.id, row.comment, row.qty, row.qty_percent, row.immediately);
3066}
3067void replay_intent(source::PineExecutionAdapter& adapter,
3068 const source::detail::IntentCloseToken& row) {
3069 source::admit_strategy_close_token(row.id, row.comment, row.qty, row.qty_percent,
3070 row.immediately, row.callsite_token);
3071 adapter.close(row.id, row.comment, row.qty, row.qty_percent, row.immediately,
3072 row.callsite_token);
3073}
3074void replay_intent(source::PineExecutionAdapter& adapter, const source::detail::IntentCloseAll&) {
3076 adapter.close_all();
3077}
3078void replay_intent(source::PineExecutionAdapter& adapter,
3079 const source::detail::IntentExit& row) {
3080 source::admit_strategy_exit(row.id, row.from_entry, row.limit_price, row.stop_price,
3081 row.trail_points, row.trail_offset, row.trail_price,
3082 row.qty_percent, row.comment, row.qty, row.oca_name,
3083 row.profit_ticks, row.loss_ticks);
3084 adapter.exit(row.id, row.from_entry, row.limit_price, row.stop_price, row.trail_points,
3085 row.trail_offset, row.trail_price, row.qty_percent, row.comment, row.qty,
3086 row.oca_name, row.profit_ticks, row.loss_ticks);
3087}
3088void replay_intent(source::PineExecutionAdapter& adapter,
3089 const source::detail::IntentExitCancelBracket& row) {
3090 source::admit_strategy_exit_cancel_bracket(row.exit_id, row.from_entry, row.comment);
3091 adapter.exit_cancel_bracket(row.exit_id, row.from_entry, row.comment);
3092}
3093void replay_intent(source::PineExecutionAdapter& adapter,
3094 const source::detail::IntentCancel& row) {
3096 adapter.cancel(row.id);
3097}
3098void replay_intent(source::PineExecutionAdapter& adapter,
3099 const source::detail::IntentCancelAll&) {
3101 adapter.cancel_all();
3102}
3103void replay_intent(source::PineExecutionAdapter& adapter,
3104 const source::detail::IntentOrder& row) {
3105 source::admit_strategy_order(row.id, row.is_long, row.qty, row.limit_price, row.stop_price,
3106 row.oca_name, row.oca_type);
3107 adapter.order(row.id, row.is_long, row.qty, row.limit_price, row.stop_price, row.oca_name,
3108 row.oca_type);
3109}
3110void replay_intent(source::PineExecutionAdapter& adapter,
3111 const source::detail::IntentRiskDirection& row) {
3112 // The retained int direction/count/cap rows cannot carry NaN/infinity.
3113 adapter.set_risk_direction(row.direction);
3114}
3115void replay_intent(source::PineExecutionAdapter& adapter,
3116 const source::detail::IntentRiskMaxConsLossDays& row) {
3117 adapter.set_risk_max_cons_loss_days(row.value);
3118}
3119void replay_intent(source::PineExecutionAdapter& adapter,
3120 const source::detail::IntentRiskMaxDrawdown& row) {
3121 admit_pine_risk_max_drawdown(row.value, row.percent);
3122 adapter.set_risk_max_drawdown(row.value, row.percent);
3123}
3124void replay_intent(source::PineExecutionAdapter& adapter,
3125 const source::detail::IntentRiskMaxIntradayLoss& row) {
3126 admit_pine_risk_max_intraday_loss(row.value, row.percent);
3127 adapter.set_risk_max_intraday_loss(row.value, row.percent);
3128}
3129// As the host setter: the script's own statement adopts the count switches first.
3130void replay_intent(source::PineExecutionAdapter& adapter,
3131 const source::detail::IntentRiskMaxIntradayFilledOrders& row) {
3132 if (row.limit > 0) adapter.cap.adopt_script_count();
3133 adapter.cap = row.limit;
3134}
3135void replay_intent(source::PineExecutionAdapter& adapter,
3136 const source::detail::IntentRiskMaxPositionSize& row) {
3138 adapter.set_risk_max_position_size(row.value);
3139}
3140
3141// A selected replay the engine cannot take, or that a native failure cut short,
3142// is the engine's fault and fails the run: it is never skipped quietly. After a
3143// native failure the consumer already holds the first cause and keeps it (its
3144// bar-open catch records only when nothing failed before).
3145[[noreturn]] void replay_invariant(const char* what) {
3146 throw coded<std::logic_error>(RunFailureCode::engine_invariant, {}, what);
3147}
3148
3149// Ends a replay bracket on every exit, normal or by exception: the retained
3150// point pointer cleared, the open activation scope closed (the grants it
3151// recorded stay for the Open match that follows) and the adapter's
3152// configuration back to the host's. Two stores, a noexcept end and a plain
3153// copy: nothing here executes a command or allocates.
3154class ReplayBracket {
3155public:
3156 ReplayBracket(source::detail::PineIntentState& state, NativeExecutionConsumer& consumer,
3157 source::PineExecutionAdapter& adapter,
3158 const source::PineStrategyConfig& config) noexcept
3159 : state_(&state), consumer_(&consumer), adapter_(&adapter), config_(&config) {}
3160 ReplayBracket(const ReplayBracket&) = delete;
3161 ReplayBracket& operator=(const ReplayBracket&) = delete;
3162 ~ReplayBracket() noexcept {
3163 state_->replay_point = nullptr;
3164 consumer_->end_open_activation();
3165 adapter_->set_configuration(*config_);
3166 }
3167
3168private:
3169 source::detail::PineIntentState* state_;
3170 NativeExecutionConsumer* consumer_;
3171 source::PineExecutionAdapter* adapter_;
3172 const source::PineStrategyConfig* config_;
3173};
3174
3175} // namespace
3176
3177// The selected window's replay (pine_intent_state.hpp): the last pre-roll
3178// evaluation's rows, placed once at the window's first Open, before the adapter
3179// reads that bar's open. They are placed as the evaluation placed them, in its
3180// own order, each under the configuration and the retained point it was called
3181// with, so the Open matcher that follows sees the prior bar's orders. Nothing of
3182// the script runs again; nothing is published, matched or executed here.
3183void source::PineStrategyHost::selected_replay_at_open(const Bar& bar,
3184 const NativeDecisionContext&) {
3185 pf_selected_window_config_v1 window{};
3186 if (!NativeExecutionConsumer::selected_window_admitted(*this, &window)
3187 || bar.timestamp < window.start_ms) {
3188 return;
3189 }
3191 if (state == nullptr) replay_invariant("selected replay without its intent state");
3192 if (state->replayed) return;
3193 if (!state->has_evaluation) {
3194 state->replayed = true;
3195 return;
3196 }
3197 // The bar before the window left the book as it was: nothing held, nothing
3198 // working or queued in the source layer, no recalculation open.
3199 if (state->capturing) replay_invariant("selected replay inside a pre-roll capture bracket");
3200 if (state->label_ms >= window.start_ms)
3201 replay_invariant("selected replay of an evaluation labelled inside the window");
3202 if (detail::run_position(*this).signed_units != 0.0)
3203 replay_invariant("selected replay over a held position");
3204 if (adapter_.coof_recalc_active_)
3205 replay_invariant("selected replay inside a calc_on_order_fills recalculation");
3206 if (!adapter_.live_handles_.empty() || !adapter_.delayed_market_orders_.empty()
3207 || !adapter_.pending_same_bar_commands_.empty() || !adapter_.pending_entries_.empty()
3208 || !adapter_.close_batch_callsites_.empty() || !adapter_.pending_bracket_legs_.empty()
3209 || !adapter_.anchored_relative_legs_.empty()
3210 || !adapter_.pending_relative_exits_.empty()) {
3211 replay_invariant("selected replay over live requests or queued source commands");
3212 }
3213 // Marked before the bracket opens: a failure inside it never retries.
3214 state->replayed = true;
3215 NativeExecutionConsumer& consumer = NativeExecutionConsumer::bound(*this);
3216 if (!consumer.begin_open_activation(*this))
3217 replay_invariant("selected replay without an open activation scope");
3218 const ReplayBracket bracket(*state, consumer, adapter_, config_);
3219 const auto stop_if_failed = [this] {
3220 if (detail::run_kind(*this) == NativeLifecycleKind::Failed)
3221 replay_invariant("selected replay stopped by a native failure");
3222 };
3223 using detail::QuietHook;
3224 using detail::skip_quiet;
3225
3226 // The evaluation again, from its start: the configuration it began under,
3227 // then its receipts and the legs staged before its body, as
3228 // scheduler_publish_source_bar takes them.
3229 state->replay_point = &state->terminal_point;
3230 assert_admitted_pine_replay_risk_config(state->start_risk);
3231 adapter_.restore_replay_risk_config(state->start_risk);
3232 adapter_.set_configuration(state->start_config);
3233 adapter_.begin_source_evaluation();
3234 adapter_.observe_terminal_receipts();
3235 stop_if_failed();
3236 if (!skip_quiet(QuietHook::BracketLegs, adapter_.pending_bracket_legs_.empty())) {
3237 adapter_.flush_pending_bracket_legs({}, /*post_calculation=*/false,
3238 /*pre_script_drain=*/true);
3239 }
3240 stop_if_failed();
3241
3242 // Its statements, in the order the script made them.
3243 for (detail::PineIntentRow& row : state->rows) {
3244 state->replay_point = &row.point;
3245 adapter_.set_configuration(row.config);
3246 std::visit([this](const auto& payload) { replay_intent(adapter_, payload); },
3247 row.payload);
3248 stop_if_failed();
3249 }
3250
3251 // Its end: the configuration it ended under (the risk configuration alone,
3252 // never a counter), then the drains every source evaluation ends with.
3253 state->replay_point = &state->terminal_point;
3254 adapter_.set_configuration(state->end_config);
3256 adapter_.restore_replay_risk_config(state->end_risk);
3257 finish_adapter_evaluation();
3258 stop_if_failed();
3259
3260 // The terminal explicit-market policy of a process_orders_on_close pass,
3261 // which the close of that bar would have run: submit-only here, so the
3262 // reconciled entries stay working for the open matcher. The caller's own
3263 // quiet and count gate, as PineExecutionAdapter::on_bar_close asks it; the
3264 // policy keeps its own filters.
3265 const NativeDecisionContext& terminal = state->terminal_point.decision;
3266 const bool bound = adapter_.host_ != nullptr;
3267 const int bar_index = adapter_.projection_bar_index(terminal);
3268 std::size_t explicit_entries = 0;
3269 if (adapter_.config_.process_orders_on_close && adapter_.config_.pyramiding == 0
3270 && adapter_.modeled_input()) {
3271 for (const auto& handle : adapter_.live_handles_) {
3272 const auto found = adapter_.placement_.find(handle.incarnation);
3273 if (found != adapter_.placement_.end() && found->second.opening
3274 && found->second.family == PineOrderFamily::Entry
3275 && found->second.projection_created_bar == bar_index
3276 && std::isfinite(found->second.requested_qty)
3277 && found->second.requested_qty > 0.0) {
3278 ++explicit_entries;
3279 }
3280 }
3281 }
3282 if (!skip_quiet(QuietHook::TerminalExplicitMarket, bound && explicit_entries < 2))
3283 adapter_.apply_terminal_explicit_market_policy(terminal, false);
3284 stop_if_failed();
3285
3286 // Spent only once it all succeeded. The retained configurations, the terminal
3287 // point and the replayed flag stay as the hash reads them.
3288 state->rows.clear();
3289}
3290
3291// The Pine report series has one point per SOURCE slot this host published,
3292// which is not the kernel's per-calculation cadence: a calc_on_order_fills
3293// re-entry marks the slot it opened at the fill, the ordinary close
3294// calculation then marks nothing, and the probe's suppressed tail marks a
3295// slot generated code never calculated. The point also has to land inside
3296// this callback, before scheduler_record_broker_hash() folds the extremes it
3297// just moved and before scheduler_record_range_end() re-marks the curve's
3298// last point. So the cadence stays here and the recording does not: the
3299// kernel owns what a report point is — the extremes fold and the curve
3300// append in engine.hpp — reached through the run spec's report policy.
3301void source::PineStrategyHost::scheduler_mark_report_point(std::int64_t script_bar_ts) {
3302 detail::run_consumer(*this).mark_script_report_point(*this, script_bar_ts);
3303}
3304
3305void source::PineStrategyHost::scheduler_record_broker_hash() {
3306 if (!broker_state_hash_recording_) return;
3307 const std::uint64_t execution = execution_consumer().continuation_hash();
3308 // As capture_script_continuation_hash: a carried read failure is thrown
3309 // before the cut hash is latched or recorded as a row.
3310 as_native_consumer(execution_consumer()).request_core().rethrow_book_read_failure();
3311 last_script_continuation_hash_ = execution;
3312 last_script_continuation_valid_ = true;
3313 broker_state_hashes_.push_back(broker_state_hash());
3314}
3315
3317 return adapter_.core_sizes_default_opening(is_long);
3318}
3319
3320// The chart's time_close. A D/W/M chart bar closes at its period's last
3321// traded close, to the millisecond, whatever time of day its stamp reads, as
3322// time_close(timeframe.period) reads it on that chart (chart_period_close_ms,
3323// src/session_time.cpp, where the rule and its tapes are stated): OANDA
3324// stamps its XAUUSD daily bars at 17:00 ET, inside the 1800-1700 session's
3325// break, and the time-of-day session filter the intraday path applies read
3326// that stamp as out of session, so the D chart answered na.
3328 return chart_bar_close_ms(current_bar_.timestamp);
3329}
3330
3332 if (calendar_period_for(script_tf_) != CalendarPeriod::NONE) {
3333 return pine_time_close(stamp, script_tf_, "", "", script_tf_,
3334 syminfo_.timezone, syminfo_.session);
3335 }
3336 return pineforge::detail::symbol_session_time_close(stamp, script_tf_, syminfo_.session,
3337 syminfo_.timezone, script_tf_);
3338}
3339
3340// TradingView's time(tf, session, tz, bars_back, timeframe_bars_back) (lab tv
3341// tapes te_time_bb_chart and te_time_bb_tf, tests/fixtures/time_bars_back):
3342// the chart bar bars_back names -- a past one from the chart's history, na
3343// before its first -- then the tf bar holding it, stepped timeframe_bars_back
3344// of its own bars. TradingView steps forward, and steps tf bars, on its
3345// symbol calendar, closed days included: NASDAQ:AAPL's Thursday 2025-04-17
3346// 15:45 bar reads its next bar at Monday 04-21 09:30 over Good Friday, its
3347// daily bar of 01-08 the next at 01-10 over the 01-09 closure, time("D", 0,
3348// -1) on 04-17 reads 04-21 and time("60", 0, 1) on Monday's 09:30 bar
3349// Thursday's 15:30. The host holds no calendar of closed days; the chart's
3350// history holds exactly the calendar's bars on every tape but one, whose
3351// history lacks bars TradingView's calendar holds (OANDA:XAUUSD's 17:00-18:00
3352// ET slot and Good Friday, the test's no-bar slots). So the host reads the
3353// bars from the input it retains (a batch's, or a stream's warmup); past its
3354// edges it walks the symbol's session calendar (session_trades_at): a batch's
3355// final bar asks the calendar for the bar after it, as a live bar does.
3356namespace {
3357
3358constexpr std::int64_t kTimeOffsetMinuteMs = 60'000;
3359// The farthest the calendar walk looks for a traded minute: past any
3360// weekend and any run of closed days.
3361constexpr int kTimeOffsetCalendarMinutes = 16 * 24 * 60;
3362// The most chart bars a step inside a tf bar may pass before it leaves it.
3363constexpr int kTimeOffsetBarsPerStep = 100'000;
3364
3365} // namespace
3366
3367int64_t source::PineStrategyHost::pine_time_offset(int64_t bar_open_ms, int bars_back,
3368 const std::string& tf,
3369 const std::string& session,
3370 const std::string& tz,
3371 int timeframe_bars_back,
3372 bool close) const {
3373 if (is_na(bar_open_ms) || is_na(bars_back) || is_na(timeframe_bars_back))
3374 return na<int64_t>();
3375 const std::string& sym_tz = syminfo_.timezone;
3376 const std::string& sym_session = syminfo_.session;
3377 const auto input = scheduler_.retained_input();
3378 const bool input_is_chart = scheduler_.retained_input_is_chart();
3379 const bool daily_chart = calendar_period_for(script_tf_) != CalendarPeriod::NONE;
3380 // A chart bar's own open (the input's stamp) and close, as the chart's
3381 // time and time_close read them (chart_time_close).
3382 const auto chart_open_of = [&](std::int64_t input_ms) -> std::int64_t {
3383 return input_is_chart ? input_ms
3384 : pine_time(input_ms, script_tf_, "", "", script_tf_, sym_tz,
3385 sym_session);
3386 };
3387 // Where the run holds TradingView's symbol calendar (set_symbol_calendar),
3388 // the chart bars ahead are its slots: the chart's next bar, the first bar
3389 // from an instant, a chart bar's close and a day's open and close are the
3390 // calendar's inside its days, whatever bars the history holds there
3391 // (OANDA:XAUUSD's 17:00 ET slot, Good Friday's session). Bars behind stay
3392 // the history's. The calendar's slots are an intraday chart's bars and a
3393 // 1D chart's days; a W, M or multi-day chart's bars are no calendar day,
3394 // so it reads its history.
3395 const std::int64_t chart_step_ms = static_cast<std::int64_t>(script_tf_seconds_) * 1000;
3396 const bool calendar_chart = !symbol_calendar_.empty()
3397 && (!daily_chart || (calendar_period_for(script_tf_) == CalendarPeriod::DAY
3398 && script_tf_seconds_ == 86400));
3399 const auto chart_close_of = [&](std::int64_t open_ms) -> std::int64_t {
3400 if (const auto* day = calendar_chart ? symbol_calendar_day(open_ms) : nullptr) {
3401 return daily_chart || chart_step_ms <= 0 ? day->second
3402 : std::min(open_ms + chart_step_ms, day->second);
3403 }
3404 return daily_chart ? pine_time_close(open_ms, script_tf_, "", "", script_tf_, sym_tz,
3405 sym_session)
3407 open_ms, script_tf_, sym_session, sym_tz, script_tf_);
3408 };
3409 const auto chart_slot_of = [&](std::int64_t ms) -> std::int64_t {
3410 return pine_time(ms, script_tf_, "", "", script_tf_, sym_tz, sym_session);
3411 };
3412 // The first chart bar opening at or after `at`: the retained input's, else
3413 // the calendar's first traded chart slot.
3414 const auto chart_open_from = [&](std::int64_t at) -> std::int64_t {
3415 if (calendar_chart) {
3416 if (const std::int64_t slot = symbol_calendar_open_from(at); !is_na(slot)) return slot;
3417 }
3418 auto it = std::lower_bound(input.begin(), input.end(), at,
3419 [](const Bar& bar, std::int64_t value) {
3420 return bar.timestamp < value;
3421 });
3422 while (!input_is_chart && it != input.end() && chart_open_of(it->timestamp) < at)
3423 ++it;
3424 if (it != input.end()) return chart_open_of(it->timestamp);
3425 std::int64_t t = at;
3426 for (int k = 0; k < kTimeOffsetCalendarMinutes; ++k, t += kTimeOffsetMinuteMs) {
3427 if (session_trades_at(sym_session, sym_tz, t)) {
3428 const std::int64_t slot = chart_slot_of(t);
3429 return is_na(slot) || slot < at ? t : slot;
3430 }
3431 }
3432 return na<int64_t>();
3433 };
3434 // The last chart bar opening before `before`: the retained input's while
3435 // it holds the instant (a stream's warmup ends where its live bars
3436 // begin), else the calendar's last traded chart slot.
3437 const auto chart_open_before = [&](std::int64_t before) -> std::int64_t {
3438 if (calendar_chart) {
3439 if (const std::int64_t slot = symbol_calendar_open_before(before); !is_na(slot))
3440 return slot;
3441 }
3442 auto it = std::lower_bound(input.begin(), input.end(), before,
3443 [](const Bar& bar, std::int64_t value) {
3444 return bar.timestamp < value;
3445 });
3446 const bool held = !scheduler_.retains_stream() || it != input.end();
3447 if (held && it != input.begin()) return chart_open_of(std::prev(it)->timestamp);
3448 std::int64_t t = before - kTimeOffsetMinuteMs;
3449 for (int k = 0; k < kTimeOffsetCalendarMinutes; ++k, t -= kTimeOffsetMinuteMs) {
3450 if (session_trades_at(sym_session, sym_tz, t)) {
3451 const std::int64_t slot = chart_slot_of(t);
3452 return is_na(slot) || slot >= before ? t : slot;
3453 }
3454 }
3455 return na<int64_t>();
3456 };
3457 // The chart bar after the one opening at `open_ms`: the retained input's
3458 // next bar, else the calendar's first traded slot past its close.
3459 const auto next_chart_open = [&](std::int64_t open_ms) -> std::int64_t {
3460 if (calendar_chart && symbol_calendar_day(open_ms) != nullptr) {
3461 if (const std::int64_t slot = symbol_calendar_open_from(open_ms + 1); !is_na(slot))
3462 return slot;
3463 }
3464 auto it = std::upper_bound(input.begin(), input.end(), open_ms,
3465 [](std::int64_t value, const Bar& bar) {
3466 return value < bar.timestamp;
3467 });
3468 while (!input_is_chart && it != input.end() && chart_open_of(it->timestamp) <= open_ms)
3469 ++it;
3470 if (it != input.end()) return chart_open_of(it->timestamp);
3471 const std::int64_t end = chart_close_of(open_ms);
3472 return chart_open_from(is_na(end) || end <= open_ms ? open_ms + 1 : end);
3473 };
3474
3475 // A chart bar before the current one is one the run has had: the
3476 // retained input's, na before its first (time[bars_back]). A stream's
3477 // live bars are past its warmup: the calendar's.
3478 const auto prior_chart_open = [&](std::int64_t open_ms) -> std::int64_t {
3479 auto it = std::lower_bound(input.begin(), input.end(), open_ms,
3480 [](const Bar& bar, std::int64_t value) {
3481 return bar.timestamp < value;
3482 });
3483 if (scheduler_.retains_stream() && it == input.end())
3484 return chart_open_before(open_ms);
3485 while (it != input.begin()) {
3486 --it;
3487 const std::int64_t prior = chart_open_of(it->timestamp);
3488 if (prior < open_ms) return prior;
3489 }
3490 return na<int64_t>();
3491 };
3492
3493 std::int64_t chart_open = bar_open_ms;
3494 for (int k = 0; k < -bars_back && !is_na(chart_open); ++k)
3495 chart_open = next_chart_open(chart_open);
3496 for (int k = 0; k < bars_back && !is_na(chart_open); ++k)
3497 chart_open = prior_chart_open(chart_open);
3498 if (is_na(chart_open)) return na<int64_t>();
3499
3500 // The chart's own timeframe without a session: the chart bars themselves.
3501 if ((tf.empty() || tf == script_tf_) && session.empty()) {
3502 for (int k = 0; k < -timeframe_bars_back && !is_na(chart_open); ++k)
3503 chart_open = next_chart_open(chart_open);
3504 for (int k = 0; k < timeframe_bars_back && !is_na(chart_open); ++k)
3505 chart_open = chart_open_before(chart_open);
3506 if (is_na(chart_open)) return na<int64_t>();
3507 return close ? chart_close_of(chart_open) : chart_open;
3508 }
3509
3510 // A week or a month opens at its first traded day: the week of Martin
3511 // Luther King Day 2025 at Tuesday 01-21 on NASDAQ:AAPL, where the
3512 // calendar reads Monday (te-time-bb-tf-aapl1d). The day the chart's
3513 // history first trades in the period, when it trades later than the
3514 // calendar's first session day.
3515 const CalendarPeriod tf_period = calendar_period_for(tf.empty() ? script_tf_ : tf);
3516 // A day of the symbol calendar is its "D" bar.
3517 const bool calendar_days = session.empty() && tf_period == CalendarPeriod::DAY
3518 && tf_to_seconds(tf.empty() ? script_tf_ : tf) == 86400;
3519 const auto calendar_open_of = [&](std::int64_t ms) -> std::int64_t {
3520 if (calendar_days) {
3521 if (const auto* day = symbol_calendar_day(ms)) return day->first;
3522 }
3523 return pine_time(ms, tf, session, tz, script_tf_, sym_tz, sym_session);
3524 };
3525 const auto tf_close_of = [&](std::int64_t open_ms) -> std::int64_t {
3526 if (calendar_days) {
3527 if (const auto* day = symbol_calendar_day(open_ms)) return day->second;
3528 }
3529 return pine_time_close(open_ms, tf, session, tz, script_tf_, sym_tz, sym_session);
3530 };
3531 const auto tf_open_of = [&](std::int64_t ms) -> std::int64_t {
3532 const std::int64_t open = calendar_open_of(ms);
3533 if (is_na(open) || !session.empty()
3534 || (tf_period != CalendarPeriod::WEEK && tf_period != CalendarPeriod::MONTH)) {
3535 return open;
3536 }
3537 const std::int64_t first = chart_open_from(open);
3538 if (is_na(first) || calendar_open_of(first) != open) return open;
3539 const std::int64_t day = pine_time(first, "D", "", "", script_tf_, sym_tz, sym_session);
3540 return !is_na(day) && day > open ? day : open;
3541 };
3542 std::int64_t open = tf_open_of(chart_open);
3543 if (is_na(open)) return na<int64_t>();
3544 // A timeframe finer than the chart's steps by its own length and reads
3545 // the chart bar holding the instant it reaches: time("60", 0, -1) on a
3546 // daily bar is that bar, time("60", 0, 1) the bar before
3547 // (te-time-bb-tf-aapl1d / -btc1d / -eurusd1d).
3548 const int tf_seconds = tf_to_seconds(tf.empty() ? script_tf_ : tf);
3549 const int chart_seconds = tf_to_seconds(script_tf_);
3550 if (timeframe_bars_back != 0 && tf_seconds > 0 && chart_seconds > 0
3551 && tf_seconds < chart_seconds) {
3552 const std::int64_t at = open - static_cast<std::int64_t>(timeframe_bars_back)
3553 * tf_seconds * 1000;
3554 const std::int64_t held = chart_open_before(at + 1);
3555 if (is_na(held)) return na<int64_t>();
3556 return close ? chart_close_of(held) : held;
3557 }
3558 for (int k = 0; k < -timeframe_bars_back && !is_na(open); ++k) {
3559 // The first chart bar past this tf bar's end that a tf bar holds.
3560 const std::int64_t end = tf_close_of(open);
3561 std::int64_t at = chart_open_from(is_na(end) || end <= open ? open + 1 : end);
3562 std::int64_t next = na<int64_t>();
3563 for (int guard = 0; guard < kTimeOffsetBarsPerStep && !is_na(at); ++guard) {
3564 next = tf_open_of(at);
3565 if (!is_na(next) && next > open) break;
3566 next = na<int64_t>();
3567 at = next_chart_open(at);
3568 }
3569 open = next;
3570 }
3571 for (int k = 0; k < timeframe_bars_back && !is_na(open); ++k) {
3572 // The last chart bar before this tf bar's open that a tf bar holds.
3573 std::int64_t at = chart_open_before(open);
3574 std::int64_t prev = na<int64_t>();
3575 for (int guard = 0; guard < kTimeOffsetBarsPerStep && !is_na(at); ++guard) {
3576 prev = tf_open_of(at);
3577 if (!is_na(prev) && prev < open) break;
3578 prev = na<int64_t>();
3579 at = chart_open_before(at);
3580 }
3581 open = prev;
3582 }
3583 if (is_na(open)) return na<int64_t>();
3584 return close ? tf_close_of(open) : open;
3585}
3586
3589 const NativeDecisionContext& context) {
3590 const bool recalculation_due = scheduler_.coof_recalculation_due(event, context, *this);
3591 if (!recalculation_due) record_applied_range_end();
3592 // A process-on-close fill can append admission events after on_native_bar.
3593 // Retire that completed tail without recapturing the broker-state row:
3594 // F0a preserves the existing recording and continuation boundaries.
3595 // A synchronous fill inside an unfinished command is folded by the
3596 // enclosing on_bar_close instead.
3597 if (!recalculation_due && context.is_terminal_sub_bar
3598 && context.coordinate.provenance == NativePriceProvenance::AfterCalculationClose
3599 && !adapter_.admission_journal.has_unfinished_events()) {
3600 adapter_.finish_admission_bar();
3601 }
3602}
3603
3604} // namespace pineforge
3605
3606namespace pineforge {
3607
3608void source::PineStrategyHost::stage_symbol_feed_views() {
3609 if (symbol_feed_views_.size() != symbol_feeds_.size())
3611 "request data: symbol feed view table is out of sync");
3612 std::vector<NativeInstrumentFeedView> descriptors(symbol_feed_views_.size());
3613 std::vector<const NativeInstrumentFeedView*> slots(symbol_feed_views_.size(), nullptr);
3614 for (std::size_t i = 0; i < symbol_feed_views_.size(); ++i) {
3615 const BorrowedFeedSlot& slot = symbol_feed_views_[i];
3616 if (!slot.present) continue;
3617 descriptors[i] = {{slot.bars, slot.size}, slot.close_ms};
3618 slots[i] = &descriptors[i];
3619 }
3620 NativeExecutionConsumer::bound(*this).stage_instrument_feed_views(
3621 slots.empty() ? nullptr : slots.data(), slots.size());
3622}
3623
3624} // namespace pineforge
static OrderBirth chart_evaluation(int bar, int64_t timestamp)
The code carried beside a thrown exception.
The refusal of a read-only state query, or of a report-only write, that may no longer be answered.
const std::shared_ptr< const CommandObservation > & observation() const
void bind(std::shared_ptr< const CommandObservation > observation)
A thrown legacy setter's original message, propagated to native begin.
Base with a code: throw coded<std::invalid_argument>(code, {args}, text).
std::uint64_t record_count() const noexcept override
ScratchStatus put(const ScratchByteSource &key, const ScratchByteSource &value, ScratchHandle &result) noexcept
The public native host: an abstract subclass of BacktestEngine with no PineScript on it.
Trade numeric_at(size_type global) const
std::string entry_id_at(size_type global) const
static std::uint64_t fold_exit_phase(std::uint64_t digest, std::uint8_t phase) noexcept
compat::pine::Calculation fixture_cap_calculation() const
std::map< long long, double > symbol_calendar_opens_
std::map< std::string, RecordedSeries > recorded_series_
void set_syminfo_session(const std::string &)
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 prepare_native_begin(const NativeBeginArgs &) final
BarTime fixture_chart_time(std::int64_t timestamp_ms) const
compat::pine::CapClock fixture_cap_clock() const
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 on_native_tick(const Bar &, const NativeTickContext &) final
const Series< double > & source_input_series(const std::string &key, const Series< double > &fallback) const
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
void configure_pine_strategy(const PineStrategyConfig &)
int observe_pending_level_resolved(int) const override
bool set_symbol_calendar(std::vector< std::pair< std::int64_t, std::int64_t > > sessions)
virtual void on_native_applied_event(const native_order::ExecutionAppliedEvent &, const NativeDecisionContext &)
std::vector< BorrowedFeedSlot > symbol_feed_views_
const Series< double > & source_series(const std::string &) const
void apply_realtime_tail_horizon(const Bar *bars, int n, bool script_bar_geometry)
int64_t chart_bar_close_ms(int64_t stamp) const
std::vector< std::pair< std::int64_t, std::int64_t > > symbol_calendar_
void on_native_applied(const native_order::ExecutionAppliedEvent &, const NativeDecisionContext &) final
std::vector< admission::Field > market_admission_fields() const
PineStrategyHost(compat::pine::CapAttachment cap=compat::pine::CapAttachment::None)
int observe_last_bar_dual_entry_path_v1() const override
void set_trade_scratch(detail::CommandScratchStore &)
std::uint64_t broker_state_hash_projection() const override
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::uint64_t fixture_applied_receipt_count() const
bool report_terminal_quote_applied(const ReportC &) const
std::vector< NativeInstrumentFeed > symbol_feeds_
std::int64_t symbol_calendar_open_before(std::int64_t before) const
void on_native_input(const Bar &, const NativeInputContext &) final
std::map< long long, double > symbol_calendar_closes_
int short_seed_collision_role_v1(native_order::RequestHandle) const noexcept
void on_native_bar_open(const Bar &, const NativeDecisionContext &) final
bool set_report_terminal_quote(std::int64_t time_ms, double close)
MarketAdmissionJournal & market_admission_journal()
static detail::ScratchKeyOrder trade_scratch_order() noexcept
NativePrecommitVerdict validate_execution_precommit(const NativePrecommitView &) const final
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
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
void set_syminfo_metadata(const std::string &, double) override
void set_strategy_override(const StrategyOverrides &)
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
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
ScratchStatus finish(CommandScratchStore &store, ScratchStatus status) noexcept
std::uint64_t slots(std::uint64_t keys)
void bind_id_pair_scratch_tables(source::PineExecutionAdapter &, const std::shared_ptr< ScratchSlab > &)
std::uint64_t ScratchHandle
constexpr ScratchHandle scratch_no_handle
int64_t symbol_session_time_close(int64_t bar_ms, const std::string &tf, const std::string &session, const std::string &tz, const std::string &chart_tf)
void bind_tree_scratch_tables(source::PineExecutionAdapter &, const std::shared_ptr< ScratchSlab > &)
void rethrow_carried_scratch_failure(const ReportScratchIoError *failure)
NativeRunPhase
Which driving is Running: a batch run(), a stream's internal warmup, or its realtime leg.
NativeCalculationReason
Why the kernel is asking the host to calculate.
NativePrecommitVerdict
The host is consulted before generic opening-margin admission.
NativeLifecycleKind
Where a host stands.
NativeInstrumentFeedReadView native_instrument_feed_view(const NativeInstrumentFeed &owned, const NativeInstrumentFeedView *borrowed) noexcept
Common accessor for effective feed data.
std::optional< Timeframe > parse_timeframe(std::string_view text)
NativePathOrder
Generic ordering for a modeled OHLC path.
std::optional< Plan > plan(double signed_position, Reduce request) noexcept
int b(int64_t c)
Definition color.hpp:44
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)
std::string SourceId
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.
int64_t na< int64_t >()
Definition na.hpp:66
CalendarPeriod calendar_period_for(const std::string &tf)
Determine the calendar period for a target TF string.
void note_run_failure(BacktestEngine &engine, std::string text, RunFailureCode code, const RunFailureArgs &args={})
double na< double >()
Definition na.hpp:64
bool session_in_market(const std::string &session, const std::string &tz, int64_t bar_ms)
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)
Definition drawing.hpp:58
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)
T na()
Definition na.hpp:54
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())
int64_t timestamp
Definition bar.hpp:8
int64_t equity_curve_len
Definition engine.hpp:309
pf_equity_point_t * equity_curve
Definition engine.hpp:308
A validated code and its canonical arguments: the JSON object with its keys sorted and no whitespace,...
int32_t exit_bar_index
Definition engine.hpp:251
bool exit_from_bracket
Definition engine.hpp:213
std::string exit_comment
Definition engine.hpp:200
execution::CloseCause close_cause
Definition engine.hpp:232
int64_t exit_time
Definition engine.hpp:189
std::string exit_id
Definition engine.hpp:201
int64_t entry_time
Definition engine.hpp:188
Ephemeral read-only facts of one anchored-leg materialization (L7b), offered to the host exactly once...
Borrowed begin-call facts.
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.
One committed execution: the definition, the cursor, the resolved price and units,...
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.
What a run holds of the symbol the N-day request reads.
int days
N of the request, at least 2.
std::vector< std::int64_t > account_fx_effective_from_ms