tradelab 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +185 -388
  3. package/dist/cjs/index.cjs +31 -9
  4. package/dist/cjs/live.cjs +409 -7
  5. package/docs/README.md +32 -66
  6. package/docs/api-reference.md +269 -144
  7. package/docs/backtest-engine.md +167 -321
  8. package/docs/data-reporting-cli.md +114 -156
  9. package/docs/examples.md +6 -6
  10. package/docs/live-trading.md +254 -134
  11. package/docs/mcp.md +244 -23
  12. package/docs/research.md +99 -45
  13. package/examples/mcpLiveTrading.js +77 -0
  14. package/package.json +11 -3
  15. package/src/engine/optimize.js +25 -1
  16. package/src/engine/portfolio.js +4 -1
  17. package/src/live/dashboard/server.js +67 -8
  18. package/src/live/engine/paperEngine.js +5 -0
  19. package/src/live/index.js +2 -0
  20. package/src/live/session.js +402 -0
  21. package/src/mcp/liveTools.js +179 -0
  22. package/src/mcp/schemas.js +119 -0
  23. package/src/mcp/server.js +5 -1
  24. package/src/mcp/tools.js +125 -2
  25. package/templates/dashboard.html +595 -108
  26. package/types/index.d.ts +25 -0
  27. package/types/live.d.ts +99 -0
  28. package/types/mcp.d.ts +17 -0
  29. package/docs/superpowers/plans/2026-00-overview.md +0 -101
  30. package/docs/superpowers/plans/2026-01-metrics-correctness.md +0 -873
  31. package/docs/superpowers/plans/2026-02-indicator-library.md +0 -677
  32. package/docs/superpowers/plans/2026-03-overfitting-toolkit.md +0 -882
  33. package/docs/superpowers/plans/2026-04-async-signals-seeding.md +0 -981
  34. package/docs/superpowers/plans/2026-05-mcp-server.md +0 -758
  35. package/docs/superpowers/plans/2026-06-parallel-param-sweep.md +0 -508
  36. package/docs/superpowers/plans/2026-07-funding-carry-costs.md +0 -535
  37. package/docs/superpowers/plans/2026-08-live-dashboard.md +0 -547
  38. package/docs/superpowers/plans/HANDOFF.md +0 -88
