tradelab 1.0.1 → 1.1.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.
Files changed (67) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +75 -12
  3. package/bin/tradelab-mcp.js +7 -0
  4. package/bin/tradelab.js +29 -0
  5. package/dist/cjs/data.cjs +149 -26
  6. package/dist/cjs/index.cjs +1893 -1003
  7. package/dist/cjs/live.cjs +134 -25
  8. package/dist/cjs/ta.cjs +339 -0
  9. package/docs/api-reference.md +46 -0
  10. package/docs/backtest-engine.md +112 -0
  11. package/docs/live-trading.md +51 -0
  12. package/docs/mcp.md +64 -0
  13. package/docs/research.md +103 -0
  14. package/docs/superpowers/plans/2026-00-overview.md +101 -0
  15. package/docs/superpowers/plans/2026-01-metrics-correctness.md +873 -0
  16. package/docs/superpowers/plans/2026-02-indicator-library.md +677 -0
  17. package/docs/superpowers/plans/2026-03-overfitting-toolkit.md +882 -0
  18. package/docs/superpowers/plans/2026-04-async-signals-seeding.md +981 -0
  19. package/docs/superpowers/plans/2026-05-mcp-server.md +758 -0
  20. package/docs/superpowers/plans/2026-06-parallel-param-sweep.md +508 -0
  21. package/docs/superpowers/plans/2026-07-funding-carry-costs.md +535 -0
  22. package/docs/superpowers/plans/2026-08-live-dashboard.md +547 -0
  23. package/docs/superpowers/plans/HANDOFF.md +88 -0
  24. package/examples/liveDashboard.js +33 -0
  25. package/examples/llmSignal.js +33 -0
  26. package/examples/optimize.js +25 -0
  27. package/package.json +16 -2
  28. package/src/engine/asyncSignal.js +28 -0
  29. package/src/engine/backtest.js +13 -1
  30. package/src/engine/backtestAsync.js +27 -0
  31. package/src/engine/backtestTicks.js +13 -2
  32. package/src/engine/barSystemRunner.js +96 -41
  33. package/src/engine/execution.js +39 -0
  34. package/src/engine/grid.js +15 -0
  35. package/src/engine/llmSignal.js +84 -0
  36. package/src/engine/optimize.js +86 -0
  37. package/src/engine/optimizeWorker.js +67 -0
  38. package/src/engine/walkForward.js +1 -0
  39. package/src/index.js +9 -0
  40. package/src/live/dashboard/server.js +120 -0
  41. package/src/live/engine/liveEngine.js +2 -2
  42. package/src/live/index.js +1 -0
  43. package/src/mcp/schemas.js +48 -0
  44. package/src/mcp/server.js +31 -0
  45. package/src/mcp/tools.js +142 -0
  46. package/src/metrics/annualize.js +32 -0
  47. package/src/metrics/benchmark.js +55 -0
  48. package/src/metrics/buildMetrics.js +34 -13
  49. package/src/metrics/finite.js +17 -0
  50. package/src/research/combinations.js +18 -0
  51. package/src/research/cpcv.js +47 -0
  52. package/src/research/deflatedSharpe.js +35 -0
  53. package/src/research/index.js +6 -0
  54. package/src/research/monteCarlo.js +88 -0
  55. package/src/research/pbo.js +69 -0
  56. package/src/research/stats.js +78 -0
  57. package/src/strategies/builtins.js +96 -0
  58. package/src/strategies/index.js +30 -0
  59. package/src/ta/channels.js +67 -0
  60. package/src/ta/index.js +16 -0
  61. package/src/ta/oscillators.js +70 -0
  62. package/src/ta/trend.js +78 -0
  63. package/src/utils/random.js +33 -0
  64. package/templates/dashboard.html +174 -0
  65. package/types/index.d.ts +154 -0
  66. package/types/live.d.ts +15 -0
  67. package/types/ta.d.ts +45 -0
