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/dist/esm/ta/index.js
CHANGED
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
* @version 6
|
|
9
9
|
*/
|
|
10
10
|
import { eq, ge, gt, le, lt } from '../compare/index.js';
|
|
11
|
-
import { runningSum, runningVariance } from './running-sum.js';
|
|
11
|
+
import { runningSum, runningSumSeries, runningVariance, runningVarianceSeries } from './running-sum.js';
|
|
12
12
|
/**
|
|
13
13
|
* Simple Moving Average - returns the moving average (sum of last y values divided by y).
|
|
14
14
|
*
|
|
15
15
|
* @param source - Series of values to process
|
|
16
|
-
* @param length - Number of bars (length)
|
|
16
|
+
* @param length - Number of bars (length), or one length per bar (a series length)
|
|
17
17
|
* @returns Simple moving average of source for length bars back
|
|
18
18
|
*
|
|
19
19
|
* @remarks
|
|
@@ -22,30 +22,25 @@ import { runningSum, runningVariance } from './running-sum.js';
|
|
|
22
22
|
* - Returns NaN until `length` non-`na` values are available
|
|
23
23
|
* - The sum is a compensated running sum, as in PineScript, so the result can differ from the exact window mean in
|
|
24
24
|
* the last bits (bit for bit with PineScript)
|
|
25
|
+
* - With a series length, the running sum is moved to the new length when the length changes, as PineScript does
|
|
26
|
+
* (see {@link math.sum}); a bar with an `na` source gives the mean of the last `length` earlier values
|
|
25
27
|
*
|
|
26
28
|
* @example
|
|
27
29
|
* ```typescript
|
|
28
30
|
* const closePrices = [10, 11, 12, 13, 14];
|
|
29
31
|
* const sma5 = ta.sma(closePrices, 5); // Returns: [NaN, NaN, NaN, NaN, 12]
|
|
32
|
+
* const smaVar = ta.sma(closePrices, [2, 2, 2, 3, 2]); // Returns: [NaN, 10.5, 11.5, 12, 13.5]
|
|
30
33
|
* ```
|
|
31
34
|
*/
|
|
32
35
|
export function sma(source, length) {
|
|
36
|
+
if (typeof length !== 'number') {
|
|
37
|
+
const sums = runningSumSeries(source, length, 'ta.sma');
|
|
38
|
+
return sums.map((sum, i) => sum / Math.floor(length[i]));
|
|
39
|
+
}
|
|
33
40
|
// Floor the length to match PineScript's auto-truncation of float to int
|
|
34
41
|
const len = Math.floor(length);
|
|
35
42
|
return runningSum(source, len).map((sum) => sum / len);
|
|
36
43
|
}
|
|
37
|
-
/** Mean of the last `length` bars; na when the window holds an na value (used by ta.dev). */
|
|
38
|
-
function strictWindowMean(source, length) {
|
|
39
|
-
const len = Math.floor(length);
|
|
40
|
-
return Array.from({ length: source.length }, (_, i) => {
|
|
41
|
-
if (i < len - 1)
|
|
42
|
-
return NaN;
|
|
43
|
-
let sum = 0;
|
|
44
|
-
for (let j = 0; j < len; j++)
|
|
45
|
-
sum += source[i - j];
|
|
46
|
-
return sum / len;
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
44
|
/**
|
|
50
45
|
* Exponential Moving Average - returns the exponentially weighted moving average.
|
|
51
46
|
*
|
|
@@ -70,24 +65,19 @@ export function ema(source, length) {
|
|
|
70
65
|
const result = [];
|
|
71
66
|
const len = Math.floor(length);
|
|
72
67
|
const multiplier = 2 / (len + 1);
|
|
73
|
-
// SMA seed
|
|
74
|
-
// An infinite value is na, as in PineScript.
|
|
68
|
+
// SMA seed on the bar of the `len`-th valid value. An infinite value is na, as in PineScript.
|
|
75
69
|
let firstValidIndex = -1;
|
|
76
70
|
let validCount = 0;
|
|
77
|
-
let initSum = 0;
|
|
78
71
|
for (let i = 0; i < source.length; i++) {
|
|
79
72
|
const val = source[i];
|
|
80
|
-
if (val !== undefined && Number.isFinite(val)) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (validCount === len) {
|
|
84
|
-
firstValidIndex = i;
|
|
85
|
-
break;
|
|
86
|
-
}
|
|
73
|
+
if (val !== undefined && Number.isFinite(val) && ++validCount === len) {
|
|
74
|
+
firstValidIndex = i;
|
|
75
|
+
break;
|
|
87
76
|
}
|
|
88
77
|
}
|
|
89
|
-
let emaValue = validCount > 0 ? initSum / validCount : NaN;
|
|
90
78
|
const emaInitialized = firstValidIndex >= 0;
|
|
79
|
+
// The seed is ta.sma on that bar: the running sum of math.sum (PineScript bits), not a plain loop sum
|
|
80
|
+
let emaValue = emaInitialized ? runningSum(source.slice(0, firstValidIndex + 1), len)[firstValidIndex] / len : NaN;
|
|
91
81
|
for (let i = 0; i < source.length; i++) {
|
|
92
82
|
if (!emaInitialized || i < firstValidIndex) {
|
|
93
83
|
result.push(NaN);
|
|
@@ -232,7 +222,7 @@ export function bb(series, length, mult) {
|
|
|
232
222
|
* Standard deviation over the last `length` non-na values.
|
|
233
223
|
*
|
|
234
224
|
* @param source - Series of values to process
|
|
235
|
-
* @param length - Number of values
|
|
225
|
+
* @param length - Number of values, or one length per bar (a series length, see {@link sma})
|
|
236
226
|
* @param biased - true (default): biased estimate (divides by `length`); false: unbiased (by `length - 1`)
|
|
237
227
|
* @returns Standard deviation series (na until `length` non-na values exist)
|
|
238
228
|
*
|
|
@@ -243,7 +233,7 @@ export function bb(series, length, mult) {
|
|
|
243
233
|
* compensated running sums of the values and of their squares, bit for bit as PineScript.
|
|
244
234
|
*/
|
|
245
235
|
export function stdev(source, length, biased = true) {
|
|
246
|
-
return
|
|
236
|
+
return varianceOf(source, length, biased, 'ta.stdev').map((v) => (Number.isNaN(v) ? NaN : v > 0 ? Math.sqrt(v) : 0));
|
|
247
237
|
}
|
|
248
238
|
/**
|
|
249
239
|
* The last `length` non-na values up to bar `i` (the bar itself may be na), or null when fewer exist.
|
|
@@ -321,6 +311,27 @@ function extremeValue(source, length, lowest) {
|
|
|
321
311
|
return v === 0 && zeros[i] ? (lowest ? -0 : 0) : v;
|
|
322
312
|
});
|
|
323
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* Compares each bar with the last earlier bar where both values were not na (PineScript rule for ta.crossover,
|
|
316
|
+
* ta.crossunder and ta.cross): false while there is no such bar, and false on a bar with an na value.
|
|
317
|
+
*
|
|
318
|
+
* @internal
|
|
319
|
+
*/
|
|
320
|
+
function crossTest(series1, series2, test) {
|
|
321
|
+
const result = [];
|
|
322
|
+
let prevA = NaN;
|
|
323
|
+
let prevB = NaN;
|
|
324
|
+
for (let i = 0; i < series1.length; i++) {
|
|
325
|
+
const a = series1[i];
|
|
326
|
+
const b = series2[i];
|
|
327
|
+
result.push(test(a, b, prevA, prevB));
|
|
328
|
+
if (!Number.isNaN(a) && !Number.isNaN(b)) {
|
|
329
|
+
prevA = a;
|
|
330
|
+
prevB = b;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return result;
|
|
334
|
+
}
|
|
324
335
|
/**
|
|
325
336
|
* Crossover - returns true when series1 crosses over series2 (moves from below to above).
|
|
326
337
|
*
|
|
@@ -329,8 +340,10 @@ function extremeValue(source, length, lowest) {
|
|
|
329
340
|
* @returns Boolean series (true at crossover points)
|
|
330
341
|
*
|
|
331
342
|
* @remarks
|
|
332
|
-
* - True when: series1[i]
|
|
343
|
+
* - True when: series1[i] > series2[i] AND series1[j] <= series2[j], where j is the last bar before i where both
|
|
344
|
+
* values were not na (PineScript rule: a crossing right after an na bar is not missed)
|
|
333
345
|
* - First value is always false (no previous value to compare)
|
|
346
|
+
* - Comparisons are exact (no tolerance)
|
|
334
347
|
* - Useful for detecting bullish signals (e.g., fast MA crossing over slow MA)
|
|
335
348
|
*
|
|
336
349
|
* @example
|
|
@@ -339,16 +352,7 @@ function extremeValue(source, length, lowest) {
|
|
|
339
352
|
* ```
|
|
340
353
|
*/
|
|
341
354
|
export function crossover(series1, series2) {
|
|
342
|
-
|
|
343
|
-
for (let i = 0; i < series1.length; i++) {
|
|
344
|
-
if (i === 0) {
|
|
345
|
-
result.push(false);
|
|
346
|
-
}
|
|
347
|
-
else {
|
|
348
|
-
result.push(series1[i] > series2[i] && series1[i - 1] <= series2[i - 1]);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
return result;
|
|
355
|
+
return crossTest(series1, series2, (a, b, pa, pb) => a > b && pa <= pb);
|
|
352
356
|
}
|
|
353
357
|
/**
|
|
354
358
|
* Crossunder - returns true when series1 crosses under series2 (moves from above to below).
|
|
@@ -358,8 +362,10 @@ export function crossover(series1, series2) {
|
|
|
358
362
|
* @returns Boolean series (true at crossunder points)
|
|
359
363
|
*
|
|
360
364
|
* @remarks
|
|
361
|
-
* - True when: series1[i]
|
|
365
|
+
* - True when: series1[i] < series2[i] AND series1[j] >= series2[j], where j is the last bar before i where both
|
|
366
|
+
* values were not na
|
|
362
367
|
* - First value is always false (no previous value to compare)
|
|
368
|
+
* - Comparisons are exact (no tolerance)
|
|
363
369
|
* - Useful for detecting bearish signals (e.g., fast MA crossing under slow MA)
|
|
364
370
|
*
|
|
365
371
|
* @example
|
|
@@ -368,16 +374,7 @@ export function crossover(series1, series2) {
|
|
|
368
374
|
* ```
|
|
369
375
|
*/
|
|
370
376
|
export function crossunder(series1, series2) {
|
|
371
|
-
|
|
372
|
-
for (let i = 0; i < series1.length; i++) {
|
|
373
|
-
if (i === 0) {
|
|
374
|
-
result.push(false);
|
|
375
|
-
}
|
|
376
|
-
else {
|
|
377
|
-
result.push(series1[i] < series2[i] && series1[i - 1] >= series2[i - 1]);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
return result;
|
|
377
|
+
return crossTest(series1, series2, (a, b, pa, pb) => a < b && pa >= pb);
|
|
381
378
|
}
|
|
382
379
|
/**
|
|
383
380
|
* Change - calculates the difference between the current value and its value length bars ago.
|
|
@@ -603,7 +600,8 @@ export function supertrend(factor, atrPeriod, high, low, close, wicks = false) {
|
|
|
603
600
|
* @remarks
|
|
604
601
|
* - Moving average used in RSI calculation
|
|
605
602
|
* - Alpha = 1 / length (different from EMA which uses alpha = 2 / (length + 1))
|
|
606
|
-
* - First value is initialized with SMA, then uses exponential smoothing
|
|
603
|
+
* - First value is initialized with SMA, then uses exponential smoothing. As in PineScript, the seed is the
|
|
604
|
+
* `ta.sma` value (its compensated running sum), so it can differ from the exact mean in the last bits
|
|
607
605
|
* - Formula: `RMA = (source + (length - 1) * RMA[1]) / length`, evaluated in this order as in PineScript
|
|
608
606
|
* (equal to `alpha * source + (1 - alpha) * RMA[1]`, but bit for bit)
|
|
609
607
|
* - `na` values (also +/-Infinity) in the source series are ignored
|
|
@@ -625,21 +623,16 @@ export function rma(source, length) {
|
|
|
625
623
|
// An infinite value is na, as in PineScript.
|
|
626
624
|
let firstValidIndex = -1;
|
|
627
625
|
let validCount = 0;
|
|
628
|
-
let initSum = 0;
|
|
629
626
|
for (let i = 0; i < source.length; i++) {
|
|
630
627
|
const val = source[i];
|
|
631
|
-
if (val !== undefined && Number.isFinite(val)) {
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
if (validCount === len) {
|
|
635
|
-
firstValidIndex = i;
|
|
636
|
-
break;
|
|
637
|
-
}
|
|
628
|
+
if (val !== undefined && Number.isFinite(val) && ++validCount === len) {
|
|
629
|
+
firstValidIndex = i;
|
|
630
|
+
break;
|
|
638
631
|
}
|
|
639
632
|
}
|
|
640
|
-
// Initialize RMA value with SMA of first `length` non-NaN values
|
|
641
|
-
let rmaValue = validCount > 0 ? initSum / validCount : NaN;
|
|
642
633
|
const rmaInitialized = firstValidIndex >= 0;
|
|
634
|
+
// The seed is ta.sma on that bar: the running sum of math.sum (PineScript bits), not a plain loop sum
|
|
635
|
+
let rmaValue = rmaInitialized ? runningSum(source.slice(0, firstValidIndex + 1), len)[firstValidIndex] / len : NaN;
|
|
643
636
|
for (let i = 0; i < source.length; i++) {
|
|
644
637
|
if (!rmaInitialized || i < firstValidIndex) {
|
|
645
638
|
// Not enough data yet for RMA
|
|
@@ -665,6 +658,14 @@ export function rma(source, length) {
|
|
|
665
658
|
}
|
|
666
659
|
return result;
|
|
667
660
|
}
|
|
661
|
+
/**
|
|
662
|
+
* One step of the PineScript ta.wma sum: `a + b`, or 0 when `|a + b| <= 1e-10 * max(1, |a| + |b|)`.
|
|
663
|
+
* Measured bit for bit: a sum that cancels to a tiny residue (relative to the operands, or below 1e-10) is 0.
|
|
664
|
+
*/
|
|
665
|
+
function wmaAdd(a, b) {
|
|
666
|
+
const sum = a + b;
|
|
667
|
+
return Math.abs(sum) <= 1e-10 * Math.max(1, Math.abs(a) + Math.abs(b)) ? 0 : sum;
|
|
668
|
+
}
|
|
668
669
|
/**
|
|
669
670
|
* Weighted Moving Average (WMA) - moving average with linearly decreasing weights.
|
|
670
671
|
*
|
|
@@ -678,6 +679,8 @@ export function rma(source, length) {
|
|
|
678
679
|
* - Formula: `sum(source[i]! * (length - i)) / sum(length - i)` for i = 0 to length-1
|
|
679
680
|
* - As in PineScript: na on a bar whose source is `na` (also +/-Infinity) and until `length` non-`na` values were
|
|
680
681
|
* received; an `na` value inside the window is replaced by the previous non-`na` value
|
|
682
|
+
* - Summed from the oldest value to the newest; as PineScript, a partial sum `s + t` with
|
|
683
|
+
* `|s + t| <= 1e-10 * max(1, |s| + |t|)` is 0 (a cancellation residue, or values below 1e-10)
|
|
681
684
|
* - More responsive to recent price changes than SMA
|
|
682
685
|
*
|
|
683
686
|
* @example
|
|
@@ -712,7 +715,7 @@ export function wma(source, length) {
|
|
|
712
715
|
let weightSum = 0;
|
|
713
716
|
for (let j = len - 1; j >= 0; j--) {
|
|
714
717
|
const weight = len - j;
|
|
715
|
-
sum
|
|
718
|
+
sum = wmaAdd(sum, filled[i - j] * weight);
|
|
716
719
|
weightSum += weight;
|
|
717
720
|
}
|
|
718
721
|
result.push(sum / weightSum);
|
|
@@ -784,10 +787,12 @@ export function cum(source) {
|
|
|
784
787
|
* @returns Boolean series (true at cross points)
|
|
785
788
|
*
|
|
786
789
|
* @remarks
|
|
787
|
-
*
|
|
788
|
-
*
|
|
789
|
-
*
|
|
790
|
-
* -
|
|
790
|
+
* PineScript rule (measured bit for bit; not `crossover or crossunder`):
|
|
791
|
+
* - The call keeps the side of the last bar where `source1` was strictly above or strictly below `source2` (no
|
|
792
|
+
* side before such a bar). Equal bars and bars with an `na` value do not change it.
|
|
793
|
+
* - True when the values are strictly on the other side of the kept side: so a run of equal values is passed, and
|
|
794
|
+
* a series that starts equal and then moves away is not a cross
|
|
795
|
+
* - False on a bar with an `na` value
|
|
791
796
|
* - Use `ta.crossover()` or `ta.crossunder()` for directional crosses
|
|
792
797
|
*
|
|
793
798
|
* @example
|
|
@@ -798,36 +803,38 @@ export function cum(source) {
|
|
|
798
803
|
*/
|
|
799
804
|
export function cross(source1, source2) {
|
|
800
805
|
const result = [];
|
|
806
|
+
let side = 0; // -1: last strictly below, 1: last strictly above, 0: none yet
|
|
801
807
|
for (let i = 0; i < source1.length; i++) {
|
|
802
|
-
|
|
808
|
+
const a = source1[i];
|
|
809
|
+
const b = source2[i];
|
|
810
|
+
if (Number.isNaN(a) || Number.isNaN(b)) {
|
|
803
811
|
result.push(false);
|
|
812
|
+
continue;
|
|
804
813
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
814
|
+
result.push((side < 0 && a > b) || (side > 0 && a < b));
|
|
815
|
+
if (a < b)
|
|
816
|
+
side = -1;
|
|
817
|
+
else if (a > b)
|
|
818
|
+
side = 1;
|
|
810
819
|
}
|
|
811
820
|
return result;
|
|
812
821
|
}
|
|
813
822
|
/**
|
|
814
|
-
* ta.rising / ta.falling:
|
|
815
|
-
*
|
|
823
|
+
* ta.rising / ta.falling: a counter of the steps from one bar to the next that pass `step(newer, older)`, reset to 0
|
|
824
|
+
* by a step that does not pass; a step with an na value (on either bar) leaves the counter unchanged. True when the
|
|
825
|
+
* counter is at least `length`.
|
|
816
826
|
*
|
|
817
827
|
* @internal
|
|
818
828
|
*/
|
|
819
829
|
function monotonic(source, length, step) {
|
|
820
830
|
const result = [];
|
|
821
|
-
|
|
831
|
+
let count = 0;
|
|
822
832
|
for (let i = 0; i < source.length; i++) {
|
|
823
833
|
const v = source[i];
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
for (let j = 1; ok && j <= length; j++)
|
|
829
|
-
ok = step(values[n - j], values[n - j - 1]);
|
|
830
|
-
result.push(ok);
|
|
834
|
+
const prev = i > 0 ? source[i - 1] : NaN;
|
|
835
|
+
if (!Number.isNaN(v) && !Number.isNaN(prev))
|
|
836
|
+
count = step(v, prev) ? count + 1 : 0;
|
|
837
|
+
result.push(count >= length);
|
|
831
838
|
}
|
|
832
839
|
return result;
|
|
833
840
|
}
|
|
@@ -840,10 +847,11 @@ function monotonic(source, length, step) {
|
|
|
840
847
|
*
|
|
841
848
|
* @remarks
|
|
842
849
|
* PineScript rules:
|
|
843
|
-
* -
|
|
844
|
-
*
|
|
845
|
-
*
|
|
846
|
-
*
|
|
850
|
+
* - a counter of rises from the previous bar (a rise is larger than the 1e-10 tolerance); a step that is not a rise
|
|
851
|
+
* resets it to 0
|
|
852
|
+
* - a step from or to an `na` bar leaves the counter unchanged (so an `na` bar, and the bar after it, keep the
|
|
853
|
+
* result of the bar before the `na`)
|
|
854
|
+
* - true when the counter is at least `length` (false at the start)
|
|
847
855
|
*
|
|
848
856
|
* @example
|
|
849
857
|
* ```typescript
|
|
@@ -863,10 +871,11 @@ export function rising(source, length) {
|
|
|
863
871
|
*
|
|
864
872
|
* @remarks
|
|
865
873
|
* PineScript rules:
|
|
866
|
-
* -
|
|
867
|
-
*
|
|
868
|
-
*
|
|
869
|
-
*
|
|
874
|
+
* - a counter of falls from the previous bar (a fall is larger than the 1e-10 tolerance); a step that is not a fall
|
|
875
|
+
* resets it to 0
|
|
876
|
+
* - a step from or to an `na` bar leaves the counter unchanged (so an `na` bar, and the bar after it, keep the
|
|
877
|
+
* result of the bar before the `na`)
|
|
878
|
+
* - true when the counter is at least `length` (false at the start)
|
|
870
879
|
*
|
|
871
880
|
* @example
|
|
872
881
|
* ```typescript
|
|
@@ -963,10 +972,11 @@ export function mom(source, length) {
|
|
|
963
972
|
*
|
|
964
973
|
* @remarks
|
|
965
974
|
* - Measures average absolute distance from the mean
|
|
966
|
-
* - Formula: `sum(abs(source[i]! - sma)) / length` for i in 0 to length-1
|
|
967
975
|
* - Less sensitive to outliers than standard deviation
|
|
968
|
-
* - `
|
|
969
|
-
*
|
|
976
|
+
* - PineScript rule (bit for bit): `sum(abs(source[i] - mean)) / length` for `i = 0 .. length - 1`, added newest
|
|
977
|
+
* first, with `mean = ta.sma(source, length)` (its compensated running sum, so a flat window can give a tiny
|
|
978
|
+
* non-zero deviation)
|
|
979
|
+
* - The loop reads the last `length` bars: an `na` among them gives `na`
|
|
970
980
|
*
|
|
971
981
|
* @example
|
|
972
982
|
* ```typescript
|
|
@@ -975,24 +985,15 @@ export function mom(source, length) {
|
|
|
975
985
|
* ```
|
|
976
986
|
*/
|
|
977
987
|
export function dev(source, length) {
|
|
978
|
-
const
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
for (let j = 0; j < length; j++) {
|
|
988
|
-
if (!isNaN(source[i - j])) {
|
|
989
|
-
sum += Math.abs(source[i - j] - meanValues[i]);
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
result.push(sum / length);
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
return result;
|
|
988
|
+
const len = Math.floor(length);
|
|
989
|
+
return sma(source, len).map((mean, i) => {
|
|
990
|
+
if (i < len - 1 || Number.isNaN(mean))
|
|
991
|
+
return NaN;
|
|
992
|
+
let sum = 0;
|
|
993
|
+
for (let k = 0; k < len; k++)
|
|
994
|
+
sum += Math.abs(source[i - k] - mean);
|
|
995
|
+
return sum / len;
|
|
996
|
+
});
|
|
996
997
|
}
|
|
997
998
|
/**
|
|
998
999
|
* Variance - expectation of squared deviation from mean.
|
|
@@ -1021,6 +1022,14 @@ export function dev(source, length) {
|
|
|
1021
1022
|
* ```
|
|
1022
1023
|
*/
|
|
1023
1024
|
export function variance(source, length, biased = true) {
|
|
1025
|
+
return varianceOf(source, length, biased, 'ta.variance');
|
|
1026
|
+
}
|
|
1027
|
+
/** {@link variance} with a fixed or a series length; `name` is used in the error messages. */
|
|
1028
|
+
function varianceOf(source, length, biased, name) {
|
|
1029
|
+
if (typeof length !== 'number') {
|
|
1030
|
+
const result = runningVarianceSeries(source, length, biased, name);
|
|
1031
|
+
return biased ? result : result.map((v, i) => (Math.floor(length[i]) > 1 ? v : NaN));
|
|
1032
|
+
}
|
|
1024
1033
|
const len = Math.floor(length);
|
|
1025
1034
|
// PineScript: the last `len` non-na values (also +/-Infinity skipped), as ta.sma
|
|
1026
1035
|
if ((biased ? len : len - 1) <= 0)
|
|
@@ -1096,7 +1105,7 @@ export function median(source, length) {
|
|
|
1096
1105
|
* @remarks
|
|
1097
1106
|
* - Fixed length of 4 bars
|
|
1098
1107
|
* - Weights: [1/6, 2/6, 2/6, 1/6] (symmetric)
|
|
1099
|
-
* - Formula: `source
|
|
1108
|
+
* - Formula, in this order as in PineScript (bit for bit): `(source + 2 * source[1] + 2 * source[2] + source[3]) / 6`
|
|
1100
1109
|
* - More weight given to middle values
|
|
1101
1110
|
* - `na` values in the source series are included in calculations and will produce an `na` result
|
|
1102
1111
|
* - Returns NaN for the first 3 bars
|
|
@@ -1108,26 +1117,7 @@ export function median(source, length) {
|
|
|
1108
1117
|
* ```
|
|
1109
1118
|
*/
|
|
1110
1119
|
export function swma(source) {
|
|
1111
|
-
|
|
1112
|
-
for (let i = 0; i < source.length; i++) {
|
|
1113
|
-
if (i < 3) {
|
|
1114
|
-
result.push(NaN);
|
|
1115
|
-
}
|
|
1116
|
-
else {
|
|
1117
|
-
// Check for any NaN values in the window
|
|
1118
|
-
if (isNaN(source[i]) || isNaN(source[i - 1]) || isNaN(source[i - 2]) || isNaN(source[i - 3])) {
|
|
1119
|
-
result.push(NaN);
|
|
1120
|
-
}
|
|
1121
|
-
else {
|
|
1122
|
-
const value = source[i - 3] * (1 / 6) +
|
|
1123
|
-
source[i - 2] * (2 / 6) +
|
|
1124
|
-
source[i - 1] * (2 / 6) +
|
|
1125
|
-
source[i] * (1 / 6);
|
|
1126
|
-
result.push(value);
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
return result;
|
|
1120
|
+
return Array.from({ length: source.length }, (_, i) => i < 3 ? NaN : (source[i] + 2 * source[i - 1] + 2 * source[i - 2] + source[i - 3]) / 6);
|
|
1131
1121
|
}
|
|
1132
1122
|
/**
|
|
1133
1123
|
* Volume Weighted Moving Average (VWMA) - moving average weighted by volume.
|
|
@@ -1189,6 +1179,9 @@ export function vwma(source, length, volume) {
|
|
|
1189
1179
|
* @remarks
|
|
1190
1180
|
* - Calculates line of best fit using least squares method
|
|
1191
1181
|
* - Formula: `linreg = intercept + slope * (length - 1 - offset)`
|
|
1182
|
+
* - As PineScript (bit for bit): x = 1 (oldest) .. length (newest), sums from the oldest value,
|
|
1183
|
+
* `slope = (length * sumXY - sumX * sumY) / (length * sumX2 - sumX * sumX)`,
|
|
1184
|
+
* `intercept = sumY / length - slope * sumX / length + slope`
|
|
1192
1185
|
* - offset=0 gives current fitted value, offset<0 gives future projection
|
|
1193
1186
|
* - `na` values in the source series are included in calculations and will produce an `na` result
|
|
1194
1187
|
* - Useful for trend detection and prediction
|
|
@@ -1220,13 +1213,14 @@ export function linreg(source, length, offset = 0) {
|
|
|
1220
1213
|
result.push(NaN);
|
|
1221
1214
|
}
|
|
1222
1215
|
else {
|
|
1223
|
-
//
|
|
1216
|
+
// Least squares as PineScript: x = 1 (oldest) .. len (newest), summed from the oldest value; the intercept
|
|
1217
|
+
// is the fitted value at x = 1
|
|
1224
1218
|
let sumX = 0;
|
|
1225
1219
|
let sumY = 0;
|
|
1226
1220
|
let sumXY = 0;
|
|
1227
1221
|
let sumX2 = 0;
|
|
1228
1222
|
for (let j = 0; j < len; j++) {
|
|
1229
|
-
const x = j;
|
|
1223
|
+
const x = j + 1;
|
|
1230
1224
|
const y = source[i - (len - 1 - j)];
|
|
1231
1225
|
sumX += x;
|
|
1232
1226
|
sumY += y;
|
|
@@ -1234,10 +1228,8 @@ export function linreg(source, length, offset = 0) {
|
|
|
1234
1228
|
sumX2 += x * x;
|
|
1235
1229
|
}
|
|
1236
1230
|
const slope = (len * sumXY - sumX * sumY) / (len * sumX2 - sumX * sumX);
|
|
1237
|
-
const intercept =
|
|
1238
|
-
|
|
1239
|
-
const x = len - 1 - offset;
|
|
1240
|
-
result.push(intercept + slope * x);
|
|
1231
|
+
const intercept = sumY / len - (slope * sumX) / len + slope;
|
|
1232
|
+
result.push(intercept + slope * (len - 1 - offset));
|
|
1241
1233
|
}
|
|
1242
1234
|
}
|
|
1243
1235
|
}
|
|
@@ -1257,8 +1249,9 @@ export function linreg(source, length, offset = 0) {
|
|
|
1257
1249
|
* - -1 = perfect negative correlation
|
|
1258
1250
|
* - 0 = no correlation
|
|
1259
1251
|
* - Measures linear relationship between two series
|
|
1260
|
-
* - `
|
|
1261
|
-
*
|
|
1252
|
+
* - As PineScript: `cov = sma(x * y) - sma(x) * sma(y)`, `r = cov / sqrt(variance(x) * variance(y))` with the
|
|
1253
|
+
* compensated running sums of `ta.sma` / `ta.variance` (each skips its own `na` values); `|cov| <= 1e-10` gives 0, a
|
|
1254
|
+
* zero variance gives `na`
|
|
1262
1255
|
*
|
|
1263
1256
|
* @example
|
|
1264
1257
|
* ```typescript
|
|
@@ -1267,54 +1260,28 @@ export function linreg(source, length, offset = 0) {
|
|
|
1267
1260
|
* ```
|
|
1268
1261
|
*/
|
|
1269
1262
|
export function correlation(source1, source2, length) {
|
|
1270
|
-
|
|
1271
|
-
//
|
|
1272
|
-
const
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
continue;
|
|
1293
|
-
}
|
|
1294
|
-
// Calculate means
|
|
1295
|
-
let sum1 = 0;
|
|
1296
|
-
let sum2 = 0;
|
|
1297
|
-
for (let k = 0; k < m; k++) {
|
|
1298
|
-
sum1 += a[k];
|
|
1299
|
-
sum2 += b[k];
|
|
1300
|
-
}
|
|
1301
|
-
const mean1 = sum1 / m;
|
|
1302
|
-
const mean2 = sum2 / m;
|
|
1303
|
-
// Calculate correlation components
|
|
1304
|
-
let numerator = 0;
|
|
1305
|
-
let sum1Sq = 0;
|
|
1306
|
-
let sum2Sq = 0;
|
|
1307
|
-
for (let k = 0; k < m; k++) {
|
|
1308
|
-
const dev1 = a[k] - mean1;
|
|
1309
|
-
const dev2 = b[k] - mean2;
|
|
1310
|
-
numerator += dev1 * dev2;
|
|
1311
|
-
sum1Sq += dev1 * dev1;
|
|
1312
|
-
sum2Sq += dev2 * dev2;
|
|
1313
|
-
}
|
|
1314
|
-
const denominator = Math.sqrt(sum1Sq * sum2Sq);
|
|
1315
|
-
result.push(denominator === 0 ? NaN : numerator / denominator);
|
|
1316
|
-
}
|
|
1317
|
-
return result;
|
|
1263
|
+
// PineScript: from compensated running sums, cov = sma(x * y) - sma(x) * sma(y), r = cov / sqrt(var(x) * var(y))
|
|
1264
|
+
// (biased variances); each running sum skips its own na values
|
|
1265
|
+
const product = Array.from({ length: source1.length }, (_, i) => {
|
|
1266
|
+
const v1 = source1[i];
|
|
1267
|
+
const v2 = source2[i];
|
|
1268
|
+
return v1 === null || v1 === undefined || v2 === null || v2 === undefined ? NaN : v1 * v2;
|
|
1269
|
+
});
|
|
1270
|
+
const meanXY = sma(product, length);
|
|
1271
|
+
const meanX = sma(source1, length);
|
|
1272
|
+
const meanY = sma(source2, length);
|
|
1273
|
+
const varX = variance(source1, length);
|
|
1274
|
+
const varY = variance(source2, length);
|
|
1275
|
+
return meanXY.map((mxy, i) => {
|
|
1276
|
+
const cov = mxy - meanX[i] * meanY[i];
|
|
1277
|
+
if (Number.isNaN(cov))
|
|
1278
|
+
return NaN;
|
|
1279
|
+
// a covariance within 1e-10 of 0 gives 0; a zero denominator gives na (a division by zero)
|
|
1280
|
+
if (Math.abs(cov) <= 1e-10)
|
|
1281
|
+
return 0;
|
|
1282
|
+
const denominator = Math.sqrt(varX[i] * varY[i]);
|
|
1283
|
+
return denominator === 0 ? NaN : cov / denominator;
|
|
1284
|
+
});
|
|
1318
1285
|
}
|
|
1319
1286
|
/**
|
|
1320
1287
|
* Percent Rank - percentage of how many previous values were less than or equal to current value.
|
|
@@ -1331,7 +1298,7 @@ export function correlation(source1, source2, length) {
|
|
|
1331
1298
|
* - Useful for identifying relative strength within a period
|
|
1332
1299
|
*
|
|
1333
1300
|
* PineScript rules:
|
|
1334
|
-
* - percentrank = count(source[k] <= source, k = 1..length) / length
|
|
1301
|
+
* - percentrank = count(source[k] <= source, k = 1..length) * 100 / length (in this order: the PineScript bits)
|
|
1335
1302
|
* - a value from bar `length` on, when the current source is not `na`; an `na` value in the window counts as not
|
|
1336
1303
|
* `<=` the current value
|
|
1337
1304
|
* - the comparison uses the 1e-10 tolerance
|
|
@@ -1357,7 +1324,7 @@ export function percentrank(source, length) {
|
|
|
1357
1324
|
if (le(source[i - j], currentValue))
|
|
1358
1325
|
countLessOrEqual++;
|
|
1359
1326
|
}
|
|
1360
|
-
result.push((countLessOrEqual
|
|
1327
|
+
result.push((countLessOrEqual * 100) / length);
|
|
1361
1328
|
}
|
|
1362
1329
|
return result;
|
|
1363
1330
|
}
|
|
@@ -1373,10 +1340,14 @@ export function percentrank(source, length) {
|
|
|
1373
1340
|
* - Typical Price = (High + Low + Close) / 3
|
|
1374
1341
|
* - Mean Deviation = Average of absolute differences from mean
|
|
1375
1342
|
* - Scaled by 0.015 to provide more readable numbers
|
|
1376
|
-
* - As in PineScript: when the mean deviation is 0 (1e-10 tolerance, a flat window), the CCI of the previous bar
|
|
1377
1343
|
* - Values above +100 indicate overbought conditions
|
|
1378
1344
|
* - Values below -100 indicate oversold conditions
|
|
1379
|
-
*
|
|
1345
|
+
*
|
|
1346
|
+
* PineScript rules (bit for bit):
|
|
1347
|
+
* - `(source - ta.sma(source, length)) / (0.015 * ta.dev(source, length))`
|
|
1348
|
+
* - when the mean deviation is 0 (1e-10 tolerance, a flat window), the last CCI value; 0 when there is none yet
|
|
1349
|
+
* (a flat first window)
|
|
1350
|
+
* - \`na\` when the mean or the deviation is \`na\` (an \`na\` in the last \`length\` bars)
|
|
1380
1351
|
*
|
|
1381
1352
|
* @example
|
|
1382
1353
|
* \`\`\`typescript
|
|
@@ -1386,22 +1357,17 @@ export function percentrank(source, length) {
|
|
|
1386
1357
|
* \`\`\`
|
|
1387
1358
|
*/
|
|
1388
1359
|
export function cci(source, length) {
|
|
1389
|
-
const result = [];
|
|
1390
1360
|
const smaValues = sma(source, length);
|
|
1391
1361
|
const devValues = dev(source, length);
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
result.push(cci);
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
return result;
|
|
1362
|
+
let last = 0;
|
|
1363
|
+
return smaValues.map((mean, i) => {
|
|
1364
|
+
const d = devValues[i];
|
|
1365
|
+
if (Number.isNaN(mean) || Number.isNaN(d))
|
|
1366
|
+
return NaN;
|
|
1367
|
+
if (!eq(d, 0))
|
|
1368
|
+
last = (source[i] - mean) / (0.015 * d);
|
|
1369
|
+
return last;
|
|
1370
|
+
});
|
|
1405
1371
|
}
|
|
1406
1372
|
/**
|
|
1407
1373
|
* Stochastic Oscillator - momentum indicator comparing closing price to price range.
|
|
@@ -1417,8 +1383,8 @@ export function cci(source, length) {
|
|
|
1417
1383
|
* - Measures where close is relative to the high-low range
|
|
1418
1384
|
* - Values above 80 typically indicate overbought
|
|
1419
1385
|
* - Values below 20 typically indicate oversold
|
|
1420
|
-
* -
|
|
1421
|
-
*
|
|
1386
|
+
* - As PineScript: when the source, \`highest(high, length)\` or \`lowest(low, length)\` is \`na\`, or the range is 0, the
|
|
1387
|
+
* result is the previous result of the call (\`na\` until a first value)
|
|
1422
1388
|
*
|
|
1423
1389
|
* @example
|
|
1424
1390
|
* \`\`\`typescript
|
|
@@ -1431,20 +1397,15 @@ export function stoch(source, high, low, length) {
|
|
|
1431
1397
|
const result = [];
|
|
1432
1398
|
const lowestValues = lowest(low, length);
|
|
1433
1399
|
const highestValues = highest(high, length);
|
|
1400
|
+
// PineScript: an na input or a zero range keeps the previous result
|
|
1401
|
+
let previous = NaN;
|
|
1434
1402
|
for (let i = 0; i < source.length; i++) {
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
result.push(NaN);
|
|
1442
|
-
}
|
|
1443
|
-
else {
|
|
1444
|
-
const stochValue = 100 * (source[i] - lowestValues[i]) / range;
|
|
1445
|
-
result.push(stochValue);
|
|
1446
|
-
}
|
|
1447
|
-
}
|
|
1403
|
+
const value = source[i];
|
|
1404
|
+
const lo = lowestValues[i];
|
|
1405
|
+
const range = highestValues[i] - lo;
|
|
1406
|
+
if (!Number.isNaN(value) && !Number.isNaN(range) && range !== 0)
|
|
1407
|
+
previous = (100 * (value - lo)) / range;
|
|
1408
|
+
result.push(previous);
|
|
1448
1409
|
}
|
|
1449
1410
|
return result;
|
|
1450
1411
|
}
|
|
@@ -1463,7 +1424,13 @@ export function stoch(source, high, low, length) {
|
|
|
1463
1424
|
* - Formula: 100 - (100 / (1 + Positive Money Flow / Negative Money Flow))
|
|
1464
1425
|
* - Values above 80 indicate overbought
|
|
1465
1426
|
* - Values below 20 indicate oversold
|
|
1466
|
-
*
|
|
1427
|
+
*
|
|
1428
|
+
* PineScript rules (bit for bit):
|
|
1429
|
+
* - the two flows are `math.sum` running sums; a sum within 1e-10 of 0 counts as 0 (the residue a running sum
|
|
1430
|
+
* keeps over a window of zeros)
|
|
1431
|
+
* - 100 when the negative flow is 0; `na` when `1 + positive / negative` is 0 (division by zero)
|
|
1432
|
+
* - a bar with an `na` source adds nothing to the sums and gives the previous result; the bar after it (an `na`
|
|
1433
|
+
* change) adds its money flow to both sums
|
|
1467
1434
|
*
|
|
1468
1435
|
* @example
|
|
1469
1436
|
* ```typescript
|
|
@@ -1496,13 +1463,18 @@ export function mfi(source, length, volume) {
|
|
|
1496
1463
|
// math.sum: the compensated running sum of the last `length` non-na values (as PineScript, bit for bit)
|
|
1497
1464
|
const upper = runningSum(upperFlow, length);
|
|
1498
1465
|
const lower = runningSum(lowerFlow, length);
|
|
1499
|
-
|
|
1500
|
-
|
|
1466
|
+
// A sum within 1e-10 of 0 (the residue of a running sum over a window of zeros) is 0
|
|
1467
|
+
const zero = (x) => (Math.abs(x) <= 1e-10 ? 0 : x);
|
|
1468
|
+
return upper.map((sumUp, i) => {
|
|
1469
|
+
const u = zero(sumUp);
|
|
1470
|
+
const l = zero(lower[i]);
|
|
1501
1471
|
if (Number.isNaN(u) || Number.isNaN(l))
|
|
1502
1472
|
return NaN;
|
|
1503
1473
|
if (l === 0)
|
|
1504
1474
|
return 100;
|
|
1505
|
-
|
|
1475
|
+
const ratio = 1 + u / l;
|
|
1476
|
+
// x / 0 is na in PineScript
|
|
1477
|
+
return ratio === 0 ? NaN : 100 - 100 / ratio;
|
|
1506
1478
|
});
|
|
1507
1479
|
}
|
|
1508
1480
|
/**
|
|
@@ -1870,10 +1842,12 @@ export function dmi(diLength, adxSmoothing, high, low, close) {
|
|
|
1870
1842
|
* @param source - Series of values to process
|
|
1871
1843
|
* @param shortLength - Short smoothing length
|
|
1872
1844
|
* @param longLength - Long smoothing length
|
|
1873
|
-
* @returns TSI series
|
|
1845
|
+
* @returns TSI series, a value in [-1, 1]
|
|
1874
1846
|
*
|
|
1875
1847
|
* @remarks
|
|
1876
|
-
* -
|
|
1848
|
+
* - As PineScript: `ta.ema(ta.ema(m, longLength), shortLength) / ta.ema(ta.ema(math.abs(m), longLength), shortLength)`
|
|
1849
|
+
* with `m = source - source[1]` (not scaled by 100)
|
|
1850
|
+
* - na when the denominator is 0 (a constant source), as a PineScript division by zero
|
|
1877
1851
|
* - Positive values indicate bullish momentum
|
|
1878
1852
|
* - Negative values indicate bearish momentum
|
|
1879
1853
|
* - Crossovers of zero line can signal trend changes
|
|
@@ -1902,17 +1876,11 @@ export function tsi(source, shortLength, longLength) {
|
|
|
1902
1876
|
// Double smooth absolute momentum
|
|
1903
1877
|
const absMomentum = momentum.map(Math.abs);
|
|
1904
1878
|
const smoothedAbsMomentum = ema(ema(absMomentum, longLength), shortLength);
|
|
1905
|
-
//
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
}
|
|
1911
|
-
else {
|
|
1912
|
-
result.push((smoothedMomentum[i] / smoothedAbsMomentum[i]) * 100);
|
|
1913
|
-
}
|
|
1914
|
-
}
|
|
1915
|
-
return result;
|
|
1879
|
+
// PineScript: a value in [-1, 1]; a division by zero is na
|
|
1880
|
+
return smoothedMomentum.map((m, i) => {
|
|
1881
|
+
const d = smoothedAbsMomentum[i];
|
|
1882
|
+
return d === 0 ? NaN : m / d;
|
|
1883
|
+
});
|
|
1916
1884
|
}
|
|
1917
1885
|
/**
|
|
1918
1886
|
* Chande Momentum Oscillator - momentum indicator similar to RSI.
|
|
@@ -1926,7 +1894,8 @@ export function tsi(source, shortLength, longLength) {
|
|
|
1926
1894
|
* - CMO > +50: overbought conditions
|
|
1927
1895
|
* - CMO < -50: oversold conditions
|
|
1928
1896
|
* - Unlike RSI, CMO uses sum of gains/losses instead of averages
|
|
1929
|
-
* -
|
|
1897
|
+
* - As in PineScript: the sums are the running sums of `math.sum` (bit for bit), and a flat window (every change 0)
|
|
1898
|
+
* is `0 / 0` = na
|
|
1930
1899
|
* - More volatile than RSI
|
|
1931
1900
|
*
|
|
1932
1901
|
* @example
|
|
@@ -1937,34 +1906,20 @@ export function tsi(source, shortLength, longLength) {
|
|
|
1937
1906
|
* ```
|
|
1938
1907
|
*/
|
|
1939
1908
|
export function cmo(source, length) {
|
|
1940
|
-
|
|
1909
|
+
// PineScript definition: mom = ta.change(src), sm1 = math.sum(mom >= 0 ? mom : 0, length),
|
|
1910
|
+
// sm2 = math.sum(mom >= 0 ? 0 : -mom, length), cmo = 100 * (sm1 - sm2) / (sm1 + sm2), with the running sums of
|
|
1911
|
+
// math.sum (bit for bit). A comparison with na is false: an na change adds 0 to sm1 and na (skipped) to sm2.
|
|
1912
|
+
const up = [];
|
|
1913
|
+
const down = [];
|
|
1941
1914
|
for (let i = 0; i < source.length; i++) {
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
}
|
|
1946
|
-
let sumGains = 0;
|
|
1947
|
-
let sumLosses = 0;
|
|
1948
|
-
for (let j = 0; j < length; j++) {
|
|
1949
|
-
const change = source[i - j] - source[i - j - 1];
|
|
1950
|
-
if (change > 0) {
|
|
1951
|
-
sumGains += change;
|
|
1952
|
-
}
|
|
1953
|
-
else {
|
|
1954
|
-
sumLosses += Math.abs(change);
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
1957
|
-
// PineScript: 100 * (sm1 - sm2) / (sm1 + sm2), so a flat window (0 / 0) is na
|
|
1958
|
-
const totalMovement = sumGains + sumLosses;
|
|
1959
|
-
if (totalMovement === 0) {
|
|
1960
|
-
result.push(NaN);
|
|
1961
|
-
}
|
|
1962
|
-
else {
|
|
1963
|
-
const cmoValue = ((sumGains - sumLosses) / totalMovement) * 100;
|
|
1964
|
-
result.push(cmoValue);
|
|
1965
|
-
}
|
|
1915
|
+
const mom = i > 0 ? source[i] - source[i - 1] : NaN;
|
|
1916
|
+
up.push(mom >= 0 ? mom : 0);
|
|
1917
|
+
down.push(mom >= 0 ? 0 : -mom);
|
|
1966
1918
|
}
|
|
1967
|
-
|
|
1919
|
+
const sm1 = runningSum(up, length);
|
|
1920
|
+
const sm2 = runningSum(down, length);
|
|
1921
|
+
// a flat window is 0 / 0 = na
|
|
1922
|
+
return sm1.map((s1, i) => (100 * (s1 - sm2[i])) / (s1 + sm2[i]));
|
|
1968
1923
|
}
|
|
1969
1924
|
/**
|
|
1970
1925
|
* Keltner Channels - volatility-based envelope indicator.
|
|
@@ -2074,6 +2029,10 @@ export function bbw(source, length, mult) {
|
|
|
2074
2029
|
* @param length - Lookback period (default: 14)
|
|
2075
2030
|
* @returns Williams %R series
|
|
2076
2031
|
*
|
|
2032
|
+
* @remarks
|
|
2033
|
+
* PineScript rule (bit for bit): `100 * (close - max) / (max - min)` with `max = ta.highest(high, length)` and
|
|
2034
|
+
* `min = ta.lowest(low, length)` (their `na` rules); `na` when the range is 0.
|
|
2035
|
+
*
|
|
2077
2036
|
* @example
|
|
2078
2037
|
* ```typescript
|
|
2079
2038
|
* const wpr = ta.wpr(high, low, close, 14);
|
|
@@ -2082,32 +2041,14 @@ export function bbw(source, length, mult) {
|
|
|
2082
2041
|
* ```
|
|
2083
2042
|
*/
|
|
2084
2043
|
export function wpr(high, low, close, length = 14) {
|
|
2085
|
-
const
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
let lowestLow = low[i - length + 1];
|
|
2094
|
-
for (let j = i - length + 2; j <= i; j++) {
|
|
2095
|
-
if (high[j] > highestHigh)
|
|
2096
|
-
highestHigh = high[j];
|
|
2097
|
-
if (low[j] < lowestLow)
|
|
2098
|
-
lowestLow = low[j];
|
|
2099
|
-
}
|
|
2100
|
-
const range = highestHigh - lowestLow;
|
|
2101
|
-
if (range === 0) {
|
|
2102
|
-
result.push(NaN);
|
|
2103
|
-
}
|
|
2104
|
-
else {
|
|
2105
|
-
// Formula: (Highest High - Close) / (Highest High - Lowest Low) * -100
|
|
2106
|
-
const wprValue = ((highestHigh - close[i]) / range) * -100;
|
|
2107
|
-
result.push(wprValue);
|
|
2108
|
-
}
|
|
2109
|
-
}
|
|
2110
|
-
return result;
|
|
2044
|
+
const highs = highest(high, length);
|
|
2045
|
+
const lows = lowest(low, length);
|
|
2046
|
+
return Array.from({ length: close.length }, (_, i) => {
|
|
2047
|
+
const hmax = highs[i];
|
|
2048
|
+
const range = hmax - lows[i];
|
|
2049
|
+
// PineScript, in this order (bit for bit): 100 * (close - max) / (max - min); x / 0 is na
|
|
2050
|
+
return range === 0 ? NaN : (100 * (close[i] - hmax)) / range;
|
|
2051
|
+
});
|
|
2111
2052
|
}
|
|
2112
2053
|
export function vwap(source, volume, anchor, stdev_mult) {
|
|
2113
2054
|
if (source.length !== volume.length) {
|
|
@@ -2116,18 +2057,20 @@ export function vwap(source, volume, anchor, stdev_mult) {
|
|
|
2116
2057
|
const mid = [];
|
|
2117
2058
|
const upper = [];
|
|
2118
2059
|
const lower = [];
|
|
2119
|
-
|
|
2120
|
-
let
|
|
2121
|
-
let
|
|
2060
|
+
// na until the first anchor (without an anchor series, bar 0 starts the sums)
|
|
2061
|
+
let sumPV = NaN;
|
|
2062
|
+
let sumV = NaN;
|
|
2063
|
+
let sumPPV = NaN;
|
|
2122
2064
|
for (let i = 0; i < source.length; i++) {
|
|
2123
|
-
if (anchor && anchor[i]) {
|
|
2124
|
-
sumPV = 0;
|
|
2125
|
-
sumV = 0;
|
|
2126
|
-
sumPPV = 0;
|
|
2127
|
-
}
|
|
2128
2065
|
const s = source[i];
|
|
2129
2066
|
const v = volume[i];
|
|
2130
|
-
if (
|
|
2067
|
+
if (anchor ? anchor[i] : i === 0) {
|
|
2068
|
+
sumPV = s * v;
|
|
2069
|
+
sumV = v;
|
|
2070
|
+
sumPPV = s * s * v;
|
|
2071
|
+
}
|
|
2072
|
+
else {
|
|
2073
|
+
// An na source or volume makes the sums na until the next anchor
|
|
2131
2074
|
sumPV += s * v;
|
|
2132
2075
|
sumV += v;
|
|
2133
2076
|
sumPPV += s * s * v;
|
|
@@ -2194,7 +2137,8 @@ export function alma(source, length = 9, offset = 0.85, sigma = 6, floor = false
|
|
|
2194
2137
|
/**
|
|
2195
2138
|
* Keltner Channels Width (KCW)
|
|
2196
2139
|
*
|
|
2197
|
-
* Measures the width of Keltner Channels
|
|
2140
|
+
* Measures the width of Keltner Channels relative to the middle line: `(upper - lower) / middle`, a ratio as in
|
|
2141
|
+
* PineScript (0.05 is a width of 5% of the middle line; not multiplied by 100).
|
|
2198
2142
|
* Similar to BBW but uses ATR instead of standard deviation.
|
|
2199
2143
|
*
|
|
2200
2144
|
* Low KCW suggests consolidation/low volatility.
|
|
@@ -2224,8 +2168,8 @@ export function kcw(source, length = 20, mult = 2, useTrueRange = true, high, lo
|
|
|
2224
2168
|
result.push(NaN);
|
|
2225
2169
|
}
|
|
2226
2170
|
else {
|
|
2227
|
-
|
|
2228
|
-
result.push(
|
|
2171
|
+
// PineScript: (upper - lower) / basis, a ratio (not a percentage)
|
|
2172
|
+
result.push((upper[i] - lower[i]) / basis[i]);
|
|
2229
2173
|
}
|
|
2230
2174
|
}
|
|
2231
2175
|
return result;
|
|
@@ -2302,46 +2246,32 @@ export function min(source) {
|
|
|
2302
2246
|
* The Center of Gravity indicator is an oscillator developed by John Ehlers.
|
|
2303
2247
|
* It identifies turning points with minimal lag and provides clear signals.
|
|
2304
2248
|
*
|
|
2305
|
-
* The COG calculates a weighted average where more recent prices have higher weights,
|
|
2306
|
-
* similar to a moving average but with a focus on momentum shifts.
|
|
2307
|
-
*
|
|
2308
2249
|
* @param source - Source series (typically close)
|
|
2309
2250
|
* @param length - Lookback period (default: 10)
|
|
2310
2251
|
* @returns COG series
|
|
2311
2252
|
*
|
|
2253
|
+
* @remarks
|
|
2254
|
+
* PineScript rule (bit for bit): `-num / math.sum(source, length)`, with `num` the sum of `source[i] * (i + 1)`
|
|
2255
|
+
* for `i = 0 .. length - 1`, added newest first.
|
|
2256
|
+
* - The denominator is the compensated running sum of `math.sum` (the last `length` non-`na` values)
|
|
2257
|
+
* - The numerator reads the last `length` bars, so an `na` among them gives `na`
|
|
2258
|
+
* - A zero denominator gives `na` (PineScript division by zero)
|
|
2259
|
+
*
|
|
2312
2260
|
* @example
|
|
2313
2261
|
* ```typescript
|
|
2314
2262
|
* const cogValue = ta.cog(close, 10);
|
|
2315
|
-
* // Use COG crossovers as signals:
|
|
2316
|
-
* // - COG crossing above 0: potential buy signal
|
|
2317
|
-
* // - COG crossing below 0: potential sell signal
|
|
2318
2263
|
* ```
|
|
2319
2264
|
*/
|
|
2320
2265
|
export function cog(source, length = 10) {
|
|
2321
|
-
const
|
|
2322
|
-
|
|
2323
|
-
if (i <
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
const weight = j + 1;
|
|
2331
|
-
const price = source[i - length + 1 + j];
|
|
2332
|
-
numerator += weight * price;
|
|
2333
|
-
denominator += price;
|
|
2334
|
-
}
|
|
2335
|
-
if (denominator === 0) {
|
|
2336
|
-
result.push(NaN);
|
|
2337
|
-
}
|
|
2338
|
-
else {
|
|
2339
|
-
// COG formula: -1 * (sum of (weight * price) / sum of prices) + (length + 1) / 2
|
|
2340
|
-
const cog = -1 * (numerator / denominator) + (length + 1) / 2;
|
|
2341
|
-
result.push(cog);
|
|
2342
|
-
}
|
|
2343
|
-
}
|
|
2344
|
-
return result;
|
|
2266
|
+
const len = Math.floor(length);
|
|
2267
|
+
return runningSum(source, len).map((sum, i) => {
|
|
2268
|
+
if (i < len - 1 || Number.isNaN(sum) || sum === 0)
|
|
2269
|
+
return NaN;
|
|
2270
|
+
let num = 0;
|
|
2271
|
+
for (let k = 0; k < len; k++)
|
|
2272
|
+
num += source[i - k] * (k + 1);
|
|
2273
|
+
return -num / sum;
|
|
2274
|
+
});
|
|
2345
2275
|
}
|
|
2346
2276
|
/**
|
|
2347
2277
|
* Mode (Most Frequent Value)
|
|
@@ -2360,29 +2290,25 @@ export function cog(source, length = 10) {
|
|
|
2360
2290
|
* ```
|
|
2361
2291
|
*
|
|
2362
2292
|
* @remarks
|
|
2363
|
-
*
|
|
2293
|
+
* PineScript rules:
|
|
2294
|
+
* - the window is the last `length` non-`na` values (an `na` bar does not enter it, and gets the mode of the
|
|
2295
|
+
* window it keeps)
|
|
2296
|
+
* - NaN until `length` non-`na` values exist
|
|
2364
2297
|
* - If no mode exists, returns the smallest value
|
|
2365
|
-
* - Returns NaN for the first (length - 1) values where there's insufficient data
|
|
2366
2298
|
*/
|
|
2367
2299
|
export function mode(source, length) {
|
|
2368
2300
|
const result = [];
|
|
2301
|
+
const len = Math.floor(length);
|
|
2302
|
+
const seen = []; // non-na values so far
|
|
2369
2303
|
for (let i = 0; i < source.length; i++) {
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
// Collect non-NaN values in the window
|
|
2375
|
-
const values = [];
|
|
2376
|
-
for (let j = 0; j < length; j++) {
|
|
2377
|
-
const value = source[i - j];
|
|
2378
|
-
if (!isNaN(value)) {
|
|
2379
|
-
values.push(value);
|
|
2380
|
-
}
|
|
2381
|
-
}
|
|
2382
|
-
if (values.length === 0) {
|
|
2304
|
+
const v = source[i];
|
|
2305
|
+
if (!Number.isNaN(v))
|
|
2306
|
+
seen.push(v);
|
|
2307
|
+
if (seen.length < len) {
|
|
2383
2308
|
result.push(NaN);
|
|
2384
2309
|
continue;
|
|
2385
2310
|
}
|
|
2311
|
+
const values = seen.slice(seen.length - len);
|
|
2386
2312
|
// Count frequency of each value
|
|
2387
2313
|
const frequencyMap = new Map();
|
|
2388
2314
|
for (const value of values) {
|
|
@@ -2531,79 +2457,75 @@ export function percentile_nearest_rank(source, length, percentage) {
|
|
|
2531
2457
|
* - +100 indicates source consistently increased over the period
|
|
2532
2458
|
* - -100 indicates source consistently decreased over the period
|
|
2533
2459
|
* - 0 indicates no directional consistency
|
|
2534
|
-
* -
|
|
2460
|
+
* - As PineScript (bit for bit): the first value is on bar `length`; values less than 1e-10 above the smallest value
|
|
2461
|
+
* of a group (ascending sweep) are ties with their average rank; a window where all values are ties gives `na`;
|
|
2462
|
+
* moment-form variances of the bar positions and the ranks, `(100 * cov) / (sd_x * sd_y)`
|
|
2463
|
+
* - A bar with an `na` source keeps the previous result; the values are kept in a ring of `length + 1` slots by bar
|
|
2464
|
+
* index (0 at the start, not written on `na` bars), so a later window sees the value of `length + 1` bars earlier
|
|
2465
|
+
* where the `na` was
|
|
2535
2466
|
*/
|
|
2536
2467
|
export function rci(source, length) {
|
|
2468
|
+
const len = Math.floor(length);
|
|
2537
2469
|
const result = [];
|
|
2470
|
+
// PineScript keeps the values in a ring of `length + 1` slots indexed by the bar index, filled with 0 at the start
|
|
2471
|
+
// and written only on bars with a value: a bar with an na source keeps the previous result, and later windows see
|
|
2472
|
+
// the value of `length + 1` bars earlier in that slot
|
|
2473
|
+
const ring = new Array(len + 1).fill(0);
|
|
2538
2474
|
for (let i = 0; i < source.length; i++) {
|
|
2539
|
-
|
|
2540
|
-
|
|
2475
|
+
const v = source[i];
|
|
2476
|
+
if (Number.isNaN(v)) {
|
|
2477
|
+
result.push(i > 0 ? result[i - 1] : NaN);
|
|
2541
2478
|
continue;
|
|
2542
2479
|
}
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
values.push(source[i - length + 1 + j]);
|
|
2547
|
-
}
|
|
2548
|
-
// Check for NaN values
|
|
2549
|
-
if (values.some(v => isNaN(v))) {
|
|
2480
|
+
ring[i % (len + 1)] = v;
|
|
2481
|
+
// the first value is on bar `length` (one bar after the window is full)
|
|
2482
|
+
if (i < len) {
|
|
2550
2483
|
result.push(NaN);
|
|
2551
2484
|
continue;
|
|
2552
2485
|
}
|
|
2553
|
-
//
|
|
2554
|
-
const
|
|
2555
|
-
//
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
const ranks = new Array(
|
|
2559
|
-
let
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
}
|
|
2582
|
-
const
|
|
2583
|
-
const
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
tieCount++;
|
|
2591
|
-
}
|
|
2592
|
-
if (tieCount > 1) {
|
|
2593
|
-
tieCorrection += (tieCount * tieCount * tieCount - tieCount) / 12;
|
|
2594
|
-
}
|
|
2595
|
-
j += tieCount;
|
|
2596
|
-
}
|
|
2597
|
-
const A = base - tieCorrection; // Corrected for tied price ranks
|
|
2598
|
-
const B = base; // Time ranks have no ties
|
|
2599
|
-
let rho;
|
|
2600
|
-
if (A === 0 || B === 0) {
|
|
2601
|
-
rho = 0;
|
|
2602
|
-
}
|
|
2603
|
-
else {
|
|
2604
|
-
rho = (A + B - sumSquaredDiff) / (2 * Math.sqrt(A * B));
|
|
2486
|
+
// window newest first (position 0 = this bar)
|
|
2487
|
+
const values = Array.from({ length: len }, (_, j) => ring[(i - j) % (len + 1)]);
|
|
2488
|
+
// Ranks: ascending sweep; a group collects the values less than 1e-10 above its smallest value; each group gets
|
|
2489
|
+
// its average 0-based rank counted from the largest value
|
|
2490
|
+
const order = values.map((_, j) => j).sort((a, b) => values[a] - values[b]);
|
|
2491
|
+
const ranks = new Array(len);
|
|
2492
|
+
for (let g = 0; g < len;) {
|
|
2493
|
+
const anchor = values[order[g]];
|
|
2494
|
+
let end = g + 1;
|
|
2495
|
+
while (end < len && values[order[end]] - anchor < 1e-10)
|
|
2496
|
+
end++;
|
|
2497
|
+
const avgRank = len - 1 - (g + g + (end - g) - 1) / 2;
|
|
2498
|
+
for (let k = g; k < end; k++)
|
|
2499
|
+
ranks[order[k]] = avgRank;
|
|
2500
|
+
g = end;
|
|
2501
|
+
}
|
|
2502
|
+
// moment-form variances, covariance from deviation products, (100 * cov) / (sd_x * sd_y): this operation order
|
|
2503
|
+
// gives PineScript's values bit for bit
|
|
2504
|
+
let sumX = 0;
|
|
2505
|
+
let sumY = 0;
|
|
2506
|
+
let sumX2 = 0;
|
|
2507
|
+
let sumY2 = 0;
|
|
2508
|
+
for (let k = 0; k < len; k++) {
|
|
2509
|
+
const y = ranks[k];
|
|
2510
|
+
sumX += k;
|
|
2511
|
+
sumY += y;
|
|
2512
|
+
sumX2 += k * k;
|
|
2513
|
+
sumY2 += y * y;
|
|
2514
|
+
}
|
|
2515
|
+
const meanX = sumX / len;
|
|
2516
|
+
const meanY = sumY / len;
|
|
2517
|
+
const varX = sumX2 / len - meanX * meanX;
|
|
2518
|
+
const varY = sumY2 / len - meanY * meanY;
|
|
2519
|
+
// a window where all values are ties has no correlation
|
|
2520
|
+
if (!(varX > 0) || !(varY > 0)) {
|
|
2521
|
+
result.push(NaN);
|
|
2522
|
+
continue;
|
|
2605
2523
|
}
|
|
2606
|
-
|
|
2524
|
+
let cov = 0;
|
|
2525
|
+
for (let k = 0; k < len; k++)
|
|
2526
|
+
cov += (k - meanX) * (ranks[k] - meanY);
|
|
2527
|
+
cov /= len;
|
|
2528
|
+
result.push((100 * cov) / (Math.sqrt(varX) * Math.sqrt(varY)));
|
|
2607
2529
|
}
|
|
2608
2530
|
return result;
|
|
2609
2531
|
}
|
|
@@ -3148,14 +3070,19 @@ export function pvt(close, volume) {
|
|
|
3148
3070
|
}
|
|
3149
3071
|
/**
|
|
3150
3072
|
* Accumulation/Distribution index: the running sum of
|
|
3151
|
-
* `(
|
|
3073
|
+
* `((close - low) - (high - close)) / (high - low) * volume`, in this order as in PineScript (bit for bit).
|
|
3074
|
+
* A bar where this is `na` (`high == low`, an `na` volume) adds nothing and keeps the previous value.
|
|
3152
3075
|
*/
|
|
3153
3076
|
export function accdist(high, low, close, volume) {
|
|
3154
|
-
|
|
3077
|
+
let ad = 0;
|
|
3078
|
+
return close.map((c, i) => {
|
|
3155
3079
|
const h = high[i];
|
|
3156
3080
|
const l = low[i];
|
|
3157
|
-
|
|
3158
|
-
|
|
3081
|
+
const mfv = (((c - l) - (h - c)) / (h - l)) * volume[i];
|
|
3082
|
+
if (Number.isFinite(mfv))
|
|
3083
|
+
ad += mfv;
|
|
3084
|
+
return ad;
|
|
3085
|
+
});
|
|
3159
3086
|
}
|
|
3160
3087
|
/**
|
|
3161
3088
|
* Intraday Intensity Index: `(2 * close - high - low) / (high - low) * volume`.
|