oakscriptjs 0.11.0 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/dist/cjs/array/index.d.ts +8 -2
  2. package/dist/cjs/array/index.d.ts.map +1 -1
  3. package/dist/cjs/array/index.js +2 -1
  4. package/dist/cjs/array/index.js.map +2 -2
  5. package/dist/cjs/box/index.js +6 -6
  6. package/dist/cjs/box/index.js.map +2 -2
  7. package/dist/cjs/callsite/index.d.ts.map +1 -1
  8. package/dist/cjs/callsite/index.js +6 -3
  9. package/dist/cjs/callsite/index.js.map +2 -2
  10. package/dist/cjs/drawing/registry.d.ts +15 -6
  11. package/dist/cjs/drawing/registry.d.ts.map +1 -1
  12. package/dist/cjs/drawing/registry.js +16 -2
  13. package/dist/cjs/drawing/registry.js.map +2 -2
  14. package/dist/cjs/index.d.ts +4 -3
  15. package/dist/cjs/index.d.ts.map +1 -1
  16. package/dist/cjs/index.js +7 -1
  17. package/dist/cjs/index.js.map +2 -2
  18. package/dist/cjs/input.d.ts +9 -1
  19. package/dist/cjs/input.d.ts.map +1 -1
  20. package/dist/cjs/input.js +16 -0
  21. package/dist/cjs/input.js.map +2 -2
  22. package/dist/cjs/label/index.js +3 -3
  23. package/dist/cjs/label/index.js.map +2 -2
  24. package/dist/cjs/line/index.d.ts +10 -27
  25. package/dist/cjs/line/index.d.ts.map +1 -1
  26. package/dist/cjs/line/index.js +10 -27
  27. package/dist/cjs/line/index.js.map +2 -2
  28. package/dist/cjs/linefill/index.d.ts +3 -0
  29. package/dist/cjs/linefill/index.d.ts.map +1 -1
  30. package/dist/cjs/linefill/index.js +15 -1
  31. package/dist/cjs/linefill/index.js.map +3 -3
  32. package/dist/cjs/runtime/adapters/SimpleInputAdapter.js +1 -1
  33. package/dist/cjs/runtime/adapters/SimpleInputAdapter.js.map +2 -2
  34. package/dist/cjs/runtime/index.d.ts +1 -1
  35. package/dist/cjs/runtime/index.d.ts.map +1 -1
  36. package/dist/cjs/runtime/index.js +1 -0
  37. package/dist/cjs/runtime/index.js.map +2 -2
  38. package/dist/cjs/runtime/inputs.d.ts +9 -0
  39. package/dist/cjs/runtime/inputs.d.ts.map +1 -1
  40. package/dist/cjs/runtime/inputs.js +8 -0
  41. package/dist/cjs/runtime/inputs.js.map +2 -2
  42. package/dist/cjs/runtime/types.d.ts +1 -1
  43. package/dist/cjs/runtime/types.d.ts.map +1 -1
  44. package/dist/cjs/runtime/types.js.map +1 -1
  45. package/dist/cjs/script/index.d.ts +66 -13
  46. package/dist/cjs/script/index.d.ts.map +1 -1
  47. package/dist/cjs/script/index.js +139 -35
  48. package/dist/cjs/script/index.js.map +3 -3
  49. package/dist/cjs/security/other.d.ts +92 -0
  50. package/dist/cjs/security/other.d.ts.map +1 -0
  51. package/dist/cjs/security/other.js +164 -0
  52. package/dist/cjs/security/other.js.map +7 -0
  53. package/dist/cjs/session/bars.d.ts +10 -1
  54. package/dist/cjs/session/bars.d.ts.map +1 -1
  55. package/dist/cjs/session/bars.js +16 -2
  56. package/dist/cjs/session/bars.js.map +2 -2
  57. package/dist/cjs/session/calendar.d.ts +4 -1
  58. package/dist/cjs/session/calendar.d.ts.map +1 -1
  59. package/dist/cjs/session/calendar.js +12 -7
  60. package/dist/cjs/session/calendar.js.map +2 -2
  61. package/dist/cjs/session/context.d.ts +22 -5
  62. package/dist/cjs/session/context.d.ts.map +1 -1
  63. package/dist/cjs/session/context.js +67 -6
  64. package/dist/cjs/session/context.js.map +2 -2
  65. package/dist/cjs/str/floatingdecimal.d.ts +31 -0
  66. package/dist/cjs/str/floatingdecimal.d.ts.map +1 -0
  67. package/dist/cjs/str/floatingdecimal.js +298 -0
  68. package/dist/cjs/str/floatingdecimal.js.map +7 -0
  69. package/dist/cjs/str/index.d.ts +4 -3
  70. package/dist/cjs/str/index.d.ts.map +1 -1
  71. package/dist/cjs/str/index.js +1 -1
  72. package/dist/cjs/str/index.js.map +2 -2
  73. package/dist/cjs/str/numberformat.d.ts +6 -4
  74. package/dist/cjs/str/numberformat.d.ts.map +1 -1
  75. package/dist/cjs/str/numberformat.js +3 -2
  76. package/dist/cjs/str/numberformat.js.map +2 -2
  77. package/dist/cjs/ta/index.d.ts +64 -45
  78. package/dist/cjs/ta/index.d.ts.map +1 -1
  79. package/dist/cjs/ta/index.js +140 -126
  80. package/dist/cjs/ta/index.js.map +3 -3
  81. package/dist/cjs/ta/limits.d.ts +11 -0
  82. package/dist/cjs/ta/limits.d.ts.map +1 -1
  83. package/dist/cjs/ta/limits.js +15 -1
  84. package/dist/cjs/ta/limits.js.map +2 -2
  85. package/dist/cjs/ta/running-sum.js +1 -1
  86. package/dist/cjs/ta/running-sum.js.map +2 -2
  87. package/dist/cjs/ta-series.d.ts +1 -1
  88. package/dist/cjs/ta-series.d.ts.map +1 -1
  89. package/dist/cjs/ta-series.js +1 -1
  90. package/dist/cjs/ta-series.js.map +2 -2
  91. package/dist/cjs/table/index.d.ts +10 -3
  92. package/dist/cjs/table/index.d.ts.map +1 -1
  93. package/dist/cjs/table/index.js +11 -1
  94. package/dist/cjs/table/index.js.map +3 -3
  95. package/dist/cjs/time/session.d.ts +9 -0
  96. package/dist/cjs/time/session.d.ts.map +1 -1
  97. package/dist/cjs/time/session.js +42 -14
  98. package/dist/cjs/time/session.js.map +3 -3
  99. package/dist/cjs/time/timezone.d.ts +6 -1
  100. package/dist/cjs/time/timezone.d.ts.map +1 -1
  101. package/dist/cjs/time/timezone.js +2 -2
  102. package/dist/cjs/time/timezone.js.map +2 -2
  103. package/dist/cjs/types/metadata.d.ts +1 -1
  104. package/dist/cjs/types/metadata.d.ts.map +1 -1
  105. package/dist/cjs/types/metadata.js.map +1 -1
  106. package/dist/esm/array/index.d.ts +8 -2
  107. package/dist/esm/array/index.d.ts.map +1 -1
  108. package/dist/esm/array/index.js +12 -3
  109. package/dist/esm/array/index.js.map +1 -1
  110. package/dist/esm/box/index.js +6 -6
  111. package/dist/esm/box/index.js.map +1 -1
  112. package/dist/esm/callsite/index.d.ts.map +1 -1
  113. package/dist/esm/callsite/index.js +6 -3
  114. package/dist/esm/callsite/index.js.map +1 -1
  115. package/dist/esm/drawing/registry.d.ts +15 -6
  116. package/dist/esm/drawing/registry.d.ts.map +1 -1
  117. package/dist/esm/drawing/registry.js +30 -7
  118. package/dist/esm/drawing/registry.js.map +1 -1
  119. package/dist/esm/index.d.ts +4 -3
  120. package/dist/esm/index.d.ts.map +1 -1
  121. package/dist/esm/index.js +8 -5
  122. package/dist/esm/index.js.map +1 -1
  123. package/dist/esm/input.d.ts +9 -1
  124. package/dist/esm/input.d.ts.map +1 -1
  125. package/dist/esm/input.js +16 -0
  126. package/dist/esm/input.js.map +1 -1
  127. package/dist/esm/label/index.js +3 -3
  128. package/dist/esm/label/index.js.map +1 -1
  129. package/dist/esm/line/index.d.ts +10 -27
  130. package/dist/esm/line/index.d.ts.map +1 -1
  131. package/dist/esm/line/index.js +20 -64
  132. package/dist/esm/line/index.js.map +1 -1
  133. package/dist/esm/linefill/index.d.ts +3 -0
  134. package/dist/esm/linefill/index.d.ts.map +1 -1
  135. package/dist/esm/linefill/index.js +20 -1
  136. package/dist/esm/linefill/index.js.map +1 -1
  137. package/dist/esm/runtime/adapters/SimpleInputAdapter.js +1 -1
  138. package/dist/esm/runtime/adapters/SimpleInputAdapter.js.map +1 -1
  139. package/dist/esm/runtime/index.d.ts +1 -1
  140. package/dist/esm/runtime/index.d.ts.map +1 -1
  141. package/dist/esm/runtime/index.js +1 -1
  142. package/dist/esm/runtime/index.js.map +1 -1
  143. package/dist/esm/runtime/inputs.d.ts +9 -0
  144. package/dist/esm/runtime/inputs.d.ts.map +1 -1
  145. package/dist/esm/runtime/inputs.js +15 -0
  146. package/dist/esm/runtime/inputs.js.map +1 -1
  147. package/dist/esm/runtime/types.d.ts +1 -1
  148. package/dist/esm/runtime/types.d.ts.map +1 -1
  149. package/dist/esm/script/index.d.ts +66 -13
  150. package/dist/esm/script/index.d.ts.map +1 -1
  151. package/dist/esm/script/index.js +187 -54
  152. package/dist/esm/script/index.js.map +1 -1
  153. package/dist/esm/security/other.d.ts +92 -0
  154. package/dist/esm/security/other.d.ts.map +1 -0
  155. package/dist/esm/security/other.js +211 -0
  156. package/dist/esm/security/other.js.map +1 -0
  157. package/dist/esm/session/bars.d.ts +10 -1
  158. package/dist/esm/session/bars.d.ts.map +1 -1
  159. package/dist/esm/session/bars.js +26 -1
  160. package/dist/esm/session/bars.js.map +1 -1
  161. package/dist/esm/session/calendar.d.ts +4 -1
  162. package/dist/esm/session/calendar.d.ts.map +1 -1
  163. package/dist/esm/session/calendar.js +23 -7
  164. package/dist/esm/session/calendar.js.map +1 -1
  165. package/dist/esm/session/context.d.ts +22 -5
  166. package/dist/esm/session/context.d.ts.map +1 -1
  167. package/dist/esm/session/context.js +86 -10
  168. package/dist/esm/session/context.js.map +1 -1
  169. package/dist/esm/str/floatingdecimal.d.ts +31 -0
  170. package/dist/esm/str/floatingdecimal.d.ts.map +1 -0
  171. package/dist/esm/str/floatingdecimal.js +272 -0
  172. package/dist/esm/str/floatingdecimal.js.map +1 -0
  173. package/dist/esm/str/index.d.ts +4 -3
  174. package/dist/esm/str/index.d.ts.map +1 -1
  175. package/dist/esm/str/index.js +6 -5
  176. package/dist/esm/str/index.js.map +1 -1
  177. package/dist/esm/str/numberformat.d.ts +6 -4
  178. package/dist/esm/str/numberformat.d.ts.map +1 -1
  179. package/dist/esm/str/numberformat.js +9 -5
  180. package/dist/esm/str/numberformat.js.map +1 -1
  181. package/dist/esm/ta/index.d.ts +64 -45
  182. package/dist/esm/ta/index.d.ts.map +1 -1
  183. package/dist/esm/ta/index.js +198 -228
  184. package/dist/esm/ta/index.js.map +1 -1
  185. package/dist/esm/ta/limits.d.ts +11 -0
  186. package/dist/esm/ta/limits.d.ts.map +1 -1
  187. package/dist/esm/ta/limits.js +27 -2
  188. package/dist/esm/ta/limits.js.map +1 -1
  189. package/dist/esm/ta/running-sum.js +1 -1
  190. package/dist/esm/ta/running-sum.js.map +1 -1
  191. package/dist/esm/ta-series.d.ts +1 -1
  192. package/dist/esm/ta-series.d.ts.map +1 -1
  193. package/dist/esm/ta-series.js +2 -1
  194. package/dist/esm/ta-series.js.map +1 -1
  195. package/dist/esm/table/index.d.ts +10 -3
  196. package/dist/esm/table/index.d.ts.map +1 -1
  197. package/dist/esm/table/index.js +23 -3
  198. package/dist/esm/table/index.js.map +1 -1
  199. package/dist/esm/time/session.d.ts +9 -0
  200. package/dist/esm/time/session.d.ts.map +1 -1
  201. package/dist/esm/time/session.js +53 -15
  202. package/dist/esm/time/session.js.map +1 -1
  203. package/dist/esm/time/timezone.d.ts +6 -1
  204. package/dist/esm/time/timezone.d.ts.map +1 -1
  205. package/dist/esm/time/timezone.js +8 -2
  206. package/dist/esm/time/timezone.js.map +1 -1
  207. package/dist/esm/types/metadata.d.ts +1 -1
  208. package/dist/esm/types/metadata.d.ts.map +1 -1
  209. package/package.json +1 -1
  210. package/src/array/index.ts +11 -3
  211. package/src/box/index.ts +6 -6
  212. package/src/callsite/index.ts +6 -3
  213. package/src/drawing/registry.ts +30 -7
  214. package/src/index.ts +16 -3
  215. package/src/input.ts +18 -1
  216. package/src/label/index.ts +3 -3
  217. package/src/line/index.ts +20 -68
  218. package/src/linefill/index.ts +20 -1
  219. package/src/runtime/adapters/SimpleInputAdapter.ts +1 -1
  220. package/src/runtime/index.ts +1 -0
  221. package/src/runtime/inputs.ts +16 -0
  222. package/src/runtime/types.ts +1 -1
  223. package/src/script/index.ts +262 -58
  224. package/src/security/other.ts +269 -0
  225. package/src/session/bars.ts +24 -1
  226. package/src/session/calendar.ts +26 -7
  227. package/src/session/context.ts +104 -10
  228. package/src/str/floatingdecimal.ts +268 -0
  229. package/src/str/index.ts +6 -5
  230. package/src/str/numberformat.ts +11 -6
  231. package/src/ta/index.ts +200 -225
  232. package/src/ta/limits.ts +28 -1
  233. package/src/ta/running-sum.ts +1 -1
  234. package/src/ta-series.ts +3 -2
  235. package/src/table/index.ts +22 -3
  236. package/src/time/session.ts +58 -16
  237. package/src/time/timezone.ts +9 -2
  238. package/src/types/metadata.ts +1 -1
