PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
timeframe.hpp
Go to the documentation of this file.
1#pragma once
2#include <string>
3#include <cstdint>
4#include <vector>
5#include <memory>
6#include "bar.hpp"
8
9namespace pineforge {
10// ─── Day-length constants ──────────────────────────────────────────────────────
11
12inline constexpr int64_t kSecPerDay = 86400;
13inline constexpr int64_t kMsPerDay = 86400000;
14
15// ─── AggregatedBar ─────────────────────────────────────────────────────────────
16
22
23// ─── TF string helpers ─────────────────────────────────────────────────────────
24/// Convert a TradingView timeframe string to seconds.
25/// Minute-based: "1","5","15","30","60","120","240" => minutes * 60
26/// Day-based: "D","1D" => 86400
27/// Week-based: "W","1W" => 604800
28/// Month-based: "M","1M" => -1 (calendar-based, not fixed seconds)
29int tf_to_seconds(const std::string& tf);
30
31/// Extract the numeric multiplier from a TF string (e.g. "15" -> 15, "D" -> 1).
32inline int tf_multiplier(const std::string& tf) {
33 int val = 0;
34 for (char c : tf) {
35 if (c >= '0' && c <= '9') val = val * 10 + (c - '0');
36 else break;
37 }
38 return val > 0 ? val : 1;
39}
40
41inline bool tf_is_intraday(const std::string& tf) {
42 int s = tf_to_seconds(tf);
43 return s > 0 && s < kSecPerDay;
44}
45
46inline bool tf_is_daily(const std::string& tf) {
47 return tf.find('D') != std::string::npos || tf_to_seconds(tf) == kSecPerDay;
48}
49
50inline bool tf_is_weekly(const std::string& tf) {
51 return tf.find('W') != std::string::npos;
52}
53
54inline bool tf_is_monthly(const std::string& tf) {
55 return !tf.empty() && tf.back() == 'M';
56}
57
58inline bool tf_is_seconds(const std::string& tf) {
59 return tf.find('S') != std::string::npos;
60}
61
62/// Check if prev/curr timestamps cross a timeframe boundary.
63bool tf_change(int64_t prev_ms, int64_t curr_ms, const std::string& tf);
64
65/// Compute how many input bars fit into one target bar.
66/// Returns positive int for ratio-based, -1 for calendar-based (month),
67/// -2 if target < input (error).
68int tf_ratio(const std::string& input_tf, const std::string& target_tf);
69
70// ─── Auto-detect timeframe from bar timestamps ────────────────────────────────
71/// Detect the timeframe string from an array of bars by computing the median
72/// timestamp delta and mapping to the nearest standard TF.
73/// Returns a TradingView-style TF string (e.g. "1", "5", "15", "60", "D", "W").
74/// Uses up to the first `max_samples` bars for detection.
75/// Returns "1" if detection fails (< 2 bars or irregular data).
76std::string detect_timeframe(const Bar* bars, int n, int max_samples = 100);
77
78// ─── Calendar boundary detection ───────────────────────────────────────────────
79
80enum class CalendarPeriod { NONE, DAY, WEEK, MONTH };
81
82/// Determine the calendar period for a target TF string.
83CalendarPeriod calendar_period_for(const std::string& tf);
84
85/// True for a daily-or-higher chart timeframe ("D", "1D", "2D", "W", "M"):
86/// a bar that covers whole session days rather than a time of day (the
87/// aggregation path's CALENDAR classification). Intraday timeframes and an
88/// empty (undetected) one are false.
89inline bool tf_is_daily_or_higher(const std::string& tf) {
91}
92
93/// Check if two timestamps (Unix milliseconds) fall in different calendar periods.
94bool crosses_boundary(int64_t prev_ms, int64_t curr_ms, CalendarPeriod period);
95
96/// Timezone/session-aware variants. The tz-less forms above evaluate the
97/// calendar in UTC and intraday buckets on the epoch grid — exactly TV's
98/// behavior for 24x7 symbols (the corpus regime). Session symbols
99/// (equities RTH, forex) anchor HTF buckets on the exchange clock instead:
100/// daily boundaries at symbol-local midnight, intraday buckets offset by the
101/// symbol's day stamp -- the session open, except OANDA's 1800-1700 metals
102/// session whose day (and TradingView's "45"/"240" grid) rolls at the 17:00
103/// ET forex roll one hour before it opens (session_day_stamp_offset_minutes
104/// in timeframe.cpp cites the pin). With tz="UTC" and session ""/"24x7"
105/// these are bit-identical to the UTC forms, so existing callers are
106/// unaffected.
107bool crosses_boundary(int64_t prev_ms, int64_t curr_ms, CalendarPeriod period,
108 const std::string& tz, const std::string& session);
109bool tf_change(int64_t prev_ms, int64_t curr_ms, const std::string& tf,
110 const std::string& tz, const std::string& session);
111
112// ─── Symbol-clock D/W/M bar anchors ────────────────────────────────────────────
113//
114// TradingView's daily bar is the SESSION day, not the UTC (nor the local
115// calendar) day: OANDA:EURUSD (America/New_York, 1700-1700) opens its daily
116// bar at 17:00 ET and its week at Sunday 17:00 ET; NASDAQ:AAPL (0930-1600)
117// opens at 09:30 ET Monday..Friday; a 24x7 UTC symbol opens at 00:00 UTC.
118// Every chart-level consumer of "the symbol's daily bar" — ta.vwap's default
119// anchor, timeframe.change("1D"), time("D")/time_close("D") — has to key on
120// this clock, the same one request.security aggregation already uses
121// (crosses_boundary / session_period_key above). The bar OPENS at the
122// session-day's DAY STAMP — the session open on every session but OANDA's
123// 1800-1700, whose D bar is stamped 17:00 ET an hour before it trades
124// (time("D") reads 17:00 ET on OANDA:XAUUSD, pin-time-hours; the session
125// open stays where the tape trades from). With tz="UTC" and an empty /
126// "24x7" session all of these reduce to plain epoch integer math,
127// bit-identical to the UTC forms the corpus pins.
128//
129// Period attribution follows TV's trading-date rule: a session that wraps
130// midnight (1700-1700) is the trading day of the date it CLOSES on (the
131// bar opening Sunday 17:00 ET is Monday's daily bar), so forex weeks open on
132// the Sunday session and a month opens on the session whose close date is
133// the 1st. Equity / 24x7 sessions close on their open date, so their weeks
134// stay Monday-partitioned and months calendar-partitioned exactly as before.
135// W/M opens are nominal (the Monday / 1st session-day); they do not consult
136// a holiday calendar.
137/// Ordinal of the session day containing `ms` (days since epoch on the
138/// session clock). UTC + no session: ms / kMsPerDay.
139int64_t session_day_index(int64_t ms, const std::string& tz,
140 const std::string& session);
141
142namespace internal {
143/// Broker trading-day ordinal on the unmerged symbol session clock. A native
144/// daily bar may combine holiday sessions, but each session still renews the
145/// intraday order budget. This function never reads the native D partition.
146int64_t session_trading_day_index(int64_t ms, const std::string& tz,
147 const std::string& session);
148} // namespace internal
149
150/// Open (Unix ms) of the symbol's D/W/M bar that contains `ms`: the day
151/// stamp of the period's first session-day (17:00 ET on OANDA 1800-1700,
152/// the session open elsewhere). CalendarPeriod::NONE returns `ms` unchanged.
153int64_t session_period_open_ms(int64_t ms, const std::string& tz,
154 const std::string& session,
155 CalendarPeriod period);
156
157/// Open (Unix ms) of the `bucket_sec`-wide intraday bucket that contains
158/// `ms` on the symbol's day-stamp-anchored grid: exchange-tz time since
159/// local midnight + day stamp, floored to the bucket width. This is THE
160/// intraday HTF grid — request.security's ratio buckets
161/// (TimeframeAggregator::bucket_open_ms), tf_change and
162/// time("<intraday tf>") / time_close all read it (pin-time-hours:
163/// time("60") on NYSE:F is 09:30 / 10:30 / .. / 15:30 ET, time("240") on
164/// NSE:NIFTY 09:15 / 13:15 IST, on OANDA:XAUUSD 17:00 ET + 4h*k). With
165/// tz="UTC" and session ""/"24x7" it is the epoch grid, bit-identical to
166/// the tz-less forms. bucket_sec <= 0 returns `ms`. Under the calling
167/// thread's native daily partition for this clock (below), a session-day
168/// that opened late -- NativeDayPartition::late_opens -- starts its grid at
169/// its own open instead of the day stamp, as tf_change's intraday form does.
170int64_t session_intraday_bucket_open_ms(int64_t ms, int64_t bucket_sec,
171 const std::string& tz,
172 const std::string& session);
173
174/// The session instant a native CALENDAR chart stamp covers. A stamp inside
175/// its session (open to exclusive close) returns unchanged. A stamp in the
176/// inter-session gap -- before its session-day's open (the 1800-1700 day
177/// stamp hour) or at / after its close -- covers the session about to open
178/// -- OANDA stamps daily FX/metal bars at the 17:00 ET break under an
179/// 1800-1700 session -- and rolls forward to that session's open.
180/// ""/24x7 sessions have no gap and always return `ms` unchanged.
181int64_t session_covered_instant_ms(int64_t ms, const std::string& tz,
182 const std::string& session);
183
184/// Exclusive close (Unix ms) of the symbol's D/W/M bar that contains `ms`:
185/// DAY -> session-day open + session length (16:00 ET on equities, the next
186/// 17:00 ET on forex, next midnight on 24x7); WEEK / MONTH -> the open of
187/// the next period's first session-day. CalendarPeriod::NONE returns `ms`.
188int64_t session_period_close_ms(int64_t ms, const std::string& tz,
189 const std::string& session,
190 CalendarPeriod period);
191
192/// Exclusive close (Unix ms) of the LAST TRADED session-day of the D/W/M
193/// bar that contains `ms`: DAY is session_period_close_ms; WEEK / MONTH
194/// step back from the period's last session-day over weekend TRADING dates
195/// (Saturday / Sunday never hold a session on exchange-calendar symbols),
196/// so an equity week ends Friday 16:00 ET, a month whose last calendar day
197/// is a weekend ends on its last Friday, and the forex week ends Friday
198/// 17:00 ET (the Friday-open session is Saturday's trading date). Exchange
199/// holidays are not modelled. CalendarPeriod::NONE returns `ms`.
200int64_t session_period_last_traded_close_ms(int64_t ms, const std::string& tz,
201 const std::string& session,
202 CalendarPeriod period);
203
204// ─── Native daily partition for the CHART symbol's D period ────────────────────
205//
206// TradingView's D period on an exchange-calendar intraday chart is its own
207// daily bar, not the nominal session-day: on CME_MINI:NQ1!/ES1! 15m the
208// time("D"), ta.change(time("D")), timeframe.change("1D") and ta.vwap's
209// default daily anchor follow the exchange TRADE-DATE daily bars, so the
210// 17:00 CT reopen after a US-holiday early close stays inside the D bar that
211// opened before the holiday (2025-05-26 Memorial Day evening: the bar
212// stamped Sun 05-25 17:00 runs to Tue 05-27 16:00; 06-19 Juneteenth; the Sun
213// 07-06 after Independence Day, whose Thu 07-03 17:00 stamp runs to Mon
214// 07-07 16:00; 09-01 Labor Day; 11-27 Thanksgiving, to Fri 11-28 12:15;
215// 2026-01-19 MLK; 02-16 Presidents' Day) and the Good-Friday-eve session
216// (Thu 04-17 17:00 CT, 2025) belongs to the Wed 04-16 17:00 bar that runs to
217// the Sun 04-20 reopen -- pinned 2026-09-05 by lab tv o-cme-dayanchor-full
218// (ledger log-20260905t123531z-7fe6b95a: 255 D periods over 2025-04-01 ..
219// 2026-05-01, every start a registry daily-feed row, feed == TV data on
220// 25530/25530 closes). Every nominal session-day rule stays as it is; when
221// the run installs TradingView's own daily bars (strategy_set_native_
222// security_feed "D") on an intraday chart, the engine builds this partition
223// from their stamps -- the same partition request.security "D" evaluators
224// take (TimeframeAggregator::set_native_periods) -- and the chart-level
225// consumers below read it for the symbol's own clock (tz + session equal to
226// the partition's): the D period holding an instant is the native bar whose
227// stamp is the latest at or before it; its trade day is the session-day of
228// its last chart bar (the merged Memorial-Day bar is Tuesday's); a W / M
229// period groups the native days by the nominal week / month of their trade
230// day and opens on the group's first stamp (b29152c's request.security rule);
231// an instant before the first stamp, or at / after the LAST stamp's nominal
232// session-day close, keeps the nominal rules. Without an installed partition
233// (no native daily feed, a 1D chart, another symbol's clock) every function
234// is bit-identical to the nominal session calendar.
235//
236// The stamps also date where a day OPENED. A stamp later than its
237// session-day's day stamp, with no input bar of that session-day before it,
238// is the open of a day that traded late (native_late_day_opens), and that
239// day's intraday grid starts there: session_intraday_bucket_open_ms and
240// tf_change's intraday form read the partition's late_opens, as a request
241// evaluator's RATIO aggregator reads the same opens from the daily feed
242// (TimeframeAggregator::set_native_day_opens). NSE's Muhurat session of
243// 2025-10-21 opens 13:45 IST, and its one 60-minute bar is stamped 13:45
244// on TradingView, not 13:15 and 14:15 from the 09:15 day stamp (lab tv
245// w14-htf-60-nifty15).
247 std::string tz;
248 std::string session;
249 std::vector<int64_t> stamps; // native daily stamps, strictly increasing
250 detail::FeedProjection<int64_t> trade_day; // per stamp: session-day ordinal
251 detail::FeedProjection<int64_t> week_open; // per stamp: W group's first stamp
252 detail::FeedProjection<int64_t> month_open; // per stamp: M group's first stamp
253 int64_t last_bound = 0; // nominal session-day close of the last stamp
254 std::vector<int64_t> late_opens; // the stamps that open their session-day late
255 bool empty() const { return stamps.empty(); }
256};
257
258/// The native daily stamps that open their session-day LATE: a stamp after
259/// the day stamp of the (nominal) session-day holding it and before that
260/// day's scheduled close, with no input bar between the day stamp and the
261/// stamp -- the day's first trade is the stamp itself, hours after the
262/// session template's open (NSE's Muhurat session of 2025-10-21, 13:45 IST).
263/// Every other stamp is left out: one at its day stamp (every regular day),
264/// one on an earlier session-day (a holiday-merged bar), one at or after
265/// its session-day's scheduled close (a venue dating its daily bars at
266/// midnight or at the close; an evening session the template does not
267/// hold, 18:00 IST on 2024-11-01, whose end it cannot know either), and one
268/// the input shows the day already trading before. Strictly increasing, as
269/// the stamps; empty for non-increasing stamps. `input_bars` may be null
270/// (n_input 0): nothing then contradicts a late stamp.
271std::vector<int64_t> native_late_day_opens(const std::string& tz,
272 const std::string& session,
273 const std::vector<int64_t>& stamps,
274 const Bar* input_bars, int n_input);
275
276/// Build the partition for the symbol clock (tz, session) from the native
277/// daily stamps and the chart's input bars (the trade instants: the last
278/// input bar before the next stamp, bounded by the last stamp's nominal
279/// session-day close; the stamp itself when no input bar lies in the
280/// period). Returns false -- and leaves `out` empty -- for no stamps or
281/// non-increasing stamps.
283 const std::string& tz,
284 const std::string& session,
285 const std::vector<int64_t>& stamps,
286 const Bar* input_bars, int n_input, const std::string& scratch_directory = {});
287
288/// As above, with an inclusive optional cache budget for EACH of the three
289/// projection columns. Zero/sub-page budgets read directly from backing.
290/// Stamps, late opens and caller input bars retain their existing ownership.
292 const std::string& tz,
293 const std::string& session,
294 const std::vector<int64_t>& stamps,
295 const Bar* input_bars, int n_input,
296 std::size_t projection_cache_bytes, const std::string& scratch_directory = {});
297
298/// Index of the native period holding `ms` under `p`: -1 before the first
299/// stamp and at / after the last stamp's nominal session-day close.
301
302/// The calling thread's active chart partition, read by session_day_index /
303/// session_period_open_ms / session_period_close_ms /
304/// session_period_last_traded_close_ms / crosses_boundary (and so by
305/// tf_change, the symbol-clock pine_time forms and ta::VWAP's session
306/// anchor) whenever their (tz, session) equal the partition's. nullptr (the
307/// default) leaves every rule nominal. set_ returns the previous pointer;
308/// BacktestEngine::run installs its chart partition through
309/// NativeDayPartitionScope for exactly the run's bar loop.
312
323
324// Feature probe: the chart-level native daily partition above exists.
325#define PINEFORGE_HAS_NATIVE_DAY_PARTITION_V1 1
326
327// ─── TimeframeAggregator ───────────────────────────────────────────────────────
328
330public:
331 /// Default: passthrough mode (no aggregation).
333
334 /// Ratio-based: every `ratio` input bars produce one output bar.
335 explicit TimeframeAggregator(int ratio);
336
337 /// Calendar-based: aggregate until day/week/month boundary.
338 TimeframeAggregator(const std::string& target_tf,
339 const std::string& input_tf);
340
341 /// Calendar/ratio aggregation anchored on a symbol clock. tz is the Pine
342 /// syminfo.timezone (exchange tz); session the Pine session string
343 /// ("0930-1600", "24x7", ...). Defaults reproduce the UTC/24x7 forms
344 /// bit-for-bit, so every existing construction site compiles unchanged.
345 TimeframeAggregator(const std::string& target_tf,
346 const std::string& input_tf,
347 const std::string& tz,
348 const std::string& session = "");
349
350 /// Feed one input bar. Returns aggregation state.
351 AggregatedBar feed(const Bar& input_bar);
352
353 /// feed() with the NEXT input bar's timestamp known (0 = unknown, the
354 /// form above). A historical run holds its whole feed, and on a declared
355 /// exchange session TradingView finalizes a D/W/M bar on the period's
356 /// actual last chart bar whatever the nominal calendar says that bar is:
357 /// the 12:45 ET bar of a 13:00 half-day (NYSE:F Fri 2025-11-28), the
358 /// Thursday 15:45 bar before a holiday Friday, the 15:45 CT bar closing
359 /// an overnight CME session-day (lab tv wm-security-buckets tapes,
360 /// 2026-09-05). CALENDAR mode therefore also completes the running
361 /// bucket when the next input bar opens a new period. RATIO and
362 /// PASSTHROUGH ignore the hint, and so do ""/"24x7" sessions (a data
363 /// hole before a 24x7 midnight is not an early close), so every caller
364 /// passing 0 and every session-less feed stay bit-identical. The rule
365 /// applies where TradingView's session template knows the early close
366 /// -- exchange calendars; set_early_close_completes(false) switches a
367 /// symbol whose template does not (OANDA's cfd / forex streams) back to
368 /// the nominal close and the lazy completion on the next period's first
369 /// bar.
370 AggregatedBar feed(const Bar& input_bar, int64_t next_input_ms);
371
372 /// feed(bar, next_input_ms) with the CALLING chart bar's nominal close
373 /// known (0 = unknown, the forms above): TradingView's time_close of
374 /// the chart bar this input bar belongs to -- the input bar's own end
375 /// on a single-feed run, the native D/W/M chart bar's session close on
376 /// the split-feed path, where a finer auxiliary slice advances
377 /// request.security. Read only by the OTC rule
378 /// (set_early_close_completes(false)): TradingView surfaces a D/W/M
379 /// value on the chart bar whose time_close reaches the period's
380 /// nominal close, and the daily bar of an early-close day keeps its
381 /// nominal time_close -- on the OANDA:XAUUSD 1D chart the Fri
382 /// 2025-07-04 bar (data to 12:45 ET) still closes at 17:00 ET, so
383 /// request.security(tickerid, "W", x) advances on it (cW 3336.61, the
384 /// early close) exactly as on every regular Friday, and "D" is the
385 /// chart bar itself there and on Mon 2026-02-16 (14:15 early close);
386 /// on the 15m chart the 12:45 / 14:15 bar's own time_close falls short
387 /// and the value waits for the next session's first bar (lab tv
388 /// oanda1d-{jul,feb,novm,decm} beside the oanda 15m pin, 2026-09-05).
389 /// So on an OTC stream a bucket the next input bar leaves completes on
390 /// this input bar iff calling_close_ms reaches the period's nominal
391 /// close (session_period_last_traded_close_ms); with 0 it waits, as
392 /// before. Exchange kinds (early_close_completes) ignore the hint: the
393 /// actual last bar completes the period either way.
394 AggregatedBar feed(const Bar& input_bar, int64_t next_input_ms,
395 int64_t calling_close_ms);
396
397 /// Current in-progress bar.
398 Bar current() const;
399
400 /// RATIO (fixed intraday target) only: whether the bucket in progress
401 /// holds sub-bars no completion has emitted yet -- the tail of a chart
402 /// bar whose last sub-bars never reached the bucket's count, real end
403 /// or session close (the 21:57Z 3m bucket of OANDA:XAUUSD's Thanksgiving
404 /// 2025-11-26 session holds only the 21:59Z minute). CALENDAR and
405 /// PASSTHROUGH answer false; so does a count-only ratio with no
406 /// wall-clock width.
408
409 /// Finalize the pending partial bucket exactly as feed() finalizes a
410 /// bucket -- the sub-bars it holds ARE the bucket -- and return it
411 /// complete. TradingView surfaces the LAST intrabar of a chart bar at
412 /// that bar's close whatever its minute count (lab tv
413 /// dca-ltf-last-intrabar, 2026-09-05), so a request.security evaluator
414 /// served by a finer feed calls this when its calling chart bar
415 /// completes. The bucket is emitted once: the next boundary resets it
416 /// without re-emitting (current_emitted_complete), and a later sub-bar
417 /// of the same bucket merges without completing it again, as after any
418 /// early completion. Without a pending partial nothing changes and the
419 /// current bar is returned incomplete.
421
422 /// Last completed aggregated bar.
424
425 /// Whether aggregation is active (non-passthrough).
426 bool is_active() const;
427
428 /// The D/W/M period a CALENDAR aggregator buckets on; NONE for RATIO
429 /// and PASSTHROUGH.
431
432 /// TradingView's own period partition for a CALENDAR aggregator, from
433 /// the native request.security feed of the aggregated timeframe: the
434 /// stamps (Unix ms, strictly increasing) are the exchange's native bars'
435 /// opens, and trade_instants[k] is an instant inside the session-day the
436 /// k-th native bar COMPLETES on -- its last input bar (the stamp itself
437 /// when no input bar falls in the period), the trade date TradingView
438 /// files the bar under. Installed, they replace the nominal session
439 /// calendar as the period key: an input bar belongs to the native bar
440 /// whose stamp is the latest at or before it, so a D period is exactly
441 /// that native bar's span. On CME_MINI:ES1! a holiday session that
442 /// pauses at 12:00 CT and reopens at 17:00 the same day (Labor Day,
443 /// Thanksgiving, Independence Day) is folded by TradingView into the
444 /// NEXT trade date's daily bar with no stamp of its own (lab tv esd pin,
445 /// 2026-09-05: the Sun 08-31 17:00 stamp runs to Tue 09-02 15:45 and is
446 /// Tuesday's bar), so no D period closes at the pause, none opens at the
447 /// reopen, and a data hole inside a native day is not a close. A W / M
448 /// period groups the native days by the nominal week / month of their
449 /// trade date (the holiday session belongs to the next trade date, so to
450 /// its week). bar_label_ms is the stamp of the bar's native period,
451 /// bucket_open_ms the group's first stamp, period_changes compares
452 /// those, and feed(bar, next_input_ms) completes the running bucket on
453 /// the last input bar before the next native stamp. The partition covers
454 /// the feed: an input bar before the first stamp keeps the nominal key,
455 /// and so does one at or after the LAST stamp's nominal period close
456 /// (session_period_close_ms of feed_period, the feed's own timeframe --
457 /// DAY for a daily feed), since without a next stamp nothing proves the
458 /// last native bar reaches further; a chart day the feed does not hold
459 /// stays a nominal session-day bucket with no native bar to substitute.
460 /// Empty vectors (the default) leave every nominal rule bit-identical;
461 /// RATIO / PASSTHROUGH ignore the call. Mismatched sizes or
462 /// non-increasing stamps install nothing.
463 void set_native_periods(std::vector<int64_t> stamps,
464 std::vector<int64_t> trade_instants,
465 CalendarPeriod feed_period);
466
467 /// A period partition generated on demand rather than stored: what
468 /// set_native_period_source installs where set_native_periods installs
469 /// stamps, for a partition whose stamps would grow with the feed (one per
470 /// N trading dates of an N-day request). The aggregator asks it exactly
471 /// what it asks of stored stamps -- the latest period opening at or before
472 /// an instant -- and keeps nothing of the answers. The source owns its
473 /// memory bound, and its answers do not depend on the order of the
474 /// questions.
476 public:
477 virtual ~PeriodSource() = default;
478 /// The open of the latest period at or before `ms`; false before the
479 /// first period. `last` says that period is the final one.
480 virtual bool stamp_at(int64_t ms, int64_t& stamp, bool& last) = 0;
481 /// The final period's open.
482 virtual int64_t last_stamp() const = 0;
483 /// How many period opens the source holds in memory right now.
484 virtual std::size_t stamps_held() const = 0;
485 };
486
487 /// Install a DAY partition from `source`: each period's group open is its
488 /// stamp, as set_native_periods leaves a DAY partition's, and the last
489 /// period reaches at least its nominal close. It replaces a stored
490 /// partition, as a later set_native_periods replaces the source. Returns
491 /// false -- and leaves no native partition -- for a null source or an
492 /// aggregator that is not CALENDAR on DAY. The source must stay valid for
493 /// every question the run asks of this aggregator.
494 bool set_native_period_source(std::shared_ptr<PeriodSource> source);
495
496 /// Period opens this aggregator holds in memory: the stamps and group
497 /// opens of a stored partition, what the source holds for a generated one.
498 std::size_t native_period_stamps_held() const;
499
500 /// A RATIO aggregator's late day opens, from the installed native daily
501 /// feed (native_late_day_opens over the evaluator's input): each opens a
502 /// session-day that traded later than its day stamp, and the intraday
503 /// grid of that session-day starts at it instead of the stamp -- the
504 /// keys feed() splits on, bucket_open_ms and bar_label_ms alike. NSE's
505 /// Muhurat session of 2025-10-21 (13:45 IST, four 15m bars) is one "60"
506 /// bucket stamped 13:45, where the 09:15 day stamp made two (13:15, 14:15)
507 /// and the bucket index ran one ahead for the rest of the feed (lab tv
508 /// w14-htf-60-nifty15, w14-htf-60ema-nifty15). Every other session-day
509 /// keeps the day-stamp grid bit for bit. Empty (the default) changes
510 /// nothing; CALENDAR / PASSTHROUGH and a count-only ratio ignore the
511 /// call; non-increasing opens install nothing.
512 void set_native_day_opens(std::vector<int64_t> late_opens);
513
514 /// Whether an installed native daily feed keys this aggregator's
515 /// buckets: its period partition (CALENDAR, set_native_periods or
516 /// set_native_period_source) or late day opens (RATIO,
517 /// set_native_day_opens).
518 bool has_native_periods() const {
519 return !native_stamps_.empty() || native_source_ != nullptr
520 || !native_late_opens_.empty();
521 }
522
523 /// Whether a session ending BEFORE its nominal close completes the
524 /// running D/W/M bucket on that session's actual last input bar
525 /// (feed(bar, next_input_ms) seeing the next input bar open a new
526 /// period) -- true, the default -- or the bucket waits for the period's
527 /// nominal close (the 16:45 bar reaching 17:00, the last traded
528 /// session-day's close) and, when no input bar reaches it, completes
529 /// lazily on the next period's first bar (false) -- unless the CALLING
530 /// chart bar's nominal close reaches the period's (feed(bar,
531 /// next_input_ms, calling_close_ms): the 1D chart's early-close daily
532 /// bar, whose time_close stays 17:00 ET). TradingView finalizes
533 /// on the actual last bar only where its session template carries the
534 /// early close: exchange calendars (stock / futures / index -- NYSE:F's
535 /// 13:00 half-days, CME's 12:00 CT early closes). OTC quote streams have
536 /// no holiday template: on OANDA:XAUUSD 15m (cfd, 1800-1700 ET) the Fri
537 /// 2025-07-04 session ends at the 12:45 ET bar, yet
538 /// request.security(tickerid, "D", x) lookahead_off surfaces that day on
539 /// Sun 07-06 18:00 -- the next session's first bar -- never on the 12:45
540 /// bar; so does the Mon 2026-02-16 day ending 14:15 (on Mon 18:00) and
541 /// the week holding the 07-04 close (lab tv oanda-{jul,julw,feb,febw}
542 /// pin, ledger log-20260905t034240z-30be11fe, 2026-09-05). The engine
543 /// sets it from syminfo.type (BacktestEngine::
544 /// session_template_knows_early_close: false for forex / cfd / crypto).
545 /// Read only by the CALENDAR next-input-bar rule without a native
546 /// partition: ""/"24x7" sessions never reach that rule, the nominal
547 /// rules are untouched, and an installed native period partition
548 /// (set_native_periods) stays authoritative either way.
549 void set_early_close_completes(bool on) { early_close_completes_ = on; }
550 bool early_close_completes() const { return early_close_completes_; }
551
552 /// CALENDAR: whether `prev_ms` and `curr_ms` lie in different periods
553 /// of this aggregator -- different native periods / W-M groups when
554 /// native periods are installed, crosses_boundary on the nominal
555 /// session calendar otherwise. RATIO with late day opens installed:
556 /// whether they lie in different buckets (bucket_open_ms). Any other
557 /// RATIO, and PASSTHROUGH: false.
558 bool period_changes(int64_t prev_ms, int64_t curr_ms) const;
559
560 /// Open (Unix ms) of the target-TF bucket an input bar stamped `ms`
561 /// belongs to, on the aggregator's anchor clock (syminfo tz + session):
562 /// CALENDAR -> session_period_open_ms of the bar's D/W/M period (the
563 /// forex week opens Sunday 17:00 ET, its month on the session whose
564 /// close date is the 1st); RATIO -> session_intraday_bucket_open_ms,
565 /// the day-stamp-anchored grid bucket (the same key feed() splits on
566 /// and time("<intraday tf>") reads), from its own late day opens
567 /// (set_native_day_opens) rather than a thread's partition; PASSTHROUGH, or a
568 /// count-only ratio with no wall-clock width, -> `ms` itself. Pure
569 /// function of the configuration: it neither reads nor advances the
570 /// aggregation state, so callers may query it before feeding the bar.
571 int64_t bucket_open_ms(int64_t ms) const;
572
573 /// Timestamp of the target-TF bar OPENED by an input bar stamped `ms`
574 /// — what TradingView dates the aggregated bar, and what feed() stamps
575 /// on every bucket it starts (finding 473). RATIO -> bucket_open_ms
576 /// (the session-anchored grid open, whether or not the grid-opening
577 /// sub-bar traded: a forex 1m tape that starts at 17:04 ET still yields
578 /// the 17:00 chart bar); CALENDAR -> the day stamp of the session-day
579 /// holding `ms` (the D/W/M bar is dated by its first TRADED session-day,
580 /// so a holiday-Monday week stays Tuesday's bar, but never by a
581 /// thin-open sub-bar inside that day; on OANDA 1800-1700 the stamp is
582 /// 17:00 ET, an hour before the day trades); PASSTHROUGH -> `ms`.
583 /// Gap-free feeds whose open is the stamp are bit-identical: there the
584 /// first sub-bar IS the bucket open.
585 int64_t bar_label_ms(int64_t ms) const;
586
587private:
588 enum class Mode { PASSTHROUGH, RATIO, CALENDAR };
589
590 Mode mode_ = Mode::PASSTHROUGH;
591 int ratio_ = 1; // for RATIO mode
592 CalendarPeriod cal_period_ = CalendarPeriod::NONE; // for CALENDAR mode
593 int64_t target_seconds_ = 0; // wall-clock seconds for RATIO boundary detection
594 int64_t input_seconds_ = 0; // input bar duration (seconds), when known
595 std::string anchor_tz_ = "UTC"; // syminfo.timezone (exchange clock)
596 std::string anchor_session_; // syminfo.session ("" or "24x7" = none)
597
598 // Native period partition (set_native_periods): the stamps, and per
599 // stamp the open of the W / M group it belongs to (the stamp itself
600 // for DAY). Empty unless a native feed installed them.
601 std::vector<int64_t> native_stamps_;
602 std::vector<int64_t> native_group_open_;
603 int64_t native_last_bound_ = 0; // nominal close of the last stamp's period
604 // The same partition generated rather than stored (set_native_period_source);
605 // native_stamps_ is empty while a source is installed.
606 std::shared_ptr<PeriodSource> native_source_;
607 // RATIO: the late day opens the grid starts from (set_native_day_opens).
608 std::vector<int64_t> native_late_opens_;
609 // set_early_close_completes: the next-input-bar completion applies.
610 bool early_close_completes_ = true;
611
612 Bar current_bar_{};
613 Bar last_completed_bar_{};
614 int sub_bar_count_ = 0;
615 bool has_completed_ = false;
616 bool current_emitted_complete_ = false;
617
618 void reset_current(const Bar& bar);
619 void merge_into_current(const Bar& bar);
620 // Index of the native period holding `ms` (-1 before the first stamp).
621 int native_index(int64_t ms) const;
622 // Open of the generated partition's period holding `ms`; false where
623 // native_index would answer -1.
624 bool native_source_period(int64_t ms, int64_t& stamp) const;
625};
626
627} // namespace pineforge
NativeDayPartitionScope & operator=(const NativeDayPartitionScope &)=delete
NativeDayPartitionScope(const NativeDayPartition *p)
NativeDayPartitionScope(const NativeDayPartitionScope &)=delete
A period partition generated on demand rather than stored: what set_native_period_source installs whe...
virtual bool stamp_at(int64_t ms, int64_t &stamp, bool &last)=0
The open of the latest period at or before ms; false before the first period.
virtual int64_t last_stamp() const =0
The final period's open.
virtual std::size_t stamps_held() const =0
How many period opens the source holds in memory right now.
AggregatedBar feed(const Bar &input_bar, int64_t next_input_ms, int64_t calling_close_ms)
feed(bar, next_input_ms) with the CALLING chart bar's nominal close known (0 = unknown,...
int64_t bucket_open_ms(int64_t ms) const
Open (Unix ms) of the target-TF bucket an input bar stamped ms belongs to, on the aggregator's anchor...
AggregatedBar feed(const Bar &input_bar, int64_t next_input_ms)
feed() with the NEXT input bar's timestamp known (0 = unknown, the form above).
Bar last_completed() const
Last completed aggregated bar.
TimeframeAggregator(const std::string &target_tf, const std::string &input_tf, const std::string &tz, const std::string &session="")
Calendar/ratio aggregation anchored on a symbol clock.
CalendarPeriod calendar_period() const
The D/W/M period a CALENDAR aggregator buckets on; NONE for RATIO and PASSTHROUGH.
std::size_t native_period_stamps_held() const
Period opens this aggregator holds in memory: the stamps and group opens of a stored partition,...
bool has_native_periods() const
Whether an installed native daily feed keys this aggregator's buckets: its period partition (CALENDAR...
void set_early_close_completes(bool on)
Whether a session ending BEFORE its nominal close completes the running D/W/M bucket on that session'...
Bar current() const
Current in-progress bar.
TimeframeAggregator()
Default: passthrough mode (no aggregation).
AggregatedBar complete_pending_partial()
Finalize the pending partial bucket exactly as feed() finalizes a bucket – the sub-bars it holds ARE ...
void set_native_day_opens(std::vector< int64_t > late_opens)
A RATIO aggregator's late day opens, from the installed native daily feed (native_late_day_opens over...
bool period_changes(int64_t prev_ms, int64_t curr_ms) const
CALENDAR: whether prev_ms and curr_ms lie in different periods of this aggregator – different native ...
TimeframeAggregator(int ratio)
Ratio-based: every ratio input bars produce one output bar.
TimeframeAggregator(const std::string &target_tf, const std::string &input_tf)
Calendar-based: aggregate until day/week/month boundary.
AggregatedBar feed(const Bar &input_bar)
Feed one input bar. Returns aggregation state.
bool set_native_period_source(std::shared_ptr< PeriodSource > source)
Install a DAY partition from source: each period's group open is its stamp, as set_native_periods lea...
bool has_pending_partial() const
RATIO (fixed intraday target) only: whether the bucket in progress holds sub-bars no completion has e...
void set_native_periods(std::vector< int64_t > stamps, std::vector< int64_t > trade_instants, CalendarPeriod feed_period)
TradingView's own period partition for a CALENDAR aggregator, from the native request....
bool is_active() const
Whether aggregation is active (non-passthrough).
int64_t bar_label_ms(int64_t ms) const
Timestamp of the target-TF bar OPENED by an input bar stamped ms — what TradingView dates the aggrega...
int64_t session_trading_day_index(int64_t ms, const std::string &tz, const std::string &session)
Broker trading-day ordinal on the unmerged symbol session clock.
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_last_traded_close_ms(int64_t ms, const std::string &tz, const std::string &session, CalendarPeriod period)
Exclusive close (Unix ms) of the LAST TRADED session-day of the D/W/M bar that contains ms: DAY is se...
bool tf_is_weekly(const std::string &tf)
Definition timeframe.hpp:50
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.
bool build_native_day_partition(NativeDayPartition &out, const std::string &tz, const std::string &session, const std::vector< int64_t > &stamps, const Bar *input_bars, int n_input, const std::string &scratch_directory={})
Build the partition for the symbol clock (tz, session) from the native daily stamps and the chart's i...
int64_t session_period_close_ms(int64_t ms, const std::string &tz, const std::string &session, CalendarPeriod period)
Exclusive close (Unix ms) of the symbol's D/W/M bar that contains ms: DAY -> session-day open + sessi...
CalendarPeriod calendar_period_for(const std::string &tf)
Determine the calendar period for a target TF string.
const NativeDayPartition * active_native_day_partition()
bool tf_change(int64_t prev_ms, int64_t curr_ms, const std::string &tf)
Check if prev/curr timestamps cross a timeframe boundary.
const NativeDayPartition * set_active_native_day_partition(const NativeDayPartition *p)
The calling thread's active chart partition, read by session_day_index / session_period_open_ms / ses...
bool tf_is_daily(const std::string &tf)
Definition timeframe.hpp:46
std::vector< int64_t > native_late_day_opens(const std::string &tz, const std::string &session, const std::vector< int64_t > &stamps, const Bar *input_bars, int n_input)
The native daily stamps that open their session-day LATE: a stamp after the day stamp of the (nominal...
constexpr int64_t kMsPerDay
Definition timeframe.hpp:13
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...
constexpr int64_t kSecPerDay
Definition timeframe.hpp:12
bool crosses_boundary(int64_t prev_ms, int64_t curr_ms, CalendarPeriod period)
Check if two timestamps (Unix milliseconds) fall in different calendar periods.
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:89
bool tf_is_intraday(const std::string &tf)
Definition timeframe.hpp:41
int64_t session_day_index(int64_t ms, const std::string &tz, const std::string &session)
Ordinal of the session day containing ms (days since epoch on the session clock).
int tf_multiplier(const std::string &tf)
Extract the numeric multiplier from a TF string (e.g. "15" -> 15, "D" -> 1).
Definition timeframe.hpp:32
int64_t session_intraday_bucket_open_ms(int64_t ms, int64_t bucket_sec, const std::string &tz, const std::string &session)
Open (Unix ms) of the bucket_sec-wide intraday bucket that contains ms on the symbol's day-stamp-anch...
int native_day_partition_index(const NativeDayPartition &p, int64_t ms)
Index of the native period holding ms under p: -1 before the first stamp and at / after the last stam...
bool tf_is_seconds(const std::string &tf)
Definition timeframe.hpp:58
bool tf_is_monthly(const std::string &tf)
Definition timeframe.hpp:54
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...
detail::FeedProjection< int64_t > week_open
std::vector< int64_t > late_opens
detail::FeedProjection< int64_t > month_open
std::vector< int64_t > stamps
detail::FeedProjection< int64_t > trade_day