tradelab 1.0.1 → 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 +112 -0
- package/README.md +188 -328
- 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 +1917 -1005
- package/dist/cjs/live.cjs +536 -25
- package/dist/cjs/ta.cjs +339 -0
- package/docs/README.md +32 -66
- package/docs/api-reference.md +283 -112
- package/docs/backtest-engine.md +210 -252
- package/docs/data-reporting-cli.md +114 -156
- package/docs/examples.md +6 -6
- package/docs/live-trading.md +263 -92
- package/docs/mcp.md +285 -0
- package/docs/research.md +157 -0
- package/examples/liveDashboard.js +33 -0
- package/examples/llmSignal.js +33 -0
- package/examples/mcpLiveTrading.js +77 -0
- package/examples/optimize.js +25 -0
- package/package.json +26 -4
- 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 +110 -0
- package/src/engine/optimizeWorker.js +67 -0
- package/src/engine/portfolio.js +4 -1
- package/src/engine/walkForward.js +1 -0
- package/src/index.js +9 -0
- package/src/live/dashboard/server.js +179 -0
- package/src/live/engine/liveEngine.js +2 -2
- package/src/live/engine/paperEngine.js +5 -0
- package/src/live/index.js +3 -0
- package/src/live/session.js +402 -0
- package/src/mcp/liveTools.js +179 -0
- package/src/mcp/schemas.js +167 -0
- package/src/mcp/server.js +35 -0
- package/src/mcp/tools.js +265 -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 +661 -0
- package/types/index.d.ts +179 -0
- package/types/live.d.ts +114 -0
- package/types/mcp.d.ts +17 -0
- package/types/ta.d.ts +45 -0
package/dist/cjs/live.cjs
CHANGED
|
@@ -48,14 +48,17 @@ __export(index_exports, {
|
|
|
48
48
|
PaperEngine: () => PaperEngine,
|
|
49
49
|
PollingFeed: () => PollingFeed,
|
|
50
50
|
RiskManager: () => RiskManager,
|
|
51
|
+
SessionManager: () => SessionManager,
|
|
51
52
|
StateManager: () => StateManager,
|
|
52
53
|
StorageProvider: () => StorageProvider,
|
|
54
|
+
TradingSession: () => TradingSession,
|
|
53
55
|
createAlpacaBroker: () => createAlpacaBroker,
|
|
54
56
|
createBinanceBroker: () => createBinanceBroker,
|
|
55
57
|
createBrokerFeed: () => createBrokerFeed,
|
|
56
58
|
createCandleAggregator: () => createCandleAggregator,
|
|
57
59
|
createClock: () => createClock,
|
|
58
60
|
createCoinbaseBroker: () => createCoinbaseBroker,
|
|
61
|
+
createDashboardServer: () => createDashboardServer,
|
|
59
62
|
createEventBus: () => createEventBus,
|
|
60
63
|
createInteractiveBrokersBroker: () => createInteractiveBrokersBroker,
|
|
61
64
|
createJsonFileStorage: () => createJsonFileStorage,
|
|
@@ -65,6 +68,7 @@ __export(index_exports, {
|
|
|
65
68
|
createPaperEngine: () => createPaperEngine,
|
|
66
69
|
createPollingFeed: () => createPollingFeed,
|
|
67
70
|
createRiskManager: () => createRiskManager,
|
|
71
|
+
createSessionManager: () => createSessionManager,
|
|
68
72
|
createStateManager: () => createStateManager
|
|
69
73
|
});
|
|
70
74
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -435,10 +439,10 @@ var AlpacaBroker = class extends BrokerAdapter {
|
|
|
435
439
|
...extra
|
|
436
440
|
};
|
|
437
441
|
}
|
|
438
|
-
async _request(method,
|
|
442
|
+
async _request(method, path3, { query = null, body = null, dataApi = false } = {}) {
|
|
439
443
|
if (!this.fetch) throw new Error("global fetch is unavailable");
|
|
440
444
|
const base = dataApi ? this.dataUrl : this.baseUrl;
|
|
441
|
-
const url = withQuery(`${base}${
|
|
445
|
+
const url = withQuery(`${base}${path3}`, query || {});
|
|
442
446
|
const response = await this.fetch(url, {
|
|
443
447
|
method,
|
|
444
448
|
headers: this._headers(),
|
|
@@ -648,11 +652,11 @@ var BinanceBroker = class extends BrokerAdapter {
|
|
|
648
652
|
const signature = import_node_crypto.default.createHmac("sha256", this.config.apiSecret || "").update(payload).digest("hex");
|
|
649
653
|
return { ...base, signature };
|
|
650
654
|
}
|
|
651
|
-
async _request(method,
|
|
655
|
+
async _request(method, path3, { signed = false, params = {}, body = null } = {}) {
|
|
652
656
|
if (!this.fetch) throw new Error("global fetch is unavailable");
|
|
653
657
|
const finalParams = signed ? this._signedParams(params) : params;
|
|
654
658
|
const qs = queryString(finalParams);
|
|
655
|
-
const url = new import_node_url2.URL(`${this.baseUrl}${
|
|
659
|
+
const url = new import_node_url2.URL(`${this.baseUrl}${path3}${qs ? `?${qs}` : ""}`);
|
|
656
660
|
const headers = {
|
|
657
661
|
"content-type": "application/json"
|
|
658
662
|
};
|
|
@@ -671,8 +675,8 @@ var BinanceBroker = class extends BrokerAdapter {
|
|
|
671
675
|
return payload;
|
|
672
676
|
}
|
|
673
677
|
async getServerTime() {
|
|
674
|
-
const
|
|
675
|
-
const data = await this._request("GET",
|
|
678
|
+
const path3 = this.config.futures ? "/fapi/v1/time" : "/api/v3/time";
|
|
679
|
+
const data = await this._request("GET", path3);
|
|
676
680
|
return Number(data.serverTime || Date.now());
|
|
677
681
|
}
|
|
678
682
|
async getAccount() {
|
|
@@ -735,8 +739,8 @@ var BinanceBroker = class extends BrokerAdapter {
|
|
|
735
739
|
return payload;
|
|
736
740
|
}
|
|
737
741
|
async submitOrder(order) {
|
|
738
|
-
const
|
|
739
|
-
const response = await this._request("POST",
|
|
742
|
+
const path3 = this.config.futures ? "/fapi/v1/order" : "/api/v3/order";
|
|
743
|
+
const response = await this._request("POST", path3, {
|
|
740
744
|
signed: true,
|
|
741
745
|
params: this._orderPayload(order)
|
|
742
746
|
});
|
|
@@ -757,8 +761,8 @@ var BinanceBroker = class extends BrokerAdapter {
|
|
|
757
761
|
return receipt;
|
|
758
762
|
}
|
|
759
763
|
async cancelOrder(orderId) {
|
|
760
|
-
const
|
|
761
|
-
await this._request("DELETE",
|
|
764
|
+
const path3 = this.config.futures ? "/fapi/v1/order" : "/api/v3/order";
|
|
765
|
+
await this._request("DELETE", path3, {
|
|
762
766
|
signed: true,
|
|
763
767
|
params: {
|
|
764
768
|
orderId
|
|
@@ -767,8 +771,8 @@ var BinanceBroker = class extends BrokerAdapter {
|
|
|
767
771
|
this.emit("order:canceled", { orderId: String(orderId) });
|
|
768
772
|
}
|
|
769
773
|
async modifyOrder(orderId, changes = {}) {
|
|
770
|
-
const
|
|
771
|
-
const response = await this._request("PUT",
|
|
774
|
+
const path3 = this.config.futures ? "/fapi/v1/order" : "/api/v3/order";
|
|
775
|
+
const response = await this._request("PUT", path3, {
|
|
772
776
|
signed: true,
|
|
773
777
|
params: {
|
|
774
778
|
orderId,
|
|
@@ -793,8 +797,8 @@ var BinanceBroker = class extends BrokerAdapter {
|
|
|
793
797
|
return receipt;
|
|
794
798
|
}
|
|
795
799
|
async getOpenOrders() {
|
|
796
|
-
const
|
|
797
|
-
const rows = await this._request("GET",
|
|
800
|
+
const path3 = this.config.futures ? "/fapi/v1/openOrders" : "/api/v3/openOrders";
|
|
801
|
+
const rows = await this._request("GET", path3, { signed: true });
|
|
798
802
|
return rows.map((row) => ({
|
|
799
803
|
orderId: String(row.orderId),
|
|
800
804
|
clientOrderId: row.clientOrderId,
|
|
@@ -810,8 +814,8 @@ var BinanceBroker = class extends BrokerAdapter {
|
|
|
810
814
|
}));
|
|
811
815
|
}
|
|
812
816
|
async getOrderStatus(orderId) {
|
|
813
|
-
const
|
|
814
|
-
const row = await this._request("GET",
|
|
817
|
+
const path3 = this.config.futures ? "/fapi/v1/order" : "/api/v3/order";
|
|
818
|
+
const row = await this._request("GET", path3, {
|
|
815
819
|
signed: true,
|
|
816
820
|
params: { orderId }
|
|
817
821
|
});
|
|
@@ -873,8 +877,8 @@ var BinanceBroker = class extends BrokerAdapter {
|
|
|
873
877
|
};
|
|
874
878
|
}
|
|
875
879
|
async getHistoricalBars(symbol, interval, limit = 200) {
|
|
876
|
-
const
|
|
877
|
-
const rows = await this._request("GET",
|
|
880
|
+
const path3 = this.config.futures ? "/fapi/v1/klines" : "/api/v3/klines";
|
|
881
|
+
const rows = await this._request("GET", path3, {
|
|
878
882
|
params: { symbol, interval, limit }
|
|
879
883
|
});
|
|
880
884
|
const bars = rows.map((row) => ({
|
|
@@ -898,7 +902,7 @@ var import_node_url3 = require("node:url");
|
|
|
898
902
|
function base64url(input) {
|
|
899
903
|
return Buffer.from(input).toString("base64url");
|
|
900
904
|
}
|
|
901
|
-
function buildJwt({ key, secret, method, host, path:
|
|
905
|
+
function buildJwt({ key, secret, method, host, path: path3 }) {
|
|
902
906
|
const now = Math.floor(Date.now() / 1e3);
|
|
903
907
|
const header = { alg: "HS256", typ: "JWT", kid: key };
|
|
904
908
|
const payload = {
|
|
@@ -906,7 +910,7 @@ function buildJwt({ key, secret, method, host, path: path2 }) {
|
|
|
906
910
|
sub: key,
|
|
907
911
|
nbf: now - 5,
|
|
908
912
|
exp: now + 120,
|
|
909
|
-
uri: `${method.toUpperCase()} ${host}${
|
|
913
|
+
uri: `${method.toUpperCase()} ${host}${path3}`
|
|
910
914
|
};
|
|
911
915
|
const encodedHeader = base64url(JSON.stringify(header));
|
|
912
916
|
const encodedPayload = base64url(JSON.stringify(payload));
|
|
@@ -965,9 +969,9 @@ var CoinbaseBroker = class extends BrokerAdapter {
|
|
|
965
969
|
path: target.pathname
|
|
966
970
|
});
|
|
967
971
|
}
|
|
968
|
-
async _request(method,
|
|
972
|
+
async _request(method, path3, { query = {}, body = null } = {}) {
|
|
969
973
|
if (!this.fetch) throw new Error("global fetch is unavailable");
|
|
970
|
-
const url = new import_node_url3.URL(`${this.baseUrl}${
|
|
974
|
+
const url = new import_node_url3.URL(`${this.baseUrl}${path3}`);
|
|
971
975
|
for (const [key, value] of Object.entries(query || {})) {
|
|
972
976
|
if (value === void 0 || value === null) continue;
|
|
973
977
|
url.searchParams.set(key, String(value));
|
|
@@ -1726,6 +1730,7 @@ function dayKeyET(timeMs) {
|
|
|
1726
1730
|
const pseudoEtTime = anchor.getTime() + hoursET * 60 * 60 * 1e3 + minutesETDay * 60 * 1e3;
|
|
1727
1731
|
return dayKeyUTC(pseudoEtTime);
|
|
1728
1732
|
}
|
|
1733
|
+
var MS_PER_YEAR = 365 * 24 * 60 * 60 * 1e3;
|
|
1729
1734
|
|
|
1730
1735
|
// src/live/engine/candleAggregator.js
|
|
1731
1736
|
function intervalToMs(interval) {
|
|
@@ -2481,6 +2486,7 @@ var PaperEngine = class extends BrokerAdapter {
|
|
|
2481
2486
|
});
|
|
2482
2487
|
const orders = [...this.openOrders.values()].filter((order) => order.symbol === symbol);
|
|
2483
2488
|
for (const order of orders) {
|
|
2489
|
+
if (!this.openOrders.has(order.orderId)) continue;
|
|
2484
2490
|
if (order.type === "limit") {
|
|
2485
2491
|
if (this._touchesLimit(order, normalizedBar)) {
|
|
2486
2492
|
this._fillOrder(order, order.limitPrice, "limit", normalizedBar.time);
|
|
@@ -2549,9 +2555,9 @@ function asNumber2(value) {
|
|
|
2549
2555
|
function formatIsoTime(time) {
|
|
2550
2556
|
return Number.isFinite(time) ? new Date(time).toISOString() : "invalid-time";
|
|
2551
2557
|
}
|
|
2552
|
-
function
|
|
2558
|
+
async function callSignalWithContextAsync({ signal, context, index, bar, symbol }) {
|
|
2553
2559
|
try {
|
|
2554
|
-
return signal(context);
|
|
2560
|
+
return await signal(context);
|
|
2555
2561
|
} catch (error) {
|
|
2556
2562
|
const cause = error instanceof Error ? error.message : String(error);
|
|
2557
2563
|
throw new Error(
|
|
@@ -3038,7 +3044,7 @@ var LiveEngine = class {
|
|
|
3038
3044
|
}
|
|
3039
3045
|
if (!this.openPosition && !this.pendingOrder) {
|
|
3040
3046
|
const context = this._signalContext(bar);
|
|
3041
|
-
const rawSignal =
|
|
3047
|
+
const rawSignal = await callSignalWithContextAsync({
|
|
3042
3048
|
signal: this.options.signal,
|
|
3043
3049
|
context,
|
|
3044
3050
|
index: context.index,
|
|
@@ -3309,6 +3315,507 @@ var LiveOrchestrator = class {
|
|
|
3309
3315
|
function createLiveOrchestrator(options) {
|
|
3310
3316
|
return new LiveOrchestrator(options);
|
|
3311
3317
|
}
|
|
3318
|
+
|
|
3319
|
+
// src/live/dashboard/server.js
|
|
3320
|
+
var import_node_http = __toESM(require("node:http"), 1);
|
|
3321
|
+
var import_node_fs2 = require("node:fs");
|
|
3322
|
+
var import_node_path2 = __toESM(require("node:path"), 1);
|
|
3323
|
+
var import_node_url4 = require("node:url");
|
|
3324
|
+
var FALLBACK_HTML = `<!doctype html>
|
|
3325
|
+
<html lang="en">
|
|
3326
|
+
<head>
|
|
3327
|
+
<meta charset="utf-8" />
|
|
3328
|
+
<title>tradelab live</title>
|
|
3329
|
+
</head>
|
|
3330
|
+
<body>
|
|
3331
|
+
<h1>tradelab live</h1>
|
|
3332
|
+
<pre id="state"></pre>
|
|
3333
|
+
<script>
|
|
3334
|
+
fetch("/state")
|
|
3335
|
+
.then((res) => res.json())
|
|
3336
|
+
.then((state) => {
|
|
3337
|
+
document.getElementById("state").textContent = JSON.stringify(state, null, 2);
|
|
3338
|
+
});
|
|
3339
|
+
</script>
|
|
3340
|
+
</body>
|
|
3341
|
+
</html>`;
|
|
3342
|
+
function callerModuleDir() {
|
|
3343
|
+
const stack = new Error().stack || "";
|
|
3344
|
+
const lines = stack.split("\n").slice(1);
|
|
3345
|
+
const match = lines.map((line) => line.match(/(?:\()?(file:\/\/\/[^\s)]+|\/[^\s)]+):\d+:\d+/)).find(Boolean);
|
|
3346
|
+
if (!match) return process.cwd();
|
|
3347
|
+
const filePath = match[1].startsWith("file://") ? (0, import_node_url4.fileURLToPath)(match[1]) : match[1];
|
|
3348
|
+
return import_node_path2.default.dirname(filePath);
|
|
3349
|
+
}
|
|
3350
|
+
function readDashboardHtml() {
|
|
3351
|
+
const here = callerModuleDir();
|
|
3352
|
+
const candidates = [
|
|
3353
|
+
import_node_path2.default.join(here, "..", "..", "..", "templates", "dashboard.html"),
|
|
3354
|
+
import_node_path2.default.join(here, "..", "..", "templates", "dashboard.html"),
|
|
3355
|
+
import_node_path2.default.join(process.cwd(), "templates", "dashboard.html")
|
|
3356
|
+
];
|
|
3357
|
+
const htmlPath = candidates.find((candidate) => (0, import_node_fs2.existsSync)(candidate));
|
|
3358
|
+
if (htmlPath) return (0, import_node_fs2.readFileSync)(htmlPath, "utf8");
|
|
3359
|
+
try {
|
|
3360
|
+
return (0, import_node_fs2.readFileSync)(import_node_path2.default.join(process.cwd(), "templates", "dashboard.html"), "utf8");
|
|
3361
|
+
} catch {
|
|
3362
|
+
return FALLBACK_HTML;
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
function createDashboardServer({ source, port = 4317, maxBuffer = 200 }) {
|
|
3366
|
+
if (!source?.eventBus || typeof source.eventBus.onAny !== "function") {
|
|
3367
|
+
throw new Error("dashboard source must expose an eventBus with onAny()");
|
|
3368
|
+
}
|
|
3369
|
+
const recent = [];
|
|
3370
|
+
const clients = /* @__PURE__ */ new Set();
|
|
3371
|
+
const unsubscribe = source.eventBus.onAny(({ event, payload }) => {
|
|
3372
|
+
const msg = { event, payload, t: Date.now() };
|
|
3373
|
+
recent.push(msg);
|
|
3374
|
+
if (recent.length > maxBuffer) recent.shift();
|
|
3375
|
+
const frame = `data: ${JSON.stringify(msg)}
|
|
3376
|
+
|
|
3377
|
+
`;
|
|
3378
|
+
for (const res of clients) res.write(frame);
|
|
3379
|
+
});
|
|
3380
|
+
const server = import_node_http.default.createServer(async (req, res) => {
|
|
3381
|
+
const url = (req.url || "/").split("?")[0];
|
|
3382
|
+
if (url === "/") {
|
|
3383
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
3384
|
+
res.end(readDashboardHtml());
|
|
3385
|
+
return;
|
|
3386
|
+
}
|
|
3387
|
+
if (url === "/state") {
|
|
3388
|
+
if (typeof source.refresh === "function") await source.refresh().catch(() => {
|
|
3389
|
+
});
|
|
3390
|
+
const status = typeof source.getStatus === "function" ? source.getStatus() : {};
|
|
3391
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3392
|
+
res.end(JSON.stringify(status));
|
|
3393
|
+
return;
|
|
3394
|
+
}
|
|
3395
|
+
if (url === "/command" && req.method === "POST") {
|
|
3396
|
+
const WHITELIST = {
|
|
3397
|
+
flatten: "flatten",
|
|
3398
|
+
stop: "stop",
|
|
3399
|
+
closePosition: "closePosition",
|
|
3400
|
+
cancelOrder: "cancelOrder"
|
|
3401
|
+
};
|
|
3402
|
+
let body = "";
|
|
3403
|
+
req.on("data", (c) => body += c);
|
|
3404
|
+
req.on("end", async () => {
|
|
3405
|
+
let cmd;
|
|
3406
|
+
try {
|
|
3407
|
+
cmd = JSON.parse(body || "{}");
|
|
3408
|
+
} catch {
|
|
3409
|
+
cmd = {};
|
|
3410
|
+
}
|
|
3411
|
+
const method = WHITELIST[cmd.type];
|
|
3412
|
+
if (!method || typeof source[method] !== "function") {
|
|
3413
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
3414
|
+
res.end(JSON.stringify({ ok: false, error: `unsupported command "${cmd.type}"` }));
|
|
3415
|
+
return;
|
|
3416
|
+
}
|
|
3417
|
+
try {
|
|
3418
|
+
const arg = cmd.type === "closePosition" ? cmd.symbol : cmd.type === "cancelOrder" ? cmd.orderId : void 0;
|
|
3419
|
+
await source[method](arg);
|
|
3420
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3421
|
+
res.end(JSON.stringify({ ok: true }));
|
|
3422
|
+
} catch (error) {
|
|
3423
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
3424
|
+
res.end(
|
|
3425
|
+
JSON.stringify({
|
|
3426
|
+
ok: false,
|
|
3427
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3428
|
+
})
|
|
3429
|
+
);
|
|
3430
|
+
}
|
|
3431
|
+
});
|
|
3432
|
+
return;
|
|
3433
|
+
}
|
|
3434
|
+
if (url === "/events") {
|
|
3435
|
+
res.writeHead(200, {
|
|
3436
|
+
"Content-Type": "text/event-stream",
|
|
3437
|
+
"Cache-Control": "no-cache",
|
|
3438
|
+
Connection: "keep-alive"
|
|
3439
|
+
});
|
|
3440
|
+
res.flushHeaders();
|
|
3441
|
+
for (const msg of recent) res.write(`data: ${JSON.stringify(msg)}
|
|
3442
|
+
|
|
3443
|
+
`);
|
|
3444
|
+
clients.add(res);
|
|
3445
|
+
req.on("close", () => clients.delete(res));
|
|
3446
|
+
return;
|
|
3447
|
+
}
|
|
3448
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
3449
|
+
res.end("not found");
|
|
3450
|
+
});
|
|
3451
|
+
return {
|
|
3452
|
+
start() {
|
|
3453
|
+
return new Promise((resolve) => {
|
|
3454
|
+
server.listen(port, () => {
|
|
3455
|
+
const address = server.address();
|
|
3456
|
+
const actualPort = typeof address === "object" && address ? address.port : port;
|
|
3457
|
+
resolve(`http://localhost:${actualPort}`);
|
|
3458
|
+
});
|
|
3459
|
+
});
|
|
3460
|
+
},
|
|
3461
|
+
close() {
|
|
3462
|
+
unsubscribe();
|
|
3463
|
+
for (const res of clients) res.end();
|
|
3464
|
+
clients.clear();
|
|
3465
|
+
return new Promise((resolve, reject) => {
|
|
3466
|
+
server.close((error) => {
|
|
3467
|
+
if (error) reject(error);
|
|
3468
|
+
else resolve();
|
|
3469
|
+
});
|
|
3470
|
+
});
|
|
3471
|
+
},
|
|
3472
|
+
server
|
|
3473
|
+
};
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3476
|
+
// src/live/session.js
|
|
3477
|
+
function oppositeSide2(side) {
|
|
3478
|
+
return side === "long" || side === "buy" ? "sell" : "buy";
|
|
3479
|
+
}
|
|
3480
|
+
function toBrokerSide(side) {
|
|
3481
|
+
return side === "long" || side === "buy" ? "buy" : "sell";
|
|
3482
|
+
}
|
|
3483
|
+
var TradingSession = class _TradingSession {
|
|
3484
|
+
constructor({
|
|
3485
|
+
id,
|
|
3486
|
+
symbol,
|
|
3487
|
+
interval = "1m",
|
|
3488
|
+
broker,
|
|
3489
|
+
mode = "paper",
|
|
3490
|
+
equity = 1e4,
|
|
3491
|
+
riskPct = 1,
|
|
3492
|
+
maxDailyLossPct = 0,
|
|
3493
|
+
maxPositionPct = 1,
|
|
3494
|
+
qtyStep = 1e-3,
|
|
3495
|
+
minQty = 1e-3,
|
|
3496
|
+
maxLeverage = 2,
|
|
3497
|
+
confirmLive = false,
|
|
3498
|
+
eventBus
|
|
3499
|
+
} = {}) {
|
|
3500
|
+
if (mode === "live" && (!_TradingSession.liveAllowed() || !confirmLive)) {
|
|
3501
|
+
throw new Error(
|
|
3502
|
+
"live trading is gated: set TRADELAB_ALLOW_LIVE=true and pass confirmLive:true with a credentialed broker"
|
|
3503
|
+
);
|
|
3504
|
+
}
|
|
3505
|
+
if (!broker) throw new Error("TradingSession requires a broker (PaperEngine by default)");
|
|
3506
|
+
if (!symbol) throw new Error("TradingSession requires a symbol");
|
|
3507
|
+
this.id = id || `${symbol}-${interval}`;
|
|
3508
|
+
this.symbol = symbol;
|
|
3509
|
+
this.interval = interval;
|
|
3510
|
+
this.broker = broker;
|
|
3511
|
+
this.mode = mode;
|
|
3512
|
+
this.equity = equity;
|
|
3513
|
+
this._startEquity = equity;
|
|
3514
|
+
this.riskPct = riskPct;
|
|
3515
|
+
this.maxPositionPct = maxPositionPct;
|
|
3516
|
+
this.qtyStep = qtyStep;
|
|
3517
|
+
this.minQty = minQty;
|
|
3518
|
+
this.maxLeverage = maxLeverage;
|
|
3519
|
+
this.eventBus = eventBus || new EventBus();
|
|
3520
|
+
this.riskManager = new RiskManager({ maxDailyLossPct, maxDrawdownPct: 0 });
|
|
3521
|
+
this.lastPrice = null;
|
|
3522
|
+
this.running = false;
|
|
3523
|
+
this.events = [];
|
|
3524
|
+
this.brackets = /* @__PURE__ */ new Map();
|
|
3525
|
+
this._pendingBracket = null;
|
|
3526
|
+
this._cachedPositions = [];
|
|
3527
|
+
this._cachedOpenOrders = [];
|
|
3528
|
+
this.candleBuffer = [];
|
|
3529
|
+
this._strategy = null;
|
|
3530
|
+
this._wireBrokerEvents();
|
|
3531
|
+
}
|
|
3532
|
+
static liveAllowed() {
|
|
3533
|
+
return process.env.TRADELAB_ALLOW_LIVE === "true";
|
|
3534
|
+
}
|
|
3535
|
+
_record(event, payload) {
|
|
3536
|
+
const msg = { event, payload, t: Date.now() };
|
|
3537
|
+
this.events.push(msg);
|
|
3538
|
+
if (this.events.length > 500) this.events.shift();
|
|
3539
|
+
this.eventBus.emitEvent(event, { sessionId: this.id, symbol: this.symbol, ...payload });
|
|
3540
|
+
}
|
|
3541
|
+
_wireBrokerEvents() {
|
|
3542
|
+
this.broker.on?.("order:filled", (order) => this._onBrokerFillSync(order));
|
|
3543
|
+
this.broker.on?.("order:submitted", (order) => this._record("order:submitted", order));
|
|
3544
|
+
this.broker.on?.("order:canceled", (order) => this._record("order:canceled", order));
|
|
3545
|
+
this.broker.on?.("equity:update", (acct) => this._record("equity:update", acct));
|
|
3546
|
+
}
|
|
3547
|
+
// Sync event handler — fire-and-forget async OCO work via a stored promise
|
|
3548
|
+
_onBrokerFillSync(order) {
|
|
3549
|
+
this._record("order:filled", order);
|
|
3550
|
+
if (this._pendingBracket && String(order.clientOrderId || "").includes("-entry-")) {
|
|
3551
|
+
const staged = this._pendingBracket;
|
|
3552
|
+
this._pendingBracket = null;
|
|
3553
|
+
this._pendingCancelPromise = Promise.resolve(
|
|
3554
|
+
this._attachBracket({ ...staged, receipt: order })
|
|
3555
|
+
);
|
|
3556
|
+
return;
|
|
3557
|
+
}
|
|
3558
|
+
const bracket = this.brackets.get(this.symbol);
|
|
3559
|
+
if (bracket && (order.orderId === bracket.stopId || order.orderId === bracket.targetId)) {
|
|
3560
|
+
const siblingId = order.orderId === bracket.stopId ? bracket.targetId : bracket.stopId;
|
|
3561
|
+
this._pendingCancelPromise = (async () => {
|
|
3562
|
+
if (siblingId) await this.broker.cancelOrder(siblingId).catch(() => {
|
|
3563
|
+
});
|
|
3564
|
+
this.brackets.delete(this.symbol);
|
|
3565
|
+
this._record("position:closed", { reason: order.orderId === bracket.stopId ? "SL" : "TP" });
|
|
3566
|
+
})();
|
|
3567
|
+
}
|
|
3568
|
+
}
|
|
3569
|
+
async start() {
|
|
3570
|
+
if (!this.broker.isConnected?.()) await this.broker.connect?.({});
|
|
3571
|
+
const acct = await this.broker.getAccount?.().catch(() => null);
|
|
3572
|
+
if (Number.isFinite(acct?.equity)) {
|
|
3573
|
+
this.equity = acct.equity;
|
|
3574
|
+
this._startEquity = acct.equity;
|
|
3575
|
+
}
|
|
3576
|
+
this.riskManager.initialize(this.equity, Date.now());
|
|
3577
|
+
this.running = true;
|
|
3578
|
+
this._record("connected", { mode: this.mode });
|
|
3579
|
+
}
|
|
3580
|
+
async stop({ flatten = false } = {}) {
|
|
3581
|
+
if (flatten) await this.flatten();
|
|
3582
|
+
this.running = false;
|
|
3583
|
+
this._record("shutdown", {});
|
|
3584
|
+
}
|
|
3585
|
+
async pushBar(b) {
|
|
3586
|
+
this.lastPrice = b.close;
|
|
3587
|
+
if (typeof this.broker.simulateBar === "function") {
|
|
3588
|
+
await this.broker.simulateBar(this.symbol, this.interval, b);
|
|
3589
|
+
}
|
|
3590
|
+
if (this._pendingCancelPromise) {
|
|
3591
|
+
await this._pendingCancelPromise;
|
|
3592
|
+
this._pendingCancelPromise = null;
|
|
3593
|
+
}
|
|
3594
|
+
this.candleBuffer.push(b);
|
|
3595
|
+
if (this.candleBuffer.length > 200) this.candleBuffer.shift();
|
|
3596
|
+
this._record("bar", { close: b.close, time: b.time });
|
|
3597
|
+
await this._syncEquityAndRisk();
|
|
3598
|
+
await this.refresh();
|
|
3599
|
+
}
|
|
3600
|
+
_riskHalted() {
|
|
3601
|
+
const state = this.riskManager.getState?.() || {};
|
|
3602
|
+
return Boolean(state.halted);
|
|
3603
|
+
}
|
|
3604
|
+
async placeOrder({ side, type = "market", qty, riskPct, stop, target, rr, limitPrice } = {}) {
|
|
3605
|
+
if (!this.running) throw new Error("session not started");
|
|
3606
|
+
if (this._riskHalted()) throw new Error("session is risk-halted for the day");
|
|
3607
|
+
const entryRef = type === "limit" ? limitPrice : this.lastPrice;
|
|
3608
|
+
if (!Number.isFinite(entryRef)) throw new Error("no price available; pushBar() a price first");
|
|
3609
|
+
let size = qty;
|
|
3610
|
+
if (!Number.isFinite(size)) {
|
|
3611
|
+
const fraction = Number.isFinite(riskPct) ? riskPct / 100 : this.riskPct / 100;
|
|
3612
|
+
if (!Number.isFinite(stop)) throw new Error("risk-based sizing requires a stop");
|
|
3613
|
+
size = calculatePositionSize({
|
|
3614
|
+
equity: this.equity,
|
|
3615
|
+
entry: entryRef,
|
|
3616
|
+
stop,
|
|
3617
|
+
riskFraction: fraction,
|
|
3618
|
+
qtyStep: this.qtyStep,
|
|
3619
|
+
minQty: this.minQty,
|
|
3620
|
+
maxLeverage: this.maxLeverage
|
|
3621
|
+
});
|
|
3622
|
+
}
|
|
3623
|
+
size = roundStep(size, this.qtyStep);
|
|
3624
|
+
if (!(size >= this.minQty)) throw new Error(`sized below minQty (${size})`);
|
|
3625
|
+
const receipt = await this.broker.submitOrder({
|
|
3626
|
+
symbol: this.symbol,
|
|
3627
|
+
side: toBrokerSide(side),
|
|
3628
|
+
type,
|
|
3629
|
+
qty: size,
|
|
3630
|
+
limitPrice: type === "limit" ? limitPrice : void 0,
|
|
3631
|
+
clientOrderId: `${this.id}-entry-${Date.now()}`
|
|
3632
|
+
});
|
|
3633
|
+
if (Number.isFinite(stop) || Number.isFinite(target) || Number.isFinite(rr)) {
|
|
3634
|
+
if (receipt.status === "filled") {
|
|
3635
|
+
await this._attachBracket({ side, size, stop, target, rr, entryRef, receipt });
|
|
3636
|
+
} else {
|
|
3637
|
+
this._pendingBracket = { side, size, stop, target, rr, entryRef };
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
await this.refresh();
|
|
3641
|
+
return receipt;
|
|
3642
|
+
}
|
|
3643
|
+
async _attachBracket({ side, size, stop, target, rr, entryRef, receipt }) {
|
|
3644
|
+
const entryFill = receipt?.avgFillPrice ?? entryRef;
|
|
3645
|
+
const risk = Number.isFinite(stop) ? Math.abs(entryFill - stop) : null;
|
|
3646
|
+
const targetPrice = Number.isFinite(target) ? target : Number.isFinite(rr) && risk ? side === "long" || side === "buy" ? entryFill + rr * risk : entryFill - rr * risk : null;
|
|
3647
|
+
const exitSide = oppositeSide2(side);
|
|
3648
|
+
const bracket = {};
|
|
3649
|
+
if (Number.isFinite(stop)) {
|
|
3650
|
+
const stopOrder = await this.broker.submitOrder({
|
|
3651
|
+
symbol: this.symbol,
|
|
3652
|
+
side: exitSide,
|
|
3653
|
+
type: "stop",
|
|
3654
|
+
qty: size,
|
|
3655
|
+
stopPrice: stop,
|
|
3656
|
+
clientOrderId: `${this.id}-stop-${Date.now()}`
|
|
3657
|
+
});
|
|
3658
|
+
bracket.stopId = stopOrder.orderId;
|
|
3659
|
+
}
|
|
3660
|
+
if (Number.isFinite(targetPrice)) {
|
|
3661
|
+
const tgtOrder = await this.broker.submitOrder({
|
|
3662
|
+
symbol: this.symbol,
|
|
3663
|
+
side: exitSide,
|
|
3664
|
+
type: "limit",
|
|
3665
|
+
qty: size,
|
|
3666
|
+
limitPrice: targetPrice,
|
|
3667
|
+
clientOrderId: `${this.id}-target-${Date.now()}`
|
|
3668
|
+
});
|
|
3669
|
+
bracket.targetId = tgtOrder.orderId;
|
|
3670
|
+
}
|
|
3671
|
+
this.brackets.set(this.symbol, bracket);
|
|
3672
|
+
}
|
|
3673
|
+
async _syncEquityAndRisk() {
|
|
3674
|
+
const acct = await this.broker.getAccount?.().catch(() => null);
|
|
3675
|
+
if (!Number.isFinite(acct?.equity)) return;
|
|
3676
|
+
const prevEquity = this.equity;
|
|
3677
|
+
this.equity = acct.equity;
|
|
3678
|
+
const pnlDelta = this.equity - prevEquity;
|
|
3679
|
+
if (pnlDelta !== 0) {
|
|
3680
|
+
this.riskManager.recordTrade({ pnl: pnlDelta, timeMs: Date.now(), equity: this.equity });
|
|
3681
|
+
} else {
|
|
3682
|
+
this.riskManager.update({ timeMs: Date.now(), equity: this.equity });
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
async closePosition(symbol = this.symbol) {
|
|
3686
|
+
const positions = await this.broker.getPositions();
|
|
3687
|
+
const pos = positions.find((p) => p.symbol === symbol);
|
|
3688
|
+
if (!pos) return null;
|
|
3689
|
+
const bracket = this.brackets.get(symbol);
|
|
3690
|
+
if (bracket) {
|
|
3691
|
+
for (const id of [bracket.stopId, bracket.targetId]) {
|
|
3692
|
+
if (id) await this.broker.cancelOrder(id).catch(() => {
|
|
3693
|
+
});
|
|
3694
|
+
}
|
|
3695
|
+
this.brackets.delete(symbol);
|
|
3696
|
+
}
|
|
3697
|
+
const receipt = await this.broker.submitOrder({
|
|
3698
|
+
symbol,
|
|
3699
|
+
side: oppositeSide2(pos.side),
|
|
3700
|
+
type: "market",
|
|
3701
|
+
qty: pos.qty,
|
|
3702
|
+
clientOrderId: `${this.id}-close-${Date.now()}`
|
|
3703
|
+
});
|
|
3704
|
+
await this._syncEquityAndRisk();
|
|
3705
|
+
await this.refresh();
|
|
3706
|
+
return receipt;
|
|
3707
|
+
}
|
|
3708
|
+
async flatten() {
|
|
3709
|
+
const positions = await this.broker.getPositions();
|
|
3710
|
+
for (const p of positions) await this.closePosition(p.symbol);
|
|
3711
|
+
const open = await this.broker.getOpenOrders?.().catch(() => []) ?? [];
|
|
3712
|
+
for (const o of open) await this.broker.cancelOrder(o.orderId).catch(() => {
|
|
3713
|
+
});
|
|
3714
|
+
await this.refresh();
|
|
3715
|
+
}
|
|
3716
|
+
async cancelOrder(orderId) {
|
|
3717
|
+
await this.broker.cancelOrder(orderId);
|
|
3718
|
+
await this.refresh();
|
|
3719
|
+
}
|
|
3720
|
+
async getAccount() {
|
|
3721
|
+
return this.broker.getAccount();
|
|
3722
|
+
}
|
|
3723
|
+
async getPositions() {
|
|
3724
|
+
return this.broker.getPositions();
|
|
3725
|
+
}
|
|
3726
|
+
recentEvents(limit = 50) {
|
|
3727
|
+
return this.events.slice(-limit);
|
|
3728
|
+
}
|
|
3729
|
+
getStatus() {
|
|
3730
|
+
const risk = this.riskManager.getState?.() || {};
|
|
3731
|
+
return {
|
|
3732
|
+
id: this.id,
|
|
3733
|
+
symbol: this.symbol,
|
|
3734
|
+
interval: this.interval,
|
|
3735
|
+
mode: this.mode,
|
|
3736
|
+
running: this.running,
|
|
3737
|
+
equity: this.equity,
|
|
3738
|
+
dayPnl: risk.dayPnl ?? 0,
|
|
3739
|
+
lastPrice: this.lastPrice,
|
|
3740
|
+
positions: this._cachedPositions ?? [],
|
|
3741
|
+
openOrders: this._cachedOpenOrders ?? [],
|
|
3742
|
+
risk: { halted: Boolean(risk.halted), ...risk }
|
|
3743
|
+
};
|
|
3744
|
+
}
|
|
3745
|
+
/** Refresh sync caches used by getStatus() */
|
|
3746
|
+
async refresh() {
|
|
3747
|
+
if (this._pendingCancelPromise) {
|
|
3748
|
+
await this._pendingCancelPromise;
|
|
3749
|
+
this._pendingCancelPromise = null;
|
|
3750
|
+
}
|
|
3751
|
+
this._cachedPositions = await this.broker.getPositions().catch(() => []);
|
|
3752
|
+
this._cachedOpenOrders = await this.broker.getOpenOrders?.().catch(() => []) ?? [];
|
|
3753
|
+
const acct = await this.broker.getAccount?.().catch(() => null);
|
|
3754
|
+
if (Number.isFinite(acct?.equity)) this.equity = acct.equity;
|
|
3755
|
+
return this.getStatus();
|
|
3756
|
+
}
|
|
3757
|
+
};
|
|
3758
|
+
var SessionManager = class {
|
|
3759
|
+
constructor({ brokerFactory } = {}) {
|
|
3760
|
+
this.sessions = /* @__PURE__ */ new Map();
|
|
3761
|
+
this.brokerFactory = brokerFactory;
|
|
3762
|
+
}
|
|
3763
|
+
async create({
|
|
3764
|
+
id,
|
|
3765
|
+
mode = "paper",
|
|
3766
|
+
symbol,
|
|
3767
|
+
interval = "1m",
|
|
3768
|
+
equity = 1e4,
|
|
3769
|
+
confirmLive = false,
|
|
3770
|
+
broker,
|
|
3771
|
+
...rest
|
|
3772
|
+
} = {}) {
|
|
3773
|
+
if (this.sessions.has(id)) throw new Error(`session "${id}" already exists`);
|
|
3774
|
+
let resolvedBroker = broker;
|
|
3775
|
+
if (mode === "live") {
|
|
3776
|
+
if (!TradingSession.liveAllowed() || !confirmLive) {
|
|
3777
|
+
throw new Error("live mode requires TRADELAB_ALLOW_LIVE=true and confirmLive:true");
|
|
3778
|
+
}
|
|
3779
|
+
if (!resolvedBroker && this.brokerFactory) {
|
|
3780
|
+
resolvedBroker = this.brokerFactory({ symbol, ...rest });
|
|
3781
|
+
}
|
|
3782
|
+
if (!resolvedBroker) throw new Error("live mode requires a credentialed broker");
|
|
3783
|
+
}
|
|
3784
|
+
if (!resolvedBroker) resolvedBroker = new PaperEngine({ equity });
|
|
3785
|
+
const session = new TradingSession({
|
|
3786
|
+
id,
|
|
3787
|
+
symbol,
|
|
3788
|
+
interval,
|
|
3789
|
+
broker: resolvedBroker,
|
|
3790
|
+
mode,
|
|
3791
|
+
equity,
|
|
3792
|
+
confirmLive,
|
|
3793
|
+
...rest
|
|
3794
|
+
});
|
|
3795
|
+
await session.start();
|
|
3796
|
+
this.sessions.set(session.id, session);
|
|
3797
|
+
return session;
|
|
3798
|
+
}
|
|
3799
|
+
get(id) {
|
|
3800
|
+
return this.sessions.get(id) ?? null;
|
|
3801
|
+
}
|
|
3802
|
+
list() {
|
|
3803
|
+
return [...this.sessions.values()];
|
|
3804
|
+
}
|
|
3805
|
+
async remove(id, { flatten = true } = {}) {
|
|
3806
|
+
const s = this.sessions.get(id);
|
|
3807
|
+
if (!s) return;
|
|
3808
|
+
await s.stop({ flatten });
|
|
3809
|
+
this.sessions.delete(id);
|
|
3810
|
+
}
|
|
3811
|
+
async haltAll() {
|
|
3812
|
+
for (const s of this.sessions.values()) await s.stop({ flatten: true });
|
|
3813
|
+
this.sessions.clear();
|
|
3814
|
+
}
|
|
3815
|
+
};
|
|
3816
|
+
function createSessionManager(opts) {
|
|
3817
|
+
return new SessionManager(opts);
|
|
3818
|
+
}
|
|
3312
3819
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3313
3820
|
0 && (module.exports = {
|
|
3314
3821
|
AlpacaBroker,
|
|
@@ -3329,14 +3836,17 @@ function createLiveOrchestrator(options) {
|
|
|
3329
3836
|
PaperEngine,
|
|
3330
3837
|
PollingFeed,
|
|
3331
3838
|
RiskManager,
|
|
3839
|
+
SessionManager,
|
|
3332
3840
|
StateManager,
|
|
3333
3841
|
StorageProvider,
|
|
3842
|
+
TradingSession,
|
|
3334
3843
|
createAlpacaBroker,
|
|
3335
3844
|
createBinanceBroker,
|
|
3336
3845
|
createBrokerFeed,
|
|
3337
3846
|
createCandleAggregator,
|
|
3338
3847
|
createClock,
|
|
3339
3848
|
createCoinbaseBroker,
|
|
3849
|
+
createDashboardServer,
|
|
3340
3850
|
createEventBus,
|
|
3341
3851
|
createInteractiveBrokersBroker,
|
|
3342
3852
|
createJsonFileStorage,
|
|
@@ -3346,5 +3856,6 @@ function createLiveOrchestrator(options) {
|
|
|
3346
3856
|
createPaperEngine,
|
|
3347
3857
|
createPollingFeed,
|
|
3348
3858
|
createRiskManager,
|
|
3859
|
+
createSessionManager,
|
|
3349
3860
|
createStateManager
|
|
3350
3861
|
});
|