@@ -0,0 +1,269 @@
1
+ /**
2
+ * PineScript `request.security(symbol, timeframe, expression, gaps, lookahead)` of another symbol: which bar of the
3
+ * other symbol each chart bar shows (#179). The bars of the other symbol are given by the caller, already on the
4
+ * requested timeframe; the expression is computed on them and its values are mapped to the chart bars with the index
5
+ * given here. The script API does this with the `bars` accessor of its chart context.
6
+ *
7
+ * PineScript rules (measured on stocks, crypto and an exchange whose trading day starts on the previous UTC day; the
8
+ * chart timeframe, D, W and M on intraday and daily charts; the four gaps / lookahead combinations):
9
+ * - lookahead off: the last bar of the other symbol whose close time is at or before the close time of the chart bar
10
+ * (a daily stock bar closes at the session end, a crypto daily bar at 00:00 UTC: a stock chart shows the crypto
11
+ * bar of the day before, a crypto chart the stock bar of the same day)
12
+ * - lookahead on, intraday chart or intraday request: the last bar of the other symbol that opened at or before the
13
+ * open of the chart bar
14
+ * - lookahead on, D / W / M chart and request: the last bar of the other symbol whose period starts at or before the trading day
15
+ * of the chart bar: the trading day of a D bar, the Monday of a W bar, the first day of the month of an M bar, each
16
+ * in the exchange time zone of its symbol (a week that opens on a Tuesday after a holiday still starts on Monday)
17
+ * - before the first bar of the other symbol: na; on chart bars where the other symbol has no bar (weekends,
18
+ * holidays, closed hours), gaps off repeats the last value
19
+ * - gaps on: a value only on the chart bar where the shown bar changes (and on the first chart bar), na on the others
20
+ * - the last bar while it is open (`lastBarConfirmed` false) with gaps off: the realtime update shows the bar given by
21
+ * lookahead on (the open bar of a higher timeframe), with lookahead off as well
22
+ *
23
+ * Close times, trading days and time zones come from the exchange calendars of the chart and of the other symbol
24
+ * (time zone and session, read by the session functions). Without them:
25
+ * - a symbol that trades every day (bars on Saturdays or Sundays; W bars at Monday 00:00 UTC; M bars on the first day
26
+ * at 00:00 UTC) is on the "24x7" UTC calendar
27
+ * - another symbol whose bars open where the chart calendar starts its periods is on the calendar of the chart
28
+ * - else the UTC rule: an intraday bar closes one bar interval after its open (or at the next bar), a D / W / M bar at
29
+ * the end of the last weekday (UTC) of its day / week / month; the trading day is the UTC date of the bar
30
+ * The fallback is exact between symbols of the same calendar and with 24x7 symbols; between a stock and a 24x7 symbol,
31
+ * or an exchange whose trading day starts on the previous UTC day, it needs the session of the stock.
32
+ *
33
+ * @module security/other
34
+ */
35
+
36
+ import * as timeframe from '../timeframe/index.js';
37
+ import { chartSessions, periodClose, periodTime, tradingDayTime, type SessionContext } from '../session/context.js';
38
+ import type { SessionSpec } from '../session/calendar.js';
39
+
40
+ const DAY = 86_400_000;
41
+
42
+ /** Exchange calendar of a symbol: time zone and session (hours, corrections, holidays). */
43
+ export interface SymbolCalendar {
44
+ timezone?: string;
45
+ session?: SessionSpec | string;
46
+ }
47
+
48
+ /** Options of {@link securityOther}. */
49
+ export interface OtherSymbolOptions {
50
+ /** PineScript `barmerge.lookahead_on`; default false */
51
+ lookahead?: boolean;
52
+ /** PineScript `barmerge.gaps_on`; default false */
53
+ gaps?: boolean;
54
+ /** Calendar of the other symbol; see the module notes for the fallback */
55
+ other?: SymbolCalendar;
56
+ }
57
+
58
+ /** The chart: its timeframe, calendar, time unit and whether its last bar is closed. */
59
+ export interface OtherSymbolChart extends SymbolCalendar {
60
+ /** Chart timeframe ("60", "1D"...) */
61
+ timeframe: string;
62
+ /** Unit of the bar times: 's' (default) or 'ms' */
63
+ timeUnit?: 's' | 'ms';
64
+ /** false: the last chart bar is still open (realtime rule) */
65
+ lastBarConfirmed?: boolean;
66
+ }
67
+
68
+ interface Zone {
69
+ timezone: string;
70
+ session: SessionSpec | string;
71
+ }
72
+
73
+ const calendars = new Map<string, SessionContext | null>();
74
+
75
+ /** The session context of a zone and a timeframe (bar times in ms); null when the session cannot be read. */
76
+ function calendarOf(zone: Zone | null, tf: string): SessionContext | null {
77
+ if (!zone) return null;
78
+ const key = `${zone.timezone}|${typeof zone.session === 'string' ? zone.session : JSON.stringify(zone.session)}|${tf}`;
79
+ let context = calendars.get(key);
80
+ if (context === undefined) {
81
+ context = { timeframe: tf, timezone: zone.timezone, session: zone.session, timeUnit: 'ms' };
82
+ try {
83
+ chartSessions(context);
84
+ } catch {
85
+ context = null;
86
+ }
87
+ if (calendars.size > 64) calendars.clear();
88
+ calendars.set(key, context);
89
+ }
90
+ return context;
91
+ }
92
+
93
+ const UTC_24X7: Zone = { timezone: 'Etc/UTC', session: '24x7' };
94
+
95
+ /** True when bars of timeframe `tf` opening at `times` (ms) are those of a symbol that trades every day in UTC. */
96
+ function tradesEveryDay(times: number[], tf: string): boolean {
97
+ if (times.length === 0) return false;
98
+ const info = timeframe.info(tf);
99
+ if (info.isweekly) return times.every((t) => t % DAY === 0 && new Date(t).getUTCDay() === 1);
100
+ if (info.ismonthly) return times.every((t) => t % DAY === 0 && new Date(t).getUTCDate() === 1);
101
+ return times.some((t) => {
102
+ const wd = new Date(t).getUTCDay();
103
+ return wd === 0 || wd === 6;
104
+ });
105
+ }
106
+
107
+ /** True when most of the last bars open at the start of a period of `tf` on the calendar `zone`. */
108
+ function onCalendar(times: number[], tf: string, zone: Zone | null): boolean {
109
+ const calendar = calendarOf(zone, tf);
110
+ if (!calendar || times.length === 0) return false;
111
+ const last = times.slice(-50);
112
+ return last.filter((t) => periodTime(t, tf, calendar) === t).length >= 0.9 * last.length;
113
+ }
114
+
115
+ const utcDay = (t: number): number => Math.floor(t / DAY) * DAY;
116
+ /** Monday 00:00 UTC of the week of the day `day` (00:00 UTC, ms). */
117
+ const weekStart = (day: number): number => day - ((new Date(day).getUTCDay() + 6) % 7) * DAY;
118
+ const monthStart = (day: number): number => {
119
+ const d = new Date(day);
120
+ return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1);
121
+ };
122
+
123
+ /** 00:00 UTC after the last weekday before `end` (00:00 UTC, ms): the end of a period of a Monday-to-Friday symbol. */
124
+ function afterLastWeekday(end: number): number {
125
+ let day = end - DAY;
126
+ while (new Date(day).getUTCDay() % 6 === 0) day -= DAY;
127
+ return day + DAY;
128
+ }
129
+
130
+ /** Close time (ms) of each bar of timeframe `tf` opening at `times` (ms). */
131
+ function closeTimes(times: number[], tf: string, zone: Zone | null): number[] {
132
+ const calendar = calendarOf(zone, tf);
133
+ const info = timeframe.info(tf);
134
+ const len = timeframe.in_seconds(tf) * 1000;
135
+ return times.map((t, i) => {
136
+ if (calendar) {
137
+ const c = periodClose(t, tf, calendar);
138
+ if (!Number.isNaN(c)) return c;
139
+ }
140
+ if (info.isintraday) return i + 1 < times.length ? Math.min(t + len, times[i + 1]!) : t + len;
141
+ const day = utcDay(t);
142
+ if (info.isweekly) return afterLastWeekday(weekStart(day) + 7 * DAY * info.multiplier);
143
+ if (info.ismonthly) {
144
+ const d = new Date(day);
145
+ return afterLastWeekday(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + info.multiplier, 1));
146
+ }
147
+ return day + DAY * info.multiplier;
148
+ });
149
+ }
150
+
151
+ /** Trading day (00:00 UTC, ms) of each bar opening at `times` (ms): its date in the exchange time zone. */
152
+ function tradingDays(times: number[], zone: Zone | null): number[] {
153
+ const calendar = calendarOf(zone, '1D');
154
+ return times.map((t) => {
155
+ const d = calendar ? tradingDayTime(t, calendar) : NaN;
156
+ return Number.isNaN(d) ? utcDay(t) : d;
157
+ });
158
+ }
159
+
160
+ /** For each element of `at` (non-decreasing), the index of the last element of `keys` (non-decreasing) <= it; -1: none. */
161
+ function latestAtOrBefore(keys: number[], at: number[]): number[] {
162
+ let j = -1;
163
+ return at.map((x) => {
164
+ while (j + 1 < keys.length && keys[j + 1]! <= x) j++;
165
+ return j;
166
+ });
167
+ }
168
+
169
+ const zoneOf = (c: SymbolCalendar | undefined): Zone | null =>
170
+ c?.timezone && c.session ? { timezone: c.timezone, session: c.session } : null;
171
+
172
+ /** Timeframes and exchange calendars of a merge: given, 24x7, the chart's, else the UTC rule. */
173
+ function setup(
174
+ ct: number[],
175
+ ot: number[],
176
+ tf: string,
177
+ chart: OtherSymbolChart,
178
+ options: OtherSymbolOptions
179
+ ): { chartTf: string; otherTf: string; chartZone: Zone | null; otherZone: Zone | null } {
180
+ const chartTf = timeframe.info(chart.timeframe).period;
181
+ const otherTf = tf.trim() === '' ? chartTf : timeframe.info(tf).period;
182
+ if (timeframe.info(otherTf).isticks) throw new RangeError(`request.security: tick timeframe "${tf}" is not supported.`);
183
+ const chartZone = zoneOf(chart) ?? (tradesEveryDay(ct, chartTf) ? UTC_24X7 : null);
184
+ const otherZone =
185
+ zoneOf(options.other) ??
186
+ (tradesEveryDay(ot, otherTf) ? UTC_24X7 : chartZone && onCalendar(ot, otherTf, chartZone) ? chartZone : null);
187
+ return { chartTf, otherTf, chartZone, otherZone };
188
+ }
189
+
190
+ /**
191
+ * PineScript `request.security` of another symbol: the index in `otherBars` of the bar shown on each chart bar, -1
192
+ * for na (#179). `otherBars` are the bars of the other symbol on `tf` ("" for the chart timeframe, or a higher
193
+ * or a lower one), with times in the unit of the chart bars. Rules in the module notes; the same rules hold for a
194
+ * lower timeframe (#181): lookahead off shows the last lower bar that closes by the close of the chart bar, lookahead
195
+ * on the last one that opened by its open. It also serves the chart symbol on a lower timeframe.
196
+ * @throws RangeError for a tick timeframe
197
+ */
198
+ export function securityOther(
199
+ chartBars: ReadonlyArray<{ time: number }>,
200
+ otherBars: ReadonlyArray<{ time: number }>,
201
+ tf: string,
202
+ chart: OtherSymbolChart,
203
+ options: OtherSymbolOptions = {}
204
+ ): number[] {
205
+ const n = chartBars.length;
206
+ if (n === 0) return [];
207
+ const unit = chart.timeUnit === 'ms' ? 1 : 1000;
208
+ const ct = chartBars.map((b) => b.time * unit);
209
+ const ot = otherBars.map((b) => b.time * unit);
210
+ const { chartTf, otherTf, chartZone, otherZone } = setup(ct, ot, tf, chart, options);
211
+ if (ot.length === 0) return new Array<number>(n).fill(-1);
212
+
213
+ // by trading day when both timeframes are D / W / M; by open time with an intraday side (#181)
214
+ const daily = timeframe.info(chartTf).isdwm && timeframe.info(otherTf).isdwm;
215
+ const realtime = chart.lastBarConfirmed === false && !options.gaps;
216
+ let on: number[] = [];
217
+ if (options.lookahead || realtime) {
218
+ if (daily) {
219
+ const info = timeframe.info(otherTf);
220
+ const starts = tradingDays(ot, otherZone).map((d) => (info.isweekly ? weekStart(d) : info.ismonthly ? monthStart(d) : d));
221
+ on = latestAtOrBefore(starts, tradingDays(ct, chartZone));
222
+ } else {
223
+ on = latestAtOrBefore(ot, ct);
224
+ }
225
+ }
226
+ const index = options.lookahead ? on : latestAtOrBefore(closeTimes(ot, otherTf, otherZone), closeTimes(ct, chartTf, chartZone));
227
+ // realtime updates of the open last bar: lookahead off shows the bar of lookahead on
228
+ if (realtime) index[n - 1] = on[n - 1]!;
229
+ if (!options.gaps) return index;
230
+ return index.map((j, i) => (i === 0 || j !== index[i - 1] ? j : -1));
231
+ }
232
+
233
+ /**
234
+ * PineScript `request.security_lower_tf` (#181): for each chart bar, the range `[from, to)` of the bars of
235
+ * `otherBars` (a lower timeframe, the chart symbol or another one) that it contains: the bars whose close time is
236
+ * after the open of the chart bar and at or before its close (a 13:00 hourly bar of a 24x7 symbol is in a 13:30 stock
237
+ * day, which closes at 20:00). An empty range for a chart bar without such bars.
238
+ */
239
+ export function securityLowerTf(
240
+ chartBars: ReadonlyArray<{ time: number }>,
241
+ otherBars: ReadonlyArray<{ time: number }>,
242
+ tf: string,
243
+ chart: OtherSymbolChart,
244
+ options: Pick<OtherSymbolOptions, 'other'> = {}
245
+ ): Array<[number, number]> {
246
+ const unit = chart.timeUnit === 'ms' ? 1 : 1000;
247
+ const ct = chartBars.map((b) => b.time * unit);
248
+ const ot = otherBars.map((b) => b.time * unit);
249
+ const { chartTf, otherTf, chartZone, otherZone } = setup(ct, ot, tf, chart, options);
250
+ const oc = closeTimes(ot, otherTf, otherZone);
251
+ const from = latestAtOrBefore(oc, ct);
252
+ const to = latestAtOrBefore(oc, closeTimes(ct, chartTf, chartZone));
253
+ return ct.map((_, i) => [from[i]! + 1, Math.max(from[i]! + 1, to[i]! + 1)]);
254
+ }
255
+
256
+ /**
257
+ * {@link securityOther} with the values of an expression: `values` has one value per bar of the other symbol; the
258
+ * result has one value per chart bar, NaN for na.
259
+ */
260
+ export function securityOtherValues(
261
+ chartBars: ReadonlyArray<{ time: number }>,
262
+ otherBars: ReadonlyArray<{ time: number }>,
263
+ tf: string,
264
+ values: ArrayLike<number>,
265
+ chart: OtherSymbolChart,
266
+ options: OtherSymbolOptions = {}
267
+ ): number[] {
268
+ return securityOther(chartBars, otherBars, tf, chart, options).map((j) => (j < 0 ? NaN : (values[j] ?? NaN)));
269
+ }
@@ -113,7 +113,8 @@ export class SessionBars {
113
113
 
114
114
  /**
115
115
  * PineScript `time(tf)` for a bar that opens at `time`; NaN when no trading day is found. An intraday bar that
116
- * opens after an early close has no day: `time("D")` is NaN on it (measured; `time("W")` keeps its value).
116
+ * opens after an early close has no day and no intraday period: `time("D")`, `time("240")` and `time("60")` are
117
+ * NaN on it (measured; `time("W")` keeps its value).
117
118
  */
118
119
  timeOf(time: number, timeframe: string): number {
119
120
  const tf = info(timeframe);
@@ -121,6 +122,8 @@ export class SessionBars {
121
122
  if (!at) return NaN;
122
123
  if (tf.isintraday) {
123
124
  if (this.chart.isdwm) return time;
125
+ // a bar after an early close has no intraday period either (#173)
126
+ if (this.stray(time)) return NaN;
124
127
  const [start] = this.calendar.periods(at.date)[at.index]!;
125
128
  const ms = in_seconds(tf.period) * 1000;
126
129
  return start + Math.floor((time - start) / ms) * ms;
@@ -141,6 +144,7 @@ export class SessionBars {
141
144
  const [, end] = this.calendar.periods(at.date)[at.index]!;
142
145
  // on D / W / M charts: the end of the session period that contains the bar open
143
146
  if (this.chart.isdwm) return end;
147
+ if (this.stray(time)) return NaN;
144
148
  return Math.min(this.timeOf(time, timeframe) + in_seconds(tf.period) * 1000, end);
145
149
  }
146
150
  const { unit, n } = unitOf(tf);
@@ -234,6 +238,25 @@ export class SessionBars {
234
238
  }
235
239
  }
236
240
 
241
+ /**
242
+ * Periods of the chart bars that open after an early close, for `request.security` (#173): such a bar has no period
243
+ * (`time("D")` and `time("240")` are na on it), but PineScript resamples it into the D / W / M bar of the bar before
244
+ * it, and into an intraday bar of its own (opening at the bar's time). The bar before it still completes its period
245
+ * (its day period is cut there). `intradayMs` is the duration of an intraday timeframe, null for D / W / M.
246
+ * Changes `starts` and `dayPeriods` in place.
247
+ */
248
+ export function strayPeriods(starts: number[], dayPeriods: number[], times: number[], bars: SessionBars, intradayMs: number | null): void {
249
+ for (let i = 1; i < starts.length; i++) {
250
+ if (!bars.stray(times[i]!)) continue;
251
+ if (Number.isNaN(starts[i]!)) {
252
+ // intraday: periods of `intradayMs` from the first such bar ("240": 18:30 and 20:30 are one bar)
253
+ const run = Number.isNaN(dayPeriods[i - 1]!) && times[i]! < starts[i - 1]! + (intradayMs ?? 0);
254
+ starts[i] = intradayMs === null || run ? starts[i - 1]! : times[i]!;
255
+ }
256
+ dayPeriods[i] = NaN;
257
+ }
258
+ }
259
+
237
260
  /** PineScript `session.*` flags of each bar. */
238
261
  export interface SessionFlags {
239
262
  isfirstbar: boolean[];
@@ -94,11 +94,16 @@ function pineVersion(text: string, from: DateNum): Version {
94
94
  const ranges = parsePineSession(text);
95
95
  if (ranges === null) throw new SyntaxError(`Session "${text}" is the symbol session`);
96
96
  const byDay: Period[][] = Array.from({ length: 8 }, () => []);
97
- for (const { start, end, days } of ranges) {
97
+ for (const { start, end, days, startDays } of ranges) {
98
+ // a run that ends at 0000 ends at 24:00 of the day it starts and belongs to that day (#173)
98
99
  const period: Period =
99
100
  start === 0 && end === 0
100
101
  ? { start: 0, end: 1440, startOffset: 0, endOffset: 0 }
101
- : { start, end, startOffset: end <= start ? 1 : 0, endOffset: 0 };
102
+ : startDays !== undefined
103
+ ? { start, end, startOffset: startDays, endOffset: 0 }
104
+ : end === 0
105
+ ? { start, end: 1440, startOffset: 0, endOffset: 0 }
106
+ : { start, end, startOffset: end <= start ? 1 : 0, endOffset: 0 };
102
107
  for (const d of days) byDay[d]!.push(period);
103
108
  }
104
109
  return { from, periods: [], days: byDay.map((p) => p.length > 0), byDay };
@@ -133,17 +138,22 @@ export class TradingCalendar {
133
138
  private readonly corrections = new Map<DateNum, Period[] | null>();
134
139
  private readonly holidays: Set<DateNum>;
135
140
  private readonly cache = new Map<DateNum, Array<[number, number]>>();
141
+ private readonly pine: boolean;
136
142
 
137
143
  /**
138
144
  * @param timezone - Time zone of the session hours (the exchange time zone)
139
145
  * @param spec - Session information (hours, corrections, holidays)
140
146
  * @param rule - Days without ":days": 'symbol' (Monday to Friday) or 'pine' (PineScript session strings)
147
+ * @param dayPlacement - PineScript session strings read for `time("D", session)`: the wall times of a clock-change
148
+ * day are placed as the JVM does (see `periods`); without it, as `time.inSession` places them
141
149
  */
142
150
  constructor(
143
151
  readonly timezone: string,
144
152
  spec: SessionSpec,
145
- rule: 'symbol' | 'pine' = 'symbol'
153
+ rule: 'symbol' | 'pine' = 'symbol',
154
+ dayPlacement = false
146
155
  ) {
156
+ this.pine = rule === 'pine' && dayPlacement;
147
157
  const [first = '', ...changes] = spec.session.split('#');
148
158
  this.versions = [parseVersion(first, 0, spec.session, rule)];
149
159
  for (const change of changes) {
@@ -177,10 +187,19 @@ export class TradingCalendar {
177
187
  const y = Math.floor(date / 10000);
178
188
  const m = Math.floor(date / 100) % 100;
179
189
  const d = date % 100;
180
- out = periods.map((p) => [
181
- zonedToUnix(this.timezone, y, m, d - p.startOffset, Math.floor(p.start / 60), p.start % 60),
182
- zonedToUnix(this.timezone, y, m, d - p.endOffset, Math.floor(p.end / 60), p.end % 60),
183
- ]);
190
+ const at = (dd: number, minutes: number, earlier = false): number =>
191
+ zonedToUnix(this.timezone, y, m, dd, Math.floor(minutes / 60), minutes % 60, 0, earlier);
192
+ // for "D", PineScript session strings place the wall times of a clock-change day as the JVM does: a skipped time moves
193
+ // past the gap, a repeated time is its first pass, and the end is the minute before it plus one minute ("0000-0300"
194
+ // on a 02:00 -> 03:00 night ends at 03:00 of the old offset, "0000-0400" at 04:00 of the new one) (#173)
195
+ out = periods.map((p) =>
196
+ this.pine
197
+ ? [at(d - p.startOffset, p.start, true), at(d - p.endOffset, p.end - 1, true) + 60_000]
198
+ : [at(d - p.startOffset, p.start), at(d - p.endOffset, p.end)]
199
+ );
200
+ // for "D", the runs of a PineScript session string on one day are one period, from the earliest start to the
201
+ // latest end: the time between two runs is inside the day (#182)
202
+ if (this.pine && out.length > 1) out = [[Math.min(...out.map((p) => p[0])), Math.max(...out.map((p) => p[1]))]];
184
203
  this.cache.set(date, out);
185
204
  return out;
186
205
  }
@@ -11,7 +11,8 @@
11
11
  import type { Bar } from '../types/index.js';
12
12
  import { info, in_seconds } from '../timeframe/index.js';
13
13
  import { afterCompletion, periodsOf, resample } from '../security/resample.js';
14
- import { SessionBars, sessionFlags, type SessionFlags } from './bars.js';
14
+ import { parsePineSession, sessionRun } from '../time/session.js';
15
+ import { SessionBars, sessionFlags, strayPeriods, type SessionFlags } from './bars.js';
15
16
  import { TradingCalendar, type SessionSpec } from './calendar.js';
16
17
 
17
18
  /** The part of a chart context that the session functions read (a `ChartContext` of the script API fits). */
@@ -61,16 +62,107 @@ export function chartSessions(chart: SessionContext): ChartSessions {
61
62
  return out;
62
63
  }
63
64
 
64
- /** PineScript `time(timeframe)` (ms) of a bar that opens at `time`: the start of its period; NaN without one. */
65
- export function periodTime(time: number, timeframe: string, chart: SessionContext): number {
66
- const s = chartSessions(chart);
67
- return s.bars.timeOf(time * s.unit, timeframe);
65
+ /** Open and close (ms) of the period that contains a bar opening at a time in ms; NaN outside the session. */
66
+ export interface SessionPeriods {
67
+ open: (time: number) => number;
68
+ close: (time: number) => number;
69
+ }
70
+
71
+ /**
72
+ * Calendar for the `session` / `timezone` arguments of `time(tf, session, timezone)`. Without both: the loaded
73
+ * session. As in PineScript, an explicit time zone reads the symbol session hours in that zone without corrections
74
+ * and holidays, and an explicit session follows PineScript's session string rules.
75
+ */
76
+ function calendarFor(
77
+ bars: SessionBars,
78
+ chart: SessionContext,
79
+ session: string | undefined,
80
+ timezone: string | undefined,
81
+ dayPlacement: boolean
82
+ ): { bars: SessionBars; test: boolean; inside?: (time: number) => boolean } {
83
+ if (!session && !timezone) return { bars, test: false };
84
+ const tz = timezone ?? chart.timezone!;
85
+ // a session string that is empty or does not start with a digit is the symbol session: "regular" the regular
86
+ // hours, "extended" and any other name the loaded hours (PineScript)
87
+ if (session !== undefined && parsePineSession(session) === null) {
88
+ const name = session.trim();
89
+ const symbol = name === 'regular' && chart.regularSession ? chart.regularSession : chart.session!;
90
+ return { bars: new SessionBars(new TradingCalendar(tz, { session: toSpec(symbol).session }, 'symbol'), bars.chart.period), test: true };
91
+ }
92
+ const spec = session ? { session } : { session: toSpec(chart.session!).session };
93
+ return {
94
+ bars: new SessionBars(new TradingCalendar(tz, spec, session ? 'pine' : 'symbol', dayPlacement), bars.chart.period),
95
+ test: true,
96
+ // a PineScript session string: the rules of time.inSession (runs ending at 0000, days around a clock change)
97
+ inside: session ? (time) => sessionRun(time, session, tz, in_seconds(bars.chart.period) * 1000) !== null : undefined,
98
+ };
99
+ }
100
+
101
+ /**
102
+ * `time(tf, session, timezone)` / `time_close(tf, session, timezone)` of a bar, as PineScript computes them (#173):
103
+ * an intraday timeframe tests the bar with the session rules of time.inSession and aligns its periods on the start
104
+ * of the run that contains the bar; "D" tests the bar against the day's session period (the calendar; the time
105
+ * between two runs of a day is inside it, #182); a timeframe of more than one day ("2D", "W", "M") has no test and
106
+ * keeps the period's time. `bars` is the calendar of the loaded session (`chartSessions(chart).bars`).
107
+ */
108
+ export function sessionPeriods(
109
+ bars: SessionBars,
110
+ chart: SessionContext,
111
+ timeframe: string,
112
+ session?: string,
113
+ timezone?: string
114
+ ): SessionPeriods {
115
+ const tf = info(timeframe);
116
+ const { bars: calendar, test, inside } = calendarFor(bars, chart, session, timezone, !tf.isintraday);
117
+ const open = (t: number): number => calendar.timeOf(t, timeframe);
118
+ const close = (t: number): number => calendar.closeOf(t, timeframe);
119
+ if (!test) return { open, close };
120
+ if (tf.isintraday) {
121
+ if (session !== undefined && parsePineSession(session) !== null && !calendar.chart.isdwm) {
122
+ const tz = timezone ?? chart.timezone!;
123
+ const barMs = in_seconds(calendar.chart.period) * 1000;
124
+ const ms = in_seconds(tf.period) * 1000;
125
+ const run = (t: number) => sessionRun(t, session, tz, barMs);
126
+ const start = (t: number, r: [number, number]) => r[0] + Math.floor((t - r[0]) / ms) * ms;
127
+ return {
128
+ open: (t) => { const r = run(t); return r ? start(t, r) : NaN; },
129
+ close: (t) => { const r = run(t); return r ? Math.min(start(t, r) + ms, r[1]) : NaN; },
130
+ };
131
+ }
132
+ const isIn = (t: number) => (inside ? inside(t) : calendar.inside(t));
133
+ return { open: (t) => (isIn(t) ? open(t) : NaN), close: (t) => (isIn(t) ? close(t) : NaN) };
134
+ }
135
+ if (tf.isdaily && tf.multiplier === 1) {
136
+ return { open: (t) => (calendar.inside(t) ? open(t) : NaN), close: (t) => (calendar.inside(t) ? close(t) : NaN) };
137
+ }
138
+ return { open, close };
68
139
  }
69
140
 
70
- /** PineScript `time_close(timeframe)` (ms) of a bar that opens at `time`. */
71
- export function periodClose(time: number, timeframe: string, chart: SessionContext): number {
141
+ const periodCache = new WeakMap<object, Map<string, SessionPeriods>>();
142
+
143
+ function periodsOfChart(chart: SessionContext, timeframe: string, session?: string, timezone?: string): { s: ChartSessions; p: SessionPeriods } {
72
144
  const s = chartSessions(chart);
73
- return s.bars.closeOf(time * s.unit, timeframe);
145
+ let byKey = periodCache.get(chart);
146
+ if (!byKey) periodCache.set(chart, (byKey = new Map()));
147
+ const key = `${timeframe}|${session ?? ''}|${timezone ?? ''}`;
148
+ let p = byKey.get(key);
149
+ if (!p) byKey.set(key, (p = sessionPeriods(s.bars, chart, timeframe, session, timezone)));
150
+ return { s, p };
151
+ }
152
+
153
+ /**
154
+ * PineScript `time(timeframe, session, timezone)` (ms) of a bar that opens at `time`: the start of its period; NaN
155
+ * without one or outside `session` (#182). Without `session` and `timezone`, the periods of the loaded session.
156
+ */
157
+ export function periodTime(time: number, timeframe: string, chart: SessionContext, session?: string, timezone?: string): number {
158
+ const { s, p } = periodsOfChart(chart, timeframe, session, timezone);
159
+ return p.open(time * s.unit);
160
+ }
161
+
162
+ /** PineScript `time_close(timeframe, session, timezone)` (ms) of a bar that opens at `time` (#182). */
163
+ export function periodClose(time: number, timeframe: string, chart: SessionContext, session?: string, timezone?: string): number {
164
+ const { s, p } = periodsOfChart(chart, timeframe, session, timezone);
165
+ return p.close(time * s.unit);
74
166
  }
75
167
 
76
168
  /** PineScript `time_tradingday` (ms, 00:00 UTC of the trading day) of a bar that opens at `time`. */
@@ -120,6 +212,7 @@ export function resampleSession(bars: Bar[], timeframe: string, chart: SessionCo
120
212
  const periods = at ? calendar.calendar.periods(at.date) : [];
121
213
  return periods.length ? calendar.timeOf(periods[0]![0], tf.period) : starts[i]!;
122
214
  });
215
+ strayPeriods(starts, dayPeriods, times, s.bars, tf.isdwm ? null : in_seconds(tf.period) * 1000);
123
216
  const reaches = times.map((t) => s.bars.closeOf(t, chartTf.period) >= calendar.periodEnd(t, tf.period));
124
217
  const { group, latest } = periodsOf(starts, dayPeriods, reaches);
125
218
  // a higher-timeframe bar ends at the bar that completes it (no value from after the completion)
@@ -144,11 +237,12 @@ export function barTimeAfter(time: number, count: number, chart: SessionContext)
144
237
  * Number of bars from the bar that opens at `from` to the bar that contains `to`, on the trading calendar of the
145
238
  * context (0 when `to` is in the bar of `from` or before it). For a drawing placed at a time after the last bar
146
239
  * (PineScript `time + n days` with `xloc.bar_time`): the chart shows it on the bar of that time, weekends and
147
- * holidays skipped. NaN when no bar is found within 100,000 bars.
240
+ * holidays skipped. `to` is first rounded to the nearest second, a half second going up (#174: 15 ms before a bar
241
+ * opens is on that bar, 501 ms before is on the bar before). NaN when no bar is found within 100,000 bars.
148
242
  */
149
243
  export function barsUntil(from: number, to: number, chart: SessionContext): number {
150
244
  const s = chartSessions(chart);
151
- const target = to * s.unit;
245
+ const target = Math.floor((to * s.unit) / 1000 + 0.5) * 1000;
152
246
  let t = from * s.unit;
153
247
  for (let k = 0; k < 100_000; k++) {
154
248
  const next = s.bars.nextBarTime(t);