PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
multi_day_periods.hpp
Go to the documentation of this file.
1#pragma once
2
3// TradingView's N-day request bars ("2D", "3D", ...) as a generated period
4// partition of bounded memory (CR-150 L19k).
5//
6// The rule (lane TAIL-A, lab tv pf-taila-ndgrid-*, tests/fixtures/
7// multi_day_buckets): N consecutive trading dates of the symbol, counted from
8// the first trading date of each calendar year, the year's last bar cut at its
9// end. The trading dates are the session calendar's: every day on a 24x7
10// symbol, every weekday on OANDA's (a holiday included: the 8D bar of EURUSD
11// opens on 2025-12-25, a date its chart has no bar of), the exchange's
12// sessions on a stock, an index or a future (NYSE:F's 8D bars skip Good
13// Friday). The run knows an exchange's sessions from the bars it holds, the
14// chart's and the auxiliary feed's; a date outside them counts when it is a
15// weekday. A period opens at its first trading date's daily bar on a daily
16// chart (NSE's Muhurat session opens its 8D bar at 13:45 IST), at that date's
17// day stamp otherwise, and at the day stamp of the session day holding its
18// local noon when no bar holds the date. The partition runs from the bar
19// holding the first held date through the year after the last, whose dates
20// follow the weekday rule: a stream's bars after the held ones.
21//
22// The bounded form. The periods are never stored. On ordered feeds a
23// generator walks the feeds once, in time order, turning the sessions it
24// meets into period opens one at a time: the days of a year are scanned in
25// order, their held sessions are merged as they come, and only the running
26// count of the year, the position in each feed and the day being scanned are
27// kept. A source answers "which period holds this instant" from the last
28// kMultiDayWindow - 1 opens the generator produced and the one it produced
29// ahead; an instant before those restarts the generator from the first bar,
30// and an instant after them advances it. Whatever the order of the questions
31// the answers are the same, and for the questions a run asks -- the current
32// period, the next input bar's, the previous one's -- they come from the
33// buffer. The memory held is the same for a month of days and for forty years
34// of them; ordered planning (make_multi_day_period_source) walks the feeds
35// twice with the same few words of state, where the table it replaces held
36// a node per held session day while it was built.
37//
38// The feeds are strictly increasing in time (the host refuses others), but
39// their resolved session dates can go back at a clock change, including
40// through the legacy coverage clock. When the bounded merge detects that,
41// planning instead rescans both feeds for each calendar year's held dates.
42// Only one year's date slots are retained, in kUnorderedRescanDays entries;
43// all resolutions are visited, chart first, keeping each date's first stamp.
44// Even a regression larger than the window is exact: the window limits
45// retained dates, not how far back a rescan looks. The source reads the
46// feeds' bars until the run that planned it is over; the aggregator it is
47// installed in is rebuilt at the next run's prepare.
48//
49// Equivalence. The buffered form answers exactly what the stored table did;
50// tests/test_multi_day_periods_bounded.cpp keeps that table's builder as the
51// reference and compares the two bar for bar. The table was built at prepare,
52// before the run's bar loop installs the chart's native day partition, so the
53// session calendar it read was the nominal one; the generator reads the
54// partition-free calendar too (NativeDayPartitionScope), however late in the
55// run it is asked.
56
57#include <pineforge/bar.hpp>
60
61#include <algorithm>
62#include <array>
63#include <cstddef>
64#include <cstdint>
65#include <memory>
66#include <string>
67#include <utility>
68#include <vector>
69
70namespace pineforge::source {
71
72/// The most period opens a multi-day period source holds at once: the recent
73/// ones it answers short questions from, and the next one ahead.
74inline constexpr std::size_t kMultiDayWindow = 16;
75
76/// Bounds the fallback's retained trading-date slots to one calendar year,
77/// including a leap year; N-day counting restarts each year. Clock changes
78/// (also through the legacy coverage clock) normally regress dates by about
79/// a day. No distance bound is assumed: full-feed rescans remain exact even
80/// beyond this window, replacing the slots rather than accumulating years.
81inline constexpr std::size_t kUnorderedRescanDays = 366;
82
83/// What a run holds of the symbol the N-day request reads.
85 int days = 0; ///< N of the request, at least 2
86 const Bar* chart = nullptr; ///< the chart's bars, strictly increasing
87 int n_chart = 0;
88 bool daily_chart = false; ///< a chart bar's own open dates its session day
89 const Bar* aux = nullptr; ///< the auxiliary feed's bars, strictly increasing
90 std::size_t n_aux = 0;
91 std::string tz; ///< the symbol's timezone
92 std::string session; ///< the symbol's session
93 bool exchange_calendar = false; ///< the held sessions are the trading dates
94};
95
97
98// A session day a feed holds: its ordinal (days since 1970-01-01 on the
99// session clock) and the stamp of its bar.
100struct HeldDay {
101 std::int64_t ordinal = 0;
102 std::int64_t value = 0;
103};
104
105// What planning learned of the run; immutable once the source exists.
106struct Params {
107 int days = 0;
108 bool exchange_calendar = false;
109 bool every_day = false;
110 bool daily_chart = false;
111 const Bar* chart = nullptr;
112 std::size_t n_chart = 0;
113 const Bar* aux = nullptr;
114 std::size_t n_aux = 0;
115 std::string tz;
116 std::string session;
118 std::int64_t first = 0; // the first and last session day any feed holds
119 std::int64_t last = 0;
120 std::int64_t first_year = 0; // the year of the first held day
121 std::int64_t last_year = 0; // the year after the last held day's
122};
123
124// The session days one feed's bars fall in, in feed order. An intraday bar
125// trades inside its session day: one inside the day the bar before it
126// resolved is not resolved again (a daily chart resolves every bar).
127class FeedWalk {
128public:
129 FeedWalk(const Params& params, const Bar* bars, std::size_t n, bool own_open)
130 : params_(&params), bars_(bars), n_(bars == nullptr ? 0 : n), own_open_(own_open) {}
131
132 bool next(HeldDay& out) {
133 while (index_ < n_) {
134 const std::int64_t ts = bars_[index_++].timestamp;
135 if (!own_open_ && cycle_ && origin_ <= ts && ts < next_origin_) continue;
136 const auto day = native_calendar::session_day_at(
137 params_->calendar,
138 session_covered_instant_ms(ts, params_->tz, params_->session));
139 cycle_ = day.has_value();
140 if (!day) continue;
141 origin_ = day->origin_ms;
142 next_origin_ = day->next_origin_ms;
143 out.ordinal = day->ordinal;
144 out.value = own_open_
145 ? ts
146 : session_period_open_ms(ts, params_->tz, params_->session, CalendarPeriod::DAY);
147 return true;
148 }
149 return false;
150 }
151
152private:
153 const Params* params_;
154 const Bar* bars_;
155 std::size_t n_;
156 bool own_open_;
157 std::size_t index_ = 0;
158 bool cycle_ = false;
159 std::int64_t origin_ = 0;
160 std::int64_t next_origin_ = 0;
161};
162
163// One feed's days, each once and in ascending order, with the next one read
164// ahead.
165struct Lane {
166 Lane(const Params& params, const Bar* bars, std::size_t n, bool own_open)
167 : walk(params, bars, n, own_open) {}
168
169 // Moves to the next day the feed holds beyond the one it gave last. A day
170 // seen again keeps its first stamp; a day that goes back is refused.
171 void refill(bool& ordered) {
172 HeldDay day;
173 while (walk.next(day)) {
174 if (seen && day.ordinal == head.ordinal) continue;
175 if (seen && day.ordinal < head.ordinal) {
176 ordered = false;
177 continue;
178 }
179 head = day;
180 seen = true;
181 live = true;
182 return;
183 }
184 live = false;
185 }
186
189 bool live = false;
190 bool seen = false;
191};
192
193// The session days either feed holds, ascending and each once. A day both
194// hold keeps the chart's stamp.
195class HeldDays {
196public:
197 explicit HeldDays(const Params& params)
198 : chart_(params, params.chart, params.n_chart, params.daily_chart),
199 aux_(params, params.aux, params.n_aux, false) {
200 chart_.refill(ordered_);
201 aux_.refill(ordered_);
202 }
203
204 bool next(HeldDay& out) {
205 if (!chart_.live && !aux_.live) return false;
206 if (chart_.live && (!aux_.live || chart_.head.ordinal <= aux_.head.ordinal)) {
207 out = chart_.head;
208 if (aux_.live && aux_.head.ordinal == out.ordinal) aux_.refill(ordered_);
209 chart_.refill(ordered_);
210 } else {
211 out = aux_.head;
212 aux_.refill(ordered_);
213 }
214 return true;
215 }
216
217 // False once a feed's days went back.
218 bool ordered() const { return ordered_; }
219
220private:
221 Lane chart_;
222 Lane aux_;
223 bool ordered_ = true;
224};
225
226// The partition's period opens in ascending order, one at a time.
227class Stamps {
228public:
229 explicit Stamps(const Params& params)
230 : params_(&params),
231 held_(params),
232 year_(params.first_year),
233 day_(native_calendar::native_civil_days(params.first_year, 1, 1)),
234 year_end_(native_calendar::native_civil_days(params.first_year + 1, 1, 1)) {
235 have_head_ = held_.next(head_);
236 }
237
238 // The next period's open; false at the end, or when the partition is
239 // refused (failed()).
240 bool next(std::int64_t& stamp) {
241 if (failed_ || done_) return false;
242 Open open;
243 if (!started_) {
244 started_ = true;
245 // The first period is the last one opening at or before the first
246 // held day; none opens there when the year's first trading date
247 // comes after it.
248 bool begun = false;
249 Open begin;
250 while (next_open(open)) {
251 if (open.date > params_->first) {
252 ahead_ = open;
253 has_ahead_ = true;
254 break;
255 }
256 begin = open;
257 begun = true;
258 }
259 if (!begun) {
260 failed_ = true;
261 return false;
262 }
263 open = begin;
264 } else if (has_ahead_) {
265 open = ahead_;
266 has_ahead_ = false;
267 } else if (!next_open(open)) {
268 done_ = true;
269 return false;
270 }
271 std::int64_t at = open.value;
272 if (!open.held) {
273 // A trading date no bar holds: the stamp of the session day
274 // holding its local noon.
276 const auto noon = native_calendar::resolve_civil(
277 params_->tz.empty() ? "UTC" : params_->tz, static_cast<int>(date.year),
278 date.month, date.day, 12, 0);
279 if (!noon) {
280 failed_ = true;
281 return false;
282 }
283 at = session_period_open_ms(noon->epoch_ms, params_->tz, params_->session,
285 }
286 if (has_previous_ && at <= previous_) {
287 failed_ = true;
288 return false;
289 }
290 has_previous_ = true;
291 previous_ = at;
292 stamp = at;
293 return true;
294 }
295
296 // True once the partition was refused: an open that does not come after
297 // the one before it, a date whose local noon does not exist, or no period
298 // opening at or before the first held day.
299 bool failed() const { return failed_; }
300
301private:
302 struct Open {
303 std::int64_t date = 0; // the trading date (days since 1970-01-01)
304 bool held = false; // a feed holds it
305 std::int64_t value = 0; // its stamp when held
306 };
307
308 // The opens of every year from the first held day's through the year after
309 // the last's: the year's trading dates counted from its first, one in N
310 // opening a period.
311 bool next_open(Open& open) {
312 const Params& p = *params_;
313 for (;;) {
314 if (day_ >= year_end_) {
315 if (year_ >= p.last_year) return false;
316 ++year_;
317 day_ = native_calendar::native_civil_days(year_, 1, 1);
318 year_end_ = native_calendar::native_civil_days(year_ + 1, 1, 1);
319 count_ = 0;
320 continue;
321 }
322 const std::int64_t d = day_++;
323 while (have_head_ && head_.ordinal < d) have_head_ = held_.next(head_);
324 const bool is_held = have_head_ && head_.ordinal == d;
325 bool trading = false;
326 if (p.exchange_calendar && d >= p.first && d <= p.last) {
327 trading = is_held;
328 } else {
329 const std::int64_t weekday = ((d % 7) + 7 + 4) % 7; // 0 Sunday; 1970-01-01 a Thursday
330 trading = p.every_day || (weekday >= 1 && weekday <= 5);
331 }
332 if (!trading) continue;
333 if (count_++ % p.days == 0) {
334 open.date = d;
335 open.held = is_held;
336 open.value = is_held ? head_.value : 0;
337 return true;
338 }
339 }
340 }
341
342 const Params* params_;
343 HeldDays held_;
344 HeldDay head_;
345 bool have_head_ = false;
346 std::int64_t year_;
347 std::int64_t day_;
348 std::int64_t year_end_;
349 std::int64_t count_ = 0;
350 Open ahead_;
351 bool has_ahead_ = false;
352 bool started_ = false;
353 bool done_ = false;
354 bool failed_ = false;
355 bool has_previous_ = false;
356 std::int64_t previous_ = 0;
357};
358
359// The generated partition: the recent opens and the next one, over a
360// generator that restarts when asked about an instant before them.
362public:
363 Source(std::shared_ptr<const Params> params, std::int64_t first_stamp,
364 std::int64_t last_stamp)
365 : params_(std::move(params)), first_stamp_(first_stamp), last_stamp_(last_stamp) {
366 recent_.reserve(kBuffered + 1);
367 }
368
369 bool stamp_at(std::int64_t ms, std::int64_t& stamp, bool& last) override {
370 if (ms < first_stamp_) return false;
371 if (recent_.empty() || ms < recent_.front()) restart();
372 if (recent_.empty()) return false;
373 while (has_next_ && next_ <= ms) advance();
374 const auto after = std::upper_bound(recent_.begin(), recent_.end(), ms);
375 if (after == recent_.begin()) return false;
376 stamp = *(after - 1);
377 last = !has_next_ && after == recent_.end();
378 return true;
379 }
380
381 std::int64_t last_stamp() const override { return last_stamp_; }
382
383 std::size_t stamps_held() const override { return recent_.size() + (has_next_ ? 1u : 0u); }
384
385private:
386 static constexpr std::size_t kBuffered = kMultiDayWindow - 1;
387
388 // Takes the next open into the recent ones, the oldest leaving when they
389 // are full. The opens are the nominal calendar's whatever chart partition
390 // the run has installed by now, as they were when the table was built at
391 // prepare, before the run's bar loop.
392 void advance() {
393 NativeDayPartitionScope nominal(nullptr);
394 recent_.push_back(next_);
395 if (recent_.size() > kBuffered) recent_.erase(recent_.begin());
396 has_next_ = stamps_->next(next_);
397 }
398
399 // Starts the generator over from the first bar: the first open and the
400 // one after it.
401 void restart() {
402 NativeDayPartitionScope nominal(nullptr);
403 stamps_ = std::make_unique<Stamps>(*params_);
404 recent_.clear();
405 has_next_ = false;
406 std::int64_t first = 0;
407 if (!stamps_->next(first)) return;
408 recent_.push_back(first);
409 has_next_ = stamps_->next(next_);
410 }
411
412 std::shared_ptr<const Params> params_;
413 std::int64_t first_stamp_;
414 std::int64_t last_stamp_;
415 std::unique_ptr<Stamps> stamps_;
416 std::vector<std::int64_t> recent_;
417 std::int64_t next_ = 0;
418 bool has_next_ = false;
419};
420
421// Separate fallback classes keep the ordered generator's code, object sizes
422// and allocation path unchanged. A year's slots are the old map restricted
423// to that year, rebuilt from the beginning of both feeds on every reload.
425public:
426 explicit RescannedHeldDays(const Params& params) : params_(&params) {}
427
428 bool find(std::int64_t ordinal, std::int64_t& value) {
429 if (failed_ || ordinal < params_->first || ordinal > params_->last) return false;
430 if (!loaded_ || ordinal < begin_ || ordinal >= end_) {
431 const std::int64_t year = native_calendar::native_civil_date(ordinal).year;
432 begin_ = native_calendar::native_civil_days(year, 1, 1);
433 end_ = native_calendar::native_civil_days(year + 1, 1, 1);
434 if (end_ <= begin_
435 || end_ - begin_ > static_cast<std::int64_t>(kUnorderedRescanDays)) {
436 failed_ = true;
437 return false;
438 }
439 present_.fill(false);
440 scan(params_->chart, params_->n_chart, params_->daily_chart);
441 if (!failed_) scan(params_->aux, params_->n_aux, false);
442 loaded_ = true;
443 }
444 if (failed_) return false;
445 if (ordinal < begin_ || ordinal >= end_) {
446 failed_ = true;
447 return false;
448 }
449 const std::size_t at = static_cast<std::size_t>(ordinal - begin_);
450 if (at >= values_.size()) {
451 failed_ = true;
452 return false;
453 }
454 if (!present_[at]) return false;
455 value = values_[at];
456 return true;
457 }
458
459 bool failed() const { return failed_; }
460
461private:
462 void scan(const Bar* bars, std::size_t n, bool own_open) {
463 FeedWalk walk(*params_, bars, n, own_open);
464 HeldDay day;
465 while (walk.next(day)) {
466 // Never stop at a high ordinal: a later resolution can return
467 // to this year, even from more than one window away.
468 if (day.ordinal < begin_ || day.ordinal >= end_) continue;
469 const std::size_t at = static_cast<std::size_t>(day.ordinal - begin_);
470 if (at >= values_.size()) {
471 failed_ = true;
472 return;
473 }
474 if (!present_[at]) {
475 present_[at] = true;
476 values_[at] = day.value;
477 }
478 }
479 }
480
481 const Params* params_;
482 std::array<std::int64_t, kUnorderedRescanDays> values_{};
483 std::array<bool, kUnorderedRescanDays> present_{};
484 std::int64_t begin_ = 0;
485 std::int64_t end_ = 0;
486 bool loaded_ = false;
487 bool failed_ = false;
488};
489
490// The same date counting, first-period selection and stamp validation as
491// Stamps, using a rescan lookup instead of the ordered held-date iterator.
493public:
494 explicit RescannedStamps(const Params& params)
495 : params_(&params),
496 held_(params),
497 year_(params.first_year),
498 day_(native_calendar::native_civil_days(params.first_year, 1, 1)),
499 year_end_(native_calendar::native_civil_days(params.first_year + 1, 1, 1)) {}
500
501 bool next(std::int64_t& stamp) {
502 if (failed_ || done_) return false;
503 Open open;
504 if (!started_) {
505 started_ = true;
506 bool begun = false;
507 Open begin;
508 while (next_open(open)) {
509 if (open.date > params_->first) {
510 ahead_ = open;
511 has_ahead_ = true;
512 break;
513 }
514 begin = open;
515 begun = true;
516 }
517 if (!begun) {
518 failed_ = true;
519 return false;
520 }
521 open = begin;
522 } else if (has_ahead_) {
523 open = ahead_;
524 has_ahead_ = false;
525 } else if (!next_open(open)) {
526 done_ = true;
527 return false;
528 }
529 std::int64_t at = open.value;
530 if (!open.held) {
532 const auto noon = native_calendar::resolve_civil(
533 params_->tz.empty() ? "UTC" : params_->tz, static_cast<int>(date.year),
534 date.month, date.day, 12, 0);
535 if (!noon) {
536 failed_ = true;
537 return false;
538 }
539 at = session_period_open_ms(noon->epoch_ms, params_->tz, params_->session,
541 }
542 if (has_previous_ && at <= previous_) {
543 failed_ = true;
544 return false;
545 }
546 has_previous_ = true;
547 previous_ = at;
548 stamp = at;
549 return true;
550 }
551
552 bool failed() const { return failed_; }
553
554private:
555 struct Open {
556 std::int64_t date = 0;
557 bool held = false;
558 std::int64_t value = 0;
559 };
560
561 bool next_open(Open& open) {
562 const Params& p = *params_;
563 for (;;) {
564 if (day_ >= year_end_) {
565 if (year_ >= p.last_year) return false;
566 ++year_;
567 day_ = native_calendar::native_civil_days(year_, 1, 1);
568 year_end_ = native_calendar::native_civil_days(year_ + 1, 1, 1);
569 count_ = 0;
570 continue;
571 }
572 const std::int64_t d = day_++;
573 std::int64_t value = 0;
574 const bool is_held = held_.find(d, value);
575 if (held_.failed()) {
576 failed_ = true;
577 return false;
578 }
579 bool trading = false;
580 if (p.exchange_calendar && d >= p.first && d <= p.last) {
581 trading = is_held;
582 } else {
583 const std::int64_t weekday = ((d % 7) + 7 + 4) % 7;
584 trading = p.every_day || (weekday >= 1 && weekday <= 5);
585 }
586 if (!trading) continue;
587 if (count_++ % p.days == 0) {
588 open.date = d;
589 open.held = is_held;
590 open.value = value;
591 return true;
592 }
593 }
594 }
595
596 const Params* params_;
597 RescannedHeldDays held_;
598 std::int64_t year_;
599 std::int64_t day_;
600 std::int64_t year_end_;
601 std::int64_t count_ = 0;
602 Open ahead_;
603 bool has_ahead_ = false;
604 bool started_ = false;
605 bool done_ = false;
606 bool failed_ = false;
607 bool has_previous_ = false;
608 std::int64_t previous_ = 0;
609};
610
611// The ordered Source's recent-open/restart policy over the fallback
612// generator, without adding a fallback field or dispatch to Source.
614public:
615 RescannedSource(std::shared_ptr<const Params> params, std::int64_t first_stamp,
616 std::int64_t last_stamp)
617 : params_(std::move(params)), first_stamp_(first_stamp), last_stamp_(last_stamp) {
618 recent_.reserve(kBuffered + 1);
619 }
620
621 bool stamp_at(std::int64_t ms, std::int64_t& stamp, bool& last) override {
622 if (ms < first_stamp_) return false;
623 if (recent_.empty() || ms < recent_.front()) restart();
624 if (recent_.empty()) return false;
625 while (has_next_ && next_ <= ms) advance();
626 const auto after = std::upper_bound(recent_.begin(), recent_.end(), ms);
627 if (after == recent_.begin()) return false;
628 stamp = *(after - 1);
629 last = !has_next_ && after == recent_.end();
630 return true;
631 }
632
633 std::int64_t last_stamp() const override { return last_stamp_; }
634
635 std::size_t stamps_held() const override { return recent_.size() + (has_next_ ? 1u : 0u); }
636
637private:
638 static constexpr std::size_t kBuffered = kMultiDayWindow - 1;
639
640 void advance() {
641 NativeDayPartitionScope nominal(nullptr);
642 recent_.push_back(next_);
643 if (recent_.size() > kBuffered) recent_.erase(recent_.begin());
644 has_next_ = stamps_->next(next_);
645 }
646
647 void restart() {
648 NativeDayPartitionScope nominal(nullptr);
649 // Release the previous date window before allocating the next one.
650 stamps_.reset();
651 stamps_ = std::make_unique<RescannedStamps>(*params_);
652 recent_.clear();
653 has_next_ = false;
654 std::int64_t first = 0;
655 if (!stamps_->next(first)) return;
656 recent_.push_back(first);
657 has_next_ = stamps_->next(next_);
658 }
659
660 std::shared_ptr<const Params> params_;
661 std::int64_t first_stamp_;
662 std::int64_t last_stamp_;
663 std::unique_ptr<RescannedStamps> stamps_;
664 std::vector<std::int64_t> recent_;
665 std::int64_t next_ = 0;
666 bool has_next_ = false;
667};
668
669inline std::shared_ptr<TimeframeAggregator::PeriodSource> make_rescanned_source(
670 std::shared_ptr<Params> params) {
671 bool any = false;
672 const auto scan_bounds = [&](const Bar* bars, std::size_t n, bool own_open) {
673 FeedWalk walk(*params, bars, n, own_open);
674 HeldDay day;
675 while (walk.next(day)) {
676 if (!any) {
677 params->first = day.ordinal;
678 params->last = day.ordinal;
679 } else {
680 params->first = std::min(params->first, day.ordinal);
681 params->last = std::max(params->last, day.ordinal);
682 }
683 any = true;
684 }
685 };
686 scan_bounds(params->chart, params->n_chart, params->daily_chart);
687 scan_bounds(params->aux, params->n_aux, false);
688 if (!any) return nullptr;
689 params->first_year = native_calendar::native_civil_date(params->first).year;
690 params->last_year = native_calendar::native_civil_date(params->last).year + 1;
691 std::int64_t first_stamp = 0;
692 std::int64_t last_stamp = 0;
693 {
694 // Allocate the date arrays only after the ordered scan failed;
695 // keep them out of the ordered generator and source objects.
696 auto stamps = std::make_unique<RescannedStamps>(*params);
697 std::int64_t stamp = 0;
698 bool have_stamp = false;
699 while (stamps->next(stamp)) {
700 if (!have_stamp) first_stamp = stamp;
701 last_stamp = stamp;
702 have_stamp = true;
703 }
704 if (!have_stamp || stamps->failed()) return nullptr;
705 }
706 return std::make_shared<RescannedSource>(std::move(params), first_stamp, last_stamp);
707}
708
709} // namespace multi_day_detail
710
711/// The N-day partition of `feeds`, or null where TradingView's rule yields
712/// none: fewer than two days, a session that does not parse, no held session
713/// day, no period opening at or before the first held day, an open that does
714/// not come after the one before it, or a date whose local noon does not
715/// exist. Ordered planning walks the feeds twice (the first and last held
716/// day, then every period once to prove the partition whole). An unordered
717/// feed instead uses full-feed rescans with at most kUnorderedRescanDays
718/// retained date slots; neither path keeps a table of all held days.
719inline std::shared_ptr<TimeframeAggregator::PeriodSource> make_multi_day_period_source(
720 const MultiDayFeeds& feeds) {
721 namespace detail = multi_day_detail;
722 if (feeds.days < 2) return nullptr;
723 NativeDayPartitionScope nominal(nullptr);
724 auto calendar = native_calendar::parse_session(feeds.session, feeds.tz);
725 if (!calendar) return nullptr;
726 auto params = std::make_shared<detail::Params>();
727 params->days = feeds.days;
728 params->exchange_calendar = feeds.exchange_calendar;
729 params->every_day = feeds.session.empty() || feeds.session == "24x7";
730 params->daily_chart = feeds.daily_chart;
731 params->chart = feeds.chart;
732 params->n_chart = feeds.n_chart > 0 ? static_cast<std::size_t>(feeds.n_chart) : 0;
733 params->aux = feeds.aux;
734 params->n_aux = feeds.n_aux;
735 params->tz = feeds.tz;
736 params->session = feeds.session;
737 params->calendar = std::move(*calendar);
738 {
739 detail::HeldDays held(*params);
740 detail::HeldDay day;
741 bool any = false;
742 while (held.next(day)) {
743 if (!any) params->first = day.ordinal;
744 params->last = day.ordinal;
745 any = true;
746 }
747 if (!any || !held.ordered()) {
748 if (!any) return nullptr;
749 return detail::make_rescanned_source(std::move(params));
750 }
751 }
752 params->first_year = native_calendar::native_civil_date(params->first).year;
753 params->last_year = native_calendar::native_civil_date(params->last).year + 1;
754 std::int64_t first_stamp = 0;
755 std::int64_t last_stamp = 0;
756 {
757 detail::Stamps stamps(*params);
758 std::int64_t stamp = 0;
759 bool any = false;
760 while (stamps.next(stamp)) {
761 if (!any) first_stamp = stamp;
762 last_stamp = stamp;
763 any = true;
764 }
765 if (!any || stamps.failed()) return nullptr;
766 }
767 return std::make_shared<detail::Source>(std::move(params), first_stamp, last_stamp);
768}
769
770} // namespace pineforge::source
A period partition generated on demand rather than stored: what set_native_period_source installs whe...
FeedWalk(const Params &params, const Bar *bars, std::size_t n, bool own_open)
bool find(std::int64_t ordinal, std::int64_t &value)
RescannedSource(std::shared_ptr< const Params > params, std::int64_t first_stamp, std::int64_t last_stamp)
bool stamp_at(std::int64_t ms, std::int64_t &stamp, bool &last) override
std::size_t stamps_held() const override
How many period opens the source holds in memory right now.
std::int64_t last_stamp() const override
The final period's open.
Source(std::shared_ptr< const Params > params, std::int64_t first_stamp, std::int64_t last_stamp)
std::int64_t last_stamp() const override
The final period's open.
bool stamp_at(std::int64_t ms, std::int64_t &stamp, bool &last) override
std::size_t stamps_held() const override
How many period opens the source holds in memory right now.
constexpr int64_t native_civil_days(int64_t year, int month, int day) noexcept
std::optional< CivilResolution > resolve_civil(std::string_view timezone, int year, int month, int day, int hour, int minute, int second=0)
constexpr NativeCivilDate native_civil_date(int64_t days) noexcept
std::optional< NativeSessionDay > session_day_at(const SessionCalendar &calendar, int64_t ms)
std::optional< SessionCalendar > parse_session(std::string_view session, std::string_view timezone)
std::shared_ptr< TimeframeAggregator::PeriodSource > make_rescanned_source(std::shared_ptr< Params > params)
constexpr std::size_t kUnorderedRescanDays
Bounds the fallback's retained trading-date slots to one calendar year, including a leap year; N-day ...
constexpr std::size_t kMultiDayWindow
The most period opens a multi-day period source holds at once: the recent ones it answers short quest...
std::shared_ptr< TimeframeAggregator::PeriodSource > make_multi_day_period_source(const MultiDayFeeds &feeds)
The N-day partition of feeds, or null where TradingView's rule yields none: fewer than two days,...
int64_t session_covered_instant_ms(int64_t ms, const std::string &tz, const std::string &session)
The session instant a native CALENDAR chart stamp covers.
int64_t session_period_open_ms(int64_t ms, const std::string &tz, const std::string &session, CalendarPeriod period)
Open (Unix ms) of the symbol's D/W/M bar that contains ms: the day stamp of the period's first sessio...
What a run holds of the symbol the N-day request reads.
const Bar * chart
the chart's bars, strictly increasing
int days
N of the request, at least 2.
std::string session
the symbol's session
bool exchange_calendar
the held sessions are the trading dates
const Bar * aux
the auxiliary feed's bars, strictly increasing
std::string tz
the symbol's timezone
bool daily_chart
a chart bar's own open dates its session day
Lane(const Params &params, const Bar *bars, std::size_t n, bool own_open)
native_calendar::SessionCalendar calendar