oakscriptjs 0.9.4 → 0.9.6

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 (177) hide show
  1. package/dist/cjs/array/index.d.ts +88 -27
  2. package/dist/cjs/array/index.d.ts.map +1 -1
  3. package/dist/cjs/array/index.js +257 -117
  4. package/dist/cjs/array/index.js.map +3 -3
  5. package/dist/cjs/callsite/index.d.ts +122 -4
  6. package/dist/cjs/callsite/index.d.ts.map +1 -1
  7. package/dist/cjs/callsite/index.js +102 -3
  8. package/dist/cjs/callsite/index.js.map +2 -2
  9. package/dist/cjs/color/index.d.ts +7 -3
  10. package/dist/cjs/color/index.d.ts.map +1 -1
  11. package/dist/cjs/color/index.js +6 -1
  12. package/dist/cjs/color/index.js.map +2 -2
  13. package/dist/cjs/index.d.ts +1 -1
  14. package/dist/cjs/index.js +1 -1
  15. package/dist/cjs/index.js.map +1 -1
  16. package/dist/cjs/label/index.d.ts.map +1 -1
  17. package/dist/cjs/label/index.js +2 -1
  18. package/dist/cjs/label/index.js.map +2 -2
  19. package/dist/cjs/linefill/index.d.ts.map +1 -1
  20. package/dist/cjs/linefill/index.js +1 -0
  21. package/dist/cjs/linefill/index.js.map +2 -2
  22. package/dist/cjs/math/index.d.ts +79 -22
  23. package/dist/cjs/math/index.d.ts.map +1 -1
  24. package/dist/cjs/math/index.js +157 -43
  25. package/dist/cjs/math/index.js.map +3 -3
  26. package/dist/cjs/matrix/index.d.ts.map +1 -1
  27. package/dist/cjs/matrix/index.js +42 -36
  28. package/dist/cjs/matrix/index.js.map +2 -2
  29. package/dist/cjs/runtime/series.d.ts +9 -0
  30. package/dist/cjs/runtime/series.d.ts.map +1 -1
  31. package/dist/cjs/runtime/series.js +16 -2
  32. package/dist/cjs/runtime/series.js.map +2 -2
  33. package/dist/cjs/script/index.d.ts +15 -31
  34. package/dist/cjs/script/index.d.ts.map +1 -1
  35. package/dist/cjs/script/index.js +32 -0
  36. package/dist/cjs/script/index.js.map +2 -2
  37. package/dist/cjs/session/calendar.d.ts.map +1 -1
  38. package/dist/cjs/session/calendar.js +15 -2
  39. package/dist/cjs/session/calendar.js.map +2 -2
  40. package/dist/cjs/str/index.d.ts +50 -38
  41. package/dist/cjs/str/index.d.ts.map +1 -1
  42. package/dist/cjs/str/index.js +43 -15
  43. package/dist/cjs/str/index.js.map +2 -2
  44. package/dist/cjs/str/messageformat.d.ts +2 -1
  45. package/dist/cjs/str/messageformat.d.ts.map +1 -1
  46. package/dist/cjs/str/messageformat.js +4 -1
  47. package/dist/cjs/str/messageformat.js.map +2 -2
  48. package/dist/cjs/str/numberformat.d.ts +4 -0
  49. package/dist/cjs/str/numberformat.d.ts.map +1 -1
  50. package/dist/cjs/str/numberformat.js +26 -4
  51. package/dist/cjs/str/numberformat.js.map +3 -3
  52. package/dist/cjs/ta/index.d.ts +95 -46
  53. package/dist/cjs/ta/index.d.ts.map +1 -1
  54. package/dist/cjs/ta/index.js +193 -318
  55. package/dist/cjs/ta/index.js.map +3 -3
  56. package/dist/cjs/ta/running-sum.d.ts +54 -1
  57. package/dist/cjs/ta/running-sum.d.ts.map +1 -1
  58. package/dist/cjs/ta/running-sum.js +132 -47
  59. package/dist/cjs/ta/running-sum.js.map +2 -2
  60. package/dist/cjs/ta-series.d.ts +4 -4
  61. package/dist/cjs/ta-series.d.ts.map +1 -1
  62. package/dist/cjs/ta-series.js +4 -3
  63. package/dist/cjs/ta-series.js.map +2 -2
  64. package/dist/cjs/time/datestring.d.ts +1 -0
  65. package/dist/cjs/time/datestring.d.ts.map +1 -1
  66. package/dist/cjs/time/datestring.js +7 -0
  67. package/dist/cjs/time/datestring.js.map +2 -2
  68. package/dist/cjs/time/index.d.ts.map +1 -1
  69. package/dist/cjs/time/index.js +2 -1
  70. package/dist/cjs/time/index.js.map +2 -2
  71. package/dist/cjs/time/session.d.ts +28 -9
  72. package/dist/cjs/time/session.d.ts.map +1 -1
  73. package/dist/cjs/time/session.js +74 -28
  74. package/dist/cjs/time/session.js.map +2 -2
  75. package/dist/cjs/time/timezone.d.ts +5 -0
  76. package/dist/cjs/time/timezone.d.ts.map +1 -1
  77. package/dist/cjs/time/timezone.js +15 -1
  78. package/dist/cjs/time/timezone.js.map +2 -2
  79. package/dist/esm/array/index.d.ts +88 -27
  80. package/dist/esm/array/index.d.ts.map +1 -1
  81. package/dist/esm/array/index.js +408 -157
  82. package/dist/esm/array/index.js.map +1 -1
  83. package/dist/esm/callsite/index.d.ts +122 -4
  84. package/dist/esm/callsite/index.d.ts.map +1 -1
  85. package/dist/esm/callsite/index.js +228 -8
  86. package/dist/esm/callsite/index.js.map +1 -1
  87. package/dist/esm/color/index.d.ts +7 -3
  88. package/dist/esm/color/index.d.ts.map +1 -1
  89. package/dist/esm/color/index.js +15 -3
  90. package/dist/esm/color/index.js.map +1 -1
  91. package/dist/esm/index.d.ts +1 -1
  92. package/dist/esm/index.js +1 -1
  93. package/dist/esm/label/index.d.ts.map +1 -1
  94. package/dist/esm/label/index.js +2 -1
  95. package/dist/esm/label/index.js.map +1 -1
  96. package/dist/esm/linefill/index.d.ts.map +1 -1
  97. package/dist/esm/linefill/index.js +3 -0
  98. package/dist/esm/linefill/index.js.map +1 -1
  99. package/dist/esm/math/index.d.ts +79 -22
  100. package/dist/esm/math/index.d.ts.map +1 -1
  101. package/dist/esm/math/index.js +229 -63
  102. package/dist/esm/math/index.js.map +1 -1
  103. package/dist/esm/matrix/index.d.ts.map +1 -1
  104. package/dist/esm/matrix/index.js +57 -43
  105. package/dist/esm/matrix/index.js.map +1 -1
  106. package/dist/esm/runtime/series.d.ts +9 -0
  107. package/dist/esm/runtime/series.d.ts.map +1 -1
  108. package/dist/esm/runtime/series.js +18 -1
  109. package/dist/esm/runtime/series.js.map +1 -1
  110. package/dist/esm/script/index.d.ts +15 -31
  111. package/dist/esm/script/index.d.ts.map +1 -1
  112. package/dist/esm/script/index.js +44 -1
  113. package/dist/esm/script/index.js.map +1 -1
  114. package/dist/esm/session/calendar.d.ts.map +1 -1
  115. package/dist/esm/session/calendar.js +22 -2
  116. package/dist/esm/session/calendar.js.map +1 -1
  117. package/dist/esm/str/index.d.ts +50 -38
  118. package/dist/esm/str/index.d.ts.map +1 -1
  119. package/dist/esm/str/index.js +103 -52
  120. package/dist/esm/str/index.js.map +1 -1
  121. package/dist/esm/str/messageformat.d.ts +2 -1
  122. package/dist/esm/str/messageformat.d.ts.map +1 -1
  123. package/dist/esm/str/messageformat.js +7 -2
  124. package/dist/esm/str/messageformat.js.map +1 -1
  125. package/dist/esm/str/numberformat.d.ts +4 -0
  126. package/dist/esm/str/numberformat.d.ts.map +1 -1
  127. package/dist/esm/str/numberformat.js +38 -4
  128. package/dist/esm/str/numberformat.js.map +1 -1
  129. package/dist/esm/ta/index.d.ts +95 -46
  130. package/dist/esm/ta/index.d.ts.map +1 -1
  131. package/dist/esm/ta/index.js +341 -414
  132. package/dist/esm/ta/index.js.map +1 -1
  133. package/dist/esm/ta/running-sum.d.ts +54 -1
  134. package/dist/esm/ta/running-sum.d.ts.map +1 -1
  135. package/dist/esm/ta/running-sum.js +166 -60
  136. package/dist/esm/ta/running-sum.js.map +1 -1
  137. package/dist/esm/ta-series.d.ts +4 -4
  138. package/dist/esm/ta-series.d.ts.map +1 -1
  139. package/dist/esm/ta-series.js +5 -4
  140. package/dist/esm/ta-series.js.map +1 -1
  141. package/dist/esm/time/datestring.d.ts +1 -0
  142. package/dist/esm/time/datestring.d.ts.map +1 -1
  143. package/dist/esm/time/datestring.js +12 -0
  144. package/dist/esm/time/datestring.js.map +1 -1
  145. package/dist/esm/time/index.d.ts.map +1 -1
  146. package/dist/esm/time/index.js +3 -1
  147. package/dist/esm/time/index.js.map +1 -1
  148. package/dist/esm/time/session.d.ts +28 -9
  149. package/dist/esm/time/session.d.ts.map +1 -1
  150. package/dist/esm/time/session.js +113 -42
  151. package/dist/esm/time/session.js.map +1 -1
  152. package/dist/esm/time/timezone.d.ts +5 -0
  153. package/dist/esm/time/timezone.d.ts.map +1 -1
  154. package/dist/esm/time/timezone.js +21 -1
  155. package/dist/esm/time/timezone.js.map +1 -1
  156. package/package.json +2 -2
  157. package/src/array/index.ts +382 -174
  158. package/src/callsite/index.ts +232 -8
  159. package/src/color/index.ts +15 -5
  160. package/src/index.ts +1 -1
  161. package/src/label/index.ts +2 -1
  162. package/src/linefill/index.ts +2 -0
  163. package/src/math/index.ts +261 -77
  164. package/src/matrix/index.ts +52 -52
  165. package/src/runtime/series.ts +22 -1
  166. package/src/script/index.ts +53 -1
  167. package/src/session/calendar.ts +22 -2
  168. package/src/str/index.ts +95 -51
  169. package/src/str/messageformat.ts +7 -2
  170. package/src/str/numberformat.ts +39 -4
  171. package/src/ta/index.ts +356 -458
  172. package/src/ta/running-sum.ts +187 -62
  173. package/src/ta-series.ts +8 -7
  174. package/src/time/datestring.ts +10 -0
  175. package/src/time/index.ts +3 -1
  176. package/src/time/session.ts +97 -47
  177. package/src/time/timezone.ts +21 -1
