oakscriptjs 0.9.5 → 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 +84 -27
- package/dist/cjs/array/index.d.ts.map +1 -1
- package/dist/cjs/array/index.js +235 -78
- package/dist/cjs/array/index.js.map +3 -3
- package/dist/cjs/callsite/index.d.ts +81 -2
- package/dist/cjs/callsite/index.d.ts.map +1 -1
- package/dist/cjs/callsite/index.js +62 -1
- 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 +76 -21
- package/dist/cjs/math/index.d.ts.map +1 -1
- package/dist/cjs/math/index.js +155 -41
- 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 +86 -43
- package/dist/cjs/ta/index.d.ts.map +1 -1
- package/dist/cjs/ta/index.js +173 -267
- package/dist/cjs/ta/index.js.map +3 -3
- package/dist/cjs/ta/running-sum.d.ts +54 -2
- package/dist/cjs/ta/running-sum.d.ts.map +1 -1
- package/dist/cjs/ta/running-sum.js +132 -52
- 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 +84 -27
- package/dist/esm/array/index.d.ts.map +1 -1
- package/dist/esm/array/index.js +373 -111
- package/dist/esm/array/index.js.map +1 -1
- package/dist/esm/callsite/index.d.ts +81 -2
- package/dist/esm/callsite/index.d.ts.map +1 -1
- package/dist/esm/callsite/index.js +145 -3
- 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 +76 -21
- package/dist/esm/math/index.d.ts.map +1 -1
- package/dist/esm/math/index.js +227 -61
- 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 +86 -43
- package/dist/esm/ta/index.d.ts.map +1 -1
- package/dist/esm/ta/index.js +296 -348
- package/dist/esm/ta/index.js.map +1 -1
- package/dist/esm/ta/running-sum.d.ts +54 -2
- package/dist/esm/ta/running-sum.d.ts.map +1 -1
- package/dist/esm/ta/running-sum.js +166 -71
- 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 +1 -1
- package/src/array/index.ts +352 -127
- package/src/callsite/index.ts +147 -3
- 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 +256 -74
- 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 +306 -386
- package/src/ta/running-sum.ts +187 -74
- 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
|
*
|
|
@@ -227,7 +222,7 @@ export function bb(series, length, mult) {
|
|
|
227
222
|
* Standard deviation over the last `length` non-na values.
|
|
228
223
|
*
|
|
229
224
|
* @param source - Series of values to process
|
|
230
|
-
* @param length - Number of values
|
|
225
|
+
* @param length - Number of values, or one length per bar (a series length, see {@link sma})
|
|
231
226
|
* @param biased - true (default): biased estimate (divides by `length`); false: unbiased (by `length - 1`)
|
|
232
227
|
* @returns Standard deviation series (na until `length` non-na values exist)
|
|
233
228
|
*
|
|
@@ -238,7 +233,7 @@ export function bb(series, length, mult) {
|
|
|
238
233
|
* compensated running sums of the values and of their squares, bit for bit as PineScript.
|
|
239
234
|
*/
|
|
240
235
|
export function stdev(source, length, biased = true) {
|
|
241
|
-
return
|
|
236
|
+
return varianceOf(source, length, biased, 'ta.stdev').map((v) => (Number.isNaN(v) ? NaN : v > 0 ? Math.sqrt(v) : 0));
|
|
242
237
|
}
|
|
243
238
|
/**
|
|
244
239
|
* The last `length` non-na values up to bar `i` (the bar itself may be na), or null when fewer exist.
|
|
@@ -605,7 +600,8 @@ export function supertrend(factor, atrPeriod, high, low, close, wicks = false) {
|
|
|
605
600
|
* @remarks
|
|
606
601
|
* - Moving average used in RSI calculation
|
|
607
602
|
* - Alpha = 1 / length (different from EMA which uses alpha = 2 / (length + 1))
|
|
608
|
-
* - 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
|
|
609
605
|
* - Formula: `RMA = (source + (length - 1) * RMA[1]) / length`, evaluated in this order as in PineScript
|
|
610
606
|
* (equal to `alpha * source + (1 - alpha) * RMA[1]`, but bit for bit)
|
|
611
607
|
* - `na` values (also +/-Infinity) in the source series are ignored
|
|
@@ -627,21 +623,16 @@ export function rma(source, length) {
|
|
|
627
623
|
// An infinite value is na, as in PineScript.
|
|
628
624
|
let firstValidIndex = -1;
|
|
629
625
|
let validCount = 0;
|
|
630
|
-
let initSum = 0;
|
|
631
626
|
for (let i = 0; i < source.length; i++) {
|
|
632
627
|
const val = source[i];
|
|
633
|
-
if (val !== undefined && Number.isFinite(val)) {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
if (validCount === len) {
|
|
637
|
-
firstValidIndex = i;
|
|
638
|
-
break;
|
|
639
|
-
}
|
|
628
|
+
if (val !== undefined && Number.isFinite(val) && ++validCount === len) {
|
|
629
|
+
firstValidIndex = i;
|
|
630
|
+
break;
|
|
640
631
|
}
|
|
641
632
|
}
|
|
642
|
-
// Initialize RMA value with SMA of first `length` non-NaN values
|
|
643
|
-
let rmaValue = validCount > 0 ? initSum / validCount : NaN;
|
|
644
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;
|
|
645
636
|
for (let i = 0; i < source.length; i++) {
|
|
646
637
|
if (!rmaInitialized || i < firstValidIndex) {
|
|
647
638
|
// Not enough data yet for RMA
|
|
@@ -667,6 +658,14 @@ export function rma(source, length) {
|
|
|
667
658
|
}
|
|
668
659
|
return result;
|
|
669
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
|
+
}
|
|
670
669
|
/**
|
|
671
670
|
* Weighted Moving Average (WMA) - moving average with linearly decreasing weights.
|
|
672
671
|
*
|
|
@@ -680,6 +679,8 @@ export function rma(source, length) {
|
|
|
680
679
|
* - Formula: `sum(source[i]! * (length - i)) / sum(length - i)` for i = 0 to length-1
|
|
681
680
|
* - As in PineScript: na on a bar whose source is `na` (also +/-Infinity) and until `length` non-`na` values were
|
|
682
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)
|
|
683
684
|
* - More responsive to recent price changes than SMA
|
|
684
685
|
*
|
|
685
686
|
* @example
|
|
@@ -714,7 +715,7 @@ export function wma(source, length) {
|
|
|
714
715
|
let weightSum = 0;
|
|
715
716
|
for (let j = len - 1; j >= 0; j--) {
|
|
716
717
|
const weight = len - j;
|
|
717
|
-
sum
|
|
718
|
+
sum = wmaAdd(sum, filled[i - j] * weight);
|
|
718
719
|
weightSum += weight;
|
|
719
720
|
}
|
|
720
721
|
result.push(sum / weightSum);
|
|
@@ -786,11 +787,12 @@ export function cum(source) {
|
|
|
786
787
|
* @returns Boolean series (true at cross points)
|
|
787
788
|
*
|
|
788
789
|
* @remarks
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
* -
|
|
793
|
-
*
|
|
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
|
|
794
796
|
* - Use `ta.crossover()` or `ta.crossunder()` for directional crosses
|
|
795
797
|
*
|
|
796
798
|
* @example
|
|
@@ -800,26 +802,39 @@ export function cum(source) {
|
|
|
800
802
|
* ```
|
|
801
803
|
*/
|
|
802
804
|
export function cross(source1, source2) {
|
|
803
|
-
|
|
805
|
+
const result = [];
|
|
806
|
+
let side = 0; // -1: last strictly below, 1: last strictly above, 0: none yet
|
|
807
|
+
for (let i = 0; i < source1.length; i++) {
|
|
808
|
+
const a = source1[i];
|
|
809
|
+
const b = source2[i];
|
|
810
|
+
if (Number.isNaN(a) || Number.isNaN(b)) {
|
|
811
|
+
result.push(false);
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
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;
|
|
819
|
+
}
|
|
820
|
+
return result;
|
|
804
821
|
}
|
|
805
822
|
/**
|
|
806
|
-
* ta.rising / ta.falling:
|
|
807
|
-
*
|
|
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`.
|
|
808
826
|
*
|
|
809
827
|
* @internal
|
|
810
828
|
*/
|
|
811
829
|
function monotonic(source, length, step) {
|
|
812
830
|
const result = [];
|
|
813
|
-
|
|
831
|
+
let count = 0;
|
|
814
832
|
for (let i = 0; i < source.length; i++) {
|
|
815
833
|
const v = source[i];
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
for (let j = 1; ok && j <= length; j++)
|
|
821
|
-
ok = step(values[n - j], values[n - j - 1]);
|
|
822
|
-
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);
|
|
823
838
|
}
|
|
824
839
|
return result;
|
|
825
840
|
}
|
|
@@ -832,10 +847,11 @@ function monotonic(source, length, step) {
|
|
|
832
847
|
*
|
|
833
848
|
* @remarks
|
|
834
849
|
* PineScript rules:
|
|
835
|
-
* -
|
|
836
|
-
*
|
|
837
|
-
*
|
|
838
|
-
*
|
|
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)
|
|
839
855
|
*
|
|
840
856
|
* @example
|
|
841
857
|
* ```typescript
|
|
@@ -855,10 +871,11 @@ export function rising(source, length) {
|
|
|
855
871
|
*
|
|
856
872
|
* @remarks
|
|
857
873
|
* PineScript rules:
|
|
858
|
-
* -
|
|
859
|
-
*
|
|
860
|
-
*
|
|
861
|
-
*
|
|
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)
|
|
862
879
|
*
|
|
863
880
|
* @example
|
|
864
881
|
* ```typescript
|
|
@@ -955,10 +972,11 @@ export function mom(source, length) {
|
|
|
955
972
|
*
|
|
956
973
|
* @remarks
|
|
957
974
|
* - Measures average absolute distance from the mean
|
|
958
|
-
* - Formula: `sum(abs(source[i]! - sma)) / length` for i in 0 to length-1
|
|
959
975
|
* - Less sensitive to outliers than standard deviation
|
|
960
|
-
* - `
|
|
961
|
-
*
|
|
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`
|
|
962
980
|
*
|
|
963
981
|
* @example
|
|
964
982
|
* ```typescript
|
|
@@ -967,24 +985,15 @@ export function mom(source, length) {
|
|
|
967
985
|
* ```
|
|
968
986
|
*/
|
|
969
987
|
export function dev(source, length) {
|
|
970
|
-
const
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
for (let j = 0; j < length; j++) {
|
|
980
|
-
if (!isNaN(source[i - j])) {
|
|
981
|
-
sum += Math.abs(source[i - j] - meanValues[i]);
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
result.push(sum / length);
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
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
|
+
});
|
|
988
997
|
}
|
|
989
998
|
/**
|
|
990
999
|
* Variance - expectation of squared deviation from mean.
|
|
@@ -1013,6 +1022,14 @@ export function dev(source, length) {
|
|
|
1013
1022
|
* ```
|
|
1014
1023
|
*/
|
|
1015
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
|
+
}
|
|
1016
1033
|
const len = Math.floor(length);
|
|
1017
1034
|
// PineScript: the last `len` non-na values (also +/-Infinity skipped), as ta.sma
|
|
1018
1035
|
if ((biased ? len : len - 1) <= 0)
|
|
@@ -1088,7 +1105,7 @@ export function median(source, length) {
|
|
|
1088
1105
|
* @remarks
|
|
1089
1106
|
* - Fixed length of 4 bars
|
|
1090
1107
|
* - Weights: [1/6, 2/6, 2/6, 1/6] (symmetric)
|
|
1091
|
-
* - Formula: `source
|
|
1108
|
+
* - Formula, in this order as in PineScript (bit for bit): `(source + 2 * source[1] + 2 * source[2] + source[3]) / 6`
|
|
1092
1109
|
* - More weight given to middle values
|
|
1093
1110
|
* - `na` values in the source series are included in calculations and will produce an `na` result
|
|
1094
1111
|
* - Returns NaN for the first 3 bars
|
|
@@ -1100,26 +1117,7 @@ export function median(source, length) {
|
|
|
1100
1117
|
* ```
|
|
1101
1118
|
*/
|
|
1102
1119
|
export function swma(source) {
|
|
1103
|
-
|
|
1104
|
-
for (let i = 0; i < source.length; i++) {
|
|
1105
|
-
if (i < 3) {
|
|
1106
|
-
result.push(NaN);
|
|
1107
|
-
}
|
|
1108
|
-
else {
|
|
1109
|
-
// Check for any NaN values in the window
|
|
1110
|
-
if (isNaN(source[i]) || isNaN(source[i - 1]) || isNaN(source[i - 2]) || isNaN(source[i - 3])) {
|
|
1111
|
-
result.push(NaN);
|
|
1112
|
-
}
|
|
1113
|
-
else {
|
|
1114
|
-
const value = source[i - 3] * (1 / 6) +
|
|
1115
|
-
source[i - 2] * (2 / 6) +
|
|
1116
|
-
source[i - 1] * (2 / 6) +
|
|
1117
|
-
source[i] * (1 / 6);
|
|
1118
|
-
result.push(value);
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
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);
|
|
1123
1121
|
}
|
|
1124
1122
|
/**
|
|
1125
1123
|
* Volume Weighted Moving Average (VWMA) - moving average weighted by volume.
|
|
@@ -1181,6 +1179,9 @@ export function vwma(source, length, volume) {
|
|
|
1181
1179
|
* @remarks
|
|
1182
1180
|
* - Calculates line of best fit using least squares method
|
|
1183
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`
|
|
1184
1185
|
* - offset=0 gives current fitted value, offset<0 gives future projection
|
|
1185
1186
|
* - `na` values in the source series are included in calculations and will produce an `na` result
|
|
1186
1187
|
* - Useful for trend detection and prediction
|
|
@@ -1212,13 +1213,14 @@ export function linreg(source, length, offset = 0) {
|
|
|
1212
1213
|
result.push(NaN);
|
|
1213
1214
|
}
|
|
1214
1215
|
else {
|
|
1215
|
-
//
|
|
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
|
|
1216
1218
|
let sumX = 0;
|
|
1217
1219
|
let sumY = 0;
|
|
1218
1220
|
let sumXY = 0;
|
|
1219
1221
|
let sumX2 = 0;
|
|
1220
1222
|
for (let j = 0; j < len; j++) {
|
|
1221
|
-
const x = j;
|
|
1223
|
+
const x = j + 1;
|
|
1222
1224
|
const y = source[i - (len - 1 - j)];
|
|
1223
1225
|
sumX += x;
|
|
1224
1226
|
sumY += y;
|
|
@@ -1226,10 +1228,8 @@ export function linreg(source, length, offset = 0) {
|
|
|
1226
1228
|
sumX2 += x * x;
|
|
1227
1229
|
}
|
|
1228
1230
|
const slope = (len * sumXY - sumX * sumY) / (len * sumX2 - sumX * sumX);
|
|
1229
|
-
const intercept =
|
|
1230
|
-
|
|
1231
|
-
const x = len - 1 - offset;
|
|
1232
|
-
result.push(intercept + slope * x);
|
|
1231
|
+
const intercept = sumY / len - (slope * sumX) / len + slope;
|
|
1232
|
+
result.push(intercept + slope * (len - 1 - offset));
|
|
1233
1233
|
}
|
|
1234
1234
|
}
|
|
1235
1235
|
}
|
|
@@ -1249,8 +1249,9 @@ export function linreg(source, length, offset = 0) {
|
|
|
1249
1249
|
* - -1 = perfect negative correlation
|
|
1250
1250
|
* - 0 = no correlation
|
|
1251
1251
|
* - Measures linear relationship between two series
|
|
1252
|
-
* - `
|
|
1253
|
-
*
|
|
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`
|
|
1254
1255
|
*
|
|
1255
1256
|
* @example
|
|
1256
1257
|
* ```typescript
|
|
@@ -1259,54 +1260,28 @@ export function linreg(source, length, offset = 0) {
|
|
|
1259
1260
|
* ```
|
|
1260
1261
|
*/
|
|
1261
1262
|
export function correlation(source1, source2, length) {
|
|
1262
|
-
|
|
1263
|
-
//
|
|
1264
|
-
const
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
continue;
|
|
1285
|
-
}
|
|
1286
|
-
// Calculate means
|
|
1287
|
-
let sum1 = 0;
|
|
1288
|
-
let sum2 = 0;
|
|
1289
|
-
for (let k = 0; k < m; k++) {
|
|
1290
|
-
sum1 += a[k];
|
|
1291
|
-
sum2 += b[k];
|
|
1292
|
-
}
|
|
1293
|
-
const mean1 = sum1 / m;
|
|
1294
|
-
const mean2 = sum2 / m;
|
|
1295
|
-
// Calculate correlation components
|
|
1296
|
-
let numerator = 0;
|
|
1297
|
-
let sum1Sq = 0;
|
|
1298
|
-
let sum2Sq = 0;
|
|
1299
|
-
for (let k = 0; k < m; k++) {
|
|
1300
|
-
const dev1 = a[k] - mean1;
|
|
1301
|
-
const dev2 = b[k] - mean2;
|
|
1302
|
-
numerator += dev1 * dev2;
|
|
1303
|
-
sum1Sq += dev1 * dev1;
|
|
1304
|
-
sum2Sq += dev2 * dev2;
|
|
1305
|
-
}
|
|
1306
|
-
const denominator = Math.sqrt(sum1Sq * sum2Sq);
|
|
1307
|
-
result.push(denominator === 0 ? NaN : numerator / denominator);
|
|
1308
|
-
}
|
|
1309
|
-
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
|
+
});
|
|
1310
1285
|
}
|
|
1311
1286
|
/**
|
|
1312
1287
|
* Percent Rank - percentage of how many previous values were less than or equal to current value.
|
|
@@ -1365,10 +1340,14 @@ export function percentrank(source, length) {
|
|
|
1365
1340
|
* - Typical Price = (High + Low + Close) / 3
|
|
1366
1341
|
* - Mean Deviation = Average of absolute differences from mean
|
|
1367
1342
|
* - Scaled by 0.015 to provide more readable numbers
|
|
1368
|
-
* - As in PineScript: when the mean deviation is 0 (1e-10 tolerance, a flat window), the CCI of the previous bar
|
|
1369
1343
|
* - Values above +100 indicate overbought conditions
|
|
1370
1344
|
* - Values below -100 indicate oversold conditions
|
|
1371
|
-
*
|
|
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)
|
|
1372
1351
|
*
|
|
1373
1352
|
* @example
|
|
1374
1353
|
* \`\`\`typescript
|
|
@@ -1378,22 +1357,17 @@ export function percentrank(source, length) {
|
|
|
1378
1357
|
* \`\`\`
|
|
1379
1358
|
*/
|
|
1380
1359
|
export function cci(source, length) {
|
|
1381
|
-
const result = [];
|
|
1382
1360
|
const smaValues = sma(source, length);
|
|
1383
1361
|
const devValues = dev(source, length);
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
result.push(cci);
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
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
|
+
});
|
|
1397
1371
|
}
|
|
1398
1372
|
/**
|
|
1399
1373
|
* Stochastic Oscillator - momentum indicator comparing closing price to price range.
|
|
@@ -1409,8 +1383,8 @@ export function cci(source, length) {
|
|
|
1409
1383
|
* - Measures where close is relative to the high-low range
|
|
1410
1384
|
* - Values above 80 typically indicate overbought
|
|
1411
1385
|
* - Values below 20 typically indicate oversold
|
|
1412
|
-
* -
|
|
1413
|
-
*
|
|
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)
|
|
1414
1388
|
*
|
|
1415
1389
|
* @example
|
|
1416
1390
|
* \`\`\`typescript
|
|
@@ -1423,20 +1397,15 @@ export function stoch(source, high, low, length) {
|
|
|
1423
1397
|
const result = [];
|
|
1424
1398
|
const lowestValues = lowest(low, length);
|
|
1425
1399
|
const highestValues = highest(high, length);
|
|
1400
|
+
// PineScript: an na input or a zero range keeps the previous result
|
|
1401
|
+
let previous = NaN;
|
|
1426
1402
|
for (let i = 0; i < source.length; i++) {
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
result.push(NaN);
|
|
1434
|
-
}
|
|
1435
|
-
else {
|
|
1436
|
-
const stochValue = 100 * (source[i] - lowestValues[i]) / range;
|
|
1437
|
-
result.push(stochValue);
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
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);
|
|
1440
1409
|
}
|
|
1441
1410
|
return result;
|
|
1442
1411
|
}
|
|
@@ -1455,7 +1424,13 @@ export function stoch(source, high, low, length) {
|
|
|
1455
1424
|
* - Formula: 100 - (100 / (1 + Positive Money Flow / Negative Money Flow))
|
|
1456
1425
|
* - Values above 80 indicate overbought
|
|
1457
1426
|
* - Values below 20 indicate oversold
|
|
1458
|
-
*
|
|
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
|
|
1459
1434
|
*
|
|
1460
1435
|
* @example
|
|
1461
1436
|
* ```typescript
|
|
@@ -1488,13 +1463,18 @@ export function mfi(source, length, volume) {
|
|
|
1488
1463
|
// math.sum: the compensated running sum of the last `length` non-na values (as PineScript, bit for bit)
|
|
1489
1464
|
const upper = runningSum(upperFlow, length);
|
|
1490
1465
|
const lower = runningSum(lowerFlow, length);
|
|
1491
|
-
|
|
1492
|
-
|
|
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]);
|
|
1493
1471
|
if (Number.isNaN(u) || Number.isNaN(l))
|
|
1494
1472
|
return NaN;
|
|
1495
1473
|
if (l === 0)
|
|
1496
1474
|
return 100;
|
|
1497
|
-
|
|
1475
|
+
const ratio = 1 + u / l;
|
|
1476
|
+
// x / 0 is na in PineScript
|
|
1477
|
+
return ratio === 0 ? NaN : 100 - 100 / ratio;
|
|
1498
1478
|
});
|
|
1499
1479
|
}
|
|
1500
1480
|
/**
|
|
@@ -1862,10 +1842,12 @@ export function dmi(diLength, adxSmoothing, high, low, close) {
|
|
|
1862
1842
|
* @param source - Series of values to process
|
|
1863
1843
|
* @param shortLength - Short smoothing length
|
|
1864
1844
|
* @param longLength - Long smoothing length
|
|
1865
|
-
* @returns TSI series
|
|
1845
|
+
* @returns TSI series, a value in [-1, 1]
|
|
1866
1846
|
*
|
|
1867
1847
|
* @remarks
|
|
1868
|
-
* -
|
|
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
|
|
1869
1851
|
* - Positive values indicate bullish momentum
|
|
1870
1852
|
* - Negative values indicate bearish momentum
|
|
1871
1853
|
* - Crossovers of zero line can signal trend changes
|
|
@@ -1894,17 +1876,11 @@ export function tsi(source, shortLength, longLength) {
|
|
|
1894
1876
|
// Double smooth absolute momentum
|
|
1895
1877
|
const absMomentum = momentum.map(Math.abs);
|
|
1896
1878
|
const smoothedAbsMomentum = ema(ema(absMomentum, longLength), shortLength);
|
|
1897
|
-
//
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
}
|
|
1903
|
-
else {
|
|
1904
|
-
result.push((smoothedMomentum[i] / smoothedAbsMomentum[i]) * 100);
|
|
1905
|
-
}
|
|
1906
|
-
}
|
|
1907
|
-
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
|
+
});
|
|
1908
1884
|
}
|
|
1909
1885
|
/**
|
|
1910
1886
|
* Chande Momentum Oscillator - momentum indicator similar to RSI.
|
|
@@ -2053,6 +2029,10 @@ export function bbw(source, length, mult) {
|
|
|
2053
2029
|
* @param length - Lookback period (default: 14)
|
|
2054
2030
|
* @returns Williams %R series
|
|
2055
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
|
+
*
|
|
2056
2036
|
* @example
|
|
2057
2037
|
* ```typescript
|
|
2058
2038
|
* const wpr = ta.wpr(high, low, close, 14);
|
|
@@ -2061,32 +2041,14 @@ export function bbw(source, length, mult) {
|
|
|
2061
2041
|
* ```
|
|
2062
2042
|
*/
|
|
2063
2043
|
export function wpr(high, low, close, length = 14) {
|
|
2064
|
-
const
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
let lowestLow = low[i - length + 1];
|
|
2073
|
-
for (let j = i - length + 2; j <= i; j++) {
|
|
2074
|
-
if (high[j] > highestHigh)
|
|
2075
|
-
highestHigh = high[j];
|
|
2076
|
-
if (low[j] < lowestLow)
|
|
2077
|
-
lowestLow = low[j];
|
|
2078
|
-
}
|
|
2079
|
-
const range = highestHigh - lowestLow;
|
|
2080
|
-
if (range === 0) {
|
|
2081
|
-
result.push(NaN);
|
|
2082
|
-
}
|
|
2083
|
-
else {
|
|
2084
|
-
// Formula: (Highest High - Close) / (Highest High - Lowest Low) * -100
|
|
2085
|
-
const wprValue = ((highestHigh - close[i]) / range) * -100;
|
|
2086
|
-
result.push(wprValue);
|
|
2087
|
-
}
|
|
2088
|
-
}
|
|
2089
|
-
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
|
+
});
|
|
2090
2052
|
}
|
|
2091
2053
|
export function vwap(source, volume, anchor, stdev_mult) {
|
|
2092
2054
|
if (source.length !== volume.length) {
|
|
@@ -2095,18 +2057,20 @@ export function vwap(source, volume, anchor, stdev_mult) {
|
|
|
2095
2057
|
const mid = [];
|
|
2096
2058
|
const upper = [];
|
|
2097
2059
|
const lower = [];
|
|
2098
|
-
|
|
2099
|
-
let
|
|
2100
|
-
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;
|
|
2101
2064
|
for (let i = 0; i < source.length; i++) {
|
|
2102
|
-
if (anchor && anchor[i]) {
|
|
2103
|
-
sumPV = 0;
|
|
2104
|
-
sumV = 0;
|
|
2105
|
-
sumPPV = 0;
|
|
2106
|
-
}
|
|
2107
2065
|
const s = source[i];
|
|
2108
2066
|
const v = volume[i];
|
|
2109
|
-
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
|
|
2110
2074
|
sumPV += s * v;
|
|
2111
2075
|
sumV += v;
|
|
2112
2076
|
sumPPV += s * s * v;
|
|
@@ -2173,7 +2137,8 @@ export function alma(source, length = 9, offset = 0.85, sigma = 6, floor = false
|
|
|
2173
2137
|
/**
|
|
2174
2138
|
* Keltner Channels Width (KCW)
|
|
2175
2139
|
*
|
|
2176
|
-
* 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).
|
|
2177
2142
|
* Similar to BBW but uses ATR instead of standard deviation.
|
|
2178
2143
|
*
|
|
2179
2144
|
* Low KCW suggests consolidation/low volatility.
|
|
@@ -2203,8 +2168,8 @@ export function kcw(source, length = 20, mult = 2, useTrueRange = true, high, lo
|
|
|
2203
2168
|
result.push(NaN);
|
|
2204
2169
|
}
|
|
2205
2170
|
else {
|
|
2206
|
-
|
|
2207
|
-
result.push(
|
|
2171
|
+
// PineScript: (upper - lower) / basis, a ratio (not a percentage)
|
|
2172
|
+
result.push((upper[i] - lower[i]) / basis[i]);
|
|
2208
2173
|
}
|
|
2209
2174
|
}
|
|
2210
2175
|
return result;
|
|
@@ -2281,46 +2246,32 @@ export function min(source) {
|
|
|
2281
2246
|
* The Center of Gravity indicator is an oscillator developed by John Ehlers.
|
|
2282
2247
|
* It identifies turning points with minimal lag and provides clear signals.
|
|
2283
2248
|
*
|
|
2284
|
-
* The COG calculates a weighted average where more recent prices have higher weights,
|
|
2285
|
-
* similar to a moving average but with a focus on momentum shifts.
|
|
2286
|
-
*
|
|
2287
2249
|
* @param source - Source series (typically close)
|
|
2288
2250
|
* @param length - Lookback period (default: 10)
|
|
2289
2251
|
* @returns COG series
|
|
2290
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
|
+
*
|
|
2291
2260
|
* @example
|
|
2292
2261
|
* ```typescript
|
|
2293
2262
|
* const cogValue = ta.cog(close, 10);
|
|
2294
|
-
* // Use COG crossovers as signals:
|
|
2295
|
-
* // - COG crossing above 0: potential buy signal
|
|
2296
|
-
* // - COG crossing below 0: potential sell signal
|
|
2297
2263
|
* ```
|
|
2298
2264
|
*/
|
|
2299
2265
|
export function cog(source, length = 10) {
|
|
2300
|
-
const
|
|
2301
|
-
|
|
2302
|
-
if (i <
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
const weight = j + 1;
|
|
2310
|
-
const price = source[i - length + 1 + j];
|
|
2311
|
-
numerator += weight * price;
|
|
2312
|
-
denominator += price;
|
|
2313
|
-
}
|
|
2314
|
-
if (denominator === 0) {
|
|
2315
|
-
result.push(NaN);
|
|
2316
|
-
}
|
|
2317
|
-
else {
|
|
2318
|
-
// COG formula: -1 * (sum of (weight * price) / sum of prices) + (length + 1) / 2
|
|
2319
|
-
const cog = -1 * (numerator / denominator) + (length + 1) / 2;
|
|
2320
|
-
result.push(cog);
|
|
2321
|
-
}
|
|
2322
|
-
}
|
|
2323
|
-
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
|
+
});
|
|
2324
2275
|
}
|
|
2325
2276
|
/**
|
|
2326
2277
|
* Mode (Most Frequent Value)
|
|
@@ -2339,29 +2290,25 @@ export function cog(source, length = 10) {
|
|
|
2339
2290
|
* ```
|
|
2340
2291
|
*
|
|
2341
2292
|
* @remarks
|
|
2342
|
-
*
|
|
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
|
|
2343
2297
|
* - If no mode exists, returns the smallest value
|
|
2344
|
-
* - Returns NaN for the first (length - 1) values where there's insufficient data
|
|
2345
2298
|
*/
|
|
2346
2299
|
export function mode(source, length) {
|
|
2347
2300
|
const result = [];
|
|
2301
|
+
const len = Math.floor(length);
|
|
2302
|
+
const seen = []; // non-na values so far
|
|
2348
2303
|
for (let i = 0; i < source.length; i++) {
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
// Collect non-NaN values in the window
|
|
2354
|
-
const values = [];
|
|
2355
|
-
for (let j = 0; j < length; j++) {
|
|
2356
|
-
const value = source[i - j];
|
|
2357
|
-
if (!isNaN(value)) {
|
|
2358
|
-
values.push(value);
|
|
2359
|
-
}
|
|
2360
|
-
}
|
|
2361
|
-
if (values.length === 0) {
|
|
2304
|
+
const v = source[i];
|
|
2305
|
+
if (!Number.isNaN(v))
|
|
2306
|
+
seen.push(v);
|
|
2307
|
+
if (seen.length < len) {
|
|
2362
2308
|
result.push(NaN);
|
|
2363
2309
|
continue;
|
|
2364
2310
|
}
|
|
2311
|
+
const values = seen.slice(seen.length - len);
|
|
2365
2312
|
// Count frequency of each value
|
|
2366
2313
|
const frequencyMap = new Map();
|
|
2367
2314
|
for (const value of values) {
|
|
@@ -2510,79 +2457,75 @@ export function percentile_nearest_rank(source, length, percentage) {
|
|
|
2510
2457
|
* - +100 indicates source consistently increased over the period
|
|
2511
2458
|
* - -100 indicates source consistently decreased over the period
|
|
2512
2459
|
* - 0 indicates no directional consistency
|
|
2513
|
-
* -
|
|
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
|
|
2514
2466
|
*/
|
|
2515
2467
|
export function rci(source, length) {
|
|
2468
|
+
const len = Math.floor(length);
|
|
2516
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);
|
|
2517
2474
|
for (let i = 0; i < source.length; i++) {
|
|
2518
|
-
|
|
2519
|
-
|
|
2475
|
+
const v = source[i];
|
|
2476
|
+
if (Number.isNaN(v)) {
|
|
2477
|
+
result.push(i > 0 ? result[i - 1] : NaN);
|
|
2520
2478
|
continue;
|
|
2521
2479
|
}
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
values.push(source[i - length + 1 + j]);
|
|
2526
|
-
}
|
|
2527
|
-
// Check for NaN values
|
|
2528
|
-
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) {
|
|
2529
2483
|
result.push(NaN);
|
|
2530
2484
|
continue;
|
|
2531
2485
|
}
|
|
2532
|
-
//
|
|
2533
|
-
const
|
|
2534
|
-
//
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
const ranks = new Array(
|
|
2538
|
-
let
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
}
|
|
2561
|
-
const
|
|
2562
|
-
const
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
tieCount++;
|
|
2570
|
-
}
|
|
2571
|
-
if (tieCount > 1) {
|
|
2572
|
-
tieCorrection += (tieCount * tieCount * tieCount - tieCount) / 12;
|
|
2573
|
-
}
|
|
2574
|
-
j += tieCount;
|
|
2575
|
-
}
|
|
2576
|
-
const A = base - tieCorrection; // Corrected for tied price ranks
|
|
2577
|
-
const B = base; // Time ranks have no ties
|
|
2578
|
-
let rho;
|
|
2579
|
-
if (A === 0 || B === 0) {
|
|
2580
|
-
rho = 0;
|
|
2581
|
-
}
|
|
2582
|
-
else {
|
|
2583
|
-
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;
|
|
2584
2523
|
}
|
|
2585
|
-
|
|
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)));
|
|
2586
2529
|
}
|
|
2587
2530
|
return result;
|
|
2588
2531
|
}
|
|
@@ -3127,14 +3070,19 @@ export function pvt(close, volume) {
|
|
|
3127
3070
|
}
|
|
3128
3071
|
/**
|
|
3129
3072
|
* Accumulation/Distribution index: the running sum of
|
|
3130
|
-
* `(
|
|
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.
|
|
3131
3075
|
*/
|
|
3132
3076
|
export function accdist(high, low, close, volume) {
|
|
3133
|
-
|
|
3077
|
+
let ad = 0;
|
|
3078
|
+
return close.map((c, i) => {
|
|
3134
3079
|
const h = high[i];
|
|
3135
3080
|
const l = low[i];
|
|
3136
|
-
|
|
3137
|
-
|
|
3081
|
+
const mfv = (((c - l) - (h - c)) / (h - l)) * volume[i];
|
|
3082
|
+
if (Number.isFinite(mfv))
|
|
3083
|
+
ad += mfv;
|
|
3084
|
+
return ad;
|
|
3085
|
+
});
|
|
3138
3086
|
}
|
|
3139
3087
|
/**
|
|
3140
3088
|
* Intraday Intensity Index: `(2 * close - high - low) / (high - low) * volume`.
|