PineForge v0.13.1-379-g9b50973
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 "bar.hpp"
6
7namespace pineforge {
8
9// ─── Day-length constants ──────────────────────────────────────────────────────
10
11inline constexpr int64_t kSecPerDay = 86400;
12inline constexpr int64_t kMsPerDay = 86400000;
13
14// ─── AggregatedBar ─────────────────────────────────────────────────────────────
15
21
22// ─── TF string helpers ─────────────────────────────────────────────────────────
23
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
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 on the epoch grid — exactly TV's
99/// behavior for 24x7 symbols (the corpus regime). Session symbols
100/// (equities RTH, forex) anchor HTF buckets on the exchange clock instead:
101/// daily boundaries at symbol-local midnight, intraday buckets offset by the
102/// symbol's day stamp -- the session open, except OANDA's 1800-1700 metals
103/// session whose day (and TradingView's "45"/"240" grid) rolls at the 17:00
104/// ET forex roll one hour before it opens (session_day_stamp_offset_minutes
105/// in timeframe.cpp cites the pin). With tz="UTC" and session ""/"24x7"
106/// these are bit-identical to the UTC forms, so existing callers are
107/// unaffected.
108bool crosses_boundary(int64_t prev_ms, int64_t curr_ms, CalendarPeriod period,
109 const std::string& tz, const std::string& session);
110bool tf_change(int64_t prev_ms, int64_t curr_ms, const std::string& tf,
111 const std::string& tz, const std::string& session);
112
113// ─── Symbol-clock D/W/M bar anchors ────────────────────────────────────────────
114//
115// TradingView's daily bar is the SESSION day, not the UTC (nor the local
116// calendar) day: OANDA:EURUSD (America/New_York, 1700-1700) opens its daily
117// bar at 17:00 ET and its week at Sunday 17:00 ET; NASDAQ:AAPL (0930-1600)
118// opens at 09:30 ET Monday..Friday; a 24x7 UTC symbol opens at 00:00 UTC.
119// Every chart-level consumer of "the symbol's daily bar" — ta.vwap's default
120// anchor, timeframe.change("1D"), time("D")/time_close("D") — has to key on
121// this clock, the same one request.security aggregation already uses
122// (crosses_boundary / session_period_key above). The bar OPENS at the
123// session-day's DAY STAMP — the session open on every session but OANDA's
124// 1800-1700, whose D bar is stamped 17:00 ET an hour before it trades
125// (time("D") reads 17:00 ET on OANDA:XAUUSD, pin-time-hours; the session
126// open stays where the tape trades from). With tz="UTC" and an empty /
127// "24x7" session all of these reduce to plain epoch integer math,
128// bit-identical to the UTC forms the corpus pins.
129//
130// Period attribution follows TV's trading-date rule: a session that wraps
131// midnight (1700-1700) is the trading day of the date it CLOSES on (the
132// bar opening Sunday 17:00 ET is Monday's daily bar), so forex weeks open on
133// the Sunday session and a month opens on the session whose close date is
134// the 1st. Equity / 24x7 sessions close on their open date, so their weeks
135// stay Monday-partitioned and months calendar-partitioned exactly as before.
136// W/M opens are nominal (the Monday / 1st session-day); they do not consult
137// a holiday calendar.
138
139/// Ordinal of the session day containing `ms` (days since epoch on the
140/// session clock). UTC + no session: ms / kMsPerDay.
141int64_t session_day_index(int64_t ms, const std::string& tz,
142 const std::string& session);
143
144namespace internal {
145/// Broker trading-day ordinal on the unmerged symbol session clock. A native
146/// daily bar may combine holiday sessions, but each session still renews the
147/// intraday order budget. This function never reads the native D partition.
148int64_t session_trading_day_index(int64_t ms, const std::string& tz,
149 const std::string& session);
150} // namespace internal
151
152/// Open (Unix ms) of the symbol's D/W/M bar that contains `ms`: the day
153/// stamp of the period's first session-day (17:00 ET on OANDA 1800-1700,
154/// the session open elsewhere). CalendarPeriod::NONE returns `ms` unchanged.
155int64_t session_period_open_ms(int64_t ms, const std::string& tz,
156 const std::string& session,
157 CalendarPeriod period);
158
159/// Open (Unix ms) of the `bucket_sec`-wide intraday bucket that contains
160/// `ms` on the symbol's day-stamp-anchored grid: exchange-tz time since
161/// local midnight + day stamp, floored to the bucket width. This is THE
162/// intraday HTF grid — request.security's ratio buckets
163/// (TimeframeAggregator::bucket_open_ms), tf_change and
164/// time("<intraday tf>") / time_close all read it (pin-time-hours:
165/// time("60") on NYSE:F is 09:30 / 10:30 / .. / 15:30 ET, time("240") on
166/// NSE:NIFTY 09:15 / 13:15 IST, on OANDA:XAUUSD 17:00 ET + 4h*k). With
167/// tz="UTC" and session ""/"24x7" it is the epoch grid, bit-identical to
168/// the tz-less forms. bucket_sec <= 0 returns `ms`.
169int64_t session_intraday_bucket_open_ms(int64_t ms, int64_t bucket_sec,
170 const std::string& tz,
171 const std::string& session);
172
173/// The session instant a native CALENDAR chart stamp covers. A stamp inside
174/// its session (open to exclusive close) returns unchanged. A stamp in the
175/// inter-session gap -- before its session-day's open (the 1800-1700 day
176/// stamp hour) or at / after its close -- covers the session about to open
177/// -- OANDA stamps daily FX/metal bars at the 17:00 ET break under an
178/// 1800-1700 session -- and rolls forward to that session's open.
179/// ""/24x7 sessions have no gap and always return `ms` unchanged.
180int64_t session_covered_instant_ms(int64_t ms, const std::string& tz,
181 const std::string& session);
182
183/// Exclusive close (Unix ms) of the symbol's D/W/M bar that contains `ms`:
184/// DAY -> session-day open + session length (16:00 ET on equities, the next
185/// 17:00 ET on forex, next midnight on 24x7); WEEK / MONTH -> the open of
186/// the next period's first session-day. CalendarPeriod::NONE returns `ms`.
187int64_t session_period_close_ms(int64_t ms, const std::string& tz,
188 const std::string& session,
189 CalendarPeriod period);
190
191/// Exclusive close (Unix ms) of the LAST TRADED session-day of the D/W/M
192/// bar that contains `ms`: DAY is session_period_close_ms; WEEK / MONTH
193/// step back from the period's last session-day over weekend TRADING dates
194/// (Saturday / Sunday never hold a session on exchange-calendar symbols),
195/// so an equity week ends Friday 16:00 ET, a month whose last calendar day
196/// is a weekend ends on its last Friday, and the forex week ends Friday
197/// 17:00 ET (the Friday-open session is Saturday's trading date). Exchange
198/// holidays are not modelled. CalendarPeriod::NONE returns `ms`.
199int64_t session_period_last_traded_close_ms(int64_t ms, const std::string& tz,
200 const std::string& session,
201 CalendarPeriod period);
202
203// ─── Native daily partition for the CHART symbol's D period ────────────────────
204//
205// TradingView's D period on an exchange-calendar intraday chart is its own
206// daily bar, not the nominal session-day: on CME_MINI:NQ1!/ES1! 15m the
207// time("D"), ta.change(time("D")), timeframe.change("1D") and ta.vwap's
208// default daily anchor follow the exchange TRADE-DATE daily bars, so the
209// 17:00 CT reopen after a US-holiday early close stays inside the D bar that
210// opened before the holiday (2025-05-26 Memorial Day evening: the bar
211// stamped Sun 05-25 17:00 runs to Tue 05-27 16:00; 06-19 Juneteenth; the Sun
212// 07-06 after Independence Day, whose Thu 07-03 17:00 stamp runs to Mon
213// 07-07 16:00; 09-01 Labor Day; 11-27 Thanksgiving, to Fri 11-28 12:15;
214// 2026-01-19 MLK; 02-16 Presidents' Day) and the Good-Friday-eve session
215// (Thu 04-17 17:00 CT, 2025) belongs to the Wed 04-16 17:00 bar that runs to
216// the Sun 04-20 reopen -- pinned 2026-09-05 by lab tv o-cme-dayanchor-full
217// (ledger log-20260905t123531z-7fe6b95a: 255 D periods over 2025-04-01 ..
218// 2026-05-01, every start a registry daily-feed row, feed == TV data on
219// 25530/25530 closes). Every nominal session-day rule stays as it is; when
220// the run installs TradingView's own daily bars (strategy_set_native_
221// security_feed "D") on an intraday chart, the engine builds this partition
222// from their stamps -- the same partition request.security "D" evaluators
223// take (TimeframeAggregator::set_native_periods) -- and the chart-level
224// consumers below read it for the symbol's own clock (tz + session equal to
225// the partition's): the D period holding an instant is the native bar whose
226// stamp is the latest at or before it; its trade day is the session-day of
227// its last chart bar (the merged Memorial-Day bar is Tuesday's); a W / M
228// period groups the native days by the nominal week / month of their trade
229// day and opens on the group's first stamp (b29152c's request.security rule);
230// an instant before the first stamp, or at / after the LAST stamp's nominal
231// session-day close, keeps the nominal rules. Without an installed partition
232// (no native daily feed, a 1D chart, another symbol's clock) every function
233// is bit-identical to the nominal session calendar.
235 std::string tz;
236 std::string session;
237 std::vector<int64_t> stamps; // native daily stamps, strictly increasing
238 std::vector<int64_t> trade_day; // per stamp: session_day_index of its trade day
239 std::vector<int64_t> week_open; // per stamp: the W group's first stamp
240 std::vector<int64_t> month_open; // per stamp: the M group's first stamp
241 int64_t last_bound = 0; // nominal session-day close of the last stamp
242 bool empty() const { return stamps.empty(); }
243};
244
245/// Build the partition for the symbol clock (tz, session) from the native
246/// daily stamps and the chart's input bars (the trade instants: the last
247/// input bar before the next stamp, bounded by the last stamp's nominal
248/// session-day close; the stamp itself when no input bar lies in the
249/// period). Returns false -- and leaves `out` empty -- for no stamps or
250/// non-increasing stamps.
252 const std::string& tz,
253 const std::string& session,
254 const std::vector<int64_t>& stamps,
255 const Bar* input_bars, int n_input);
256
257/// Index of the native period holding `ms` under `p`: -1 before the first
258/// stamp and at / after the last stamp's nominal session-day close.
260
261/// The calling thread's active chart partition, read by session_day_index /
262/// session_period_open_ms / session_period_close_ms /
263/// session_period_last_traded_close_ms / crosses_boundary (and so by
264/// tf_change, the symbol-clock pine_time forms and ta::VWAP's session
265/// anchor) whenever their (tz, session) equal the partition's. nullptr (the
266/// default) leaves every rule nominal. set_ returns the previous pointer;
267/// BacktestEngine::run installs its chart partition through
268/// NativeDayPartitionScope for exactly the run's bar loop.
271
282
283// Feature probe: the chart-level native daily partition above exists.
284#define PINEFORGE_HAS_NATIVE_DAY_PARTITION_V1 1
285
286// ─── TimeframeAggregator ───────────────────────────────────────────────────────
287
289public:
290 /// Default: passthrough mode (no aggregation).
292
293 /// Ratio-based: every `ratio` input bars produce one output bar.
294 explicit TimeframeAggregator(int ratio);
295
296 /// Calendar-based: aggregate until day/week/month boundary.
297 TimeframeAggregator(const std::string& target_tf,
298 const std::string& input_tf);
299
300 /// Calendar/ratio aggregation anchored on a symbol clock. tz is the Pine
301 /// syminfo.timezone (exchange tz); session the Pine session string
302 /// ("0930-1600", "24x7", ...). Defaults reproduce the UTC/24x7 forms
303 /// bit-for-bit, so every existing construction site compiles unchanged.
304 TimeframeAggregator(const std::string& target_tf,
305 const std::string& input_tf,
306 const std::string& tz,
307 const std::string& session = "");
308
309 /// Feed one input bar. Returns aggregation state.
310 AggregatedBar feed(const Bar& input_bar);
311
312 /// feed() with the NEXT input bar's timestamp known (0 = unknown, the
313 /// form above). A historical run holds its whole feed, and on a declared
314 /// exchange session TradingView finalizes a D/W/M bar on the period's
315 /// actual last chart bar whatever the nominal calendar says that bar is:
316 /// the 12:45 ET bar of a 13:00 half-day (NYSE:F Fri 2025-11-28), the
317 /// Thursday 15:45 bar before a holiday Friday, the 15:45 CT bar closing
318 /// an overnight CME session-day (lab tv wm-security-buckets tapes,
319 /// 2026-09-05). CALENDAR mode therefore also completes the running
320 /// bucket when the next input bar opens a new period. RATIO and
321 /// PASSTHROUGH ignore the hint, and so do ""/"24x7" sessions (a data
322 /// hole before a 24x7 midnight is not an early close), so every caller
323 /// passing 0 and every session-less feed stay bit-identical. The rule
324 /// applies where TradingView's session template knows the early close
325 /// -- exchange calendars; set_early_close_completes(false) switches a
326 /// symbol whose template does not (OANDA's cfd / forex streams) back to
327 /// the nominal close and the lazy completion on the next period's first
328 /// bar.
329 AggregatedBar feed(const Bar& input_bar, int64_t next_input_ms);
330
331 /// feed(bar, next_input_ms) with the CALLING chart bar's nominal close
332 /// known (0 = unknown, the forms above): TradingView's time_close of
333 /// the chart bar this input bar belongs to -- the input bar's own end
334 /// on a single-feed run, the native D/W/M chart bar's session close on
335 /// the split-feed path, where a finer auxiliary slice advances
336 /// request.security. Read only by the OTC rule
337 /// (set_early_close_completes(false)): TradingView surfaces a D/W/M
338 /// value on the chart bar whose time_close reaches the period's
339 /// nominal close, and the daily bar of an early-close day keeps its
340 /// nominal time_close -- on the OANDA:XAUUSD 1D chart the Fri
341 /// 2025-07-04 bar (data to 12:45 ET) still closes at 17:00 ET, so
342 /// request.security(tickerid, "W", x) advances on it (cW 3336.61, the
343 /// early close) exactly as on every regular Friday, and "D" is the
344 /// chart bar itself there and on Mon 2026-02-16 (14:15 early close);
345 /// on the 15m chart the 12:45 / 14:15 bar's own time_close falls short
346 /// and the value waits for the next session's first bar (lab tv
347 /// oanda1d-{jul,feb,novm,decm} beside the oanda 15m pin, 2026-09-05).
348 /// So on an OTC stream a bucket the next input bar leaves completes on
349 /// this input bar iff calling_close_ms reaches the period's nominal
350 /// close (session_period_last_traded_close_ms); with 0 it waits, as
351 /// before. Exchange kinds (early_close_completes) ignore the hint: the
352 /// actual last bar completes the period either way.
353 AggregatedBar feed(const Bar& input_bar, int64_t next_input_ms,
354 int64_t calling_close_ms);
355
356 /// Current in-progress bar.
357 Bar current() const;
358
359 /// RATIO (fixed intraday target) only: whether the bucket in progress
360 /// holds sub-bars no completion has emitted yet -- the tail of a chart
361 /// bar whose last sub-bars never reached the bucket's count, real end
362 /// or session close (the 21:57Z 3m bucket of OANDA:XAUUSD's Thanksgiving
363 /// 2025-11-26 session holds only the 21:59Z minute). CALENDAR and
364 /// PASSTHROUGH answer false; so does a count-only ratio with no
365 /// wall-clock width.
367
368 /// Finalize the pending partial bucket exactly as feed() finalizes a
369 /// bucket -- the sub-bars it holds ARE the bucket -- and return it
370 /// complete. TradingView surfaces the LAST intrabar of a chart bar at
371 /// that bar's close whatever its minute count (lab tv
372 /// dca-ltf-last-intrabar, 2026-09-05), so a request.security evaluator
373 /// served by a finer feed calls this when its calling chart bar
374 /// completes. The bucket is emitted once: the next boundary resets it
375 /// without re-emitting (current_emitted_complete), and a later sub-bar
376 /// of the same bucket merges without completing it again, as after any
377 /// early completion. Without a pending partial nothing changes and the
378 /// current bar is returned incomplete.
380
381 /// Last completed aggregated bar.
383
384 /// Whether aggregation is active (non-passthrough).
385 bool is_active() const;
386
387 /// The D/W/M period a CALENDAR aggregator buckets on; NONE for RATIO
388 /// and PASSTHROUGH.
390
391 /// TradingView's own period partition for a CALENDAR aggregator, from
392 /// the native request.security feed of the aggregated timeframe: the
393 /// stamps (Unix ms, strictly increasing) are the exchange's native bars'
394 /// opens, and trade_instants[k] is an instant inside the session-day the
395 /// k-th native bar COMPLETES on -- its last input bar (the stamp itself
396 /// when no input bar falls in the period), the trade date TradingView
397 /// files the bar under. Installed, they replace the nominal session
398 /// calendar as the period key: an input bar belongs to the native bar
399 /// whose stamp is the latest at or before it, so a D period is exactly
400 /// that native bar's span. On CME_MINI:ES1! a holiday session that
401 /// pauses at 12:00 CT and reopens at 17:00 the same day (Labor Day,
402 /// Thanksgiving, Independence Day) is folded by TradingView into the
403 /// NEXT trade date's daily bar with no stamp of its own (lab tv esd pin,
404 /// 2026-09-05: the Sun 08-31 17:00 stamp runs to Tue 09-02 15:45 and is
405 /// Tuesday's bar), so no D period closes at the pause, none opens at the
406 /// reopen, and a data hole inside a native day is not a close. A W / M
407 /// period groups the native days by the nominal week / month of their
408 /// trade date (the holiday session belongs to the next trade date, so to
409 /// its week). bar_label_ms is the stamp of the bar's native period,
410 /// bucket_open_ms the group's first stamp, period_changes compares
411 /// those, and feed(bar, next_input_ms) completes the running bucket on
412 /// the last input bar before the next native stamp. The partition covers
413 /// the feed: an input bar before the first stamp keeps the nominal key,
414 /// and so does one at or after the LAST stamp's nominal period close
415 /// (session_period_close_ms of feed_period, the feed's own timeframe --
416 /// DAY for a daily feed), since without a next stamp nothing proves the
417 /// last native bar reaches further; a chart day the feed does not hold
418 /// stays a nominal session-day bucket with no native bar to substitute.
419 /// Empty vectors (the default) leave every nominal rule bit-identical;
420 /// RATIO / PASSTHROUGH ignore the call. Mismatched sizes or
421 /// non-increasing stamps install nothing.
422 void set_native_periods(std::vector<int64_t> stamps,
423 std::vector<int64_t> trade_instants,
424 CalendarPeriod feed_period);
425 bool has_native_periods() const { return !native_stamps_.empty(); }
426
427 /// Whether a session ending BEFORE its nominal close completes the
428 /// running D/W/M bucket on that session's actual last input bar
429 /// (feed(bar, next_input_ms) seeing the next input bar open a new
430 /// period) -- true, the default -- or the bucket waits for the period's
431 /// nominal close (the 16:45 bar reaching 17:00, the last traded
432 /// session-day's close) and, when no input bar reaches it, completes
433 /// lazily on the next period's first bar (false) -- unless the CALLING
434 /// chart bar's nominal close reaches the period's (feed(bar,
435 /// next_input_ms, calling_close_ms): the 1D chart's early-close daily
436 /// bar, whose time_close stays 17:00 ET). TradingView finalizes
437 /// on the actual last bar only where its session template carries the
438 /// early close: exchange calendars (stock / futures / index -- NYSE:F's
439 /// 13:00 half-days, CME's 12:00 CT early closes). OTC quote streams have
440 /// no holiday template: on OANDA:XAUUSD 15m (cfd, 1800-1700 ET) the Fri
441 /// 2025-07-04 session ends at the 12:45 ET bar, yet
442 /// request.security(tickerid, "D", x) lookahead_off surfaces that day on
443 /// Sun 07-06 18:00 -- the next session's first bar -- never on the 12:45
444 /// bar; so does the Mon 2026-02-16 day ending 14:15 (on Mon 18:00) and
445 /// the week holding the 07-04 close (lab tv oanda-{jul,julw,feb,febw}
446 /// pin, ledger log-20260905t034240z-30be11fe, 2026-09-05). The engine
447 /// sets it from syminfo.type (BacktestEngine::
448 /// session_template_knows_early_close: false for forex / cfd / crypto).
449 /// Read only by the CALENDAR next-input-bar rule without a native
450 /// partition: ""/"24x7" sessions never reach that rule, the nominal
451 /// rules are untouched, and an installed native period partition
452 /// (set_native_periods) stays authoritative either way.
453 void set_early_close_completes(bool on) { early_close_completes_ = on; }
454 bool early_close_completes() const { return early_close_completes_; }
455
456 /// CALENDAR: whether `prev_ms` and `curr_ms` lie in different periods
457 /// of this aggregator -- different native periods / W-M groups when
458 /// native periods are installed, crosses_boundary on the nominal
459 /// session calendar otherwise. RATIO / PASSTHROUGH: false.
460 bool period_changes(int64_t prev_ms, int64_t curr_ms) const;
461
462 /// Open (Unix ms) of the target-TF bucket an input bar stamped `ms`
463 /// belongs to, on the aggregator's anchor clock (syminfo tz + session):
464 /// CALENDAR -> session_period_open_ms of the bar's D/W/M period (the
465 /// forex week opens Sunday 17:00 ET, its month on the session whose
466 /// close date is the 1st); RATIO -> session_intraday_bucket_open_ms,
467 /// the day-stamp-anchored grid bucket (the same key feed() splits on
468 /// and time("<intraday tf>") reads); PASSTHROUGH, or a
469 /// count-only ratio with no wall-clock width, -> `ms` itself. Pure
470 /// function of the configuration: it neither reads nor advances the
471 /// aggregation state, so callers may query it before feeding the bar.
472 int64_t bucket_open_ms(int64_t ms) const;
473
474 /// Timestamp of the target-TF bar OPENED by an input bar stamped `ms`
475 /// — what TradingView dates the aggregated bar, and what feed() stamps
476 /// on every bucket it starts (finding 473). RATIO -> bucket_open_ms
477 /// (the session-anchored grid open, whether or not the grid-opening
478 /// sub-bar traded: a forex 1m tape that starts at 17:04 ET still yields
479 /// the 17:00 chart bar); CALENDAR -> the day stamp of the session-day
480 /// holding `ms` (the D/W/M bar is dated by its first TRADED session-day,
481 /// so a holiday-Monday week stays Tuesday's bar, but never by a
482 /// thin-open sub-bar inside that day; on OANDA 1800-1700 the stamp is
483 /// 17:00 ET, an hour before the day trades); PASSTHROUGH -> `ms`.
484 /// Gap-free feeds whose open is the stamp are bit-identical: there the
485 /// first sub-bar IS the bucket open.
486 int64_t bar_label_ms(int64_t ms) const;
487
488private:
489 enum class Mode { PASSTHROUGH, RATIO, CALENDAR };
490
491 Mode mode_ = Mode::PASSTHROUGH;
492 int ratio_ = 1; // for RATIO mode
493 CalendarPeriod cal_period_ = CalendarPeriod::NONE; // for CALENDAR mode
494 int64_t target_seconds_ = 0; // wall-clock seconds for RATIO boundary detection
495 int64_t input_seconds_ = 0; // input bar duration (seconds), when known
496 std::string anchor_tz_ = "UTC"; // syminfo.timezone (exchange clock)
497 std::string anchor_session_; // syminfo.session ("" or "24x7" = none)
498
499 // Native period partition (set_native_periods): the stamps, and per
500 // stamp the open of the W / M group it belongs to (the stamp itself
501 // for DAY). Empty unless a native feed installed them.
502 std::vector<int64_t> native_stamps_;
503 std::vector<int64_t> native_group_open_;
504 int64_t native_last_bound_ = 0; // nominal close of the last stamp's period
505 // set_early_close_completes: the next-input-bar completion applies.
506 bool early_close_completes_ = true;
507
508 Bar current_bar_{};
509 Bar last_completed_bar_{};
510 int sub_bar_count_ = 0;
511 bool has_completed_ = false;
512 bool current_emitted_complete_ = false;
513
514 void reset_current(const Bar& bar);
515 void merge_into_current(const Bar& bar);
516 // Index of the native period holding `ms` (-1 before the first stamp).
517 int native_index(int64_t ms) const;
518};
519
520} // namespace pineforge
NativeDayPartitionScope & operator=(const NativeDayPartitionScope &)=delete
NativeDayPartitionScope(const NativeDayPartition *p)
NativeDayPartitionScope(const NativeDayPartitionScope &)=delete
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.
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 ...
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 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.
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
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)
Build the partition for the symbol clock (tz, session) from the native daily stamps and the chart's i...
constexpr int64_t kMsPerDay
Definition timeframe.hpp:12
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:11
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: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...
std::vector< int64_t > stamps
std::vector< int64_t > trade_day
std::vector< int64_t > week_open
std::vector< int64_t > month_open