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/src/ta/index.ts
CHANGED
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
|
|
11
11
|
import type { series_float, series_bool, series_int, int, Source, simple_int, simple_float, simple_bool } from '../types/index.js';
|
|
12
12
|
import { eq, ge, gt, le, lt } from '../compare/index.js';
|
|
13
|
-
import { runningSum, runningVariance } from './running-sum.js';
|
|
13
|
+
import { runningSum, runningSumSeries, runningVariance, runningVarianceSeries } from './running-sum.js';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Simple Moving Average - returns the moving average (sum of last y values divided by y).
|
|
17
17
|
*
|
|
18
18
|
* @param source - Series of values to process
|
|
19
|
-
* @param length - Number of bars (length)
|
|
19
|
+
* @param length - Number of bars (length), or one length per bar (a series length)
|
|
20
20
|
* @returns Simple moving average of source for length bars back
|
|
21
21
|
*
|
|
22
22
|
* @remarks
|
|
@@ -25,30 +25,26 @@ import { runningSum, runningVariance } from './running-sum.js';
|
|
|
25
25
|
* - Returns NaN until `length` non-`na` values are available
|
|
26
26
|
* - The sum is a compensated running sum, as in PineScript, so the result can differ from the exact window mean in
|
|
27
27
|
* the last bits (bit for bit with PineScript)
|
|
28
|
+
* - With a series length, the running sum is moved to the new length when the length changes, as PineScript does
|
|
29
|
+
* (see {@link math.sum}); a bar with an `na` source gives the mean of the last `length` earlier values
|
|
28
30
|
*
|
|
29
31
|
* @example
|
|
30
32
|
* ```typescript
|
|
31
33
|
* const closePrices = [10, 11, 12, 13, 14];
|
|
32
34
|
* const sma5 = ta.sma(closePrices, 5); // Returns: [NaN, NaN, NaN, NaN, 12]
|
|
35
|
+
* const smaVar = ta.sma(closePrices, [2, 2, 2, 3, 2]); // Returns: [NaN, 10.5, 11.5, 12, 13.5]
|
|
33
36
|
* ```
|
|
34
37
|
*/
|
|
35
|
-
export function sma(source: Source, length: simple_int): series_float {
|
|
38
|
+
export function sma(source: Source, length: simple_int | ArrayLike<number>): series_float {
|
|
39
|
+
if (typeof length !== 'number') {
|
|
40
|
+
const sums = runningSumSeries(source, length, 'ta.sma');
|
|
41
|
+
return sums.map((sum, i) => sum / Math.floor(length[i]!));
|
|
42
|
+
}
|
|
36
43
|
// Floor the length to match PineScript's auto-truncation of float to int
|
|
37
44
|
const len = Math.floor(length);
|
|
38
45
|
return runningSum(source, len).map((sum) => sum / len);
|
|
39
46
|
}
|
|
40
47
|
|
|
41
|
-
/** Mean of the last `length` bars; na when the window holds an na value (used by ta.dev). */
|
|
42
|
-
function strictWindowMean(source: Source, length: simple_int): series_float {
|
|
43
|
-
const len = Math.floor(length);
|
|
44
|
-
return Array.from({ length: source.length }, (_, i) => {
|
|
45
|
-
if (i < len - 1) return NaN;
|
|
46
|
-
let sum = 0;
|
|
47
|
-
for (let j = 0; j < len; j++) sum += source[i - j]!;
|
|
48
|
-
return sum / len;
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
48
|
/**
|
|
53
49
|
* Exponential Moving Average - returns the exponentially weighted moving average.
|
|
54
50
|
*
|
|
@@ -256,7 +252,7 @@ export function bb(
|
|
|
256
252
|
* Standard deviation over the last `length` non-na values.
|
|
257
253
|
*
|
|
258
254
|
* @param source - Series of values to process
|
|
259
|
-
* @param length - Number of values
|
|
255
|
+
* @param length - Number of values, or one length per bar (a series length, see {@link sma})
|
|
260
256
|
* @param biased - true (default): biased estimate (divides by `length`); false: unbiased (by `length - 1`)
|
|
261
257
|
* @returns Standard deviation series (na until `length` non-na values exist)
|
|
262
258
|
*
|
|
@@ -266,8 +262,12 @@ export function bb(
|
|
|
266
262
|
* The result is the square root of {@link variance} (0 when the variance is not positive), computed from
|
|
267
263
|
* compensated running sums of the values and of their squares, bit for bit as PineScript.
|
|
268
264
|
*/
|
|
269
|
-
export function stdev(
|
|
270
|
-
|
|
265
|
+
export function stdev(
|
|
266
|
+
source: Source,
|
|
267
|
+
length: simple_int | ArrayLike<number>,
|
|
268
|
+
biased: simple_bool = true
|
|
269
|
+
): series_float {
|
|
270
|
+
return varianceOf(source, length, biased, 'ta.stdev').map((v) => (Number.isNaN(v) ? NaN : v > 0 ? Math.sqrt(v) : 0));
|
|
271
271
|
}
|
|
272
272
|
|
|
273
273
|
/**
|
|
@@ -677,7 +677,8 @@ export function supertrend(
|
|
|
677
677
|
* @remarks
|
|
678
678
|
* - Moving average used in RSI calculation
|
|
679
679
|
* - Alpha = 1 / length (different from EMA which uses alpha = 2 / (length + 1))
|
|
680
|
-
* - First value is initialized with SMA, then uses exponential smoothing
|
|
680
|
+
* - First value is initialized with SMA, then uses exponential smoothing. As in PineScript, the seed is the
|
|
681
|
+
* `ta.sma` value (its compensated running sum), so it can differ from the exact mean in the last bits
|
|
681
682
|
* - Formula: `RMA = (source + (length - 1) * RMA[1]) / length`, evaluated in this order as in PineScript
|
|
682
683
|
* (equal to `alpha * source + (1 - alpha) * RMA[1]`, but bit for bit)
|
|
683
684
|
* - `na` values (also +/-Infinity) in the source series are ignored
|
|
@@ -700,23 +701,18 @@ export function rma(source: Source, length: simple_int): series_float {
|
|
|
700
701
|
// An infinite value is na, as in PineScript.
|
|
701
702
|
let firstValidIndex = -1;
|
|
702
703
|
let validCount = 0;
|
|
703
|
-
let initSum = 0;
|
|
704
704
|
|
|
705
705
|
for (let i = 0; i < source.length; i++) {
|
|
706
706
|
const val = source[i];
|
|
707
|
-
if (val !== undefined && Number.isFinite(val)) {
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
if (validCount === len) {
|
|
711
|
-
firstValidIndex = i;
|
|
712
|
-
break;
|
|
713
|
-
}
|
|
707
|
+
if (val !== undefined && Number.isFinite(val) && ++validCount === len) {
|
|
708
|
+
firstValidIndex = i;
|
|
709
|
+
break;
|
|
714
710
|
}
|
|
715
711
|
}
|
|
716
712
|
|
|
717
|
-
// Initialize RMA value with SMA of first `length` non-NaN values
|
|
718
|
-
let rmaValue = validCount > 0 ? initSum / validCount : NaN;
|
|
719
713
|
const rmaInitialized = firstValidIndex >= 0;
|
|
714
|
+
// The seed is ta.sma on that bar: the running sum of math.sum (PineScript bits), not a plain loop sum
|
|
715
|
+
let rmaValue = rmaInitialized ? runningSum(source.slice(0, firstValidIndex + 1), len)[firstValidIndex]! / len : NaN;
|
|
720
716
|
|
|
721
717
|
for (let i = 0; i < source.length; i++) {
|
|
722
718
|
if (!rmaInitialized || i < firstValidIndex) {
|
|
@@ -742,6 +738,15 @@ export function rma(source: Source, length: simple_int): series_float {
|
|
|
742
738
|
return result;
|
|
743
739
|
}
|
|
744
740
|
|
|
741
|
+
/**
|
|
742
|
+
* One step of the PineScript ta.wma sum: `a + b`, or 0 when `|a + b| <= 1e-10 * max(1, |a| + |b|)`.
|
|
743
|
+
* Measured bit for bit: a sum that cancels to a tiny residue (relative to the operands, or below 1e-10) is 0.
|
|
744
|
+
*/
|
|
745
|
+
function wmaAdd(a: number, b: number): number {
|
|
746
|
+
const sum = a + b;
|
|
747
|
+
return Math.abs(sum) <= 1e-10 * Math.max(1, Math.abs(a) + Math.abs(b)) ? 0 : sum;
|
|
748
|
+
}
|
|
749
|
+
|
|
745
750
|
/**
|
|
746
751
|
* Weighted Moving Average (WMA) - moving average with linearly decreasing weights.
|
|
747
752
|
*
|
|
@@ -755,6 +760,8 @@ export function rma(source: Source, length: simple_int): series_float {
|
|
|
755
760
|
* - Formula: `sum(source[i]! * (length - i)) / sum(length - i)` for i = 0 to length-1
|
|
756
761
|
* - As in PineScript: na on a bar whose source is `na` (also +/-Infinity) and until `length` non-`na` values were
|
|
757
762
|
* received; an `na` value inside the window is replaced by the previous non-`na` value
|
|
763
|
+
* - Summed from the oldest value to the newest; as PineScript, a partial sum `s + t` with
|
|
764
|
+
* `|s + t| <= 1e-10 * max(1, |s| + |t|)` is 0 (a cancellation residue, or values below 1e-10)
|
|
758
765
|
* - More responsive to recent price changes than SMA
|
|
759
766
|
*
|
|
760
767
|
* @example
|
|
@@ -789,7 +796,7 @@ export function wma(source: Source, length: simple_int): series_float {
|
|
|
789
796
|
let weightSum = 0;
|
|
790
797
|
for (let j = len - 1; j >= 0; j--) {
|
|
791
798
|
const weight = len - j;
|
|
792
|
-
sum
|
|
799
|
+
sum = wmaAdd(sum, filled[i - j]! * weight);
|
|
793
800
|
weightSum += weight;
|
|
794
801
|
}
|
|
795
802
|
result.push(sum / weightSum);
|
|
@@ -864,11 +871,12 @@ export function cum(source: Source): series_float {
|
|
|
864
871
|
* @returns Boolean series (true at cross points)
|
|
865
872
|
*
|
|
866
873
|
* @remarks
|
|
867
|
-
*
|
|
868
|
-
*
|
|
869
|
-
*
|
|
870
|
-
* -
|
|
871
|
-
*
|
|
874
|
+
* PineScript rule (measured bit for bit; not `crossover or crossunder`):
|
|
875
|
+
* - The call keeps the side of the last bar where `source1` was strictly above or strictly below `source2` (no
|
|
876
|
+
* side before such a bar). Equal bars and bars with an `na` value do not change it.
|
|
877
|
+
* - True when the values are strictly on the other side of the kept side: so a run of equal values is passed, and
|
|
878
|
+
* a series that starts equal and then moves away is not a cross
|
|
879
|
+
* - False on a bar with an `na` value
|
|
872
880
|
* - Use `ta.crossover()` or `ta.crossunder()` for directional crosses
|
|
873
881
|
*
|
|
874
882
|
* @example
|
|
@@ -878,25 +886,37 @@ export function cum(source: Source): series_float {
|
|
|
878
886
|
* ```
|
|
879
887
|
*/
|
|
880
888
|
export function cross(source1: Source, source2: Source): series_bool {
|
|
881
|
-
|
|
889
|
+
const result: series_bool = [];
|
|
890
|
+
let side = 0; // -1: last strictly below, 1: last strictly above, 0: none yet
|
|
891
|
+
for (let i = 0; i < source1.length; i++) {
|
|
892
|
+
const a = source1[i]!;
|
|
893
|
+
const b = source2[i]!;
|
|
894
|
+
if (Number.isNaN(a) || Number.isNaN(b)) {
|
|
895
|
+
result.push(false);
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
result.push((side < 0 && a > b) || (side > 0 && a < b));
|
|
899
|
+
if (a < b) side = -1;
|
|
900
|
+
else if (a > b) side = 1;
|
|
901
|
+
}
|
|
902
|
+
return result;
|
|
882
903
|
}
|
|
883
904
|
|
|
884
905
|
/**
|
|
885
|
-
* ta.rising / ta.falling:
|
|
886
|
-
*
|
|
906
|
+
* ta.rising / ta.falling: a counter of the steps from one bar to the next that pass `step(newer, older)`, reset to 0
|
|
907
|
+
* by a step that does not pass; a step with an na value (on either bar) leaves the counter unchanged. True when the
|
|
908
|
+
* counter is at least `length`.
|
|
887
909
|
*
|
|
888
910
|
* @internal
|
|
889
911
|
*/
|
|
890
912
|
function monotonic(source: Source, length: simple_int, step: (newer: number, older: number) => boolean): series_bool {
|
|
891
913
|
const result: series_bool = [];
|
|
892
|
-
|
|
914
|
+
let count = 0;
|
|
893
915
|
for (let i = 0; i < source.length; i++) {
|
|
894
916
|
const v = source[i]!;
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
for (let j = 1; ok && j <= length; j++) ok = step(values[n - j]!, values[n - j - 1]!);
|
|
899
|
-
result.push(ok);
|
|
917
|
+
const prev = i > 0 ? source[i - 1]! : NaN;
|
|
918
|
+
if (!Number.isNaN(v) && !Number.isNaN(prev)) count = step(v, prev) ? count + 1 : 0;
|
|
919
|
+
result.push(count >= length);
|
|
900
920
|
}
|
|
901
921
|
return result;
|
|
902
922
|
}
|
|
@@ -910,10 +930,11 @@ function monotonic(source: Source, length: simple_int, step: (newer: number, old
|
|
|
910
930
|
*
|
|
911
931
|
* @remarks
|
|
912
932
|
* PineScript rules:
|
|
913
|
-
* -
|
|
914
|
-
*
|
|
915
|
-
*
|
|
916
|
-
*
|
|
933
|
+
* - a counter of rises from the previous bar (a rise is larger than the 1e-10 tolerance); a step that is not a rise
|
|
934
|
+
* resets it to 0
|
|
935
|
+
* - a step from or to an `na` bar leaves the counter unchanged (so an `na` bar, and the bar after it, keep the
|
|
936
|
+
* result of the bar before the `na`)
|
|
937
|
+
* - true when the counter is at least `length` (false at the start)
|
|
917
938
|
*
|
|
918
939
|
* @example
|
|
919
940
|
* ```typescript
|
|
@@ -934,10 +955,11 @@ export function rising(source: Source, length: simple_int): series_bool {
|
|
|
934
955
|
*
|
|
935
956
|
* @remarks
|
|
936
957
|
* PineScript rules:
|
|
937
|
-
* -
|
|
938
|
-
*
|
|
939
|
-
*
|
|
940
|
-
*
|
|
958
|
+
* - a counter of falls from the previous bar (a fall is larger than the 1e-10 tolerance); a step that is not a fall
|
|
959
|
+
* resets it to 0
|
|
960
|
+
* - a step from or to an `na` bar leaves the counter unchanged (so an `na` bar, and the bar after it, keep the
|
|
961
|
+
* result of the bar before the `na`)
|
|
962
|
+
* - true when the counter is at least `length` (false at the start)
|
|
941
963
|
*
|
|
942
964
|
* @example
|
|
943
965
|
* ```typescript
|
|
@@ -1037,10 +1059,11 @@ export function mom(source: Source, length: simple_int): series_float {
|
|
|
1037
1059
|
*
|
|
1038
1060
|
* @remarks
|
|
1039
1061
|
* - Measures average absolute distance from the mean
|
|
1040
|
-
* - Formula: `sum(abs(source[i]! - sma)) / length` for i in 0 to length-1
|
|
1041
1062
|
* - Less sensitive to outliers than standard deviation
|
|
1042
|
-
* - `
|
|
1043
|
-
*
|
|
1063
|
+
* - PineScript rule (bit for bit): `sum(abs(source[i] - mean)) / length` for `i = 0 .. length - 1`, added newest
|
|
1064
|
+
* first, with `mean = ta.sma(source, length)` (its compensated running sum, so a flat window can give a tiny
|
|
1065
|
+
* non-zero deviation)
|
|
1066
|
+
* - The loop reads the last `length` bars: an `na` among them gives `na`
|
|
1044
1067
|
*
|
|
1045
1068
|
* @example
|
|
1046
1069
|
* ```typescript
|
|
@@ -1049,25 +1072,13 @@ export function mom(source: Source, length: simple_int): series_float {
|
|
|
1049
1072
|
* ```
|
|
1050
1073
|
*/
|
|
1051
1074
|
export function dev(source: Source, length: simple_int): series_float {
|
|
1052
|
-
const
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
} else {
|
|
1060
|
-
let sum = 0;
|
|
1061
|
-
for (let j = 0; j < length; j++) {
|
|
1062
|
-
if (!isNaN(source[i - j]!)) {
|
|
1063
|
-
sum += Math.abs(source[i - j]! - meanValues[i]!);
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
result.push(sum / length);
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
return result;
|
|
1075
|
+
const len = Math.floor(length);
|
|
1076
|
+
return sma(source, len).map((mean, i) => {
|
|
1077
|
+
if (i < len - 1 || Number.isNaN(mean)) return NaN;
|
|
1078
|
+
let sum = 0;
|
|
1079
|
+
for (let k = 0; k < len; k++) sum += Math.abs(source[i - k]! - mean);
|
|
1080
|
+
return sum / len;
|
|
1081
|
+
});
|
|
1071
1082
|
}
|
|
1072
1083
|
|
|
1073
1084
|
/**
|
|
@@ -1096,7 +1107,25 @@ export function dev(source: Source, length: simple_int): series_float {
|
|
|
1096
1107
|
* const sampleVariance = ta.variance(closePrices, 20, false);
|
|
1097
1108
|
* ```
|
|
1098
1109
|
*/
|
|
1099
|
-
export function variance(
|
|
1110
|
+
export function variance(
|
|
1111
|
+
source: Source,
|
|
1112
|
+
length: simple_int | ArrayLike<number>,
|
|
1113
|
+
biased: simple_bool = true
|
|
1114
|
+
): series_float {
|
|
1115
|
+
return varianceOf(source, length, biased, 'ta.variance');
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/** {@link variance} with a fixed or a series length; `name` is used in the error messages. */
|
|
1119
|
+
function varianceOf(
|
|
1120
|
+
source: Source,
|
|
1121
|
+
length: simple_int | ArrayLike<number>,
|
|
1122
|
+
biased: boolean,
|
|
1123
|
+
name: string
|
|
1124
|
+
): series_float {
|
|
1125
|
+
if (typeof length !== 'number') {
|
|
1126
|
+
const result = runningVarianceSeries(source, length, biased, name);
|
|
1127
|
+
return biased ? result : result.map((v, i) => (Math.floor(length[i]!) > 1 ? v : NaN));
|
|
1128
|
+
}
|
|
1100
1129
|
const len = Math.floor(length);
|
|
1101
1130
|
// PineScript: the last `len` non-na values (also +/-Infinity skipped), as ta.sma
|
|
1102
1131
|
if ((biased ? len : len - 1) <= 0) return source.map(() => NaN);
|
|
@@ -1168,7 +1197,7 @@ export function median(source: Source, length: simple_int): series_float {
|
|
|
1168
1197
|
* @remarks
|
|
1169
1198
|
* - Fixed length of 4 bars
|
|
1170
1199
|
* - Weights: [1/6, 2/6, 2/6, 1/6] (symmetric)
|
|
1171
|
-
* - Formula: `source
|
|
1200
|
+
* - Formula, in this order as in PineScript (bit for bit): `(source + 2 * source[1] + 2 * source[2] + source[3]) / 6`
|
|
1172
1201
|
* - More weight given to middle values
|
|
1173
1202
|
* - `na` values in the source series are included in calculations and will produce an `na` result
|
|
1174
1203
|
* - Returns NaN for the first 3 bars
|
|
@@ -1180,27 +1209,9 @@ export function median(source: Source, length: simple_int): series_float {
|
|
|
1180
1209
|
* ```
|
|
1181
1210
|
*/
|
|
1182
1211
|
export function swma(source: Source): series_float {
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
if (i < 3) {
|
|
1187
|
-
result.push(NaN);
|
|
1188
|
-
} else {
|
|
1189
|
-
// Check for any NaN values in the window
|
|
1190
|
-
if (isNaN(source[i]!) || isNaN(source[i - 1]!) || isNaN(source[i - 2]!) || isNaN(source[i - 3]!)) {
|
|
1191
|
-
result.push(NaN);
|
|
1192
|
-
} else {
|
|
1193
|
-
const value =
|
|
1194
|
-
source[i - 3]! * (1 / 6) +
|
|
1195
|
-
source[i - 2]! * (2 / 6) +
|
|
1196
|
-
source[i - 1]! * (2 / 6) +
|
|
1197
|
-
source[i]! * (1 / 6);
|
|
1198
|
-
result.push(value);
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
return result;
|
|
1212
|
+
return Array.from({ length: source.length }, (_, i) =>
|
|
1213
|
+
i < 3 ? NaN : (source[i]! + 2 * source[i - 1]! + 2 * source[i - 2]! + source[i - 3]!) / 6
|
|
1214
|
+
);
|
|
1204
1215
|
}
|
|
1205
1216
|
|
|
1206
1217
|
/**
|
|
@@ -1269,6 +1280,9 @@ export function vwma(source: Source, length: simple_int, volume?: Source): serie
|
|
|
1269
1280
|
* @remarks
|
|
1270
1281
|
* - Calculates line of best fit using least squares method
|
|
1271
1282
|
* - Formula: `linreg = intercept + slope * (length - 1 - offset)`
|
|
1283
|
+
* - As PineScript (bit for bit): x = 1 (oldest) .. length (newest), sums from the oldest value,
|
|
1284
|
+
* `slope = (length * sumXY - sumX * sumY) / (length * sumX2 - sumX * sumX)`,
|
|
1285
|
+
* `intercept = sumY / length - slope * sumX / length + slope`
|
|
1272
1286
|
* - offset=0 gives current fitted value, offset<0 gives future projection
|
|
1273
1287
|
* - `na` values in the source series are included in calculations and will produce an `na` result
|
|
1274
1288
|
* - Useful for trend detection and prediction
|
|
@@ -1300,27 +1314,23 @@ export function linreg(source: Source, length: simple_int, offset: simple_int =
|
|
|
1300
1314
|
if (hasNaN) {
|
|
1301
1315
|
result.push(NaN);
|
|
1302
1316
|
} else {
|
|
1303
|
-
//
|
|
1317
|
+
// Least squares as PineScript: x = 1 (oldest) .. len (newest), summed from the oldest value; the intercept
|
|
1318
|
+
// is the fitted value at x = 1
|
|
1304
1319
|
let sumX = 0;
|
|
1305
1320
|
let sumY = 0;
|
|
1306
1321
|
let sumXY = 0;
|
|
1307
1322
|
let sumX2 = 0;
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
const
|
|
1311
|
-
const y = source[i - (len - 1 - j)]!;
|
|
1323
|
+
for (let j = 0; j < len; j++) {
|
|
1324
|
+
const x = j + 1;
|
|
1325
|
+
const y = source[i - (len - 1 - j)]!;
|
|
1312
1326
|
sumX += x;
|
|
1313
1327
|
sumY += y;
|
|
1314
1328
|
sumXY += x * y;
|
|
1315
1329
|
sumX2 += x * x;
|
|
1316
1330
|
}
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
// Calculate linreg value at offset
|
|
1322
|
-
const x = len - 1 - offset;
|
|
1323
|
-
result.push(intercept + slope * x);
|
|
1331
|
+
const slope = (len * sumXY - sumX * sumY) / (len * sumX2 - sumX * sumX);
|
|
1332
|
+
const intercept = sumY / len - (slope * sumX) / len + slope;
|
|
1333
|
+
result.push(intercept + slope * (len - 1 - offset));
|
|
1324
1334
|
}
|
|
1325
1335
|
}
|
|
1326
1336
|
}
|
|
@@ -1342,8 +1352,9 @@ export function linreg(source: Source, length: simple_int, offset: simple_int =
|
|
|
1342
1352
|
* - -1 = perfect negative correlation
|
|
1343
1353
|
* - 0 = no correlation
|
|
1344
1354
|
* - Measures linear relationship between two series
|
|
1345
|
-
* - `
|
|
1346
|
-
*
|
|
1355
|
+
* - As PineScript: `cov = sma(x * y) - sma(x) * sma(y)`, `r = cov / sqrt(variance(x) * variance(y))` with the
|
|
1356
|
+
* compensated running sums of `ta.sma` / `ta.variance` (each skips its own `na` values); `|cov| <= 1e-10` gives 0, a
|
|
1357
|
+
* zero variance gives `na`
|
|
1347
1358
|
*
|
|
1348
1359
|
* @example
|
|
1349
1360
|
* ```typescript
|
|
@@ -1352,59 +1363,26 @@ export function linreg(source: Source, length: simple_int, offset: simple_int =
|
|
|
1352
1363
|
* ```
|
|
1353
1364
|
*/
|
|
1354
1365
|
export function correlation(source1: Source, source2: Source, length: simple_int): series_float {
|
|
1355
|
-
|
|
1356
|
-
//
|
|
1357
|
-
const
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
}
|
|
1376
|
-
if (m === 0) {
|
|
1377
|
-
result.push(NaN);
|
|
1378
|
-
continue;
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
// Calculate means
|
|
1382
|
-
let sum1 = 0;
|
|
1383
|
-
let sum2 = 0;
|
|
1384
|
-
for (let k = 0; k < m; k++) {
|
|
1385
|
-
sum1 += a[k]!;
|
|
1386
|
-
sum2 += b[k]!;
|
|
1387
|
-
}
|
|
1388
|
-
const mean1 = sum1 / m;
|
|
1389
|
-
const mean2 = sum2 / m;
|
|
1390
|
-
|
|
1391
|
-
// Calculate correlation components
|
|
1392
|
-
let numerator = 0;
|
|
1393
|
-
let sum1Sq = 0;
|
|
1394
|
-
let sum2Sq = 0;
|
|
1395
|
-
for (let k = 0; k < m; k++) {
|
|
1396
|
-
const dev1 = a[k]! - mean1;
|
|
1397
|
-
const dev2 = b[k]! - mean2;
|
|
1398
|
-
numerator += dev1 * dev2;
|
|
1399
|
-
sum1Sq += dev1 * dev1;
|
|
1400
|
-
sum2Sq += dev2 * dev2;
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
const denominator = Math.sqrt(sum1Sq * sum2Sq);
|
|
1404
|
-
result.push(denominator === 0 ? NaN : numerator / denominator);
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
return result;
|
|
1366
|
+
// PineScript: from compensated running sums, cov = sma(x * y) - sma(x) * sma(y), r = cov / sqrt(var(x) * var(y))
|
|
1367
|
+
// (biased variances); each running sum skips its own na values
|
|
1368
|
+
const product = Array.from({ length: source1.length }, (_, i) => {
|
|
1369
|
+
const v1 = source1[i];
|
|
1370
|
+
const v2 = source2[i];
|
|
1371
|
+
return v1 === null || v1 === undefined || v2 === null || v2 === undefined ? NaN : v1 * v2;
|
|
1372
|
+
});
|
|
1373
|
+
const meanXY = sma(product, length);
|
|
1374
|
+
const meanX = sma(source1, length);
|
|
1375
|
+
const meanY = sma(source2, length);
|
|
1376
|
+
const varX = variance(source1, length);
|
|
1377
|
+
const varY = variance(source2, length);
|
|
1378
|
+
return meanXY.map((mxy, i) => {
|
|
1379
|
+
const cov = mxy - meanX[i]! * meanY[i]!;
|
|
1380
|
+
if (Number.isNaN(cov)) return NaN;
|
|
1381
|
+
// a covariance within 1e-10 of 0 gives 0; a zero denominator gives na (a division by zero)
|
|
1382
|
+
if (Math.abs(cov) <= 1e-10) return 0;
|
|
1383
|
+
const denominator = Math.sqrt(varX[i]! * varY[i]!);
|
|
1384
|
+
return denominator === 0 ? NaN : cov / denominator;
|
|
1385
|
+
});
|
|
1408
1386
|
}
|
|
1409
1387
|
|
|
1410
1388
|
/**
|
|
@@ -1466,10 +1444,14 @@ export function percentrank(source: Source, length: simple_int): series_float {
|
|
|
1466
1444
|
* - Typical Price = (High + Low + Close) / 3
|
|
1467
1445
|
* - Mean Deviation = Average of absolute differences from mean
|
|
1468
1446
|
* - Scaled by 0.015 to provide more readable numbers
|
|
1469
|
-
* - As in PineScript: when the mean deviation is 0 (1e-10 tolerance, a flat window), the CCI of the previous bar
|
|
1470
1447
|
* - Values above +100 indicate overbought conditions
|
|
1471
1448
|
* - Values below -100 indicate oversold conditions
|
|
1472
|
-
*
|
|
1449
|
+
*
|
|
1450
|
+
* PineScript rules (bit for bit):
|
|
1451
|
+
* - `(source - ta.sma(source, length)) / (0.015 * ta.dev(source, length))`
|
|
1452
|
+
* - when the mean deviation is 0 (1e-10 tolerance, a flat window), the last CCI value; 0 when there is none yet
|
|
1453
|
+
* (a flat first window)
|
|
1454
|
+
* - \`na\` when the mean or the deviation is \`na\` (an \`na\` in the last \`length\` bars)
|
|
1473
1455
|
*
|
|
1474
1456
|
* @example
|
|
1475
1457
|
* \`\`\`typescript
|
|
@@ -1479,22 +1461,15 @@ export function percentrank(source: Source, length: simple_int): series_float {
|
|
|
1479
1461
|
* \`\`\`
|
|
1480
1462
|
*/
|
|
1481
1463
|
export function cci(source: Source, length: simple_int): series_float {
|
|
1482
|
-
const result: series_float = [];
|
|
1483
1464
|
const smaValues = sma(source, length);
|
|
1484
1465
|
const devValues = dev(source, length);
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
const cci = (source[i]! - smaValues[i]!) / (0.015 * devValues[i]!);
|
|
1493
|
-
result.push(cci);
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
return result;
|
|
1466
|
+
let last = 0;
|
|
1467
|
+
return smaValues.map((mean, i) => {
|
|
1468
|
+
const d = devValues[i]!;
|
|
1469
|
+
if (Number.isNaN(mean) || Number.isNaN(d)) return NaN;
|
|
1470
|
+
if (!eq(d, 0)) last = (source[i]! - mean) / (0.015 * d);
|
|
1471
|
+
return last;
|
|
1472
|
+
});
|
|
1498
1473
|
}
|
|
1499
1474
|
|
|
1500
1475
|
/**
|
|
@@ -1511,8 +1486,8 @@ export function cci(source: Source, length: simple_int): series_float {
|
|
|
1511
1486
|
* - Measures where close is relative to the high-low range
|
|
1512
1487
|
* - Values above 80 typically indicate overbought
|
|
1513
1488
|
* - Values below 20 typically indicate oversold
|
|
1514
|
-
* -
|
|
1515
|
-
*
|
|
1489
|
+
* - As PineScript: when the source, \`highest(high, length)\` or \`lowest(low, length)\` is \`na\`, or the range is 0, the
|
|
1490
|
+
* result is the previous result of the call (\`na\` until a first value)
|
|
1516
1491
|
*
|
|
1517
1492
|
* @example
|
|
1518
1493
|
* \`\`\`typescript
|
|
@@ -1526,18 +1501,14 @@ export function stoch(source: Source, high: Source, low: Source, length: simple_
|
|
|
1526
1501
|
const lowestValues = lowest(low, length);
|
|
1527
1502
|
const highestValues = highest(high, length);
|
|
1528
1503
|
|
|
1504
|
+
// PineScript: an na input or a zero range keeps the previous result
|
|
1505
|
+
let previous = NaN;
|
|
1529
1506
|
for (let i = 0; i < source.length; i++) {
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
result.push(NaN);
|
|
1536
|
-
} else {
|
|
1537
|
-
const stochValue = 100 * (source[i]! - lowestValues[i]!) / range;
|
|
1538
|
-
result.push(stochValue);
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1507
|
+
const value = source[i]!;
|
|
1508
|
+
const lo = lowestValues[i]!;
|
|
1509
|
+
const range = highestValues[i]! - lo;
|
|
1510
|
+
if (!Number.isNaN(value) && !Number.isNaN(range) && range !== 0) previous = (100 * (value - lo)) / range;
|
|
1511
|
+
result.push(previous);
|
|
1541
1512
|
}
|
|
1542
1513
|
|
|
1543
1514
|
return result;
|
|
@@ -1559,7 +1530,13 @@ export function stoch(source: Source, high: Source, low: Source, length: simple_
|
|
|
1559
1530
|
* - Formula: 100 - (100 / (1 + Positive Money Flow / Negative Money Flow))
|
|
1560
1531
|
* - Values above 80 indicate overbought
|
|
1561
1532
|
* - Values below 20 indicate oversold
|
|
1562
|
-
*
|
|
1533
|
+
*
|
|
1534
|
+
* PineScript rules (bit for bit):
|
|
1535
|
+
* - the two flows are `math.sum` running sums; a sum within 1e-10 of 0 counts as 0 (the residue a running sum
|
|
1536
|
+
* keeps over a window of zeros)
|
|
1537
|
+
* - 100 when the negative flow is 0; `na` when `1 + positive / negative` is 0 (division by zero)
|
|
1538
|
+
* - a bar with an `na` source adds nothing to the sums and gives the previous result; the bar after it (an `na`
|
|
1539
|
+
* change) adds its money flow to both sums
|
|
1563
1540
|
*
|
|
1564
1541
|
* @example
|
|
1565
1542
|
* ```typescript
|
|
@@ -1596,11 +1573,16 @@ export function mfi(source: Source, length: simple_int, volume?: Source): series
|
|
|
1596
1573
|
const upper = runningSum(upperFlow, length);
|
|
1597
1574
|
const lower = runningSum(lowerFlow, length);
|
|
1598
1575
|
|
|
1599
|
-
|
|
1600
|
-
|
|
1576
|
+
// A sum within 1e-10 of 0 (the residue of a running sum over a window of zeros) is 0
|
|
1577
|
+
const zero = (x: number) => (Math.abs(x) <= 1e-10 ? 0 : x);
|
|
1578
|
+
return upper.map((sumUp, i) => {
|
|
1579
|
+
const u = zero(sumUp);
|
|
1580
|
+
const l = zero(lower[i]!);
|
|
1601
1581
|
if (Number.isNaN(u) || Number.isNaN(l)) return NaN;
|
|
1602
1582
|
if (l === 0) return 100;
|
|
1603
|
-
|
|
1583
|
+
const ratio = 1 + u / l;
|
|
1584
|
+
// x / 0 is na in PineScript
|
|
1585
|
+
return ratio === 0 ? NaN : 100 - 100 / ratio;
|
|
1604
1586
|
});
|
|
1605
1587
|
}
|
|
1606
1588
|
|
|
@@ -2012,10 +1994,12 @@ export function dmi(
|
|
|
2012
1994
|
* @param source - Series of values to process
|
|
2013
1995
|
* @param shortLength - Short smoothing length
|
|
2014
1996
|
* @param longLength - Long smoothing length
|
|
2015
|
-
* @returns TSI series
|
|
1997
|
+
* @returns TSI series, a value in [-1, 1]
|
|
2016
1998
|
*
|
|
2017
1999
|
* @remarks
|
|
2018
|
-
* -
|
|
2000
|
+
* - As PineScript: `ta.ema(ta.ema(m, longLength), shortLength) / ta.ema(ta.ema(math.abs(m), longLength), shortLength)`
|
|
2001
|
+
* with `m = source - source[1]` (not scaled by 100)
|
|
2002
|
+
* - na when the denominator is 0 (a constant source), as a PineScript division by zero
|
|
2019
2003
|
* - Positive values indicate bullish momentum
|
|
2020
2004
|
* - Negative values indicate bearish momentum
|
|
2021
2005
|
* - Crossovers of zero line can signal trend changes
|
|
@@ -2047,17 +2031,11 @@ export function tsi(source: Source, shortLength: simple_int, longLength: simple_
|
|
|
2047
2031
|
const absMomentum = momentum.map(Math.abs);
|
|
2048
2032
|
const smoothedAbsMomentum = ema(ema(absMomentum, longLength), shortLength);
|
|
2049
2033
|
|
|
2050
|
-
//
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
} else {
|
|
2056
|
-
result.push((smoothedMomentum[i]! / smoothedAbsMomentum[i]!) * 100);
|
|
2057
|
-
}
|
|
2058
|
-
}
|
|
2059
|
-
|
|
2060
|
-
return result;
|
|
2034
|
+
// PineScript: a value in [-1, 1]; a division by zero is na
|
|
2035
|
+
return smoothedMomentum.map((m, i) => {
|
|
2036
|
+
const d = smoothedAbsMomentum[i]!;
|
|
2037
|
+
return d === 0 ? NaN : m / d;
|
|
2038
|
+
});
|
|
2061
2039
|
}
|
|
2062
2040
|
|
|
2063
2041
|
/**
|
|
@@ -2223,6 +2201,10 @@ export function bbw(source: Source, length: simple_int, mult: simple_float): ser
|
|
|
2223
2201
|
* @param length - Lookback period (default: 14)
|
|
2224
2202
|
* @returns Williams %R series
|
|
2225
2203
|
*
|
|
2204
|
+
* @remarks
|
|
2205
|
+
* PineScript rule (bit for bit): `100 * (close - max) / (max - min)` with `max = ta.highest(high, length)` and
|
|
2206
|
+
* `min = ta.lowest(low, length)` (their `na` rules); `na` when the range is 0.
|
|
2207
|
+
*
|
|
2226
2208
|
* @example
|
|
2227
2209
|
* ```typescript
|
|
2228
2210
|
* const wpr = ta.wpr(high, low, close, 14);
|
|
@@ -2231,34 +2213,14 @@ export function bbw(source: Source, length: simple_int, mult: simple_float): ser
|
|
|
2231
2213
|
* ```
|
|
2232
2214
|
*/
|
|
2233
2215
|
export function wpr(high: Source, low: Source, close: Source, length: simple_int = 14): series_float {
|
|
2234
|
-
const
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
// Find highest high and lowest low in the period
|
|
2243
|
-
let highestHigh = high[i - length + 1]!;
|
|
2244
|
-
let lowestLow = low[i - length + 1]!;
|
|
2245
|
-
|
|
2246
|
-
for (let j = i - length + 2; j <= i; j++) {
|
|
2247
|
-
if (high[j]! > highestHigh) highestHigh = high[j]!;
|
|
2248
|
-
if (low[j]! < lowestLow) lowestLow = low[j]!;
|
|
2249
|
-
}
|
|
2250
|
-
|
|
2251
|
-
const range = highestHigh - lowestLow;
|
|
2252
|
-
if (range === 0) {
|
|
2253
|
-
result.push(NaN);
|
|
2254
|
-
} else {
|
|
2255
|
-
// Formula: (Highest High - Close) / (Highest High - Lowest Low) * -100
|
|
2256
|
-
const wprValue = ((highestHigh! - close[i]!) / range) * -100;
|
|
2257
|
-
result.push(wprValue);
|
|
2258
|
-
}
|
|
2259
|
-
}
|
|
2260
|
-
|
|
2261
|
-
return result;
|
|
2216
|
+
const highs = highest(high, length);
|
|
2217
|
+
const lows = lowest(low, length);
|
|
2218
|
+
return Array.from({ length: close.length }, (_, i) => {
|
|
2219
|
+
const hmax = highs[i]!;
|
|
2220
|
+
const range = hmax - lows[i]!;
|
|
2221
|
+
// PineScript, in this order (bit for bit): 100 * (close - max) / (max - min); x / 0 is na
|
|
2222
|
+
return range === 0 ? NaN : (100 * (close[i]! - hmax)) / range;
|
|
2223
|
+
});
|
|
2262
2224
|
}
|
|
2263
2225
|
|
|
2264
2226
|
/**
|
|
@@ -2274,6 +2236,9 @@ export function wpr(high: Source, low: Source, close: Source, length: simple_int
|
|
|
2274
2236
|
* @remarks
|
|
2275
2237
|
* As in PineScript: the sums restart on anchor bars; the standard deviation is
|
|
2276
2238
|
* `sqrt(Σ(volume × source²) / Σvolume − vwap²)`.
|
|
2239
|
+
* - With an anchor series: `na` before the first anchor bar
|
|
2240
|
+
* - A bar with an `na` source (or volume) makes the sums `na` until the next anchor bar (`na` values are not
|
|
2241
|
+
* skipped)
|
|
2277
2242
|
*/
|
|
2278
2243
|
export function vwap(source: Source, volume: Source, anchor?: ArrayLike<boolean | number>): series_float;
|
|
2279
2244
|
export function vwap(
|
|
@@ -2294,18 +2259,19 @@ export function vwap(
|
|
|
2294
2259
|
const mid: series_float = [];
|
|
2295
2260
|
const upper: series_float = [];
|
|
2296
2261
|
const lower: series_float = [];
|
|
2297
|
-
|
|
2298
|
-
let
|
|
2299
|
-
let
|
|
2262
|
+
// na until the first anchor (without an anchor series, bar 0 starts the sums)
|
|
2263
|
+
let sumPV = NaN;
|
|
2264
|
+
let sumV = NaN;
|
|
2265
|
+
let sumPPV = NaN;
|
|
2300
2266
|
for (let i = 0; i < source.length; i++) {
|
|
2301
|
-
if (anchor && anchor[i]) {
|
|
2302
|
-
sumPV = 0;
|
|
2303
|
-
sumV = 0;
|
|
2304
|
-
sumPPV = 0;
|
|
2305
|
-
}
|
|
2306
2267
|
const s = source[i]!;
|
|
2307
2268
|
const v = volume[i]!;
|
|
2308
|
-
if (
|
|
2269
|
+
if (anchor ? anchor[i] : i === 0) {
|
|
2270
|
+
sumPV = s * v;
|
|
2271
|
+
sumV = v;
|
|
2272
|
+
sumPPV = s * s * v;
|
|
2273
|
+
} else {
|
|
2274
|
+
// An na source or volume makes the sums na until the next anchor
|
|
2309
2275
|
sumPV += s * v;
|
|
2310
2276
|
sumV += v;
|
|
2311
2277
|
sumPPV += s * s * v;
|
|
@@ -2387,7 +2353,8 @@ export function alma(
|
|
|
2387
2353
|
/**
|
|
2388
2354
|
* Keltner Channels Width (KCW)
|
|
2389
2355
|
*
|
|
2390
|
-
* Measures the width of Keltner Channels
|
|
2356
|
+
* Measures the width of Keltner Channels relative to the middle line: `(upper - lower) / middle`, a ratio as in
|
|
2357
|
+
* PineScript (0.05 is a width of 5% of the middle line; not multiplied by 100).
|
|
2391
2358
|
* Similar to BBW but uses ATR instead of standard deviation.
|
|
2392
2359
|
*
|
|
2393
2360
|
* Low KCW suggests consolidation/low volatility.
|
|
@@ -2425,8 +2392,8 @@ export function kcw(
|
|
|
2425
2392
|
if (isNaN(basis[i]!) || basis[i]! === 0) {
|
|
2426
2393
|
result.push(NaN);
|
|
2427
2394
|
} else {
|
|
2428
|
-
|
|
2429
|
-
result.push(
|
|
2395
|
+
// PineScript: (upper - lower) / basis, a ratio (not a percentage)
|
|
2396
|
+
result.push((upper[i]! - lower[i]!) / basis[i]!);
|
|
2430
2397
|
}
|
|
2431
2398
|
}
|
|
2432
2399
|
|
|
@@ -2508,50 +2475,30 @@ export function min(source: Source): series_float {
|
|
|
2508
2475
|
* The Center of Gravity indicator is an oscillator developed by John Ehlers.
|
|
2509
2476
|
* It identifies turning points with minimal lag and provides clear signals.
|
|
2510
2477
|
*
|
|
2511
|
-
* The COG calculates a weighted average where more recent prices have higher weights,
|
|
2512
|
-
* similar to a moving average but with a focus on momentum shifts.
|
|
2513
|
-
*
|
|
2514
2478
|
* @param source - Source series (typically close)
|
|
2515
2479
|
* @param length - Lookback period (default: 10)
|
|
2516
2480
|
* @returns COG series
|
|
2517
2481
|
*
|
|
2482
|
+
* @remarks
|
|
2483
|
+
* PineScript rule (bit for bit): `-num / math.sum(source, length)`, with `num` the sum of `source[i] * (i + 1)`
|
|
2484
|
+
* for `i = 0 .. length - 1`, added newest first.
|
|
2485
|
+
* - The denominator is the compensated running sum of `math.sum` (the last `length` non-`na` values)
|
|
2486
|
+
* - The numerator reads the last `length` bars, so an `na` among them gives `na`
|
|
2487
|
+
* - A zero denominator gives `na` (PineScript division by zero)
|
|
2488
|
+
*
|
|
2518
2489
|
* @example
|
|
2519
2490
|
* ```typescript
|
|
2520
2491
|
* const cogValue = ta.cog(close, 10);
|
|
2521
|
-
* // Use COG crossovers as signals:
|
|
2522
|
-
* // - COG crossing above 0: potential buy signal
|
|
2523
|
-
* // - COG crossing below 0: potential sell signal
|
|
2524
2492
|
* ```
|
|
2525
2493
|
*/
|
|
2526
2494
|
export function cog(source: Source, length: simple_int = 10): series_float {
|
|
2527
|
-
const
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
let numerator = 0;
|
|
2536
|
-
let denominator = 0;
|
|
2537
|
-
|
|
2538
|
-
for (let j = 0; j < length; j++) {
|
|
2539
|
-
const weight = j + 1;
|
|
2540
|
-
const price = source[i - length + 1 + j]!;
|
|
2541
|
-
numerator += weight * price;
|
|
2542
|
-
denominator += price;
|
|
2543
|
-
}
|
|
2544
|
-
|
|
2545
|
-
if (denominator === 0) {
|
|
2546
|
-
result.push(NaN);
|
|
2547
|
-
} else {
|
|
2548
|
-
// COG formula: -1 * (sum of (weight * price) / sum of prices) + (length + 1) / 2
|
|
2549
|
-
const cog = -1 * (numerator / denominator) + (length + 1) / 2;
|
|
2550
|
-
result.push(cog);
|
|
2551
|
-
}
|
|
2552
|
-
}
|
|
2553
|
-
|
|
2554
|
-
return result;
|
|
2495
|
+
const len = Math.floor(length);
|
|
2496
|
+
return runningSum(source, len).map((sum, i) => {
|
|
2497
|
+
if (i < len - 1 || Number.isNaN(sum) || sum === 0) return NaN;
|
|
2498
|
+
let num = 0;
|
|
2499
|
+
for (let k = 0; k < len; k++) num += source[i - k]! * (k + 1);
|
|
2500
|
+
return -num / sum;
|
|
2501
|
+
});
|
|
2555
2502
|
}
|
|
2556
2503
|
|
|
2557
2504
|
/**
|
|
@@ -2571,32 +2518,25 @@ export function cog(source: Source, length: simple_int = 10): series_float {
|
|
|
2571
2518
|
* ```
|
|
2572
2519
|
*
|
|
2573
2520
|
* @remarks
|
|
2574
|
-
*
|
|
2521
|
+
* PineScript rules:
|
|
2522
|
+
* - the window is the last `length` non-`na` values (an `na` bar does not enter it, and gets the mode of the
|
|
2523
|
+
* window it keeps)
|
|
2524
|
+
* - NaN until `length` non-`na` values exist
|
|
2575
2525
|
* - If no mode exists, returns the smallest value
|
|
2576
|
-
* - Returns NaN for the first (length - 1) values where there's insufficient data
|
|
2577
2526
|
*/
|
|
2578
2527
|
export function mode(source: Source, length: simple_int): series_float {
|
|
2579
2528
|
const result: series_float = [];
|
|
2529
|
+
const len = Math.floor(length);
|
|
2530
|
+
const seen: number[] = []; // non-na values so far
|
|
2580
2531
|
|
|
2581
2532
|
for (let i = 0; i < source.length; i++) {
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
}
|
|
2586
|
-
|
|
2587
|
-
// Collect non-NaN values in the window
|
|
2588
|
-
const values: number[] = [];
|
|
2589
|
-
for (let j = 0; j < length; j++) {
|
|
2590
|
-
const value = source[i - j]!;
|
|
2591
|
-
if (!isNaN(value)) {
|
|
2592
|
-
values.push(value);
|
|
2593
|
-
}
|
|
2594
|
-
}
|
|
2595
|
-
|
|
2596
|
-
if (values.length === 0) {
|
|
2533
|
+
const v = source[i]!;
|
|
2534
|
+
if (!Number.isNaN(v)) seen.push(v);
|
|
2535
|
+
if (seen.length < len) {
|
|
2597
2536
|
result.push(NaN);
|
|
2598
2537
|
continue;
|
|
2599
2538
|
}
|
|
2539
|
+
const values = seen.slice(seen.length - len);
|
|
2600
2540
|
|
|
2601
2541
|
// Count frequency of each value
|
|
2602
2542
|
const frequencyMap = new Map<number, number>();
|
|
@@ -2759,95 +2699,73 @@ export function percentile_nearest_rank(
|
|
|
2759
2699
|
* - +100 indicates source consistently increased over the period
|
|
2760
2700
|
* - -100 indicates source consistently decreased over the period
|
|
2761
2701
|
* - 0 indicates no directional consistency
|
|
2762
|
-
* -
|
|
2702
|
+
* - As PineScript (bit for bit): the first value is on bar `length`; values less than 1e-10 above the smallest value
|
|
2703
|
+
* of a group (ascending sweep) are ties with their average rank; a window where all values are ties gives `na`;
|
|
2704
|
+
* moment-form variances of the bar positions and the ranks, `(100 * cov) / (sd_x * sd_y)`
|
|
2705
|
+
* - A bar with an `na` source keeps the previous result; the values are kept in a ring of `length + 1` slots by bar
|
|
2706
|
+
* index (0 at the start, not written on `na` bars), so a later window sees the value of `length + 1` bars earlier
|
|
2707
|
+
* where the `na` was
|
|
2763
2708
|
*/
|
|
2764
2709
|
export function rci(source: Source, length: simple_int): series_float {
|
|
2710
|
+
const len = Math.floor(length);
|
|
2765
2711
|
const result: series_float = [];
|
|
2766
|
-
|
|
2712
|
+
// PineScript keeps the values in a ring of `length + 1` slots indexed by the bar index, filled with 0 at the start
|
|
2713
|
+
// and written only on bars with a value: a bar with an na source keeps the previous result, and later windows see
|
|
2714
|
+
// the value of `length + 1` bars earlier in that slot
|
|
2715
|
+
const ring = new Array<number>(len + 1).fill(0);
|
|
2767
2716
|
for (let i = 0; i < source.length; i++) {
|
|
2768
|
-
|
|
2769
|
-
|
|
2717
|
+
const v = source[i]!;
|
|
2718
|
+
if (Number.isNaN(v)) {
|
|
2719
|
+
result.push(i > 0 ? result[i - 1]! : NaN);
|
|
2770
2720
|
continue;
|
|
2771
2721
|
}
|
|
2772
|
-
|
|
2773
|
-
//
|
|
2774
|
-
|
|
2775
|
-
for (let j = 0; j < length; j++) {
|
|
2776
|
-
values.push(source[i - length + 1 + j]!);
|
|
2777
|
-
}
|
|
2778
|
-
|
|
2779
|
-
// Check for NaN values
|
|
2780
|
-
if (values.some(v => isNaN(v))) {
|
|
2722
|
+
ring[i % (len + 1)] = v;
|
|
2723
|
+
// the first value is on bar `length` (one bar after the window is full)
|
|
2724
|
+
if (i < len) {
|
|
2781
2725
|
result.push(NaN);
|
|
2782
2726
|
continue;
|
|
2783
2727
|
}
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
const
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
}
|
|
2820
|
-
|
|
2821
|
-
const n = length;
|
|
2822
|
-
const base = (n * n * n - n) / 12;
|
|
2823
|
-
|
|
2824
|
-
// Compute tie correction for price ranks
|
|
2825
|
-
// Count tied groups from the sorted array
|
|
2826
|
-
let tieCorrection = 0;
|
|
2827
|
-
for (let j = 0; j < sorted.length; ) {
|
|
2828
|
-
let tieCount = 1;
|
|
2829
|
-
while (j + tieCount < sorted.length && sorted[j]!.value === sorted[j + tieCount]!.value) {
|
|
2830
|
-
tieCount++;
|
|
2831
|
-
}
|
|
2832
|
-
if (tieCount > 1) {
|
|
2833
|
-
tieCorrection += (tieCount * tieCount * tieCount - tieCount) / 12;
|
|
2834
|
-
}
|
|
2835
|
-
j += tieCount;
|
|
2836
|
-
}
|
|
2837
|
-
|
|
2838
|
-
const A = base - tieCorrection; // Corrected for tied price ranks
|
|
2839
|
-
const B = base; // Time ranks have no ties
|
|
2840
|
-
|
|
2841
|
-
let rho: number;
|
|
2842
|
-
if (A === 0 || B === 0) {
|
|
2843
|
-
rho = 0;
|
|
2844
|
-
} else {
|
|
2845
|
-
rho = (A + B - sumSquaredDiff) / (2 * Math.sqrt(A * B));
|
|
2728
|
+
// window newest first (position 0 = this bar)
|
|
2729
|
+
const values = Array.from({ length: len }, (_, j) => ring[(i - j) % (len + 1)]!);
|
|
2730
|
+
// Ranks: ascending sweep; a group collects the values less than 1e-10 above its smallest value; each group gets
|
|
2731
|
+
// its average 0-based rank counted from the largest value
|
|
2732
|
+
const order = values.map((_, j) => j).sort((a, b) => values[a]! - values[b]!);
|
|
2733
|
+
const ranks = new Array<number>(len);
|
|
2734
|
+
for (let g = 0; g < len; ) {
|
|
2735
|
+
const anchor = values[order[g]!]!;
|
|
2736
|
+
let end = g + 1;
|
|
2737
|
+
while (end < len && values[order[end]!]! - anchor < 1e-10) end++;
|
|
2738
|
+
const avgRank = len - 1 - (g + g + (end - g) - 1) / 2;
|
|
2739
|
+
for (let k = g; k < end; k++) ranks[order[k]!] = avgRank;
|
|
2740
|
+
g = end;
|
|
2741
|
+
}
|
|
2742
|
+
// moment-form variances, covariance from deviation products, (100 * cov) / (sd_x * sd_y): this operation order
|
|
2743
|
+
// gives PineScript's values bit for bit
|
|
2744
|
+
let sumX = 0;
|
|
2745
|
+
let sumY = 0;
|
|
2746
|
+
let sumX2 = 0;
|
|
2747
|
+
let sumY2 = 0;
|
|
2748
|
+
for (let k = 0; k < len; k++) {
|
|
2749
|
+
const y = ranks[k]!;
|
|
2750
|
+
sumX += k;
|
|
2751
|
+
sumY += y;
|
|
2752
|
+
sumX2 += k * k;
|
|
2753
|
+
sumY2 += y * y;
|
|
2754
|
+
}
|
|
2755
|
+
const meanX = sumX / len;
|
|
2756
|
+
const meanY = sumY / len;
|
|
2757
|
+
const varX = sumX2 / len - meanX * meanX;
|
|
2758
|
+
const varY = sumY2 / len - meanY * meanY;
|
|
2759
|
+
// a window where all values are ties has no correlation
|
|
2760
|
+
if (!(varX > 0) || !(varY > 0)) {
|
|
2761
|
+
result.push(NaN);
|
|
2762
|
+
continue;
|
|
2846
2763
|
}
|
|
2847
|
-
|
|
2848
|
-
|
|
2764
|
+
let cov = 0;
|
|
2765
|
+
for (let k = 0; k < len; k++) cov += (k - meanX) * (ranks[k]! - meanY);
|
|
2766
|
+
cov /= len;
|
|
2767
|
+
result.push((100 * cov) / (Math.sqrt(varX) * Math.sqrt(varY)));
|
|
2849
2768
|
}
|
|
2850
|
-
|
|
2851
2769
|
return result;
|
|
2852
2770
|
}
|
|
2853
2771
|
|
|
@@ -3468,16 +3386,18 @@ export function pvt(close: Source, volume: Source): series_float {
|
|
|
3468
3386
|
|
|
3469
3387
|
/**
|
|
3470
3388
|
* Accumulation/Distribution index: the running sum of
|
|
3471
|
-
* `(
|
|
3389
|
+
* `((close - low) - (high - close)) / (high - low) * volume`, in this order as in PineScript (bit for bit).
|
|
3390
|
+
* A bar where this is `na` (`high == low`, an `na` volume) adds nothing and keeps the previous value.
|
|
3472
3391
|
*/
|
|
3473
3392
|
export function accdist(high: Source, low: Source, close: Source, volume: Source): series_float {
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3393
|
+
let ad = 0;
|
|
3394
|
+
return close.map((c, i) => {
|
|
3395
|
+
const h = high[i]!;
|
|
3396
|
+
const l = low[i]!;
|
|
3397
|
+
const mfv = (((c - l) - (h - c)) / (h - l)) * volume[i]!;
|
|
3398
|
+
if (Number.isFinite(mfv)) ad += mfv;
|
|
3399
|
+
return ad;
|
|
3400
|
+
});
|
|
3481
3401
|
}
|
|
3482
3402
|
|
|
3483
3403
|
/**
|