ta-crypto 0.3.0 → 0.3.2
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/README.md +16 -3
- package/dist/core/overlap.js +16 -11
- package/dist/core/rolling.d.ts +62 -0
- package/dist/core/rolling.js +214 -0
- package/dist/stateful.js +4 -13
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -73,12 +73,21 @@ ws.on("message", (tick) => {
|
|
|
73
73
|
## Examples
|
|
74
74
|
|
|
75
75
|
Real-world entry points live in `examples/`:
|
|
76
|
-
- `examples/rsi-strategy.
|
|
77
|
-
- `examples/vwap-session.
|
|
78
|
-
- `examples/funding-arbitrage.
|
|
76
|
+
- `examples/rsi-strategy.mjs`
|
|
77
|
+
- `examples/vwap-session.mjs`
|
|
78
|
+
- `examples/funding-arbitrage.mjs`
|
|
79
|
+
- `examples/rsi-technicalindicators-check.mjs` (external reference check)
|
|
79
80
|
|
|
80
81
|
If you're using ta-crypto in production or experiments, feel free to add your example here.
|
|
81
82
|
|
|
83
|
+
Run examples:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npm run example:all
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Detailed commands and expected outputs: `examples/README.md`
|
|
90
|
+
|
|
82
91
|
## Compatibility
|
|
83
92
|
|
|
84
93
|
`ta-crypto` now ships golden tests (`test/fixtures/golden.json`) to lock behavior across releases.
|
|
@@ -295,6 +304,10 @@ npm run version:patch
|
|
|
295
304
|
npm run release
|
|
296
305
|
```
|
|
297
306
|
|
|
307
|
+
## Trust and Verification
|
|
308
|
+
|
|
309
|
+
For release integrity, compatibility policy, and verification workflow, see `docs/trust.md`.
|
|
310
|
+
|
|
298
311
|
## License
|
|
299
312
|
|
|
300
313
|
MIT
|
package/dist/core/overlap.js
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import { assertSameLength, isNum, makeSeries, mean
|
|
1
|
+
import { assertSameLength, isNum, makeSeries, mean } from "./math.js";
|
|
2
|
+
import { rollingMean, rollingMeanStdDev } from "./rolling.js";
|
|
2
3
|
export function sma(values, length = 14) {
|
|
3
|
-
const out = makeSeries(values.length);
|
|
4
4
|
if (length <= 0)
|
|
5
|
-
return
|
|
6
|
-
|
|
7
|
-
out[i] = mean(values, i - length + 1, i);
|
|
8
|
-
}
|
|
9
|
-
return out;
|
|
5
|
+
return makeSeries(values.length);
|
|
6
|
+
return rollingMean(values, length);
|
|
10
7
|
}
|
|
11
8
|
export function ema(values, length = 14) {
|
|
12
9
|
const out = makeSeries(values.length);
|
|
@@ -90,11 +87,19 @@ export function vwap(high, low, close, volume, length) {
|
|
|
90
87
|
return out;
|
|
91
88
|
}
|
|
92
89
|
export function bbands(values, length = 20, std = 2) {
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
if (length <= 0) {
|
|
91
|
+
return {
|
|
92
|
+
basis: makeSeries(values.length),
|
|
93
|
+
upper: makeSeries(values.length),
|
|
94
|
+
lower: makeSeries(values.length)
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const { mean: basis, stdDev: upper } = rollingMeanStdDev(values, length);
|
|
95
98
|
const lower = makeSeries(values.length);
|
|
96
|
-
for (let i =
|
|
97
|
-
const s =
|
|
99
|
+
for (let i = 0; i < values.length; i++) {
|
|
100
|
+
const s = upper[i];
|
|
101
|
+
if (s === null)
|
|
102
|
+
continue;
|
|
98
103
|
upper[i] = basis[i] + std * s;
|
|
99
104
|
lower[i] = basis[i] - std * s;
|
|
100
105
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export type RollingValue = number | null;
|
|
2
|
+
export declare class RollingWindow {
|
|
3
|
+
readonly period: number;
|
|
4
|
+
private readonly values;
|
|
5
|
+
private cursor;
|
|
6
|
+
private count;
|
|
7
|
+
constructor(period: number);
|
|
8
|
+
get size(): number;
|
|
9
|
+
get ready(): boolean;
|
|
10
|
+
push(value: number): number | null;
|
|
11
|
+
reset(): void;
|
|
12
|
+
}
|
|
13
|
+
export declare class RollingSum {
|
|
14
|
+
private readonly window;
|
|
15
|
+
private total;
|
|
16
|
+
constructor(period: number);
|
|
17
|
+
get ready(): boolean;
|
|
18
|
+
next(value: number): RollingValue;
|
|
19
|
+
reset(): void;
|
|
20
|
+
}
|
|
21
|
+
export declare class RollingMean {
|
|
22
|
+
readonly period: number;
|
|
23
|
+
private readonly sum;
|
|
24
|
+
constructor(period: number);
|
|
25
|
+
next(value: number): RollingValue;
|
|
26
|
+
reset(): void;
|
|
27
|
+
}
|
|
28
|
+
export declare class RollingStdDev {
|
|
29
|
+
readonly period: number;
|
|
30
|
+
private readonly window;
|
|
31
|
+
private total;
|
|
32
|
+
private totalSquares;
|
|
33
|
+
constructor(period: number);
|
|
34
|
+
next(value: number): RollingValue;
|
|
35
|
+
reset(): void;
|
|
36
|
+
}
|
|
37
|
+
declare class RollingExtremum {
|
|
38
|
+
private readonly period;
|
|
39
|
+
private readonly compare;
|
|
40
|
+
private readonly deque;
|
|
41
|
+
private head;
|
|
42
|
+
private index;
|
|
43
|
+
constructor(period: number, compare: (candidate: number, tail: number) => boolean);
|
|
44
|
+
next(value: number): RollingValue;
|
|
45
|
+
reset(): void;
|
|
46
|
+
}
|
|
47
|
+
export declare class RollingMin extends RollingExtremum {
|
|
48
|
+
constructor(period: number);
|
|
49
|
+
}
|
|
50
|
+
export declare class RollingMax extends RollingExtremum {
|
|
51
|
+
constructor(period: number);
|
|
52
|
+
}
|
|
53
|
+
export declare function rollingSum(values: number[], period: number): RollingValue[];
|
|
54
|
+
export declare function rollingMean(values: number[], period: number): RollingValue[];
|
|
55
|
+
export declare function rollingStdDev(values: number[], period: number): RollingValue[];
|
|
56
|
+
export declare function rollingMeanStdDev(values: number[], period: number): {
|
|
57
|
+
mean: RollingValue[];
|
|
58
|
+
stdDev: RollingValue[];
|
|
59
|
+
};
|
|
60
|
+
export declare function rollingMin(values: number[], period: number): RollingValue[];
|
|
61
|
+
export declare function rollingMax(values: number[], period: number): RollingValue[];
|
|
62
|
+
export {};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
function assertPeriod(period) {
|
|
2
|
+
if (!Number.isInteger(period) || period <= 0) {
|
|
3
|
+
throw new Error("period must be a positive integer");
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
function assertValue(value) {
|
|
7
|
+
if (!Number.isFinite(value)) {
|
|
8
|
+
throw new Error("value must be a finite number");
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class RollingWindow {
|
|
12
|
+
constructor(period) {
|
|
13
|
+
this.cursor = 0;
|
|
14
|
+
this.count = 0;
|
|
15
|
+
assertPeriod(period);
|
|
16
|
+
this.period = period;
|
|
17
|
+
this.values = new Array(period);
|
|
18
|
+
}
|
|
19
|
+
get size() {
|
|
20
|
+
return this.count;
|
|
21
|
+
}
|
|
22
|
+
get ready() {
|
|
23
|
+
return this.count === this.period;
|
|
24
|
+
}
|
|
25
|
+
push(value) {
|
|
26
|
+
assertValue(value);
|
|
27
|
+
const removed = this.ready ? this.values[this.cursor] : null;
|
|
28
|
+
this.values[this.cursor] = value;
|
|
29
|
+
this.cursor = (this.cursor + 1) % this.period;
|
|
30
|
+
if (this.count < this.period)
|
|
31
|
+
this.count += 1;
|
|
32
|
+
return removed;
|
|
33
|
+
}
|
|
34
|
+
reset() {
|
|
35
|
+
this.cursor = 0;
|
|
36
|
+
this.count = 0;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export class RollingSum {
|
|
40
|
+
constructor(period) {
|
|
41
|
+
this.total = 0;
|
|
42
|
+
this.window = new RollingWindow(period);
|
|
43
|
+
}
|
|
44
|
+
get ready() {
|
|
45
|
+
return this.window.ready;
|
|
46
|
+
}
|
|
47
|
+
next(value) {
|
|
48
|
+
const removed = this.window.push(value);
|
|
49
|
+
this.total += value - (removed ?? 0);
|
|
50
|
+
return this.ready ? this.total : null;
|
|
51
|
+
}
|
|
52
|
+
reset() {
|
|
53
|
+
this.window.reset();
|
|
54
|
+
this.total = 0;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export class RollingMean {
|
|
58
|
+
constructor(period) {
|
|
59
|
+
assertPeriod(period);
|
|
60
|
+
this.period = period;
|
|
61
|
+
this.sum = new RollingSum(period);
|
|
62
|
+
}
|
|
63
|
+
next(value) {
|
|
64
|
+
const total = this.sum.next(value);
|
|
65
|
+
return total === null ? null : total / this.period;
|
|
66
|
+
}
|
|
67
|
+
reset() {
|
|
68
|
+
this.sum.reset();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export class RollingStdDev {
|
|
72
|
+
constructor(period) {
|
|
73
|
+
this.total = 0;
|
|
74
|
+
this.totalSquares = 0;
|
|
75
|
+
assertPeriod(period);
|
|
76
|
+
this.period = period;
|
|
77
|
+
this.window = new RollingWindow(period);
|
|
78
|
+
}
|
|
79
|
+
next(value) {
|
|
80
|
+
const removed = this.window.push(value);
|
|
81
|
+
this.total += value - (removed ?? 0);
|
|
82
|
+
this.totalSquares += value * value - (removed === null ? 0 : removed * removed);
|
|
83
|
+
if (!this.window.ready)
|
|
84
|
+
return null;
|
|
85
|
+
const mean = this.total / this.period;
|
|
86
|
+
const variance = Math.max(0, this.totalSquares / this.period - mean * mean);
|
|
87
|
+
return Math.sqrt(variance);
|
|
88
|
+
}
|
|
89
|
+
reset() {
|
|
90
|
+
this.window.reset();
|
|
91
|
+
this.total = 0;
|
|
92
|
+
this.totalSquares = 0;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
class RollingExtremum {
|
|
96
|
+
constructor(period, compare) {
|
|
97
|
+
this.deque = [];
|
|
98
|
+
this.head = 0;
|
|
99
|
+
this.index = -1;
|
|
100
|
+
assertPeriod(period);
|
|
101
|
+
this.period = period;
|
|
102
|
+
this.compare = compare;
|
|
103
|
+
}
|
|
104
|
+
next(value) {
|
|
105
|
+
assertValue(value);
|
|
106
|
+
this.index += 1;
|
|
107
|
+
const firstValid = this.index - this.period + 1;
|
|
108
|
+
while (this.head < this.deque.length && this.deque[this.head].index < firstValid)
|
|
109
|
+
this.head += 1;
|
|
110
|
+
while (this.deque.length > this.head && this.compare(value, this.deque[this.deque.length - 1].value)) {
|
|
111
|
+
this.deque.pop();
|
|
112
|
+
}
|
|
113
|
+
this.deque.push({ index: this.index, value });
|
|
114
|
+
if (this.head > 64 && this.head * 2 > this.deque.length) {
|
|
115
|
+
this.deque.splice(0, this.head);
|
|
116
|
+
this.head = 0;
|
|
117
|
+
}
|
|
118
|
+
return this.index + 1 < this.period ? null : this.deque[this.head].value;
|
|
119
|
+
}
|
|
120
|
+
reset() {
|
|
121
|
+
this.deque.length = 0;
|
|
122
|
+
this.head = 0;
|
|
123
|
+
this.index = -1;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
export class RollingMin extends RollingExtremum {
|
|
127
|
+
constructor(period) {
|
|
128
|
+
super(period, (candidate, tail) => candidate <= tail);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export class RollingMax extends RollingExtremum {
|
|
132
|
+
constructor(period) {
|
|
133
|
+
super(period, (candidate, tail) => candidate >= tail);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function collect(values, engine) {
|
|
137
|
+
return values.map(value => engine.next(value));
|
|
138
|
+
}
|
|
139
|
+
export function rollingSum(values, period) {
|
|
140
|
+
assertPeriod(period);
|
|
141
|
+
const out = new Array(values.length).fill(null);
|
|
142
|
+
let total = 0;
|
|
143
|
+
for (let i = 0; i < values.length; i++) {
|
|
144
|
+
total += values[i];
|
|
145
|
+
if (i >= period)
|
|
146
|
+
total -= values[i - period];
|
|
147
|
+
if (i >= period - 1)
|
|
148
|
+
out[i] = total;
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
export function rollingMean(values, period) {
|
|
153
|
+
assertPeriod(period);
|
|
154
|
+
const out = new Array(values.length).fill(null);
|
|
155
|
+
let total = 0;
|
|
156
|
+
for (let i = 0; i < values.length; i++) {
|
|
157
|
+
total += values[i];
|
|
158
|
+
if (i >= period)
|
|
159
|
+
total -= values[i - period];
|
|
160
|
+
if (i >= period - 1)
|
|
161
|
+
out[i] = total / period;
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
export function rollingStdDev(values, period) {
|
|
166
|
+
assertPeriod(period);
|
|
167
|
+
const out = new Array(values.length).fill(null);
|
|
168
|
+
let total = 0;
|
|
169
|
+
let totalSquares = 0;
|
|
170
|
+
for (let i = 0; i < values.length; i++) {
|
|
171
|
+
const value = values[i];
|
|
172
|
+
total += value;
|
|
173
|
+
totalSquares += value * value;
|
|
174
|
+
if (i >= period) {
|
|
175
|
+
const removed = values[i - period];
|
|
176
|
+
total -= removed;
|
|
177
|
+
totalSquares -= removed * removed;
|
|
178
|
+
}
|
|
179
|
+
if (i >= period - 1) {
|
|
180
|
+
const mean = total / period;
|
|
181
|
+
out[i] = Math.sqrt(Math.max(0, totalSquares / period - mean * mean));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
export function rollingMeanStdDev(values, period) {
|
|
187
|
+
assertPeriod(period);
|
|
188
|
+
const mean = new Array(values.length).fill(null);
|
|
189
|
+
const stdDev = new Array(values.length).fill(null);
|
|
190
|
+
let total = 0;
|
|
191
|
+
let totalSquares = 0;
|
|
192
|
+
for (let i = 0; i < values.length; i++) {
|
|
193
|
+
const value = values[i];
|
|
194
|
+
total += value;
|
|
195
|
+
totalSquares += value * value;
|
|
196
|
+
if (i >= period) {
|
|
197
|
+
const removed = values[i - period];
|
|
198
|
+
total -= removed;
|
|
199
|
+
totalSquares -= removed * removed;
|
|
200
|
+
}
|
|
201
|
+
if (i >= period - 1) {
|
|
202
|
+
const currentMean = total / period;
|
|
203
|
+
mean[i] = currentMean;
|
|
204
|
+
stdDev[i] = Math.sqrt(Math.max(0, totalSquares / period - currentMean * currentMean));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return { mean, stdDev };
|
|
208
|
+
}
|
|
209
|
+
export function rollingMin(values, period) {
|
|
210
|
+
return collect(values, new RollingMin(period));
|
|
211
|
+
}
|
|
212
|
+
export function rollingMax(values, period) {
|
|
213
|
+
return collect(values, new RollingMax(period));
|
|
214
|
+
}
|
package/dist/stateful.js
CHANGED
|
@@ -1,27 +1,18 @@
|
|
|
1
|
+
import { RollingMean } from "./core/rolling.js";
|
|
1
2
|
export function createSMA(period = 14) {
|
|
2
3
|
if (period <= 0) {
|
|
3
4
|
throw new Error("period must be > 0");
|
|
4
5
|
}
|
|
5
|
-
const
|
|
6
|
-
let sum = 0;
|
|
6
|
+
const rolling = new RollingMean(period);
|
|
7
7
|
return {
|
|
8
8
|
next(value) {
|
|
9
9
|
if (!Number.isFinite(value)) {
|
|
10
10
|
throw new Error("value must be a finite number");
|
|
11
11
|
}
|
|
12
|
-
|
|
13
|
-
sum += value;
|
|
14
|
-
if (window.length < period) {
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
17
|
-
if (window.length > period) {
|
|
18
|
-
sum -= window.shift();
|
|
19
|
-
}
|
|
20
|
-
return sum / period;
|
|
12
|
+
return rolling.next(value);
|
|
21
13
|
},
|
|
22
14
|
reset() {
|
|
23
|
-
|
|
24
|
-
sum = 0;
|
|
15
|
+
rolling.reset();
|
|
25
16
|
}
|
|
26
17
|
};
|
|
27
18
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ta-crypto",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Technical analysis for crypto markets in Node.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -50,11 +50,17 @@
|
|
|
50
50
|
"generate:golden": "npm run build && node ./scripts/generate-golden.js",
|
|
51
51
|
"generate:compat": "npm run build && node ./scripts/export-compat-vectors.js",
|
|
52
52
|
"bench": "npm run build && node ./scripts/bench.js",
|
|
53
|
+
"bench:rolling": "npm run build && node ./scripts/bench-rolling.js",
|
|
53
54
|
"test": "npm run build && node --test test/*.test.mjs",
|
|
54
55
|
"test:golden": "npm run build && node --test test/golden.test.mjs",
|
|
55
56
|
"test:compat:technicalindicators": "npm run generate:compat && node ./scripts/compare-technicalindicators.js",
|
|
56
57
|
"test:compat:python": "npm run generate:compat && python ./scripts/compare-python-refs.py",
|
|
57
|
-
"test:compat": "npm run test:compat:technicalindicators && npm run test:compat:python"
|
|
58
|
+
"test:compat": "npm run test:compat:technicalindicators && npm run test:compat:python",
|
|
59
|
+
"example:rsi": "npm run build && node ./examples/rsi-strategy.mjs",
|
|
60
|
+
"example:vwap": "npm run build && node ./examples/vwap-session.mjs",
|
|
61
|
+
"example:funding": "npm run build && node ./examples/funding-arbitrage.mjs",
|
|
62
|
+
"example:rsi:compat": "npm run build && node ./examples/rsi-technicalindicators-check.mjs",
|
|
63
|
+
"example:all": "npm run example:rsi && npm run example:vwap && npm run example:funding && npm run example:rsi:compat"
|
|
58
64
|
},
|
|
59
65
|
"devDependencies": {
|
|
60
66
|
"rimraf": "^5.0.5",
|