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