PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
magnifier_intrabars.cpp
Go to the documentation of this file.
1// TradingView's bar-magnifier intrabars (R5 lane MAG-INTRABAR): the table
2// that picks the intrabar timeframe for a chart, and the lower-timeframe path
3// the Pine adapter hands the kernel's intrabar path for a magnified run.
5
8
9#include "../native_calendar_memo.hpp"
10
11#include <algorithm>
12#include <cstdint>
13#include <limits>
14#include <optional>
15#include <stdexcept>
16
17namespace pineforge::source {
18
19namespace {
20
21namespace cal = native_calendar;
22
23constexpr std::int64_t kSecondMs = 1000;
24constexpr std::int64_t kMinuteMs = 60 * kSecondMs;
25
26// A fixed timeframe's duration, or 0 for a calendar one.
27std::int64_t fixed_ms(const cal::Timeframe& tf) noexcept {
28 switch (tf.unit()) {
29 case cal::TimeframeUnit::Second: return tf.count() * kSecondMs;
30 case cal::TimeframeUnit::Minute: return tf.count() * kMinuteMs;
31 default: return 0;
32 }
33}
34
35Bar one_price(double price, std::int64_t timestamp) noexcept {
36 Bar bar{};
37 bar.open = bar.high = bar.low = bar.close = price;
38 bar.volume = 0.0;
39 bar.timestamp = timestamp;
40 return bar;
41}
42
43} // namespace
44
45std::string tradingview_intrabar_timeframe(std::string_view chart_tf) {
46 const auto chart = cal::parse_timeframe(chart_tf);
47 if (!chart || !chart->valid()) return {};
48 std::int64_t seconds = 0;
49 switch (chart->unit()) {
50 case cal::TimeframeUnit::Second: seconds = chart->count(); break;
51 case cal::TimeframeUnit::Minute: seconds = std::int64_t{chart->count()} * 60; break;
52 case cal::TimeframeUnit::Day: seconds = std::int64_t{chart->count()} * 86400; break;
53 case cal::TimeframeUnit::Week:
54 case cal::TimeframeUnit::Month:
55 return "1D";
56 }
57 constexpr std::int64_t kMinute = 60;
58 constexpr std::int64_t kDay = 1440 * kMinute;
59 if (seconds < 30) return "1S";
60 if (seconds < kMinute) return "5S";
61 if (seconds < 5 * kMinute) return "10S";
62 if (seconds < 10 * kMinute) return "30S";
63 if (seconds < 15 * kMinute) return "1";
64 if (seconds < 30 * kMinute) return "2";
65 if (seconds < 60 * kMinute) return "5";
66 if (seconds < 240 * kMinute) return "10";
67 if (seconds < kDay) return "30";
68 if (seconds < 3 * kDay) return "60";
69 if (seconds < 7 * kDay) return "240";
70 return "1D";
71}
72
73std::vector<Bar> tradingview_magnifier_bars(const Bar* bars, int n,
74 const std::string& input_tf,
75 const std::string& script_tf,
76 const std::string& session,
77 const std::string& timezone) {
78 std::vector<Bar> unchanged;
79 if (!bars || n <= 0) return unchanged;
80 unchanged.assign(bars, bars + n);
81
82 const auto input = cal::parse_timeframe(input_tf);
83 const auto chart = cal::parse_timeframe(script_tf);
84 const auto intrabar = cal::parse_timeframe(tradingview_intrabar_timeframe(script_tf));
85 if (!input || !chart || !intrabar) return unchanged;
86 // The intrabars are aggregated from the input, so the input has to be a
87 // strictly finer fixed grid that tiles them: an intrabar of the input's
88 // own timeframe is the input path already, and a finer one (a 1- or
89 // 5-minute chart walks 10- or 30-second bars) cannot be built from it.
90 const std::int64_t input_ms = fixed_ms(*input);
91 const std::int64_t intrabar_ms = fixed_ms(*intrabar);
92 if (input_ms <= 0) return unchanged;
93 if (intrabar_ms != 0 && (intrabar_ms <= input_ms || intrabar_ms % input_ms != 0))
94 return unchanged;
95 for (int i = 1; i < n; ++i) {
96 if (bars[i].timestamp <= bars[i - 1].timestamp) return unchanged;
97 }
98 const auto calendar = cal::parse_session(session, timezone);
99 if (!calendar) return unchanged;
100 cal::SessionDayMemo memo;
101
102 // Every input bar's chart bar, and its intrabar with the chart bar that
103 // owns it -- the one holding the intrabar's last traded instant. Both are
104 // the calendar's own intervals, the ones the kernel aggregates with.
105 struct Row { std::int64_t chart_open; std::int64_t slot_open; std::int64_t owner_open; };
106 std::vector<Row> rows(static_cast<std::size_t>(n));
107 std::optional<cal::NativeInterval> chart_at, slot_at, owner_at;
108 for (int i = 0; i < n; ++i) {
109 const std::int64_t t = bars[i].timestamp;
110 if (!chart_at || t < chart_at->open_ms || t >= chart_at->next_period_open_ms) {
111 chart_at = cal::interval_containing(*calendar, *chart, t, memo);
112 if (!chart_at) return unchanged;
113 }
114 if (!slot_at || t < slot_at->open_ms || t >= slot_at->next_period_open_ms) {
115 slot_at = cal::interval_containing(*calendar, *intrabar, t, memo);
116 if (!slot_at || slot_at->last_traded_close_ms <= slot_at->open_ms) return unchanged;
117 owner_at = cal::interval_containing(*calendar, *chart,
118 slot_at->last_traded_close_ms - 1, memo);
119 if (!owner_at) return unchanged;
120 }
121 rows[static_cast<std::size_t>(i)] = {chart_at->open_ms, slot_at->open_ms, owner_at->open_ms};
122 }
123
124 // Consecutive runs of the input: chart bars, and intrabars with their
125 // owner. Both partitions are time-ordered, and an owner never precedes
126 // the chart bar of its intrabar's first input.
127 struct Span { std::int64_t key; std::int64_t owner; int first; int last; };
128 std::vector<Span> charts, slots;
129 for (int i = 0; i < n; ++i) {
130 const auto& row = rows[static_cast<std::size_t>(i)];
131 if (charts.empty() || charts.back().key != row.chart_open)
132 charts.push_back({row.chart_open, row.chart_open, i, i});
133 else
134 charts.back().last = i;
135 if (slots.empty() || slots.back().key != row.slot_open)
136 slots.push_back({row.slot_open, row.owner_open, i, i});
137 else
138 slots.back().last = i;
139 }
140
141 std::vector<Bar> path;
142 path.reserve(slots.size() + 2 * charts.size());
143 std::size_t slot = 0;
144 for (const auto& bar : charts) {
145 // An intrabar owned by a chart bar the input never reaches has no
146 // chart bar to walk in; TradingView has none either.
147 while (slot < slots.size() && slots[slot].owner < bar.key) ++slot;
148 const std::size_t owned_begin = slot;
149 while (slot < slots.size() && slots[slot].owner == bar.key) ++slot;
150 if (owned_begin == slot) continue; // the kernel walks the chart bar's own OHLC
151 const Bar& chart_first = bars[bar.first];
152 const Bar& chart_last = bars[bar.last];
153 std::int64_t floor = chart_first.timestamp;
154 if (slots[owned_begin].first < bar.first) {
155 // The straddler reaches back into the previous chart bar, so its
156 // open is not this bar's: the path opens at the chart bar's own
157 // open first (where an order placed at the last close fills).
158 path.push_back(one_price(chart_first.open, floor));
159 ++floor;
160 }
161 for (std::size_t s = owned_begin; s < slot; ++s) {
162 const Span& span = slots[s];
163 Bar intrabar = bars[span.first];
164 for (int i = span.first + 1; i <= span.last; ++i) {
165 intrabar.high = std::max(intrabar.high, bars[i].high);
166 intrabar.low = std::min(intrabar.low, bars[i].low);
167 intrabar.close = bars[i].close;
168 intrabar.volume += bars[i].volume;
169 }
170 intrabar.timestamp = std::max(bars[span.first].timestamp, floor);
171 floor = intrabar.timestamp + 1;
172 path.push_back(intrabar);
173 }
174 if (slots[slot - 1].last < bar.last) {
175 // The chart bar's last inputs open a later bar's straddler: the
176 // path still closes at this chart bar's own close.
177 path.push_back(one_price(chart_last.close, std::max(chart_last.timestamp, floor)));
178 }
179 }
180 return path;
181}
182
183namespace {
184
185[[noreturn]] void path_failure(const char* message) {
187 {{"option", "intrabar_path"}}, message);
188}
189
190[[noreturn]] void path_invariant(const char* message) {
191 throw coded<std::logic_error>(RunFailureCode::engine_invariant, {}, message);
192}
193
194std::int64_t after_stamp(std::int64_t stamp) {
195 if (stamp == std::numeric_limits<std::int64_t>::max())
196 throw coded<std::overflow_error>(RunFailureCode::run_options_rejected,
197 {{"option", "intrabar_path"}},
198 "magnifier timestamp exhausted");
199 return stamp + 1;
200}
201
202// All durable fields below are derived cursor/storage state. Identity is the
203// count-first fold of the complete logical path, taken eagerly at configure;
204// caches, cursor positions and buffer addresses introduce no broker state.
205class MagnifierPathSource final : public NativeLowerPathSource {
206 const Bar* input_;
207 int n_;
208 std::optional<cal::Timeframe> chart_tf_, slot_tf_;
209 std::optional<cal::SessionCalendar> calendar_;
210 bool fallback_ = true;
211 std::size_t count_ = 0;
212
213 struct Row {
214 std::int64_t chart, slot, owner;
215 int index;
216 };
217 struct Rows {
218 const MagnifierPathSource* source;
219 int at = 0;
220 bool failed = false;
221 cal::SessionDayMemo memo;
222 std::optional<cal::NativeInterval> chart, slot, owner;
223
224 explicit Rows(const MagnifierPathSource* value) : source(value) {}
225 // Calendar memos are disposable caches, not part of cursor position.
226 Rows(const Rows& other)
227 : source(other.source), at(other.at), failed(other.failed),
228 chart(other.chart), slot(other.slot), owner(other.owner) {}
229 bool next(Row& row) {
230 if (at == source->n_) return false;
231 const auto t = source->input_[at].timestamp;
232 if (!chart || t < chart->open_ms || t >= chart->next_period_open_ms) {
233 chart = cal::interval_containing(*source->calendar_, *source->chart_tf_, t, memo);
234 if (!chart) { failed = true; return false; }
235 }
236 if (!slot || t < slot->open_ms || t >= slot->next_period_open_ms) {
237 slot = cal::interval_containing(*source->calendar_, *source->slot_tf_, t, memo);
238 if (!slot || slot->last_traded_close_ms <= slot->open_ms) {
239 failed = true; return false;
240 }
241 owner = cal::interval_containing(*source->calendar_, *source->chart_tf_,
242 slot->last_traded_close_ms - 1, memo);
243 if (!owner) { failed = true; return false; }
244 }
245 row = {chart->open_ms, slot->open_ms, owner->open_ms, at++};
246 return true;
247 }
248 };
249 struct Span {
250 std::int64_t key, owner;
251 int first, last;
252 Bar value;
253 };
254 struct Spans {
255 Rows rows;
256 bool slots;
257 std::optional<Row> pending;
258 Spans(const MagnifierPathSource* source, bool is_slot) : rows(source), slots(is_slot) {}
259 std::optional<Span> next() {
260 Row row{};
261 if (pending) { row = *pending; pending.reset(); }
262 else if (!rows.next(row)) {
263 if (rows.failed) path_failure("magnifier calendar lookup changed after pre-scan");
264 return std::nullopt;
265 }
266 Span out{slots ? row.slot : row.chart, slots ? row.owner : row.chart,
267 row.index, row.index, rows.source->input_[row.index]};
268 while (rows.next(row)) {
269 if ((slots ? row.slot : row.chart) != out.key) {
270 pending = row;
271 break;
272 }
273 out.last = row.index;
274 if (slots) {
275 const Bar& bar = rows.source->input_[row.index];
276 out.value.high = std::max(out.value.high, bar.high);
277 out.value.low = std::min(out.value.low, bar.low);
278 out.value.close = bar.close;
279 out.value.volume += bar.volume;
280 }
281 }
282 if (rows.failed) path_failure("magnifier calendar lookup changed after pre-scan");
283 return out;
284 }
285 };
286 struct Cursor {
287 const MagnifierPathSource* source;
288 Spans charts, slots;
289 std::optional<Span> chart, slot;
290 std::int64_t floor = 0;
291 bool active = false, opening = false, closing = false;
292 explicit Cursor(const MagnifierPathSource* value)
293 : source(value), charts(value, false), slots(value, true), slot(slots.next()) {}
294
295 bool next(Bar& out) {
296 while (!active) {
297 chart = charts.next();
298 if (!chart) return false;
299 while (slot && slot->owner < chart->key) slot = slots.next();
300 // No owned slot: the kernel walks this chart bar's own OHLC.
301 if (!slot || slot->owner != chart->key) continue;
302 floor = source->input_[chart->first].timestamp;
303 opening = slot->first < chart->first;
304 closing = false;
305 active = true;
306 }
307 if (opening) {
308 out = one_price(source->input_[chart->first].open, floor);
309 floor = after_stamp(floor);
310 opening = false;
311 } else if (closing) {
312 const Bar& last = source->input_[chart->last];
313 out = one_price(last.close, std::max(last.timestamp, floor));
314 active = false;
315 } else {
316 out = slot->value;
317 out.timestamp = std::max(out.timestamp, floor);
318 floor = after_stamp(out.timestamp);
319 const int last = slot->last;
320 slot = slots.next();
321 if (!slot || slot->owner != chart->key) {
322 closing = last < chart->last;
323 if (!closing) active = false;
324 }
325 }
326 return true;
327 }
328 };
329
330 bool selected_ = false;
331 std::int64_t begin_ = 0, end_ = 0;
332 NativeBarView view_{nullptr, 0};
333 std::vector<Bar> segment_, scratch_;
334 std::unique_ptr<Cursor> cursor_;
335 std::optional<Bar> next_;
336
337public:
338 MagnifierPathSource(const Bar* input, int n, const std::string& input_tf,
339 const std::string& script_tf, const std::string& session,
340 const std::string& timezone)
341 : input_(input), n_(input && n > 0 ? n : 0) {
342 count_ = static_cast<std::size_t>(n_);
343 if (n_ == 0) return;
344 const auto input_grid = cal::parse_timeframe(input_tf);
345 chart_tf_ = cal::parse_timeframe(script_tf);
346 slot_tf_ = cal::parse_timeframe(tradingview_intrabar_timeframe(script_tf));
347 if (!input_grid || !chart_tf_ || !slot_tf_) return;
348 const auto input_ms = fixed_ms(*input_grid);
349 const auto slot_ms = fixed_ms(*slot_tf_);
350 if (input_ms <= 0 || (slot_ms != 0 && (slot_ms <= input_ms || slot_ms % input_ms != 0)))
351 return;
352 for (int i = 1; i < n_; ++i)
353 if (input_[i].timestamp <= input_[i - 1].timestamp) return;
354 calendar_ = cal::parse_session(session, timezone);
355 if (!calendar_) return;
356 // A failure anywhere means today's ENTIRE unchanged input. Decide
357 // before the count/digest can emit even one aggregated row.
358 Rows rows(this);
359 Row row{};
360 while (rows.next(row)) {}
361 if (rows.failed) return;
362 fallback_ = false;
363 count_ = 0;
364 Cursor cursor(this);
365 Bar bar{};
366 while (cursor.next(bar)) {
367 if (count_ == std::numeric_limits<std::size_t>::max())
368 throw coded<std::overflow_error>(RunFailureCode::run_options_rejected,
369 {{"option", "intrabar_path"}},
370 "magnifier path count exhausted");
371 ++count_;
372 }
373 }
374
375 std::size_t count() const override { return count_; }
376 bool for_each(Visitor visitor, void* context) const override {
377 if (visitor == nullptr) path_invariant("null magnifier visitor");
378 if (fallback_) {
379 for (int i = 0; i < n_; ++i) if (!visitor(context, input_[i])) return false;
380 } else {
381 Cursor cursor(this);
382 Bar bar{};
383 while (cursor.next(bar)) if (!visitor(context, bar)) return false;
384 }
385 return true;
386 }
387
388 NativeBarView segment(std::int64_t begin, std::int64_t end) override {
389 if (end < begin || (selected_ && (begin < begin_ || end < end_)))
390 path_invariant("magnifier window moved backwards");
391 if (selected_ && begin == begin_ && end == end_) return view_;
392 if (fallback_) {
393 // Fallback is the input view itself: no path allocation or copy.
394 const Bar* first = input_;
395 const Bar* last = input_;
396 if (n_ != 0) {
397 const auto less = [](const Bar& bar, std::int64_t t) { return bar.timestamp < t; };
398 first = std::lower_bound(input_, input_ + n_, begin, less);
399 last = std::lower_bound(first, input_ + n_, end, less);
400 }
401 view_ = {first, n_ == 0 ? 0 : static_cast<std::size_t>(last - first)};
402 next_ = n_ == 0 || last == input_ + n_ ? std::nullopt : std::optional<Bar>(*last);
403 } else {
404 // A candidate cursor and second reusable buffer make publication
405 // atomic. Keep overlap rows from the previous window, then append
406 // only new rows; look-ahead is a distinct single-bar buffer.
407 auto candidate = cursor_ ? std::make_unique<Cursor>(*cursor_)
408 : std::make_unique<Cursor>(this);
409 auto next = next_;
410 scratch_.clear();
411 for (const auto& bar : segment_)
412 if (bar.timestamp >= begin) scratch_.push_back(bar);
413 Bar bar{};
414 for (;;) {
415 if (next) { bar = *next; next.reset(); }
416 else if (!candidate->next(bar)) break;
417 if (bar.timestamp >= end) { next = bar; break; }
418 if (bar.timestamp >= begin) scratch_.push_back(bar);
419 }
420 segment_.swap(scratch_);
421 cursor_.swap(candidate);
422 next_ = next;
423 view_ = {segment_.data(), segment_.size()};
424 }
425 begin_ = begin;
426 end_ = end;
427 selected_ = true;
428 return view_;
429 }
430 const Bar* next_after_segment() const noexcept override {
431 return next_ ? &*next_ : nullptr;
432 }
433};
434
435} // namespace
436
437std::unique_ptr<NativeLowerPathSource> make_magnifier_path_source(
438 const Bar* bars, int n, const std::string& input_tf,
439 const std::string& script_tf, const std::string& session,
440 const std::string& timezone) {
441 return std::make_unique<MagnifierPathSource>(bars, n, input_tf, script_tf, session, timezone);
442}
443
444} // namespace pineforge::source
Base with a code: throw coded<std::invalid_argument>(code, {args}, text).
int t(int64_t c)
Definition color.hpp:45
std::unique_ptr< NativeLowerPathSource > make_magnifier_path_source(const Bar *bars, int n, const std::string &input_tf, const std::string &script_tf, const std::string &session, const std::string &timezone)
The same logical path without retaining the input or a whole-run path.
std::vector< Bar > tradingview_magnifier_bars(const Bar *bars, int n, const std::string &input_tf, const std::string &script_tf, const std::string &session, const std::string &timezone)
The lower-timeframe path TradingView's bar magnifier walks for every chart bar of a run whose chart b...
std::string tradingview_intrabar_timeframe(std::string_view chart_tf)
The intrabar timeframe TradingView's bar magnifier walks on a chart of chart_tf, from the table in Tr...
double open
Definition bar.hpp:7
double close
Definition bar.hpp:7
double low
Definition bar.hpp:7
double volume
Definition bar.hpp:7
int64_t timestamp
Definition bar.hpp:8
double high
Definition bar.hpp:7