@@ -3194,6 +3194,7 @@ function forceExitAll(runners, time) {
3194
3194
  function backtestPortfolio({
3195
3195
  systems = [],
3196
3196
  equity = 1e4,
3197
+ interval,
3197
3198
  allocation = "equal",
3198
3199
  collectEqSeries = true,
3199
3200
  collectReplay = false,
@@ -3327,17 +3328,19 @@ function backtestPortfolio({
3327
3328
  const replay = combineReplay(systemResults, eqSeries, collectReplay);
3328
3329
  const allCandles = systems.flatMap((system) => system.candles || []);
3329
3330
  const orderedCandles = [...allCandles].sort((left, right) => left.time - right.time);
3331
+ const metricsInterval = interval ?? systems[0]?.interval;
3330
3332
  const metrics = buildMetrics({
3331
3333
  closed: trades,
3332
3334
  equityStart: equity,
3333
3335
  equityFinal: eqSeries.length ? eqSeries[eqSeries.length - 1].equity : equity,
3334
3336
  candles: orderedCandles,
3335
3337
  estBarMs: estimateBarMs(orderedCandles),
3336
- eqSeries
3338
+ eqSeries,
3339
+ interval: metricsInterval
3337
3340
  });
3338
3341
  return {
3339
3342
  symbol: "PORTFOLIO",
3340
- interval: void 0,
3343
+ interval: metricsInterval,
3341
3344
  range: void 0,
3342
3345
  trades,
3343
3346
  positions,
@@ -3632,7 +3635,9 @@ function walkForwardOptimize({
3632
3635
  // src/engine/optimize.js
3633
3636
  var import_node_worker_threads = require("node:worker_threads");
3634
3637
  var import_node_os = __toESM(require("node:os"), 1);
3635
- var import_meta = {};
3638
+ var import_node_fs = require("node:fs");
3639
+ var import_node_path = __toESM(require("node:path"), 1);
3640
+ var import_node_url = require("node:url");
3636
3641
  function defaultConcurrency() {
3637
3642
  return Math.max(1, (import_node_os.default.cpus()?.length ?? 2) - 1);
3638
3643
  }
@@ -3640,6 +3645,23 @@ function scoreValue(metrics, scoreBy) {
3640
3645
  const v = metrics?.[scoreBy];
3641
3646
  return Number.isFinite(v) ? v : -Infinity;
3642
3647
  }
3648
+ function callerModuleDir() {
3649
+ const stack = new Error().stack || "";
3650
+ const lines = stack.split("\n").slice(1);
3651
+ const match = lines.map((line) => line.match(/(?:\()?(file:\/\/\/[^\s)]+|\/[^\s)]+):\d+:\d+/)).find(Boolean);
3652
+ if (!match) return process.cwd();
3653
+ const filePath = match[1].startsWith("file://") ? (0, import_node_url.fileURLToPath)(match[1]) : match[1];
3654
+ return import_node_path.default.dirname(filePath);
3655
+ }
3656
+ function workerUrl() {
3657
+ const here = callerModuleDir();
3658
+ const candidates = [
3659
+ import_node_path.default.join(here, "optimizeWorker.js"),
3660
+ import_node_path.default.join(here, "..", "..", "src", "engine", "optimizeWorker.js"),
3661
+ import_node_path.default.join(process.cwd(), "src", "engine", "optimizeWorker.js")
3662
+ ];
3663
+ return (0, import_node_url.pathToFileURL)(candidates.find((candidate) => (0, import_node_fs.existsSync)(candidate)) || candidates[0]);
3664
+ }
3643
3665
  function optimize({
3644
3666
  candles,
3645
3667
  signalModulePath,
@@ -3682,7 +3704,7 @@ function optimize({
3682
3704
  worker.postMessage({ type: "run", index, params: parameterSets[index] });
3683
3705
  };
3684
3706
  for (let i = 0; i < poolSize; i += 1) {
3685
- const worker = new import_node_worker_threads.Worker(new URL("./optimizeWorker.js", import_meta.url), {
3707
+ const worker = new import_node_worker_threads.Worker(workerUrl(), {
3686
3708
  workerData: { candles, signalModulePath, interval, backtestOptions }
3687
3709
  });
3688
3710
  workers.push(worker);
@@ -3952,22 +3974,22 @@ function monteCarlo({
3952
3974
  const drawdowns = [];
3953
3975
  const pathSamples = Array.from({ length: n + 1 }, () => []);
3954
3976
  for (let it = 0; it < iterations; it += 1) {
3955
- const path6 = [equityStart];
3977
+ const path7 = [equityStart];
3956
3978
  let equity = equityStart;
3957
3979
  let filled = 0;
3958
3980
  while (filled < n) {
3959
3981
  const start = randInt(rng, n);
3960
3982
  for (let k = 0; k < block && filled < n; k += 1) {
3961
3983
  equity += tradePnls[(start + k) % n];
3962
- path6.push(equity);
3984
+ path7.push(equity);
3963
3985
  filled += 1;
3964
3986
  }
3965
3987
  }
3966
- for (let step = 0; step < path6.length; step += 1) {
3967
- pathSamples[step].push(path6[step]);
3988
+ for (let step = 0; step < path7.length; step += 1) {
3989
+ pathSamples[step].push(path7[step]);
3968
3990
  }
3969
3991
  finals.push(equity);
3970
- drawdowns.push(maxDrawdownOf(path6));
3992
+ drawdowns.push(maxDrawdownOf(path7));
3971
3993
  }
3972
3994
  const sortedFinals = [...finals].sort((a, b) => a - b);
3973
3995
  const sortedDd = [...drawdowns].sort((a, b) => a - b);
package/dist/cjs/live.cjs CHANGED
@@ -48,8 +48,10 @@ __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,
@@ -66,6 +68,7 @@ __export(index_exports, {
66
68
  createPaperEngine: () => createPaperEngine,
67
69
  createPollingFeed: () => createPollingFeed,
68
70
  createRiskManager: () => createRiskManager,
71
+ createSessionManager: () => createSessionManager,
69
72
  createStateManager: () => createStateManager
70
73
  });
71
74
  module.exports = __toCommonJS(index_exports);
@@ -2483,6 +2486,7 @@ var PaperEngine = class extends BrokerAdapter {
2483
2486
  });
2484
2487
  const orders = [...this.openOrders.values()].filter((order) => order.symbol === symbol);
2485
2488
  for (const order of orders) {
2489
+ if (!this.openOrders.has(order.orderId)) continue;
2486
2490
  if (order.type === "limit") {
2487
2491
  if (this._touchesLimit(order, normalizedBar)) {
2488
2492
  this._fillOrder(order, order.limitPrice, "limit", normalizedBar.time);
@@ -3317,7 +3321,6 @@ var import_node_http = __toESM(require("node:http"), 1);
3317
3321
  var import_node_fs2 = require("node:fs");
3318
3322
  var import_node_path2 = __toESM(require("node:path"), 1);
3319
3323
  var import_node_url4 = require("node:url");
3320
- var import_meta = {};
3321
3324
  var FALLBACK_HTML = `<!doctype html>
3322
3325
  <html lang="en">
3323
3326
  <head>
@@ -3336,12 +3339,23 @@ var FALLBACK_HTML = `<!doctype html>
3336
3339
  </script>
3337
3340
  </body>
3338
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
+ }
3339
3350
  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
- }
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");
3345
3359
  try {
3346
3360
  return (0, import_node_fs2.readFileSync)(import_node_path2.default.join(process.cwd(), "templates", "dashboard.html"), "utf8");
3347
3361
  } catch {
@@ -3363,7 +3377,7 @@ function createDashboardServer({ source, port = 4317, maxBuffer = 200 }) {
3363
3377
  `;
3364
3378
  for (const res of clients) res.write(frame);
3365
3379
  });
3366
- const server = import_node_http.default.createServer((req, res) => {
3380
+ const server = import_node_http.default.createServer(async (req, res) => {
3367
3381
  const url = (req.url || "/").split("?")[0];
3368
3382
  if (url === "/") {
3369
3383
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
@@ -3371,11 +3385,52 @@ function createDashboardServer({ source, port = 4317, maxBuffer = 200 }) {
3371
3385
  return;
3372
3386
  }
3373
3387
  if (url === "/state") {
3388
+ if (typeof source.refresh === "function") await source.refresh().catch(() => {
3389
+ });
3374
3390
  const status = typeof source.getStatus === "function" ? source.getStatus() : {};
3375
3391
  res.writeHead(200, { "Content-Type": "application/json" });
3376
3392
  res.end(JSON.stringify(status));
3377
3393
  return;
3378
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
+ }
3379
3434
  if (url === "/events") {
3380
3435
  res.writeHead(200, {
3381
3436
  "Content-Type": "text/event-stream",
@@ -3417,6 +3472,350 @@ function createDashboardServer({ source, port = 4317, maxBuffer = 200 }) {
3417
3472
  server
3418
3473
  };
3419
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
+ }
3420
3819
  // Annotate the CommonJS export names for ESM import in node:
3421
3820
  0 && (module.exports = {
3422
3821
  AlpacaBroker,
@@ -3437,8 +3836,10 @@ function createDashboardServer({ source, port = 4317, maxBuffer = 200 }) {
3437
3836
  PaperEngine,
3438
3837
  PollingFeed,
3439
3838
  RiskManager,
3839
+ SessionManager,
3440
3840
  StateManager,
3441
3841
  StorageProvider,
3842
+ TradingSession,
3442
3843
  createAlpacaBroker,
3443
3844
  createBinanceBroker,
3444
3845
  createBrokerFeed,
@@ -3455,5 +3856,6 @@ function createDashboardServer({ source, port = 4317, maxBuffer = 200 }) {
3455
3856
  createPaperEngine,
3456
3857
  createPollingFeed,
3457
3858
  createRiskManager,
3859
+ createSessionManager,
3458
3860
  createStateManager
3459
3861
  });