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
package/dist/cjs/live.cjs CHANGED
@@ -56,6 +56,7 @@ __export(index_exports, {
56
56
  createCandleAggregator: () => createCandleAggregator,
57
57
  createClock: () => createClock,
58
58
  createCoinbaseBroker: () => createCoinbaseBroker,
59
+ createDashboardServer: () => createDashboardServer,
59
60
  createEventBus: () => createEventBus,
60
61
  createInteractiveBrokersBroker: () => createInteractiveBrokersBroker,
61
62
  createJsonFileStorage: () => createJsonFileStorage,
@@ -435,10 +436,10 @@ var AlpacaBroker = class extends BrokerAdapter {
435
436
  ...extra
436
437
  };
437
438
  }
438
- async _request(method, path2, { query = null, body = null, dataApi = false } = {}) {
439
+ async _request(method, path3, { query = null, body = null, dataApi = false } = {}) {
439
440
  if (!this.fetch) throw new Error("global fetch is unavailable");
440
441
  const base = dataApi ? this.dataUrl : this.baseUrl;
441
- const url = withQuery(`${base}${path2}`, query || {});
442
+ const url = withQuery(`${base}${path3}`, query || {});
442
443
  const response = await this.fetch(url, {
443
444
  method,
444
445
  headers: this._headers(),
@@ -648,11 +649,11 @@ var BinanceBroker = class extends BrokerAdapter {
648
649
  const signature = import_node_crypto.default.createHmac("sha256", this.config.apiSecret || "").update(payload).digest("hex");
649
650
  return { ...base, signature };
650
651
  }
651
- async _request(method, path2, { signed = false, params = {}, body = null } = {}) {
652
+ async _request(method, path3, { signed = false, params = {}, body = null } = {}) {
652
653
  if (!this.fetch) throw new Error("global fetch is unavailable");
653
654
  const finalParams = signed ? this._signedParams(params) : params;
654
655
  const qs = queryString(finalParams);
655
- const url = new import_node_url2.URL(`${this.baseUrl}${path2}${qs ? `?${qs}` : ""}`);
656
+ const url = new import_node_url2.URL(`${this.baseUrl}${path3}${qs ? `?${qs}` : ""}`);
656
657
  const headers = {
657
658
  "content-type": "application/json"
658
659
  };
@@ -671,8 +672,8 @@ var BinanceBroker = class extends BrokerAdapter {
671
672
  return payload;
672
673
  }
673
674
  async getServerTime() {
674
- const path2 = this.config.futures ? "/fapi/v1/time" : "/api/v3/time";
675
- const data = await this._request("GET", path2);
675
+ const path3 = this.config.futures ? "/fapi/v1/time" : "/api/v3/time";
676
+ const data = await this._request("GET", path3);
676
677
  return Number(data.serverTime || Date.now());
677
678
  }
678
679
  async getAccount() {
@@ -735,8 +736,8 @@ var BinanceBroker = class extends BrokerAdapter {
735
736
  return payload;
736
737
  }
737
738
  async submitOrder(order) {
738
- const path2 = this.config.futures ? "/fapi/v1/order" : "/api/v3/order";
739
- const response = await this._request("POST", path2, {
739
+ const path3 = this.config.futures ? "/fapi/v1/order" : "/api/v3/order";
740
+ const response = await this._request("POST", path3, {
740
741
  signed: true,
741
742
  params: this._orderPayload(order)
742
743
  });
@@ -757,8 +758,8 @@ var BinanceBroker = class extends BrokerAdapter {
757
758
  return receipt;
758
759
  }
759
760
  async cancelOrder(orderId) {
760
- const path2 = this.config.futures ? "/fapi/v1/order" : "/api/v3/order";
761
- await this._request("DELETE", path2, {
761
+ const path3 = this.config.futures ? "/fapi/v1/order" : "/api/v3/order";
762
+ await this._request("DELETE", path3, {
762
763
  signed: true,
763
764
  params: {
764
765
  orderId
@@ -767,8 +768,8 @@ var BinanceBroker = class extends BrokerAdapter {
767
768
  this.emit("order:canceled", { orderId: String(orderId) });
768
769
  }
769
770
  async modifyOrder(orderId, changes = {}) {
770
- const path2 = this.config.futures ? "/fapi/v1/order" : "/api/v3/order";
771
- const response = await this._request("PUT", path2, {
771
+ const path3 = this.config.futures ? "/fapi/v1/order" : "/api/v3/order";
772
+ const response = await this._request("PUT", path3, {
772
773
  signed: true,
773
774
  params: {
774
775
  orderId,
@@ -793,8 +794,8 @@ var BinanceBroker = class extends BrokerAdapter {
793
794
  return receipt;
794
795
  }
795
796
  async getOpenOrders() {
796
- const path2 = this.config.futures ? "/fapi/v1/openOrders" : "/api/v3/openOrders";
797
- const rows = await this._request("GET", path2, { signed: true });
797
+ const path3 = this.config.futures ? "/fapi/v1/openOrders" : "/api/v3/openOrders";
798
+ const rows = await this._request("GET", path3, { signed: true });
798
799
  return rows.map((row) => ({
799
800
  orderId: String(row.orderId),
800
801
  clientOrderId: row.clientOrderId,
@@ -810,8 +811,8 @@ var BinanceBroker = class extends BrokerAdapter {
810
811
  }));
811
812
  }
812
813
  async getOrderStatus(orderId) {
813
- const path2 = this.config.futures ? "/fapi/v1/order" : "/api/v3/order";
814
- const row = await this._request("GET", path2, {
814
+ const path3 = this.config.futures ? "/fapi/v1/order" : "/api/v3/order";
815
+ const row = await this._request("GET", path3, {
815
816
  signed: true,
816
817
  params: { orderId }
817
818
  });
@@ -873,8 +874,8 @@ var BinanceBroker = class extends BrokerAdapter {
873
874
  };
874
875
  }
875
876
  async getHistoricalBars(symbol, interval, limit = 200) {
876
- const path2 = this.config.futures ? "/fapi/v1/klines" : "/api/v3/klines";
877
- const rows = await this._request("GET", path2, {
877
+ const path3 = this.config.futures ? "/fapi/v1/klines" : "/api/v3/klines";
878
+ const rows = await this._request("GET", path3, {
878
879
  params: { symbol, interval, limit }
879
880
  });
880
881
  const bars = rows.map((row) => ({
@@ -898,7 +899,7 @@ var import_node_url3 = require("node:url");
898
899
  function base64url(input) {
899
900
  return Buffer.from(input).toString("base64url");
900
901
  }
901
- function buildJwt({ key, secret, method, host, path: path2 }) {
902
+ function buildJwt({ key, secret, method, host, path: path3 }) {
902
903
  const now = Math.floor(Date.now() / 1e3);
903
904
  const header = { alg: "HS256", typ: "JWT", kid: key };
904
905
  const payload = {
@@ -906,7 +907,7 @@ function buildJwt({ key, secret, method, host, path: path2 }) {
906
907
  sub: key,
907
908
  nbf: now - 5,
908
909
  exp: now + 120,
909
- uri: `${method.toUpperCase()} ${host}${path2}`
910
+ uri: `${method.toUpperCase()} ${host}${path3}`
910
911
  };
911
912
  const encodedHeader = base64url(JSON.stringify(header));
912
913
  const encodedPayload = base64url(JSON.stringify(payload));
@@ -965,9 +966,9 @@ var CoinbaseBroker = class extends BrokerAdapter {
965
966
  path: target.pathname
966
967
  });
967
968
  }
968
- async _request(method, path2, { query = {}, body = null } = {}) {
969
+ async _request(method, path3, { query = {}, body = null } = {}) {
969
970
  if (!this.fetch) throw new Error("global fetch is unavailable");
970
- const url = new import_node_url3.URL(`${this.baseUrl}${path2}`);
971
+ const url = new import_node_url3.URL(`${this.baseUrl}${path3}`);
971
972
  for (const [key, value] of Object.entries(query || {})) {
972
973
  if (value === void 0 || value === null) continue;
973
974
  url.searchParams.set(key, String(value));
@@ -1726,6 +1727,7 @@ function dayKeyET(timeMs) {
1726
1727
  const pseudoEtTime = anchor.getTime() + hoursET * 60 * 60 * 1e3 + minutesETDay * 60 * 1e3;
1727
1728
  return dayKeyUTC(pseudoEtTime);
1728
1729
  }
1730
+ var MS_PER_YEAR = 365 * 24 * 60 * 60 * 1e3;
1729
1731
 
1730
1732
  // src/live/engine/candleAggregator.js
1731
1733
  function intervalToMs(interval) {
@@ -2549,9 +2551,9 @@ function asNumber2(value) {
2549
2551
  function formatIsoTime(time) {
2550
2552
  return Number.isFinite(time) ? new Date(time).toISOString() : "invalid-time";
2551
2553
  }
2552
- function callSignalWithContext({ signal, context, index, bar, symbol }) {
2554
+ async function callSignalWithContextAsync({ signal, context, index, bar, symbol }) {
2553
2555
  try {
2554
- return signal(context);
2556
+ return await signal(context);
2555
2557
  } catch (error) {
2556
2558
  const cause = error instanceof Error ? error.message : String(error);
2557
2559
  throw new Error(
@@ -3038,7 +3040,7 @@ var LiveEngine = class {
3038
3040
  }
3039
3041
  if (!this.openPosition && !this.pendingOrder) {
3040
3042
  const context = this._signalContext(bar);
3041
- const rawSignal = callSignalWithContext({
3043
+ const rawSignal = await callSignalWithContextAsync({
3042
3044
  signal: this.options.signal,
3043
3045
  context,
3044
3046
  index: context.index,
@@ -3309,6 +3311,112 @@ var LiveOrchestrator = class {
3309
3311
  function createLiveOrchestrator(options) {
3310
3312
  return new LiveOrchestrator(options);
3311
3313
  }
3314
+
3315
+ // src/live/dashboard/server.js
3316
+ var import_node_http = __toESM(require("node:http"), 1);
3317
+ var import_node_fs2 = require("node:fs");
3318
+ var import_node_path2 = __toESM(require("node:path"), 1);
3319
+ var import_node_url4 = require("node:url");
3320
+ var import_meta = {};
3321
+ var FALLBACK_HTML = `<!doctype html>
3322
+ <html lang="en">
3323
+ <head>
3324
+ <meta charset="utf-8" />
3325
+ <title>tradelab live</title>
3326
+ </head>
3327
+ <body>
3328
+ <h1>tradelab live</h1>
3329
+ <pre id="state"></pre>
3330
+ <script>
3331
+ fetch("/state")
3332
+ .then((res) => res.json())
3333
+ .then((state) => {
3334
+ document.getElementById("state").textContent = JSON.stringify(state, null, 2);
3335
+ });
3336
+ </script>
3337
+ </body>
3338
+ </html>`;
3339
+ function readDashboardHtml() {
3340
+ if (import_meta.url) {
3341
+ const here = import_node_path2.default.dirname((0, import_node_url4.fileURLToPath)(import_meta.url));
3342
+ const htmlPath = import_node_path2.default.join(here, "..", "..", "..", "templates", "dashboard.html");
3343
+ return (0, import_node_fs2.readFileSync)(htmlPath, "utf8");
3344
+ }
3345
+ try {
3346
+ return (0, import_node_fs2.readFileSync)(import_node_path2.default.join(process.cwd(), "templates", "dashboard.html"), "utf8");
3347
+ } catch {
3348
+ return FALLBACK_HTML;
3349
+ }
3350
+ }
3351
+ function createDashboardServer({ source, port = 4317, maxBuffer = 200 }) {
3352
+ if (!source?.eventBus || typeof source.eventBus.onAny !== "function") {
3353
+ throw new Error("dashboard source must expose an eventBus with onAny()");
3354
+ }
3355
+ const recent = [];
3356
+ const clients = /* @__PURE__ */ new Set();
3357
+ const unsubscribe = source.eventBus.onAny(({ event, payload }) => {
3358
+ const msg = { event, payload, t: Date.now() };
3359
+ recent.push(msg);
3360
+ if (recent.length > maxBuffer) recent.shift();
3361
+ const frame = `data: ${JSON.stringify(msg)}
3362
+
3363
+ `;
3364
+ for (const res of clients) res.write(frame);
3365
+ });
3366
+ const server = import_node_http.default.createServer((req, res) => {
3367
+ const url = (req.url || "/").split("?")[0];
3368
+ if (url === "/") {
3369
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
3370
+ res.end(readDashboardHtml());
3371
+ return;
3372
+ }
3373
+ if (url === "/state") {
3374
+ const status = typeof source.getStatus === "function" ? source.getStatus() : {};
3375
+ res.writeHead(200, { "Content-Type": "application/json" });
3376
+ res.end(JSON.stringify(status));
3377
+ return;
3378
+ }
3379
+ if (url === "/events") {
3380
+ res.writeHead(200, {
3381
+ "Content-Type": "text/event-stream",
3382
+ "Cache-Control": "no-cache",
3383
+ Connection: "keep-alive"
3384
+ });
3385
+ res.flushHeaders();
3386
+ for (const msg of recent) res.write(`data: ${JSON.stringify(msg)}
3387
+
3388
+ `);
3389
+ clients.add(res);
3390
+ req.on("close", () => clients.delete(res));
3391
+ return;
3392
+ }
3393
+ res.writeHead(404, { "Content-Type": "text/plain" });
3394
+ res.end("not found");
3395
+ });
3396
+ return {
3397
+ start() {
3398
+ return new Promise((resolve) => {
3399
+ server.listen(port, () => {
3400
+ const address = server.address();
3401
+ const actualPort = typeof address === "object" && address ? address.port : port;
3402
+ resolve(`http://localhost:${actualPort}`);
3403
+ });
3404
+ });
3405
+ },
3406
+ close() {
3407
+ unsubscribe();
3408
+ for (const res of clients) res.end();
3409
+ clients.clear();
3410
+ return new Promise((resolve, reject) => {
3411
+ server.close((error) => {
3412
+ if (error) reject(error);
3413
+ else resolve();
3414
+ });
3415
+ });
3416
+ },
3417
+ server
3418
+ };
3419
+ }
3312
3420
  // Annotate the CommonJS export names for ESM import in node:
3313
3421
  0 && (module.exports = {
3314
3422
  AlpacaBroker,
@@ -3337,6 +3445,7 @@ function createLiveOrchestrator(options) {
3337
3445
  createCandleAggregator,
3338
3446
  createClock,
3339
3447
  createCoinbaseBroker,
3448
+ createDashboardServer,
3340
3449
  createEventBus,
3341
3450
  createInteractiveBrokersBroker,
3342
3451
  createJsonFileStorage,
@@ -0,0 +1,339 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/ta/index.js
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ atr: () => atr,
24
+ bollinger: () => bollinger,
25
+ detectFVG: () => detectFVG,
26
+ donchian: () => donchian,
27
+ ema: () => ema,
28
+ keltner: () => keltner,
29
+ lastSwing: () => lastSwing,
30
+ macd: () => macd,
31
+ rsi: () => rsi,
32
+ stochastic: () => stochastic,
33
+ structureState: () => structureState,
34
+ supertrend: () => supertrend,
35
+ swingHigh: () => swingHigh,
36
+ swingLow: () => swingLow,
37
+ vwap: () => vwap
38
+ });
39
+ module.exports = __toCommonJS(index_exports);
40
+
41
+ // src/utils/indicators.js
42
+ function ema(values, period = 14) {
43
+ if (!values?.length) return [];
44
+ const lookback = Math.max(1, period | 0);
45
+ const output = new Array(values.length);
46
+ let warmupSum = 0;
47
+ for (let index = 0; index < values.length; index += 1) {
48
+ const value = values[index];
49
+ if (!Number.isFinite(value)) {
50
+ output[index] = index === 0 ? 0 : output[index - 1];
51
+ continue;
52
+ }
53
+ if (index < lookback) {
54
+ warmupSum += value;
55
+ output[index] = index === lookback - 1 ? warmupSum / lookback : value;
56
+ continue;
57
+ }
58
+ const smoothing = 2 / (lookback + 1);
59
+ output[index] = value * smoothing + output[index - 1] * (1 - smoothing);
60
+ }
61
+ return output;
62
+ }
63
+ function swingHigh(bars, index, left = 2, right = 2) {
64
+ if (index < left || index + right >= bars.length) return false;
65
+ const high = bars[index].high;
66
+ for (let cursor = index - left; cursor <= index + right; cursor += 1) {
67
+ if (cursor !== index && bars[cursor].high >= high) return false;
68
+ }
69
+ return true;
70
+ }
71
+ function swingLow(bars, index, left = 2, right = 2) {
72
+ if (index < left || index + right >= bars.length) return false;
73
+ const low = bars[index].low;
74
+ for (let cursor = index - left; cursor <= index + right; cursor += 1) {
75
+ if (cursor !== index && bars[cursor].low <= low) return false;
76
+ }
77
+ return true;
78
+ }
79
+ function detectFVG(bars, index) {
80
+ if (index < 2) return null;
81
+ const first = bars[index - 2];
82
+ const third = bars[index];
83
+ if (first.high < third.low) {
84
+ return {
85
+ type: "bull",
86
+ top: first.high,
87
+ bottom: third.low,
88
+ mid: (first.high + third.low) / 2
89
+ };
90
+ }
91
+ if (first.low > third.high) {
92
+ return {
93
+ type: "bear",
94
+ top: third.high,
95
+ bottom: first.low,
96
+ mid: (third.high + first.low) / 2
97
+ };
98
+ }
99
+ return null;
100
+ }
101
+ function lastSwing(bars, index, direction) {
102
+ for (let cursor = index - 1; cursor >= 0; cursor -= 1) {
103
+ if (direction === "up" && swingLow(bars, cursor)) {
104
+ return { idx: cursor, price: bars[cursor].low };
105
+ }
106
+ if (direction === "down" && swingHigh(bars, cursor)) {
107
+ return { idx: cursor, price: bars[cursor].high };
108
+ }
109
+ }
110
+ return null;
111
+ }
112
+ function structureState(bars, index) {
113
+ return {
114
+ lastLow: lastSwing(bars, index, "up"),
115
+ lastHigh: lastSwing(bars, index, "down")
116
+ };
117
+ }
118
+ function atr(bars, period = 14) {
119
+ if (!bars?.length || period <= 0) return [];
120
+ const trueRanges = new Array(bars.length);
121
+ for (let index = 0; index < bars.length; index += 1) {
122
+ if (index === 0) {
123
+ trueRanges[index] = bars[index].high - bars[index].low;
124
+ continue;
125
+ }
126
+ const high = bars[index].high;
127
+ const low = bars[index].low;
128
+ const previousClose = bars[index - 1].close;
129
+ trueRanges[index] = Math.max(
130
+ high - low,
131
+ Math.abs(high - previousClose),
132
+ Math.abs(low - previousClose)
133
+ );
134
+ }
135
+ const output = new Array(trueRanges.length);
136
+ let previousAtr;
137
+ for (let index = 0; index < trueRanges.length; index += 1) {
138
+ if (index < period) {
139
+ output[index] = void 0;
140
+ if (index === period - 1) {
141
+ let seed = 0;
142
+ for (let cursor = 0; cursor < period; cursor += 1) {
143
+ seed += trueRanges[cursor];
144
+ }
145
+ previousAtr = seed / period;
146
+ output[index] = previousAtr;
147
+ }
148
+ continue;
149
+ }
150
+ previousAtr = (previousAtr * (period - 1) + trueRanges[index]) / period;
151
+ output[index] = previousAtr;
152
+ }
153
+ return output;
154
+ }
155
+
156
+ // src/ta/oscillators.js
157
+ function rsi(closes, period = 14) {
158
+ const out = new Array(closes.length).fill(void 0);
159
+ if (closes.length <= period) return out;
160
+ let gainSum = 0;
161
+ let lossSum = 0;
162
+ for (let i = 1; i <= period; i += 1) {
163
+ const change = closes[i] - closes[i - 1];
164
+ if (change >= 0) gainSum += change;
165
+ else lossSum -= change;
166
+ }
167
+ let avgGain = gainSum / period;
168
+ let avgLoss = lossSum / period;
169
+ out[period] = avgLoss === 0 ? 100 : 100 - 100 / (1 + avgGain / avgLoss);
170
+ for (let i = period + 1; i < closes.length; i += 1) {
171
+ const change = closes[i] - closes[i - 1];
172
+ const gain = change > 0 ? change : 0;
173
+ const loss = change < 0 ? -change : 0;
174
+ avgGain = (avgGain * (period - 1) + gain) / period;
175
+ avgLoss = (avgLoss * (period - 1) + loss) / period;
176
+ out[i] = avgLoss === 0 ? 100 : 100 - 100 / (1 + avgGain / avgLoss);
177
+ }
178
+ return out;
179
+ }
180
+ function macd(closes, fast = 12, slow = 26, signalPeriod = 9) {
181
+ const emaFast = ema(closes, fast);
182
+ const emaSlow = ema(closes, slow);
183
+ const macdLine = closes.map((_, i) => emaFast[i] - emaSlow[i]);
184
+ const signalLine = ema(macdLine, signalPeriod);
185
+ const histogram = macdLine.map((v, i) => v - signalLine[i]);
186
+ return { macd: macdLine, signal: signalLine, histogram };
187
+ }
188
+ function stochastic(bars, kPeriod = 14, dPeriod = 3) {
189
+ const k = new Array(bars.length).fill(void 0);
190
+ for (let i = kPeriod - 1; i < bars.length; i += 1) {
191
+ let hh = -Infinity;
192
+ let ll = Infinity;
193
+ for (let j = i - kPeriod + 1; j <= i; j += 1) {
194
+ if (bars[j].high > hh) hh = bars[j].high;
195
+ if (bars[j].low < ll) ll = bars[j].low;
196
+ }
197
+ const range = hh - ll;
198
+ k[i] = range === 0 ? 0 : (bars[i].close - ll) / range * 100;
199
+ }
200
+ const d = new Array(bars.length).fill(void 0);
201
+ for (let i = 0; i < bars.length; i += 1) {
202
+ if (i < kPeriod - 1 + dPeriod - 1) continue;
203
+ let sum = 0;
204
+ for (let j = i - dPeriod + 1; j <= i; j += 1) sum += k[j];
205
+ d[i] = sum / dPeriod;
206
+ }
207
+ return { k, d };
208
+ }
209
+
210
+ // src/ta/channels.js
211
+ function rollingMean(values, period, i) {
212
+ let sum = 0;
213
+ for (let j = i - period + 1; j <= i; j += 1) sum += values[j];
214
+ return sum / period;
215
+ }
216
+ function bollinger(closes, period = 20, mult = 2) {
217
+ const middle = new Array(closes.length).fill(void 0);
218
+ const upper = new Array(closes.length).fill(void 0);
219
+ const lower = new Array(closes.length).fill(void 0);
220
+ for (let i = period - 1; i < closes.length; i += 1) {
221
+ const avg = rollingMean(closes, period, i);
222
+ let variance = 0;
223
+ for (let j = i - period + 1; j <= i; j += 1) variance += (closes[j] - avg) ** 2;
224
+ const sd = Math.sqrt(variance / period);
225
+ middle[i] = avg;
226
+ upper[i] = avg + mult * sd;
227
+ lower[i] = avg - mult * sd;
228
+ }
229
+ return { middle, upper, lower };
230
+ }
231
+ function donchian(bars, period = 20) {
232
+ const upper = new Array(bars.length).fill(void 0);
233
+ const lower = new Array(bars.length).fill(void 0);
234
+ const middle = new Array(bars.length).fill(void 0);
235
+ for (let i = period - 1; i < bars.length; i += 1) {
236
+ let hh = -Infinity;
237
+ let ll = Infinity;
238
+ for (let j = i - period + 1; j <= i; j += 1) {
239
+ if (bars[j].high > hh) hh = bars[j].high;
240
+ if (bars[j].low < ll) ll = bars[j].low;
241
+ }
242
+ upper[i] = hh;
243
+ lower[i] = ll;
244
+ middle[i] = (hh + ll) / 2;
245
+ }
246
+ return { upper, lower, middle };
247
+ }
248
+ function keltner(bars, emaPeriod = 20, atrPeriod = 14, mult = 2) {
249
+ const closes = bars.map((b) => b.close);
250
+ const mid = ema(closes, emaPeriod);
251
+ const range = atr(bars, atrPeriod);
252
+ const upper = new Array(bars.length).fill(void 0);
253
+ const lower = new Array(bars.length).fill(void 0);
254
+ const middle = new Array(bars.length).fill(void 0);
255
+ for (let i = 0; i < bars.length; i += 1) {
256
+ if (range[i] === void 0) continue;
257
+ middle[i] = mid[i];
258
+ upper[i] = mid[i] + mult * range[i];
259
+ lower[i] = mid[i] - mult * range[i];
260
+ }
261
+ return { upper, lower, middle };
262
+ }
263
+
264
+ // src/ta/trend.js
265
+ function supertrend(bars, period = 10, mult = 3) {
266
+ const range = atr(bars, period);
267
+ const line = new Array(bars.length).fill(void 0);
268
+ const direction = new Array(bars.length).fill(void 0);
269
+ let prevUpper = Infinity;
270
+ let prevLower = -Infinity;
271
+ let prevDir = 1;
272
+ for (let i = 0; i < bars.length; i += 1) {
273
+ if (range[i] === void 0) continue;
274
+ const mid = (bars[i].high + bars[i].low) / 2;
275
+ const basicUpper = mid + mult * range[i];
276
+ const basicLower = mid - mult * range[i];
277
+ const close = bars[i].close;
278
+ const prevClose = i > 0 ? bars[i - 1].close : close;
279
+ const upper = basicUpper < prevUpper || prevClose > prevUpper ? basicUpper : prevUpper;
280
+ const lower = basicLower > prevLower || prevClose < prevLower ? basicLower : prevLower;
281
+ let dir = prevDir;
282
+ if (prevDir === 1 && close < lower) dir = -1;
283
+ else if (prevDir === -1 && close > upper) dir = 1;
284
+ line[i] = dir === 1 ? lower : upper;
285
+ direction[i] = dir;
286
+ prevUpper = upper;
287
+ prevLower = lower;
288
+ prevDir = dir;
289
+ }
290
+ return { line, direction };
291
+ }
292
+ function dayKeyUTC(timeMs) {
293
+ const d = new Date(timeMs);
294
+ return d.getUTCFullYear() * 1e4 + (d.getUTCMonth() + 1) * 100 + d.getUTCDate();
295
+ }
296
+ function vwap(bars) {
297
+ const out = new Array(bars.length).fill(void 0);
298
+ let currentDay = null;
299
+ let cumPV = 0;
300
+ let cumV = 0;
301
+ let cumTP = 0;
302
+ let count = 0;
303
+ for (let i = 0; i < bars.length; i += 1) {
304
+ const day = dayKeyUTC(bars[i].time);
305
+ if (day !== currentDay) {
306
+ currentDay = day;
307
+ cumPV = 0;
308
+ cumV = 0;
309
+ cumTP = 0;
310
+ count = 0;
311
+ }
312
+ const tp = (bars[i].high + bars[i].low + bars[i].close) / 3;
313
+ const vol = Number.isFinite(bars[i].volume) ? bars[i].volume : 0;
314
+ cumPV += tp * vol;
315
+ cumV += vol;
316
+ cumTP += tp;
317
+ count += 1;
318
+ out[i] = cumV > 0 ? cumPV / cumV : cumTP / count;
319
+ }
320
+ return out;
321
+ }
322
+ // Annotate the CommonJS export names for ESM import in node:
323
+ 0 && (module.exports = {
324
+ atr,
325
+ bollinger,
326
+ detectFVG,
327
+ donchian,
328
+ ema,
329
+ keltner,
330
+ lastSwing,
331
+ macd,
332
+ rsi,
333
+ stochastic,
334
+ structureState,
335
+ supertrend,
336
+ swingHigh,
337
+ swingLow,
338
+ vwap
339
+ });
@@ -123,11 +123,57 @@ import { LiveEngine, PaperEngine } from "tradelab/live";
123
123
  - `parseWindowsCSV(csv)`
124
124
  - `inWindowsET(timeMs, windows)`
125
125
 
126
+ ## Technical analysis (`tradelab/ta`)
127
+
128
+ TA exports are under a separate entrypoint:
129
+
130
+ ```js
131
+ import { rsi, macd, bollinger, vwap, supertrend } from "tradelab/ta";
132
+ ```
133
+
134
+ Every indicator returns a **full-length array aligned to the input** — warmup positions are `undefined` so values index 1:1 with candles. Oscillators accept a `number[]` of closes; range-based indicators accept `{ high, low, close }` candle arrays.
135
+
136
+ ### Oscillators
137
+
138
+ | Export | Input | Returns | Description |
139
+ | ------------------------------------------- | ------------ | ----------------------------- | -------------------------------------------------------- |
140
+ | `rsi(closes, period?)` | `number[]` | `(number \| undefined)[]` | Wilder's RSI; warmup positions are `undefined` |
141
+ | `macd(closes, fast?, slow?, signalPeriod?)` | `number[]` | `{ macd, signal, histogram }` | MACD line, signal line, and histogram; all full-length |
142
+ | `stochastic(bars, kPeriod?, dPeriod?)` | candle array | `{ k, d }` | Stochastic %K and %D; `k` and `d` are full-length arrays |
143
+
144
+ ### Bands & channels
145
+
146
+ | Export | Input | Returns | Description |
147
+ | ---------------------------------------------- | ------------ | -------------------------- | ------------------------------------------------------------- |
148
+ | `bollinger(closes, period?, mult?)` | `number[]` | `{ middle, upper, lower }` | Bollinger Bands with SMA middle and stddev-scaled outer bands |
149
+ | `donchian(bars, period?)` | candle array | `{ upper, lower, middle }` | Donchian channel: rolling highest-high / lowest-low |
150
+ | `keltner(bars, emaPeriod?, atrPeriod?, mult?)` | candle array | `{ upper, lower, middle }` | Keltner channel: EMA middle with ATR-scaled width |
151
+
152
+ ### Trend & volume
153
+
154
+ | Export | Input | Returns | Description |
155
+ | ---------------------------------- | ------------------------------------- | ------------------------- | -------------------------------------------------------------------------- |
156
+ | `supertrend(bars, period?, mult?)` | candle array | `{ line, direction }` | Supertrend support/resistance line; `direction` is `1` (up) or `-1` (down) |
157
+ | `vwap(bars)` | candle array with `time` and `volume` | `(number \| undefined)[]` | Session VWAP, resets on each UTC calendar day |
158
+
159
+ ### Re-exported from main module
160
+
161
+ | Export | Description |
162
+ | --------------------------------------- | ---------------------------------- |
163
+ | `ema(values, period?)` | Exponential moving average |
164
+ | `atr(bars, period?)` | Average True Range |
165
+ | `swingHigh(bars, index, left?, right?)` | Detect swing high at index |
166
+ | `swingLow(bars, index, left?, right?)` | Detect swing low at index |
167
+ | `detectFVG(bars, index)` | Detect Fair Value Gap at index |
168
+ | `lastSwing(bars, index, direction)` | Find the last swing in a direction |
169
+ | `structureState(bars, index)` | Assess market structure state |
170
+
126
171
  ## Types
127
172
 
128
173
  The package ships declarations in:
129
174
 
130
175
  - [../types/index.d.ts](../types/index.d.ts) for the main module
131
176
  - [../types/live.d.ts](../types/live.d.ts) for `tradelab/live`
177
+ - [../types/ta.d.ts](../types/ta.d.ts) for `tradelab/ta`
132
178
 
133
179
  <small>[Back to main page](README.md)</small>