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 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.ts`
77
- - `examples/vwap-session.ts`
78
- - `examples/funding-arbitrage.ts`
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
@@ -1,12 +1,9 @@
1
- import { assertSameLength, isNum, makeSeries, mean, stdev } from "./math.js";
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 out;
6
- for (let i = length - 1; i < values.length; i++) {
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
- const basis = sma(values, length);
94
- const upper = makeSeries(values.length);
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 = length - 1; i < values.length; i++) {
97
- const s = stdev(values, i - length + 1, i);
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 window = [];
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
- window.push(value);
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
- window.length = 0;
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.0",
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",