@@ -6,7 +6,8 @@
6
6
  * - each bar first removes, with a Kahan step, the compensated value that was added for the value that leaves the
7
7
  * window, then adds the new value with a Kahan step and keeps its compensated value;
8
8
  * - when the compensation of the previous step would be lost when applied to the new value, the sum is computed
9
- * again from the window values (newest first) and the compensation is reset.
9
+ * again from the window values (newest first) and the compensation is reset. The test moves the new value toward
10
+ * +infinity by the size of the compensation, in floating point (#136, #142).
10
11
  *
11
12
  * So the result can differ from the exact window sum in the last bits (e.g. a small residue on a window of zeros),
12
13
  * as in PineScript.
@@ -14,104 +15,228 @@
14
15
  * @internal
15
16
  */
16
17
 
17
- const view = new DataView(new ArrayBuffer(8));
18
-
19
- /** Binary exponent of a finite non-zero number (from its bits). */
20
- function exponentOf(x: number): number {
21
- view.setFloat64(0, x);
22
- const e = (view.getUint16(0) >> 4) & 0x7ff;
23
- return e === 0 ? -1074 : e - 1023;
24
- }
25
-
26
18
  /**
27
- * Whether the compensation `c` is (partly) lost when it is applied to `x`: `x - c` rounded on the grid of `x`
28
- * (spacing of the binade of `x`, ties to an even mantissa) applies less than `c`.
19
+ * Whether the compensation `c` is (partly) lost when it is applied to `x`: `x + |c|` in floating point moves `x` by
20
+ * less than `|c|`. Moving toward +infinity uses the grid above `x` (the coarser binade just below a power of two, the
21
+ * finer binade for a negative power of two), as PineScript (measured bit for bit, #142).
29
22
  */
30
23
  function compensationLost(x: number, c: number): boolean {
31
24
  if (c === 0 || x === 0) return false;
32
- const ulp = 2 ** (exponentOf(x) - 52);
33
- const q = c / ulp;
34
- const mantissa = x / ulp;
35
- const f = Math.floor(q);
36
- const d = q - f;
37
- const applied = d > 0.5 ? f + 1 : d < 0.5 ? f : (mantissa - f) % 2 === 0 ? f : f + 1;
38
- return Math.abs(applied) < Math.abs(q);
25
+ const size = Math.abs(c);
26
+ return Math.abs(x + size - x) < size;
39
27
  }
40
28
 
41
29
  /** A value PineScript treats as na in window functions (also +/-Infinity). */
42
- function isNa(v: number | null | undefined): boolean {
30
+ export function isNa(v: number | null | undefined): boolean {
43
31
  return v === undefined || v === null || !Number.isFinite(v);
44
32
  }
45
33
 
46
34
  /**
47
- * The sum of the last `length` non-na values on each bar, as PineScript computes it.
35
+ * Running sum state, one value at a time. {@link step} computes the sum with a new value from the kept state without
36
+ * changing it; {@link commit} keeps the last step. So several candidate values can be tried on one bar and only the
37
+ * last one kept (a ta.* call in a loop).
48
38
  *
49
- * @param source - Values (na: NaN, null, undefined or +/-Infinity, skipped)
50
- * @param length - Number of non-na values in the window
51
- * @returns The running sum per bar: NaN until `length` non-na values exist; a bar with an na value keeps the
52
- * previous result
39
+ * The length can change from one step to the next (a series length). PineScript then first moves the kept sum to the
40
+ * new length with Kahan steps of the raw values (not the stored compensated values): a smaller length removes the
41
+ * values that leave the window, oldest first; a larger length adds back the values from the one before the old window
42
+ * down to the one `length` values back, newest first (one more than the window needs). Then the usual step runs with
43
+ * the new length: it removes the stored value of the one `length` values back and adds the new value.
53
44
  */
54
- export function runningSum(source: ArrayLike<number | null | undefined>, length: number): number[] {
55
- const len = Math.floor(length);
56
- const out: number[] = new Array<number>(source.length).fill(NaN);
57
- if (!(len >= 1)) return out;
58
- const values: number[] = [];
59
- const stored: number[] = [];
60
- let s = 0;
61
- let c = 0;
62
- let previous = NaN;
63
- const kahanAdd = (v: number): number => {
64
- const y = v - c;
65
- const t = s + y;
66
- c = (t - s) - y;
67
- s = t;
68
- return y;
69
- };
70
- for (let i = 0; i < source.length; i++) {
71
- const v = source[i];
72
- if (isNa(v)) {
73
- out[i] = previous;
74
- continue;
45
+ export class RunningSum {
46
+ private readonly values: number[] = [];
47
+ private readonly stored: number[] = [];
48
+ private keptLength = NaN;
49
+ private s = 0;
50
+ private c = 0;
51
+ private stepLength = NaN;
52
+ private stepX = NaN;
53
+ private stepY = NaN;
54
+ private stepS = 0;
55
+ private stepC = 0;
56
+
57
+ /** Kahan steps that move the kept sum from the kept length to `len`; the result goes to stepS / stepC. */
58
+ private resize(len: number): void {
59
+ const values = this.values;
60
+ const n = values.length;
61
+ const kept = this.keptLength;
62
+ let s = this.s;
63
+ let c = this.c;
64
+ const add = (v: number) => {
65
+ const y = v - c;
66
+ const t = s + y;
67
+ c = (t - s) - y;
68
+ s = t;
69
+ };
70
+ if (len < kept) {
71
+ for (let k = Math.max(0, n - kept); k <= n - len - 1; k++) add(-values[k]!);
72
+ } else if (len > kept) {
73
+ for (let k = n - kept - 1; k >= Math.max(0, n - len); k--) add(values[k]!);
75
74
  }
76
- const x = v as number;
75
+ this.stepS = s;
76
+ this.stepC = c;
77
+ }
78
+
79
+ /**
80
+ * The running sum of the last `length` values with the (not na) value `x` added, from the kept state; the state is
81
+ * unchanged until {@link commit}.
82
+ *
83
+ * @returns The sum, or NaN while fewer than `length` values exist (with `x`)
84
+ */
85
+ step(x: number, length: number): number {
86
+ const len = length;
87
+ const values = this.values;
88
+ this.resize(len);
89
+ let s = this.stepS;
90
+ let c = this.stepC;
77
91
  const c0 = c;
78
92
  const n = values.length;
79
- if (n >= len) kahanAdd(-stored[n - len]!);
80
- values.push(x);
93
+ if (n >= len) {
94
+ const y = -this.stored[n - len]! - c;
95
+ const t = s + y;
96
+ c = (t - s) - y;
97
+ s = t;
98
+ }
81
99
  if (compensationLost(x, c0)) {
82
- let fresh = 0;
83
- for (let k = values.length - 1; k >= Math.max(0, values.length - len); k--) fresh += values[k]!;
100
+ let fresh = x;
101
+ for (let k = n - 1; k >= Math.max(0, n + 1 - len); k--) fresh += values[k]!;
84
102
  s = fresh;
85
103
  c = 0;
86
- stored.push(x);
104
+ this.stepY = x;
87
105
  } else {
88
- stored.push(kahanAdd(x));
106
+ const y = x - c;
107
+ const t = s + y;
108
+ c = (t - s) - y;
109
+ s = t;
110
+ this.stepY = y;
89
111
  }
90
- previous = values.length >= len ? s : NaN;
91
- out[i] = previous;
112
+ this.stepLength = len;
113
+ this.stepX = x;
114
+ this.stepS = s;
115
+ this.stepC = c;
116
+ return n + 1 >= len ? s : NaN;
117
+ }
118
+
119
+ /**
120
+ * The running sum of the last `length` values on a bar with an na value (no value added), from the kept state; the
121
+ * state is unchanged until {@link commit}. With an unchanged length, it is the previous result.
122
+ *
123
+ * @returns The sum, or NaN while fewer than `length` values exist
124
+ */
125
+ stepNa(length: number): number {
126
+ this.resize(length);
127
+ this.stepLength = length;
128
+ this.stepX = NaN;
129
+ const n = this.values.length;
130
+ return n > 0 && n >= length ? this.stepS : NaN;
131
+ }
132
+
133
+ /** Keeps the state of the last {@link step} or {@link stepNa}. */
134
+ commit(): void {
135
+ if (!Number.isNaN(this.stepX)) {
136
+ this.values.push(this.stepX);
137
+ this.stored.push(this.stepY);
138
+ }
139
+ this.keptLength = this.stepLength;
140
+ this.s = this.stepS;
141
+ this.c = this.stepC;
142
+ }
143
+ }
144
+
145
+ /** The running sums of `source` with the length `lengthAt(i)` on bar `i`. */
146
+ function sumWindows(source: ArrayLike<number | null | undefined>, lengthAt: (i: number) => number): number[] {
147
+ const out: number[] = new Array<number>(source.length);
148
+ const sum = new RunningSum();
149
+ for (let i = 0; i < source.length; i++) {
150
+ const v = source[i];
151
+ const len = lengthAt(i);
152
+ out[i] = isNa(v) ? sum.stepNa(len) : sum.step(v as number, len);
153
+ sum.commit();
92
154
  }
93
155
  return out;
94
156
  }
95
157
 
96
158
  /**
97
- * Window variance from the running sums of the values and of their squares, as PineScript computes it:
98
- * biased `sumSq / n - mean * mean`, unbiased `sumSq / (n - 1) - mean * sum / (n - 1)`, with `mean = sum / n`.
159
+ * The sum of the last `length` non-na values on each bar, as PineScript computes it.
160
+ *
161
+ * @param source - Values (na: NaN, null, undefined or +/-Infinity, skipped)
162
+ * @param length - Number of non-na values in the window
163
+ * @returns The running sum per bar: NaN until `length` non-na values exist; a bar with an na value keeps the
164
+ * previous result
99
165
  */
100
- export function runningVariance(
166
+ export function runningSum(source: ArrayLike<number | null | undefined>, length: number): number[] {
167
+ const len = Math.floor(length);
168
+ if (!(len >= 1)) return new Array<number>(source.length).fill(NaN);
169
+ return sumWindows(source, () => len);
170
+ }
171
+
172
+ /** The values of a series length, floored, checked to be at least 1 on every bar. */
173
+ function seriesLengths(lengths: ArrayLike<number>, count: number, name: string): number[] {
174
+ if (lengths.length < count) {
175
+ throw new Error(`${name}: the length series has ${lengths.length} values for ${count} bars`);
176
+ }
177
+ return Array.from({ length: count }, (_, i) => {
178
+ const len = Math.floor(lengths[i]!);
179
+ if (!(len >= 1)) throw new Error(`${name}: length must be at least 1 (bar ${i}: ${lengths[i]})`);
180
+ return len;
181
+ });
182
+ }
183
+
184
+ /**
185
+ * The sum of the last `lengths[i]` non-na values on each bar `i` (a series length), as PineScript computes it (see
186
+ * {@link RunningSum}). A bar with an na value gives the sum of the last `lengths[i]` earlier values.
187
+ *
188
+ * @param name - Function name for the error messages
189
+ */
190
+ export function runningSumSeries(
101
191
  source: ArrayLike<number | null | undefined>,
102
- length: number,
192
+ lengths: ArrayLike<number>,
193
+ name = 'math.sum'
194
+ ): number[] {
195
+ const lens = seriesLengths(lengths, source.length, name);
196
+ return sumWindows(source, (i) => lens[i]!);
197
+ }
198
+
199
+ /** Variance from the running sums of the values and of their squares; rules in {@link runningVariance}. */
200
+ function varianceFromSums(
201
+ source: ArrayLike<number | null | undefined>,
202
+ sumOf: (src: ArrayLike<number | null | undefined>) => number[],
203
+ lengthAt: (i: number) => number,
103
204
  biased: boolean
104
205
  ): number[] {
105
- const len = Math.floor(length);
106
206
  const squares = Array.from({ length: source.length }, (_, i) => {
107
207
  const v = source[i];
108
208
  return isNa(v) ? NaN : (v as number) * (v as number);
109
209
  });
110
- const sums = runningSum(source, len);
111
- const sumSquares = runningSum(squares, len);
210
+ const sums = sumOf(source);
211
+ const sumSquares = sumOf(squares);
112
212
  return sums.map((sum, i) => {
213
+ const len = lengthAt(i);
113
214
  const mean = sum / len;
114
215
  const sq = sumSquares[i]!;
115
216
  return biased ? sq / len - mean * mean : sq / (len - 1) - (mean * sum) / (len - 1);
116
217
  });
117
218
  }
219
+
220
+ /**
221
+ * Window variance from the running sums of the values and of their squares, as PineScript computes it:
222
+ * biased `sumSq / n - mean * mean`, unbiased `sumSq / (n - 1) - mean * sum / (n - 1)`, with `mean = sum / n`.
223
+ */
224
+ export function runningVariance(
225
+ source: ArrayLike<number | null | undefined>,
226
+ length: number,
227
+ biased: boolean
228
+ ): number[] {
229
+ const len = Math.floor(length);
230
+ return varianceFromSums(source, (src) => runningSum(src, len), () => len, biased);
231
+ }
232
+
233
+ /** {@link runningVariance} with a series length (the length of bar `i` is `lengths[i]`). */
234
+ export function runningVarianceSeries(
235
+ source: ArrayLike<number | null | undefined>,
236
+ lengths: ArrayLike<number>,
237
+ biased: boolean,
238
+ name = 'ta.variance'
239
+ ): number[] {
240
+ const lens = seriesLengths(lengths, source.length, name);
241
+ return varianceFromSums(source, (src) => sumWindows(src, (i) => lens[i]!), (i) => lens[i]!, biased);
242
+ }
package/src/ta-series.ts CHANGED
@@ -14,10 +14,10 @@ import type { Bar } from './types/index.js';
14
14
  * @param length - Period length
15
15
  * @returns Series with SMA values
16
16
  */
17
- export function sma(source: Series, length: number): Series {
17
+ export function sma(source: Series, length: number | Series): Series {
18
18
  const bars = source.bars as Bar[];
19
19
  const sourceValues = source.toArray();
20
- const result = taCore.sma(sourceValues, length);
20
+ const result = taCore.sma(sourceValues, length instanceof Series ? length.toArray() : length);
21
21
  return Series.fromArray(bars, result);
22
22
  }
23
23
 
@@ -124,9 +124,10 @@ export function bb(source: Series, length: number, mult: number): [Series, Serie
124
124
  * @param biased - true (default): divide by length; false: by length - 1
125
125
  * @returns Series with standard deviation values
126
126
  */
127
- export function stdev(source: Series, length: number, biased: boolean = true): Series {
127
+ export function stdev(source: Series, length: number | Series, biased: boolean = true): Series {
128
128
  const bars = source.bars as Bar[];
129
- return Series.fromArray(bars, taCore.stdev(source.toArray(), length, biased));
129
+ const len = length instanceof Series ? length.toArray() : length;
130
+ return Series.fromArray(bars, taCore.stdev(source.toArray(), len, biased));
130
131
  }
131
132
 
132
133
 
@@ -600,9 +601,9 @@ export function dev(source: Series, length: number): Series {
600
601
  * @param biased - Use biased estimation (default: true)
601
602
  * @returns Series with variance values
602
603
  */
603
- export function variance(source: Series, length: number, biased: boolean = true): Series {
604
+ export function variance(source: Series, length: number | Series, biased: boolean = true): Series {
604
605
  const bars = source.bars as Bar[];
605
- const result = taCore.variance(source.toArray(), length, biased);
606
+ const result = taCore.variance(source.toArray(), length instanceof Series ? length.toArray() : length, biased);
606
607
  return Series.fromArray(bars, result);
607
608
  }
608
609
 
@@ -757,7 +758,7 @@ export function dmi(bars: Bar[], diLength: number, adxSmoothing: number): [Serie
757
758
  * @param source - Source series
758
759
  * @param shortLength - Short smoothing period
759
760
  * @param longLength - Long smoothing period
760
- * @returns Series with TSI values
761
+ * @returns Series with TSI values, in [-1, 1] as PineScript
761
762
  */
762
763
  export function tsi(source: Series, shortLength: number, longLength: number): Series {
763
764
  const bars = source.bars as Bar[];
@@ -10,6 +10,7 @@
10
10
  * Time zones: "UT", "UTC", "GMT", "+0530", "-05:00", "GMT+5", "UTC-0300", and the RFC 2822
11
11
  * names EST, EDT, CST, CDT, MST, MDT, PST, PDT. After a "T" time, only numeric offsets.
12
12
  * Without a time the time is 00:00. Without a time zone the time zone is GMT+0.
13
+ * "2025" and "2025-06" are the first day of the year / month. "01 Jan 2022 GMT+3" (a zone without a time) gives na.
13
14
  * "24:00" is midnight at the end of the day.
14
15
  *
15
16
  * "Z", "2024-01-01T09:30:00 GMT", "24:30" and "09:30:60" are compile errors in PineScript, and
@@ -34,6 +35,7 @@ const NUMERIC_ZONE = String.raw`([+-]\d{2}(?::?\d{2})?)`;
34
35
  const DATE = String.raw`(\d{4})([-/])(\d{1,2})\2(\d{1,2})`;
35
36
  const WEEKDAY = String.raw`(?:[a-z]{3,9},?\s+)?`;
36
37
 
38
+ const YEAR_MONTH = /^(\d{4})(?:-(\d{1,2}))?$/;
37
39
  const ISO_T = new RegExp(String.raw`^${DATE}t${TIME}\s*${NUMERIC_ZONE}?$`, 'i');
38
40
  const ISO = new RegExp(String.raw`^${DATE}(?:\s+${TIME})?\s*${ZONE}?$`, 'i');
39
41
  const DAY_FIRST = new RegExp(String.raw`^${WEEKDAY}(\d{1,2})(?:\s+|-)([a-z]+)\.?,?(?:\s+|-)(\d{4})(?:,?\s+${TIME})?\s*${ZONE}?$`, 'i');
@@ -68,12 +70,20 @@ export function parseDateString(dateString: string): number {
68
70
  let rest: Array<string | undefined>;
69
71
 
70
72
  let m: RegExpExecArray | null;
73
+ // "2025" and "2025-06": the first day of the year / month, 00:00 UTC (PineScript)
74
+ if ((m = YEAR_MONTH.exec(text))) {
75
+ const mo = m[2] ? Number(m[2]) : 1;
76
+ if (mo < 1 || mo > 12) throw new SyntaxError(`Invalid date string "${dateString}": a field is out of range`);
77
+ return Date.UTC(Number(m[1]), mo - 1, 1);
78
+ }
71
79
  if ((m = ISO_T.exec(text) ?? ISO.exec(text))) {
72
80
  [year, month, day] = [m[1]!, Number(m[3]), m[4]!];
73
81
  rest = m.slice(5);
74
82
  } else if ((m = DAY_FIRST.exec(text))) {
75
83
  [day, month, year] = [m[1]!, monthNumber(m[2]!), m[3]!];
76
84
  rest = m.slice(4);
85
+ // a time zone without a time ("01 Jan 2022 GMT+3") gives na in PineScript
86
+ if (rest[0] === undefined && rest[4] !== undefined) return NaN;
77
87
  } else if ((m = MONTH_FIRST.exec(text))) {
78
88
  [month, day, year] = [monthNumber(m[1]!), m[2]!, m[3]!];
79
89
  rest = m.slice(4);
package/src/time/index.ts CHANGED
@@ -61,8 +61,10 @@ export function timestamp(
61
61
  export function timestamp(first: string | simple_int, ...rest: Array<simple_int | undefined>): int {
62
62
  if (typeof first === 'string') {
63
63
  if (rest.length === 0) return parseDateString(first);
64
+ // PineScript: an na field counts as 0 (day na of January 2020 is 2019-12-31)
65
+ const f = (v: number | undefined) => (v === undefined || v === null || Number.isNaN(v) ? 0 : v);
64
66
  const [year, month, day, hour, minute, second] = rest as [number, number, number, number?, number?, number?];
65
- return zonedToUnix(first, year, month, day, hour ?? 0, minute ?? 0, second ?? 0);
67
+ return zonedToUnix(first, f(year), f(month), f(day), f(hour), f(minute), f(second));
66
68
  }
67
69
  const [month, day, hour, minute, second] = rest as [number, number, number?, number?, number?];
68
70
  return new Date(first, month - 1, day, hour || 0, minute || 0, second || 0).getTime();
@@ -1,78 +1,128 @@
1
1
  /**
2
- * Session strings ("0930-1600", "0930-1600:23456", "1000-1100,1400-1500:23456", "24x7").
2
+ * PineScript session strings ("0930-1600", "0930-1600:23456", "1000-1100,1400-1500:23456", "0930-1600|1000-1300:7",
3
+ * "24x7").
3
4
  *
4
- * Format `<periods>[:<days>]`:
5
- * - a period is "HHmm-HHmm"; several periods are separated by commas
6
- * - days are digits 1 (Sunday) to 7 (Saturday). Without days, a single period covers every
7
- * day, and several periods cover Monday to Friday ("23456"), as in PineScript
8
- * - an end before the start is an overnight period that ends on the next day, and it
9
- * belongs to the day it ends: "1700-1700:23456" starts Sunday 17:00 and covers Monday
10
- * - "0000-0000" and "24x7" are the 24-hour session of every day
5
+ * Grammar (measured on PineScript): `range[,range...][:days]`, several sections joined by "|".
6
+ * - a range is exactly "HHMM-HHMM"; hours and minutes are minute counts taken modulo one day ("0930-2500" runs
7
+ * 09:30 to 01:00, "0960-1600" 10:00 to 16:00); a start of 24:00 or later must open a run of at most a day
8
+ * - days are digits 1 (Sunday) to 7 (Saturday), the day each run ends on
9
+ * - a lone range without days runs every day; otherwise a section without days is the default section, which runs
10
+ * on the weekdays (2-6) no other section names, and a later section naming a day replaces the earlier ones on that
11
+ * day ("1100-1400|1200-1300:7": 11:00-14:00 Monday to Friday, 12:00-13:00 Saturday)
12
+ * - a trailing ",", ":" or "|" ends its list ("1100-1400," is "1100-1400" Monday to Friday)
13
+ * - an end before the start is an overnight run that belongs to the day it ends; equal ends are 24 hours;
14
+ * "0000-0000" and "24x7" are the whole day, every day
15
+ * - a string that is empty or does not start with a digit ("regular", "extended") is the symbol's own session
16
+ * - a malformed string ("1100 -1400", "0930-16", ":8") is an error, as the PineScript runtime error
11
17
  */
12
18
 
13
19
  import { zonedFields } from './timezone.js';
14
20
 
15
- interface Period {
16
- /** Minutes after midnight, 0-1439 */
21
+ const MINUTES_PER_DAY = 1440;
22
+ const WEEKDAYS = [2, 3, 4, 5, 6];
23
+ const ALL_DAYS = [1, 2, 3, 4, 5, 6, 7];
24
+
25
+ /** A run of a session: minutes after midnight (0-1439) and the days it ends on (1 = Sunday ... 7 = Saturday). */
26
+ export interface SessionRange {
17
27
  start: number;
18
28
  end: number;
29
+ days: Set<number>;
19
30
  }
20
31
 
21
- interface ParsedSession {
22
- periods: Period[];
23
- /** Included days, indexed 1 (Sunday) to 7 (Saturday) */
24
- days: boolean[];
32
+ function invalid(session: string): SyntaxError {
33
+ return new SyntaxError(`Invalid session "${session}": expected "HHMM-HHMM[,HHMM-HHMM...][:days][|...]"`);
25
34
  }
26
35
 
27
- const PERIOD = /^(\d{2})(\d{2})-(\d{2})(\d{2})$/;
28
-
29
- const sessions = new Map<string, ParsedSession>();
30
-
31
- function parseSession(session: string): ParsedSession {
32
- let parsed = sessions.get(session);
33
- if (parsed) return parsed;
34
-
35
- const invalid = () => new SyntaxError(`Invalid session "${session}": expected "HHmm-HHmm[,HHmm-HHmm...][:days]"`);
36
- const text = session.trim() === '24x7' ? '0000-0000' : session.replace(/\s+/g, '');
37
- const [periodText = '', dayText, extra] = text.split(':');
38
- if (extra !== undefined || periodText === '') throw invalid();
39
-
40
- const periods = periodText.split(',').map((p) => {
41
- const m = PERIOD.exec(p);
42
- if (!m) throw invalid();
43
- const [h1, m1, h2, m2] = [m[1], m[2], m[3], m[4]].map(Number) as [number, number, number, number];
44
- if (h1 > 23 || h2 > 23 || m1 > 59 || m2 > 59) throw invalid();
45
- return { start: h1 * 60 + m1, end: h2 * 60 + m2 };
36
+ function parseRanges(text: string, session: string): Array<[number, number]> {
37
+ if (!text) return [];
38
+ const entries = text.split(',');
39
+ if (entries.length > 1 && entries[entries.length - 1] === '') entries.pop();
40
+ return entries.map((entry) => {
41
+ if (!/^\d{4}-\d{4}$/.test(entry)) throw invalid(session);
42
+ let start = Number(entry.slice(0, 2)) * 60 + Number(entry.slice(2, 4));
43
+ let end = Number(entry.slice(5, 7)) * 60 + Number(entry.slice(7, 9));
44
+ if (start >= MINUTES_PER_DAY) {
45
+ const endOfRun = end || MINUTES_PER_DAY;
46
+ if (!(start - MINUTES_PER_DAY < endOfRun && endOfRun <= start && start < 2 * MINUTES_PER_DAY)) throw invalid(session);
47
+ }
48
+ start %= MINUTES_PER_DAY;
49
+ end %= MINUTES_PER_DAY;
50
+ return [start, end];
46
51
  });
52
+ }
47
53
 
48
- const dayDigits = dayText ?? (periods.length > 1 ? '23456' : '1234567');
49
- if (!/^[1-7]+$/.test(dayDigits)) throw invalid();
50
- const days = new Array<boolean>(8).fill(false);
51
- for (const d of dayDigits) days[Number(d)] = true;
54
+ function parseDays(text: string, session: string): Set<number> {
55
+ if (!/^[1-7]+$/.test(text)) throw invalid(session);
56
+ return new Set([...text].map(Number));
57
+ }
58
+
59
+ const parsed = new Map<string, SessionRange[] | null>();
52
60
 
53
- parsed = { periods, days };
54
- sessions.set(session, parsed);
55
- return parsed;
61
+ /**
62
+ * Runs of a PineScript session string, or null for the symbol's own session (empty or not starting with a digit).
63
+ * @throws SyntaxError when the string is malformed (a PineScript runtime error)
64
+ */
65
+ export function parsePineSession(session: string): SessionRange[] | null {
66
+ const cached = parsed.get(session);
67
+ if (cached !== undefined) return cached;
68
+ const spec = session.trim();
69
+ let out: SessionRange[] | null;
70
+ if (!spec || !/^\d/.test(spec)) {
71
+ out = null;
72
+ } else if (spec === '24x7') {
73
+ out = [{ start: 0, end: 0, days: new Set(ALL_DAYS) }];
74
+ } else if (!/[:,|]/.test(spec)) {
75
+ out = parseRanges(spec, session).map(([start, end]) => ({ start, end, days: new Set(ALL_DAYS) }));
76
+ } else {
77
+ const sections = spec.split('|');
78
+ if (sections.length > 1 && sections[sections.length - 1] === '') sections.pop();
79
+ const items = sections.map((section) => {
80
+ const parts = section.split(':');
81
+ if (parts.length > 2) throw invalid(session);
82
+ const ranges = parseRanges(parts[0]!, session);
83
+ const days = parts.length === 1 || parts[1] === '' ? null : parseDays(parts[1]!, session);
84
+ return { ranges, days };
85
+ });
86
+ if (items.filter((it) => it.days === null).length > 1) throw invalid(session);
87
+ // a later section naming a day replaces the earlier ones on that day; the default section takes the weekdays
88
+ // no other section names
89
+ const own: Set<number>[] = items.map(() => new Set<number>());
90
+ const claimed = new Set<number>();
91
+ for (let i = items.length - 1; i >= 0; i--) {
92
+ const days = items[i]!.days;
93
+ if (days === null) continue;
94
+ for (const d of days) if (!claimed.has(d)) own[i]!.add(d);
95
+ for (const d of days) claimed.add(d);
96
+ }
97
+ items.forEach((it, i) => {
98
+ if (it.days === null) for (const d of WEEKDAYS) if (!claimed.has(d)) own[i]!.add(d);
99
+ });
100
+ out = items.flatMap((it, i) => (own[i]!.size ? it.ranges.map(([start, end]) => ({ start, end, days: own[i]! })) : []));
101
+ }
102
+ parsed.set(session, out);
103
+ return out;
56
104
  }
57
105
 
58
106
  /**
59
107
  * True when `time` falls inside `session`, with the session hours read in `timezone`.
60
- * A period includes its start minute and excludes its end minute.
108
+ * A run includes its start minute and excludes its end minute.
109
+ * @throws Error for the symbol's own session ("regular", "extended", ""), which this function does not know: use
110
+ * `time(timeframe, session)` in a script with the chart session
61
111
  */
62
112
  export function inSession(time: number, session: string, timezone: string): boolean {
63
113
  if (Number.isNaN(time)) return false;
64
- const { periods, days } = parseSession(session);
114
+ const ranges = parsePineSession(session);
115
+ if (ranges === null) throw new Error(`Session "${session}" is the symbol session: it needs the chart session (time(timeframe, session) in a script)`);
65
116
  const f = zonedFields(time, timezone);
66
117
  const minute = f.hour * 60 + f.minute;
67
118
  const nextDay = (f.dayOfWeek % 7) + 1;
68
-
69
- for (const { start, end } of periods) {
119
+ for (const { start, end, days } of ranges) {
70
120
  if (start < end || (start === 0 && end === 0)) {
71
- if (minute >= start && (minute < end || end === 0) && days[f.dayOfWeek]) return true;
121
+ if (minute >= start && (minute < end || end === 0) && days.has(f.dayOfWeek)) return true;
72
122
  } else {
73
123
  // Overnight (24 hours when start === end): the part from `start` belongs to the next day.
74
- if (minute >= start && days[nextDay]) return true;
75
- if (minute < end && days[f.dayOfWeek]) return true;
124
+ if (minute >= start && days.has(nextDay)) return true;
125
+ if (minute < end && days.has(f.dayOfWeek)) return true;
76
126
  }
77
127
  }
78
128
  return false;
@@ -157,6 +157,26 @@ export function zonedFields(time: number, timezone: string): ZonedFields {
157
157
  * resolve to the later instant, and wall-clock times that do not exist (start of
158
158
  * daylight saving time) are shifted forward by the length of the gap.
159
159
  */
160
+ /**
161
+ * UNIX time (ms) of a UTC wall-clock date: Gregorian calendar from 1582-10-15, Julian calendar before it (as
162
+ * PineScript: 1582-10-05 is 1582-10-15). Out-of-range fields roll over like `Date.UTC`.
163
+ */
164
+ export function civilToUnix(year: number, month: number, day: number, hour = 0, minute = 0, second = 0): number {
165
+ const gregorian = Date.UTC(year, month - 1, day, hour, minute, second);
166
+ // normalized year / month / day of the Gregorian reading decides the calendar
167
+ const d = new Date(Date.UTC(year, month - 1, 1));
168
+ const y = d.getUTCFullYear();
169
+ const m = d.getUTCMonth() + 1;
170
+ if (y > 1582 || (y === 1582 && (m > 10 || (m === 10 && day >= 15)))) return gregorian;
171
+ // Julian day number of y-m-1, then the day offset and the time of day
172
+ const a = Math.floor((14 - m) / 12);
173
+ const yy = y + 4800 - a;
174
+ const mm = m + 12 * a - 3;
175
+ const jdn = 1 + Math.floor((153 * mm + 2) / 5) + 365 * yy + Math.floor(yy / 4) - 32083;
176
+ const days = jdn - 2440588 + (day - 1);
177
+ return days * MS_PER_DAY + Date.UTC(1970, 0, 1, hour, minute, second);
178
+ }
179
+
160
180
  export function zonedToUnix(
161
181
  timezone: string,
162
182
  year: number,
@@ -167,7 +187,7 @@ export function zonedToUnix(
167
187
  second: number = 0
168
188
  ): number {
169
189
  const zone = resolveZone(timezone);
170
- const local = Date.UTC(year, month - 1, day, hour, minute, second);
190
+ const local = civilToUnix(year, month, day, hour, minute, second);
171
191
  if (zone.kind === 'fixed') return local - zone.offsetMs;
172
192
 
173
193
  // At most one offset transition happens within a day of `local`: the offsets