tradelab 1.1.0 → 1.2.1

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 (39) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/README.md +183 -373
  3. package/dist/cjs/index.cjs +39 -12
  4. package/dist/cjs/live.cjs +457 -18
  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 +6 -2
  17. package/src/live/dashboard/server.js +67 -8
  18. package/src/live/engine/paperEngine.js +21 -11
  19. package/src/live/index.js +2 -0
  20. package/src/live/session.js +439 -0
  21. package/src/mcp/liveTools.js +202 -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/src/research/monteCarlo.js +6 -2
  26. package/templates/dashboard.html +595 -108
  27. package/types/index.d.ts +25 -0
  28. package/types/live.d.ts +102 -1
  29. package/types/mcp.d.ts +17 -0
  30. package/docs/superpowers/plans/2026-00-overview.md +0 -101
  31. package/docs/superpowers/plans/2026-01-metrics-correctness.md +0 -873
  32. package/docs/superpowers/plans/2026-02-indicator-library.md +0 -677
  33. package/docs/superpowers/plans/2026-03-overfitting-toolkit.md +0 -882
  34. package/docs/superpowers/plans/2026-04-async-signals-seeding.md +0 -981
  35. package/docs/superpowers/plans/2026-05-mcp-server.md +0 -758
  36. package/docs/superpowers/plans/2026-06-parallel-param-sweep.md +0 -508
  37. package/docs/superpowers/plans/2026-07-funding-carry-costs.md +0 -535
  38. package/docs/superpowers/plans/2026-08-live-dashboard.md +0 -547
  39. 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,20 @@ 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;
3332
+ const finalState = portfolioState(runners, equity);
3330
3333
  const metrics = buildMetrics({
3331
3334
  closed: trades,
3332
3335
  equityStart: equity,
3333
- equityFinal: eqSeries.length ? eqSeries[eqSeries.length - 1].equity : equity,
3336
+ equityFinal: eqSeries.length ? eqSeries[eqSeries.length - 1].equity : finalState.markedEquity,
3334
3337
  candles: orderedCandles,
3335
3338
  estBarMs: estimateBarMs(orderedCandles),
3336
- eqSeries
3339
+ eqSeries,
3340
+ interval: metricsInterval
3337
3341
  });
