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.
- package/CHANGELOG.md +66 -0
- package/README.md +75 -12
- package/bin/tradelab-mcp.js +7 -0
- package/bin/tradelab.js +29 -0
- package/dist/cjs/data.cjs +149 -26
- package/dist/cjs/index.cjs +1893 -1003
- package/dist/cjs/live.cjs +134 -25
- package/dist/cjs/ta.cjs +339 -0
- package/docs/api-reference.md +46 -0
- package/docs/backtest-engine.md +112 -0
- package/docs/live-trading.md +51 -0
- package/docs/mcp.md +64 -0
- package/docs/research.md +103 -0
- package/docs/superpowers/plans/2026-00-overview.md +101 -0
- package/docs/superpowers/plans/2026-01-metrics-correctness.md +873 -0
- package/docs/superpowers/plans/2026-02-indicator-library.md +677 -0
- package/docs/superpowers/plans/2026-03-overfitting-toolkit.md +882 -0
- package/docs/superpowers/plans/2026-04-async-signals-seeding.md +981 -0
- package/docs/superpowers/plans/2026-05-mcp-server.md +758 -0
- package/docs/superpowers/plans/2026-06-parallel-param-sweep.md +508 -0
- package/docs/superpowers/plans/2026-07-funding-carry-costs.md +535 -0
- package/docs/superpowers/plans/2026-08-live-dashboard.md +547 -0
- package/docs/superpowers/plans/HANDOFF.md +88 -0
- package/examples/liveDashboard.js +33 -0
- package/examples/llmSignal.js +33 -0
- package/examples/optimize.js +25 -0
- package/package.json +16 -2
- package/src/engine/asyncSignal.js +28 -0
- package/src/engine/backtest.js +13 -1
- package/src/engine/backtestAsync.js +27 -0
- package/src/engine/backtestTicks.js +13 -2
- package/src/engine/barSystemRunner.js +96 -41
- package/src/engine/execution.js +39 -0
- package/src/engine/grid.js +15 -0
- package/src/engine/llmSignal.js +84 -0
- package/src/engine/optimize.js +86 -0
- package/src/engine/optimizeWorker.js +67 -0
- package/src/engine/walkForward.js +1 -0
- package/src/index.js +9 -0
- package/src/live/dashboard/server.js +120 -0
- package/src/live/engine/liveEngine.js +2 -2
- package/src/live/index.js +1 -0
- package/src/mcp/schemas.js +48 -0
- package/src/mcp/server.js +31 -0
- package/src/mcp/tools.js +142 -0
- package/src/metrics/annualize.js +32 -0
- package/src/metrics/benchmark.js +55 -0
- package/src/metrics/buildMetrics.js +34 -13
- package/src/metrics/finite.js +17 -0
- package/src/research/combinations.js +18 -0
- package/src/research/cpcv.js +47 -0
- package/src/research/deflatedSharpe.js +35 -0
- package/src/research/index.js +6 -0
- package/src/research/monteCarlo.js +88 -0
- package/src/research/pbo.js +69 -0
- package/src/research/stats.js +78 -0
- package/src/strategies/builtins.js +96 -0
- package/src/strategies/index.js +30 -0
- package/src/ta/channels.js +67 -0
- package/src/ta/index.js +16 -0
- package/src/ta/oscillators.js +70 -0
- package/src/ta/trend.js +78 -0
- package/src/utils/random.js +33 -0
- package/templates/dashboard.html +174 -0
- package/types/index.d.ts +154 -0
- package/types/live.d.ts +15 -0
- 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,
|
|
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}${
|
|
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,
|
|
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}${
|
|
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
|
|
675
|
-
const data = await this._request("GET",
|
|
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
|
|
739
|
-
const response = await this._request("POST",
|
|
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
|
|
761
|
-
await this._request("DELETE",
|
|
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
|
|
771
|
-
const response = await this._request("PUT",
|
|
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
|
|
797
|
-
const rows = await this._request("GET",
|
|
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
|
|
814
|
-
const row = await this._request("GET",
|
|
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
|
|
877
|
-
const rows = await this._request("GET",
|
|
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:
|
|
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}${
|
|
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,
|
|
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}${
|
|
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
|
|
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 =
|
|
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,
|
package/dist/cjs/ta.cjs
ADDED
|
@@ -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
|
+
});
|
package/docs/api-reference.md
CHANGED
|
@@ -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>
|