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.
- package/dist/cjs/array/index.d.ts +88 -27
- package/dist/cjs/array/index.d.ts.map +1 -1
- package/dist/cjs/array/index.js +257 -117
- package/dist/cjs/array/index.js.map +3 -3
- package/dist/cjs/callsite/index.d.ts +122 -4
- package/dist/cjs/callsite/index.d.ts.map +1 -1
- package/dist/cjs/callsite/index.js +102 -3
- package/dist/cjs/callsite/index.js.map +2 -2
- package/dist/cjs/color/index.d.ts +7 -3
- package/dist/cjs/color/index.d.ts.map +1 -1
- package/dist/cjs/color/index.js +6 -1
- package/dist/cjs/color/index.js.map +2 -2
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/label/index.d.ts.map +1 -1
- package/dist/cjs/label/index.js +2 -1
- package/dist/cjs/label/index.js.map +2 -2
- package/dist/cjs/linefill/index.d.ts.map +1 -1
- package/dist/cjs/linefill/index.js +1 -0
- package/dist/cjs/linefill/index.js.map +2 -2
- package/dist/cjs/math/index.d.ts +79 -22
- package/dist/cjs/math/index.d.ts.map +1 -1
- package/dist/cjs/math/index.js +157 -43
- package/dist/cjs/math/index.js.map +3 -3
- package/dist/cjs/matrix/index.d.ts.map +1 -1
- package/dist/cjs/matrix/index.js +42 -36
- package/dist/cjs/matrix/index.js.map +2 -2
- package/dist/cjs/runtime/series.d.ts +9 -0
- package/dist/cjs/runtime/series.d.ts.map +1 -1
- package/dist/cjs/runtime/series.js +16 -2
- package/dist/cjs/runtime/series.js.map +2 -2
- package/dist/cjs/script/index.d.ts +15 -31
- package/dist/cjs/script/index.d.ts.map +1 -1
- package/dist/cjs/script/index.js +32 -0
- package/dist/cjs/script/index.js.map +2 -2
- package/dist/cjs/session/calendar.d.ts.map +1 -1
- package/dist/cjs/session/calendar.js +15 -2
- package/dist/cjs/session/calendar.js.map +2 -2
- package/dist/cjs/str/index.d.ts +50 -38
- package/dist/cjs/str/index.d.ts.map +1 -1
- package/dist/cjs/str/index.js +43 -15
- package/dist/cjs/str/index.js.map +2 -2
- package/dist/cjs/str/messageformat.d.ts +2 -1
- package/dist/cjs/str/messageformat.d.ts.map +1 -1
- package/dist/cjs/str/messageformat.js +4 -1
- package/dist/cjs/str/messageformat.js.map +2 -2
- package/dist/cjs/str/numberformat.d.ts +4 -0
- package/dist/cjs/str/numberformat.d.ts.map +1 -1
- package/dist/cjs/str/numberformat.js +26 -4
- package/dist/cjs/str/numberformat.js.map +3 -3
- package/dist/cjs/ta/index.d.ts +95 -46
- package/dist/cjs/ta/index.d.ts.map +1 -1
- package/dist/cjs/ta/index.js +193 -318
- package/dist/cjs/ta/index.js.map +3 -3
- package/dist/cjs/ta/running-sum.d.ts +54 -1
- package/dist/cjs/ta/running-sum.d.ts.map +1 -1
- package/dist/cjs/ta/running-sum.js +132 -47
- package/dist/cjs/ta/running-sum.js.map +2 -2
- package/dist/cjs/ta-series.d.ts +4 -4
- package/dist/cjs/ta-series.d.ts.map +1 -1
- package/dist/cjs/ta-series.js +4 -3
- package/dist/cjs/ta-series.js.map +2 -2
- package/dist/cjs/time/datestring.d.ts +1 -0
- package/dist/cjs/time/datestring.d.ts.map +1 -1
- package/dist/cjs/time/datestring.js +7 -0
- package/dist/cjs/time/datestring.js.map +2 -2
- package/dist/cjs/time/index.d.ts.map +1 -1
- package/dist/cjs/time/index.js +2 -1
- package/dist/cjs/time/index.js.map +2 -2
- package/dist/cjs/time/session.d.ts +28 -9
- package/dist/cjs/time/session.d.ts.map +1 -1
- package/dist/cjs/time/session.js +74 -28
- package/dist/cjs/time/session.js.map +2 -2
- package/dist/cjs/time/timezone.d.ts +5 -0
- package/dist/cjs/time/timezone.d.ts.map +1 -1
- package/dist/cjs/time/timezone.js +15 -1
- package/dist/cjs/time/timezone.js.map +2 -2
- package/dist/esm/array/index.d.ts +88 -27
- package/dist/esm/array/index.d.ts.map +1 -1
- package/dist/esm/array/index.js +408 -157
- package/dist/esm/array/index.js.map +1 -1
- package/dist/esm/callsite/index.d.ts +122 -4
- package/dist/esm/callsite/index.d.ts.map +1 -1
- package/dist/esm/callsite/index.js +228 -8
- package/dist/esm/callsite/index.js.map +1 -1
- package/dist/esm/color/index.d.ts +7 -3
- package/dist/esm/color/index.d.ts.map +1 -1
- package/dist/esm/color/index.js +15 -3
- package/dist/esm/color/index.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/label/index.d.ts.map +1 -1
- package/dist/esm/label/index.js +2 -1
- package/dist/esm/label/index.js.map +1 -1
- package/dist/esm/linefill/index.d.ts.map +1 -1
- package/dist/esm/linefill/index.js +3 -0
- package/dist/esm/linefill/index.js.map +1 -1
- package/dist/esm/math/index.d.ts +79 -22
- package/dist/esm/math/index.d.ts.map +1 -1
- package/dist/esm/math/index.js +229 -63
- package/dist/esm/math/index.js.map +1 -1
- package/dist/esm/matrix/index.d.ts.map +1 -1
- package/dist/esm/matrix/index.js +57 -43
- package/dist/esm/matrix/index.js.map +1 -1
- package/dist/esm/runtime/series.d.ts +9 -0
- package/dist/esm/runtime/series.d.ts.map +1 -1
- package/dist/esm/runtime/series.js +18 -1
- package/dist/esm/runtime/series.js.map +1 -1
- package/dist/esm/script/index.d.ts +15 -31
- package/dist/esm/script/index.d.ts.map +1 -1
- package/dist/esm/script/index.js +44 -1
- package/dist/esm/script/index.js.map +1 -1
- package/dist/esm/session/calendar.d.ts.map +1 -1
- package/dist/esm/session/calendar.js +22 -2
- package/dist/esm/session/calendar.js.map +1 -1
- package/dist/esm/str/index.d.ts +50 -38
- package/dist/esm/str/index.d.ts.map +1 -1
- package/dist/esm/str/index.js +103 -52
- package/dist/esm/str/index.js.map +1 -1
- package/dist/esm/str/messageformat.d.ts +2 -1
- package/dist/esm/str/messageformat.d.ts.map +1 -1
- package/dist/esm/str/messageformat.js +7 -2
- package/dist/esm/str/messageformat.js.map +1 -1
- package/dist/esm/str/numberformat.d.ts +4 -0
- package/dist/esm/str/numberformat.d.ts.map +1 -1
- package/dist/esm/str/numberformat.js +38 -4
- package/dist/esm/str/numberformat.js.map +1 -1
- package/dist/esm/ta/index.d.ts +95 -46
- package/dist/esm/ta/index.d.ts.map +1 -1
- package/dist/esm/ta/index.js +341 -414
- package/dist/esm/ta/index.js.map +1 -1
- package/dist/esm/ta/running-sum.d.ts +54 -1
- package/dist/esm/ta/running-sum.d.ts.map +1 -1
- package/dist/esm/ta/running-sum.js +166 -60
- package/dist/esm/ta/running-sum.js.map +1 -1
- package/dist/esm/ta-series.d.ts +4 -4
- package/dist/esm/ta-series.d.ts.map +1 -1
- package/dist/esm/ta-series.js +5 -4
- package/dist/esm/ta-series.js.map +1 -1
- package/dist/esm/time/datestring.d.ts +1 -0
- package/dist/esm/time/datestring.d.ts.map +1 -1
- package/dist/esm/time/datestring.js +12 -0
- package/dist/esm/time/datestring.js.map +1 -1
- package/dist/esm/time/index.d.ts.map +1 -1
- package/dist/esm/time/index.js +3 -1
- package/dist/esm/time/index.js.map +1 -1
- package/dist/esm/time/session.d.ts +28 -9
- package/dist/esm/time/session.d.ts.map +1 -1
- package/dist/esm/time/session.js +113 -42
- package/dist/esm/time/session.js.map +1 -1
- package/dist/esm/time/timezone.d.ts +5 -0
- package/dist/esm/time/timezone.d.ts.map +1 -1
- package/dist/esm/time/timezone.js +21 -1
- package/dist/esm/time/timezone.js.map +1 -1
- package/package.json +2 -2
- package/src/array/index.ts +382 -174
- package/src/callsite/index.ts +232 -8
- package/src/color/index.ts +15 -5
- package/src/index.ts +1 -1
- package/src/label/index.ts +2 -1
- package/src/linefill/index.ts +2 -0
- package/src/math/index.ts +261 -77
- package/src/matrix/index.ts +52 -52
- package/src/runtime/series.ts +22 -1
- package/src/script/index.ts +53 -1
- package/src/session/calendar.ts +22 -2
- package/src/str/index.ts +95 -51
- package/src/str/messageformat.ts +7 -2
- package/src/str/numberformat.ts +39 -4
- package/src/ta/index.ts +356 -458
- package/src/ta/running-sum.ts +187 -62
- package/src/ta-series.ts +8 -7
- package/src/time/datestring.ts +10 -0
- package/src/time/index.ts +3 -1
- package/src/time/session.ts +97 -47
- package/src/time/timezone.ts +21 -1
package/src/ta/running-sum.ts
CHANGED
|
@@ -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
|
|
28
|
-
*
|
|
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
|
|
33
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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)
|
|
80
|
-
|
|
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 =
|
|
83
|
-
for (let 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
|
-
|
|
104
|
+
this.stepY = x;
|
|
87
105
|
} else {
|
|
88
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
*
|
|
98
|
-
*
|
|
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
|
|
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
|
-
|
|
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 =
|
|
111
|
-
const sumSquares =
|
|
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
|
-
|
|
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[];
|
package/src/time/datestring.ts
CHANGED
|
@@ -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
|
|
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();
|
package/src/time/session.ts
CHANGED
|
@@ -1,78 +1,128 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* PineScript session strings ("0930-1600", "0930-1600:23456", "1000-1100,1400-1500:23456", "0930-1600|1000-1300:7",
|
|
3
|
+
* "24x7").
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
* - a
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
49
|
-
if (!/^[1-7]+$/.test(
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
75
|
-
if (minute < end && days
|
|
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;
|
package/src/time/timezone.ts
CHANGED
|
@@ -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 =
|
|
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
|