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.
Files changed (66) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +188 -328
  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 +1917 -1005
  7. package/dist/cjs/live.cjs +536 -25
  8. package/dist/cjs/ta.cjs +339 -0
  9. package/docs/README.md +32 -66
  10. package/docs/api-reference.md +283 -112
  11. package/docs/backtest-engine.md +210 -252
  12. package/docs/data-reporting-cli.md +114 -156
  13. package/docs/examples.md +6 -6
  14. package/docs/live-trading.md +263 -92
  15. package/docs/mcp.md +285 -0
  16. package/docs/research.md +157 -0
  17. package/examples/liveDashboard.js +33 -0
  18. package/examples/llmSignal.js +33 -0
  19. package/examples/mcpLiveTrading.js +77 -0
  20. package/examples/optimize.js +25 -0
  21. package/package.json +26 -4
  22. package/src/engine/asyncSignal.js +28 -0
  23. package/src/engine/backtest.js +13 -1
  24. package/src/engine/backtestAsync.js +27 -0
  25. package/src/engine/backtestTicks.js +13 -2
  26. package/src/engine/barSystemRunner.js +96 -41
  27. package/src/engine/execution.js +39 -0
  28. package/src/engine/grid.js +15 -0
  29. package/src/engine/llmSignal.js +84 -0
  30. package/src/engine/optimize.js +110 -0
  31. package/src/engine/optimizeWorker.js +67 -0
  32. package/src/engine/portfolio.js +4 -1
  33. package/src/engine/walkForward.js +1 -0
  34. package/src/index.js +9 -0
  35. package/src/live/dashboard/server.js +179 -0
  36. package/src/live/engine/liveEngine.js +2 -2
  37. package/src/live/engine/paperEngine.js +5 -0
  38. package/src/live/index.js +3 -0
  39. package/src/live/session.js +402 -0
  40. package/src/mcp/liveTools.js +179 -0
  41. package/src/mcp/schemas.js +167 -0
  42. package/src/mcp/server.js +35 -0
  43. package/src/mcp/tools.js +265 -0
  44. package/src/metrics/annualize.js +32 -0
  45. package/src/metrics/benchmark.js +55 -0
  46. package/src/metrics/buildMetrics.js +34 -13
  47. package/src/metrics/finite.js +17 -0
  48. package/src/research/combinations.js +18 -0
  49. package/src/research/cpcv.js +47 -0
  50. package/src/research/deflatedSharpe.js +35 -0
  51. package/src/research/index.js +6 -0
  52. package/src/research/monteCarlo.js +88 -0
  53. package/src/research/pbo.js +69 -0
  54. package/src/research/stats.js +78 -0
  55. package/src/strategies/builtins.js +96 -0
  56. package/src/strategies/index.js +30 -0
  57. package/src/ta/channels.js +67 -0
  58. package/src/ta/index.js +16 -0
  59. package/src/ta/oscillators.js +70 -0
  60. package/src/ta/trend.js +78 -0
  61. package/src/utils/random.js +33 -0
  62. package/templates/dashboard.html +661 -0
  63. package/types/index.d.ts +179 -0
  64. package/types/live.d.ts +114 -0
  65. package/types/mcp.d.ts +17 -0
  66. 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, path2, { query = null, body = null, dataApi = false } = {}) {
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}${path2}`, query || {});
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, path2, { signed = false, params = {}, body = null } = {}) {
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}${path2}${qs ? `?${qs}` : ""}`);
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 path2 = this.config.futures ? "/fapi/v1/time" : "/api/v3/time";
675
- const data = await this._request("GET", path2);
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 path2 = this.config.futures ? "/fapi/v1/order" : "/api/v3/order";
739
- const response = await this._request("POST", path2, {
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 path2 = this.config.futures ? "/fapi/v1/order" : "/api/v3/order";
761
- await this._request("DELETE", path2, {
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 path2 = this.config.futures ? "/fapi/v1/order" : "/api/v3/order";
771
- const response = await this._request("PUT", path2, {
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 path2 = this.config.futures ? "/fapi/v1/openOrders" : "/api/v3/openOrders";
797
- const rows = await this._request("GET", path2, { signed: true });
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 path2 = this.config.futures ? "/fapi/v1/order" : "/api/v3/order";
814
- const row = await this._request("GET", path2, {
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 path2 = this.config.futures ? "/fapi/v1/klines" : "/api/v3/klines";
877
- const rows = await this._request("GET", path2, {
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: path2 }) {
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}${path2}`
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, path2, { query = {}, body = null } = {}) {
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}${path2}`);
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 callSignalWithContext({ signal, context, index, bar, symbol }) {
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 = callSignalWithContext({
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
  });