3338
3342
  return {
3339
3343
  symbol: "PORTFOLIO",
3340
- interval: void 0,
3344
+ interval: metricsInterval,
3341
3345
  range: void 0,
3342
3346
  trades,
3343
3347
  positions,
@@ -3632,7 +3636,9 @@ function walkForwardOptimize({
3632
3636
  // src/engine/optimize.js
3633
3637
  var import_node_worker_threads = require("node:worker_threads");
3634
3638
  var import_node_os = __toESM(require("node:os"), 1);
3635
- var import_meta = {};
3639
+ var import_node_fs = require("node:fs");
3640
+ var import_node_path = __toESM(require("node:path"), 1);
3641
+ var import_node_url = require("node:url");
3636
3642
  function defaultConcurrency() {
3637
3643
  return Math.max(1, (import_node_os.default.cpus()?.length ?? 2) - 1);
3638
3644
  }
@@ -3640,6 +3646,23 @@ function scoreValue(metrics, scoreBy) {
3640
3646
  const v = metrics?.[scoreBy];
3641
3647
  return Number.isFinite(v) ? v : -Infinity;
3642
3648
  }
3649
+ function callerModuleDir() {
3650
+ const stack = new Error().stack || "";
3651
+ const lines = stack.split("\n").slice(1);
3652
+ const match = lines.map((line) => line.match(/(?:\()?(file:\/\/\/[^\s)]+|\/[^\s)]+):\d+:\d+/)).find(Boolean);
3653
+ if (!match) return process.cwd();
3654
+ const filePath = match[1].startsWith("file://") ? (0, import_node_url.fileURLToPath)(match[1]) : match[1];
3655
+ return import_node_path.default.dirname(filePath);
3656
+ }
3657
+ function workerUrl() {
3658
+ const here = callerModuleDir();
3659
+ const candidates = [
3660
+ import_node_path.default.join(here, "optimizeWorker.js"),
3661
+ import_node_path.default.join(here, "..", "..", "src", "engine", "optimizeWorker.js"),
3662
+ import_node_path.default.join(process.cwd(), "src", "engine", "optimizeWorker.js")
3663
+ ];
3664
+ return (0, import_node_url.pathToFileURL)(candidates.find((candidate) => (0, import_node_fs.existsSync)(candidate)) || candidates[0]);
3665
+ }
3643
3666
  function optimize({
3644
3667
  candles,
3645
3668
  signalModulePath,
@@ -3682,7 +3705,7 @@ function optimize({
3682
3705
  worker.postMessage({ type: "run", index, params: parameterSets[index] });
3683
3706
  };
3684
3707
  for (let i = 0; i < poolSize; i += 1) {
3685
- const worker = new import_node_worker_threads.Worker(new URL("./optimizeWorker.js", import_meta.url), {
3708
+ const worker = new import_node_worker_threads.Worker(workerUrl(), {
3686
3709
  workerData: { candles, signalModulePath, interval, backtestOptions }
3687
3710
  });
3688
3711
  workers.push(worker);
@@ -3945,29 +3968,33 @@ function monteCarlo({
3945
3968
  if (!Array.isArray(tradePnls) || tradePnls.length === 0) {
3946
3969
  throw new Error("monteCarlo() requires a non-empty tradePnls array");
3947
3970
  }
3971
+ const runCount = Math.floor(Number(iterations));
3972
+ if (!Number.isFinite(runCount) || runCount < 1) {
3973
+ throw new Error("monteCarlo() requires positive iterations");
3974
+ }
3948
3975
  const rng = makeRng(seed);
3949
3976
  const n = tradePnls.length;
3950
3977
  const block = Math.max(1, Math.floor(blockSize));
3951
3978
  const finals = [];
3952
3979
  const drawdowns = [];
3953
3980
  const pathSamples = Array.from({ length: n + 1 }, () => []);
3954
- for (let it = 0; it < iterations; it += 1) {
3955
- const path6 = [equityStart];
3981
+ for (let it = 0; it < runCount; it += 1) {
3982
+ const path7 = [equityStart];
3956
3983
  let equity = equityStart;
3957
3984
  let filled = 0;
3958
3985
  while (filled < n) {
3959
3986
  const start = randInt(rng, n);
3960
3987
  for (let k = 0; k < block && filled < n; k += 1) {
3961
3988
  equity += tradePnls[(start + k) % n];
3962
- path6.push(equity);
3989
+ path7.push(equity);
3963
3990
  filled += 1;
3964
3991
  }
3965
3992
  }
3966
- for (let step = 0; step < path6.length; step += 1) {
3967
- pathSamples[step].push(path6[step]);
3993
+ for (let step = 0; step < path7.length; step += 1) {
3994
+ pathSamples[step].push(path7[step]);
3968
3995
  }
3969
3996
  finals.push(equity);
3970
- drawdowns.push(maxDrawdownOf(path6));
3997
+ drawdowns.push(maxDrawdownOf(path7));
3971
3998
  }
3972
3999
  const sortedFinals = [...finals].sort((a, b) => a - b);
3973
4000
  const sortedDd = [...drawdowns].sort((a, b) => a - b);
@@ -3983,7 +4010,7 @@ function monteCarlo({
3983
4010
  p95: percentile2(sorted, 0.95)
3984
4011
  });
3985
4012
  return {
3986
- iterations,
4013
+ iterations: runCount,
3987
4014
  blockSize: block,
3988
4015
  finalEquity: bands(sortedFinals),
3989
4016
  maxDrawdown: bands(sortedDd),
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);
@@ -2298,15 +2301,20 @@ var PaperEngine = class extends BrokerAdapter {
2298
2301
  _recordOrder(order) {
2299
2302
  this.orderHistory.set(order.orderId, { ...order });
2300
2303
  }
2304
+ _rejectOrder(order, reason) {
2305
+ order.status = "rejected";
2306
+ order.rejectReason = reason;
2307
+ this._recordOrder(order);
2308
+ this.openOrders.delete(order.orderId);
2309
+ const receipt = cloneOrder(order);
2310
+ this.emit("order:rejected", receipt);
2311
+ return receipt;
2312
+ }
2301
2313
  _fillOrder(order, fillPrice, kind = "market", fillTime = Date.now()) {
2302
2314
  const side = normalizeOrderSide(order.side);
2303
2315
  const qty = Math.max(0, asNumber(order.qty, 0));
2304
2316
  if (!(qty > 0)) {
2305
- order.status = "rejected";
2306
- order.rejectReason = "invalid quantity";
2307
- this._recordOrder(order);
2308
- this.emit("order:rejected", cloneOrder(order));
2309
- return cloneOrder(order);
2317
+ return this._rejectOrder(order, "invalid quantity");
2310
2318
  }
2311
2319
  const sideForFill = side === "buy" ? "long" : "short";
2312
2320
  const filled = applyFill(fillPrice, sideForFill, {
@@ -2414,17 +2422,16 @@ var PaperEngine = class extends BrokerAdapter {
2414
2422
  rejectReason: void 0
2415
2423
  };
2416
2424
  if (!(normalized.qty > 0)) {
2417
- normalized.status = "rejected";
2418
- normalized.rejectReason = "invalid quantity";
2419
- this._recordOrder(normalized);
2420
- this.emit("order:rejected", cloneOrder(normalized));
2421
- return cloneOrder(normalized);
2425
+ return this._rejectOrder(normalized, "invalid quantity");
2422
2426
  }
2423
2427
  this._recordOrder(normalized);
2424
2428
  this.emit("order:submitted", cloneOrder(normalized));
2425
2429
  if (normalized.type === "market") {
2426
2430
  const mark = this.lastPrices.get(normalized.symbol);
2427
- const fillPrice = mark ?? normalized.limitPrice ?? normalized.stopPrice ?? 0;
2431
+ const fillPrice = mark ?? normalized.limitPrice ?? normalized.stopPrice;
2432
+ if (!Number.isFinite(fillPrice)) {
2433
+ return this._rejectOrder(normalized, "no price available for market order");
2434
+ }
2428
2435
  return this._fillOrder(normalized, fillPrice, "market");
2429
2436
  }
2430
2437
  this.openOrders.set(normalized.orderId, normalized);
@@ -2483,6 +2490,7 @@ var PaperEngine = class extends BrokerAdapter {
2483
2490
  });
2484
2491
  const orders = [...this.openOrders.values()].filter((order) => order.symbol === symbol);
2485
2492
  for (const order of orders) {
2493
+ if (!this.openOrders.has(order.orderId)) continue;
2486
2494
  if (order.type === "limit") {
2487
2495
  if (this._touchesLimit(order, normalizedBar)) {
2488
2496
  this._fillOrder(order, order.limitPrice, "limit", normalizedBar.time);
@@ -3317,7 +3325,6 @@ var import_node_http = __toESM(require("node:http"), 1);
3317
3325
  var import_node_fs2 = require("node:fs");
3318
3326
  var import_node_path2 = __toESM(require("node:path"), 1);
3319
3327
  var import_node_url4 = require("node:url");
3320
- var import_meta = {};
3321
3328
  var FALLBACK_HTML = `<!doctype html>
3322
3329
  <html lang="en">
3323
3330
  <head>
@@ -3336,12 +3343,23 @@ var FALLBACK_HTML = `<!doctype html>
3336
3343
  </script>
3337
3344
  </body>
3338
3345
  </html>`;
3346
+ function callerModuleDir() {
3347
+ const stack = new Error().stack || "";
3348
+ const lines = stack.split("\n").slice(1);
3349
+ const match = lines.map((line) => line.match(/(?:\()?(file:\/\/\/[^\s)]+|\/[^\s)]+):\d+:\d+/)).find(Boolean);
3350
+ if (!match) return process.cwd();
3351
+ const filePath = match[1].startsWith("file://") ? (0, import_node_url4.fileURLToPath)(match[1]) : match[1];
3352
+ return import_node_path2.default.dirname(filePath);
3353
+ }
3339
3354
  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
- }
3355
+ const here = callerModuleDir();
3356
+ const candidates = [
3357
+ import_node_path2.default.join(here, "..", "..", "..", "templates", "dashboard.html"),
3358
+ import_node_path2.default.join(here, "..", "..", "templates", "dashboard.html"),
3359
+ import_node_path2.default.join(process.cwd(), "templates", "dashboard.html")
3360
+ ];
3361
+ const htmlPath = candidates.find((candidate) => (0, import_node_fs2.existsSync)(candidate));
3362
+ if (htmlPath) return (0, import_node_fs2.readFileSync)(htmlPath, "utf8");
3345
3363
  try {
3346
3364
  return (0, import_node_fs2.readFileSync)(import_node_path2.default.join(process.cwd(), "templates", "dashboard.html"), "utf8");
3347
3365
  } catch {
@@ -3363,7 +3381,7 @@ function createDashboardServer({ source, port = 4317, maxBuffer = 200 }) {
3363
3381
  `;
3364
3382
  for (const res of clients) res.write(frame);
3365
3383
  });
3366
- const server = import_node_http.default.createServer((req, res) => {
3384
+ const server = import_node_http.default.createServer(async (req, res) => {
3367
3385
  const url = (req.url || "/").split("?")[0];
3368
3386
  if (url === "/") {
3369
3387
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
@@ -3371,11 +3389,52 @@ function createDashboardServer({ source, port = 4317, maxBuffer = 200 }) {
3371
3389
  return;
3372
3390
  }
3373
3391
  if (url === "/state") {
3392
+ if (typeof source.refresh === "function") await source.refresh().catch(() => {
3393
+ });
3374
3394
  const status = typeof source.getStatus === "function" ? source.getStatus() : {};
3375
3395
  res.writeHead(200, { "Content-Type": "application/json" });
3376
3396
  res.end(JSON.stringify(status));
3377
3397
  return;
3378
3398
  }
3399
+ if (url === "/command" && req.method === "POST") {
3400
+ const WHITELIST = {
3401
+ flatten: "flatten",
3402
+ stop: "stop",
3403
+ closePosition: "closePosition",
3404
+ cancelOrder: "cancelOrder"
3405
+ };
3406
+ let body = "";
3407
+ req.on("data", (c) => body += c);
3408
+ req.on("end", async () => {
3409
+ let cmd;
3410
+ try {
3411
+ cmd = JSON.parse(body || "{}");
3412
+ } catch {
3413
+ cmd = {};
3414
+ }
3415
+ const method = WHITELIST[cmd.type];
3416
+ if (!method || typeof source[method] !== "function") {
3417
+ res.writeHead(400, { "Content-Type": "application/json" });
3418
+ res.end(JSON.stringify({ ok: false, error: `unsupported command "${cmd.type}"` }));
3419
+ return;
3420
+ }
3421
+ try {
3422
+ const arg = cmd.type === "closePosition" ? cmd.symbol : cmd.type === "cancelOrder" ? cmd.orderId : void 0;
3423
+ await source[method](arg);
3424
+ res.writeHead(200, { "Content-Type": "application/json" });
3425
+ res.end(JSON.stringify({ ok: true }));
3426
+ } catch (error) {
3427
+ res.writeHead(500, { "Content-Type": "application/json" });
3428
+ res.end(
3429
+ JSON.stringify({
3430
+ ok: false,
3431
+ error: error instanceof Error ? error.message : String(error)
3432
+ })
3433
+ );
3434
+ }
3435
+ });
3436
+ return;
3437
+ }
3379
3438
  if (url === "/events") {
3380
3439
  res.writeHead(200, {
3381
3440
  "Content-Type": "text/event-stream",
@@ -3417,6 +3476,383 @@ function createDashboardServer({ source, port = 4317, maxBuffer = 200 }) {
3417
3476
  server
3418
3477
  };
3419
3478
  }
3479
+
3480
+ // src/live/session.js
3481
+ function oppositeSide2(side) {
3482
+ return side === "long" || side === "buy" ? "sell" : "buy";
3483
+ }
3484
+ function toBrokerSide(side) {
3485
+ return side === "long" || side === "buy" ? "buy" : "sell";
3486
+ }
3487
+ function matchesOrderRef(reference, order) {
3488
+ if (!reference || !order) return false;
3489
+ if (reference.orderId && order.orderId && reference.orderId === order.orderId) return true;
3490
+ if (reference.clientOrderId && order.clientOrderId && reference.clientOrderId === order.clientOrderId) {
3491
+ return true;
3492
+ }
3493
+ return false;
3494
+ }
3495
+ var TradingSession = class _TradingSession {
3496
+ constructor({
3497
+ id,
3498
+ symbol,
3499
+ interval = "1m",
3500
+ broker,
3501
+ mode = "paper",
3502
+ equity = 1e4,
3503
+ riskPct = 1,
3504
+ maxDailyLossPct = 0,
3505
+ maxPositionPct = 1,
3506
+ qtyStep = 1e-3,
3507
+ minQty = 1e-3,
3508
+ maxLeverage = 2,
3509
+ confirmLive = false,
3510
+ eventBus
3511
+ } = {}) {
3512
+ if (mode === "live" && (!_TradingSession.liveAllowed() || !confirmLive)) {
3513
+ throw new Error(
3514
+ "live trading is gated: set TRADELAB_ALLOW_LIVE=true and pass confirmLive:true with a credentialed broker"
3515
+ );
3516
+ }
3517
+ if (!broker) throw new Error("TradingSession requires a broker (PaperEngine by default)");
3518
+ if (!symbol) throw new Error("TradingSession requires a symbol");
3519
+ this.id = id || `${symbol}-${interval}`;
3520
+ this.symbol = symbol;
3521
+ this.interval = interval;
3522
+ this.broker = broker;
3523
+ this.mode = mode;
3524
+ this.equity = equity;
3525
+ this._startEquity = equity;
3526
+ this.riskPct = riskPct;
3527
+ this.maxPositionPct = maxPositionPct;
3528
+ this.qtyStep = qtyStep;
3529
+ this.minQty = minQty;
3530
+ this.maxLeverage = maxLeverage;
3531
+ this.eventBus = eventBus || new EventBus();
3532
+ this.riskManager = new RiskManager({ maxDailyLossPct, maxDrawdownPct: 0 });
3533
+ this.lastPrice = null;
3534
+ this.running = false;
3535
+ this.events = [];
3536
+ this.brackets = /* @__PURE__ */ new Map();
3537
+ this._pendingBracket = null;
3538
+ this._cachedPositions = [];
3539
+ this._cachedOpenOrders = [];
3540
+ this.candleBuffer = [];
3541
+ this._strategy = null;
3542
+ this._wireBrokerEvents();
3543
+ }
3544
+ static liveAllowed() {
3545
+ return process.env.TRADELAB_ALLOW_LIVE === "true";
3546
+ }
3547
+ _record(event, payload) {
3548
+ const msg = { event, payload, t: Date.now() };
3549
+ this.events.push(msg);
3550
+ if (this.events.length > 500) this.events.shift();
3551
+ this.eventBus.emitEvent(event, { sessionId: this.id, symbol: this.symbol, ...payload });
3552
+ }
3553
+ _wireBrokerEvents() {
3554
+ this.broker.on?.("order:filled", (order) => this._onBrokerFillSync(order));
3555
+ this.broker.on?.("order:submitted", (order) => this._record("order:submitted", order));
3556
+ this.broker.on?.(
3557
+ "order:canceled",
3558
+ (order) => this._onBrokerTerminalOrderSync("order:canceled", order)
3559
+ );
3560
+ this.broker.on?.(
3561
+ "order:rejected",
3562
+ (order) => this._onBrokerTerminalOrderSync("order:rejected", order)
3563
+ );
3564
+ this.broker.on?.("equity:update", (acct) => this._record("equity:update", acct));
3565
+ }
3566
+ _onBrokerTerminalOrderSync(event, order) {
3567
+ this._record(event, order);
3568
+ if (matchesOrderRef(this._pendingBracket, order)) {
3569
+ this._pendingBracket = null;
3570
+ }
3571
+ }
3572
+ // Sync event handler — fire-and-forget async OCO work via a stored promise
3573
+ _onBrokerFillSync(order) {
3574
+ this._record("order:filled", order);
3575
+ if (matchesOrderRef(this._pendingBracket, order)) {
3576
+ const staged = this._pendingBracket;
3577
+ this._pendingBracket = null;
3578
+ this._pendingCancelPromise = Promise.resolve(
3579
+ this._attachBracket({ ...staged, receipt: order })
3580
+ );
3581
+ return;
3582
+ }
3583
+ const bracket = this.brackets.get(this.symbol);
3584
+ if (bracket && (order.orderId === bracket.stopId || order.orderId === bracket.targetId)) {
3585
+ const siblingId = order.orderId === bracket.stopId ? bracket.targetId : bracket.stopId;
3586
+ this._pendingCancelPromise = (async () => {
3587
+ if (siblingId) await this.broker.cancelOrder(siblingId).catch(() => {
3588
+ });
3589
+ this.brackets.delete(this.symbol);
3590
+ this._record("position:closed", { reason: order.orderId === bracket.stopId ? "SL" : "TP" });
3591
+ })();
3592
+ }
3593
+ }
3594
+ async start() {
3595
+ if (!this.broker.isConnected?.()) await this.broker.connect?.({});
3596
+ const acct = await this.broker.getAccount?.().catch(() => null);
3597
+ if (Number.isFinite(acct?.equity)) {
3598
+ this.equity = acct.equity;
3599
+ this._startEquity = acct.equity;
3600
+ }
3601
+ this.riskManager.initialize(this.equity, Date.now());
3602
+ this.running = true;
3603
+ this._record("connected", { mode: this.mode });
3604
+ }
3605
+ async stop({ flatten = false } = {}) {
3606
+ if (flatten) await this.flatten();
3607
+ this.running = false;
3608
+ this._record("shutdown", {});
3609
+ }
3610
+ async pushBar(b) {
3611
+ this.lastPrice = b.close;
3612
+ if (typeof this.broker.simulateBar === "function") {
3613
+ await this.broker.simulateBar(this.symbol, this.interval, b);
3614
+ }
3615
+ if (this._pendingCancelPromise) {
3616
+ await this._pendingCancelPromise;
3617
+ this._pendingCancelPromise = null;
3618
+ }
3619
+ this.candleBuffer.push(b);
3620
+ if (this.candleBuffer.length > 200) this.candleBuffer.shift();
3621
+ this._record("bar", { close: b.close, time: b.time });
3622
+ await this._syncEquityAndRisk();
3623
+ await this.refresh();
3624
+ }
3625
+ _riskHalted() {
3626
+ const state = this.riskManager.getState?.() || {};
3627
+ return Boolean(state.halted);
3628
+ }
3629
+ async placeOrder({ side, type = "market", qty, riskPct, stop, target, rr, limitPrice } = {}) {
3630
+ if (!this.running) throw new Error("session not started");
3631
+ if (this._riskHalted()) throw new Error("session is risk-halted for the day");
3632
+ const entryRef = type === "limit" ? limitPrice : this.lastPrice;
3633
+ if (!Number.isFinite(entryRef)) throw new Error("no price available; pushBar() a price first");
3634
+ let size = qty;
3635
+ if (!Number.isFinite(size)) {
3636
+ const fraction = Number.isFinite(riskPct) ? riskPct / 100 : this.riskPct / 100;
3637
+ if (!Number.isFinite(stop)) throw new Error("risk-based sizing requires a stop");
3638
+ size = calculatePositionSize({
3639
+ equity: this.equity,
3640
+ entry: entryRef,
3641
+ stop,
3642
+ riskFraction: fraction,
3643
+ qtyStep: this.qtyStep,
3644
+ minQty: this.minQty,
3645
+ maxLeverage: this.maxLeverage
3646
+ });
3647
+ }
3648
+ size = roundStep(size, this.qtyStep);
3649
+ if (!(size >= this.minQty)) throw new Error(`sized below minQty (${size})`);
3650
+ const entryClientOrderId = `${this.id}-entry-${Date.now()}`;
3651
+ const receipt = await this.broker.submitOrder({
3652
+ symbol: this.symbol,
3653
+ side: toBrokerSide(side),
3654
+ type,
3655
+ qty: size,
3656
+ limitPrice: type === "limit" ? limitPrice : void 0,
3657
+ clientOrderId: entryClientOrderId
3658
+ });
3659
+ if (Number.isFinite(stop) || Number.isFinite(target) || Number.isFinite(rr)) {
3660
+ if (receipt.status === "filled") {
3661
+ await this._attachBracket({ side, size, stop, target, rr, entryRef, receipt });
3662
+ } else if (receipt.status !== "rejected") {
3663
+ this._pendingBracket = {
3664
+ side,
3665
+ size,
3666
+ stop,
3667
+ target,
3668
+ rr,
3669
+ entryRef,
3670
+ orderId: receipt.orderId,
3671
+ clientOrderId: receipt.clientOrderId || entryClientOrderId
3672
+ };
3673
+ } else {
3674
+ this._pendingBracket = null;
3675
+ }
3676
+ }
3677
+ await this.refresh();
3678
+ return receipt;
3679
+ }
3680
+ async _attachBracket({ side, size, stop, target, rr, entryRef, receipt }) {
3681
+ const entryFill = receipt?.avgFillPrice ?? entryRef;
3682
+ const risk = Number.isFinite(stop) ? Math.abs(entryFill - stop) : null;
3683
+ const targetPrice = Number.isFinite(target) ? target : Number.isFinite(rr) && risk ? side === "long" || side === "buy" ? entryFill + rr * risk : entryFill - rr * risk : null;
3684
+ const exitSide = oppositeSide2(side);
3685
+ const bracket = {};
3686
+ if (Number.isFinite(stop)) {
3687
+ const stopOrder = await this.broker.submitOrder({
3688
+ symbol: this.symbol,
3689
+ side: exitSide,
3690
+ type: "stop",
3691
+ qty: size,
3692
+ stopPrice: stop,
3693
+ clientOrderId: `${this.id}-stop-${Date.now()}`
3694
+ });
3695
+ bracket.stopId = stopOrder.orderId;
3696
+ }
3697
+ if (Number.isFinite(targetPrice)) {
3698
+ const tgtOrder = await this.broker.submitOrder({
3699
+ symbol: this.symbol,
3700
+ side: exitSide,
3701
+ type: "limit",
3702
+ qty: size,
3703
+ limitPrice: targetPrice,
3704
+ clientOrderId: `${this.id}-target-${Date.now()}`
3705
+ });
3706
+ bracket.targetId = tgtOrder.orderId;
3707
+ }
3708
+ this.brackets.set(this.symbol, bracket);
3709
+ }
3710
+ async _syncEquityAndRisk() {
3711
+ const acct = await this.broker.getAccount?.().catch(() => null);
3712
+ if (!Number.isFinite(acct?.equity)) return;
3713
+ const prevEquity = this.equity;
3714
+ this.equity = acct.equity;
3715
+ const pnlDelta = this.equity - prevEquity;
3716
+ if (pnlDelta !== 0) {
3717
+ this.riskManager.recordTrade({ pnl: pnlDelta, timeMs: Date.now(), equity: this.equity });
3718
+ } else {
3719
+ this.riskManager.update({ timeMs: Date.now(), equity: this.equity });
3720
+ }
3721
+ }
3722
+ async closePosition(symbol = this.symbol) {
3723
+ const positions = await this.broker.getPositions();
3724
+ const pos = positions.find((p) => p.symbol === symbol);
3725
+ if (!pos) return null;
3726
+ const bracket = this.brackets.get(symbol);
3727
+ if (bracket) {
3728
+ for (const id of [bracket.stopId, bracket.targetId]) {
3729
+ if (id) await this.broker.cancelOrder(id).catch(() => {
3730
+ });
3731
+ }
3732
+ this.brackets.delete(symbol);
3733
+ }
3734
+ const receipt = await this.broker.submitOrder({
3735
+ symbol,
3736
+ side: oppositeSide2(pos.side),
3737
+ type: "market",
3738
+ qty: pos.qty,
3739
+ clientOrderId: `${this.id}-close-${Date.now()}`
3740
+ });
3741
+ await this._syncEquityAndRisk();
3742
+ await this.refresh();
3743
+ return receipt;
3744
+ }
3745
+ async flatten() {
3746
+ const positions = await this.broker.getPositions();
3747
+ for (const p of positions) await this.closePosition(p.symbol);
3748
+ const open = await this.broker.getOpenOrders?.().catch(() => []) ?? [];
3749
+ for (const o of open) await this.broker.cancelOrder(o.orderId).catch(() => {
3750
+ });
3751
+ await this.refresh();
3752
+ }
3753
+ async cancelOrder(orderId) {
3754
+ await this.broker.cancelOrder(orderId);
3755
+ await this.refresh();
3756
+ }
3757
+ async getAccount() {
3758
+ return this.broker.getAccount();
3759
+ }
3760
+ async getPositions() {
3761
+ return this.broker.getPositions();
3762
+ }
3763
+ recentEvents(limit = 50) {
3764
+ return this.events.slice(-limit);
3765
+ }
3766
+ getStatus() {
3767
+ const risk = this.riskManager.getState?.() || {};
3768
+ return {
3769
+ id: this.id,
3770
+ symbol: this.symbol,
3771
+ interval: this.interval,
3772
+ mode: this.mode,
3773
+ running: this.running,
3774
+ equity: this.equity,
3775
+ dayPnl: risk.dayPnl ?? 0,
3776
+ lastPrice: this.lastPrice,
3777
+ positions: this._cachedPositions ?? [],
3778
+ openOrders: this._cachedOpenOrders ?? [],
3779
+ risk: { halted: Boolean(risk.halted), ...risk }
3780
+ };
3781
+ }
3782
+ /** Refresh sync caches used by getStatus() */
3783
+ async refresh() {
3784
+ if (this._pendingCancelPromise) {
3785
+ await this._pendingCancelPromise;
3786
+ this._pendingCancelPromise = null;
3787
+ }
3788
+ this._cachedPositions = await this.broker.getPositions().catch(() => []);
3789
+ this._cachedOpenOrders = await this.broker.getOpenOrders?.().catch(() => []) ?? [];
3790
+ const acct = await this.broker.getAccount?.().catch(() => null);
3791
+ if (Number.isFinite(acct?.equity)) this.equity = acct.equity;
3792
+ return this.getStatus();
3793
+ }
3794
+ };
3795
+ var SessionManager = class {
3796
+ constructor({ brokerFactory } = {}) {
3797
+ this.sessions = /* @__PURE__ */ new Map();
3798
+ this.brokerFactory = brokerFactory;
3799
+ }
3800
+ async create({
3801
+ id,
3802
+ mode = "paper",
3803
+ symbol,
3804
+ interval = "1m",
3805
+ equity = 1e4,
3806
+ confirmLive = false,
3807
+ broker,
3808
+ ...rest
3809
+ } = {}) {
3810
+ if (this.sessions.has(id)) throw new Error(`session "${id}" already exists`);
3811
+ let resolvedBroker = broker;
3812
+ if (mode === "live") {
3813
+ if (!TradingSession.liveAllowed() || !confirmLive) {
3814
+ throw new Error("live mode requires TRADELAB_ALLOW_LIVE=true and confirmLive:true");
3815
+ }
3816
+ if (!resolvedBroker && this.brokerFactory) {
3817
+ resolvedBroker = this.brokerFactory({ symbol, ...rest });
3818
+ }
3819
+ if (!resolvedBroker) throw new Error("live mode requires a credentialed broker");
3820
+ }
3821
+ if (!resolvedBroker) resolvedBroker = new PaperEngine({ equity });
3822
+ const session = new TradingSession({
3823
+ id,
3824
+ symbol,
3825
+ interval,
3826
+ broker: resolvedBroker,
3827
+ mode,
3828
+ equity,
3829
+ confirmLive,
3830
+ ...rest
3831
+ });
3832
+ await session.start();
3833
+ this.sessions.set(session.id, session);
3834
+ return session;
3835
+ }
3836
+ get(id) {
3837
+ return this.sessions.get(id) ?? null;
3838
+ }
3839
+ list() {
3840
+ return [...this.sessions.values()];
3841
+ }
3842
+ async remove(id, { flatten = true } = {}) {
3843
+ const s = this.sessions.get(id);
3844
+ if (!s) return;
3845
+ await s.stop({ flatten });
3846
+ this.sessions.delete(id);
3847
+ }
3848
+ async haltAll() {
3849
+ for (const s of this.sessions.values()) await s.stop({ flatten: true });
3850
+ this.sessions.clear();
3851
+ }
3852
+ };
3853
+ function createSessionManager(opts) {
3854
+ return new SessionManager(opts);
3855
+ }
3420
3856
  // Annotate the CommonJS export names for ESM import in node:
3421
3857
  0 && (module.exports = {
3422
3858
  AlpacaBroker,
@@ -3437,8 +3873,10 @@ function createDashboardServer({ source, port = 4317, maxBuffer = 200 }) {
3437
3873
  PaperEngine,
3438
3874
  PollingFeed,
3439
3875
  RiskManager,
3876
+ SessionManager,
3440
3877
  StateManager,
3441
3878
  StorageProvider,
3879
+ TradingSession,
3442
3880
  createAlpacaBroker,
3443
3881
  createBinanceBroker,
3444
3882
  createBrokerFeed,
@@ -3455,5 +3893,6 @@ function createDashboardServer({ source, port = 4317, maxBuffer = 200 }) {
3455
3893
  createPaperEngine,
3456
3894
  createPollingFeed,
3457
3895
  createRiskManager,
3896
+ createSessionManager,
3458
3897
  createStateManager
3459
3898
  });