@@ -0,0 +1,677 @@
1
+ # Indicator Library (`tradelab/ta`) Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Ship a real technical-analysis namespace (`tradelab/ta`) with RSI, MACD, Bollinger Bands, VWAP, Supertrend, Donchian, Keltner, and Stochastics, plus re-exports of the existing `ema`/`atr`/swing helpers.
6
+
7
+ **Architecture:** One module per indicator family under `src/ta/`, aggregated by `src/ta/index.js`. Every indicator returns a **full-length array aligned to the input** (warmup region filled with `undefined` or the seed value, matching the existing `atr` convention) so values index 1:1 with candles inside `signal()`. Indicators consume either a `number[]` of closes (oscillators) or the candle array (range-based). A new subpath export `tradelab/ta` is added.
8
+
9
+ **Tech Stack:** Node ESM, `node:test`. No new dependencies.
10
+
11
+ ---
12
+
13
+ ### Task 1: Scaffold the `ta` subpath
14
+
15
+ **Files:**
16
+
17
+ - Create: `src/ta/index.js`
18
+ - Modify: `package.json` (exports map + files)
19
+ - Modify: `scripts/build-cjs.mjs` (add `ta` bundle)
20
+ - Test: `test/ta/scaffold.test.js`
21
+
22
+ - [ ] **Step 1: Write the failing test**
23
+
24
+ ```js
25
+ // test/ta/scaffold.test.js
26
+ import test from "node:test";
27
+ import assert from "node:assert/strict";
28
+ import * as ta from "../../src/ta/index.js";
29
+
30
+ test("ta namespace re-exports existing ema and atr", () => {
31
+ assert.equal(typeof ta.ema, "function");
32
+ assert.equal(typeof ta.atr, "function");
33
+ });
34
+ ```
35
+
36
+ - [ ] **Step 2: Run to verify it fails**
37
+
38
+ Run: `node --test test/ta/scaffold.test.js`
39
+ Expected: FAIL — cannot find module `../../src/ta/index.js`.
40
+
41
+ - [ ] **Step 3: Create src/ta/index.js**
42
+
43
+ ```js
44
+ // src/ta/index.js
45
+ export {
46
+ ema,
47
+ atr,
48
+ swingHigh,
49
+ swingLow,
50
+ detectFVG,
51
+ lastSwing,
52
+ structureState,
53
+ } from "../utils/indicators.js";
54
+ ```
55
+
56
+ - [ ] **Step 4: Run to verify it passes**
57
+
58
+ Run: `node --test test/ta/scaffold.test.js`
59
+ Expected: PASS.
60
+
61
+ - [ ] **Step 5: Add the subpath to package.json**
62
+
63
+ In the `exports` object, after the `"./live"` block, add:
64
+
65
+ ```json
66
+ "./ta": {
67
+ "types": "./types/ta.d.ts",
68
+ "import": "./src/ta/index.js",
69
+ "require": "./dist/cjs/ta.cjs"
70
+ },
71
+ ```
72
+
73
+ - [ ] **Step 6: Add the ta bundle to scripts/build-cjs.mjs**
74
+
75
+ Open `scripts/build-cjs.mjs`, find the array/list of entry points it bundles
76
+ (the existing `index`, `data`, `live` entries), and add a `ta` entry mirroring
77
+ the `live` one. For example, if entries look like:
78
+
79
+ ```js
80
+ const entries = [
81
+ { in: "src/index.js", out: "dist/cjs/index.cjs" },
82
+ { in: "src/data/index.js", out: "dist/cjs/data.cjs" },
83
+ { in: "src/live/index.js", out: "dist/cjs/live.cjs" },
84
+ ];
85
+ ```
86
+
87
+ add:
88
+
89
+ ```js
90
+ { in: "src/ta/index.js", out: "dist/cjs/ta.cjs" },
91
+ ```
92
+
93
+ (If the file uses a different shape, follow that shape — the requirement is one
94
+ esbuild output `dist/cjs/ta.cjs` from `src/ta/index.js`.)
95
+
96
+ - [ ] **Step 7: Verify the build produces the bundle**
97
+
98
+ Run: `npm run build`
99
+ Expected: exit 0; `dist/cjs/ta.cjs` now exists. Confirm with `ls dist/cjs/ta.cjs`.
100
+
101
+ - [ ] **Step 8: Commit**
102
+
103
+ ```bash
104
+ git add src/ta/index.js test/ta/scaffold.test.js package.json scripts/build-cjs.mjs
105
+ git commit -m "feat: scaffold tradelab/ta subpath
106
+
107
+ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
108
+ ```
109
+
110
+ ---
111
+
112
+ ### Task 2: Oscillators — RSI, Stochastics, MACD
113
+
114
+ **Files:**
115
+
116
+ - Create: `src/ta/oscillators.js`
117
+ - Modify: `src/ta/index.js`
118
+ - Test: `test/ta/oscillators.test.js`
119
+
120
+ - [ ] **Step 1: Write the failing test**
121
+
122
+ ```js
123
+ // test/ta/oscillators.test.js
124
+ import test from "node:test";
125
+ import assert from "node:assert/strict";
126
+ import { rsi, stochastic, macd } from "../../src/ta/oscillators.js";
127
+
128
+ test("rsi of a strictly rising series approaches 100", () => {
129
+ const closes = Array.from({ length: 30 }, (_, i) => 100 + i);
130
+ const out = rsi(closes, 14);
131
+ assert.equal(out.length, closes.length);
132
+ assert.equal(out[5], undefined); // warmup
133
+ assert.ok(out[29] > 99);
134
+ });
135
+
136
+ test("rsi of a strictly falling series approaches 0", () => {
137
+ const closes = Array.from({ length: 30 }, (_, i) => 100 - i);
138
+ const out = rsi(closes, 14);
139
+ assert.ok(out[29] < 1);
140
+ });
141
+
142
+ test("macd returns aligned macd/signal/histogram arrays", () => {
143
+ const closes = Array.from({ length: 60 }, (_, i) => 100 + Math.sin(i / 3) * 5);
144
+ const out = macd(closes, 12, 26, 9);
145
+ assert.equal(out.macd.length, closes.length);
146
+ assert.equal(out.signal.length, closes.length);
147
+ assert.equal(out.histogram.length, closes.length);
148
+ assert.ok(Number.isFinite(out.macd[59]));
149
+ });
150
+
151
+ test("stochastic %K stays within [0,100]", () => {
152
+ const bars = Array.from({ length: 40 }, (_, i) => ({
153
+ high: 101 + Math.sin(i),
154
+ low: 99 + Math.sin(i),
155
+ close: 100 + Math.sin(i) * 0.5,
156
+ }));
157
+ const out = stochastic(bars, 14, 3);
158
+ const k = out.k[39];
159
+ assert.ok(k >= 0 && k <= 100);
160
+ assert.equal(out.d.length, bars.length);
161
+ });
162
+ ```
163
+
164
+ - [ ] **Step 2: Run to verify it fails**
165
+
166
+ Run: `node --test test/ta/oscillators.test.js`
167
+ Expected: FAIL — cannot find module.
168
+
169
+ - [ ] **Step 3: Implement src/ta/oscillators.js**
170
+
171
+ ```js
172
+ // src/ta/oscillators.js
173
+ import { ema } from "../utils/indicators.js";
174
+
175
+ /**
176
+ * Wilder's RSI. Returns a full-length array; warmup positions are `undefined`.
177
+ */
178
+ export function rsi(closes, period = 14) {
179
+ const out = new Array(closes.length).fill(undefined);
180
+ if (closes.length <= period) return out;
181
+
182
+ let gainSum = 0;
183
+ let lossSum = 0;
184
+ for (let i = 1; i <= period; i += 1) {
185
+ const change = closes[i] - closes[i - 1];
186
+ if (change >= 0) gainSum += change;
187
+ else lossSum -= change;
188
+ }
189
+ let avgGain = gainSum / period;
190
+ let avgLoss = lossSum / period;
191
+ out[period] = avgLoss === 0 ? 100 : 100 - 100 / (1 + avgGain / avgLoss);
192
+
193
+ for (let i = period + 1; i < closes.length; i += 1) {
194
+ const change = closes[i] - closes[i - 1];
195
+ const gain = change > 0 ? change : 0;
196
+ const loss = change < 0 ? -change : 0;
197
+ avgGain = (avgGain * (period - 1) + gain) / period;
198
+ avgLoss = (avgLoss * (period - 1) + loss) / period;
199
+ out[i] = avgLoss === 0 ? 100 : 100 - 100 / (1 + avgGain / avgLoss);
200
+ }
201
+ return out;
202
+ }
203
+
204
+ /**
205
+ * MACD line, signal line, histogram. All full-length, aligned to input.
206
+ */
207
+ export function macd(closes, fast = 12, slow = 26, signalPeriod = 9) {
208
+ const emaFast = ema(closes, fast);
209
+ const emaSlow = ema(closes, slow);
210
+ const macdLine = closes.map((_, i) => emaFast[i] - emaSlow[i]);
211
+ const signalLine = ema(macdLine, signalPeriod);
212
+ const histogram = macdLine.map((v, i) => v - signalLine[i]);
213
+ return { macd: macdLine, signal: signalLine, histogram };
214
+ }
215
+
216
+ /**
217
+ * Stochastic oscillator %K (smoothed close position in the high-low range) and
218
+ * %D (SMA of %K). `bars` need { high, low, close }.
219
+ */
220
+ export function stochastic(bars, kPeriod = 14, dPeriod = 3) {
221
+ const k = new Array(bars.length).fill(undefined);
222
+ for (let i = kPeriod - 1; i < bars.length; i += 1) {
223
+ let hh = -Infinity;
224
+ let ll = Infinity;
225
+ for (let j = i - kPeriod + 1; j <= i; j += 1) {
226
+ if (bars[j].high > hh) hh = bars[j].high;
227
+ if (bars[j].low < ll) ll = bars[j].low;
228
+ }
229
+ const range = hh - ll;
230
+ k[i] = range === 0 ? 0 : ((bars[i].close - ll) / range) * 100;
231
+ }
232
+
233
+ const d = new Array(bars.length).fill(undefined);
234
+ for (let i = 0; i < bars.length; i += 1) {
235
+ if (i < kPeriod - 1 + dPeriod - 1) continue;
236
+ let sum = 0;
237
+ for (let j = i - dPeriod + 1; j <= i; j += 1) sum += k[j];
238
+ d[i] = sum / dPeriod;
239
+ }
240
+ return { k, d };
241
+ }
242
+ ```
243
+
244
+ - [ ] **Step 4: Run to verify it passes**
245
+
246
+ Run: `node --test test/ta/oscillators.test.js`
247
+ Expected: PASS (4 tests).
248
+
249
+ - [ ] **Step 5: Re-export from src/ta/index.js**
250
+
251
+ Append to `src/ta/index.js`:
252
+
253
+ ```js
254
+ export { rsi, macd, stochastic } from "./oscillators.js";
255
+ ```
256
+
257
+ - [ ] **Step 6: Commit**
258
+
259
+ ```bash
260
+ git add src/ta/oscillators.js src/ta/index.js test/ta/oscillators.test.js
261
+ git commit -m "feat: add rsi, macd, stochastic to ta
262
+
263
+ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
264
+ ```
265
+
266
+ ---
267
+
268
+ ### Task 3: Bands & channels — Bollinger, Donchian, Keltner
269
+
270
+ **Files:**
271
+
272
+ - Create: `src/ta/channels.js`
273
+ - Modify: `src/ta/index.js`
274
+ - Test: `test/ta/channels.test.js`
275
+
276
+ - [ ] **Step 1: Write the failing test**
277
+
278
+ ```js
279
+ // test/ta/channels.test.js
280
+ import test from "node:test";
281
+ import assert from "node:assert/strict";
282
+ import { bollinger, donchian, keltner } from "../../src/ta/channels.js";
283
+
284
+ test("bollinger middle equals SMA and band width scales with stddev mult", () => {
285
+ const closes = Array.from({ length: 30 }, (_, i) => 100 + (i % 2 === 0 ? 1 : -1));
286
+ const out = bollinger(closes, 20, 2);
287
+ assert.equal(out.middle.length, closes.length);
288
+ const i = 25;
289
+ assert.ok(out.upper[i] > out.middle[i]);
290
+ assert.ok(out.lower[i] < out.middle[i]);
291
+ assert.ok(Math.abs(out.upper[i] - out.middle[i] - (out.middle[i] - out.lower[i])) < 1e-9);
292
+ });
293
+
294
+ test("donchian upper is the rolling high and lower is the rolling low", () => {
295
+ const bars = Array.from({ length: 25 }, (_, i) => ({
296
+ high: 100 + i,
297
+ low: 90 + i,
298
+ close: 95 + i,
299
+ }));
300
+ const out = donchian(bars, 20);
301
+ const i = 24;
302
+ assert.equal(out.upper[i], 100 + 24);
303
+ assert.equal(out.lower[i], 90 + 5);
304
+ });
305
+
306
+ test("keltner band is centered on EMA with ATR-scaled width", () => {
307
+ const bars = Array.from({ length: 40 }, (_, i) => ({
308
+ high: 101 + i * 0.1,
309
+ low: 99 + i * 0.1,
310
+ close: 100 + i * 0.1,
311
+ }));
312
+ const out = keltner(bars, 20, 14, 2);
313
+ const i = 39;
314
+ assert.ok(out.upper[i] > out.middle[i]);
315
+ assert.ok(out.lower[i] < out.middle[i]);
316
+ });
317
+ ```
318
+
319
+ - [ ] **Step 2: Run to verify it fails**
320
+
321
+ Run: `node --test test/ta/channels.test.js`
322
+ Expected: FAIL — cannot find module.
323
+
324
+ - [ ] **Step 3: Implement src/ta/channels.js**
325
+
326
+ ```js
327
+ // src/ta/channels.js
328
+ import { ema, atr } from "../utils/indicators.js";
329
+
330
+ function rollingMean(values, period, i) {
331
+ let sum = 0;
332
+ for (let j = i - period + 1; j <= i; j += 1) sum += values[j];
333
+ return sum / period;
334
+ }
335
+
336
+ /**
337
+ * Bollinger Bands. `mult` standard deviations around the SMA middle band.
338
+ */
339
+ export function bollinger(closes, period = 20, mult = 2) {
340
+ const middle = new Array(closes.length).fill(undefined);
341
+ const upper = new Array(closes.length).fill(undefined);
342
+ const lower = new Array(closes.length).fill(undefined);
343
+ for (let i = period - 1; i < closes.length; i += 1) {
344
+ const avg = rollingMean(closes, period, i);
345
+ let variance = 0;
346
+ for (let j = i - period + 1; j <= i; j += 1) variance += (closes[j] - avg) ** 2;
347
+ const sd = Math.sqrt(variance / period);
348
+ middle[i] = avg;
349
+ upper[i] = avg + mult * sd;
350
+ lower[i] = avg - mult * sd;
351
+ }
352
+ return { middle, upper, lower };
353
+ }
354
+
355
+ /**
356
+ * Donchian channel: rolling highest-high / lowest-low over `period` bars.
357
+ */
358
+ export function donchian(bars, period = 20) {
359
+ const upper = new Array(bars.length).fill(undefined);
360
+ const lower = new Array(bars.length).fill(undefined);
361
+ const middle = new Array(bars.length).fill(undefined);
362
+ for (let i = period - 1; i < bars.length; i += 1) {
363
+ let hh = -Infinity;
364
+ let ll = Infinity;
365
+ for (let j = i - period + 1; j <= i; j += 1) {
366
+ if (bars[j].high > hh) hh = bars[j].high;
367
+ if (bars[j].low < ll) ll = bars[j].low;
368
+ }
369
+ upper[i] = hh;
370
+ lower[i] = ll;
371
+ middle[i] = (hh + ll) / 2;
372
+ }
373
+ return { upper, lower, middle };
374
+ }
375
+
376
+ /**
377
+ * Keltner channel: EMA middle, +/- mult * ATR width.
378
+ */
379
+ export function keltner(bars, emaPeriod = 20, atrPeriod = 14, mult = 2) {
380
+ const closes = bars.map((b) => b.close);
381
+ const mid = ema(closes, emaPeriod);
382
+ const range = atr(bars, atrPeriod);
383
+ const upper = new Array(bars.length).fill(undefined);
384
+ const lower = new Array(bars.length).fill(undefined);
385
+ const middle = new Array(bars.length).fill(undefined);
386
+ for (let i = 0; i < bars.length; i += 1) {
387
+ if (range[i] === undefined) continue;
388
+ middle[i] = mid[i];
389
+ upper[i] = mid[i] + mult * range[i];
390
+ lower[i] = mid[i] - mult * range[i];
391
+ }
392
+ return { upper, lower, middle };
393
+ }
394
+ ```
395
+
396
+ - [ ] **Step 4: Run to verify it passes**
397
+
398
+ Run: `node --test test/ta/channels.test.js`
399
+ Expected: PASS (3 tests).
400
+
401
+ - [ ] **Step 5: Re-export**
402
+
403
+ Append to `src/ta/index.js`:
404
+
405
+ ```js
406
+ export { bollinger, donchian, keltner } from "./channels.js";
407
+ ```
408
+
409
+ - [ ] **Step 6: Commit**
410
+
411
+ ```bash
412
+ git add src/ta/channels.js src/ta/index.js test/ta/channels.test.js
413
+ git commit -m "feat: add bollinger, donchian, keltner to ta
414
+
415
+ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
416
+ ```
417
+
418
+ ---
419
+
420
+ ### Task 4: Trend & volume — Supertrend, VWAP
421
+
422
+ **Files:**
423
+
424
+ - Create: `src/ta/trend.js`
425
+ - Modify: `src/ta/index.js`
426
+ - Test: `test/ta/trend.test.js`
427
+
428
+ - [ ] **Step 1: Write the failing test**
429
+
430
+ ```js
431
+ // test/ta/trend.test.js
432
+ import test from "node:test";
433
+ import assert from "node:assert/strict";
434
+ import { supertrend, vwap } from "../../src/ta/trend.js";
435
+
436
+ test("supertrend marks an uptrend (dir=1) on a rising series", () => {
437
+ const bars = Array.from({ length: 40 }, (_, i) => ({
438
+ high: 101 + i,
439
+ low: 99 + i,
440
+ close: 100 + i,
441
+ }));
442
+ const out = supertrend(bars, 10, 3);
443
+ assert.equal(out.direction.length, bars.length);
444
+ assert.equal(out.direction[39], 1);
445
+ assert.ok(out.line[39] < bars[39].close); // support trails below price
446
+ });
447
+
448
+ test("vwap resets each UTC day and lies within the day's range", () => {
449
+ const day1 = Date.UTC(2025, 0, 2, 14, 30);
450
+ const day2 = Date.UTC(2025, 0, 3, 14, 30);
451
+ const bars = [
452
+ { time: day1, high: 102, low: 98, close: 100, volume: 10 },
453
+ { time: day1 + 60000, high: 104, low: 100, close: 103, volume: 30 },
454
+ { time: day2, high: 50, low: 48, close: 49, volume: 5 },
455
+ ];
456
+ const out = vwap(bars);
457
+ assert.equal(out.length, 3);
458
+ assert.ok(out[1] >= 98 && out[1] <= 104);
459
+ // day2 resets: vwap equals the typical price of the single day-2 bar
460
+ assert.ok(Math.abs(out[2] - (50 + 48 + 49) / 3) < 1e-9);
461
+ });
462
+ ```
463
+
464
+ - [ ] **Step 2: Run to verify it fails**
465
+
466
+ Run: `node --test test/ta/trend.test.js`
467
+ Expected: FAIL — cannot find module.
468
+
469
+ - [ ] **Step 3: Implement src/ta/trend.js**
470
+
471
+ ```js
472
+ // src/ta/trend.js
473
+ import { atr } from "../utils/indicators.js";
474
+
475
+ /**
476
+ * Supertrend. Returns { line, direction } full-length.
477
+ * direction: 1 = uptrend (line is support below price), -1 = downtrend.
478
+ */
479
+ export function supertrend(bars, period = 10, mult = 3) {
480
+ const range = atr(bars, period);
481
+ const line = new Array(bars.length).fill(undefined);
482
+ const direction = new Array(bars.length).fill(undefined);
483
+
484
+ let prevUpper = Infinity;
485
+ let prevLower = -Infinity;
486
+ let prevDir = 1;
487
+
488
+ for (let i = 0; i < bars.length; i += 1) {
489
+ if (range[i] === undefined) continue;
490
+ const mid = (bars[i].high + bars[i].low) / 2;
491
+ const basicUpper = mid + mult * range[i];
492
+ const basicLower = mid - mult * range[i];
493
+ const close = bars[i].close;
494
+ const prevClose = i > 0 ? bars[i - 1].close : close;
495
+
496
+ const upper = basicUpper < prevUpper || prevClose > prevUpper ? basicUpper : prevUpper;
497
+ const lower = basicLower > prevLower || prevClose < prevLower ? basicLower : prevLower;
498
+
499
+ let dir = prevDir;
500
+ if (prevDir === 1 && close < lower) dir = -1;
501
+ else if (prevDir === -1 && close > upper) dir = 1;
502
+
503
+ line[i] = dir === 1 ? lower : upper;
504
+ direction[i] = dir;
505
+
506
+ prevUpper = upper;
507
+ prevLower = lower;
508
+ prevDir = dir;
509
+ }
510
+ return { line, direction };
511
+ }
512
+
513
+ function dayKeyUTC(timeMs) {
514
+ const d = new Date(timeMs);
515
+ return d.getUTCFullYear() * 10000 + (d.getUTCMonth() + 1) * 100 + d.getUTCDate();
516
+ }
517
+
518
+ /**
519
+ * Session VWAP, reset on each UTC calendar day. `bars` need
520
+ * { time, high, low, close, volume }. When volume is missing/zero, falls back
521
+ * to an unweighted cumulative typical-price average for that day.
522
+ */
523
+ export function vwap(bars) {
524
+ const out = new Array(bars.length).fill(undefined);
525
+ let currentDay = null;
526
+ let cumPV = 0;
527
+ let cumV = 0;
528
+ let cumTP = 0;
529
+ let count = 0;
530
+
531
+ for (let i = 0; i < bars.length; i += 1) {
532
+ const day = dayKeyUTC(bars[i].time);
533
+ if (day !== currentDay) {
534
+ currentDay = day;
535
+ cumPV = 0;
536
+ cumV = 0;
537
+ cumTP = 0;
538
+ count = 0;
539
+ }
540
+ const tp = (bars[i].high + bars[i].low + bars[i].close) / 3;
541
+ const vol = Number.isFinite(bars[i].volume) ? bars[i].volume : 0;
542
+ cumPV += tp * vol;
543
+ cumV += vol;
544
+ cumTP += tp;
545
+ count += 1;
546
+ out[i] = cumV > 0 ? cumPV / cumV : cumTP / count;
547
+ }
548
+ return out;
549
+ }
550
+ ```
551
+
552
+ - [ ] **Step 4: Run to verify it passes**
553
+
554
+ Run: `node --test test/ta/trend.test.js`
555
+ Expected: PASS (2 tests).
556
+
557
+ - [ ] **Step 5: Re-export**
558
+
559
+ Append to `src/ta/index.js`:
560
+
561
+ ```js
562
+ export { supertrend, vwap } from "./trend.js";
563
+ ```
564
+
565
+ - [ ] **Step 6: Commit**
566
+
567
+ ```bash
568
+ git add src/ta/trend.js src/ta/index.js test/ta/trend.test.js
569
+ git commit -m "feat: add supertrend and session vwap to ta
570
+
571
+ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
572
+ ```
573
+
574
+ ---
575
+
576
+ ### Task 5: Types + docs + README mention
577
+
578
+ **Files:**
579
+
580
+ - Create: `types/ta.d.ts`
581
+ - Modify: `docs/api-reference.md`
582
+ - Modify: `README.md` (mention the `ta` subpath under importing)
583
+
584
+ - [ ] **Step 1: Write the type declaration file**
585
+
586
+ ```ts
587
+ // types/ta.d.ts
588
+ export type Candle = {
589
+ time: number;
590
+ open: number;
591
+ high: number;
592
+ low: number;
593
+ close: number;
594
+ volume?: number;
595
+ };
596
+
597
+ export function ema(values: number[], period?: number): number[];
598
+ export function atr(bars: Candle[], period?: number): (number | undefined)[];
599
+ export function rsi(closes: number[], period?: number): (number | undefined)[];
600
+ export function macd(
601
+ closes: number[],
602
+ fast?: number,
603
+ slow?: number,
604
+ signalPeriod?: number
605
+ ): { macd: number[]; signal: number[]; histogram: number[] };
606
+ export function stochastic(
607
+ bars: Candle[],
608
+ kPeriod?: number,
609
+ dPeriod?: number
610
+ ): { k: (number | undefined)[]; d: (number | undefined)[] };
611
+ export function bollinger(
612
+ closes: number[],
613
+ period?: number,
614
+ mult?: number
615
+ ): { middle: (number | undefined)[]; upper: (number | undefined)[]; lower: (number | undefined)[] };
616
+ export function donchian(
617
+ bars: Candle[],
618
+ period?: number
619
+ ): { upper: (number | undefined)[]; lower: (number | undefined)[]; middle: (number | undefined)[] };
620
+ export function keltner(
621
+ bars: Candle[],
622
+ emaPeriod?: number,
623
+ atrPeriod?: number,
624
+ mult?: number
625
+ ): { upper: (number | undefined)[]; lower: (number | undefined)[]; middle: (number | undefined)[] };
626
+ export function supertrend(
627
+ bars: Candle[],
628
+ period?: number,
629
+ mult?: number
630
+ ): { line: (number | undefined)[]; direction: (number | undefined)[] };
631
+ export function vwap(bars: Candle[]): (number | undefined)[];
632
+ ```
633
+
634
+ - [ ] **Step 2: Typecheck**
635
+
636
+ Run: `npm run typecheck`
637
+ Expected: exit 0 (no errors introduced).
638
+
639
+ - [ ] **Step 3: Document in docs/api-reference.md**
640
+
641
+ Add a `### tradelab/ta` section listing each indicator, its input shape (closes
642
+ array vs candle array), and its return shape — copy the signatures from
643
+ `types/ta.d.ts` and add one line per indicator describing what it computes.
644
+
645
+ - [ ] **Step 4: Mention in README importing section**
646
+
647
+ Under `## Importing` → `### ESM`, add:
648
+
649
+ ```js
650
+ import { rsi, macd, bollinger, vwap, supertrend } from "tradelab/ta";
651
+ ```
652
+
653
+ and the CJS equivalent under `### CommonJS`.
654
+
655
+ - [ ] **Step 5: Full build + test + lint, then commit**
656
+
657
+ ```bash
658
+ npm run build && npm run lint && npm run format:check && npm test
659
+ ```
660
+
661
+ Expected: all green.
662
+
663
+ ```bash
664
+ git add types/ta.d.ts docs/api-reference.md README.md
665
+ git commit -m "docs: document tradelab/ta indicator namespace
666
+
667
+ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
668
+ ```
669
+
670
+ ---
671
+
672
+ ## Self-review checklist
673
+
674
+ - [ ] Every indicator returns a full-length array/object aligned to the input (warmup = `undefined`), matching the existing `atr` convention so `signal()` can index by candle position. ✔
675
+ - [ ] `tradelab/ta` resolves in ESM (`exports` map), CJS (`dist/cjs/ta.cjs` from build), and types (`types/ta.d.ts`). ✔ (Tasks 1, 5)
676
+ - [ ] No existing export in `src/utils/indicators.js` changed — `ta` re-exports them. ✔
677
+ - [ ] Signatures in `types/ta.d.ts` match the implementations (e.g. `macd` returns `{ macd, signal, histogram }`, `stochastic` returns `{ k, d }`). ✔