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/index.ts
CHANGED
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
|
|
11
11
|
import type { series_float, series_bool, series_int, int, Source, simple_int, simple_float, simple_bool } from '../types/index.js';
|
|
12
12
|
import { eq, ge, gt, le, lt } from '../compare/index.js';
|
|
13
|
-
import { runningSum, runningVariance } from './running-sum.js';
|
|
13
|
+
import { runningSum, runningSumSeries, runningVariance, runningVarianceSeries } from './running-sum.js';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Simple Moving Average - returns the moving average (sum of last y values divided by y).
|
|
17
17
|
*
|
|
18
18
|
* @param source - Series of values to process
|
|
19
|
-
* @param length - Number of bars (length)
|
|
19
|
+
* @param length - Number of bars (length), or one length per bar (a series length)
|
|
20
20
|
* @returns Simple moving average of source for length bars back
|
|
21
21
|
*
|
|
22
22
|
* @remarks
|
|
@@ -25,30 +25,26 @@ import { runningSum, runningVariance } from './running-sum.js';
|
|
|
25
25
|
* - Returns NaN until `length` non-`na` values are available
|
|
26
26
|
* - The sum is a compensated running sum, as in PineScript, so the result can differ from the exact window mean in
|
|
27
27
|
* the last bits (bit for bit with PineScript)
|
|
28
|
+
* - With a series length, the running sum is moved to the new length when the length changes, as PineScript does
|
|
29
|
+
* (see {@link math.sum}); a bar with an `na` source gives the mean of the last `length` earlier values
|
|
28
30
|
*
|
|
29
31
|
* @example
|
|
30
32
|
* ```typescript
|
|
31
33
|
* const closePrices = [10, 11, 12, 13, 14];
|
|
32
34
|
* const sma5 = ta.sma(closePrices, 5); // Returns: [NaN, NaN, NaN, NaN, 12]
|
|
35
|
+
* const smaVar = ta.sma(closePrices, [2, 2, 2, 3, 2]); // Returns: [NaN, 10.5, 11.5, 12, 13.5]
|
|
33
36
|
* ```
|
|
34
37
|
*/
|
|
35
|
-
export function sma(source: Source, length: simple_int): series_float {
|
|
38
|
+
export function sma(source: Source, length: simple_int | ArrayLike<number>): series_float {
|
|
39
|
+
if (typeof length !== 'number') {
|
|
40
|
+
const sums = runningSumSeries(source, length, 'ta.sma');
|
|
41
|
+
return sums.map((sum, i) => sum / Math.floor(length[i]!));
|
|
42
|
+
}
|
|
36
43
|
// Floor the length to match PineScript's auto-truncation of float to int
|
|
37
44
|
const len = Math.floor(length);
|
|
38
45
|
return runningSum(source, len).map((sum) => sum / len);
|
|
39
46
|
}
|
|
40
47
|
|
|
41
|
-
/** Mean of the last `length` bars; na when the window holds an na value (used by ta.dev). */
|
|
42
|
-
function strictWindowMean(source: Source, length: simple_int): series_float {
|
|
43
|
-
const len = Math.floor(length);
|
|
44
|
-
return Array.from({ length: source.length }, (_, i) => {
|
|
45
|
-
if (i < len - 1) return NaN;
|
|
46
|
-
let sum = 0;
|
|
47
|
-
for (let j = 0; j < len; j++) sum += source[i - j]!;
|
|
48
|
-
return sum / len;
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
48
|
/**
|
|
53
49
|
* Exponential Moving Average - returns the exponentially weighted moving average.
|
|
54
50
|
*
|
|
@@ -74,26 +70,21 @@ export function ema(source: Source, length: simple_int): series_float {
|
|
|
74
70
|
const len = Math.floor(length);
|
|
75
71
|
const multiplier = 2 / (len + 1);
|
|
76
72
|
|
|
77
|
-
// SMA seed
|
|
78
|
-
// An infinite value is na, as in PineScript.
|
|
73
|
+
// SMA seed on the bar of the `len`-th valid value. An infinite value is na, as in PineScript.
|
|
79
74
|
let firstValidIndex = -1;
|
|
80
75
|
let validCount = 0;
|
|
81
|
-
let initSum = 0;
|
|
82
76
|
|
|
83
77
|
for (let i = 0; i < source.length; i++) {
|
|
84
78
|
const val = source[i];
|
|
85
|
-
if (val !== undefined && Number.isFinite(val)) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (validCount === len) {
|
|
89
|
-
firstValidIndex = i;
|
|
90
|
-
break;
|
|
91
|
-
}
|
|
79
|
+
if (val !== undefined && Number.isFinite(val) && ++validCount === len) {
|
|
80
|
+
firstValidIndex = i;
|
|
81
|
+
break;
|
|
92
82
|
}
|
|
93
83
|
}
|
|
94
84
|
|
|
95
|
-
let emaValue = validCount > 0 ? initSum / validCount : NaN;
|
|
96
85
|
const emaInitialized = firstValidIndex >= 0;
|
|
86
|
+
// The seed is ta.sma on that bar: the running sum of math.sum (PineScript bits), not a plain loop sum
|
|
87
|
+
let emaValue = emaInitialized ? runningSum(source.slice(0, firstValidIndex + 1), len)[firstValidIndex]! / len : NaN;
|
|
97
88
|
|
|
98
89
|
for (let i = 0; i < source.length; i++) {
|
|
99
90
|
if (!emaInitialized || i < firstValidIndex) {
|
|
@@ -261,7 +252,7 @@ export function bb(
|
|
|
261
252
|
* Standard deviation over the last `length` non-na values.
|
|
262
253
|
*
|
|
263
254
|
* @param source - Series of values to process
|
|
264
|
-
* @param length - Number of values
|
|
255
|
+
* @param length - Number of values, or one length per bar (a series length, see {@link sma})
|
|
265
256
|
* @param biased - true (default): biased estimate (divides by `length`); false: unbiased (by `length - 1`)
|
|
266
257
|
* @returns Standard deviation series (na until `length` non-na values exist)
|
|
267
258
|
*
|
|
@@ -271,8 +262,12 @@ export function bb(
|
|
|
271
262
|
* The result is the square root of {@link variance} (0 when the variance is not positive), computed from
|
|
272
263
|
* compensated running sums of the values and of their squares, bit for bit as PineScript.
|
|
273
264
|
*/
|
|
274
|
-
export function stdev(
|
|
275
|
-
|
|
265
|
+
export function stdev(
|
|
266
|
+
source: Source,
|
|
267
|
+
length: simple_int | ArrayLike<number>,
|
|
268
|
+
biased: simple_bool = true
|
|
269
|
+
): series_float {
|
|
270
|
+
return varianceOf(source, length, biased, 'ta.stdev').map((v) => (Number.isNaN(v) ? NaN : v > 0 ? Math.sqrt(v) : 0));
|
|
276
271
|
}
|
|
277
272
|
|
|
278
273
|
/**
|
|
@@ -350,6 +345,32 @@ function extremeValue(source: Source, length: simple_int, lowest: boolean): seri
|
|
|
350
345
|
});
|
|
351
346
|
}
|
|
352
347
|
|
|
348
|
+
/**
|
|
349
|
+
* Compares each bar with the last earlier bar where both values were not na (PineScript rule for ta.crossover,
|
|
350
|
+
* ta.crossunder and ta.cross): false while there is no such bar, and false on a bar with an na value.
|
|
351
|
+
*
|
|
352
|
+
* @internal
|
|
353
|
+
*/
|
|
354
|
+
function crossTest(
|
|
355
|
+
series1: Source,
|
|
356
|
+
series2: Source,
|
|
357
|
+
test: (a: number, b: number, prevA: number, prevB: number) => boolean,
|
|
358
|
+
): series_bool {
|
|
359
|
+
const result: series_bool = [];
|
|
360
|
+
let prevA = NaN;
|
|
361
|
+
let prevB = NaN;
|
|
362
|
+
for (let i = 0; i < series1.length; i++) {
|
|
363
|
+
const a = series1[i]!;
|
|
364
|
+
const b = series2[i]!;
|
|
365
|
+
result.push(test(a, b, prevA, prevB));
|
|
366
|
+
if (!Number.isNaN(a) && !Number.isNaN(b)) {
|
|
367
|
+
prevA = a;
|
|
368
|
+
prevB = b;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return result;
|
|
372
|
+
}
|
|
373
|
+
|
|
353
374
|
/**
|
|
354
375
|
* Crossover - returns true when series1 crosses over series2 (moves from below to above).
|
|
355
376
|
*
|
|
@@ -358,8 +379,10 @@ function extremeValue(source: Source, length: simple_int, lowest: boolean): seri
|
|
|
358
379
|
* @returns Boolean series (true at crossover points)
|
|
359
380
|
*
|
|
360
381
|
* @remarks
|
|
361
|
-
* - True when: series1[i]
|
|
382
|
+
* - True when: series1[i] > series2[i] AND series1[j] <= series2[j], where j is the last bar before i where both
|
|
383
|
+
* values were not na (PineScript rule: a crossing right after an na bar is not missed)
|
|
362
384
|
* - First value is always false (no previous value to compare)
|
|
385
|
+
* - Comparisons are exact (no tolerance)
|
|
363
386
|
* - Useful for detecting bullish signals (e.g., fast MA crossing over slow MA)
|
|
364
387
|
*
|
|
365
388
|
* @example
|
|
@@ -368,17 +391,7 @@ function extremeValue(source: Source, length: simple_int, lowest: boolean): seri
|
|
|
368
391
|
* ```
|
|
369
392
|
*/
|
|
370
393
|
export function crossover(series1: Source, series2: Source): series_bool {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
for (let i = 0; i < series1.length; i++) {
|
|
374
|
-
if (i === 0) {
|
|
375
|
-
result.push(false);
|
|
376
|
-
} else {
|
|
377
|
-
result.push(series1[i]! > series2[i]! && series1[i - 1]! <= series2[i - 1]!);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
return result;
|
|
394
|
+
return crossTest(series1, series2, (a, b, pa, pb) => a > b && pa <= pb);
|
|
382
395
|
}
|
|
383
396
|
|
|
384
397
|
/**
|
|
@@ -389,8 +402,10 @@ export function crossover(series1: Source, series2: Source): series_bool {
|
|
|
389
402
|
* @returns Boolean series (true at crossunder points)
|
|
390
403
|
*
|
|
391
404
|
* @remarks
|
|
392
|
-
* - True when: series1[i]
|
|
405
|
+
* - True when: series1[i] < series2[i] AND series1[j] >= series2[j], where j is the last bar before i where both
|
|
406
|
+
* values were not na
|
|
393
407
|
* - First value is always false (no previous value to compare)
|
|
408
|
+
* - Comparisons are exact (no tolerance)
|
|
394
409
|
* - Useful for detecting bearish signals (e.g., fast MA crossing under slow MA)
|
|
395
410
|
*
|
|
396
411
|
* @example
|
|
@@ -399,17 +414,7 @@ export function crossover(series1: Source, series2: Source): series_bool {
|
|
|
399
414
|
* ```
|
|
400
415
|
*/
|
|
401
416
|
export function crossunder(series1: Source, series2: Source): series_bool {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
for (let i = 0; i < series1.length; i++) {
|
|
405
|
-
if (i === 0) {
|
|
406
|
-
result.push(false);
|
|
407
|
-
} else {
|
|
408
|
-
result.push(series1[i]! < series2[i]! && series1[i - 1]! >= series2[i - 1]!);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
return result;
|
|
417
|
+
return crossTest(series1, series2, (a, b, pa, pb) => a < b && pa >= pb);
|
|
413
418
|
}
|
|
414
419
|
|
|
415
420
|
/**
|
|
@@ -672,7 +677,8 @@ export function supertrend(
|
|
|
672
677
|
* @remarks
|
|
673
678
|
* - Moving average used in RSI calculation
|
|
674
679
|
* - Alpha = 1 / length (different from EMA which uses alpha = 2 / (length + 1))
|
|
675
|
-
* - First value is initialized with SMA, then uses exponential smoothing
|
|
680
|
+
* - First value is initialized with SMA, then uses exponential smoothing. As in PineScript, the seed is the
|
|
681
|
+
* `ta.sma` value (its compensated running sum), so it can differ from the exact mean in the last bits
|
|
676
682
|
* - Formula: `RMA = (source + (length - 1) * RMA[1]) / length`, evaluated in this order as in PineScript
|
|
677
683
|
* (equal to `alpha * source + (1 - alpha) * RMA[1]`, but bit for bit)
|
|
678
684
|
* - `na` values (also +/-Infinity) in the source series are ignored
|
|
@@ -695,23 +701,18 @@ export function rma(source: Source, length: simple_int): series_float {
|
|
|
695
701
|
// An infinite value is na, as in PineScript.
|
|
696
702
|
let firstValidIndex = -1;
|
|
697
703
|
let validCount = 0;
|
|
698
|
-
let initSum = 0;
|
|
699
704
|
|
|
700
705
|
for (let i = 0; i < source.length; i++) {
|
|
701
706
|
const val = source[i];
|
|
702
|
-
if (val !== undefined && Number.isFinite(val)) {
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
if (validCount === len) {
|
|
706
|
-
firstValidIndex = i;
|
|
707
|
-
break;
|
|
708
|
-
}
|
|
707
|
+
if (val !== undefined && Number.isFinite(val) && ++validCount === len) {
|
|
708
|
+
firstValidIndex = i;
|
|
709
|
+
break;
|
|
709
710
|
}
|
|
710
711
|
}
|
|
711
712
|
|
|
712
|
-
// Initialize RMA value with SMA of first `length` non-NaN values
|
|
713
|
-
let rmaValue = validCount > 0 ? initSum / validCount : NaN;
|
|
714
713
|
const rmaInitialized = firstValidIndex >= 0;
|
|
714
|
+
// The seed is ta.sma on that bar: the running sum of math.sum (PineScript bits), not a plain loop sum
|
|
715
|
+
let rmaValue = rmaInitialized ? runningSum(source.slice(0, firstValidIndex + 1), len)[firstValidIndex]! / len : NaN;
|
|
715
716
|
|
|
716
717
|
for (let i = 0; i < source.length; i++) {
|
|
717
718
|
if (!rmaInitialized || i < firstValidIndex) {
|
|
@@ -737,6 +738,15 @@ export function rma(source: Source, length: simple_int): series_float {
|
|
|
737
738
|
return result;
|
|
738
739
|
}
|
|
739
740
|
|
|
741
|
+
/**
|
|
742
|
+
* One step of the PineScript ta.wma sum: `a + b`, or 0 when `|a + b| <= 1e-10 * max(1, |a| + |b|)`.
|
|
743
|
+
* Measured bit for bit: a sum that cancels to a tiny residue (relative to the operands, or below 1e-10) is 0.
|
|
744
|
+
*/
|
|
745
|
+
function wmaAdd(a: number, b: number): number {
|
|
746
|
+
const sum = a + b;
|
|
747
|
+
return Math.abs(sum) <= 1e-10 * Math.max(1, Math.abs(a) + Math.abs(b)) ? 0 : sum;
|
|
748
|
+
}
|
|
749
|
+
|
|
740
750
|
/**
|
|
741
751
|
* Weighted Moving Average (WMA) - moving average with linearly decreasing weights.
|
|
742
752
|
*
|
|
@@ -750,6 +760,8 @@ export function rma(source: Source, length: simple_int): series_float {
|
|
|
750
760
|
* - Formula: `sum(source[i]! * (length - i)) / sum(length - i)` for i = 0 to length-1
|
|
751
761
|
* - As in PineScript: na on a bar whose source is `na` (also +/-Infinity) and until `length` non-`na` values were
|
|
752
762
|
* received; an `na` value inside the window is replaced by the previous non-`na` value
|
|
763
|
+
* - Summed from the oldest value to the newest; as PineScript, a partial sum `s + t` with
|
|
764
|
+
* `|s + t| <= 1e-10 * max(1, |s| + |t|)` is 0 (a cancellation residue, or values below 1e-10)
|
|
753
765
|
* - More responsive to recent price changes than SMA
|
|
754
766
|
*
|
|
755
767
|
* @example
|
|
@@ -784,7 +796,7 @@ export function wma(source: Source, length: simple_int): series_float {
|
|
|
784
796
|
let weightSum = 0;
|
|
785
797
|
for (let j = len - 1; j >= 0; j--) {
|
|
786
798
|
const weight = len - j;
|
|
787
|
-
sum
|
|
799
|
+
sum = wmaAdd(sum, filled[i - j]! * weight);
|
|
788
800
|
weightSum += weight;
|
|
789
801
|
}
|
|
790
802
|
result.push(sum / weightSum);
|
|
@@ -859,10 +871,12 @@ export function cum(source: Source): series_float {
|
|
|
859
871
|
* @returns Boolean series (true at cross points)
|
|
860
872
|
*
|
|
861
873
|
* @remarks
|
|
862
|
-
*
|
|
863
|
-
*
|
|
864
|
-
*
|
|
865
|
-
* -
|
|
874
|
+
* PineScript rule (measured bit for bit; not `crossover or crossunder`):
|
|
875
|
+
* - The call keeps the side of the last bar where `source1` was strictly above or strictly below `source2` (no
|
|
876
|
+
* side before such a bar). Equal bars and bars with an `na` value do not change it.
|
|
877
|
+
* - True when the values are strictly on the other side of the kept side: so a run of equal values is passed, and
|
|
878
|
+
* a series that starts equal and then moves away is not a cross
|
|
879
|
+
* - False on a bar with an `na` value
|
|
866
880
|
* - Use `ta.crossover()` or `ta.crossunder()` for directional crosses
|
|
867
881
|
*
|
|
868
882
|
* @example
|
|
@@ -873,36 +887,36 @@ export function cum(source: Source): series_float {
|
|
|
873
887
|
*/
|
|
874
888
|
export function cross(source1: Source, source2: Source): series_bool {
|
|
875
889
|
const result: series_bool = [];
|
|
876
|
-
|
|
890
|
+
let side = 0; // -1: last strictly below, 1: last strictly above, 0: none yet
|
|
877
891
|
for (let i = 0; i < source1.length; i++) {
|
|
878
|
-
|
|
892
|
+
const a = source1[i]!;
|
|
893
|
+
const b = source2[i]!;
|
|
894
|
+
if (Number.isNaN(a) || Number.isNaN(b)) {
|
|
879
895
|
result.push(false);
|
|
880
|
-
|
|
881
|
-
const crossedUp = source1[i]! > source2[i]! && source1[i - 1]! <= source2[i - 1]!;
|
|
882
|
-
const crossedDown = source1[i]! < source2[i]! && source1[i - 1]! >= source2[i - 1]!;
|
|
883
|
-
result.push(crossedUp || crossedDown);
|
|
896
|
+
continue;
|
|
884
897
|
}
|
|
898
|
+
result.push((side < 0 && a > b) || (side > 0 && a < b));
|
|
899
|
+
if (a < b) side = -1;
|
|
900
|
+
else if (a > b) side = 1;
|
|
885
901
|
}
|
|
886
|
-
|
|
887
902
|
return result;
|
|
888
903
|
}
|
|
889
904
|
|
|
890
905
|
/**
|
|
891
|
-
* ta.rising / ta.falling:
|
|
892
|
-
*
|
|
906
|
+
* ta.rising / ta.falling: a counter of the steps from one bar to the next that pass `step(newer, older)`, reset to 0
|
|
907
|
+
* by a step that does not pass; a step with an na value (on either bar) leaves the counter unchanged. True when the
|
|
908
|
+
* counter is at least `length`.
|
|
893
909
|
*
|
|
894
910
|
* @internal
|
|
895
911
|
*/
|
|
896
912
|
function monotonic(source: Source, length: simple_int, step: (newer: number, older: number) => boolean): series_bool {
|
|
897
913
|
const result: series_bool = [];
|
|
898
|
-
|
|
914
|
+
let count = 0;
|
|
899
915
|
for (let i = 0; i < source.length; i++) {
|
|
900
916
|
const v = source[i]!;
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
for (let j = 1; ok && j <= length; j++) ok = step(values[n - j]!, values[n - j - 1]!);
|
|
905
|
-
result.push(ok);
|
|
917
|
+
const prev = i > 0 ? source[i - 1]! : NaN;
|
|
918
|
+
if (!Number.isNaN(v) && !Number.isNaN(prev)) count = step(v, prev) ? count + 1 : 0;
|
|
919
|
+
result.push(count >= length);
|
|
906
920
|
}
|
|
907
921
|
return result;
|
|
908
922
|
}
|
|
@@ -916,10 +930,11 @@ function monotonic(source: Source, length: simple_int, step: (newer: number, old
|
|
|
916
930
|
*
|
|
917
931
|
* @remarks
|
|
918
932
|
* PineScript rules:
|
|
919
|
-
* -
|
|
920
|
-
*
|
|
921
|
-
*
|
|
922
|
-
*
|
|
933
|
+
* - a counter of rises from the previous bar (a rise is larger than the 1e-10 tolerance); a step that is not a rise
|
|
934
|
+
* resets it to 0
|
|
935
|
+
* - a step from or to an `na` bar leaves the counter unchanged (so an `na` bar, and the bar after it, keep the
|
|
936
|
+
* result of the bar before the `na`)
|
|
937
|
+
* - true when the counter is at least `length` (false at the start)
|
|
923
938
|
*
|
|
924
939
|
* @example
|
|
925
940
|
* ```typescript
|
|
@@ -940,10 +955,11 @@ export function rising(source: Source, length: simple_int): series_bool {
|
|
|
940
955
|
*
|
|
941
956
|
* @remarks
|
|
942
957
|
* PineScript rules:
|
|
943
|
-
* -
|
|
944
|
-
*
|
|
945
|
-
*
|
|
946
|
-
*
|
|
958
|
+
* - a counter of falls from the previous bar (a fall is larger than the 1e-10 tolerance); a step that is not a fall
|
|
959
|
+
* resets it to 0
|
|
960
|
+
* - a step from or to an `na` bar leaves the counter unchanged (so an `na` bar, and the bar after it, keep the
|
|
961
|
+
* result of the bar before the `na`)
|
|
962
|
+
* - true when the counter is at least `length` (false at the start)
|
|
947
963
|
*
|
|
948
964
|
* @example
|
|
949
965
|
* ```typescript
|
|
@@ -1043,10 +1059,11 @@ export function mom(source: Source, length: simple_int): series_float {
|
|
|
1043
1059
|
*
|
|
1044
1060
|
* @remarks
|
|
1045
1061
|
* - Measures average absolute distance from the mean
|
|
1046
|
-
* - Formula: `sum(abs(source[i]! - sma)) / length` for i in 0 to length-1
|
|
1047
1062
|
* - Less sensitive to outliers than standard deviation
|
|
1048
|
-
* - `
|
|
1049
|
-
*
|
|
1063
|
+
* - PineScript rule (bit for bit): `sum(abs(source[i] - mean)) / length` for `i = 0 .. length - 1`, added newest
|
|
1064
|
+
* first, with `mean = ta.sma(source, length)` (its compensated running sum, so a flat window can give a tiny
|
|
1065
|
+
* non-zero deviation)
|
|
1066
|
+
* - The loop reads the last `length` bars: an `na` among them gives `na`
|
|
1050
1067
|
*
|
|
1051
1068
|
* @example
|
|
1052
1069
|
* ```typescript
|
|
@@ -1055,25 +1072,13 @@ export function mom(source: Source, length: simple_int): series_float {
|
|
|
1055
1072
|
* ```
|
|
1056
1073
|
*/
|
|
1057
1074
|
export function dev(source: Source, length: simple_int): series_float {
|
|
1058
|
-
const
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
} else {
|
|
1066
|
-
let sum = 0;
|
|
1067
|
-
for (let j = 0; j < length; j++) {
|
|
1068
|
-
if (!isNaN(source[i - j]!)) {
|
|
1069
|
-
sum += Math.abs(source[i - j]! - meanValues[i]!);
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
result.push(sum / length);
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
return result;
|
|
1075
|
+
const len = Math.floor(length);
|
|
1076
|
+
return sma(source, len).map((mean, i) => {
|
|
1077
|
+
if (i < len - 1 || Number.isNaN(mean)) return NaN;
|
|
1078
|
+
let sum = 0;
|
|
1079
|
+
for (let k = 0; k < len; k++) sum += Math.abs(source[i - k]! - mean);
|
|
1080
|
+
return sum / len;
|
|
1081
|
+
});
|
|
1077
1082
|
}
|
|
1078
1083
|
|
|
1079
1084
|
/**
|
|
@@ -1102,7 +1107,25 @@ export function dev(source: Source, length: simple_int): series_float {
|
|
|
1102
1107
|
* const sampleVariance = ta.variance(closePrices, 20, false);
|
|
1103
1108
|
* ```
|
|
1104
1109
|
*/
|
|
1105
|
-
export function variance(
|
|
1110
|
+
export function variance(
|
|
1111
|
+
source: Source,
|
|
1112
|
+
length: simple_int | ArrayLike<number>,
|
|
1113
|
+
biased: simple_bool = true
|
|
1114
|
+
): series_float {
|
|
1115
|
+
return varianceOf(source, length, biased, 'ta.variance');
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/** {@link variance} with a fixed or a series length; `name` is used in the error messages. */
|
|
1119
|
+
function varianceOf(
|
|
1120
|
+
source: Source,
|
|
1121
|
+
length: simple_int | ArrayLike<number>,
|
|
1122
|
+
biased: boolean,
|
|
1123
|
+
name: string
|
|
1124
|
+
): series_float {
|
|
1125
|
+
if (typeof length !== 'number') {
|
|
1126
|
+
const result = runningVarianceSeries(source, length, biased, name);
|
|
1127
|
+
return biased ? result : result.map((v, i) => (Math.floor(length[i]!) > 1 ? v : NaN));
|
|
1128
|
+
}
|
|
1106
1129
|
const len = Math.floor(length);
|
|
1107
1130
|
// PineScript: the last `len` non-na values (also +/-Infinity skipped), as ta.sma
|
|
1108
1131
|
if ((biased ? len : len - 1) <= 0) return source.map(() => NaN);
|
|
@@ -1174,7 +1197,7 @@ export function median(source: Source, length: simple_int): series_float {
|
|
|
1174
1197
|
* @remarks
|
|
1175
1198
|
* - Fixed length of 4 bars
|
|
1176
1199
|
* - Weights: [1/6, 2/6, 2/6, 1/6] (symmetric)
|
|
1177
|
-
* - Formula: `source
|
|
1200
|
+
* - Formula, in this order as in PineScript (bit for bit): `(source + 2 * source[1] + 2 * source[2] + source[3]) / 6`
|
|
1178
1201
|
* - More weight given to middle values
|
|
1179
1202
|
* - `na` values in the source series are included in calculations and will produce an `na` result
|
|
1180
1203
|
* - Returns NaN for the first 3 bars
|
|
@@ -1186,27 +1209,9 @@ export function median(source: Source, length: simple_int): series_float {
|
|
|
1186
1209
|
* ```
|
|
1187
1210
|
*/
|
|
1188
1211
|
export function swma(source: Source): series_float {
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
if (i < 3) {
|
|
1193
|
-
result.push(NaN);
|
|
1194
|
-
} else {
|
|
1195
|
-
// Check for any NaN values in the window
|
|
1196
|
-
if (isNaN(source[i]!) || isNaN(source[i - 1]!) || isNaN(source[i - 2]!) || isNaN(source[i - 3]!)) {
|
|
1197
|
-
result.push(NaN);
|
|
1198
|
-
} else {
|
|
1199
|
-
const value =
|
|
1200
|
-
source[i - 3]! * (1 / 6) +
|
|
1201
|
-
source[i - 2]! * (2 / 6) +
|
|
1202
|
-
source[i - 1]! * (2 / 6) +
|
|
1203
|
-
source[i]! * (1 / 6);
|
|
1204
|
-
result.push(value);
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
return result;
|
|
1212
|
+
return Array.from({ length: source.length }, (_, i) =>
|
|
1213
|
+
i < 3 ? NaN : (source[i]! + 2 * source[i - 1]! + 2 * source[i - 2]! + source[i - 3]!) / 6
|
|
1214
|
+
);
|
|
1210
1215
|
}
|
|
1211
1216
|
|
|
1212
1217
|
/**
|
|
@@ -1275,6 +1280,9 @@ export function vwma(source: Source, length: simple_int, volume?: Source): serie
|
|
|
1275
1280
|
* @remarks
|
|
1276
1281
|
* - Calculates line of best fit using least squares method
|
|
1277
1282
|
* - Formula: `linreg = intercept + slope * (length - 1 - offset)`
|
|
1283
|
+
* - As PineScript (bit for bit): x = 1 (oldest) .. length (newest), sums from the oldest value,
|
|
1284
|
+
* `slope = (length * sumXY - sumX * sumY) / (length * sumX2 - sumX * sumX)`,
|
|
1285
|
+
* `intercept = sumY / length - slope * sumX / length + slope`
|
|
1278
1286
|
* - offset=0 gives current fitted value, offset<0 gives future projection
|
|
1279
1287
|
* - `na` values in the source series are included in calculations and will produce an `na` result
|
|
1280
1288
|
* - Useful for trend detection and prediction
|
|
@@ -1306,27 +1314,23 @@ export function linreg(source: Source, length: simple_int, offset: simple_int =
|
|
|
1306
1314
|
if (hasNaN) {
|
|
1307
1315
|
result.push(NaN);
|
|
1308
1316
|
} else {
|
|
1309
|
-
//
|
|
1317
|
+
// Least squares as PineScript: x = 1 (oldest) .. len (newest), summed from the oldest value; the intercept
|
|
1318
|
+
// is the fitted value at x = 1
|
|
1310
1319
|
let sumX = 0;
|
|
1311
1320
|
let sumY = 0;
|
|
1312
1321
|
let sumXY = 0;
|
|
1313
1322
|
let sumX2 = 0;
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
const
|
|
1317
|
-
const y = source[i - (len - 1 - j)]!;
|
|
1323
|
+
for (let j = 0; j < len; j++) {
|
|
1324
|
+
const x = j + 1;
|
|
1325
|
+
const y = source[i - (len - 1 - j)]!;
|
|
1318
1326
|
sumX += x;
|
|
1319
1327
|
sumY += y;
|
|
1320
1328
|
sumXY += x * y;
|
|
1321
1329
|
sumX2 += x * x;
|
|
1322
1330
|
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
// Calculate linreg value at offset
|
|
1328
|
-
const x = len - 1 - offset;
|
|
1329
|
-
result.push(intercept + slope * x);
|
|
1331
|
+
const slope = (len * sumXY - sumX * sumY) / (len * sumX2 - sumX * sumX);
|
|
1332
|
+
const intercept = sumY / len - (slope * sumX) / len + slope;
|
|
1333
|
+
result.push(intercept + slope * (len - 1 - offset));
|
|
1330
1334
|
}
|
|
1331
1335
|
}
|
|
1332
1336
|
}
|
|
@@ -1348,8 +1352,9 @@ export function linreg(source: Source, length: simple_int, offset: simple_int =
|
|
|
1348
1352
|
* - -1 = perfect negative correlation
|
|
1349
1353
|
* - 0 = no correlation
|
|
1350
1354
|
* - Measures linear relationship between two series
|
|
1351
|
-
* - `
|
|
1352
|
-
*
|
|
1355
|
+
* - As PineScript: `cov = sma(x * y) - sma(x) * sma(y)`, `r = cov / sqrt(variance(x) * variance(y))` with the
|
|
1356
|
+
* compensated running sums of `ta.sma` / `ta.variance` (each skips its own `na` values); `|cov| <= 1e-10` gives 0, a
|
|
1357
|
+
* zero variance gives `na`
|
|
1353
1358
|
*
|
|
1354
1359
|
* @example
|
|
1355
1360
|
* ```typescript
|
|
@@ -1358,59 +1363,26 @@ export function linreg(source: Source, length: simple_int, offset: simple_int =
|
|
|
1358
1363
|
* ```
|
|
1359
1364
|
*/
|
|
1360
1365
|
export function correlation(source1: Source, source2: Source, length: simple_int): series_float {
|
|
1361
|
-
|
|
1362
|
-
//
|
|
1363
|
-
const
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
}
|
|
1382
|
-
if (m === 0) {
|
|
1383
|
-
result.push(NaN);
|
|
1384
|
-
continue;
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
// Calculate means
|
|
1388
|
-
let sum1 = 0;
|
|
1389
|
-
let sum2 = 0;
|
|
1390
|
-
for (let k = 0; k < m; k++) {
|
|
1391
|
-
sum1 += a[k]!;
|
|
1392
|
-
sum2 += b[k]!;
|
|
1393
|
-
}
|
|
1394
|
-
const mean1 = sum1 / m;
|
|
1395
|
-
const mean2 = sum2 / m;
|
|
1396
|
-
|
|
1397
|
-
// Calculate correlation components
|
|
1398
|
-
let numerator = 0;
|
|
1399
|
-
let sum1Sq = 0;
|
|
1400
|
-
let sum2Sq = 0;
|
|
1401
|
-
for (let k = 0; k < m; k++) {
|
|
1402
|
-
const dev1 = a[k]! - mean1;
|
|
1403
|
-
const dev2 = b[k]! - mean2;
|
|
1404
|
-
numerator += dev1 * dev2;
|
|
1405
|
-
sum1Sq += dev1 * dev1;
|
|
1406
|
-
sum2Sq += dev2 * dev2;
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
const denominator = Math.sqrt(sum1Sq * sum2Sq);
|
|
1410
|
-
result.push(denominator === 0 ? NaN : numerator / denominator);
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
return result;
|
|
1366
|
+
// PineScript: from compensated running sums, cov = sma(x * y) - sma(x) * sma(y), r = cov / sqrt(var(x) * var(y))
|
|
1367
|
+
// (biased variances); each running sum skips its own na values
|
|
1368
|
+
const product = Array.from({ length: source1.length }, (_, i) => {
|
|
1369
|
+
const v1 = source1[i];
|
|
1370
|
+
const v2 = source2[i];
|
|
1371
|
+
return v1 === null || v1 === undefined || v2 === null || v2 === undefined ? NaN : v1 * v2;
|
|
1372
|
+
});
|
|
1373
|
+
const meanXY = sma(product, length);
|
|
1374
|
+
const meanX = sma(source1, length);
|
|
1375
|
+
const meanY = sma(source2, length);
|
|
1376
|
+
const varX = variance(source1, length);
|
|
1377
|
+
const varY = variance(source2, length);
|
|
1378
|
+
return meanXY.map((mxy, i) => {
|
|
1379
|
+
const cov = mxy - meanX[i]! * meanY[i]!;
|
|
1380
|
+
if (Number.isNaN(cov)) return NaN;
|
|
1381
|
+
// a covariance within 1e-10 of 0 gives 0; a zero denominator gives na (a division by zero)
|
|
1382
|
+
if (Math.abs(cov) <= 1e-10) return 0;
|
|
1383
|
+
const denominator = Math.sqrt(varX[i]! * varY[i]!);
|
|
1384
|
+
return denominator === 0 ? NaN : cov / denominator;
|
|
1385
|
+
});
|
|
1414
1386
|
}
|
|
1415
1387
|
|
|
1416
1388
|
/**
|
|
@@ -1428,7 +1400,7 @@ export function correlation(source1: Source, source2: Source, length: simple_int
|
|
|
1428
1400
|
* - Useful for identifying relative strength within a period
|
|
1429
1401
|
*
|
|
1430
1402
|
* PineScript rules:
|
|
1431
|
-
* - percentrank = count(source[k] <= source, k = 1..length) / length
|
|
1403
|
+
* - percentrank = count(source[k] <= source, k = 1..length) * 100 / length (in this order: the PineScript bits)
|
|
1432
1404
|
* - a value from bar `length` on, when the current source is not `na`; an `na` value in the window counts as not
|
|
1433
1405
|
* `<=` the current value
|
|
1434
1406
|
* - the comparison uses the 1e-10 tolerance
|
|
@@ -1454,7 +1426,7 @@ export function percentrank(source: Source, length: simple_int): series_float {
|
|
|
1454
1426
|
for (let j = 1; j <= length; j++) {
|
|
1455
1427
|
if (le(source[i - j]!, currentValue)) countLessOrEqual++;
|
|
1456
1428
|
}
|
|
1457
|
-
result.push((countLessOrEqual
|
|
1429
|
+
result.push((countLessOrEqual * 100) / length);
|
|
1458
1430
|
}
|
|
1459
1431
|
|
|
1460
1432
|
return result;
|
|
@@ -1472,10 +1444,14 @@ export function percentrank(source: Source, length: simple_int): series_float {
|
|
|
1472
1444
|
* - Typical Price = (High + Low + Close) / 3
|
|
1473
1445
|
* - Mean Deviation = Average of absolute differences from mean
|
|
1474
1446
|
* - Scaled by 0.015 to provide more readable numbers
|
|
1475
|
-
* - As in PineScript: when the mean deviation is 0 (1e-10 tolerance, a flat window), the CCI of the previous bar
|
|
1476
1447
|
* - Values above +100 indicate overbought conditions
|
|
1477
1448
|
* - Values below -100 indicate oversold conditions
|
|
1478
|
-
*
|
|
1449
|
+
*
|
|
1450
|
+
* PineScript rules (bit for bit):
|
|
1451
|
+
* - `(source - ta.sma(source, length)) / (0.015 * ta.dev(source, length))`
|
|
1452
|
+
* - when the mean deviation is 0 (1e-10 tolerance, a flat window), the last CCI value; 0 when there is none yet
|
|
1453
|
+
* (a flat first window)
|
|
1454
|
+
* - \`na\` when the mean or the deviation is \`na\` (an \`na\` in the last \`length\` bars)
|
|
1479
1455
|
*
|
|
1480
1456
|
* @example
|
|
1481
1457
|
* \`\`\`typescript
|
|
@@ -1485,22 +1461,15 @@ export function percentrank(source: Source, length: simple_int): series_float {
|
|
|
1485
1461
|
* \`\`\`
|
|
1486
1462
|
*/
|
|
1487
1463
|
export function cci(source: Source, length: simple_int): series_float {
|
|
1488
|
-
const result: series_float = [];
|
|
1489
1464
|
const smaValues = sma(source, length);
|
|
1490
1465
|
const devValues = dev(source, length);
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
const cci = (source[i]! - smaValues[i]!) / (0.015 * devValues[i]!);
|
|
1499
|
-
result.push(cci);
|
|
1500
|
-
}
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
return result;
|
|
1466
|
+
let last = 0;
|
|
1467
|
+
return smaValues.map((mean, i) => {
|
|
1468
|
+
const d = devValues[i]!;
|
|
1469
|
+
if (Number.isNaN(mean) || Number.isNaN(d)) return NaN;
|
|
1470
|
+
if (!eq(d, 0)) last = (source[i]! - mean) / (0.015 * d);
|
|
1471
|
+
return last;
|
|
1472
|
+
});
|
|
1504
1473
|
}
|
|
1505
1474
|
|
|
1506
1475
|
/**
|
|
@@ -1517,8 +1486,8 @@ export function cci(source: Source, length: simple_int): series_float {
|
|
|
1517
1486
|
* - Measures where close is relative to the high-low range
|
|
1518
1487
|
* - Values above 80 typically indicate overbought
|
|
1519
1488
|
* - Values below 20 typically indicate oversold
|
|
1520
|
-
* -
|
|
1521
|
-
*
|
|
1489
|
+
* - As PineScript: when the source, \`highest(high, length)\` or \`lowest(low, length)\` is \`na\`, or the range is 0, the
|
|
1490
|
+
* result is the previous result of the call (\`na\` until a first value)
|
|
1522
1491
|
*
|
|
1523
1492
|
* @example
|
|
1524
1493
|
* \`\`\`typescript
|
|
@@ -1532,18 +1501,14 @@ export function stoch(source: Source, high: Source, low: Source, length: simple_
|
|
|
1532
1501
|
const lowestValues = lowest(low, length);
|
|
1533
1502
|
const highestValues = highest(high, length);
|
|
1534
1503
|
|
|
1504
|
+
// PineScript: an na input or a zero range keeps the previous result
|
|
1505
|
+
let previous = NaN;
|
|
1535
1506
|
for (let i = 0; i < source.length; i++) {
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
result.push(NaN);
|
|
1542
|
-
} else {
|
|
1543
|
-
const stochValue = 100 * (source[i]! - lowestValues[i]!) / range;
|
|
1544
|
-
result.push(stochValue);
|
|
1545
|
-
}
|
|
1546
|
-
}
|
|
1507
|
+
const value = source[i]!;
|
|
1508
|
+
const lo = lowestValues[i]!;
|
|
1509
|
+
const range = highestValues[i]! - lo;
|
|
1510
|
+
if (!Number.isNaN(value) && !Number.isNaN(range) && range !== 0) previous = (100 * (value - lo)) / range;
|
|
1511
|
+
result.push(previous);
|
|
1547
1512
|
}
|
|
1548
1513
|
|
|
1549
1514
|
return result;
|
|
@@ -1565,7 +1530,13 @@ export function stoch(source: Source, high: Source, low: Source, length: simple_
|
|
|
1565
1530
|
* - Formula: 100 - (100 / (1 + Positive Money Flow / Negative Money Flow))
|
|
1566
1531
|
* - Values above 80 indicate overbought
|
|
1567
1532
|
* - Values below 20 indicate oversold
|
|
1568
|
-
*
|
|
1533
|
+
*
|
|
1534
|
+
* PineScript rules (bit for bit):
|
|
1535
|
+
* - the two flows are `math.sum` running sums; a sum within 1e-10 of 0 counts as 0 (the residue a running sum
|
|
1536
|
+
* keeps over a window of zeros)
|
|
1537
|
+
* - 100 when the negative flow is 0; `na` when `1 + positive / negative` is 0 (division by zero)
|
|
1538
|
+
* - a bar with an `na` source adds nothing to the sums and gives the previous result; the bar after it (an `na`
|
|
1539
|
+
* change) adds its money flow to both sums
|
|
1569
1540
|
*
|
|
1570
1541
|
* @example
|
|
1571
1542
|
* ```typescript
|
|
@@ -1602,11 +1573,16 @@ export function mfi(source: Source, length: simple_int, volume?: Source): series
|
|
|
1602
1573
|
const upper = runningSum(upperFlow, length);
|
|
1603
1574
|
const lower = runningSum(lowerFlow, length);
|
|
1604
1575
|
|
|
1605
|
-
|
|
1606
|
-
|
|
1576
|
+
// A sum within 1e-10 of 0 (the residue of a running sum over a window of zeros) is 0
|
|
1577
|
+
const zero = (x: number) => (Math.abs(x) <= 1e-10 ? 0 : x);
|
|
1578
|
+
return upper.map((sumUp, i) => {
|
|
1579
|
+
const u = zero(sumUp);
|
|
1580
|
+
const l = zero(lower[i]!);
|
|
1607
1581
|
if (Number.isNaN(u) || Number.isNaN(l)) return NaN;
|
|
1608
1582
|
if (l === 0) return 100;
|
|
1609
|
-
|
|
1583
|
+
const ratio = 1 + u / l;
|
|
1584
|
+
// x / 0 is na in PineScript
|
|
1585
|
+
return ratio === 0 ? NaN : 100 - 100 / ratio;
|
|
1610
1586
|
});
|
|
1611
1587
|
}
|
|
1612
1588
|
|
|
@@ -2018,10 +1994,12 @@ export function dmi(
|
|
|
2018
1994
|
* @param source - Series of values to process
|
|
2019
1995
|
* @param shortLength - Short smoothing length
|
|
2020
1996
|
* @param longLength - Long smoothing length
|
|
2021
|
-
* @returns TSI series
|
|
1997
|
+
* @returns TSI series, a value in [-1, 1]
|
|
2022
1998
|
*
|
|
2023
1999
|
* @remarks
|
|
2024
|
-
* -
|
|
2000
|
+
* - As PineScript: `ta.ema(ta.ema(m, longLength), shortLength) / ta.ema(ta.ema(math.abs(m), longLength), shortLength)`
|
|
2001
|
+
* with `m = source - source[1]` (not scaled by 100)
|
|
2002
|
+
* - na when the denominator is 0 (a constant source), as a PineScript division by zero
|
|
2025
2003
|
* - Positive values indicate bullish momentum
|
|
2026
2004
|
* - Negative values indicate bearish momentum
|
|
2027
2005
|
* - Crossovers of zero line can signal trend changes
|
|
@@ -2053,17 +2031,11 @@ export function tsi(source: Source, shortLength: simple_int, longLength: simple_
|
|
|
2053
2031
|
const absMomentum = momentum.map(Math.abs);
|
|
2054
2032
|
const smoothedAbsMomentum = ema(ema(absMomentum, longLength), shortLength);
|
|
2055
2033
|
|
|
2056
|
-
//
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
} else {
|
|
2062
|
-
result.push((smoothedMomentum[i]! / smoothedAbsMomentum[i]!) * 100);
|
|
2063
|
-
}
|
|
2064
|
-
}
|
|
2065
|
-
|
|
2066
|
-
return result;
|
|
2034
|
+
// PineScript: a value in [-1, 1]; a division by zero is na
|
|
2035
|
+
return smoothedMomentum.map((m, i) => {
|
|
2036
|
+
const d = smoothedAbsMomentum[i]!;
|
|
2037
|
+
return d === 0 ? NaN : m / d;
|
|
2038
|
+
});
|
|
2067
2039
|
}
|
|
2068
2040
|
|
|
2069
2041
|
/**
|
|
@@ -2078,7 +2050,8 @@ export function tsi(source: Source, shortLength: simple_int, longLength: simple_
|
|
|
2078
2050
|
* - CMO > +50: overbought conditions
|
|
2079
2051
|
* - CMO < -50: oversold conditions
|
|
2080
2052
|
* - Unlike RSI, CMO uses sum of gains/losses instead of averages
|
|
2081
|
-
* -
|
|
2053
|
+
* - As in PineScript: the sums are the running sums of `math.sum` (bit for bit), and a flat window (every change 0)
|
|
2054
|
+
* is `0 / 0` = na
|
|
2082
2055
|
* - More volatile than RSI
|
|
2083
2056
|
*
|
|
2084
2057
|
* @example
|
|
@@ -2089,37 +2062,20 @@ export function tsi(source: Source, shortLength: simple_int, longLength: simple_
|
|
|
2089
2062
|
* ```
|
|
2090
2063
|
*/
|
|
2091
2064
|
export function cmo(source: Source, length: simple_int): series_float {
|
|
2092
|
-
|
|
2093
|
-
|
|
2065
|
+
// PineScript definition: mom = ta.change(src), sm1 = math.sum(mom >= 0 ? mom : 0, length),
|
|
2066
|
+
// sm2 = math.sum(mom >= 0 ? 0 : -mom, length), cmo = 100 * (sm1 - sm2) / (sm1 + sm2), with the running sums of
|
|
2067
|
+
// math.sum (bit for bit). A comparison with na is false: an na change adds 0 to sm1 and na (skipped) to sm2.
|
|
2068
|
+
const up: number[] = [];
|
|
2069
|
+
const down: number[] = [];
|
|
2094
2070
|
for (let i = 0; i < source.length; i++) {
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
}
|
|
2099
|
-
|
|
2100
|
-
let sumGains = 0;
|
|
2101
|
-
let sumLosses = 0;
|
|
2102
|
-
|
|
2103
|
-
for (let j = 0; j < length; j++) {
|
|
2104
|
-
const change = source[i - j]! - source[i - j - 1]!;
|
|
2105
|
-
if (change > 0) {
|
|
2106
|
-
sumGains += change;
|
|
2107
|
-
} else {
|
|
2108
|
-
sumLosses += Math.abs(change);
|
|
2109
|
-
}
|
|
2110
|
-
}
|
|
2111
|
-
|
|
2112
|
-
// PineScript: 100 * (sm1 - sm2) / (sm1 + sm2), so a flat window (0 / 0) is na
|
|
2113
|
-
const totalMovement = sumGains + sumLosses;
|
|
2114
|
-
if (totalMovement === 0) {
|
|
2115
|
-
result.push(NaN);
|
|
2116
|
-
} else {
|
|
2117
|
-
const cmoValue = ((sumGains - sumLosses) / totalMovement) * 100;
|
|
2118
|
-
result.push(cmoValue);
|
|
2119
|
-
}
|
|
2071
|
+
const mom = i > 0 ? source[i]! - source[i - 1]! : NaN;
|
|
2072
|
+
up.push(mom >= 0 ? mom : 0);
|
|
2073
|
+
down.push(mom >= 0 ? 0 : -mom);
|
|
2120
2074
|
}
|
|
2121
|
-
|
|
2122
|
-
|
|
2075
|
+
const sm1 = runningSum(up, length);
|
|
2076
|
+
const sm2 = runningSum(down, length);
|
|
2077
|
+
// a flat window is 0 / 0 = na
|
|
2078
|
+
return sm1.map((s1, i) => (100 * (s1 - sm2[i]!)) / (s1 + sm2[i]!));
|
|
2123
2079
|
}
|
|
2124
2080
|
|
|
2125
2081
|
/**
|
|
@@ -2245,6 +2201,10 @@ export function bbw(source: Source, length: simple_int, mult: simple_float): ser
|
|
|
2245
2201
|
* @param length - Lookback period (default: 14)
|
|
2246
2202
|
* @returns Williams %R series
|
|
2247
2203
|
*
|
|
2204
|
+
* @remarks
|
|
2205
|
+
* PineScript rule (bit for bit): `100 * (close - max) / (max - min)` with `max = ta.highest(high, length)` and
|
|
2206
|
+
* `min = ta.lowest(low, length)` (their `na` rules); `na` when the range is 0.
|
|
2207
|
+
*
|
|
2248
2208
|
* @example
|
|
2249
2209
|
* ```typescript
|
|
2250
2210
|
* const wpr = ta.wpr(high, low, close, 14);
|
|
@@ -2253,34 +2213,14 @@ export function bbw(source: Source, length: simple_int, mult: simple_float): ser
|
|
|
2253
2213
|
* ```
|
|
2254
2214
|
*/
|
|
2255
2215
|
export function wpr(high: Source, low: Source, close: Source, length: simple_int = 14): series_float {
|
|
2256
|
-
const
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
// Find highest high and lowest low in the period
|
|
2265
|
-
let highestHigh = high[i - length + 1]!;
|
|
2266
|
-
let lowestLow = low[i - length + 1]!;
|
|
2267
|
-
|
|
2268
|
-
for (let j = i - length + 2; j <= i; j++) {
|
|
2269
|
-
if (high[j]! > highestHigh) highestHigh = high[j]!;
|
|
2270
|
-
if (low[j]! < lowestLow) lowestLow = low[j]!;
|
|
2271
|
-
}
|
|
2272
|
-
|
|
2273
|
-
const range = highestHigh - lowestLow;
|
|
2274
|
-
if (range === 0) {
|
|
2275
|
-
result.push(NaN);
|
|
2276
|
-
} else {
|
|
2277
|
-
// Formula: (Highest High - Close) / (Highest High - Lowest Low) * -100
|
|
2278
|
-
const wprValue = ((highestHigh! - close[i]!) / range) * -100;
|
|
2279
|
-
result.push(wprValue);
|
|
2280
|
-
}
|
|
2281
|
-
}
|
|
2282
|
-
|
|
2283
|
-
return result;
|
|
2216
|
+
const highs = highest(high, length);
|
|
2217
|
+
const lows = lowest(low, length);
|
|
2218
|
+
return Array.from({ length: close.length }, (_, i) => {
|
|
2219
|
+
const hmax = highs[i]!;
|
|
2220
|
+
const range = hmax - lows[i]!;
|
|
2221
|
+
// PineScript, in this order (bit for bit): 100 * (close - max) / (max - min); x / 0 is na
|
|
2222
|
+
return range === 0 ? NaN : (100 * (close[i]! - hmax)) / range;
|
|
2223
|
+
});
|
|
2284
2224
|
}
|
|
2285
2225
|
|
|
2286
2226
|
/**
|
|
@@ -2296,6 +2236,9 @@ export function wpr(high: Source, low: Source, close: Source, length: simple_int
|
|
|
2296
2236
|
* @remarks
|
|
2297
2237
|
* As in PineScript: the sums restart on anchor bars; the standard deviation is
|
|
2298
2238
|
* `sqrt(Σ(volume × source²) / Σvolume − vwap²)`.
|
|
2239
|
+
* - With an anchor series: `na` before the first anchor bar
|
|
2240
|
+
* - A bar with an `na` source (or volume) makes the sums `na` until the next anchor bar (`na` values are not
|
|
2241
|
+
* skipped)
|
|
2299
2242
|
*/
|
|
2300
2243
|
export function vwap(source: Source, volume: Source, anchor?: ArrayLike<boolean | number>): series_float;
|
|
2301
2244
|
export function vwap(
|
|
@@ -2316,18 +2259,19 @@ export function vwap(
|
|
|
2316
2259
|
const mid: series_float = [];
|
|
2317
2260
|
const upper: series_float = [];
|
|
2318
2261
|
const lower: series_float = [];
|
|
2319
|
-
|
|
2320
|
-
let
|
|
2321
|
-
let
|
|
2262
|
+
// na until the first anchor (without an anchor series, bar 0 starts the sums)
|
|
2263
|
+
let sumPV = NaN;
|
|
2264
|
+
let sumV = NaN;
|
|
2265
|
+
let sumPPV = NaN;
|
|
2322
2266
|
for (let i = 0; i < source.length; i++) {
|
|
2323
|
-
if (anchor && anchor[i]) {
|
|
2324
|
-
sumPV = 0;
|
|
2325
|
-
sumV = 0;
|
|
2326
|
-
sumPPV = 0;
|
|
2327
|
-
}
|
|
2328
2267
|
const s = source[i]!;
|
|
2329
2268
|
const v = volume[i]!;
|
|
2330
|
-
if (
|
|
2269
|
+
if (anchor ? anchor[i] : i === 0) {
|
|
2270
|
+
sumPV = s * v;
|
|
2271
|
+
sumV = v;
|
|
2272
|
+
sumPPV = s * s * v;
|
|
2273
|
+
} else {
|
|
2274
|
+
// An na source or volume makes the sums na until the next anchor
|
|
2331
2275
|
sumPV += s * v;
|
|
2332
2276
|
sumV += v;
|
|
2333
2277
|
sumPPV += s * s * v;
|
|
@@ -2409,7 +2353,8 @@ export function alma(
|
|
|
2409
2353
|
/**
|
|
2410
2354
|
* Keltner Channels Width (KCW)
|
|
2411
2355
|
*
|
|
2412
|
-
* Measures the width of Keltner Channels
|
|
2356
|
+
* Measures the width of Keltner Channels relative to the middle line: `(upper - lower) / middle`, a ratio as in
|
|
2357
|
+
* PineScript (0.05 is a width of 5% of the middle line; not multiplied by 100).
|
|
2413
2358
|
* Similar to BBW but uses ATR instead of standard deviation.
|
|
2414
2359
|
*
|
|
2415
2360
|
* Low KCW suggests consolidation/low volatility.
|
|
@@ -2447,8 +2392,8 @@ export function kcw(
|
|
|
2447
2392
|
if (isNaN(basis[i]!) || basis[i]! === 0) {
|
|
2448
2393
|
result.push(NaN);
|
|
2449
2394
|
} else {
|
|
2450
|
-
|
|
2451
|
-
result.push(
|
|
2395
|
+
// PineScript: (upper - lower) / basis, a ratio (not a percentage)
|
|
2396
|
+
result.push((upper[i]! - lower[i]!) / basis[i]!);
|
|
2452
2397
|
}
|
|
2453
2398
|
}
|
|
2454
2399
|
|
|
@@ -2530,50 +2475,30 @@ export function min(source: Source): series_float {
|
|
|
2530
2475
|
* The Center of Gravity indicator is an oscillator developed by John Ehlers.
|
|
2531
2476
|
* It identifies turning points with minimal lag and provides clear signals.
|
|
2532
2477
|
*
|
|
2533
|
-
* The COG calculates a weighted average where more recent prices have higher weights,
|
|
2534
|
-
* similar to a moving average but with a focus on momentum shifts.
|
|
2535
|
-
*
|
|
2536
2478
|
* @param source - Source series (typically close)
|
|
2537
2479
|
* @param length - Lookback period (default: 10)
|
|
2538
2480
|
* @returns COG series
|
|
2539
2481
|
*
|
|
2482
|
+
* @remarks
|
|
2483
|
+
* PineScript rule (bit for bit): `-num / math.sum(source, length)`, with `num` the sum of `source[i] * (i + 1)`
|
|
2484
|
+
* for `i = 0 .. length - 1`, added newest first.
|
|
2485
|
+
* - The denominator is the compensated running sum of `math.sum` (the last `length` non-`na` values)
|
|
2486
|
+
* - The numerator reads the last `length` bars, so an `na` among them gives `na`
|
|
2487
|
+
* - A zero denominator gives `na` (PineScript division by zero)
|
|
2488
|
+
*
|
|
2540
2489
|
* @example
|
|
2541
2490
|
* ```typescript
|
|
2542
2491
|
* const cogValue = ta.cog(close, 10);
|
|
2543
|
-
* // Use COG crossovers as signals:
|
|
2544
|
-
* // - COG crossing above 0: potential buy signal
|
|
2545
|
-
* // - COG crossing below 0: potential sell signal
|
|
2546
2492
|
* ```
|
|
2547
2493
|
*/
|
|
2548
2494
|
export function cog(source: Source, length: simple_int = 10): series_float {
|
|
2549
|
-
const
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
let numerator = 0;
|
|
2558
|
-
let denominator = 0;
|
|
2559
|
-
|
|
2560
|
-
for (let j = 0; j < length; j++) {
|
|
2561
|
-
const weight = j + 1;
|
|
2562
|
-
const price = source[i - length + 1 + j]!;
|
|
2563
|
-
numerator += weight * price;
|
|
2564
|
-
denominator += price;
|
|
2565
|
-
}
|
|
2566
|
-
|
|
2567
|
-
if (denominator === 0) {
|
|
2568
|
-
result.push(NaN);
|
|
2569
|
-
} else {
|
|
2570
|
-
// COG formula: -1 * (sum of (weight * price) / sum of prices) + (length + 1) / 2
|
|
2571
|
-
const cog = -1 * (numerator / denominator) + (length + 1) / 2;
|
|
2572
|
-
result.push(cog);
|
|
2573
|
-
}
|
|
2574
|
-
}
|
|
2575
|
-
|
|
2576
|
-
return result;
|
|
2495
|
+
const len = Math.floor(length);
|
|
2496
|
+
return runningSum(source, len).map((sum, i) => {
|
|
2497
|
+
if (i < len - 1 || Number.isNaN(sum) || sum === 0) return NaN;
|
|
2498
|
+
let num = 0;
|
|
2499
|
+
for (let k = 0; k < len; k++) num += source[i - k]! * (k + 1);
|
|
2500
|
+
return -num / sum;
|
|
2501
|
+
});
|
|
2577
2502
|
}
|
|
2578
2503
|
|
|
2579
2504
|
/**
|
|
@@ -2593,32 +2518,25 @@ export function cog(source: Source, length: simple_int = 10): series_float {
|
|
|
2593
2518
|
* ```
|
|
2594
2519
|
*
|
|
2595
2520
|
* @remarks
|
|
2596
|
-
*
|
|
2521
|
+
* PineScript rules:
|
|
2522
|
+
* - the window is the last `length` non-`na` values (an `na` bar does not enter it, and gets the mode of the
|
|
2523
|
+
* window it keeps)
|
|
2524
|
+
* - NaN until `length` non-`na` values exist
|
|
2597
2525
|
* - If no mode exists, returns the smallest value
|
|
2598
|
-
* - Returns NaN for the first (length - 1) values where there's insufficient data
|
|
2599
2526
|
*/
|
|
2600
2527
|
export function mode(source: Source, length: simple_int): series_float {
|
|
2601
2528
|
const result: series_float = [];
|
|
2529
|
+
const len = Math.floor(length);
|
|
2530
|
+
const seen: number[] = []; // non-na values so far
|
|
2602
2531
|
|
|
2603
2532
|
for (let i = 0; i < source.length; i++) {
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
}
|
|
2608
|
-
|
|
2609
|
-
// Collect non-NaN values in the window
|
|
2610
|
-
const values: number[] = [];
|
|
2611
|
-
for (let j = 0; j < length; j++) {
|
|
2612
|
-
const value = source[i - j]!;
|
|
2613
|
-
if (!isNaN(value)) {
|
|
2614
|
-
values.push(value);
|
|
2615
|
-
}
|
|
2616
|
-
}
|
|
2617
|
-
|
|
2618
|
-
if (values.length === 0) {
|
|
2533
|
+
const v = source[i]!;
|
|
2534
|
+
if (!Number.isNaN(v)) seen.push(v);
|
|
2535
|
+
if (seen.length < len) {
|
|
2619
2536
|
result.push(NaN);
|
|
2620
2537
|
continue;
|
|
2621
2538
|
}
|
|
2539
|
+
const values = seen.slice(seen.length - len);
|
|
2622
2540
|
|
|
2623
2541
|
// Count frequency of each value
|
|
2624
2542
|
const frequencyMap = new Map<number, number>();
|
|
@@ -2781,95 +2699,73 @@ export function percentile_nearest_rank(
|
|
|
2781
2699
|
* - +100 indicates source consistently increased over the period
|
|
2782
2700
|
* - -100 indicates source consistently decreased over the period
|
|
2783
2701
|
* - 0 indicates no directional consistency
|
|
2784
|
-
* -
|
|
2702
|
+
* - As PineScript (bit for bit): the first value is on bar `length`; values less than 1e-10 above the smallest value
|
|
2703
|
+
* of a group (ascending sweep) are ties with their average rank; a window where all values are ties gives `na`;
|
|
2704
|
+
* moment-form variances of the bar positions and the ranks, `(100 * cov) / (sd_x * sd_y)`
|
|
2705
|
+
* - A bar with an `na` source keeps the previous result; the values are kept in a ring of `length + 1` slots by bar
|
|
2706
|
+
* index (0 at the start, not written on `na` bars), so a later window sees the value of `length + 1` bars earlier
|
|
2707
|
+
* where the `na` was
|
|
2785
2708
|
*/
|
|
2786
2709
|
export function rci(source: Source, length: simple_int): series_float {
|
|
2710
|
+
const len = Math.floor(length);
|
|
2787
2711
|
const result: series_float = [];
|
|
2788
|
-
|
|
2712
|
+
// PineScript keeps the values in a ring of `length + 1` slots indexed by the bar index, filled with 0 at the start
|
|
2713
|
+
// and written only on bars with a value: a bar with an na source keeps the previous result, and later windows see
|
|
2714
|
+
// the value of `length + 1` bars earlier in that slot
|
|
2715
|
+
const ring = new Array<number>(len + 1).fill(0);
|
|
2789
2716
|
for (let i = 0; i < source.length; i++) {
|
|
2790
|
-
|
|
2791
|
-
|
|
2717
|
+
const v = source[i]!;
|
|
2718
|
+
if (Number.isNaN(v)) {
|
|
2719
|
+
result.push(i > 0 ? result[i - 1]! : NaN);
|
|
2792
2720
|
continue;
|
|
2793
2721
|
}
|
|
2794
|
-
|
|
2795
|
-
//
|
|
2796
|
-
|
|
2797
|
-
for (let j = 0; j < length; j++) {
|
|
2798
|
-
values.push(source[i - length + 1 + j]!);
|
|
2799
|
-
}
|
|
2800
|
-
|
|
2801
|
-
// Check for NaN values
|
|
2802
|
-
if (values.some(v => isNaN(v))) {
|
|
2722
|
+
ring[i % (len + 1)] = v;
|
|
2723
|
+
// the first value is on bar `length` (one bar after the window is full)
|
|
2724
|
+
if (i < len) {
|
|
2803
2725
|
result.push(NaN);
|
|
2804
2726
|
continue;
|
|
2805
2727
|
}
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
const
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
}
|
|
2842
|
-
|
|
2843
|
-
const n = length;
|
|
2844
|
-
const base = (n * n * n - n) / 12;
|
|
2845
|
-
|
|
2846
|
-
// Compute tie correction for price ranks
|
|
2847
|
-
// Count tied groups from the sorted array
|
|
2848
|
-
let tieCorrection = 0;
|
|
2849
|
-
for (let j = 0; j < sorted.length; ) {
|
|
2850
|
-
let tieCount = 1;
|
|
2851
|
-
while (j + tieCount < sorted.length && sorted[j]!.value === sorted[j + tieCount]!.value) {
|
|
2852
|
-
tieCount++;
|
|
2853
|
-
}
|
|
2854
|
-
if (tieCount > 1) {
|
|
2855
|
-
tieCorrection += (tieCount * tieCount * tieCount - tieCount) / 12;
|
|
2856
|
-
}
|
|
2857
|
-
j += tieCount;
|
|
2858
|
-
}
|
|
2859
|
-
|
|
2860
|
-
const A = base - tieCorrection; // Corrected for tied price ranks
|
|
2861
|
-
const B = base; // Time ranks have no ties
|
|
2862
|
-
|
|
2863
|
-
let rho: number;
|
|
2864
|
-
if (A === 0 || B === 0) {
|
|
2865
|
-
rho = 0;
|
|
2866
|
-
} else {
|
|
2867
|
-
rho = (A + B - sumSquaredDiff) / (2 * Math.sqrt(A * B));
|
|
2728
|
+
// window newest first (position 0 = this bar)
|
|
2729
|
+
const values = Array.from({ length: len }, (_, j) => ring[(i - j) % (len + 1)]!);
|
|
2730
|
+
// Ranks: ascending sweep; a group collects the values less than 1e-10 above its smallest value; each group gets
|
|
2731
|
+
// its average 0-based rank counted from the largest value
|
|
2732
|
+
const order = values.map((_, j) => j).sort((a, b) => values[a]! - values[b]!);
|
|
2733
|
+
const ranks = new Array<number>(len);
|
|
2734
|
+
for (let g = 0; g < len; ) {
|
|
2735
|
+
const anchor = values[order[g]!]!;
|
|
2736
|
+
let end = g + 1;
|
|
2737
|
+
while (end < len && values[order[end]!]! - anchor < 1e-10) end++;
|
|
2738
|
+
const avgRank = len - 1 - (g + g + (end - g) - 1) / 2;
|
|
2739
|
+
for (let k = g; k < end; k++) ranks[order[k]!] = avgRank;
|
|
2740
|
+
g = end;
|
|
2741
|
+
}
|
|
2742
|
+
// moment-form variances, covariance from deviation products, (100 * cov) / (sd_x * sd_y): this operation order
|
|
2743
|
+
// gives PineScript's values bit for bit
|
|
2744
|
+
let sumX = 0;
|
|
2745
|
+
let sumY = 0;
|
|
2746
|
+
let sumX2 = 0;
|
|
2747
|
+
let sumY2 = 0;
|
|
2748
|
+
for (let k = 0; k < len; k++) {
|
|
2749
|
+
const y = ranks[k]!;
|
|
2750
|
+
sumX += k;
|
|
2751
|
+
sumY += y;
|
|
2752
|
+
sumX2 += k * k;
|
|
2753
|
+
sumY2 += y * y;
|
|
2754
|
+
}
|
|
2755
|
+
const meanX = sumX / len;
|
|
2756
|
+
const meanY = sumY / len;
|
|
2757
|
+
const varX = sumX2 / len - meanX * meanX;
|
|
2758
|
+
const varY = sumY2 / len - meanY * meanY;
|
|
2759
|
+
// a window where all values are ties has no correlation
|
|
2760
|
+
if (!(varX > 0) || !(varY > 0)) {
|
|
2761
|
+
result.push(NaN);
|
|
2762
|
+
continue;
|
|
2868
2763
|
}
|
|
2869
|
-
|
|
2870
|
-
|
|
2764
|
+
let cov = 0;
|
|
2765
|
+
for (let k = 0; k < len; k++) cov += (k - meanX) * (ranks[k]! - meanY);
|
|
2766
|
+
cov /= len;
|
|
2767
|
+
result.push((100 * cov) / (Math.sqrt(varX) * Math.sqrt(varY)));
|
|
2871
2768
|
}
|
|
2872
|
-
|
|
2873
2769
|
return result;
|
|
2874
2770
|
}
|
|
2875
2771
|
|
|
@@ -3490,16 +3386,18 @@ export function pvt(close: Source, volume: Source): series_float {
|
|
|
3490
3386
|
|
|
3491
3387
|
/**
|
|
3492
3388
|
* Accumulation/Distribution index: the running sum of
|
|
3493
|
-
* `(
|
|
3389
|
+
* `((close - low) - (high - close)) / (high - low) * volume`, in this order as in PineScript (bit for bit).
|
|
3390
|
+
* A bar where this is `na` (`high == low`, an `na` volume) adds nothing and keeps the previous value.
|
|
3494
3391
|
*/
|
|
3495
3392
|
export function accdist(high: Source, low: Source, close: Source, volume: Source): series_float {
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3393
|
+
let ad = 0;
|
|
3394
|
+
return close.map((c, i) => {
|
|
3395
|
+
const h = high[i]!;
|
|
3396
|
+
const l = low[i]!;
|
|
3397
|
+
const mfv = (((c - l) - (h - c)) / (h - l)) * volume[i]!;
|
|
3398
|
+
if (Number.isFinite(mfv)) ad += mfv;
|
|
3399
|
+
return ad;
|
|
3400
|
+
});
|
|
3503
3401
|
}
|
|
3504
3402
|
|
|
3505
3403
|
/**
|