@hoox-sh/pynets 0.2.0 → 0.4.0
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/CHANGELOG.md +56 -0
- package/README.md +21 -12
- package/dist/browser.js +89 -84
- package/dist/index.js +21011 -12972
- package/package.json +1 -1
- package/src/ast/builder.ts +235 -12
- package/src/ast/nodes.ts +114 -4
- package/src/ast/unparse.ts +192 -17
- package/src/cli/rich.ts +1 -1
- package/src/generated/PinescriptLexer.interp +5 -1
- package/src/generated/PinescriptLexer.tokens +97 -95
- package/src/generated/PinescriptLexer.ts +512 -502
- package/src/generated/PinescriptParser.interp +8 -1
- package/src/generated/PinescriptParser.tokens +97 -95
- package/src/generated/PinescriptParser.ts +1907 -1287
- package/src/generated/PinescriptParserVisitor.ts +36 -1
- package/src/index.ts +1 -0
- package/src/runtime/array.ts +238 -68
- package/src/runtime/compile/collections.ts +191 -34
- package/src/runtime/compile/draw.ts +609 -19
- package/src/runtime/compile/emit.ts +346 -9
- package/src/runtime/compile/emit_call.ts +737 -44
- package/src/runtime/compile/runtime.ts +82 -2
- package/src/runtime/compile/strategy.ts +339 -6
- package/src/runtime/compile/types.ts +2 -0
- package/src/runtime/drawings.ts +405 -8
- package/src/runtime/interpret.ts +2352 -277
- package/src/runtime/matrix.ts +371 -42
- package/src/runtime/strategy.ts +695 -33
- package/src/runtime/ta.ts +1046 -20
- package/src/runtime/timeframe.ts +199 -0
package/src/runtime/ta.ts
CHANGED
|
@@ -55,6 +55,12 @@ interface HighestLowestState {
|
|
|
55
55
|
window: Cell[];
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
interface AroonState {
|
|
59
|
+
length: number;
|
|
60
|
+
highs: Cell[];
|
|
61
|
+
lows: Cell[];
|
|
62
|
+
}
|
|
63
|
+
|
|
58
64
|
interface StdevState {
|
|
59
65
|
period: number;
|
|
60
66
|
window: Cell[];
|
|
@@ -249,7 +255,80 @@ interface VolIndexState {
|
|
|
249
255
|
}
|
|
250
256
|
|
|
251
257
|
interface WvadState {
|
|
252
|
-
|
|
258
|
+
period: number;
|
|
259
|
+
vols: number[];
|
|
260
|
+
volSum: number;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
interface CmfState {
|
|
264
|
+
period: number;
|
|
265
|
+
mf: number[];
|
|
266
|
+
vol: number[];
|
|
267
|
+
mfSum: number;
|
|
268
|
+
volSum: number;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
interface KlingerState {
|
|
272
|
+
prev: Cell;
|
|
273
|
+
cum: number;
|
|
274
|
+
started: boolean;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
interface UoState {
|
|
278
|
+
n1: number;
|
|
279
|
+
n2: number;
|
|
280
|
+
n3: number;
|
|
281
|
+
prev: Cell;
|
|
282
|
+
window: Array<[Cell, Cell]>;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
interface RciState {
|
|
286
|
+
length: number;
|
|
287
|
+
window: Cell[];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
interface DpoState {
|
|
291
|
+
length: number;
|
|
292
|
+
window: Cell[];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
interface KstState {
|
|
296
|
+
key: string;
|
|
297
|
+
window: Cell[];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
interface StochRsiState {
|
|
301
|
+
rsiLen: number;
|
|
302
|
+
stochLen: number;
|
|
303
|
+
prices: number[];
|
|
304
|
+
rsiRing: number[];
|
|
305
|
+
bars: number;
|
|
306
|
+
validRsi: number;
|
|
307
|
+
signal: number | null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
interface BbPctState {
|
|
311
|
+
length: number;
|
|
312
|
+
window: Cell[];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
interface EmvState {
|
|
316
|
+
length: number;
|
|
317
|
+
prevH: Cell;
|
|
318
|
+
prevL: Cell;
|
|
319
|
+
started: boolean;
|
|
320
|
+
valid: number[];
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
interface FractalState {
|
|
324
|
+
period: number;
|
|
325
|
+
highs: Cell[];
|
|
326
|
+
lows: Cell[];
|
|
327
|
+
bars: number;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
interface ZigzagState {
|
|
331
|
+
samples: Cell[];
|
|
253
332
|
}
|
|
254
333
|
|
|
255
334
|
export type PivotPoints = {
|
|
@@ -264,6 +343,25 @@ function finiteCell(value: Cell): Cell {
|
|
|
264
343
|
return value !== null && Number.isFinite(value) ? value : null;
|
|
265
344
|
}
|
|
266
345
|
|
|
346
|
+
/** Plain dict returned by `ta.*` handlers. Attribute reads are brand-gated. */
|
|
347
|
+
export interface TaRecord {
|
|
348
|
+
readonly __taRecord: true;
|
|
349
|
+
[key: string]: number | null | boolean | string;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function taRec(fields: Record<string, number | null | boolean | string>): TaRecord {
|
|
353
|
+
return { __taRecord: true, ...fields };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function pushCap<T>(window: T[], value: T, cap: number): void {
|
|
357
|
+
if (window.length === cap) window.shift();
|
|
358
|
+
window.push(value);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function finiteOut(value: number): Cell {
|
|
362
|
+
return Number.isFinite(value) ? value : null;
|
|
363
|
+
}
|
|
364
|
+
|
|
267
365
|
/** Near-integer floats round (14.0000000001 → 14); else floor. Python `_float_to_period_int`. */
|
|
268
366
|
const PERIOD_INT_EPS = 1e-9;
|
|
269
367
|
|
|
@@ -349,6 +447,18 @@ export class TaEngine {
|
|
|
349
447
|
private readonly nviSites = new Map<string, VolIndexState>();
|
|
350
448
|
private readonly pviSites = new Map<string, VolIndexState>();
|
|
351
449
|
private readonly wvadSites = new Map<string, WvadState>();
|
|
450
|
+
private readonly cmfSites = new Map<string, CmfState>();
|
|
451
|
+
private readonly klingerSites = new Map<string, KlingerState>();
|
|
452
|
+
private readonly aroonSites = new Map<string, AroonState>();
|
|
453
|
+
private readonly uoSites = new Map<string, UoState>();
|
|
454
|
+
private readonly rciSites = new Map<string, RciState>();
|
|
455
|
+
private readonly dpoSites = new Map<string, DpoState>();
|
|
456
|
+
private readonly kstSites = new Map<string, KstState>();
|
|
457
|
+
private readonly stochRsiSites = new Map<string, StochRsiState>();
|
|
458
|
+
private readonly bbPctSites = new Map<string, BbPctState>();
|
|
459
|
+
private readonly emvSites = new Map<string, EmvState>();
|
|
460
|
+
private readonly fractalSites = new Map<string, FractalState>();
|
|
461
|
+
private readonly zigzagSites = new Map<string, ZigzagState>();
|
|
352
462
|
|
|
353
463
|
sma(site: string, source: Cell, period: number): Cell {
|
|
354
464
|
const n = pinePeriod(period);
|
|
@@ -1597,10 +1707,11 @@ export class TaEngine {
|
|
|
1597
1707
|
}
|
|
1598
1708
|
|
|
1599
1709
|
/**
|
|
1600
|
-
* Williams AD. First bar 0
|
|
1601
|
-
* -(high-close). H/L
|
|
1710
|
+
* Williams AD (`_wad_inc_update`). First bar 0. Later: up adds
|
|
1711
|
+
* vol*(close-low); down subtracts vol*(high-close). Missing H/L
|
|
1712
|
+
* fallback to close; vol na → 0.
|
|
1602
1713
|
*/
|
|
1603
|
-
wad(site: string, high: Cell, low: Cell, close: Cell): Cell {
|
|
1714
|
+
wad(site: string, high: Cell, low: Cell, close: Cell, volume: Cell): Cell {
|
|
1604
1715
|
let st = this.wadSites.get(site);
|
|
1605
1716
|
if (st === undefined) {
|
|
1606
1717
|
st = { wad: 0, prev: null, started: false };
|
|
@@ -1609,18 +1720,21 @@ export class TaEngine {
|
|
|
1609
1720
|
const h = finiteCell(high);
|
|
1610
1721
|
const l = finiteCell(low);
|
|
1611
1722
|
const c = finiteCell(close);
|
|
1612
|
-
|
|
1723
|
+
const vv = finiteCell(volume) ?? 0;
|
|
1613
1724
|
if (!st.started) {
|
|
1614
1725
|
st.started = true;
|
|
1615
1726
|
st.prev = c;
|
|
1616
|
-
|
|
1727
|
+
st.wad = 0;
|
|
1728
|
+
return 0;
|
|
1617
1729
|
}
|
|
1618
1730
|
const prev = st.prev;
|
|
1619
|
-
if (prev !== null) {
|
|
1620
|
-
|
|
1621
|
-
|
|
1731
|
+
if (c !== null && prev !== null) {
|
|
1732
|
+
const hi = h ?? c;
|
|
1733
|
+
const lo = l ?? c;
|
|
1734
|
+
if (c > prev) st.wad += vv * (c - lo);
|
|
1735
|
+
else if (c < prev) st.wad -= vv * (hi - c);
|
|
1622
1736
|
}
|
|
1623
|
-
st.prev = c;
|
|
1737
|
+
if (c !== null) st.prev = c;
|
|
1624
1738
|
return st.wad;
|
|
1625
1739
|
}
|
|
1626
1740
|
|
|
@@ -1684,24 +1798,90 @@ export class TaEngine {
|
|
|
1684
1798
|
}
|
|
1685
1799
|
|
|
1686
1800
|
/**
|
|
1687
|
-
* Williams
|
|
1688
|
-
*
|
|
1801
|
+
* Williams Volume AD (`_wvad_inc_update`): last WAD / rolling volume sum.
|
|
1802
|
+
* period<=0 or zero vol sum → 0. Nested WAD uses `${site}:wad`.
|
|
1689
1803
|
*/
|
|
1690
|
-
wvad(site: string,
|
|
1804
|
+
wvad(site: string, high: Cell, low: Cell, close: Cell, volume: Cell, period: number): Cell {
|
|
1805
|
+
const wad = this.wad(`${site}:wad`, high, low, close, volume);
|
|
1806
|
+
const n = Number.isFinite(period) ? Math.trunc(period) : 0;
|
|
1807
|
+
if (n <= 0) return 0;
|
|
1691
1808
|
let st = this.wvadSites.get(site);
|
|
1692
|
-
if (st === undefined) {
|
|
1693
|
-
st = {
|
|
1809
|
+
if (st === undefined || st.period !== n) {
|
|
1810
|
+
st = { period: n, vols: [], volSum: 0 };
|
|
1694
1811
|
this.wvadSites.set(site, st);
|
|
1695
1812
|
}
|
|
1696
|
-
const
|
|
1813
|
+
const vv = finiteCell(volume) ?? 0;
|
|
1814
|
+
if (st.vols.length === n) st.volSum -= st.vols.shift()!;
|
|
1815
|
+
st.vols.push(vv);
|
|
1816
|
+
st.volSum += vv;
|
|
1817
|
+
if (st.volSum > 0 && wad !== null) return wad / st.volSum;
|
|
1818
|
+
return 0;
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
/**
|
|
1822
|
+
* Chaikin Money Flow (`_cmf_inc_update`). CLV*vol rolling window.
|
|
1823
|
+
* period<=0 → na; zero vol sum → 0; missing H/L/C → clv 0.
|
|
1824
|
+
*/
|
|
1825
|
+
cmf(site: string, high: Cell, low: Cell, close: Cell, volume: Cell, period: number): Cell {
|
|
1826
|
+
const n = Number.isFinite(period) ? Math.trunc(period) : 0;
|
|
1827
|
+
if (n <= 0) return null;
|
|
1828
|
+
let st = this.cmfSites.get(site);
|
|
1829
|
+
if (st === undefined || st.period !== n) {
|
|
1830
|
+
st = { period: n, mf: [], vol: [], mfSum: 0, volSum: 0 };
|
|
1831
|
+
this.cmfSites.set(site, st);
|
|
1832
|
+
}
|
|
1697
1833
|
const h = finiteCell(high);
|
|
1698
1834
|
const l = finiteCell(low);
|
|
1699
1835
|
const c = finiteCell(close);
|
|
1700
|
-
if (o === null || h === null || l === null || c === null) return st.value;
|
|
1701
|
-
const rng = h - l;
|
|
1702
1836
|
const vv = finiteCell(volume) ?? 0;
|
|
1703
|
-
|
|
1704
|
-
|
|
1837
|
+
let clv = 0;
|
|
1838
|
+
if (h !== null && l !== null && c !== null) {
|
|
1839
|
+
const rng = h - l;
|
|
1840
|
+
clv = rng === 0 ? 0 : ((c - l) - (h - c)) / rng;
|
|
1841
|
+
}
|
|
1842
|
+
const mf = clv * vv;
|
|
1843
|
+
if (st.mf.length === n) {
|
|
1844
|
+
st.mfSum -= st.mf.shift()!;
|
|
1845
|
+
st.volSum -= st.vol.shift()!;
|
|
1846
|
+
}
|
|
1847
|
+
st.mf.push(mf);
|
|
1848
|
+
st.vol.push(vv);
|
|
1849
|
+
st.mfSum += mf;
|
|
1850
|
+
st.volSum += vv;
|
|
1851
|
+
return st.volSum > 0 ? st.mfSum / st.volSum : 0;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
/**
|
|
1855
|
+
* Klinger oscillator (`_klinger_inc_update`): signed-volume cumulant,
|
|
1856
|
+
* then EMA(fast)−EMA(slow) via `${site}:fast` / `${site}:slow`.
|
|
1857
|
+
*/
|
|
1858
|
+
klinger(site: string, close: Cell, volume: Cell, fast: number, slow: number): Cell {
|
|
1859
|
+
let st = this.klingerSites.get(site);
|
|
1860
|
+
if (st === undefined) {
|
|
1861
|
+
st = { prev: null, cum: 0, started: false };
|
|
1862
|
+
this.klingerSites.set(site, st);
|
|
1863
|
+
}
|
|
1864
|
+
const c = finiteCell(close);
|
|
1865
|
+
const vv = finiteCell(volume) ?? 0;
|
|
1866
|
+
if (!st.started) {
|
|
1867
|
+
st.started = true;
|
|
1868
|
+
st.prev = c;
|
|
1869
|
+
st.cum = 0;
|
|
1870
|
+
} else {
|
|
1871
|
+
const prev = st.prev;
|
|
1872
|
+
let trv = 0;
|
|
1873
|
+
if (c !== null && prev !== null) {
|
|
1874
|
+
if (c > prev) trv = vv;
|
|
1875
|
+
else if (c < prev) trv = -vv;
|
|
1876
|
+
}
|
|
1877
|
+
st.cum += trv;
|
|
1878
|
+
if (c !== null) st.prev = c;
|
|
1879
|
+
}
|
|
1880
|
+
const fastV = this.ema(`${site}:fast`, st.cum, fast);
|
|
1881
|
+
const slowV = this.ema(`${site}:slow`, st.cum, slow);
|
|
1882
|
+
if (fastV === null || slowV === null) return null;
|
|
1883
|
+
const out = fastV - slowV;
|
|
1884
|
+
return Number.isFinite(out) ? out : null;
|
|
1705
1885
|
}
|
|
1706
1886
|
|
|
1707
1887
|
/**
|
|
@@ -2090,4 +2270,850 @@ export class TaEngine {
|
|
|
2090
2270
|
min(site: string, source: Cell, period: number): Cell {
|
|
2091
2271
|
return this.lowest(site, source, period);
|
|
2092
2272
|
}
|
|
2273
|
+
|
|
2274
|
+
/**
|
|
2275
|
+
* Awesome Oscillator: SMA(hl2, fast) − SMA(hl2, slow). Default 5 / 34.
|
|
2276
|
+
* Warm-up until the slow window is full (`_builtin_ta_ao`).
|
|
2277
|
+
*/
|
|
2278
|
+
ao(site: string, high: Cell, low: Cell, fast = 5, slow = 34): Cell {
|
|
2279
|
+
const f = pinePeriod(fast);
|
|
2280
|
+
const s = pinePeriod(slow);
|
|
2281
|
+
if (f === null || s === null) return null;
|
|
2282
|
+
const h = finiteCell(high);
|
|
2283
|
+
const l = finiteCell(low);
|
|
2284
|
+
const hl2 = h !== null && l !== null ? (h + l) / 2 : null;
|
|
2285
|
+
const fv = this.sma(`${site}:ao:fast:${f}`, hl2, f);
|
|
2286
|
+
const sv = this.sma(`${site}:ao:slow:${s}`, hl2, s);
|
|
2287
|
+
if (fv === null || sv === null) return null;
|
|
2288
|
+
const out = fv - sv;
|
|
2289
|
+
return Number.isFinite(out) ? out : null;
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
/**
|
|
2293
|
+
* Aroon pair `{down, up}`. Window is `length + 1`; ties keep the oldest
|
|
2294
|
+
* extreme (`_builtin_ta_aroon` / `numba_aroon`).
|
|
2295
|
+
*/
|
|
2296
|
+
aroon(
|
|
2297
|
+
site: string,
|
|
2298
|
+
high: Cell,
|
|
2299
|
+
low: Cell,
|
|
2300
|
+
length = 14,
|
|
2301
|
+
): { down: Cell; up: Cell } {
|
|
2302
|
+
const n = pinePeriod(length);
|
|
2303
|
+
const na = { down: null, up: null };
|
|
2304
|
+
if (n === null) return na;
|
|
2305
|
+
const window = n + 1;
|
|
2306
|
+
let st = this.aroonSites.get(site);
|
|
2307
|
+
if (st === undefined || st.length !== n) {
|
|
2308
|
+
st = { length: n, highs: [], lows: [] };
|
|
2309
|
+
this.aroonSites.set(site, st);
|
|
2310
|
+
}
|
|
2311
|
+
if (st.highs.length === window) {
|
|
2312
|
+
st.highs.shift();
|
|
2313
|
+
st.lows.shift();
|
|
2314
|
+
}
|
|
2315
|
+
st.highs.push(finiteCell(high));
|
|
2316
|
+
st.lows.push(finiteCell(low));
|
|
2317
|
+
if (st.highs.length < window) return na;
|
|
2318
|
+
let hh: number | null = null;
|
|
2319
|
+
let ll: number | null = null;
|
|
2320
|
+
let hhK = 0;
|
|
2321
|
+
let llK = 0;
|
|
2322
|
+
for (let k = 0; k < window; k++) {
|
|
2323
|
+
const hv = st.highs[k]!;
|
|
2324
|
+
const lv = st.lows[k]!;
|
|
2325
|
+
if (hv === null || lv === null) return na;
|
|
2326
|
+
if (hh === null || hv > hh) {
|
|
2327
|
+
hh = hv;
|
|
2328
|
+
hhK = k;
|
|
2329
|
+
}
|
|
2330
|
+
if (ll === null || lv < ll) {
|
|
2331
|
+
ll = lv;
|
|
2332
|
+
llK = k;
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
const barsSinceHh = window - 1 - hhK;
|
|
2336
|
+
const barsSinceLl = window - 1 - llK;
|
|
2337
|
+
const up = (100 * (n - barsSinceHh)) / n;
|
|
2338
|
+
const down = (100 * (n - barsSinceLl)) / n;
|
|
2339
|
+
return {
|
|
2340
|
+
down: Number.isFinite(down) ? down : null,
|
|
2341
|
+
up: Number.isFinite(up) ? up : null,
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
/**
|
|
2346
|
+
* Ultimate Oscillator. BP/TR window; first bar has no prev close.
|
|
2347
|
+
* Weights 4, 2, 1. Ready once the longest window is all finite (`_uo_inc_update`).
|
|
2348
|
+
*/
|
|
2349
|
+
uo(site: string, high: Cell, low: Cell, close: Cell, n1: number, n2: number, n3: number): Cell {
|
|
2350
|
+
const a = pinePeriod(n1);
|
|
2351
|
+
const b = pinePeriod(n2);
|
|
2352
|
+
const c = pinePeriod(n3);
|
|
2353
|
+
if (a === null || b === null || c === null) return null;
|
|
2354
|
+
const need = Math.max(a, b, c);
|
|
2355
|
+
let st = this.uoSites.get(site);
|
|
2356
|
+
if (st === undefined || st.n1 !== a || st.n2 !== b || st.n3 !== c) {
|
|
2357
|
+
st = { n1: a, n2: b, n3: c, prev: null, window: [] };
|
|
2358
|
+
this.uoSites.set(site, st);
|
|
2359
|
+
}
|
|
2360
|
+
const h = finiteCell(high);
|
|
2361
|
+
const l = finiteCell(low);
|
|
2362
|
+
const cl = finiteCell(close);
|
|
2363
|
+
const prev = st.prev;
|
|
2364
|
+
st.prev = cl;
|
|
2365
|
+
let bp: Cell = null;
|
|
2366
|
+
let tr: Cell = null;
|
|
2367
|
+
if (prev !== null && h !== null && l !== null && cl !== null) {
|
|
2368
|
+
const lowRef = l < prev ? l : prev;
|
|
2369
|
+
const highRef = h > prev ? h : prev;
|
|
2370
|
+
bp = cl - lowRef;
|
|
2371
|
+
tr = highRef - lowRef;
|
|
2372
|
+
}
|
|
2373
|
+
pushCap(st.window, [bp, tr], need);
|
|
2374
|
+
if (st.window.length < need) return null;
|
|
2375
|
+
const avg = (length: number): Cell => {
|
|
2376
|
+
let bpS = 0;
|
|
2377
|
+
let trS = 0;
|
|
2378
|
+
const start = st.window.length - length;
|
|
2379
|
+
for (let i = start; i < st.window.length; i++) {
|
|
2380
|
+
const pair = st.window[i]!;
|
|
2381
|
+
if (pair[0] === null || pair[1] === null) return null;
|
|
2382
|
+
bpS += pair[0];
|
|
2383
|
+
trS += pair[1];
|
|
2384
|
+
}
|
|
2385
|
+
if (trS === 0) return null;
|
|
2386
|
+
return bpS / trS;
|
|
2387
|
+
};
|
|
2388
|
+
const a1 = avg(a);
|
|
2389
|
+
const a2 = avg(b);
|
|
2390
|
+
const a3 = avg(c);
|
|
2391
|
+
if (a1 === null || a2 === null || a3 === null) return null;
|
|
2392
|
+
return finiteOut((100 * (4 * a1 + 2 * a2 + a3)) / 7);
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
/**
|
|
2396
|
+
* Rank Correlation Index: Spearman rho of time vs value (not ×100).
|
|
2397
|
+
* Length < 2 raises, matching `_builtin_ta_rci` (not the kernel's soft na).
|
|
2398
|
+
*/
|
|
2399
|
+
rci(site: string, source: Cell, length: number): Cell {
|
|
2400
|
+
const n = pinePeriod(length);
|
|
2401
|
+
if (n === null || n < 2) {
|
|
2402
|
+
throw new Error("ta.rci length must be at least 2");
|
|
2403
|
+
}
|
|
2404
|
+
let st = this.rciSites.get(site);
|
|
2405
|
+
if (st === undefined || st.length !== n) {
|
|
2406
|
+
st = { length: n, window: [] };
|
|
2407
|
+
this.rciSites.set(site, st);
|
|
2408
|
+
}
|
|
2409
|
+
pushCap(st.window, finiteCell(source), n);
|
|
2410
|
+
if (st.window.length < n) return null;
|
|
2411
|
+
const vals: number[] = [];
|
|
2412
|
+
for (const v of st.window) {
|
|
2413
|
+
if (v === null) return null;
|
|
2414
|
+
vals.push(v);
|
|
2415
|
+
}
|
|
2416
|
+
return spearmanRho(vals);
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
/** `close[length//2+1] - sma(close, length)` (`_dpo_inc_update`). Any na in the window → na. */
|
|
2420
|
+
dpo(site: string, close: Cell, length: number): Cell {
|
|
2421
|
+
const n = pinePeriod(length);
|
|
2422
|
+
if (n === null) return null;
|
|
2423
|
+
const disp = Math.floor(n / 2) + 1;
|
|
2424
|
+
const keep = Math.max(n, disp);
|
|
2425
|
+
let st = this.dpoSites.get(site);
|
|
2426
|
+
if (st === undefined || st.length !== n) {
|
|
2427
|
+
st = { length: n, window: [] };
|
|
2428
|
+
this.dpoSites.set(site, st);
|
|
2429
|
+
}
|
|
2430
|
+
const x = finiteCell(close);
|
|
2431
|
+
pushCap(st.window, x, keep);
|
|
2432
|
+
if (st.window.length < n || st.window.length < disp) return null;
|
|
2433
|
+
const smaStart = st.window.length - n;
|
|
2434
|
+
if (x === null) return null;
|
|
2435
|
+
let sum = 0;
|
|
2436
|
+
for (let i = smaStart; i < st.window.length; i++) {
|
|
2437
|
+
const v = st.window[i]!;
|
|
2438
|
+
if (v === null) return null;
|
|
2439
|
+
sum += v;
|
|
2440
|
+
}
|
|
2441
|
+
const displaced = st.window[st.window.length - disp];
|
|
2442
|
+
if (displaced === null || displaced === undefined) return null;
|
|
2443
|
+
return finiteOut(displaced - sum / n);
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
/**
|
|
2447
|
+
* Know Sure Thing. ROC is `(close - close[-length]) / close[-length] * 100`
|
|
2448
|
+
* (lookback `length`, not `length` bars via `src[length]`). Weights 1/2/3/4, /10.
|
|
2449
|
+
*/
|
|
2450
|
+
kst(site: string, close: Cell, l1: number, l2: number, l3: number, l4: number): Cell {
|
|
2451
|
+
const lengths = [pinePeriod(l1), pinePeriod(l2), pinePeriod(l3), pinePeriod(l4)];
|
|
2452
|
+
if (lengths.some((v) => v === null)) return null;
|
|
2453
|
+
const ns = lengths as number[];
|
|
2454
|
+
if (Math.min(...ns) < 1) return null;
|
|
2455
|
+
const keep = Math.max(...ns);
|
|
2456
|
+
const key = ns.join(",");
|
|
2457
|
+
let st = this.kstSites.get(site);
|
|
2458
|
+
if (st === undefined || st.key !== key) {
|
|
2459
|
+
st = { key, window: [] };
|
|
2460
|
+
this.kstSites.set(site, st);
|
|
2461
|
+
}
|
|
2462
|
+
const x = finiteCell(close);
|
|
2463
|
+
pushCap(st.window, x, keep);
|
|
2464
|
+
if (st.window.length < keep || x === null) return null;
|
|
2465
|
+
const rocs: number[] = [];
|
|
2466
|
+
for (const length of ns) {
|
|
2467
|
+
const base = st.window[st.window.length - length];
|
|
2468
|
+
if (base === null || base === undefined) return null;
|
|
2469
|
+
if (base === 0) rocs.push(0);
|
|
2470
|
+
else rocs.push(((x - base) / base) * 100);
|
|
2471
|
+
}
|
|
2472
|
+
return finiteOut((rocs[0]! * 1 + rocs[1]! * 2 + rocs[2]! * 3 + rocs[3]! * 4) / 10);
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
/**
|
|
2476
|
+
* StochRSI. Simple (non-Wilder) RSI: sum of gains/losses over `rsiLen` closes
|
|
2477
|
+
* divided by `rsiLen`. First RSI omits the oldest close (`bars > rsiLen`).
|
|
2478
|
+
* Signal is `0.33 * stoch + 0.67 * prev` (`_stochrsi_inc_update`).
|
|
2479
|
+
*/
|
|
2480
|
+
stochRsi(site: string, close: Cell, rsiLength: number, stochLength: number): TaRecord {
|
|
2481
|
+
const empty = taRec({ stochrsi: null, signal: null });
|
|
2482
|
+
const rsiLen = pinePeriod(rsiLength);
|
|
2483
|
+
const stochLen = pinePeriod(stochLength);
|
|
2484
|
+
if (rsiLen === null || stochLen === null) return empty;
|
|
2485
|
+
let st = this.stochRsiSites.get(site);
|
|
2486
|
+
if (st === undefined || st.rsiLen !== rsiLen || st.stochLen !== stochLen) {
|
|
2487
|
+
st = {
|
|
2488
|
+
rsiLen,
|
|
2489
|
+
stochLen,
|
|
2490
|
+
prices: [],
|
|
2491
|
+
rsiRing: [],
|
|
2492
|
+
bars: 0,
|
|
2493
|
+
validRsi: 0,
|
|
2494
|
+
signal: null,
|
|
2495
|
+
};
|
|
2496
|
+
this.stochRsiSites.set(site, st);
|
|
2497
|
+
}
|
|
2498
|
+
const x = finiteCell(close);
|
|
2499
|
+
if (x === null) return taRec({ stochrsi: null, signal: st.signal });
|
|
2500
|
+
pushCap(st.prices, x, rsiLen);
|
|
2501
|
+
st.bars += 1;
|
|
2502
|
+
if (st.bars <= rsiLen) return empty;
|
|
2503
|
+
if (st.prices.length < rsiLen) return empty;
|
|
2504
|
+
let gains = 0;
|
|
2505
|
+
let losses = 0;
|
|
2506
|
+
for (let j = 1; j < st.prices.length; j++) {
|
|
2507
|
+
const d = st.prices[j]! - st.prices[j - 1]!;
|
|
2508
|
+
if (d > 0) gains += d;
|
|
2509
|
+
else losses += -d;
|
|
2510
|
+
}
|
|
2511
|
+
const avgGain = gains / rsiLen;
|
|
2512
|
+
const avgLoss = losses / rsiLen;
|
|
2513
|
+
const rs = avgLoss !== 0 ? avgGain / avgLoss : 100;
|
|
2514
|
+
const rsiVal = 100 - 100 / (1 + rs);
|
|
2515
|
+
pushCap(st.rsiRing, rsiVal, stochLen);
|
|
2516
|
+
st.validRsi += 1;
|
|
2517
|
+
if (st.validRsi < stochLen || st.rsiRing.length < stochLen) return empty;
|
|
2518
|
+
let rsiHigh = st.rsiRing[0]!;
|
|
2519
|
+
let rsiLow = rsiHigh;
|
|
2520
|
+
for (const v of st.rsiRing) {
|
|
2521
|
+
if (v > rsiHigh) rsiHigh = v;
|
|
2522
|
+
if (v < rsiLow) rsiLow = v;
|
|
2523
|
+
}
|
|
2524
|
+
const rsiRange = rsiHigh - rsiLow;
|
|
2525
|
+
const stoch = rsiRange === 0 ? 0 : ((rsiVal - rsiLow) / rsiRange) * 100;
|
|
2526
|
+
const prev = st.signal === null ? stoch : st.signal;
|
|
2527
|
+
const signal = stoch * 0.33 + prev * 0.67;
|
|
2528
|
+
st.signal = signal;
|
|
2529
|
+
return taRec({
|
|
2530
|
+
stochrsi: finiteOut(stoch),
|
|
2531
|
+
signal: finiteOut(signal),
|
|
2532
|
+
});
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
/** Donchian channel via nested highest/lowest (`_donchian_inc_update`). */
|
|
2536
|
+
donchian(site: string, high: Cell, low: Cell, length: number): TaRecord {
|
|
2537
|
+
const empty = taRec({ high: null, low: null, mid: null });
|
|
2538
|
+
const hi = this.highest(`${site}:hi`, high, length);
|
|
2539
|
+
const lo = this.lowest(`${site}:lo`, low, length);
|
|
2540
|
+
if (hi === null || lo === null) return empty;
|
|
2541
|
+
return taRec({ high: hi, low: lo, mid: (hi + lo) / 2 });
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
/**
|
|
2545
|
+
* Ichimoku. Tenkan/kijun are high-low midpoints. Senkou B is fixed at 52
|
|
2546
|
+
* (`ICHIMOKU_SENKOU_B_PERIOD`), not `2 * slow`.
|
|
2547
|
+
*/
|
|
2548
|
+
ichimoku(site: string, high: Cell, low: Cell, fast: number, slow: number): TaRecord {
|
|
2549
|
+
const mid = (tag: string, period: number): Cell => {
|
|
2550
|
+
const hi = this.highest(`${site}:${tag}:hi`, high, period);
|
|
2551
|
+
const lo = this.lowest(`${site}:${tag}:lo`, low, period);
|
|
2552
|
+
if (hi === null || lo === null) return null;
|
|
2553
|
+
return (hi + lo) / 2;
|
|
2554
|
+
};
|
|
2555
|
+
const tenkan = mid("tenkan", fast);
|
|
2556
|
+
const kijun = mid("kijun", slow);
|
|
2557
|
+
const senkouA = tenkan !== null && kijun !== null ? (tenkan + kijun) / 2 : null;
|
|
2558
|
+
const senkouB = mid("spanb", 52);
|
|
2559
|
+
return taRec({
|
|
2560
|
+
tenkan_sen: tenkan,
|
|
2561
|
+
kijun_sen: kijun,
|
|
2562
|
+
senkou_span_a: senkouA,
|
|
2563
|
+
senkou_span_b: senkouB,
|
|
2564
|
+
});
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
/** Bollinger %B × 100, population variance (`/ length`), clamped to 0..100. */
|
|
2568
|
+
bbPct(site: string, close: Cell, length: number, mult: number): Cell {
|
|
2569
|
+
const n = pinePeriod(length);
|
|
2570
|
+
if (n === null) return null;
|
|
2571
|
+
const m = Number.isFinite(mult) ? mult : 2;
|
|
2572
|
+
let st = this.bbPctSites.get(site);
|
|
2573
|
+
if (st === undefined || st.length !== n) {
|
|
2574
|
+
st = { length: n, window: [] };
|
|
2575
|
+
this.bbPctSites.set(site, st);
|
|
2576
|
+
}
|
|
2577
|
+
pushCap(st.window, finiteCell(close), n);
|
|
2578
|
+
if (st.window.length < n) return null;
|
|
2579
|
+
let sum = 0;
|
|
2580
|
+
for (const v of st.window) {
|
|
2581
|
+
if (v === null) return null;
|
|
2582
|
+
sum += v;
|
|
2583
|
+
}
|
|
2584
|
+
const sma = sum / n;
|
|
2585
|
+
let varSum = 0;
|
|
2586
|
+
for (const v of st.window) {
|
|
2587
|
+
const d = v! - sma;
|
|
2588
|
+
varSum += d * d;
|
|
2589
|
+
}
|
|
2590
|
+
const std = Math.sqrt(varSum / n);
|
|
2591
|
+
const upper = sma + std * m;
|
|
2592
|
+
const lower = sma - std * m;
|
|
2593
|
+
if (upper === lower) return 50;
|
|
2594
|
+
const last = st.window[st.window.length - 1]!;
|
|
2595
|
+
let pct = ((last - lower) / (upper - lower)) * 100;
|
|
2596
|
+
if (pct < 0) pct = 0;
|
|
2597
|
+
if (pct > 100) pct = 100;
|
|
2598
|
+
return finiteOut(pct);
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
/**
|
|
2602
|
+
* Ease of Movement, SMA of the last `length` finite EMV samples
|
|
2603
|
+
* (na bars are skipped, not zeros). Bar 0 is na.
|
|
2604
|
+
*/
|
|
2605
|
+
emv(site: string, high: Cell, low: Cell, volume: Cell, length: number): Cell {
|
|
2606
|
+
const n = pinePeriod(length);
|
|
2607
|
+
if (n === null) return null;
|
|
2608
|
+
let st = this.emvSites.get(site);
|
|
2609
|
+
if (st === undefined || st.length !== n) {
|
|
2610
|
+
st = { length: n, prevH: null, prevL: null, started: false, valid: [] };
|
|
2611
|
+
this.emvSites.set(site, st);
|
|
2612
|
+
}
|
|
2613
|
+
const h = finiteCell(high);
|
|
2614
|
+
const l = finiteCell(low);
|
|
2615
|
+
const vol = finiteCell(volume);
|
|
2616
|
+
if (!st.started) {
|
|
2617
|
+
st.started = true;
|
|
2618
|
+
st.prevH = h;
|
|
2619
|
+
st.prevL = l;
|
|
2620
|
+
return null;
|
|
2621
|
+
}
|
|
2622
|
+
let sample: number | null = null;
|
|
2623
|
+
if (h !== null && l !== null && vol !== null && vol !== 0 && st.prevH !== null && st.prevL !== null) {
|
|
2624
|
+
const distance = (h + l) / 2 - (st.prevH + st.prevL) / 2;
|
|
2625
|
+
const box = h - l;
|
|
2626
|
+
if (box !== 0) {
|
|
2627
|
+
const emv = (distance / box) * (h - l) / vol;
|
|
2628
|
+
sample = Number.isFinite(emv) ? emv : null;
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
st.prevH = h;
|
|
2632
|
+
st.prevL = l;
|
|
2633
|
+
if (sample !== null) pushCap(st.valid, sample, n);
|
|
2634
|
+
if (st.valid.length < n) return null;
|
|
2635
|
+
let sum = 0;
|
|
2636
|
+
for (const v of st.valid) sum += v;
|
|
2637
|
+
return finiteOut(sum / n);
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
/**
|
|
2641
|
+
* Fractal flags. Python evaluates at the series end, so the forward half of
|
|
2642
|
+
* the centered window is empty: once `bars >= 2*period+1`, current equals
|
|
2643
|
+
* the extreme of the last `period+1` bars (ties count). Warmup is false, not na.
|
|
2644
|
+
*/
|
|
2645
|
+
fractal(site: string, high: Cell, low: Cell, period: number): TaRecord {
|
|
2646
|
+
const none = taRec({ is_high_fractal: false, is_low_fractal: false });
|
|
2647
|
+
const n = pinePeriod(period);
|
|
2648
|
+
if (n === null) return none;
|
|
2649
|
+
const win = n + 1;
|
|
2650
|
+
const need = n * 2 + 1;
|
|
2651
|
+
let st = this.fractalSites.get(site);
|
|
2652
|
+
if (st === undefined || st.period !== n) {
|
|
2653
|
+
st = { period: n, highs: [], lows: [], bars: 0 };
|
|
2654
|
+
this.fractalSites.set(site, st);
|
|
2655
|
+
}
|
|
2656
|
+
st.bars += 1;
|
|
2657
|
+
pushCap(st.highs, finiteCell(high), win);
|
|
2658
|
+
pushCap(st.lows, finiteCell(low), win);
|
|
2659
|
+
if (st.bars < need || st.highs.length < win) return none;
|
|
2660
|
+
const ch = st.highs[st.highs.length - 1]!;
|
|
2661
|
+
const cl = st.lows[st.lows.length - 1]!;
|
|
2662
|
+
if (ch === null || cl === null) return none;
|
|
2663
|
+
let maxH = ch;
|
|
2664
|
+
let minL = cl;
|
|
2665
|
+
for (const h of st.highs) {
|
|
2666
|
+
if (h === null) return none;
|
|
2667
|
+
if (h > maxH) maxH = h;
|
|
2668
|
+
}
|
|
2669
|
+
for (const l of st.lows) {
|
|
2670
|
+
if (l === null) return none;
|
|
2671
|
+
if (l < minL) minL = l;
|
|
2672
|
+
}
|
|
2673
|
+
return taRec({ is_high_fractal: ch === maxH, is_low_fractal: cl === minL });
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
/** ATR stop around chart close. Non-positive ATR → na (`_builtin_ta_atr_stop`). */
|
|
2677
|
+
atrStop(close: Cell, atr: Cell, multiplier: number): TaRecord {
|
|
2678
|
+
const c = finiteCell(close);
|
|
2679
|
+
const a = finiteCell(atr);
|
|
2680
|
+
const m = Number.isFinite(multiplier) ? multiplier : 2;
|
|
2681
|
+
if (c === null || a === null || a <= 0) return taRec({ long_stop: null, short_stop: null });
|
|
2682
|
+
return taRec({ long_stop: finiteOut(c - a * m), short_stop: finiteOut(c + a * m) });
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
/**
|
|
2686
|
+
* Zigzag tuple `(high, low, direction)`. `_expect_list` materializes
|
|
2687
|
+
* newest-first history, so `[-2:]` is the two oldest finite samples
|
|
2688
|
+
* (PineSeries cap 1000), not the last two bars.
|
|
2689
|
+
*/
|
|
2690
|
+
zigzag(site: string, source: Cell, threshold: number): { high: Cell; low: Cell; dir: number } {
|
|
2691
|
+
const th = Number.isFinite(threshold) ? threshold : 5;
|
|
2692
|
+
let st = this.zigzagSites.get(site);
|
|
2693
|
+
if (st === undefined) {
|
|
2694
|
+
st = { samples: [] };
|
|
2695
|
+
this.zigzagSites.set(site, st);
|
|
2696
|
+
}
|
|
2697
|
+
pushCap(st.samples, finiteCell(source), 1000);
|
|
2698
|
+
const finite: number[] = [];
|
|
2699
|
+
for (const v of st.samples) {
|
|
2700
|
+
if (v !== null) finite.push(v);
|
|
2701
|
+
}
|
|
2702
|
+
if (finite.length < 2) return { high: null, low: null, dir: 0 };
|
|
2703
|
+
const oldest = finite[0]!;
|
|
2704
|
+
const second = finite[1]!;
|
|
2705
|
+
const recentHigh = Math.max(oldest, second);
|
|
2706
|
+
const recentLow = Math.min(oldest, second);
|
|
2707
|
+
const pct = recentLow !== 0 ? ((recentHigh - recentLow) / recentLow) * 100 : 0;
|
|
2708
|
+
const direction = recentHigh === oldest ? 1 : -1;
|
|
2709
|
+
return { high: recentHigh, low: recentLow, dir: pct > th ? 1 : direction };
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
private readonly accelSites = new Map<string, Cell[]>();
|
|
2713
|
+
|
|
2714
|
+
/** Python `pine_expect_int`: near-integers round, other finites floor. `na` throws. */
|
|
2715
|
+
private coerceTaInt(value: Cell, message: string): number {
|
|
2716
|
+
if (value === null || !Number.isFinite(value)) throw new Error(`${message}. Got: na`);
|
|
2717
|
+
const nearest = Math.round(value);
|
|
2718
|
+
if (Math.abs(value - nearest) <= PERIOD_INT_EPS) return nearest;
|
|
2719
|
+
return Math.floor(value);
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
/**
|
|
2723
|
+
* Current-bar hammer / doji (`patterns.py` `_hammer`). Both flags can be
|
|
2724
|
+
* true; strength prefers doji. `na` OHLC raises (Python `TypeError`).
|
|
2725
|
+
*/
|
|
2726
|
+
hammer(open: Cell, high: Cell, low: Cell, close: Cell): TaRecord {
|
|
2727
|
+
if (open === null || high === null || low === null || close === null) {
|
|
2728
|
+
throw new Error("bad operand type for abs(): 'NoneType'");
|
|
2729
|
+
}
|
|
2730
|
+
const body = Math.abs(close - open);
|
|
2731
|
+
const range = high - low;
|
|
2732
|
+
const lower = Math.min(open, close) - low;
|
|
2733
|
+
const upper = high - Math.max(open, close);
|
|
2734
|
+
const isDoji = body < range * 0.1;
|
|
2735
|
+
const isHammer = body > 0 && lower > body * 2 && upper < body;
|
|
2736
|
+
let strength = 0;
|
|
2737
|
+
if (isDoji) strength = 1 - body / (range + 0.0001);
|
|
2738
|
+
else if (isHammer) strength = Math.min(1, lower / (range + 0.0001));
|
|
2739
|
+
return taRec({
|
|
2740
|
+
is_hammer: isHammer,
|
|
2741
|
+
is_doji: isDoji,
|
|
2742
|
+
pattern_strength: Number.isFinite(strength) ? strength : null,
|
|
2743
|
+
});
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
/**
|
|
2747
|
+
* Gap vs an explicit prev-close scalar (`patterns.py` `_gap_detector`).
|
|
2748
|
+
* `na` prev-close is the missing-scalar sentinel (zeros), not an error.
|
|
2749
|
+
* Numeric prev with `na` high/low raises, matching Python arithmetic.
|
|
2750
|
+
*/
|
|
2751
|
+
gapDetector(high: Cell, low: Cell, prevClose: Cell): TaRecord {
|
|
2752
|
+
const none = taRec({ gap_size: 0, gap_type: 0, gap_percent: 0 });
|
|
2753
|
+
if (prevClose === null) return none;
|
|
2754
|
+
if (high === null || low === null) {
|
|
2755
|
+
throw new Error("unsupported operand type(s) for -: 'float' and 'NoneType'");
|
|
2756
|
+
}
|
|
2757
|
+
const upside = Math.max(0, low - prevClose);
|
|
2758
|
+
const downside = Math.max(0, prevClose - high);
|
|
2759
|
+
if (upside > downside) {
|
|
2760
|
+
const pct = prevClose !== 0 ? (upside / prevClose) * 100 : 0;
|
|
2761
|
+
return taRec({
|
|
2762
|
+
gap_size: upside,
|
|
2763
|
+
gap_type: 1,
|
|
2764
|
+
gap_percent: Number.isFinite(pct) ? pct : null,
|
|
2765
|
+
});
|
|
2766
|
+
}
|
|
2767
|
+
if (downside > 0) {
|
|
2768
|
+
const pct = prevClose !== 0 ? (downside / prevClose) * 100 : 0;
|
|
2769
|
+
return taRec({
|
|
2770
|
+
gap_size: downside,
|
|
2771
|
+
gap_type: -1,
|
|
2772
|
+
gap_percent: Number.isFinite(pct) ? pct : null,
|
|
2773
|
+
});
|
|
2774
|
+
}
|
|
2775
|
+
return none;
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
/** `macd_line - signal_line` (`oscillators.py`). Either `na` → `na`. */
|
|
2779
|
+
macdSignal(macdLine: Cell, signalLine: Cell): Cell {
|
|
2780
|
+
if (macdLine === null || signalLine === null) return null;
|
|
2781
|
+
return finiteOut(macdLine - signalLine);
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
/**
|
|
2785
|
+
* `advanced.py` `_builtin_ta_acceleration_factor` (wins over economics).
|
|
2786
|
+
* `_expect_list` is newest-first PineSeries history, cap 1000, so
|
|
2787
|
+
* `[-period-1:]` is the oldest window — the value sticks once warm.
|
|
2788
|
+
*/
|
|
2789
|
+
accelerationFactor(site: string, source: Cell, period: Cell): Cell {
|
|
2790
|
+
const p = this.coerceTaInt(period, "ta.acceleration_factor() requires 2 arguments");
|
|
2791
|
+
let samples = this.accelSites.get(site);
|
|
2792
|
+
if (samples === undefined) {
|
|
2793
|
+
samples = [];
|
|
2794
|
+
this.accelSites.set(site, samples);
|
|
2795
|
+
}
|
|
2796
|
+
pushCap(samples, finiteCell(source), 1000);
|
|
2797
|
+
if (samples.length < p + 1 || p <= 0) return 0;
|
|
2798
|
+
const oldest = samples.slice(0, p + 1);
|
|
2799
|
+
const newestFirst: number[] = [];
|
|
2800
|
+
for (let i = oldest.length - 1; i >= 0; i--) {
|
|
2801
|
+
const v = oldest[i];
|
|
2802
|
+
if (v !== null) newestFirst.push(v);
|
|
2803
|
+
}
|
|
2804
|
+
if (newestFirst.length < 2) return 0;
|
|
2805
|
+
let oldSum = 0;
|
|
2806
|
+
for (let i = 0; i < newestFirst.length - 1; i++) oldSum += newestFirst[i]!;
|
|
2807
|
+
const oldMom = oldSum / (newestFirst.length - 1);
|
|
2808
|
+
if (Math.abs(oldMom) < 1e-10) return 0;
|
|
2809
|
+
const newMom = newestFirst[newestFirst.length - 1]!;
|
|
2810
|
+
let acc = (newMom - oldMom) / oldMom;
|
|
2811
|
+
if (acc < -2) acc = -2;
|
|
2812
|
+
if (acc > 2) acc = 2;
|
|
2813
|
+
return finiteOut(acc);
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
/**
|
|
2817
|
+
* Bar mode sees a 1-element OHLC list (`oscillators.py` `_builtin_ta_stoch_smooth`).
|
|
2818
|
+
* Raw %K is the current bar; `_ema` is `na` unless both smooth lengths are 1.
|
|
2819
|
+
* Period `<= 0` makes `max()` of an empty window raise.
|
|
2820
|
+
*/
|
|
2821
|
+
stochSmooth(high: Cell, low: Cell, close: Cell, period: Cell, smoothK: Cell, smoothD: Cell): Cell {
|
|
2822
|
+
const p = this.coerceTaInt(period, "ta.stoch_smooth period must be integer");
|
|
2823
|
+
const k = this.coerceTaInt(smoothK, "ta.stoch_smooth smooth_k must be integer");
|
|
2824
|
+
const d = this.coerceTaInt(smoothD, "ta.stoch_smooth smooth_d must be integer");
|
|
2825
|
+
if (p <= 0) throw new Error("max() iterable argument is empty");
|
|
2826
|
+
if (high === null || low === null || close === null) {
|
|
2827
|
+
throw new Error("unsupported operand type(s) for /: 'float' and 'NoneType'");
|
|
2828
|
+
}
|
|
2829
|
+
if (k !== 1 || d !== 1) return null;
|
|
2830
|
+
const span = high - low;
|
|
2831
|
+
const stoch = span !== 0 ? (100 * (close - low)) / span : 50;
|
|
2832
|
+
return finiteOut(stoch);
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
/**
|
|
2836
|
+
* Bar-mode OHLC is a PineSeries, not a list (`len < 2`). Sentinel every bar
|
|
2837
|
+
* (`patterns.py` `_builtin_ta_engulfing`) — a lookback port would disagree.
|
|
2838
|
+
*/
|
|
2839
|
+
engulfing(): TaRecord {
|
|
2840
|
+
return taRec({ is_bullish: false, is_bearish: false, pattern_strength: 0 });
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
/** Same one-element series: `advanced.py` `_builtin_ta_inside_bar_pattern` is always false. */
|
|
2844
|
+
insideBarPattern(): boolean {
|
|
2845
|
+
return false;
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
/**
|
|
2849
|
+
* One-element high/low never fills the window (`patterns.py`
|
|
2850
|
+
* `_builtin_ta_double_top_bottom`). Any finite period is the none-sentinel;
|
|
2851
|
+
* `na` period still raises.
|
|
2852
|
+
*/
|
|
2853
|
+
doubleTopBottom(period: Cell): TaRecord {
|
|
2854
|
+
if (period === null || !Number.isFinite(period)) {
|
|
2855
|
+
throw new Error("period must be integer. Got: na");
|
|
2856
|
+
}
|
|
2857
|
+
return taRec({ pattern_type: "none", strength: 0, breakout_level: 0 });
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
// `_expect_list` materializes PineSeries history newest-first (cap 1000).
|
|
2861
|
+
// Negative indexes are the oldest samples, not the latest bars.
|
|
2862
|
+
private readonly cumDeltaSites = new Map<string, { close: Cell[]; volume: Cell[] }>();
|
|
2863
|
+
private readonly volMomSites = new Map<string, { volume: Cell[] }>();
|
|
2864
|
+
private readonly volProfileLowSites = new Map<string, { close: Cell[]; volume: Cell[] }>();
|
|
2865
|
+
private readonly volThrustSites = new Map<string, { close: Cell[]; volume: Cell[]; sma: Cell[] }>();
|
|
2866
|
+
|
|
2867
|
+
private asCount(value: number): number | null {
|
|
2868
|
+
if (!Number.isFinite(value)) return null;
|
|
2869
|
+
const nearest = Math.round(value);
|
|
2870
|
+
return Math.abs(value - nearest) <= PERIOD_INT_EPS ? nearest : Math.floor(value);
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
private pushChrono(buf: Cell[], value: Cell): void {
|
|
2874
|
+
buf.push(value !== null && Number.isFinite(value) ? value : null);
|
|
2875
|
+
if (buf.length > 1000) buf.shift();
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
/** Newest-first negative index into a chronological buffer. */
|
|
2879
|
+
private nf(buf: Cell[], i: number): Cell {
|
|
2880
|
+
const v = buf[-i - 1];
|
|
2881
|
+
return v === undefined ? null : v;
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
/** `ta.voi` — non-numeric inputs are 0 (`volume.py`). */
|
|
2885
|
+
voi(buy: Cell, sell: Cell): Cell {
|
|
2886
|
+
const b = buy !== null && Number.isFinite(buy) ? buy : 0;
|
|
2887
|
+
const s = sell !== null && Number.isFinite(sell) ? sell : 0;
|
|
2888
|
+
const total = b + s;
|
|
2889
|
+
if (total === 0) return 0;
|
|
2890
|
+
return finiteOut((b - s) / total);
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
/** `ta.bid_ask_imbalance` — `{imbalance_ratio, spread}` (`volume.py`). */
|
|
2894
|
+
bidAskImbalance(bidSize: Cell, askSize: Cell, bidPrice: Cell, askPrice: Cell): TaRecord {
|
|
2895
|
+
const bid = bidSize !== null && Number.isFinite(bidSize) ? bidSize : 0;
|
|
2896
|
+
const ask = askSize !== null && Number.isFinite(askSize) ? askSize : 0;
|
|
2897
|
+
const bidPx = bidPrice !== null && Number.isFinite(bidPrice) ? bidPrice : 0;
|
|
2898
|
+
const askPx = askPrice !== null && Number.isFinite(askPrice) ? askPrice : 0;
|
|
2899
|
+
const total = bid + ask;
|
|
2900
|
+
if (total === 0) return taRec({ imbalance_ratio: 0, spread: 0 });
|
|
2901
|
+
return taRec({
|
|
2902
|
+
imbalance_ratio: finiteOut((bid - ask) / total),
|
|
2903
|
+
spread: finiteOut(bidPx > 0 ? askPx - bidPx : 0),
|
|
2904
|
+
});
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
/** `ta.rsi_oversold_overbought` — current sample only (`oscillators.py`). */
|
|
2908
|
+
rsiOversoldOverbought(rsi: Cell, oversold: number, overbought: number): TaRecord {
|
|
2909
|
+
if (rsi === null || !Number.isFinite(rsi)) {
|
|
2910
|
+
return taRec({ is_oversold: false, is_overbought: false, rsi: null });
|
|
2911
|
+
}
|
|
2912
|
+
const os = this.asCount(oversold);
|
|
2913
|
+
const ob = this.asCount(overbought);
|
|
2914
|
+
if (os === null || ob === null) {
|
|
2915
|
+
return taRec({ is_oversold: false, is_overbought: false, rsi });
|
|
2916
|
+
}
|
|
2917
|
+
return taRec({ is_oversold: rsi < os, is_overbought: rsi > ob, rsi });
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
/**
|
|
2921
|
+
* `ta.rsi_divergence` — last element of the list Python returns.
|
|
2922
|
+
* A scalar is a one-element series (`i < period` → 0). Do not accumulate.
|
|
2923
|
+
*/
|
|
2924
|
+
rsiDivergence(series: Cell[], period: number): Cell {
|
|
2925
|
+
const p = this.asCount(period);
|
|
2926
|
+
if (p === null || series.length === 0) return null;
|
|
2927
|
+
let last = 0;
|
|
2928
|
+
for (let i = 0; i < series.length; i++) {
|
|
2929
|
+
if (i < p) {
|
|
2930
|
+
last = 0;
|
|
2931
|
+
continue;
|
|
2932
|
+
}
|
|
2933
|
+
const start = Math.max(0, i - p);
|
|
2934
|
+
const vals: number[] = [];
|
|
2935
|
+
for (let j = start; j <= i; j++) {
|
|
2936
|
+
const v = series[j];
|
|
2937
|
+
if (v !== null && Number.isFinite(v)) vals.push(v);
|
|
2938
|
+
}
|
|
2939
|
+
if (vals.length < 2) {
|
|
2940
|
+
last = 0;
|
|
2941
|
+
continue;
|
|
2942
|
+
}
|
|
2943
|
+
let rsiMin = vals[0]!;
|
|
2944
|
+
let rsiMax = vals[0]!;
|
|
2945
|
+
for (const v of vals) {
|
|
2946
|
+
if (v < rsiMin) rsiMin = v;
|
|
2947
|
+
if (v > rsiMax) rsiMax = v;
|
|
2948
|
+
}
|
|
2949
|
+
const cur = series[i];
|
|
2950
|
+
const range = rsiMax - rsiMin;
|
|
2951
|
+
if (cur === null || !Number.isFinite(cur) || range <= 0) {
|
|
2952
|
+
last = 0;
|
|
2953
|
+
continue;
|
|
2954
|
+
}
|
|
2955
|
+
last = ((cur - rsiMin) / range) * 2 - 1;
|
|
2956
|
+
}
|
|
2957
|
+
return finiteOut(last);
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2960
|
+
/** `ta.cumulative_delta` — oldest `period` bars (`advanced.py`). */
|
|
2961
|
+
cumulativeDelta(site: string, close: Cell, volume: Cell, period: number): Cell {
|
|
2962
|
+
const n = this.asCount(period);
|
|
2963
|
+
let st = this.cumDeltaSites.get(site);
|
|
2964
|
+
if (st === undefined) {
|
|
2965
|
+
st = { close: [], volume: [] };
|
|
2966
|
+
this.cumDeltaSites.set(site, st);
|
|
2967
|
+
}
|
|
2968
|
+
this.pushChrono(st.close, close);
|
|
2969
|
+
this.pushChrono(st.volume, volume);
|
|
2970
|
+
if (n === null || n <= 0 || st.close.length < n || st.volume.length < n) return 0;
|
|
2971
|
+
let delta = 0;
|
|
2972
|
+
for (let i = -n; i < 0; i++) {
|
|
2973
|
+
const cRaw = this.nf(st.close, i);
|
|
2974
|
+
const vRaw = this.nf(st.volume, i);
|
|
2975
|
+
const c = cRaw !== null && Number.isFinite(cRaw) ? cRaw : 0;
|
|
2976
|
+
const v = vRaw !== null && Number.isFinite(vRaw) ? vRaw : 0;
|
|
2977
|
+
if (i > -n) {
|
|
2978
|
+
const prevRaw = this.nf(st.close, i - 1);
|
|
2979
|
+
const prev = prevRaw !== null && Number.isFinite(prevRaw) ? prevRaw : c;
|
|
2980
|
+
if (c > prev) delta += v;
|
|
2981
|
+
else if (c < prev) delta -= v;
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
return finiteOut(delta);
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
/** `ta.volume_momentum` — oldest `period + 1` volumes (`advanced.py`). */
|
|
2988
|
+
volumeMomentum(site: string, volume: Cell, period: number): Cell {
|
|
2989
|
+
const n = this.asCount(period);
|
|
2990
|
+
let st = this.volMomSites.get(site);
|
|
2991
|
+
if (st === undefined) {
|
|
2992
|
+
st = { volume: [] };
|
|
2993
|
+
this.volMomSites.set(site, st);
|
|
2994
|
+
}
|
|
2995
|
+
this.pushChrono(st.volume, volume);
|
|
2996
|
+
if (n === null || n <= 0 || st.volume.length < n + 1) return 0;
|
|
2997
|
+
const clean: number[] = [];
|
|
2998
|
+
for (let i = 0; i < n + 1; i++) {
|
|
2999
|
+
const v = st.volume[i];
|
|
3000
|
+
if (v !== null && Number.isFinite(v)) clean.push(v);
|
|
3001
|
+
}
|
|
3002
|
+
if (clean.length < 2) return 0;
|
|
3003
|
+
const newVol = clean[0]!;
|
|
3004
|
+
let sum = 0;
|
|
3005
|
+
for (let i = 1; i < clean.length; i++) sum += clean[i]!;
|
|
3006
|
+
const oldVol = sum / (clean.length - 1);
|
|
3007
|
+
if (oldVol === 0) return 0;
|
|
3008
|
+
let momentum = ((newVol - oldVol) / oldVol) * 100;
|
|
3009
|
+
if (momentum > 100) momentum = 100;
|
|
3010
|
+
if (momentum < -100) momentum = -100;
|
|
3011
|
+
return finiteOut(momentum);
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
/**
|
|
3015
|
+
* `ta.volume_profile_high` — numeric list only (`advanced.py`).
|
|
3016
|
+
* A one-element pair is that price when its volume is the strict max.
|
|
3017
|
+
*/
|
|
3018
|
+
volumeProfileHigh(prices: Cell[], volumes: Cell[]): Cell {
|
|
3019
|
+
if (prices.length === 0 || volumes.length === 0 || prices.length !== volumes.length) return 0;
|
|
3020
|
+
let maxVol = 0;
|
|
3021
|
+
let maxPrice = 0;
|
|
3022
|
+
for (let i = 0; i < prices.length; i++) {
|
|
3023
|
+
const v = volumes[i];
|
|
3024
|
+
if (v === null || !Number.isFinite(v) || !(v > maxVol)) continue;
|
|
3025
|
+
maxVol = v;
|
|
3026
|
+
const p = prices[i];
|
|
3027
|
+
maxPrice = p !== null && Number.isFinite(p) ? p : 0;
|
|
3028
|
+
}
|
|
3029
|
+
return finiteOut(maxPrice);
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
/** `ta.volume_profile_low` — buckets over the oldest `period` pairs (`advanced.py`). */
|
|
3033
|
+
volumeProfileLow(site: string, close: Cell, volume: Cell, period: number, levels: number): Cell {
|
|
3034
|
+
const p = this.asCount(period);
|
|
3035
|
+
const lv = this.asCount(levels);
|
|
3036
|
+
let st = this.volProfileLowSites.get(site);
|
|
3037
|
+
if (st === undefined) {
|
|
3038
|
+
st = { close: [], volume: [] };
|
|
3039
|
+
this.volProfileLowSites.set(site, st);
|
|
3040
|
+
}
|
|
3041
|
+
this.pushChrono(st.close, close);
|
|
3042
|
+
this.pushChrono(st.volume, volume);
|
|
3043
|
+
const oldest = st.close.length > 0 ? st.close[0]! : null;
|
|
3044
|
+
const fallback: Cell = st.close.length === 0 ? 100 : oldest !== null && Number.isFinite(oldest) ? oldest : null;
|
|
3045
|
+
if (p === null || lv === null || p <= 0 || lv <= 0 || st.close.length < p) return fallback;
|
|
3046
|
+
const prices: number[] = [];
|
|
3047
|
+
const vols: number[] = [];
|
|
3048
|
+
for (let i = -p; i < 0; i++) {
|
|
3049
|
+
const c = this.nf(st.close, i);
|
|
3050
|
+
const v = this.nf(st.volume, i);
|
|
3051
|
+
if (c !== null && Number.isFinite(c) && v !== null && Number.isFinite(v)) {
|
|
3052
|
+
prices.push(c);
|
|
3053
|
+
vols.push(v);
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
if (prices.length === 0) return fallback;
|
|
3057
|
+
let minP = prices[0]!;
|
|
3058
|
+
let maxP = prices[0]!;
|
|
3059
|
+
for (const c of prices) {
|
|
3060
|
+
if (c < minP) minP = c;
|
|
3061
|
+
if (c > maxP) maxP = c;
|
|
3062
|
+
}
|
|
3063
|
+
if (minP === maxP) return minP;
|
|
3064
|
+
const bucket = (maxP - minP) / lv;
|
|
3065
|
+
const buckets = new Array<number>(lv).fill(0);
|
|
3066
|
+
for (let k = 0; k < prices.length; k++) {
|
|
3067
|
+
let idx = Math.trunc((prices[k]! - minP) / bucket);
|
|
3068
|
+
if (idx > lv - 1) idx = lv - 1;
|
|
3069
|
+
if (idx < 0) idx = 0;
|
|
3070
|
+
buckets[idx] = (buckets[idx] ?? 0) + vols[k]!;
|
|
3071
|
+
}
|
|
3072
|
+
let minIdx = 0;
|
|
3073
|
+
for (let i = 1; i < lv; i++) {
|
|
3074
|
+
if ((buckets[i] ?? 0) < (buckets[minIdx] ?? 0)) minIdx = i;
|
|
3075
|
+
}
|
|
3076
|
+
return finiteOut(minP + minIdx * bucket);
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
/**
|
|
3080
|
+
* `ta.volume_thrust` — bool as 1/0. `[-1]`/`[-2]` are the oldest two
|
|
3081
|
+
* samples (`advanced.py`), not the current bar.
|
|
3082
|
+
*/
|
|
3083
|
+
volumeThrust(site: string, close: Cell, volume: Cell, volSma: Cell, sensitivity: number): Cell {
|
|
3084
|
+
const sens = Number.isFinite(sensitivity) ? sensitivity : 0.3;
|
|
3085
|
+
let st = this.volThrustSites.get(site);
|
|
3086
|
+
if (st === undefined) {
|
|
3087
|
+
st = { close: [], volume: [], sma: [] };
|
|
3088
|
+
this.volThrustSites.set(site, st);
|
|
3089
|
+
}
|
|
3090
|
+
this.pushChrono(st.close, close);
|
|
3091
|
+
this.pushChrono(st.volume, volume);
|
|
3092
|
+
this.pushChrono(st.sma, volSma);
|
|
3093
|
+
if (st.close.length === 0 || st.volume.length === 0 || st.sma.length === 0) return 0;
|
|
3094
|
+
const numOr = (v: Cell, fallback: number): number => (v !== null && Number.isFinite(v) ? v : fallback);
|
|
3095
|
+
const cVal = numOr(this.nf(st.close, -1), 100);
|
|
3096
|
+
const cPrev = st.close.length > 1 ? numOr(this.nf(st.close, -2), 100) : 100;
|
|
3097
|
+
const vVal = numOr(this.nf(st.volume, -1), 1000);
|
|
3098
|
+
const vsVal = numOr(this.nf(st.sma, -1), 1000);
|
|
3099
|
+
const spike = vVal > vsVal * (1 + sens);
|
|
3100
|
+
const move = cPrev !== 0 ? Math.abs(cVal - cPrev) / cPrev > 0.01 : false;
|
|
3101
|
+
return spike && move ? 1 : 0;
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
/** Spearman rho. Rank is the stable sort position (`_rci_spearman`). */
|
|
3106
|
+
function spearmanRho(vals: number[]): Cell {
|
|
3107
|
+
const n = vals.length;
|
|
3108
|
+
if (n < 2) return null;
|
|
3109
|
+
const order = vals.map((_, i) => i);
|
|
3110
|
+
order.sort((i, j) => vals[i]! - vals[j]!);
|
|
3111
|
+
let d2 = 0;
|
|
3112
|
+
for (let pos = 0; pos < n; pos++) {
|
|
3113
|
+
const d = order[pos]! - pos;
|
|
3114
|
+
d2 += d * d;
|
|
3115
|
+
}
|
|
3116
|
+
const denom = n * (n * n - 1);
|
|
3117
|
+
if (denom === 0) return null;
|
|
3118
|
+
return finiteOut(1 - (6 * d2) / denom);
|
|
2093
3119
|
}
|