oakscriptjs 0.9.3 → 0.9.5
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 +4 -0
- package/dist/cjs/array/index.d.ts.map +1 -1
- package/dist/cjs/array/index.js +22 -39
- package/dist/cjs/array/index.js.map +2 -2
- package/dist/cjs/callsite/index.d.ts +62 -3
- package/dist/cjs/callsite/index.d.ts.map +1 -1
- package/dist/cjs/callsite/index.js +53 -2
- package/dist/cjs/callsite/index.js.map +3 -3
- 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/math/index.d.ts +3 -1
- package/dist/cjs/math/index.d.ts.map +1 -1
- package/dist/cjs/math/index.js +2 -2
- package/dist/cjs/math/index.js.map +2 -2
- package/dist/cjs/ta/index.d.ts +12 -6
- package/dist/cjs/ta/index.d.ts.map +1 -1
- package/dist/cjs/ta/index.js +27 -58
- package/dist/cjs/ta/index.js.map +3 -3
- package/dist/cjs/ta/running-sum.d.ts +2 -1
- package/dist/cjs/ta/running-sum.d.ts.map +1 -1
- package/dist/cjs/ta/running-sum.js +7 -2
- package/dist/cjs/ta/running-sum.js.map +2 -2
- package/dist/esm/array/index.d.ts +4 -0
- package/dist/esm/array/index.d.ts.map +1 -1
- package/dist/esm/array/index.js +35 -46
- package/dist/esm/array/index.js.map +1 -1
- package/dist/esm/callsite/index.d.ts +62 -3
- package/dist/esm/callsite/index.d.ts.map +1 -1
- package/dist/esm/callsite/index.js +117 -6
- package/dist/esm/callsite/index.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/math/index.d.ts +3 -1
- package/dist/esm/math/index.d.ts.map +1 -1
- package/dist/esm/math/index.js +2 -2
- package/dist/esm/math/index.js.map +1 -1
- package/dist/esm/ta/index.d.ts +12 -6
- package/dist/esm/ta/index.d.ts.map +1 -1
- package/dist/esm/ta/index.js +56 -77
- package/dist/esm/ta/index.js.map +1 -1
- package/dist/esm/ta/running-sum.d.ts +2 -1
- package/dist/esm/ta/running-sum.d.ts.map +1 -1
- package/dist/esm/ta/running-sum.js +15 -4
- package/dist/esm/ta/running-sum.js.map +1 -1
- package/package.json +2 -2
- package/src/array/index.ts +30 -47
- package/src/callsite/index.ts +119 -6
- package/src/index.ts +1 -1
- package/src/math/index.ts +5 -3
- package/src/ta/index.ts +61 -83
- package/src/ta/running-sum.ts +16 -4
package/src/callsite/index.ts
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* loops, called only on the bars where the Pine call runs.
|
|
12
12
|
* - {@link lowest} / {@link highest}: `ta.lowest` / `ta.highest` with a series length (a different length on each
|
|
13
13
|
* call), which PineScript computes with a state kept between calls.
|
|
14
|
+
* - {@link lowestByBar} / {@link highestByBar} / {@link linreg}: `ta.lowest` / `ta.highest` / `ta.linreg` in a
|
|
15
|
+
* conditional block, whose history is kept by bar (not by call).
|
|
14
16
|
*
|
|
15
17
|
* Since PineScript v6, `and` / `or` are lazy: the right operand runs only when the left operand does not decide
|
|
16
18
|
* the result. A ta.* call in the right operand of `a or b` runs on the bars where `a` is false, in `a and b` on the
|
|
@@ -40,6 +42,7 @@
|
|
|
40
42
|
*/
|
|
41
43
|
|
|
42
44
|
import type { Source } from '../types/index.js';
|
|
45
|
+
import { linreg as linregSeries } from '../ta/index.js';
|
|
43
46
|
|
|
44
47
|
/**
|
|
45
48
|
* Runs `fn` on the bars where `called` is true only, like a ta.* call inside an `if` block.
|
|
@@ -65,24 +68,35 @@ export function whenCalled<T extends number | boolean>(
|
|
|
65
68
|
return out;
|
|
66
69
|
}
|
|
67
70
|
|
|
68
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* A call site that keeps the arguments of the last call where both were not na (na before such a call), as
|
|
73
|
+
* ta.crossover / ta.crossunder / ta.cross.
|
|
74
|
+
*/
|
|
69
75
|
function twoSeriesSite(test: (a: number, b: number, prevA: number, prevB: number) => boolean) {
|
|
70
76
|
let prevA = NaN;
|
|
71
77
|
let prevB = NaN;
|
|
72
78
|
return (a: number, b: number): boolean => {
|
|
73
79
|
const result = test(a, b, prevA, prevB);
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
if (!Number.isNaN(a) && !Number.isNaN(b)) {
|
|
81
|
+
prevA = a;
|
|
82
|
+
prevB = b;
|
|
83
|
+
}
|
|
76
84
|
return result;
|
|
77
85
|
};
|
|
78
86
|
}
|
|
79
87
|
|
|
80
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* One `ta.crossover(a, b)` call site: compared with the last call where both arguments were not na; false before
|
|
90
|
+
* such a call.
|
|
91
|
+
*/
|
|
81
92
|
export function crossover(): (a: number, b: number) => boolean {
|
|
82
93
|
return twoSeriesSite((a, b, pa, pb) => a > b && pa <= pb);
|
|
83
94
|
}
|
|
84
95
|
|
|
85
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* One `ta.crossunder(a, b)` call site: compared with the last call where both arguments were not na; false before
|
|
98
|
+
* such a call.
|
|
99
|
+
*/
|
|
86
100
|
export function crossunder(): (a: number, b: number) => boolean {
|
|
87
101
|
return twoSeriesSite((a, b, pa, pb) => a < b && pa >= pb);
|
|
88
102
|
}
|
|
@@ -100,7 +114,10 @@ export function barssince(): (condition: boolean | number) => number {
|
|
|
100
114
|
};
|
|
101
115
|
}
|
|
102
116
|
|
|
103
|
-
/**
|
|
117
|
+
/**
|
|
118
|
+
* One `ta.cross(a, b)` call site: compared with the last call where both arguments were not na; false before such
|
|
119
|
+
* a call.
|
|
120
|
+
*/
|
|
104
121
|
export function cross(): (a: number, b: number) => boolean {
|
|
105
122
|
return twoSeriesSite((a, b, pa, pb) => (a > b && pa <= pb) || (a < b && pa >= pb));
|
|
106
123
|
}
|
|
@@ -176,3 +193,99 @@ export function lowest(): (value: number, length: number) => number {
|
|
|
176
193
|
export function highest(): (value: number, length: number) => number {
|
|
177
194
|
return extremeSite(false);
|
|
178
195
|
}
|
|
196
|
+
|
|
197
|
+
/** One `ta.lowest` / `ta.highest` call site whose history is kept by bar; rules in {@link lowestByBar}. */
|
|
198
|
+
function extremeByBarSite(isLowest: boolean): (barIndex: number, value: number, length: number) => number {
|
|
199
|
+
const better = isLowest ? (a: number, b: number) => a < b : (a: number, b: number) => a > b;
|
|
200
|
+
let ring: number[] = [];
|
|
201
|
+
let extreme = NaN;
|
|
202
|
+
let extremeAt = -1;
|
|
203
|
+
return (barIndex, value, length) => {
|
|
204
|
+
const len = Math.floor(length);
|
|
205
|
+
if (ring.length !== len + 1) {
|
|
206
|
+
ring = new Array<number>(len + 1).fill(0);
|
|
207
|
+
extreme = NaN;
|
|
208
|
+
}
|
|
209
|
+
ring[barIndex % (len + 1)] = value;
|
|
210
|
+
if (Number.isNaN(extreme) || better(value, extreme)) {
|
|
211
|
+
extreme = value;
|
|
212
|
+
extremeAt = barIndex;
|
|
213
|
+
} else if (barIndex - extremeAt >= len) {
|
|
214
|
+
extreme = NaN;
|
|
215
|
+
for (let b = barIndex - len + 1; b <= barIndex; b++) {
|
|
216
|
+
const v = ring[b % (len + 1)]!;
|
|
217
|
+
if (!Number.isNaN(v) && (Number.isNaN(extreme) || better(v, extreme))) {
|
|
218
|
+
extreme = v;
|
|
219
|
+
extremeAt = b;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return barIndex < len - 1 ? NaN : extreme;
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* One `ta.lowest(source, length)` call site in a conditional block whose history is kept by bar (not by call), called
|
|
229
|
+
* with `(barIndex, value, length)` on the bars where the Pine call runs.
|
|
230
|
+
*
|
|
231
|
+
* PineScript rules (measured on a call in a ternary branch, length 6):
|
|
232
|
+
* - the values are kept in a ring of `length + 1` slots indexed by the bar index, filled with 0 at the start and
|
|
233
|
+
* written only on the bars where the call runs: a bar before the first call counts as 0, and a bar without a call
|
|
234
|
+
* holds the value of the call `length + 1` bars earlier (0 if there was none)
|
|
235
|
+
* - on a call, the value replaces the kept extreme when there is none or when it is strictly lower; only otherwise,
|
|
236
|
+
* when the kept extreme is `length` bars old, the last `length` bars are read again from the ring (the oldest bar
|
|
237
|
+
* wins a tie)
|
|
238
|
+
* - na before bar `length - 1` (by bar, not by call)
|
|
239
|
+
*
|
|
240
|
+
* {@link lowest} keeps the history by call instead; both rules were measured, on different scripts.
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* ```typescript
|
|
244
|
+
* // Pine: lo = cond ? ta.lowest(low, 6) : na
|
|
245
|
+
* const site = callsite.lowestByBar();
|
|
246
|
+
* const lo = lows.map((v, i) => (cond[i] ? site(i, v, 6) : NaN));
|
|
247
|
+
* ```
|
|
248
|
+
*/
|
|
249
|
+
export function lowestByBar(): (barIndex: number, value: number, length: number) => number {
|
|
250
|
+
return extremeByBarSite(true);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* One `ta.highest(source, length)` call site in a conditional block whose history is kept by bar, called with
|
|
255
|
+
* `(barIndex, value, length)`. Same rules as {@link lowestByBar} (a value replaces the kept extreme when it is
|
|
256
|
+
* strictly higher).
|
|
257
|
+
*/
|
|
258
|
+
export function highestByBar(): (barIndex: number, value: number, length: number) => number {
|
|
259
|
+
return extremeByBarSite(false);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* One `ta.linreg(source, length, offset)` call site in a conditional block, called with
|
|
264
|
+
* `(barIndex, value, length, offset)` on the bars where the Pine call runs.
|
|
265
|
+
*
|
|
266
|
+
* PineScript rules (measured): `ta.linreg` keeps the values it receives in a ring of `length + 1` slots indexed by the
|
|
267
|
+
* bar index, filled with 0 at the start and written only on the bars where it is called. The window is read from the
|
|
268
|
+
* ring for the last `length` bars: a bar before the first call counts as 0, and a bar without a call holds the value of
|
|
269
|
+
* the call `length + 1` bars earlier (0 if there was none). The result is na before bar `length - 1`. Called on every
|
|
270
|
+
* bar, it equals `ta.linreg`.
|
|
271
|
+
*
|
|
272
|
+
* @example
|
|
273
|
+
* ```typescript
|
|
274
|
+
* // Pine: if bar_index >= 20
|
|
275
|
+
* // a := ta.linreg(close, 10, 0)
|
|
276
|
+
* const site = callsite.linreg();
|
|
277
|
+
* const a = closes.map((c, i) => (i >= 20 ? site(i, c, 10, 0) : NaN));
|
|
278
|
+
* ```
|
|
279
|
+
*/
|
|
280
|
+
export function linreg(): (barIndex: number, value: number, length: number, offset?: number) => number {
|
|
281
|
+
let ring: number[] = [];
|
|
282
|
+
return (barIndex, value, length, offset = 0) => {
|
|
283
|
+
const len = Math.floor(length);
|
|
284
|
+
if (ring.length !== len + 1) ring = new Array<number>(len + 1).fill(0);
|
|
285
|
+
ring[barIndex % (len + 1)] = value;
|
|
286
|
+
if (barIndex < len - 1) return NaN;
|
|
287
|
+
const window = Array.from({ length: len }, (_, k) => ring[(barIndex - len + 1 + k) % (len + 1)]!);
|
|
288
|
+
return linregSeries(window, len, offset)[len - 1]!;
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
package/src/index.ts
CHANGED
|
@@ -164,7 +164,7 @@ export type { LightweightSeriesDefinitions } from './runtime/adapters/Lightweigh
|
|
|
164
164
|
export { SimpleInputAdapter } from './runtime/adapters/SimpleInputAdapter.js';
|
|
165
165
|
|
|
166
166
|
// Version
|
|
167
|
-
export const VERSION = '0.9.
|
|
167
|
+
export const VERSION = '0.9.5';
|
|
168
168
|
|
|
169
169
|
/**
|
|
170
170
|
* Library information
|
package/src/math/index.ts
CHANGED
|
@@ -952,12 +952,13 @@ export function random(min?: float, max?: float, _seed?: int): float {
|
|
|
952
952
|
* Returns the sign of a number.
|
|
953
953
|
*
|
|
954
954
|
* @param value - The number to check or Series
|
|
955
|
-
* @returns 1 for positive, -1 for negative, 0 for zero
|
|
955
|
+
* @returns 1 for positive, -1 for negative, 0 for zero, na for na
|
|
956
956
|
*
|
|
957
957
|
* @remarks
|
|
958
958
|
* - Returns 1 if value > 0
|
|
959
959
|
* - Returns -1 if value < 0
|
|
960
960
|
* - Returns 0 if value = 0
|
|
961
|
+
* - Returns na if value is na (as PineScript)
|
|
961
962
|
* - Useful for determining direction or polarity
|
|
962
963
|
* - If value is a Series, returns a Series; if value is a number, returns a number
|
|
963
964
|
*
|
|
@@ -967,6 +968,7 @@ export function random(min?: float, max?: float, _seed?: int): float {
|
|
|
967
968
|
* math.sign(-3) // Returns: -1
|
|
968
969
|
* math.sign(0) // Returns: 0
|
|
969
970
|
* math.sign(0.001) // Returns: 1
|
|
971
|
+
* math.sign(NaN) // Returns: NaN
|
|
970
972
|
* ```
|
|
971
973
|
*/
|
|
972
974
|
export function sign(value: Series): Series;
|
|
@@ -980,13 +982,13 @@ export function sign(value: float | Series): int | Series {
|
|
|
980
982
|
const result: number[] = [];
|
|
981
983
|
for (let i = 0; i < length; i++) {
|
|
982
984
|
const v = valueArray[i] ?? NaN;
|
|
983
|
-
result.push(v > 0 ? 1 : v < 0 ? -1 : 0);
|
|
985
|
+
result.push(v > 0 ? 1 : v < 0 ? -1 : v === 0 ? 0 : NaN);
|
|
984
986
|
}
|
|
985
987
|
|
|
986
988
|
return Series.fromArray(bars, result);
|
|
987
989
|
}
|
|
988
990
|
|
|
989
|
-
return value > 0 ? 1 : value < 0 ? -1 : 0;
|
|
991
|
+
return value > 0 ? 1 : value < 0 ? -1 : value === 0 ? 0 : NaN;
|
|
990
992
|
}
|
|
991
993
|
|
|
992
994
|
/**
|
package/src/ta/index.ts
CHANGED
|
@@ -74,26 +74,21 @@ export function ema(source: Source, length: simple_int): series_float {
|
|
|
74
74
|
const len = Math.floor(length);
|
|
75
75
|
const multiplier = 2 / (len + 1);
|
|
76
76
|
|
|
77
|
-
// SMA seed
|
|
78
|
-
// An infinite value is na, as in PineScript.
|
|
77
|
+
// SMA seed on the bar of the `len`-th valid value. An infinite value is na, as in PineScript.
|
|
79
78
|
let firstValidIndex = -1;
|
|
80
79
|
let validCount = 0;
|
|
81
|
-
let initSum = 0;
|
|
82
80
|
|
|
83
81
|
for (let i = 0; i < source.length; i++) {
|
|
84
82
|
const val = source[i];
|
|
85
|
-
if (val !== undefined && Number.isFinite(val)) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (validCount === len) {
|
|
89
|
-
firstValidIndex = i;
|
|
90
|
-
break;
|
|
91
|
-
}
|
|
83
|
+
if (val !== undefined && Number.isFinite(val) && ++validCount === len) {
|
|
84
|
+
firstValidIndex = i;
|
|
85
|
+
break;
|
|
92
86
|
}
|
|
93
87
|
}
|
|
94
88
|
|
|
95
|
-
let emaValue = validCount > 0 ? initSum / validCount : NaN;
|
|
96
89
|
const emaInitialized = firstValidIndex >= 0;
|
|
90
|
+
// The seed is ta.sma on that bar: the running sum of math.sum (PineScript bits), not a plain loop sum
|
|
91
|
+
let emaValue = emaInitialized ? runningSum(source.slice(0, firstValidIndex + 1), len)[firstValidIndex]! / len : NaN;
|
|
97
92
|
|
|
98
93
|
for (let i = 0; i < source.length; i++) {
|
|
99
94
|
if (!emaInitialized || i < firstValidIndex) {
|
|
@@ -350,6 +345,32 @@ function extremeValue(source: Source, length: simple_int, lowest: boolean): seri
|
|
|
350
345
|
});
|
|
351
346
|
}
|
|
352
347
|
|
|
348
|
+
/**
|
|
349
|
+
* Compares each bar with the last earlier bar where both values were not na (PineScript rule for ta.crossover,
|
|
350
|
+
* ta.crossunder and ta.cross): false while there is no such bar, and false on a bar with an na value.
|
|
351
|
+
*
|
|
352
|
+
* @internal
|
|
353
|
+
*/
|
|
354
|
+
function crossTest(
|
|
355
|
+
series1: Source,
|
|
356
|
+
series2: Source,
|
|
357
|
+
test: (a: number, b: number, prevA: number, prevB: number) => boolean,
|
|
358
|
+
): series_bool {
|
|
359
|
+
const result: series_bool = [];
|
|
360
|
+
let prevA = NaN;
|
|
361
|
+
let prevB = NaN;
|
|
362
|
+
for (let i = 0; i < series1.length; i++) {
|
|
363
|
+
const a = series1[i]!;
|
|
364
|
+
const b = series2[i]!;
|
|
365
|
+
result.push(test(a, b, prevA, prevB));
|
|
366
|
+
if (!Number.isNaN(a) && !Number.isNaN(b)) {
|
|
367
|
+
prevA = a;
|
|
368
|
+
prevB = b;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return result;
|
|
372
|
+
}
|
|
373
|
+
|
|
353
374
|
/**
|
|
354
375
|
* Crossover - returns true when series1 crosses over series2 (moves from below to above).
|
|
355
376
|
*
|
|
@@ -358,8 +379,10 @@ function extremeValue(source: Source, length: simple_int, lowest: boolean): seri
|
|
|
358
379
|
* @returns Boolean series (true at crossover points)
|
|
359
380
|
*
|
|
360
381
|
* @remarks
|
|
361
|
-
* - True when: series1[i]
|
|
382
|
+
* - True when: series1[i] > series2[i] AND series1[j] <= series2[j], where j is the last bar before i where both
|
|
383
|
+
* values were not na (PineScript rule: a crossing right after an na bar is not missed)
|
|
362
384
|
* - First value is always false (no previous value to compare)
|
|
385
|
+
* - Comparisons are exact (no tolerance)
|
|
363
386
|
* - Useful for detecting bullish signals (e.g., fast MA crossing over slow MA)
|
|
364
387
|
*
|
|
365
388
|
* @example
|
|
@@ -368,17 +391,7 @@ function extremeValue(source: Source, length: simple_int, lowest: boolean): seri
|
|
|
368
391
|
* ```
|
|
369
392
|
*/
|
|
370
393
|
export function crossover(series1: Source, series2: Source): series_bool {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
for (let i = 0; i < series1.length; i++) {
|
|
374
|
-
if (i === 0) {
|
|
375
|
-
result.push(false);
|
|
376
|
-
} else {
|
|
377
|
-
result.push(series1[i]! > series2[i]! && series1[i - 1]! <= series2[i - 1]!);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
return result;
|
|
394
|
+
return crossTest(series1, series2, (a, b, pa, pb) => a > b && pa <= pb);
|
|
382
395
|
}
|
|
383
396
|
|
|
384
397
|
/**
|
|
@@ -389,8 +402,10 @@ export function crossover(series1: Source, series2: Source): series_bool {
|
|
|
389
402
|
* @returns Boolean series (true at crossunder points)
|
|
390
403
|
*
|
|
391
404
|
* @remarks
|
|
392
|
-
* - True when: series1[i]
|
|
405
|
+
* - True when: series1[i] < series2[i] AND series1[j] >= series2[j], where j is the last bar before i where both
|
|
406
|
+
* values were not na
|
|
393
407
|
* - First value is always false (no previous value to compare)
|
|
408
|
+
* - Comparisons are exact (no tolerance)
|
|
394
409
|
* - Useful for detecting bearish signals (e.g., fast MA crossing under slow MA)
|
|
395
410
|
*
|
|
396
411
|
* @example
|
|
@@ -399,17 +414,7 @@ export function crossover(series1: Source, series2: Source): series_bool {
|
|
|
399
414
|
* ```
|
|
400
415
|
*/
|
|
401
416
|
export function crossunder(series1: Source, series2: Source): series_bool {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
for (let i = 0; i < series1.length; i++) {
|
|
405
|
-
if (i === 0) {
|
|
406
|
-
result.push(false);
|
|
407
|
-
} else {
|
|
408
|
-
result.push(series1[i]! < series2[i]! && series1[i - 1]! >= series2[i - 1]!);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
return result;
|
|
417
|
+
return crossTest(series1, series2, (a, b, pa, pb) => a < b && pa >= pb);
|
|
413
418
|
}
|
|
414
419
|
|
|
415
420
|
/**
|
|
@@ -859,8 +864,9 @@ export function cum(source: Source): series_float {
|
|
|
859
864
|
* @returns Boolean series (true at cross points)
|
|
860
865
|
*
|
|
861
866
|
* @remarks
|
|
862
|
-
* - True when: (source1[i]
|
|
863
|
-
* (source1[i]
|
|
867
|
+
* - True when: (source1[i] > source2[i] AND source1[j] <= source2[j]) OR
|
|
868
|
+
* (source1[i] < source2[i] AND source1[j] >= source2[j]),
|
|
869
|
+
* where j is the last bar before i where both values were not na
|
|
864
870
|
* - First value is always false (no previous value to compare)
|
|
865
871
|
* - Detects any crossing (either over or under)
|
|
866
872
|
* - Use `ta.crossover()` or `ta.crossunder()` for directional crosses
|
|
@@ -872,19 +878,7 @@ export function cum(source: Source): series_float {
|
|
|
872
878
|
* ```
|
|
873
879
|
*/
|
|
874
880
|
export function cross(source1: Source, source2: Source): series_bool {
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
for (let i = 0; i < source1.length; i++) {
|
|
878
|
-
if (i === 0) {
|
|
879
|
-
result.push(false);
|
|
880
|
-
} else {
|
|
881
|
-
const crossedUp = source1[i]! > source2[i]! && source1[i - 1]! <= source2[i - 1]!;
|
|
882
|
-
const crossedDown = source1[i]! < source2[i]! && source1[i - 1]! >= source2[i - 1]!;
|
|
883
|
-
result.push(crossedUp || crossedDown);
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
return result;
|
|
881
|
+
return crossTest(source1, source2, (a, b, pa, pb) => (a > b && pa <= pb) || (a < b && pa >= pb));
|
|
888
882
|
}
|
|
889
883
|
|
|
890
884
|
/**
|
|
@@ -1428,7 +1422,7 @@ export function correlation(source1: Source, source2: Source, length: simple_int
|
|
|
1428
1422
|
* - Useful for identifying relative strength within a period
|
|
1429
1423
|
*
|
|
1430
1424
|
* PineScript rules:
|
|
1431
|
-
* - percentrank = count(source[k] <= source, k = 1..length) / length
|
|
1425
|
+
* - percentrank = count(source[k] <= source, k = 1..length) * 100 / length (in this order: the PineScript bits)
|
|
1432
1426
|
* - a value from bar `length` on, when the current source is not `na`; an `na` value in the window counts as not
|
|
1433
1427
|
* `<=` the current value
|
|
1434
1428
|
* - the comparison uses the 1e-10 tolerance
|
|
@@ -1454,7 +1448,7 @@ export function percentrank(source: Source, length: simple_int): series_float {
|
|
|
1454
1448
|
for (let j = 1; j <= length; j++) {
|
|
1455
1449
|
if (le(source[i - j]!, currentValue)) countLessOrEqual++;
|
|
1456
1450
|
}
|
|
1457
|
-
result.push((countLessOrEqual
|
|
1451
|
+
result.push((countLessOrEqual * 100) / length);
|
|
1458
1452
|
}
|
|
1459
1453
|
|
|
1460
1454
|
return result;
|
|
@@ -2078,7 +2072,8 @@ export function tsi(source: Source, shortLength: simple_int, longLength: simple_
|
|
|
2078
2072
|
* - CMO > +50: overbought conditions
|
|
2079
2073
|
* - CMO < -50: oversold conditions
|
|
2080
2074
|
* - Unlike RSI, CMO uses sum of gains/losses instead of averages
|
|
2081
|
-
* -
|
|
2075
|
+
* - As in PineScript: the sums are the running sums of `math.sum` (bit for bit), and a flat window (every change 0)
|
|
2076
|
+
* is `0 / 0` = na
|
|
2082
2077
|
* - More volatile than RSI
|
|
2083
2078
|
*
|
|
2084
2079
|
* @example
|
|
@@ -2089,37 +2084,20 @@ export function tsi(source: Source, shortLength: simple_int, longLength: simple_
|
|
|
2089
2084
|
* ```
|
|
2090
2085
|
*/
|
|
2091
2086
|
export function cmo(source: Source, length: simple_int): series_float {
|
|
2092
|
-
|
|
2093
|
-
|
|
2087
|
+
// PineScript definition: mom = ta.change(src), sm1 = math.sum(mom >= 0 ? mom : 0, length),
|
|
2088
|
+
// sm2 = math.sum(mom >= 0 ? 0 : -mom, length), cmo = 100 * (sm1 - sm2) / (sm1 + sm2), with the running sums of
|
|
2089
|
+
// math.sum (bit for bit). A comparison with na is false: an na change adds 0 to sm1 and na (skipped) to sm2.
|
|
2090
|
+
const up: number[] = [];
|
|
2091
|
+
const down: number[] = [];
|
|
2094
2092
|
for (let i = 0; i < source.length; i++) {
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
for (let j = 0; j < length; j++) {
|
|
2104
|
-
const change = source[i - j]! - source[i - j - 1]!;
|
|
2105
|
-
if (change > 0) {
|
|
2106
|
-
sumGains += change;
|
|
2107
|
-
} else {
|
|
2108
|
-
sumLosses += Math.abs(change);
|
|
2109
|
-
}
|
|
2110
|
-
}
|
|
2111
|
-
|
|
2112
|
-
// PineScript: 100 * (sm1 - sm2) / (sm1 + sm2), so a flat window (0 / 0) is na
|
|
2113
|
-
const totalMovement = sumGains + sumLosses;
|
|
2114
|
-
if (totalMovement === 0) {
|
|
2115
|
-
result.push(NaN);
|
|
2116
|
-
} else {
|
|
2117
|
-
const cmoValue = ((sumGains - sumLosses) / totalMovement) * 100;
|
|
2118
|
-
result.push(cmoValue);
|
|
2119
|
-
}
|
|
2120
|
-
}
|
|
2121
|
-
|
|
2122
|
-
return result;
|
|
2093
|
+
const mom = i > 0 ? source[i]! - source[i - 1]! : NaN;
|
|
2094
|
+
up.push(mom >= 0 ? mom : 0);
|
|
2095
|
+
down.push(mom >= 0 ? 0 : -mom);
|
|
2096
|
+
}
|
|
2097
|
+
const sm1 = runningSum(up, length);
|
|
2098
|
+
const sm2 = runningSum(down, length);
|
|
2099
|
+
// a flat window is 0 / 0 = na
|
|
2100
|
+
return sm1.map((s1, i) => (100 * (s1 - sm2[i]!)) / (s1 + sm2[i]!));
|
|
2123
2101
|
}
|
|
2124
2102
|
|
|
2125
2103
|
/**
|
package/src/ta/running-sum.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* - each bar first removes, with a Kahan step, the compensated value that was added for the value that leaves the
|
|
7
7
|
* window, then adds the new value with a Kahan step and keeps its compensated value;
|
|
8
8
|
* - when the compensation of the previous step would be lost when applied to the new value, the sum is computed
|
|
9
|
-
* again from the window values (newest first) and the compensation is reset.
|
|
9
|
+
* again from the window values (newest first) and the compensation is reset. The test rounds on the grid of the
|
|
10
|
+
* new value toward +infinity: for a negative power of two, the finer grid of the binade below (#136).
|
|
10
11
|
*
|
|
11
12
|
* So the result can differ from the exact window sum in the last bits (e.g. a small residue on a window of zeros),
|
|
12
13
|
* as in PineScript.
|
|
@@ -23,18 +24,29 @@ function exponentOf(x: number): number {
|
|
|
23
24
|
return e === 0 ? -1074 : e - 1023;
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Distance from a finite non-zero `x` to the next number toward +infinity: the spacing of the binade of `x`, or half
|
|
29
|
+
* of it when `x` is a negative power of two (the next number up is in the binade below).
|
|
30
|
+
*/
|
|
31
|
+
function spacingUp(x: number): number {
|
|
32
|
+
const e = exponentOf(x);
|
|
33
|
+
const powerOfTwo = (view.getUint16(0) & 0xf) === 0 && view.getUint16(2) === 0 && view.getUint32(4) === 0;
|
|
34
|
+
return 2 ** (e - (x < 0 && powerOfTwo && e > -1022 ? 53 : 52));
|
|
35
|
+
}
|
|
36
|
+
|
|
26
37
|
/**
|
|
27
38
|
* Whether the compensation `c` is (partly) lost when it is applied to `x`: `x - c` rounded on the grid of `x`
|
|
28
|
-
* (spacing
|
|
39
|
+
* (spacing {@link spacingUp}, ties to an even mantissa) applies less than `c`.
|
|
29
40
|
*/
|
|
30
41
|
function compensationLost(x: number, c: number): boolean {
|
|
31
42
|
if (c === 0 || x === 0) return false;
|
|
32
|
-
const ulp =
|
|
43
|
+
const ulp = spacingUp(x);
|
|
33
44
|
const q = c / ulp;
|
|
34
45
|
const mantissa = x / ulp;
|
|
35
46
|
const f = Math.floor(q);
|
|
36
47
|
const d = q - f;
|
|
37
|
-
|
|
48
|
+
// parity of mantissa - f from each part: the mantissa can be 2^53 (half spacing), where mantissa - f is not exact
|
|
49
|
+
const applied = d > 0.5 ? f + 1 : d < 0.5 ? f : Math.abs(mantissa % 2) === Math.abs(f % 2) ? f : f + 1;
|
|
38
50
|
return Math.abs(applied) < Math.abs(q);
|
|
39
51
|
}
|
|
40
52
|
|