tradelab 1.1.0 → 1.2.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 +46 -0
- package/README.md +185 -388
- package/dist/cjs/index.cjs +31 -9
- package/dist/cjs/live.cjs +409 -7
- package/docs/README.md +32 -66
- package/docs/api-reference.md +269 -144
- package/docs/backtest-engine.md +167 -321
- package/docs/data-reporting-cli.md +114 -156
- package/docs/examples.md +6 -6
- package/docs/live-trading.md +254 -134
- package/docs/mcp.md +244 -23
- package/docs/research.md +99 -45
- package/examples/mcpLiveTrading.js +77 -0
- package/package.json +11 -3
- package/src/engine/optimize.js +25 -1
- package/src/engine/portfolio.js +4 -1
- package/src/live/dashboard/server.js +67 -8
- package/src/live/engine/paperEngine.js +5 -0
- package/src/live/index.js +2 -0
- package/src/live/session.js +402 -0
- package/src/mcp/liveTools.js +179 -0
- package/src/mcp/schemas.js +119 -0
- package/src/mcp/server.js +5 -1
- package/src/mcp/tools.js +125 -2
- package/templates/dashboard.html +595 -108
- package/types/index.d.ts +25 -0
- package/types/live.d.ts +99 -0
- package/types/mcp.d.ts +17 -0
- package/docs/superpowers/plans/2026-00-overview.md +0 -101
- package/docs/superpowers/plans/2026-01-metrics-correctness.md +0 -873
- package/docs/superpowers/plans/2026-02-indicator-library.md +0 -677
- package/docs/superpowers/plans/2026-03-overfitting-toolkit.md +0 -882
- package/docs/superpowers/plans/2026-04-async-signals-seeding.md +0 -981
- package/docs/superpowers/plans/2026-05-mcp-server.md +0 -758
- package/docs/superpowers/plans/2026-06-parallel-param-sweep.md +0 -508
- package/docs/superpowers/plans/2026-07-funding-carry-costs.md +0 -535
- package/docs/superpowers/plans/2026-08-live-dashboard.md +0 -547
- package/docs/superpowers/plans/HANDOFF.md +0 -88
|
@@ -1,677 +0,0 @@
|
|
|
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 }`). ✔
|