PineForge v0.13.1-379-g9b50973
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
pine_strategy_host.cpp
Go to the documentation of this file.
2#include <pineforge/ta.hpp>
4
5#include "../engine_internal.hpp"
7#include "../timezone.hpp"
8#include "../native_execution_consumer.hpp"
9
10#include <algorithm>
11#include <cmath>
12#include <ctime>
13#include <limits>
14#include <numeric>
15#include <stdexcept>
16#include <utility>
17#include <variant>
18
19namespace pineforge {
20using namespace source;
21
22namespace {
23
24bool priced_opening_trigger(const native_order::Trigger& trigger) {
25 return std::holds_alternative<native_order::Stop>(trigger)
26 || std::holds_alternative<native_order::Limit>(trigger)
27 || std::holds_alternative<native_order::StopLimit>(trigger);
28}
29
30// ab9714be pine_fills.cpp:35-44: a priced entry masks the extreme traversed
31// before the fill position on the assumed OHLC path.
32void set_entry_fill_excursion_masks(PyramidEntry& pe, const Bar& bar, double fill_price) {
33 double fill_pos = 0.0;
34 if (!internal::first_touch_position(bar, fill_price, &fill_pos)) return;
35 const bool high_first = internal::bar_path_uses_high_first(bar);
36 const double high_pos = high_first ? 1.0 : 2.0;
37 const double low_pos = high_first ? 2.0 : 1.0;
38 pe.skip_entry_bar_high = (high_pos < fill_pos);
39 pe.skip_entry_bar_low = (low_pos < fill_pos);
40}
41
42// The kernel still samples the delivered path at every driver point, which
43// can book an entry-bar extreme the owner masks out (a gap-through stop is
44// filled 1 ulp beyond the open). On the entry bar of a masked lot the host's
45// own masked H/L/C walk is authoritative, so it replaces whatever the path
46// sampling carried.
47
48void replace_masked_entry_bar_extremes(std::vector<PyramidEntry>& lots, PositionSide side,
49 int bar_index, const Bar& bar) {
50 if (side == PositionSide::FLAT || lots.empty()) return;
51 if (!std::isfinite(bar.high) || !std::isfinite(bar.low) || !std::isfinite(bar.close))
52 return;
53 const bool is_long = (side == PositionSide::LONG);
54 for (auto& pe : lots) {
55 if (pe.entry_bar_index != bar_index) continue;
56 if (!pe.skip_entry_bar_high && !pe.skip_entry_bar_low) continue;
57 const double pe_hi = pe.skip_entry_bar_high ? pe.price : bar.high;
58 const double pe_lo = pe.skip_entry_bar_low ? pe.price : bar.low;
59 const double fav_px = is_long ? pe_hi : pe_lo;
60 const double adv_px = is_long ? pe_lo : pe_hi;
61 const double favorable = is_long ? (fav_px - pe.price) * pe.qty
62 : (pe.price - fav_px) * pe.qty;
63 const double adverse = is_long ? (pe.price - adv_px) * pe.qty
64 : (adv_px - pe.price) * pe.qty;
65 const double closing = is_long ? (bar.close - pe.price) * pe.qty
66 : (pe.price - bar.close) * pe.qty;
67 pe.max_runup = std::max(0.0, std::max(favorable, closing));
68 pe.max_drawdown = std::max(0.0, std::max(adverse, -closing));
69 }
70}
71
72[[noreturn]] void reject_begin_bar(int index, const char* field, const char* detail) {
73 throw std::invalid_argument(
74 "bar[" + std::to_string(index) + "]." + field + (detail ? detail : ""));
75}
76
77// Validate the borrowed public begin array before the source provider stages
78// syminfo, inputs, adapter state, or a native run spec. This mirrors the
79// legacy chart/stream shape checks and deliberately does not impose native
80// calendar or slot-label policy; those remain the generic preflight's job.
81void validate_source_begin_bars(const NativeBeginArgs& args) {
82 if (args.n < 0) throw std::invalid_argument("bar count must be non-negative");
83 if (args.n > 0 && args.bars == nullptr)
84 throw std::invalid_argument("bars must be non-null for a nonempty array");
85 for (int i = 0; i < args.n; ++i) {
86 const Bar& bar = args.bars[i];
87 if (!std::isfinite(bar.open)) reject_begin_bar(i, "open", " must be finite");
88 if (!std::isfinite(bar.high)) reject_begin_bar(i, "high", " must be finite");
89 if (!std::isfinite(bar.low)) reject_begin_bar(i, "low", " must be finite");
90 if (!std::isfinite(bar.close)) reject_begin_bar(i, "close", " must be finite");
91 if (args.is_stream) {
92 if (bar.timestamp < 0)
93 reject_begin_bar(i, "timestamp", " must be non-negative");
94 if (bar.open < 0.0) reject_begin_bar(i, "open", " must be non-negative");
95 if (bar.high < 0.0) reject_begin_bar(i, "high", " must be non-negative");
96 if (bar.low < 0.0) reject_begin_bar(i, "low", " must be non-negative");
97 if (bar.close < 0.0) reject_begin_bar(i, "close", " must be non-negative");
98 if (!std::isfinite(bar.volume) || bar.volume < 0.0)
99 reject_begin_bar(i, "volume", " must be non-negative finite");
100 } else if (!std::isnan(bar.volume)
101 && (!std::isfinite(bar.volume) || bar.volume < 0.0)) {
102 reject_begin_bar(i, "volume", " must be non-negative finite or NaN (unavailable)");
103 }
104 if (bar.low > std::min(bar.open, bar.close))
105 reject_begin_bar(i, "low", " must not exceed open or close");
106 if (bar.high < std::max(bar.open, bar.close))
107 reject_begin_bar(i, "high", " must not be below open or close");
108 if (i > 0) {
109 const std::int64_t previous = args.bars[i - 1].timestamp;
110 if (bar.timestamp <= previous)
111 reject_begin_bar(i, "timestamp", " must be strictly increasing");
112 if (previous < 0
113 && bar.timestamp > std::numeric_limits<std::int64_t>::max() + previous) {
114 reject_begin_bar(i, "timestamp", " delta exceeds int64 range");
115 }
116 }
117 }
118 if (args.is_stream && args.n > 0
119 && (!std::isfinite(args.bars[args.n - 1].close)
120 || args.bars[args.n - 1].close <= 0.0)) {
121 throw std::invalid_argument("stream warmup final close must be finite and positive");
122 }
123}
124
125} // namespace
126
129 adapter_(*this, cap),
131 _src_open_(scheduler_.language()._src_open_),
132 _src_high_(scheduler_.language()._src_high_),
133 _src_low_(scheduler_.language()._src_low_),
136 _src_hl2_(scheduler_.language()._src_hl2_),
137 _src_hlc3_(scheduler_.language()._src_hlc3_),
141 // ab9714be LegacyCompatibilityConsumer::refuse was a no-op on this handle.
142 host_mutation_guard_inert_ = true;
143}
144
145std::uint64_t source::PineStrategyHost::adapter_event_high_water(
146 const NativeStrategyHost& base) noexcept {
147 const auto& host = static_cast<const PineStrategyHost&>(base);
148 return as_native_consumer(const_cast<IExecutionConsumer&>(host.execution_consumer()))
149 .event_high_water();
150}
151
152std::uint64_t source::PineStrategyHost::adapter_terminal_receipt_high_water(
153 const NativeStrategyHost& base) noexcept {
154 const auto& host = static_cast<const PineStrategyHost&>(base);
155 return as_native_consumer(const_cast<IExecutionConsumer&>(host.execution_consumer()))
156 .terminal_receipt_high_water();
157}
158
160 // Fold current source/generic state with the last script-point
161 // continuation. Recording only controls whether the per-bar array is
162 // retained; the continuation snapshot keeps the scalar independent of
163 // that switch and of NativeCompleted teardown.
164 const std::uint64_t execution = last_script_continuation_valid_
165 ? last_script_continuation_hash_
166 : execution_consumer().continuation_hash();
167 return broker_state_hash_from_execution_hash(execution);
168}
169
171 return compute_liquidation_price();
172}
173
174double source::PineStrategyHost::compute_liquidation_price() const {
175 if (position_side_ == PositionSide::FLAT) return na<double>();
176 const double point_value = syminfo_.pointvalue;
177 const double quantity = position_qty_;
178 if (!(quantity > 0.0) || !(point_value > 0.0)) return na<double>();
179 const double direction = position_side_ == PositionSide::LONG ? 1.0 : -1.0;
180 const double margin_pct = position_side_ == PositionSide::LONG
181 ? config_.margin_long : config_.margin_short;
182 const double denominator = (margin_pct / 100.0) - direction;
183 if (std::abs(denominator) < 1e-12) return na<double>();
184 const double equity_basis =
185 (initial_capital_ + net_profit_sum_) / active_account_currency_fx();
186 double liquidation =
187 (equity_basis / (quantity * point_value) - direction * position_entry_price_)
188 / denominator;
189 if (syminfo_mintick_ > 0.0) {
190 liquidation = position_side_ == PositionSide::SHORT
191 ? std::ceil(liquidation / syminfo_mintick_) * syminfo_mintick_
192 : std::floor(liquidation / syminfo_mintick_) * syminfo_mintick_;
193 }
194 return liquidation;
195}
196
197PineStrategyConfig source::PineStrategyHost::apply_overrides(
198 PineStrategyConfig config, const StrategyOverrides& overrides) {
199 if (!std::isnan(overrides.initial_capital)) config.initial_capital = overrides.initial_capital;
200 if (!std::isnan(overrides.commission_value)) config.commission_value = overrides.commission_value;
201 if (!std::isnan(overrides.default_qty_value)) config.default_qty_value = overrides.default_qty_value;
202 if (overrides.pyramiding >= 0) config.pyramiding = overrides.pyramiding;
203 if (overrides.slippage >= 0) config.slippage = overrides.slippage;
204 if (overrides.commission_type >= 0) config.commission_type = overrides.commission_type;
205 if (overrides.default_qty_type >= 0) config.default_qty_type = overrides.default_qty_type;
206 if (overrides.process_orders_on_close >= 0)
207 config.process_orders_on_close = overrides.process_orders_on_close != 0;
208 if (overrides.calc_on_order_fills >= 0)
209 config.calc_on_order_fills = overrides.calc_on_order_fills != 0;
210 if (overrides.close_entries_rule >= 0)
211 config.close_entries_rule_any = overrides.close_entries_rule != 0;
212 return config;
213}
214
215StagedConfiguration source::PineStrategyHost::staged_configuration() const {
216 StagedConfiguration staged;
217 staged.syminfo = syminfo_;
218 staged.syminfo.mintick = syminfo_mintick_;
219 staged.inputs = inputs_;
220 staged.chart_timezone = chart_timezone_;
221 staged.account_fx = account_currency_fx_;
222 staged.account_fx_effective_from_ms = account_currency_fx_timestamps_;
223 staged.account_fx_per_quote = account_currency_fx_rates_;
224 if (std::isfinite(qty_step_) && qty_step_ > 0.0) staged.quantity_grid = qty_step_;
225 return staged;
226}
227
229 // Idle abort requests are consumed by the public begin entry even when
230 // input validation refuses before a run starts. Crucially, validation
231 // runs before any provider-owned state is changed.
232 abort_requested_.store(false, std::memory_order_relaxed);
233 validate_source_begin_bars(args);
234 if (args.syminfo) {
235 syminfo_ = *args.syminfo;
236 syminfo_mintick_ = syminfo_.mintick;
237 if (std::isfinite(syminfo_.qty_step) && syminfo_.qty_step > 0.0)
238 qty_step_ = syminfo_.qty_step;
239 }
240 if (args.inputs) inputs_ = *args.inputs;
241
242 if (args.is_stream && native_security_feed_enabled()) {
243 throw std::runtime_error(
244 "native request.security feed supports historical runs only");
245 }
246
247 if (!(args.n < 2 && !args.is_stream)) {
248 std::string effective_input = args.input_tf;
249 if (effective_input.empty() && args.n >= 2 && args.bars != nullptr)
250 effective_input = detect_timeframe(args.bars, args.n);
251 const std::string effective_script = args.script_tf.empty()
252 ? effective_input : args.script_tf;
253 try {
254 if (!effective_input.empty() && !effective_script.empty()
255 && tf_ratio(effective_input, effective_script) == -2) {
256 throw std::runtime_error(
257 "script timeframe must be coarser than or equal to input timeframe: requested script_tf "
258 + effective_script + " from input timeframe " + effective_input);
259 }
260 } catch (const std::runtime_error&) {
261 throw;
262 } catch (...) {
263 // The native specification validator owns malformed literals.
264 }
265 }
266
267 if (args.is_stream && config_.calc_on_order_fills) {
268 throw std::runtime_error(
269 "native stream requires close-only calculation; calc_on_order_fills is unsupported");
270 }
272 throw std::runtime_error("native stream cannot use historical probe/tail overrides");
273 }
274 PineStrategyConfig effective = config_;
275 if (args.overrides_opaque) {
276 const auto* overrides = static_cast<const StrategyOverrides*>(args.overrides_opaque);
277 effective = apply_overrides(effective, *overrides);
278 }
279 const StagedConfiguration staged = staged_configuration();
280 if (!staged.account_fx_effective_from_ms.empty() && effective.calc_on_order_fills)
281 throw std::logic_error(
282 "timestamped account-currency FX does not support calc_on_order_fills");
283 if (!staged.account_fx_effective_from_ms.empty() && args.bar_magnifier)
284 throw std::logic_error(
285 "timestamped account-currency FX is not supported with bar magnifier");
286
287 adapter_.reset_for_run();
288 adapter_.set_receipt_high_water_readers(&PineStrategyHost::adapter_event_high_water,
289 &PineStrategyHost::adapter_terminal_receipt_high_water);
290 if (args.n > 0 && static_cast<std::size_t>(args.n)
291 <= std::numeric_limits<std::size_t>::max() / 4U) {
292 as_native_consumer(execution_consumer()).reserve_driver_log(
293 static_cast<std::size_t>(args.n) * 4U);
294 }
295 // Source placement evidence is retained by request incarnation so a
296 // re-issued bracket can preserve its exact historical projection. Batch
297 // callers already disclose their bar count here; reserve the ordinary
298 // two-leg-per-bar capacity once instead of repeatedly rehashing that
299 // durable table during a long replay.
300 if (args.n > 0 && static_cast<std::size_t>(args.n)
301 <= adapter_.placement_.max_size() / 2U) {
302 adapter_.placement_.reserve(static_cast<std::size_t>(args.n) * 2U);
303 }
304 adapter_.set_configuration(effective);
305 adapter_.set_staged_configuration(staged);
306 adapter_.set_begin_mode(args.is_stream, args.bar_magnifier);
307 adapter_.set_margin_call_enabled(margin_call_enabled_);
308 scheduler_.capture_begin(args);
309 scheduler_.set_source_series_active(effective.src_series_active);
310 const NativePathOrder path_order = path_order_mode_ == 1
311 ? NativePathOrder::HighFirst
312 : (path_order_mode_ == 2 ? NativePathOrder::LowFirst
313 : NativePathOrder::Auto);
314 adapter_.set_path_order(path_order);
315 const NativeRunSpec spec = adapter_.project(effective, staged, args, path_order);
316 const auto setup = configure_native(spec);
317 if (setup.status != NativeSetupStatus::Applied)
318 throw std::logic_error("Pine native adapter failed to configure projected run spec");
319 config_ = effective;
321}
322
328 try {
329 scheduler_.run_begin(*this);
330 } catch (const std::exception& error) {
332 last_error_ = error.what();
333 } catch (...) {
335 last_error_ = "unknown error during Pine script preparation";
336 }
337}
338
339void source::PineStrategyHost::capture_script_continuation_hash() {
340 last_script_continuation_hash_ = execution_consumer().continuation_hash();
341 last_script_continuation_valid_ = true;
342 if (broker_state_hash_recording_ && !broker_state_hashes_.empty()) {
343 broker_state_hashes_.back() = broker_state_hash();
344 }
345}
346
348 const Bar& bar, const NativeInputContext& context) {
349 if (source_prepare_failed_) return;
350 if (native_state().phase == NativeRunPhase::Realtime)
351 stream_warmup_mode_ = false;
352 scheduler_.input(bar, context, *this);
353 // Aggregation can deliver leftover input after the last script callback.
354 // Refresh the last recorded row (and the continuation snapshot) so the
355 // scalar stays the same fold with or without recording.
356 if (scheduler_.terminal_source_bar()) capture_script_continuation_hash();
357}
358
360 const Bar& tick, const NativeTickContext& context) {
361 if (source_prepare_failed_) return;
362 if (native_state().phase == NativeRunPhase::Realtime)
363 stream_warmup_mode_ = false;
364 {
365 // ab9714be pine_stream.cpp:298/:450 samples the excursion at every
366 // realtime print (a price point: H == L == C == print).
367 const int sample_index = scheduler_.bar_magnifier_enabled()
368 ? scheduler_.source_bar_index_for(context.decision)
371 pyramid_entries_, position_side_, sample_index, tick);
372 }
373 scheduler_.tick(tick, context, *this);
374 adapter_.on_tick(tick, context);
375}
376
378 const Bar& bar, const NativeDecisionContext& context) {
379 if (source_prepare_failed_) return;
380 bar_magnifier_enabled_ = scheduler_.bar_magnifier_enabled();
381 diag_magnifier_sub_bars_processed_ = bar_magnifier_enabled_
382 ? static_cast<std::int64_t>(context.driver_statistics.sub_bars_processed) : 0;
383 diag_magnifier_sample_ticks_processed_ = bar_magnifier_enabled_
384 ? static_cast<std::int64_t>(context.driver_statistics.sample_ticks_processed) : 0;
385 adapter_.on_bar_open(bar, context);
386 scheduler_.bar_open(bar, context, *this);
387}
388
390 const Bar& bar, const NativeDecisionContext& context) {
391 if (source_prepare_failed_) return;
392 bar_magnifier_enabled_ = scheduler_.bar_magnifier_enabled();
393 diag_magnifier_sub_bars_processed_ = bar_magnifier_enabled_
394 ? static_cast<std::int64_t>(context.driver_statistics.sub_bars_processed) : 0;
395 diag_magnifier_sample_ticks_processed_ = bar_magnifier_enabled_
396 ? static_cast<std::int64_t>(context.driver_statistics.sample_ticks_processed) : 0;
397 adapter_.observe_terminal_receipts();
398 {
399 const int sample_index = scheduler_.bar_magnifier_enabled()
400 ? scheduler_.source_bar_index_for(context)
401 : context.coordinate.interval_index;
403 pyramid_entries_, position_side_, sample_index, bar);
404 replace_masked_entry_bar_extremes(
405 pyramid_entries_, position_side_, sample_index, bar);
406 }
407 scheduler_.bar(bar, context, *this);
408 adapter_.on_bar_close(bar, context);
409 if (adapter_.config_.slippage > 0) {
410 for (auto& lot : pyramid_entries_) {
411 if (lot.entry_bar_index == context.coordinate.interval_index && lot.qty > 0.0) {
412 const auto found = adapter_.placement_.find(lot.entry_incarnation);
413 if (found != adapter_.placement_.end()) {
414 const auto& snap = found->second;
415 const bool pure_stop_entry = snap.family == PineOrderFamily::Entry
416 && std::isfinite(snap.exit_levels.stop) && snap.exit_levels.stop > 0.0
417 && !std::isfinite(snap.exit_levels.limit);
418 if (pure_stop_entry) {
419 // ab9714be pine_risk.cpp:276-282: update_per_trade_extremes measures adverse excursion against opposite extreme
420 if (lot.price > bar.high && std::isfinite(bar.low) && bar.low > 0.0) {
421 lot.max_drawdown = std::max(lot.max_drawdown, (lot.price - bar.low) * lot.qty);
422 } else if (lot.price < bar.low && std::isfinite(bar.high) && bar.high > 0.0) {
423 lot.max_drawdown = std::max(lot.max_drawdown, (bar.high - lot.price) * lot.qty);
424 }
425 }
426 }
427 }
428 }
429 }
430 if (context.is_terminal_sub_bar
432 scheduler_record_range_end(bar);
433 }
434 const bool recording = broker_state_hash_recording_ && !broker_state_hashes_.empty();
435 const bool last_batch = context.is_terminal_sub_bar
437 const bool stream_script = context.is_terminal_sub_bar
438 && stream_phase_ == StreamPhase::REALTIME;
439 if (recording || last_batch || stream_script) {
440 // ab9714be pine_scheduler.cpp:1753/:1875 records after dispatch_bar,
441 // including the terminal source policy updates. The native hook
442 // returns through adapter_.on_bar_close after the scheduler callback,
443 // so refresh the continuation snapshot (and the just-appended row)
444 // at that boundary.
445 capture_script_continuation_hash();
446 }
447}
448
451 const NativeDecisionContext& context) {
452 if (source_prepare_failed_) return;
453 if (scheduler_.bar_magnifier_enabled()) {
454 const int source_index = scheduler_.source_bar_index_for(context);
455 for (auto& lot : pyramid_entries_) {
456 if (lot.entry_incarnation == event.handle().incarnation
457 && event.opened_units != 0.0) {
458 lot.entry_bar_index = source_index;
459 }
460 }
461 for (std::size_t i = 0; i < event.closed_trade_count; ++i) {
462 const std::size_t index = event.first_trade_index + i;
463 if (index >= trades_.size()) continue;
464 trades_[index].exit_bar_index = source_index;
465 }
466 }
467 const auto p = adapter_.placement_.find(event.handle().incarnation);
468 // ab9714be pine_fills.cpp:42: a priced (stop/limit) entry masks the
469 // assumed-OHLC extreme the path reaches BEFORE the fill.
470 const bool pine_priced = p != adapter_.placement_.end()
471 && ((std::isfinite(p->second.exit_levels.stop) && p->second.exit_levels.stop > 0.0)
472 || (std::isfinite(p->second.exit_levels.limit) && p->second.exit_levels.limit > 0.0));
473 if (event.opened_units != 0.0) {
474 if (pine_priced) {
475 const Bar& mask_bar = current_bar_;
476 for (auto& lot : pyramid_entries_) {
477 if (lot.entry_incarnation != event.handle().incarnation) continue;
478 set_entry_fill_excursion_masks(lot, mask_bar, lot.price);
479 }
480 } else if (event.cursor.point.path_phase == NativePathPhase::Close) {
481 for (auto& lot : pyramid_entries_) {
482 if (lot.entry_incarnation != event.handle().incarnation) continue;
483 lot.skip_entry_bar_high = true;
484 lot.skip_entry_bar_low = true;
485 }
486 }
487 // ab9714be pine_orders.cpp:750-753 and pine_fills.cpp:7189-7193
488 // (KI-62): a MARKET entry that adds to a live same-side position is
489 // flagged so a same-bar from_entry bracket exit covers it.
490 const bool market_add = !pine_priced && event.closed_units == 0.0
491 && p != adapter_.placement_.end()
492 && (p->second.family == PineOrderFamily::Entry
493 || p->second.family == PineOrderFamily::Order)
494 && std::any_of(pyramid_entries_.begin(), pyramid_entries_.end(),
495 [&](const PyramidEntry& lot) {
496 return lot.entry_incarnation != event.handle().incarnation;
497 });
498 if (market_add) adapter_.mark_market_pyramid_add(event.handle().incarnation);
499 }
500 // The legacy source observer counted one broker fill for every committed
501 // execution event. The native consumer owns those events now; mirror the
502 // count at its notification boundary so restored source tests and public
503 // source-side policy reads see the same monotone value.
504 // ab9714be pine_fills.cpp:5954/:6300 and the margin/FX sites: one source
505 // broker fill sequence is consumed per applied broker instruction, not
506 // per closed trade row. Native ordinals remain the execution authority;
507 // this is the generated/source-visible diagnostic projection.
508 if (broker_fill_event_seq_ == std::numeric_limits<std::uint64_t>::max())
509 throw std::overflow_error("source broker fill sequence exhausted");
510 ++broker_fill_event_seq_;
512 excursion_level_fill_ = false;
516 excursion_trail_offset_ticks_ = std::numeric_limits<double>::quiet_NaN();
517 excursion_trail_raw_price_ = std::numeric_limits<double>::quiet_NaN();
518 if (position_side_ != PositionSide::FLAT) {
519 // ab9714be engine_orders.cpp:531-541: settle_position_after_partial_exit resets to flat when position_qty_ <= kQtyEpsilon or empty
520 bool changed = false;
521 for (auto it = pyramid_entries_.begin(); it != pyramid_entries_.end(); ) {
522 if (it->qty <= internal::kQtyEpsilon) {
523 it = pyramid_entries_.erase(it);
524 changed = true;
525 } else {
526 ++it;
527 }
528 }
529 if (pyramid_entries_.empty() || position_qty_ <= internal::kQtyEpsilon) {
530 reset_position_state_to_flat();
531 } else if (changed) {
532 double total_qty = 0.0;
533 double weighted_price = 0.0;
534 for (const auto& pe : pyramid_entries_) {
535 total_qty += pe.qty;
536 weighted_price += pe.price * pe.qty;
537 }
538 position_qty_ = total_qty;
539 position_entry_price_ = weighted_price / total_qty;
540 position_entry_count_ = static_cast<int>(pyramid_entries_.size());
541 }
542 }
543 adapter_.on_applied(event, context);
544 precommit_held_units_ = std::numeric_limits<double>::quiet_NaN();
545 if (adapter_.take_intraday_loss_relabel(event.ordinal)) {
546 for (std::size_t i = 0; i < event.closed_trade_count; ++i) {
547 const std::size_t index = event.first_trade_index + i;
548 if (index >= trades_.size()) continue;
549 trades_[index].exit_id.clear();
550 trades_[index].exit_comment = source::kIntradayLossComment;
551 }
552 }
553 // R5 lane L12 (2.ii l): the kernel used to read these strings back out of
554 // the row to answer strategy_closed_trade_close_cause. They are the
555 // adapter's, so the adapter's host states the cause instead, on the rows
556 // this event produced and after the relabel above has settled them. The
557 // predicates are the retired kernel branches verbatim, including the
558 // empty-exit-id gate and the filled-orders PREFIX match.
559 for (std::size_t i = 0; i < event.closed_trade_count; ++i) {
560 const std::size_t index = event.first_trade_index + i;
561 if (index >= trades_.size()) continue;
562 Trade& row = trades_[index];
565 } else if (row.exit_id.empty()) {
566 if (row.exit_comment.rfind(source::kFillCapCommentPrefix, 0) == 0)
568 else if (row.exit_comment.rfind(source::kIntradayLossComment, 0) == 0)
570 }
571 }
572 project_short_seed_report_rows(event);
573 scheduler_.applied(event);
574 // R6: with calc_on_order_fills the kernel drives this event's
575 // recalculation next, in the same drain iteration and at the same cursor
576 // (NativeCalculationTrigger::BarCloseAndFills). The range-end row stays
577 // after it, exactly where the adapter's own cascade used to put it.
578 if (!scheduler_.coof_recalculation_due(event, context, *this))
579 record_applied_range_end();
580}
581
582void source::PineStrategyHost::record_applied_range_end() {
583 if (scheduler_.terminal_source_bar() || barstate_islast_) {
584 const Bar terminal = scheduler_.current_script_bar()
585 ? *scheduler_.current_script_bar() : current_bar_;
586 scheduler_record_range_end(terminal);
587 }
588}
589
591 const Bar& bar, const NativeDecisionContext& context,
594 if (reason != NativeCalculationReason::OrderFill) {
595 // BarClose is the script bar's own calculation; Tick and SubBar are
596 // cadences the Pine spec never selects.
597 on_native_bar(bar, context);
598 return;
599 }
600 if (source_prepare_failed_ || cause == nullptr) return;
601 // A fill Pine does not recalculate on (POOC's terminal close fill, a
602 // grouped-stop sibling, a replayed ordinal) still spends a kernel
603 // recalculation slot; it publishes nothing and books nothing.
604 if (!scheduler_.coof_recalculation_due(*cause, context, *this)) return;
605 scheduler_.recalculate(*cause, context, *this);
606 record_applied_range_end();
607}
608
613
615 const NativeMarginCheckPoint& point) const {
616 return adapter_.margin_check_allowed(point);
617}
618
620 const NativeMarginRequirementView& view) const {
621 return adapter_.resolve_margin_requirement(view);
622}
623
625 const NativeMarginCallView& view) const {
626 return adapter_.resolve_margin_call_units(view);
627}
628
630 const NativeAnchoredLevelView& view) const {
631 return adapter_.resolve_anchored_level(view);
632}
633
635 const NativePrecommitView& view) const {
636 // ab9714be pine_fills.cpp:5741: the exit-bar path prefix belongs to the
637 // closing lot's excursion only while a priced fill is being applied. The
638 // host's own sampler consumes this cache in closed_lot_excursion().
639 bool priced = false;
640 if (view.definition) {
641 const auto& trigger = view.definition->request.trigger;
642 priced = priced_opening_trigger(trigger)
643 || std::holds_alternative<native_order::Trail>(trigger);
644 }
645 // L10j: an exit leg carrying priced stop/limit/trailing terms folds its
646 // pre-fill path extremes too; the magnifier one-price gate below applies
647 // to it as well (L10h), which the former adapter-side override bypassed.
648 priced = priced || adapter_.source_priced_exit(view.target.incarnation);
649 // When an opposite market entry or script close is pending at the bar open,
650 // the legacy owner models the exit as the market order (closing the trade
651 // at the open without folding the exit bar's path).
652 if (view.cursor.point.path_phase == NativePathPhase::Open
653 && adapter_.has_pending_market_exit(view.cursor.point.interval_index)) {
654 priced = false;
655 }
656 double trail_ticks = std::numeric_limits<double>::quiet_NaN();
657 if (const auto off = adapter_.source_trail_offset_ticks(view.target.incarnation)) {
658 trail_ticks = *off;
659 }
660 excursion_priced_fill_ = priced;
661 excursion_level_fill_ = adapter_.source_post_parent_calc_level_fill(
662 view.target.incarnation);
663 excursion_margin_call_ = adapter_.source_margin_exit(view.target.incarnation)
665 excursion_trail_offset_ticks_ = trail_ticks;
666 // ab9714be pine_fills.cpp:5766-5770: the peak a TRAIL fill retraces from is
667 // taken off the matcher's pre-slip price, which this view still carries; the
668 // booked facts.fill_price the sampler reads has already been slipped by
669 // resolve_terms.
671 (!std::isnan(trail_ticks) && std::isfinite(view.raw_price)
672 && view.raw_price > 0.0)
673 ? view.raw_price
674 : std::numeric_limits<double>::quiet_NaN();
675 // A zero-offset trail's matcher touch is the half-tick boundary of the
676 // rounded price path, not its fill; the owner's peak is the pre-slip fill
677 // itself, snapped onto the tick grid (ab9714be pine_fills.cpp:5761-5771,
678 // engine_internal.hpp:187-198). Its open-gap fill is no TRAIL event
679 // (engine_path_resolve.cpp:620-631) and folds no peak.
680 if (trail_ticks == 0.0 && view.cursor.point.path_phase == NativePathPhase::Open) {
681 excursion_trail_offset_ticks_ = std::numeric_limits<double>::quiet_NaN();
682 excursion_trail_raw_price_ = std::numeric_limits<double>::quiet_NaN();
683 } else if (trail_ticks == 0.0 && std::isfinite(view.resolved_price)
684 && view.resolved_price > 0.0) {
685 const double slip = config_.slippage * syminfo_.mintick;
687 physical_position().signed_units > 0.0 ? view.resolved_price + slip
688 : view.resolved_price - slip,
689 syminfo_.mintick);
690 }
691 // A fill-through (slipped touch) trail leg books its fill slippage ticks
692 // past the touch; the owner's peak is still that leg's PRE-slip fill
693 // (ab9714be pine_fills.cpp:5760-5769 runs before apply_fill_slippage),
694 // which is the booked price with the slippage step taken back. An
695 // open-gap fill is not a TRAIL event at all (ab9714be
696 // engine_path_resolve.cpp:620-631 leaves is_trail false), so it folds
697 // no peak.
698 if (!std::isnan(trail_ticks) && view.definition && std::isfinite(view.resolved_price)) {
699 const auto* touch = std::get_if<native_order::Limit>(&view.definition->request.trigger);
700 if (touch && touch->fill_through) {
701 if (view.cursor.point.path_phase == NativePathPhase::Open) {
702 excursion_trail_offset_ticks_ = std::numeric_limits<double>::quiet_NaN();
703 excursion_trail_raw_price_ = std::numeric_limits<double>::quiet_NaN();
704 } else {
705 const double slip = config_.slippage * syminfo_.mintick;
706 excursion_trail_raw_price_ = physical_position().signed_units > 0.0
707 ? view.resolved_price + slip : view.resolved_price - slip;
708 }
709 }
710 }
711 // The margin slice's sampling chronology is a book fact resolved in the
712 // adapter precommit pass; start clean for every request.
715 precommit_held_units_ = std::abs(physical_position().signed_units);
716 return adapter_.validate_precommit(view);
717}
718
720 const ClosedLotExcursionFacts& facts) const {
721 // ab9714be engine_orders.cpp:319 build_close_trade_with_costs, excursion
722 // half. Everything here is the owner's model: the carried per-lot extremes
723 // already hold every completed source bar's masked H/L/C walk
724 // (sample_open_trade_extremes), scaled to the closed slice; the exit fill
725 // itself always belongs to the trade; a TRAIL fill retrace contributes the
726 // peak that armed it; and for a priced exit the assumed OHLC path prefix
727 // the fill sits behind is folded in, honoring the entry-bar masks when the
728 // lot opened on this same bar.
729 const double slice =
730 (facts.lot_qty > 0.0) ? (facts.closed_qty / facts.lot_qty) : 1.0;
731 double fill_fav =
732 (facts.is_long ? (facts.fill_price - facts.entry_price)
733 : (facts.entry_price - facts.fill_price))
734 * facts.closed_qty;
735 // Open-gap scratches book the script open on both legs; a 1-ULP
736 // entry/exit residual formats as CSV -0.000000 against owner's 0.
737 if (std::abs(facts.fill_price - facts.entry_price) < 1e-9) {
738 fill_fav = 0.0;
739 }
740 ClosedLotExcursion owned;
741 // ab9714be src/source/pine_scheduler.cpp:242,257 samples the bar's H/L/C
742 // into every OPEN trade (update_per_trade_extremes, step 2) only after the
743 // resting priced exits of step 1 have closed. A leg this route force-fills
744 // at its level on its own entry bar therefore keeps the owner's unsampled
745 // entry seed: no bar of this trade was ever walked by the sampler, so the
746 // exit fill and the pre-fill path prefix below are the whole excursion.
747 const bool unsampled_entry_bar = facts.entry_bar_index == facts.exit_bar_index
749 const double carried_favorable = unsampled_entry_bar ? 0.0 : facts.carried_favorable;
750 const double carried_adverse = unsampled_entry_bar ? 0.0 : facts.carried_adverse;
751 owned.favorable = std::max(carried_favorable * slice, fill_fav);
752 owned.adverse = std::max(carried_adverse * slice, -fill_fav);
753 // ab9714be pine_fills.cpp:5766-5770: a TRAIL fill retraces exactly the
754 // trailing offset from the peak that armed it, so that peak is a pre-fill
755 // favorable excursion no bar-boundary sample ever sees.
756 if (!std::isnan(excursion_trail_offset_ticks_)) {
757 const double off = excursion_trail_offset_ticks_ * syminfo_.mintick;
758 const double basis = std::isnan(excursion_trail_raw_price_)
759 ? facts.fill_price
761 const double peak = facts.is_long ? (basis + off) : (basis - off);
762 const double peak_fav = (facts.is_long ? (peak - facts.entry_price)
763 : (facts.entry_price - peak))
764 * facts.closed_qty;
765 owned.favorable = std::max(owned.favorable, peak_fav);
766 }
767 // A range-end report row is a projection at the terminal close: the owner
768 // folds no exit-bar path prefix there. A market fill lands on a bar
769 // boundary the sampler already walked, and under the bar magnifier a
770 // one-price open bar has no path left to fold.
771 if (excursion_range_end_projection_) return owned;
774 // ab9714be pine_risk.cpp:256-292: a margin-call liquidation at the
775 // adverse extreme owns the rest of the bar. Which part of the bar it
776 // owns is the slice's birth chronology: the non-POOC opening trim
777 // inherits the complete bar, while the POOC/pre-exit prefix routes
778 // sample only the traversed waypoint prefix.
779 const Bar sample_bar = margin_call_sample_bar(
780 current_bar_, facts.fill_price, excursion_margin_prefix_,
781 internal::bar_path_uses_high_first(current_bar_),
782 syminfo_.mintick, config_.slippage);
783 const bool margin_same_bar = facts.entry_bar_index == facts.exit_bar_index;
784 const double margin_high = (margin_same_bar && facts.entry_bar_high_masked)
785 ? facts.entry_price : sample_bar.high;
786 const double margin_low = (margin_same_bar && facts.entry_bar_low_masked)
787 ? facts.entry_price : sample_bar.low;
788 const double fav_px = facts.is_long ? margin_high : margin_low;
789 const double adv_px = facts.is_long ? margin_low : margin_high;
790 const double fav = (facts.is_long ? (fav_px - facts.entry_price)
791 : (facts.entry_price - fav_px))
792 * facts.closed_qty;
793 const double adv = (facts.is_long ? (facts.entry_price - adv_px)
794 : (adv_px - facts.entry_price))
795 * facts.closed_qty;
796 owned.favorable = std::max(owned.favorable, fav);
797 owned.adverse = std::max(owned.adverse, adv);
798 const double closing = (facts.is_long ? (sample_bar.close - facts.entry_price)
799 : (facts.entry_price - sample_bar.close))
800 * facts.closed_qty;
801 owned.favorable = std::max(owned.favorable, closing);
802 owned.adverse = std::max(owned.adverse, -closing);
803 return owned;
804 }
805 if (!excursion_priced_fill_) return owned;
806 // ab9714be pine_scheduler.cpp:99-106 and 548-552: the calc_on_order_fills
807 // historical dispatch books an O-point fill against a one-price point
808 // bar, so no path extreme precedes it.
809 if ((scheduler_.bar_magnifier_enabled() || config_.calc_on_order_fills)
810 && std::abs(facts.fill_price - current_bar_.open) < 1e-7) {
811 return owned;
812 }
813 const double touch_price = bar_fill_price(facts.fill_price);
814 double fill_pos = 0.0;
815 if (!internal::first_touch_position(current_bar_, touch_price, &fill_pos))
816 return owned;
817 const bool high_first = internal::bar_path_uses_high_first(current_bar_);
818 const double high_pos = high_first ? 1.0 : 2.0;
819 const double low_pos = high_first ? 2.0 : 1.0;
820 const bool same_bar = facts.entry_bar_index == facts.exit_bar_index;
821 const bool mask_high = same_bar && facts.entry_bar_high_masked;
822 const bool mask_low = same_bar && facts.entry_bar_low_masked;
823 if (high_pos < fill_pos && !mask_high) {
824 const double hi_fav = (facts.is_long
825 ? (current_bar_.high - facts.entry_price)
826 : (facts.entry_price - current_bar_.high))
827 * facts.closed_qty;
828 owned.favorable = std::max(owned.favorable, hi_fav);
829 owned.adverse = std::max(owned.adverse, -hi_fav);
830 }
831 if (low_pos < fill_pos && !mask_low) {
832 const double lo_fav = (facts.is_long
833 ? (current_bar_.low - facts.entry_price)
834 : (facts.entry_price - current_bar_.low))
835 * facts.closed_qty;
836 owned.favorable = std::max(owned.favorable, lo_fav);
837 owned.adverse = std::max(owned.adverse, -lo_fav);
838 }
839 return owned;
840}
841
843 guard_native_mutation("configure_pine_strategy");
844 config_ = config;
845 adapter_.set_configuration(config_);
846 scheduler_.set_source_series_active(config_.src_series_active);
848}
849
851 guard_native_mutation("set_strategy_override");
852 override_ = overrides;
853 config_ = apply_overrides(config_, override_);
854 adapter_.set_configuration(config_);
855 scheduler_.set_source_series_active(config_.src_series_active);
857}
858
859void source::PineStrategyHost::set_syminfo_session(const std::string& session) {
860 if (stream_warmup_mode_) {
861 (void)session;
862 return;
863 }
864 BacktestEngine::set_syminfo_session(session);
865}
866
868 adapter_.set_risk_direction(direction);
869}
870
872 adapter_.set_risk_max_cons_loss_days(value);
873}
874
876 adapter_.set_risk_max_drawdown(value, percent);
877}
878
880 adapter_.set_risk_max_intraday_loss(value, percent);
881}
882
886
888 adapter_.set_risk_max_position_size(value);
889}
890
892 return source_bar_index_ + scheduler_.bar_index_offset();
893}
894
896 return source_last_bar_index_ + scheduler_.bar_index_offset();
897}
898
900 return scheduler_.is_first_tick();
901}
902
904 return scheduler_.is_last_tick();
905}
906
908 NativeDecisionContext context;
909 if (const auto point = current_execution_point()) {
910 context = point->decision;
911 } else {
912 context.coordinate.interval_index = bar_index_;
913 context.sub_bar_open_ms = current_bar_.timestamp;
914 context.script_bar_open_ms = current_bar_.timestamp;
915 }
916 const BarTime time = fixture_chart_time(context.sub_bar_open_ms);
917 return {context.sub_bar_open_ms,
918 syminfo_.session.empty() ? "24x7" : syminfo_.session,
919 syminfo_.timezone.empty() ? "UTC" : syminfo_.timezone,
920 time.dayofmonth, time.month};
921}
922
924 NativeDecisionContext context;
925 if (const auto point = current_execution_point()) {
926 context = point->decision;
927 } else {
928 context.coordinate.interval_index = bar_index_;
929 context.sub_bar_open_ms = current_bar_.timestamp;
930 context.script_bar_open_ms = current_bar_.timestamp;
931 }
932 return adapter_.cap_calculation(context);
933}
934
939
940source::PineStrategyHost::BarTime source::PineStrategyHost::fixture_chart_time(
941 std::int64_t timestamp_ms) const {
942 const std::time_t seconds = static_cast<std::time_t>(timestamp_ms / 1000);
943 std::tm tm{};
944 const auto utc = [&]() {
945 return ::gmtime_r(&seconds, &tm) != nullptr;
946 };
947 if (chart_timezone_.empty() || chart_timezone_ == "UTC"
948 || chart_timezone_ == "Etc/UTC") {
949 (void)utc();
950 } else {
951 try {
952 tz_util::ScopedTimezone guard(chart_timezone_);
953 if (::localtime_r(&seconds, &tm) == nullptr) (void)utc();
954 } catch (...) {
955 (void)utc();
956 }
957 }
958 BarTime result;
959 result.year = tm.tm_year + 1900;
960 result.month = tm.tm_mon + 1;
961 result.dayofmonth = tm.tm_mday;
962 result.hour = tm.tm_hour;
963 result.minute = tm.tm_min;
964 result.second = tm.tm_sec;
965 result.dayofweek = tm.tm_wday + 1;
966 result.weekofyear = (tm.tm_yday + 7 - ((tm.tm_wday + 6) % 7)) / 7;
967 return result;
968}
969
971 std::uint64_t count = 0;
972 for (const auto& event : native_events(0)) {
973 if (!event.command
974 || !std::holds_alternative<native_order::ExecutionAppliedEvent>(*event.command)) {
975 continue;
976 }
977 ++count;
978 }
979 return count;
980}
981
983 return scheduler_.history_advances_new_bar();
984}
985
986
988 return scheduler_.previous_chart_close();
989}
990
992 return adapter_.pending_intent_view().last_bar_dual_entry_path();
993}
994
996 return scheduler_.script_position_view(bar_index_, position_side_, position_qty_);
997}
998
1000 scheduler_.freeze_script_position_view(
1001 bar_index_, position_side_, position_qty_, pyramid_entries_);
1002}
1003
1005 scheduler_.clear_script_position_view();
1006}
1007
1008const Series<double>& source::PineStrategyHost::source_series(const std::string& key) const {
1009 return scheduler_.source_series(key);
1010}
1011
1013 const std::string& key, const Series<double>& fallback) const {
1014 const auto found = inputs_.find(key);
1015 if (found == inputs_.end() || found->second.empty()) return fallback;
1016 try {
1017 return scheduler_.source_series(found->second);
1018 } catch (const std::invalid_argument&) {
1019 return fallback;
1020 }
1021}
1022
1024 return physical_position().signed_units;
1025}
1026
1030
1034
1036 return adapter_.admission_journal;
1037}
1038
1039std::vector<admission::Field> source::PineStrategyHost::market_admission_fields() const {
1040 std::vector<admission::Field> fields;
1041 adapter_.admission_journal.reflect("journal", [&](const admission::Field& field) {
1042 fields.push_back(field);
1043 });
1044 return fields;
1045}
1046
1048 int index, double fill_price, double* qty, int* close_only, int* partition) const {
1049 return pending_intent_view().probe_fill_qty(index, fill_price, qty, close_only, partition);
1050}
1051
1053 return pending_intent_view().level_resolved(index);
1054}
1055
1057 int index, double* stop, double* limit, double* trail_activation) const {
1058 return pending_intent_view().effective_levels(index, stop, limit, trail_activation);
1059}
1060
1062 return adapter_.pending_intent_view();
1063}
1064
1066 native_order::RequestHandle handle) const noexcept {
1067 return adapter_.short_seed_collision_role_v1(std::move(handle));
1068}
1069
1071 adapter_.enable_intraday_cap();
1072}
1073
1075 adapter_.attach_execution_adapter();
1076 // Generated constructors attach the source execution bridge before their
1077 // risk statements and metadata arrive. The intraday-cap configuration is
1078 // part of that same source-policy attachment; leaving it detached makes a
1079 // later max_intraday_filled_orders declaration silently inert.
1080 adapter_.enable_intraday_cap();
1081}
1082
1084 const std::string& key, double value) {
1085 BacktestEngine::set_syminfo_metadata(key, value);
1086 if (key == "bar_index_offset") {
1087 scheduler_.set_bar_index_offset(std::isfinite(value)
1088 ? static_cast<int>(std::llround(value)) : 0);
1089 }
1090 if (key == "security_range_start_na_warmup") {
1091 if (std::isfinite(value) && value > 0.0) {
1093 security_range_start_ms_ = static_cast<int64_t>(std::llround(value));
1094 } else {
1097 }
1098 }
1099 if (key == "chart_ema_na_warmup")
1100 chart_ema_na_warmup_ = std::isfinite(value) && value > 0.0;
1101 if (key == "historical_security_lookahead_projection")
1102 historical_security_lookahead_projection_ = std::isfinite(value) && value > 0.0;
1103 if (key == "margin_long" && config_.margin_long == 100.0)
1104 config_.margin_long = (std::isfinite(value) && value > 0.0) ? value : 100.0;
1105 if (key == "margin_short" && config_.margin_short == 100.0)
1106 config_.margin_short = (std::isfinite(value) && value > 0.0) ? value : 100.0;
1107 adapter_.set_configuration(config_);
1108 adapter_.priority.metadata(key, value);
1109 adapter_.cap.metadata(key, value);
1110}
1111
1113 return pending_intent_view().last_bar_dual_entry_path();
1114}
1115
1119
1121 int index, pf_pending_order_v1_t* out) const {
1122 return pending_intent_view().copy_v1(index, out);
1123}
1124
1126 int index, double fill_price, double* qty, int* close_only, int* partition) const {
1127 return pending_intent_view().probe_fill_qty(index, fill_price, qty, close_only, partition);
1128}
1129
1131 return pending_intent_view().level_resolved(index);
1132}
1133
1135 int index, double* stop, double* limit, double* trail_activation) const {
1136 return pending_intent_view().effective_levels(index, stop, limit, trail_activation);
1137}
1138
1140 return adapter_.pending_intent_view().trail_best_price();
1141}
1142
1143void source::PineStrategyHost::adapter_label_bracket_trades(
1144 const native_order::ExecutionAppliedEvent& event, bool from_bracket) {
1145 // ab9714be pine_fills.cpp:6232-6252: every trade row emitted by a real
1146 // strategy.exit leg carries the bracket cause; strategy.close and
1147 // close_all requests remain script closes.
1148 for (std::size_t offset = 0; offset < event.closed_trade_count; ++offset) {
1149 const std::size_t index = event.first_trade_index + offset;
1150 if (index >= trades_.size()) continue;
1151 auto& trade = trades_[index];
1152 trade.exit_from_bracket = from_bracket;
1153 }
1154}
1155
1156bool source::PineStrategyHost::adapter_has_open_entry_id(
1157 const std::string& id) const {
1158 return std::any_of(pyramid_entries_.begin(), pyramid_entries_.end(),
1159 [&](const PyramidEntry& row) { return row.entry_id == id && row.qty > 0.0; });
1160}
1161
1162const std::vector<source::PineStrategyHost::FixtureIntentRow>&
1165 source_pending_view_cache_.reserve(adapter_.pending_same_bar_commands_.size()
1166 + adapter_.pending_entries_.size() + adapter_.pending_bracket_legs_.size()
1167 + adapter_.pending_coof_requests_.size()
1168 + adapter_.delayed_market_orders_.size()
1169 + adapter_.source_shadow_pending_.size() + adapter_.live_handles_.size());
1170 const auto append = [&](const PlacementSnapshot& snapshot, const std::string& label) {
1171 FixtureIntentKind type = FixtureIntentKind::MARKET;
1172 switch (snapshot.family) {
1180 type = FixtureIntentKind::EXIT;
1181 break;
1183 type = FixtureIntentKind::RAW_ORDER;
1184 break;
1186 break;
1187 }
1188 FixtureIntentRow row;
1189 row.id = snapshot.family == PineOrderFamily::Close
1190 ? "__close__" + snapshot.source_id
1191 : (snapshot.frozen_market_targeted_close ? label : snapshot.source_id);
1192 row.type = type;
1193 const bool default_stop = snapshot.family == PineOrderFamily::Entry
1194 && !std::isfinite(snapshot.exit_levels.limit)
1195 && std::isfinite(snapshot.exit_levels.stop)
1196 && std::isnan(snapshot.requested_qty)
1197 && config_.default_qty_type == static_cast<int>(QtyType::PERCENT_OF_EQUITY)
1198 && config_.default_qty_value <= 100.0;
1199 const double absent = std::numeric_limits<double>::quiet_NaN();
1200 row.default_stop_placement_qty = default_stop ? snapshot.sizing.frozen_units : absent;
1201 row.default_stop_sizing_price = snapshot.sizing.price;
1204 row.from_entry = snapshot.from_entry;
1205 row.is_long = snapshot.is_long;
1206 row.qty = snapshot.family == PineOrderFamily::Close
1207 ? snapshot.requested_qty
1208 : (std::isfinite(snapshot.projection_remaining_qty)
1209 ? snapshot.projection_remaining_qty : snapshot.requested_qty);
1210 row.qty_percent = snapshot.qty_percent;
1211 row.created_bar = snapshot.projection_created_bar;
1212 row.created_seq = static_cast<std::int64_t>(snapshot.source_sequence);
1213 row.incarnation = snapshot.source_sequence;
1216 row.paired_flat_market_transaction_qty = std::numeric_limits<double>::quiet_NaN();
1217 row.frozen_default_qty = default_stop ? absent : snapshot.sizing.frozen_units;
1218 row.default_stop_placement_equity = default_stop
1219 ? snapshot.projection_default_stop_equity : absent;
1220 row.default_stop_placement_signal_close = default_stop
1221 ? snapshot.projection_default_stop_signal_close : absent;
1223 row.market_admission = snapshot.market_admission;
1224 if (!row.market_admission.observation()
1225 && snapshot.family == PineOrderFamily::Entry
1226 && snapshot.frozen_market_instruction
1227 && std::isfinite(snapshot.requested_qty)) {
1228 auto observation = std::make_shared<admission::CommandObservation>();
1229 observation->command = snapshot.command_ordinal;
1230 observation->kind = admission::CommandKind::Entry;
1231 observation->birth = snapshot.birth.cause() == OrderBirthCause::Unattributed
1232 ? OrderBirth::chart_evaluation(source_bar_index_, current_bar_.timestamp)
1233 : snapshot.birth;
1234 observation->id = snapshot.source_id;
1235 observation->requested_quantity = snapshot.requested_qty;
1236 observation->quantity_type = snapshot.qty_type;
1237 observation->buy = snapshot.is_long;
1238 observation->prices = {snapshot.exit_levels.limit, snapshot.exit_levels.stop};
1239 observation->oca_name = snapshot.oca_name;
1240 observation->oca_type = snapshot.oca_type;
1241 auto& configuration = observation->configuration;
1242 configuration.process_on_close = config_.process_orders_on_close;
1243 configuration.calc_on_fills = config_.calc_on_order_fills;
1244 configuration.slippage = config_.slippage;
1245 configuration.pyramiding = config_.pyramiding;
1246 configuration.default_quantity_type = config_.default_qty_type;
1247 configuration.default_quantity_value = config_.default_qty_value;
1248 configuration.long_margin = config_.margin_long;
1249 configuration.short_margin = config_.margin_short;
1250 configuration.commission_value = config_.commission_value;
1251 configuration.commission_type = config_.commission_type;
1252 configuration.pointvalue = staged_configuration().syminfo.pointvalue;
1253 configuration.fx = snapshot.sizing.fx;
1254 configuration.quantity_step = staged_configuration().quantity_grid
1255 ? *staged_configuration().quantity_grid : 0.0;
1256 configuration.mintick = staged_configuration().syminfo.mintick;
1257 observation->bar = source_bar_index_;
1258 observation->placement_side = static_cast<int>(PositionSide::FLAT);
1259 observation->placement_cycle = snapshot.placement_cycle;
1260 observation->held_quantity = 0.0;
1261 observation->held_entries = 0;
1262 observation->realized_equity = snapshot.sizing.equity;
1263 observation->placement_equity = snapshot.sizing.equity;
1264 observation->signal_close = snapshot.sizing.price;
1265 observation->quantized_fixed_quantity =
1266 snapshot.frozen_market_own_units;
1267 observation->original_sizing = admission::SizingObservation{
1268 snapshot.requested_qty, snapshot.sizing.equity,
1269 snapshot.sizing.price, snapshot.sizing.mark, snapshot.sizing.fx};
1270 row.market_admission.bind(std::move(observation));
1271 }
1272 source_pending_view_cache_.push_back(std::move(row));
1273 };
1274 for (const auto& command : adapter_.pending_same_bar_commands_) {
1275 // ab9714be strategy.close under process_orders_on_close is held in the
1276 // same-bar close accumulator until the callback returns; the legacy
1277 // pending_orders_ observer therefore sees the two entry commands but
1278 // not that staged close during the source body.
1279 if (config_.process_orders_on_close
1280 && command.snapshot.family == PineOrderFamily::Close
1281 && !command.snapshot.birth.at_terminal_fill()) {
1282 continue;
1283 }
1284 append(command.snapshot, command.request.label);
1285 }
1286 for (const auto& pending : adapter_.pending_entries_)
1287 append(pending.snapshot, pending.request.label);
1288 for (const auto& delayed : adapter_.delayed_market_orders_)
1289 append(delayed.snapshot, delayed.request.label);
1290 for (const auto& leg : adapter_.pending_bracket_legs_)
1291 append(leg.snapshot, leg.request.label);
1292 for (const auto& pending : adapter_.pending_coof_requests_)
1293 append(pending.snapshot, pending.request.label);
1294 for (const auto& shadow : adapter_.source_shadow_pending_)
1295 append(shadow.snapshot, shadow.label);
1296 for (const auto& handle : adapter_.live_handles_) {
1297 const auto found = adapter_.placement_.find(handle.incarnation);
1298 if (found == adapter_.placement_.end()) continue;
1299 if (config_.process_orders_on_close
1300 && found->second.family == PineOrderFamily::Close
1301 && found->second.projection_created_bar == source_bar_index_
1302 && !found->second.birth.at_terminal_fill()) {
1303 continue;
1304 }
1305 append(found->second, found->second.source_id);
1306 }
1308}
1309
1312
1313void source::PineStrategyHost::project_short_seed_report_rows(
1315 const ShortSeedPlan plan = adapter_.short_seed_;
1316 if (!plan.report_swap_pending || event.closed_trade_count == 0
1317 || event.handle() == plan.final_short) {
1318 return;
1319 }
1320 std::optional<PlacementSnapshot> placement_snapshot;
1321 if (const auto placement = adapter_.placement_.find(event.handle().incarnation);
1322 placement != adapter_.placement_.end()) {
1323 placement_snapshot = placement->second;
1324 }
1325 if (!placement_snapshot || placement_snapshot->family != PineOrderFamily::Close
1326 || (placement_snapshot->from_entry != plan.seed_id
1327 && placement_snapshot->source_id != plan.seed_id)) {
1328 return;
1329 }
1330 for (auto& trade : trades_) {
1331 if (trade.entry_incarnation == plan.materialize_long.incarnation
1332 && trade.entry_id == plan.materialize_label) {
1333 trade.entry_incarnation = plan.final_short.incarnation;
1334 }
1335 }
1336 const std::size_t begin = event.first_trade_index;
1337 const std::size_t end = begin + event.closed_trade_count;
1338 for (std::size_t index = begin; index < end && index < trades_.size(); ++index) {
1339 if (trades_[index].entry_incarnation == plan.final_short.incarnation
1340 && trades_[index].entry_id == plan.final_short_id) {
1341 trades_[index].entry_incarnation = plan.materialize_long.incarnation;
1342 }
1343 }
1344 adapter_.short_seed_.report_swap_pending = false;
1345}
1346
1347void source::PineStrategyHost::scheduler_prepare_script_run(
1348 const std::vector<Bar>& bars, bool static_eligible,
1349 int expected_script_bars, bool script_bar_geometry) {
1350 if (const auto state = native_state(); state.spec) {
1351 input_tf_ = state.spec->timeframe_undetected ? "" : state.spec->input_tf;
1352 script_tf_ = state.spec->timeframe_undetected ? "" : state.spec->script_tf;
1353 script_tf_seconds_ = tf_to_seconds(script_tf_);
1354 }
1355 prepare_script_run(bars.empty() ? nullptr : bars.data(), static_cast<int>(bars.size()),
1356 static_eligible);
1357 last_bar_index_ = expected_script_bars - 1;
1358 last_bar_time_ = bars.empty() ? 0 : bars.back().timestamp;
1359 apply_realtime_tail_horizon(
1360 bars.empty() ? nullptr : bars.data(), static_cast<int>(bars.size()),
1361 script_bar_geometry);
1362 source_last_bar_index_ = last_bar_index_;
1363}
1364
1365// Live-runtime tail (spec §3.1): the horizon freeze of last_bar_index_ /
1366// last_bar_time_. Moved verbatim from src/engine_run.cpp (R5 lane N14); the
1367// geometry rules are documented on the declaration.
1369 bool script_bar_geometry) {
1370 if (!realtime_tail_ || realtime_tail_horizon_bars_ <= 0 || n <= 0 || bars == nullptr) return;
1371 const int horizon = realtime_tail_horizon_bars_;
1372 last_bar_index_ = horizon - 1;
1373 const int64_t script_tf_ms =
1374 static_cast<int64_t>(script_tf_seconds_ > 0 ? script_tf_seconds_ : 0) * 1000;
1375 if (script_bar_geometry) {
1376 if (horizon <= n) {
1377 last_bar_time_ = bars[horizon - 1].timestamp;
1378 } else {
1379 last_bar_time_ = bars[n - 1].timestamp
1380 + static_cast<int64_t>(horizon - n) * script_tf_ms;
1381 }
1382 } else {
1383 last_bar_time_ = bars[0].timestamp
1384 + static_cast<int64_t>(horizon - 1) * script_tf_ms;
1385 }
1386}
1387
1388void source::PineStrategyHost::scheduler_configure_security_evaluators() {
1389 configure_security_evaluators();
1390 prune_pine_security_states();
1391}
1392
1393bool source::PineStrategyHost::scheduler_uses_aux_security_feed() const noexcept {
1394#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
1395 return aux_security_feed_enabled();
1396#else
1397 return false;
1398#endif
1399}
1400
1401void source::PineStrategyHost::scheduler_prepare_security_sequence(
1402 const std::vector<Bar>& bars) {
1403 security_input_tf_ = input_tf_;
1404#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
1405 if (aux_security_feed_enabled()) security_input_tf_ = aux_security_input_tf_;
1406#endif
1407 validate_security_timeframes(security_input_tf_);
1408 security_first_chart_bar_ms_ = bars.empty() ? 0 : bars.front().timestamp;
1409 if (declare_security_sites_to_kernel()) {
1410 // The kernel registers the declared sites after this callback returns
1411 // and prepares their feeds itself (begin_timeframe_subscriptions).
1412 // The chart's day partition is the chart's, not a site's, and stays.
1413 security_next_input_ms_ = 0;
1414 security_calling_close_ms_ = 0;
1415 clear_historical_security_lookahead_projections();
1416 prepare_chart_day_partition(
1417 bars.empty() ? nullptr : bars.data(), static_cast<int>(bars.size()));
1418 return;
1419 }
1420 init_security_eval_states_for_run(security_input_tf_);
1421 prepare_native_security_feeds(
1422 bars.empty() ? nullptr : bars.data(), static_cast<int>(bars.size()));
1423#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
1424 if (aux_security_feed_enabled()) {
1425 prepare_aux_security_chart_ranges(
1426 bars.empty() ? nullptr : bars.data(), static_cast<int>(bars.size()), script_tf_);
1427 }
1428#endif
1429 prepare_historical_security_lookahead_projections(
1430 bars.empty() ? nullptr : bars.data(), static_cast<int>(bars.size()), input_tf_);
1431 prepare_chart_day_partition(
1432 bars.empty() ? nullptr : bars.data(), static_cast<int>(bars.size()));
1433}
1434
1435bool source::PineStrategyHost::security_sites_kernel_routed() const noexcept {
1436 const auto view = native_state();
1437 return view.spec != nullptr && !view.spec->subscriptions.empty();
1438}
1439
1440bool source::PineStrategyHost::declare_security_sites_to_kernel() {
1441 if (security_eval_states_.empty()) return false;
1442 // Run shapes with a Pine-only rule around the step: a stream feeds its
1443 // sites from realtime prints (the kernel takes confirmed bars only); an
1444 // aggregated chart or the bar magnifier can hold an input back until the
1445 // calling bar's callback has run (PineScheduler::input's deferrals); the
1446 // auxiliary slice is fed per chart bar; the KI-55 range-start cut, the
1447 // historical lookahead projection and the OTC-daily pins veto or
1448 // re-shape inputs before the aggregator sees them.
1449 if (stream_warmup_mode_ || scheduler_.bar_magnifier_enabled()) return false;
1450 if (security_range_start_na_warmup_ || historical_security_lookahead_projection_)
1451 return false;
1452#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
1453 if (aux_security_feed_enabled()) return false;
1454#endif
1455 if (input_tf_.empty() || input_tf_ != script_tf_ || security_input_tf_ != input_tf_)
1456 return false;
1457 const auto view = native_state();
1458 if (view.spec == nullptr || view.spec->timeframe_undetected
1459 || view.spec->input_tf != input_tf_) {
1460 return false;
1461 }
1462 const bool otc_daily_pins = (syminfo_.type == "forex" || syminfo_.type == "cfd")
1463 && native_security_feeds_.empty() && script_tf_seconds_ > 0
1464 && script_tf_seconds_ < 86400;
1465 std::vector<NativeTimeframeSubscription> declared;
1466 declared.reserve(security_eval_states_.size());
1467 for (std::size_t i = 0; i < security_eval_states_.size(); ++i) {
1468 const SecurityEvalState& state = security_eval_states_[i];
1469 // The kernel registers sec_id = index, which is what generated code
1470 // dispatches on.
1471 if (state.sec_id != static_cast<int>(i) || state.tf.empty()) return false;
1472 const PineSecurityEvalState& pine = pine_security_state(state.sec_id);
1473 // Site rules inside the step: the lookahead_on peeks and merge latch
1474 // (and the publication gate they arm), ticker.heikinashi, both
1475 // lower-timeframe paths, the auxiliary-slice completions.
1476 if (pine.lookahead_on || pine.heikinashi || pine.lower_tf_requested
1477 || pine.lower_tf_emulation || pine.lower_tf_use_input
1478 || pine.lower_tf_array_requested || pine.publish_gate_tf_seconds > 0
1479 || pine.calling_close_completes_partial || pine.calling_open_latches_first) {
1480 return false;
1481 }
1482 if (otc_daily_pins && (state.tf == "D" || state.tf == "1D")) return false;
1483 NativeTimeframeSubscription subscription;
1484 subscription.tf = state.tf;
1485 subscription.gaps = pine.gaps_on;
1486 declared.push_back(std::move(subscription));
1487 }
1488 if (!declare_timeframe_subscriptions(std::move(declared))) return false;
1489 // The kernel registers these very sites, sec_id by index, after this
1490 // callback returns; the per-site table keeps their Pine semantics.
1491 security_eval_states_.clear();
1492 return true;
1493}
1494
1495bool source::PineStrategyHost::scheduler_feed_security_input(
1496 const Bar& bar, std::int64_t next_input_ms, bool calling_bar_complete,
1497 bool defer_boundary_gate) {
1498 security_next_input_ms_ = next_input_ms;
1499 security_calling_close_ms_ = 0;
1500 bool deferred = false;
1501 for (auto& state : security_eval_states_) {
1502 if (defer_boundary_gate
1503 && pine_security_state(state.sec_id).publish_gate_tf_seconds > 0) {
1504 deferred = true;
1505 continue;
1506 }
1507 pine_feed_security_eval_state(state, bar, calling_bar_complete);
1508 }
1509 return deferred;
1510}
1511
1512void source::PineStrategyHost::scheduler_publish_security_boundary() {
1513 for (auto& state : security_eval_states_) {
1514 if (pine_security_state(state.sec_id).publish_gate_tf_seconds > 0)
1515 publish_security_eval_state_at_calling_boundary(state);
1516 }
1517}
1518
1519void source::PineStrategyHost::scheduler_feed_deferred_security_input(
1520 const Bar& bar, std::int64_t next_input_ms) {
1521 security_next_input_ms_ = next_input_ms;
1522 security_calling_close_ms_ = 0;
1523 for (auto& state : security_eval_states_) {
1524 if (pine_security_state(state.sec_id).publish_gate_tf_seconds > 0)
1525 pine_feed_security_eval_state(state, bar, false);
1526 }
1527}
1528
1529void source::PineStrategyHost::scheduler_feed_aux_security(int chart_index) {
1530#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
1531 if (aux_security_feed_enabled() && scheduler_coof_enabled()) {
1532 // ab9714be pine_scheduler.cpp:1705-1708 feeds the auxiliary slice
1533 // before dispatch_bar, whose COOF bar takes its script checkpoint
1534 // only afterwards (pine_scheduler.cpp:467). The native scheduler
1535 // checkpoints at the script-bar open and restores that checkpoint
1536 // after this feed, which would discard the fed request.security
1537 // values every bar. Rebase the checkpoint on the open state plus
1538 // this feed so the restore keeps them.
1539 restore_script_state();
1540 feed_aux_security_for_chart_bar(chart_index);
1541 snapshot_script_state();
1542 return;
1543 }
1544 if (aux_security_feed_enabled()) feed_aux_security_for_chart_bar(chart_index);
1545#else
1546 (void)chart_index;
1547#endif
1548}
1549
1550void source::PineStrategyHost::scheduler_feed_deferred_aux_security(int chart_index) {
1551#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
1552 if (aux_security_feed_enabled()) feed_deferred_aux_security_for_chart_bar(chart_index);
1553#else
1554 (void)chart_index;
1555#endif
1556}
1557
1558void source::PineStrategyHost::scheduler_finish_security_sequence() {
1559 clear_historical_security_lookahead_projections();
1560#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
1561 clear_aux_security_chart_ranges();
1562#endif
1563}
1564
1565static void sort_same_bar_exit_trades(std::vector<Trade>&, source::PineExecutionAdapter&);
1566
1567void source::PineStrategyHost::scheduler_record_range_end(const Bar& terminal_bar) {
1568 range_end_trades_.clear();
1569 if (stream_warmup_mode_ || realtime_tail_
1570 || position_side_ == PositionSide::FLAT || equity_curve_.empty()
1571 || !std::isfinite(terminal_bar.close)) return;
1572 const Bar saved = current_bar_;
1573 current_bar_ = terminal_bar;
1574 const double fill_price = bar_fill_price(current_bar_.close);
1575 const auto saved_timestamp = current_bar_.timestamp;
1576 current_bar_.timestamp = equity_curve_.back().time_ms;
1577 // R5 audit lane Q6 (duplicate D5): the row loop itself is the kernel's
1578 // generic producer, called here at TradingView's own mark instead of
1579 // being restated. What stays below is the part that is report SHAPE and
1580 // not a mark-to-market row — the reason the kernel's run-end producer is
1581 // gated out of every Pine run (pine_adapter.cpp project()).
1582 excursion_range_end_projection_ = true;
1583 const double range_end_pnl = as_native_consumer(execution_consumer())
1584 .append_open_position_report_rows(*this, fill_price, current_bar_.timestamp, bar_index_);
1585 excursion_range_end_projection_ = false;
1586 current_bar_.timestamp = saved_timestamp;
1587 auto& last = equity_curve_.back();
1588 last.open_profit = 0.0;
1589 last.equity = initial_capital_ + net_profit_sum_ + range_end_pnl;
1590 max_equity_ = initial_capital_;
1591 min_equity_ = initial_capital_;
1592 max_drawdown_ = 0.0;
1593 max_runup_ = 0.0;
1594 for (const auto& point : equity_curve_) fold_equity_extreme(point.equity);
1595 sort_same_bar_exit_trades(trades_, adapter_);
1596 current_bar_ = saved;
1597}
1598
1599// ab9714be pine_fills.cpp:664-670: same-bar bracket exit trades sort by script command sequence created_seq
1600static void sort_same_bar_exit_trades(std::vector<Trade>& trades,
1602 if (trades.size() < 2) return;
1603 const std::size_t end = trades.size();
1604 std::size_t start = end - 1;
1605 while (start > 0
1606 && trades[start - 1].exit_time == trades[end - 1].exit_time
1607 && trades[start - 1].entry_time == trades[end - 1].entry_time
1608 && trades[start - 1].entry_id == trades[end - 1].entry_id
1609 && trades[start - 1].exit_from_bracket
1610 && trades[end - 1].exit_from_bracket) {
1611 --start;
1612 }
1613 if (end - start > 1) {
1614 // ab9714be pine_scheduler.cpp:262-283 and pine_fills.cpp:3710-3730:
1615 // fills on the bar OPEN precede intrabar fills chronologically. Sibling
1616 // bracket exits that fill within the same phase tie-break by command
1617 // sequence (test_l10m_corpus_parity.cpp).
1618 std::vector<std::size_t> indices(end - start);
1619 std::iota(indices.begin(), indices.end(), start);
1620 std::stable_sort(indices.begin(), indices.end(),
1621 [&](std::size_t ia, std::size_t ib) {
1622 const bool open_a = adapter.is_open_phase_exit(ia);
1623 const bool open_b = adapter.is_open_phase_exit(ib);
1624 if (open_a != open_b) return open_a;
1625 const auto sa = adapter.command_sequence_for_exit(trades[ia].exit_id, trades[ia].entry_id);
1626 const auto sb = adapter.command_sequence_for_exit(trades[ib].exit_id, trades[ib].entry_id);
1627 return sa < sb;
1628 });
1629 std::vector<Trade> sorted;
1630 sorted.reserve(end - start);
1631 for (std::size_t idx : indices) sorted.push_back(std::move(trades[idx]));
1632 for (std::size_t i = 0; i < sorted.size(); ++i) trades[start + i] = std::move(sorted[i]);
1633 // The adapter's exit phases are keyed by trade index: move them with
1634 // the trades so the next call reads each trade's own phase.
1635 adapter.permute_exit_phases(start, indices);
1636 }
1637}
1638
1639void source::PineStrategyHost::scheduler_update_session_state(
1640 const Bar& bar, std::optional<std::int64_t> next_script_open_ms) {
1641 const bool in_session = chart_bar_ismarket(bar.timestamp);
1642 bool next_in_session = false;
1643 if (in_session && next_script_open_ms) {
1644 next_in_session = chart_bar_ismarket(*next_script_open_ms);
1645 } else if (in_session && realtime_tail_ && script_tf_seconds_ > 0
1646 && bar.timestamp <= std::numeric_limits<std::int64_t>::max()
1647 - static_cast<std::int64_t>(script_tf_seconds_) * 1000) {
1648 next_in_session = chart_bar_ismarket(
1649 bar.timestamp + static_cast<std::int64_t>(script_tf_seconds_) * 1000);
1650 } else if (in_session && realtime_tail_) {
1651 next_in_session = true;
1652 }
1653 session_ismarket_ = in_session;
1654 if (tf_is_daily_or_higher(script_tf_)) {
1655 session_isfirstbar_ = in_session;
1656 session_islastbar_ = in_session;
1657 } else {
1658 session_isfirstbar_ = in_session && !prev_in_session_;
1659 session_islastbar_ = in_session && !next_in_session;
1660 }
1661 prev_in_session_ = in_session;
1662}
1663
1664void source::PineStrategyHost::scheduler_publish_source_bar(
1665 const Bar& bar, bool, bool advance_source_index) {
1666 current_bar_ = bar;
1667 const bool temporary_index = !advance_source_index
1668 && (!scheduler_.current_script_bar()
1669 || scheduler_.current_script_bar()->timestamp != bar.timestamp);
1670 const int previous_bar_index = bar_index_;
1671 const bool previous_barstate_islast = barstate_islast_;
1672 if (advance_source_index || temporary_index) ++source_bar_index_;
1673 ++source_callback_count_;
1674 bar_index_ = source_bar_index_;
1675 const auto lifecycle = native_state();
1676 if (lifecycle.kind == NativeLifecycleKind::Running
1677 && lifecycle.phase == NativeRunPhase::Warmup) {
1678 barstate_islast_ = false;
1679 } else if (lifecycle.kind == NativeLifecycleKind::Running
1680 && lifecycle.phase == NativeRunPhase::Realtime) {
1681 barstate_islast_ = true;
1682 } else {
1683 barstate_islast_ = source_bar_index_ == source_last_bar_index_;
1684 }
1685 NativeDayPartitionScope chart_day_partition(
1686 chart_day_partition_.empty() ? nullptr : &chart_day_partition_);
1687 // A named-entry cancellation token has source-evaluation scope. Clear a
1688 // prior callback before publishing receipts and entering this body.
1689 adapter_.begin_source_evaluation();
1690 // Publish terminal and group-adjustment receipts before the source body
1691 // reads its public pending projection at this decision boundary.
1692 sort_same_bar_exit_trades(trades_, adapter_);
1693 adapter_.observe_terminal_receipts();
1694 // ab9714be src/source/pine_scheduler.cpp:242,258: under
1695 // process_orders_on_close the orders that were already resting fill at step 1
1696 // (process_pending_orders(before_pooc_script=true)) BEFORE the strategy body
1697 // runs at step 3, so a bracket leg this route parked while its parent entry
1698 // was pending settles on the touch bar ahead of this bar's source
1699 // evaluation. Draining it here keeps the body's position state, and the
1700 // levels it re-prices, on the same side of the fill as the owner; a leg that
1701 // carries a predecessor receipt stays staged for the flush below the body.
1702 adapter_.flush_pending_bracket_legs({}, /*post_calculation=*/false,
1703 /*pre_script_drain=*/true);
1704 struct ChartEmaNaWarmupScope {
1705 bool previous;
1706 explicit ChartEmaNaWarmupScope(bool enabled)
1707 : previous(ta::ema_na_warmup_flag()) {
1708 ta::ema_na_warmup_flag() = enabled;
1709 }
1710 ~ChartEmaNaWarmupScope() { ta::ema_na_warmup_flag() = previous; }
1711 } ema_scope(chart_ema_na_warmup_);
1712 ta::BarContextScope bar_scope(pine_bar_index(), scheduler_.bar_index_offset());
1713 position_entry_count_ = physical_position().signed_units == 0.0
1714 ? 0 : adapter_.source_entry_slot_count();
1715 on_source_bar(bar);
1716 // Handwritten/source-generated callbacks historically read and could
1717 // update the live Pine configuration fields directly. Keep the adapter's
1718 // source policy view synchronized at the callback boundary; the generic
1719 // NativeRunSpec remains immutable for the run.
1720 adapter_.set_configuration(config_);
1721 if (temporary_index) {
1722 --source_bar_index_;
1723 bar_index_ = previous_bar_index;
1724 barstate_islast_ = previous_barstate_islast;
1725 }
1726 adapter_.flush_pending_closes();
1727 adapter_.flush_pending_entries();
1728 adapter_.flush_pending_bracket_legs();
1729 // After every command of this evaluation is in the kernel: a queued
1730 // relative strategy.exit whose parent entry is now a live request becomes
1731 // that parent's anchored bracket child.
1732 adapter_.anchor_relative_exits();
1733 if (advance_source_index) {
1734 scheduler_mark_report_point(bar.timestamp);
1735 prev_bar_timestamp_ = bar.timestamp;
1736 }
1737}
1738
1739void source::PineStrategyHost::scheduler_publish_suppressed_tail(const Bar& bar) {
1740 // ab9714be pine_scheduler.cpp:222-231: the forming probe tail advances
1741 // source history and settles the already-matched broker book, but does
1742 // not invoke generated code or synthesize a range-end close.
1743 current_bar_ = bar;
1744 ++source_bar_index_;
1745 bar_index_ = source_bar_index_;
1746 barstate_islast_ = false;
1747 NativeDayPartitionScope chart_day_partition(
1748 chart_day_partition_.empty() ? nullptr : &chart_day_partition_);
1749 adapter_.begin_source_evaluation();
1750 adapter_.observe_terminal_receipts();
1751 scheduler_mark_report_point(bar.timestamp);
1752 prev_bar_timestamp_ = bar.timestamp;
1753}
1754
1755// The Pine report series has one point per SOURCE slot this host published,
1756// which is not the kernel's per-calculation cadence: a calc_on_order_fills
1757// re-entry marks the slot it opened at the fill, the ordinary close
1758// calculation then marks nothing, and the probe's suppressed tail marks a
1759// slot generated code never calculated. The point also has to land inside
1760// this callback, before scheduler_record_broker_hash() folds the extremes it
1761// just moved and before scheduler_record_range_end() re-marks the curve's
1762// last point. So the cadence stays here and the recording does not: the
1763// kernel owns what a report point is — the extremes fold and the curve
1764// append in engine.hpp — reached through the run spec's report policy.
1765void source::PineStrategyHost::scheduler_mark_report_point(std::int64_t script_bar_ts) {
1766 as_native_consumer(execution_consumer())
1767 .mark_script_report_point(*this, script_bar_ts);
1768}
1769
1770void source::PineStrategyHost::scheduler_record_broker_hash() {
1771 if (!broker_state_hash_recording_) return;
1772 last_script_continuation_hash_ = execution_consumer().continuation_hash();
1773 last_script_continuation_valid_ = true;
1774 broker_state_hashes_.push_back(broker_state_hash());
1775}
1776
1777void source::PineStrategyHost::scheduler_set_session_bar_state(
1778 bool in_session, bool intraday_is_last_bar) {
1779 // ab9714be pine_scheduler.cpp:1661-1675. These generated Pine facts are
1780 // sourced by the scheduler immediately before the source callback; they
1781 // are not generic native-calendar policy.
1782 session_ismarket_ = in_session;
1783 if (tf_is_daily_or_higher(script_tf_)) {
1784 session_isfirstbar_ = in_session;
1785 session_islastbar_ = in_session;
1786 return;
1787 }
1788 session_isfirstbar_ = in_session && !prev_in_session_;
1789 session_islastbar_ = intraday_is_last_bar;
1790}
1791
1792execution::AccountEffectProjection source::PineStrategyHost::adapter_project_flatten(
1793 double price, const std::string& id, const std::string& comment,
1794 std::uint64_t incarnation) const {
1795 return project_native_settlement_v1(
1796 execution::Flatten{}, execution::Fill{price, id, comment, incarnation});
1797}
1798
1800 return adapter_.core_sizes_default_opening(is_long);
1801}
1802
1803} // namespace pineforge
static OrderBirth chart_evaluation(int bar, int64_t timestamp)
OrderBirthCause cause() const
const std::shared_ptr< const CommandObservation > & observation() const
void bind(std::shared_ptr< const CommandObservation > observation)
The public native host: an abstract subclass of BacktestEngine with no PineScript on it.
void permute_exit_phases(std::size_t start, const std::vector< std::size_t > &indices)
static bool source_kernel_liquidation(const native_order::DefinitionRef &) noexcept
compat::pine::Calculation fixture_cap_calculation() const
void set_syminfo_session(const std::string &)
std::optional< NativeMarginDecision > resolve_margin_requirement(const NativeMarginRequirementView &) const final
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 on_native_tick(const Bar &, const NativeTickContext &) final
ClosedLotExcursion closed_lot_excursion(const ClosedLotExcursionFacts &) const 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
const Series< double > & source_series(const std::string &) const
void apply_realtime_tail_horizon(const Bar *bars, int n, bool script_bar_geometry)
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
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
void on_native_input(const Bar &, const NativeInputContext &) final
int short_seed_collision_role_v1(native_order::RequestHandle) const noexcept
void on_native_bar_open(const Bar &, const NativeDecisionContext &) final
MarketAdmissionJournal & market_admission_journal()
NativePrecommitVerdict validate_execution_precommit(const NativePrecommitView &) const final
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
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
void source_stream_entry_comment(const PyramidEntry &, std::string &) const override
double snap_trail_level_to_tick_grid(double price, double mintick)
NativeCalculationReason
Why the kernel is asking the host to calculate.
NativePrecommitVerdict
The host is consulted before generic opening-margin admission.
bool in_session(const SessionCalendar &calendar, int64_t ms)
std::variant< Market, Limit, Stop, StopLimit, Trail > Trigger
NativePathOrder
Generic ordering for a modeled OHLC path.
std::optional< Plan > plan(double signed_position, Reduce request) noexcept
constexpr char kMarginCallLabel[]
constexpr char kIntradayLossComment[]
Bar margin_call_sample_bar(const Bar &bar, double fire_price, bool prefix_sample, bool high_first, double mintick=0.0, int slippage=0)
constexpr char kFillCapCommentPrefix[]
void sample_open_trade_extremes(std::vector< PyramidEntry > &lots, PositionSide side, int bar_index, const Bar &bar)
bool & ema_na_warmup_flag()
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.
double na< double >()
Definition na.hpp:17
static void sort_same_bar_exit_trades(std::vector< Trade > &, source::PineExecutionAdapter &)
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...
bool tf_is_daily_or_higher(const std::string &tf)
True for a daily-or-higher chart timeframe ("D", "1D", "2D", "W", "M"): a bar that covers whole sessi...
Definition timeframe.hpp:90
admission::Journal MarketAdmissionJournal
double close
Definition bar.hpp:7
double low
Definition bar.hpp:7
int64_t timestamp
Definition bar.hpp:8
double high
Definition bar.hpp:7
std::string exit_comment
Definition engine.hpp:163
execution::CloseCause close_cause
Definition engine.hpp:193
std::string exit_id
Definition engine.hpp:164
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.
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< std::int64_t > account_fx_effective_from_ms