tradelab 0.5.0 → 1.0.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 (54) hide show
  1. package/README.md +89 -41
  2. package/bin/tradelab.js +276 -30
  3. package/dist/cjs/data.cjs +134 -104
  4. package/dist/cjs/index.cjs +378 -177
  5. package/dist/cjs/live.cjs +3350 -0
  6. package/docs/README.md +21 -9
  7. package/docs/api-reference.md +87 -29
  8. package/docs/backtest-engine.md +37 -53
  9. package/docs/data-reporting-cli.md +60 -34
  10. package/docs/examples.md +6 -12
  11. package/docs/live-trading.md +186 -0
  12. package/examples/yahooEmaCross.js +1 -6
  13. package/package.json +18 -3
  14. package/src/data/csv.js +24 -14
  15. package/src/data/index.js +1 -5
  16. package/src/data/yahoo.js +6 -19
  17. package/src/engine/backtest.js +137 -144
  18. package/src/engine/backtestTicks.js +89 -37
  19. package/src/engine/barSystemRunner.js +182 -118
  20. package/src/engine/execution.js +11 -39
  21. package/src/engine/portfolio.js +54 -6
  22. package/src/engine/walkForward.js +37 -14
  23. package/src/index.js +2 -11
  24. package/src/live/broker/alpaca.js +254 -0
  25. package/src/live/broker/binance.js +351 -0
  26. package/src/live/broker/coinbase.js +339 -0
  27. package/src/live/broker/interactiveBrokers.js +123 -0
  28. package/src/live/broker/interface.js +74 -0
  29. package/src/live/clock.js +56 -0
  30. package/src/live/engine/candleAggregator.js +154 -0
  31. package/src/live/engine/liveEngine.js +694 -0
  32. package/src/live/engine/paperEngine.js +453 -0
  33. package/src/live/engine/riskManager.js +185 -0
  34. package/src/live/engine/stateManager.js +112 -0
  35. package/src/live/events.js +48 -0
  36. package/src/live/feed/brokerFeed.js +35 -0
  37. package/src/live/feed/interface.js +28 -0
  38. package/src/live/feed/pollingFeed.js +105 -0
  39. package/src/live/index.js +27 -0
  40. package/src/live/logger.js +82 -0
  41. package/src/live/orchestrator.js +133 -0
  42. package/src/live/storage/interface.js +36 -0
  43. package/src/live/storage/jsonFileStorage.js +112 -0
  44. package/src/metrics/buildMetrics.js +18 -41
  45. package/src/reporting/exportBacktestArtifacts.js +1 -4
  46. package/src/reporting/exportTradesCsv.js +2 -7
  47. package/src/reporting/renderHtmlReport.js +8 -13
  48. package/src/utils/indicators.js +1 -2
  49. package/src/utils/positionSizing.js +16 -2
  50. package/src/utils/time.js +4 -12
  51. package/templates/report.html +23 -9
  52. package/templates/report.js +83 -69
  53. package/types/index.d.ts +21 -3
  54. package/types/live.d.ts +382 -0
@@ -0,0 +1,3350 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/live/index.js
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ AlpacaBroker: () => AlpacaBroker,
34
+ BinanceBroker: () => BinanceBroker,
35
+ BrokerAdapter: () => BrokerAdapter,
36
+ BrokerClock: () => BrokerClock,
37
+ BrokerFeed: () => BrokerFeed,
38
+ CandleAggregator: () => CandleAggregator,
39
+ CoinbaseBroker: () => CoinbaseBroker,
40
+ EventBus: () => EventBus,
41
+ FeedProvider: () => FeedProvider,
42
+ InteractiveBrokersBroker: () => InteractiveBrokersBroker,
43
+ JsonFileStorage: () => JsonFileStorage,
44
+ LIVE_EVENTS: () => LIVE_EVENTS,
45
+ LiveEngine: () => LiveEngine,
46
+ LiveLogger: () => LiveLogger,
47
+ LiveOrchestrator: () => LiveOrchestrator,
48
+ PaperEngine: () => PaperEngine,
49
+ PollingFeed: () => PollingFeed,
50
+ RiskManager: () => RiskManager,
51
+ StateManager: () => StateManager,
52
+ StorageProvider: () => StorageProvider,
53
+ createAlpacaBroker: () => createAlpacaBroker,
54
+ createBinanceBroker: () => createBinanceBroker,
55
+ createBrokerFeed: () => createBrokerFeed,
56
+ createCandleAggregator: () => createCandleAggregator,
57
+ createClock: () => createClock,
58
+ createCoinbaseBroker: () => createCoinbaseBroker,
59
+ createEventBus: () => createEventBus,
60
+ createInteractiveBrokersBroker: () => createInteractiveBrokersBroker,
61
+ createJsonFileStorage: () => createJsonFileStorage,
62
+ createLiveEngine: () => createLiveEngine,
63
+ createLiveOrchestrator: () => createLiveOrchestrator,
64
+ createLogger: () => createLogger,
65
+ createPaperEngine: () => createPaperEngine,
66
+ createPollingFeed: () => createPollingFeed,
67
+ createRiskManager: () => createRiskManager,
68
+ createStateManager: () => createStateManager
69
+ });
70
+ module.exports = __toCommonJS(index_exports);
71
+
72
+ // src/live/events.js
73
+ var import_node_events = require("node:events");
74
+ var LIVE_EVENTS = [
75
+ "signal",
76
+ "order:submitted",
77
+ "order:filled",
78
+ "order:canceled",
79
+ "order:rejected",
80
+ "order:modified",
81
+ "position:opened",
82
+ "position:updated",
83
+ "position:closed",
84
+ "equity:update",
85
+ "risk:warning",
86
+ "risk:halt",
87
+ "bar",
88
+ "tick",
89
+ "error",
90
+ "connected",
91
+ "disconnected",
92
+ "reconnecting",
93
+ "shutdown",
94
+ "stateRestored",
95
+ "reconciled"
96
+ ];
97
+ var EventBus = class extends import_node_events.EventEmitter {
98
+ emitEvent(event, payload = {}) {
99
+ this.emit(event, payload);
100
+ this.emit("*", { event, payload });
101
+ return true;
102
+ }
103
+ onAny(handler) {
104
+ this.on("*", handler);
105
+ return () => this.off("*", handler);
106
+ }
107
+ };
108
+ function createEventBus() {
109
+ return new EventBus();
110
+ }
111
+
112
+ // src/live/logger.js
113
+ var LOG_PRIORITIES = {
114
+ debug: 10,
115
+ info: 20,
116
+ warn: 30,
117
+ error: 40,
118
+ silent: 100
119
+ };
120
+ function normalizeLevel(level) {
121
+ return Object.prototype.hasOwnProperty.call(LOG_PRIORITIES, level) ? level : "info";
122
+ }
123
+ var LiveLogger = class {
124
+ constructor({ level = "info", stream = process.stdout } = {}) {
125
+ this.level = normalizeLevel(level);
126
+ this.stream = stream;
127
+ this._unsub = null;
128
+ }
129
+ shouldLog(level) {
130
+ return LOG_PRIORITIES[level] >= LOG_PRIORITIES[this.level];
131
+ }
132
+ write(level, message, fields = {}) {
133
+ const normalizedLevel = normalizeLevel(level);
134
+ if (!this.shouldLog(normalizedLevel)) return;
135
+ const record = {
136
+ t: (/* @__PURE__ */ new Date()).toISOString(),
137
+ level: normalizedLevel,
138
+ msg: message,
139
+ ...fields
140
+ };
141
+ this.stream.write(`${JSON.stringify(record)}
142
+ `);
143
+ }
144
+ debug(message, fields) {
145
+ this.write("debug", message, fields);
146
+ }
147
+ info(message, fields) {
148
+ this.write("info", message, fields);
149
+ }
150
+ warn(message, fields) {
151
+ this.write("warn", message, fields);
152
+ }
153
+ error(message, fields) {
154
+ this.write("error", message, fields);
155
+ }
156
+ attach(eventBus) {
157
+ if (!eventBus || typeof eventBus.onAny !== "function") return () => {
158
+ };
159
+ this.detach();
160
+ this._unsub = eventBus.onAny(({ event, payload }) => {
161
+ const level = event === "error" ? "error" : event.startsWith("risk:") ? "warn" : event === "reconnecting" || event === "disconnected" ? "warn" : "info";
162
+ this.write(level, event, { event, payload });
163
+ });
164
+ return () => this.detach();
165
+ }
166
+ detach() {
167
+ if (typeof this._unsub === "function") {
168
+ this._unsub();
169
+ this._unsub = null;
170
+ }
171
+ }
172
+ };
173
+ function createLogger(options) {
174
+ return new LiveLogger(options);
175
+ }
176
+
177
+ // src/live/clock.js
178
+ var BrokerClock = class {
179
+ constructor({ warnThresholdMs = 2e3 } = {}) {
180
+ this.warnThresholdMs = Math.max(0, warnThresholdMs);
181
+ this.offsetMs = 0;
182
+ this.syncedAt = null;
183
+ }
184
+ now() {
185
+ return Date.now() + this.offsetMs;
186
+ }
187
+ getOffsetMs() {
188
+ return this.offsetMs;
189
+ }
190
+ async syncWithBroker(broker) {
191
+ if (!broker || typeof broker.getServerTime !== "function") {
192
+ this.offsetMs = 0;
193
+ this.syncedAt = Date.now();
194
+ return {
195
+ serverTime: null,
196
+ localTime: this.syncedAt,
197
+ offsetMs: this.offsetMs,
198
+ warning: null
199
+ };
200
+ }
201
+ let serverTime = null;
202
+ try {
203
+ serverTime = Number(await broker.getServerTime());
204
+ } catch {
205
+ serverTime = null;
206
+ }
207
+ const localTime = Date.now();
208
+ this.offsetMs = Number.isFinite(serverTime) ? serverTime - localTime : 0;
209
+ this.syncedAt = localTime;
210
+ const warning = Math.abs(this.offsetMs) > this.warnThresholdMs ? `clock offset ${this.offsetMs}ms exceeds threshold ${this.warnThresholdMs}ms` : null;
211
+ return {
212
+ serverTime,
213
+ localTime,
214
+ offsetMs: this.offsetMs,
215
+ warning
216
+ };
217
+ }
218
+ };
219
+ function createClock(options) {
220
+ return new BrokerClock(options);
221
+ }
222
+
223
+ // src/live/broker/interface.js
224
+ var import_node_events2 = require("node:events");
225
+ function notImplemented(method) {
226
+ throw new Error(`BrokerAdapter.${method}() not implemented`);
227
+ }
228
+ var BrokerAdapter = class extends import_node_events2.EventEmitter {
229
+ async connect(_config = {}) {
230
+ notImplemented("connect");
231
+ }
232
+ async disconnect() {
233
+ notImplemented("disconnect");
234
+ }
235
+ isConnected() {
236
+ notImplemented("isConnected");
237
+ }
238
+ async getAccount() {
239
+ notImplemented("getAccount");
240
+ }
241
+ async getPositions() {
242
+ notImplemented("getPositions");
243
+ }
244
+ async getServerTime() {
245
+ return Date.now();
246
+ }
247
+ async submitOrder(_order) {
248
+ notImplemented("submitOrder");
249
+ }
250
+ async cancelOrder(_orderId) {
251
+ notImplemented("cancelOrder");
252
+ }
253
+ async modifyOrder(_orderId, _changes) {
254
+ notImplemented("modifyOrder");
255
+ }
256
+ async getOpenOrders() {
257
+ notImplemented("getOpenOrders");
258
+ }
259
+ async getOrderStatus(_orderId) {
260
+ notImplemented("getOrderStatus");
261
+ }
262
+ async subscribeQuotes(_symbol, _handler) {
263
+ notImplemented("subscribeQuotes");
264
+ }
265
+ async subscribeTrades(_symbol, _handler) {
266
+ notImplemented("subscribeTrades");
267
+ }
268
+ async subscribeBars(_symbol, _interval, _handler) {
269
+ notImplemented("subscribeBars");
270
+ }
271
+ async getHistoricalBars(_symbol, _interval, _limit = 200) {
272
+ notImplemented("getHistoricalBars");
273
+ }
274
+ supportsPaperNative() {
275
+ return false;
276
+ }
277
+ };
278
+
279
+ // src/live/broker/alpaca.js
280
+ var import_node_url = require("node:url");
281
+
282
+ // src/data/csv.js
283
+ function resolveDate(value, customDateParser) {
284
+ if (value === void 0 || value === null || value === "") {
285
+ throw new Error("Missing date value");
286
+ }
287
+ if (typeof customDateParser === "function") {
288
+ const parsed2 = customDateParser(value);
289
+ const time = parsed2 instanceof Date ? parsed2.getTime() : Number(parsed2);
290
+ if (Number.isFinite(time)) return time;
291
+ }
292
+ if (value instanceof Date) {
293
+ const time = value.getTime();
294
+ if (Number.isFinite(time)) return time;
295
+ }
296
+ const raw = String(value).trim().replace(/^['"]|['"]$/g, "");
297
+ const numeric = Number(raw);
298
+ if (Number.isFinite(numeric)) {
299
+ return numeric < 1e11 ? numeric * 1e3 : numeric;
300
+ }
301
+ const parsed = Date.parse(raw);
302
+ if (Number.isFinite(parsed)) return parsed;
303
+ const mt = raw.match(/^(\d{4})\.(\d{2})\.(\d{2})\s+(\d{2}):(\d{2})(?::(\d{2}))?$/);
304
+ if (mt) {
305
+ const [, year, month, day, hour, minute, second = "0"] = mt;
306
+ return new Date(
307
+ Number(year),
308
+ Number(month) - 1,
309
+ Number(day),
310
+ Number(hour),
311
+ Number(minute),
312
+ Number(second)
313
+ ).getTime();
314
+ }
315
+ throw new Error(`Cannot parse date: ${raw}`);
316
+ }
317
+ function normalizeCandles(candles) {
318
+ if (!Array.isArray(candles)) return [];
319
+ const parsed = candles.map((bar) => {
320
+ try {
321
+ const time = resolveDate(bar?.time ?? bar?.timestamp ?? bar?.date);
322
+ const open = Number(bar?.open ?? bar?.o);
323
+ const high = Number(bar?.high ?? bar?.h);
324
+ const low = Number(bar?.low ?? bar?.l);
325
+ const close = Number(bar?.close ?? bar?.c);
326
+ const volume = Number(bar?.volume ?? bar?.v ?? 0);
327
+ if (!Number.isFinite(time) || !Number.isFinite(open) || !Number.isFinite(high) || !Number.isFinite(low) || !Number.isFinite(close)) {
328
+ return null;
329
+ }
330
+ return {
331
+ time,
332
+ open,
333
+ high: Math.max(high, open, close),
334
+ low: Math.min(low, open, close),
335
+ close,
336
+ volume: Number.isFinite(volume) ? volume : 0
337
+ };
338
+ } catch {
339
+ return null;
340
+ }
341
+ }).filter(Boolean);
342
+ let reordered = false;
343
+ let duplicateCount = 0;
344
+ for (let index = 1; index < parsed.length; index += 1) {
345
+ const prev = parsed[index - 1].time;
346
+ const current = parsed[index].time;
347
+ if (current < prev) reordered = true;
348
+ if (current === prev) duplicateCount += 1;
349
+ }
350
+ const normalized = parsed.sort((left, right) => left.time - right.time);
351
+ const deduped = [];
352
+ let lastTime = null;
353
+ for (const candle of normalized) {
354
+ if (candle.time === lastTime) continue;
355
+ deduped.push(candle);
356
+ lastTime = candle.time;
357
+ }
358
+ const removedDuplicates = normalized.length - deduped.length;
359
+ if (reordered || removedDuplicates > 0 || duplicateCount > 0) {
360
+ console.warn(
361
+ `[tradelab] normalizeCandles() reordered or deduplicated candles (input=${candles.length}, valid=${parsed.length}, output=${deduped.length})`
362
+ );
363
+ }
364
+ return deduped;
365
+ }
366
+
367
+ // src/live/broker/alpaca.js
368
+ function withQuery(url, query = {}) {
369
+ const target = new import_node_url.URL(url);
370
+ for (const [key, value] of Object.entries(query)) {
371
+ if (value === void 0 || value === null) continue;
372
+ target.searchParams.set(key, String(value));
373
+ }
374
+ return target.toString();
375
+ }
376
+ function mapOrderStatus(status) {
377
+ const normalized = String(status || "").toLowerCase();
378
+ if (normalized === "partially_filled") return "partially_filled";
379
+ if (normalized === "filled") return "filled";
380
+ if (normalized === "canceled" || normalized === "cancelled") return "canceled";
381
+ if (normalized === "rejected") return "rejected";
382
+ if (normalized === "expired") return "expired";
383
+ return "new";
384
+ }
385
+ function mapOrderReceipt(order) {
386
+ return {
387
+ orderId: String(order.id),
388
+ clientOrderId: order.client_order_id,
389
+ status: mapOrderStatus(order.status),
390
+ filledQty: Number(order.filled_qty || 0),
391
+ avgFillPrice: Number.isFinite(Number(order.filled_avg_price)) ? Number(order.filled_avg_price) : void 0,
392
+ filledAt: order.filled_at ? Date.parse(order.filled_at) : void 0,
393
+ symbol: order.symbol,
394
+ side: order.side,
395
+ type: String(order.type || "").toLowerCase(),
396
+ qty: Number(order.qty || 0),
397
+ rejectReason: order.reject_reason
398
+ };
399
+ }
400
+ var AlpacaBroker = class extends BrokerAdapter {
401
+ constructor({ fetchImpl = globalThis.fetch } = {}) {
402
+ super();
403
+ this.fetch = fetchImpl;
404
+ this.connected = false;
405
+ this.config = {};
406
+ this.subscriptions = {
407
+ bars: /* @__PURE__ */ new Map(),
408
+ quotes: /* @__PURE__ */ new Map(),
409
+ trades: /* @__PURE__ */ new Map()
410
+ };
411
+ }
412
+ async connect(config = {}) {
413
+ this.config = { ...config };
414
+ this.baseUrl = config.baseUrl || (config.paper ? "https://paper-api.alpaca.markets" : "https://api.alpaca.markets");
415
+ this.dataUrl = config.dataUrl || "https://data.alpaca.markets";
416
+ this.connected = true;
417
+ }
418
+ async disconnect() {
419
+ this.connected = false;
420
+ this.subscriptions.bars.clear();
421
+ this.subscriptions.quotes.clear();
422
+ this.subscriptions.trades.clear();
423
+ }
424
+ isConnected() {
425
+ return this.connected;
426
+ }
427
+ supportsPaperNative() {
428
+ return true;
429
+ }
430
+ _headers(extra = {}) {
431
+ return {
432
+ "content-type": "application/json",
433
+ "APCA-API-KEY-ID": this.config.apiKey || "",
434
+ "APCA-API-SECRET-KEY": this.config.apiSecret || "",
435
+ ...extra
436
+ };
437
+ }
438
+ async _request(method, path2, { query = null, body = null, dataApi = false } = {}) {
439
+ if (!this.fetch) throw new Error("global fetch is unavailable");
440
+ const base = dataApi ? this.dataUrl : this.baseUrl;
441
+ const url = withQuery(`${base}${path2}`, query || {});
442
+ const response = await this.fetch(url, {
443
+ method,
444
+ headers: this._headers(),
445
+ body: body ? JSON.stringify(body) : void 0
446
+ });
447
+ const text = await response.text();
448
+ const payload = text ? JSON.parse(text) : {};
449
+ if (!response.ok) {
450
+ const message = payload?.message || payload?.error || `alpaca request failed (${response.status})`;
451
+ throw new Error(message);
452
+ }
453
+ return payload;
454
+ }
455
+ async getAccount() {
456
+ const account = await this._request("GET", "/v2/account");
457
+ return {
458
+ equity: Number(account.equity || 0),
459
+ buyingPower: Number(account.buying_power || 0),
460
+ cash: Number(account.cash || 0),
461
+ currency: account.currency || "USD",
462
+ marginUsed: Number(account.initial_margin || 0)
463
+ };
464
+ }
465
+ async getPositions() {
466
+ const positions = await this._request("GET", "/v2/positions");
467
+ return positions.map((position) => ({
468
+ symbol: position.symbol,
469
+ side: String(position.side || "long").toLowerCase(),
470
+ qty: Number(position.qty || 0),
471
+ avgEntry: Number(position.avg_entry_price || 0),
472
+ marketValue: Number(position.market_value || 0),
473
+ unrealizedPnl: Number(position.unrealized_pl || 0)
474
+ }));
475
+ }
476
+ async getServerTime() {
477
+ const clock = await this._request("GET", "/v2/clock");
478
+ return clock.timestamp ? Date.parse(clock.timestamp) : Date.now();
479
+ }
480
+ async submitOrder(order) {
481
+ const payload = {
482
+ symbol: order.symbol,
483
+ side: order.side,
484
+ type: order.type,
485
+ qty: String(order.qty),
486
+ time_in_force: order.timeInForce || "day",
487
+ client_order_id: order.clientOrderId
488
+ };
489
+ if (order.limitPrice !== void 0) payload.limit_price = String(order.limitPrice);
490
+ if (order.stopPrice !== void 0) payload.stop_price = String(order.stopPrice);
491
+ const response = await this._request("POST", "/v2/orders", { body: payload });
492
+ const receipt = mapOrderReceipt(response);
493
+ this.emit("order:submitted", receipt);
494
+ return receipt;
495
+ }
496
+ async cancelOrder(orderId) {
497
+ await this._request("DELETE", `/v2/orders/${orderId}`);
498
+ this.emit("order:canceled", { orderId });
499
+ }
500
+ async modifyOrder(orderId, changes) {
501
+ const payload = {};
502
+ if (changes.qty !== void 0) payload.qty = String(changes.qty);
503
+ if (changes.limitPrice !== void 0) payload.limit_price = String(changes.limitPrice);
504
+ if (changes.stopPrice !== void 0) payload.stop_price = String(changes.stopPrice);
505
+ const response = await this._request("PATCH", `/v2/orders/${orderId}`, { body: payload });
506
+ const receipt = mapOrderReceipt(response);
507
+ this.emit("order:modified", receipt);
508
+ return receipt;
509
+ }
510
+ async getOpenOrders() {
511
+ const orders = await this._request("GET", "/v2/orders", { query: { status: "open" } });
512
+ return orders.map(mapOrderReceipt);
513
+ }
514
+ async getOrderStatus(orderId) {
515
+ const order = await this._request("GET", `/v2/orders/${orderId}`);
516
+ return mapOrderReceipt(order);
517
+ }
518
+ async subscribeQuotes(symbol, handler) {
519
+ const key = symbol;
520
+ const list = this.subscriptions.quotes.get(key) || [];
521
+ list.push(handler);
522
+ this.subscriptions.quotes.set(key, list);
523
+ return {
524
+ unsubscribe: () => {
525
+ const current = this.subscriptions.quotes.get(key) || [];
526
+ this.subscriptions.quotes.set(
527
+ key,
528
+ current.filter((candidate) => candidate !== handler)
529
+ );
530
+ }
531
+ };
532
+ }
533
+ async subscribeTrades(symbol, handler) {
534
+ const key = symbol;
535
+ const list = this.subscriptions.trades.get(key) || [];
536
+ list.push(handler);
537
+ this.subscriptions.trades.set(key, list);
538
+ return {
539
+ unsubscribe: () => {
540
+ const current = this.subscriptions.trades.get(key) || [];
541
+ this.subscriptions.trades.set(
542
+ key,
543
+ current.filter((candidate) => candidate !== handler)
544
+ );
545
+ }
546
+ };
547
+ }
548
+ async subscribeBars(symbol, interval, handler) {
549
+ const key = `${symbol}::${interval}`;
550
+ const list = this.subscriptions.bars.get(key) || [];
551
+ list.push(handler);
552
+ this.subscriptions.bars.set(key, list);
553
+ return {
554
+ unsubscribe: () => {
555
+ const current = this.subscriptions.bars.get(key) || [];
556
+ this.subscriptions.bars.set(
557
+ key,
558
+ current.filter((candidate) => candidate !== handler)
559
+ );
560
+ }
561
+ };
562
+ }
563
+ async getHistoricalBars(symbol, interval, limit = 200) {
564
+ const response = await this._request("GET", `/v2/stocks/${symbol}/bars`, {
565
+ dataApi: true,
566
+ query: {
567
+ timeframe: interval,
568
+ limit
569
+ }
570
+ });
571
+ const bars = Array.isArray(response?.bars) ? response.bars.map((bar) => ({
572
+ time: Date.parse(bar.t),
573
+ open: Number(bar.o),
574
+ high: Number(bar.h),
575
+ low: Number(bar.l),
576
+ close: Number(bar.c),
577
+ volume: Number(bar.v ?? 0)
578
+ })) : [];
579
+ return normalizeCandles(bars);
580
+ }
581
+ };
582
+ function createAlpacaBroker(options) {
583
+ return new AlpacaBroker(options);
584
+ }
585
+
586
+ // src/live/broker/binance.js
587
+ var import_node_crypto = __toESM(require("node:crypto"), 1);
588
+ var import_node_url2 = require("node:url");
589
+ function queryString(params = {}) {
590
+ const parts = [];
591
+ for (const [key, value] of Object.entries(params)) {
592
+ if (value === void 0 || value === null) continue;
593
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
594
+ }
595
+ return parts.join("&");
596
+ }
597
+ function mapOrderStatus2(status) {
598
+ const normalized = String(status || "").toUpperCase();
599
+ if (normalized === "PARTIALLY_FILLED") return "partially_filled";
600
+ if (normalized === "FILLED") return "filled";
601
+ if (normalized === "CANCELED" || normalized === "CANCELLED") return "canceled";
602
+ if (normalized === "REJECTED") return "rejected";
603
+ if (normalized === "EXPIRED" || normalized === "EXPIRED_IN_MATCH") return "expired";
604
+ return "new";
605
+ }
606
+ var BinanceBroker = class extends BrokerAdapter {
607
+ constructor({ fetchImpl = globalThis.fetch } = {}) {
608
+ super();
609
+ this.fetch = fetchImpl;
610
+ this.connected = false;
611
+ this.config = {};
612
+ this.subscriptions = { bars: /* @__PURE__ */ new Map(), trades: /* @__PURE__ */ new Map(), quotes: /* @__PURE__ */ new Map() };
613
+ }
614
+ async connect(config = {}) {
615
+ this.config = { ...config };
616
+ const useFutures = Boolean(config.futures);
617
+ if (config.baseUrl) {
618
+ this.baseUrl = config.baseUrl;
619
+ } else if (config.paper && useFutures) {
620
+ this.baseUrl = "https://testnet.binancefuture.com";
621
+ } else if (config.paper) {
622
+ this.baseUrl = "https://testnet.binance.vision";
623
+ } else if (useFutures) {
624
+ this.baseUrl = "https://fapi.binance.com";
625
+ } else {
626
+ this.baseUrl = "https://api.binance.com";
627
+ }
628
+ this.connected = true;
629
+ }
630
+ async disconnect() {
631
+ this.connected = false;
632
+ this.subscriptions.bars.clear();
633
+ this.subscriptions.trades.clear();
634
+ this.subscriptions.quotes.clear();
635
+ }
636
+ isConnected() {
637
+ return this.connected;
638
+ }
639
+ supportsPaperNative() {
640
+ return true;
641
+ }
642
+ _signedParams(params = {}) {
643
+ const base = {
644
+ ...params,
645
+ timestamp: Date.now()
646
+ };
647
+ const payload = queryString(base);
648
+ const signature = import_node_crypto.default.createHmac("sha256", this.config.apiSecret || "").update(payload).digest("hex");
649
+ return { ...base, signature };
650
+ }
651
+ async _request(method, path2, { signed = false, params = {}, body = null } = {}) {
652
+ if (!this.fetch) throw new Error("global fetch is unavailable");
653
+ const finalParams = signed ? this._signedParams(params) : params;
654
+ const qs = queryString(finalParams);
655
+ const url = new import_node_url2.URL(`${this.baseUrl}${path2}${qs ? `?${qs}` : ""}`);
656
+ const headers = {
657
+ "content-type": "application/json"
658
+ };
659
+ if (this.config.apiKey) headers["X-MBX-APIKEY"] = this.config.apiKey;
660
+ const response = await this.fetch(url, {
661
+ method,
662
+ headers,
663
+ body: body ? JSON.stringify(body) : void 0
664
+ });
665
+ const text = await response.text();
666
+ const payload = text ? JSON.parse(text) : {};
667
+ if (!response.ok) {
668
+ const message = payload?.msg || payload?.message || `binance request failed (${response.status})`;
669
+ throw new Error(message);
670
+ }
671
+ return payload;
672
+ }
673
+ async getServerTime() {
674
+ const path2 = this.config.futures ? "/fapi/v1/time" : "/api/v3/time";
675
+ const data = await this._request("GET", path2);
676
+ return Number(data.serverTime || Date.now());
677
+ }
678
+ async getAccount() {
679
+ if (this.config.futures) {
680
+ const account2 = await this._request("GET", "/fapi/v2/account", { signed: true });
681
+ return {
682
+ equity: Number(account2.totalWalletBalance || 0),
683
+ buyingPower: Number(account2.availableBalance || 0),
684
+ cash: Number(account2.availableBalance || 0),
685
+ currency: "USDT",
686
+ marginUsed: Number(account2.totalPositionInitialMargin || 0)
687
+ };
688
+ }
689
+ const account = await this._request("GET", "/api/v3/account", { signed: true });
690
+ const free = Number(
691
+ (account.balances || []).reduce((sum, item) => sum + Number(item.free || 0), 0)
692
+ );
693
+ return {
694
+ equity: free,
695
+ buyingPower: free,
696
+ cash: free,
697
+ currency: "USDT",
698
+ marginUsed: 0
699
+ };
700
+ }
701
+ async getPositions() {
702
+ if (this.config.futures) {
703
+ const rows = await this._request("GET", "/fapi/v2/positionRisk", { signed: true });
704
+ return rows.map((row) => ({
705
+ symbol: row.symbol,
706
+ qty: Math.abs(Number(row.positionAmt || 0)),
707
+ side: Number(row.positionAmt || 0) >= 0 ? "long" : "short",
708
+ avgEntry: Number(row.entryPrice || 0),
709
+ marketValue: Math.abs(Number(row.positionAmt || 0) * Number(row.markPrice || 0)),
710
+ unrealizedPnl: Number(row.unRealizedProfit || 0)
711
+ })).filter((row) => row.qty > 0);
712
+ }
713
+ const account = await this._request("GET", "/api/v3/account", { signed: true });
714
+ return (account.balances || []).map((asset) => ({
715
+ symbol: `${asset.asset}USDT`,
716
+ side: "long",
717
+ qty: Number(asset.free || 0),
718
+ avgEntry: 0,
719
+ marketValue: Number(asset.free || 0),
720
+ unrealizedPnl: 0
721
+ })).filter((position) => position.qty > 0);
722
+ }
723
+ _orderPayload(order) {
724
+ const payload = {
725
+ symbol: order.symbol,
726
+ side: String(order.side || "").toUpperCase(),
727
+ quantity: String(order.qty),
728
+ type: order.type === "stop_limit" ? "STOP_LOSS_LIMIT" : String(order.type || "market").toUpperCase(),
729
+ timeInForce: String(order.timeInForce || "GTC").toUpperCase(),
730
+ newClientOrderId: order.clientOrderId
731
+ };
732
+ if (order.limitPrice !== void 0) payload.price = String(order.limitPrice);
733
+ if (order.stopPrice !== void 0) payload.stopPrice = String(order.stopPrice);
734
+ if (payload.type === "MARKET") delete payload.timeInForce;
735
+ return payload;
736
+ }
737
+ async submitOrder(order) {
738
+ const path2 = this.config.futures ? "/fapi/v1/order" : "/api/v3/order";
739
+ const response = await this._request("POST", path2, {
740
+ signed: true,
741
+ params: this._orderPayload(order)
742
+ });
743
+ const receipt = {
744
+ orderId: String(response.orderId),
745
+ clientOrderId: response.clientOrderId,
746
+ status: mapOrderStatus2(response.status),
747
+ filledQty: Number(response.executedQty || 0),
748
+ avgFillPrice: Number.isFinite(Number(response.avgPrice)) ? Number(response.avgPrice) : void 0,
749
+ filledAt: response.transactTime ? Number(response.transactTime) : void 0,
750
+ symbol: response.symbol,
751
+ side: String(response.side || "").toLowerCase(),
752
+ type: String(response.type || "").toLowerCase(),
753
+ qty: Number(response.origQty || 0),
754
+ rejectReason: response.rejectReason
755
+ };
756
+ this.emit("order:submitted", receipt);
757
+ return receipt;
758
+ }
759
+ async cancelOrder(orderId) {
760
+ const path2 = this.config.futures ? "/fapi/v1/order" : "/api/v3/order";
761
+ await this._request("DELETE", path2, {
762
+ signed: true,
763
+ params: {
764
+ orderId
765
+ }
766
+ });
767
+ this.emit("order:canceled", { orderId: String(orderId) });
768
+ }
769
+ async modifyOrder(orderId, changes = {}) {
770
+ const path2 = this.config.futures ? "/fapi/v1/order" : "/api/v3/order";
771
+ const response = await this._request("PUT", path2, {
772
+ signed: true,
773
+ params: {
774
+ orderId,
775
+ quantity: changes.qty,
776
+ price: changes.limitPrice,
777
+ stopPrice: changes.stopPrice
778
+ }
779
+ });
780
+ const receipt = {
781
+ orderId: String(response.orderId),
782
+ clientOrderId: response.clientOrderId,
783
+ status: mapOrderStatus2(response.status),
784
+ filledQty: Number(response.executedQty || 0),
785
+ avgFillPrice: Number(response.avgPrice || 0) || void 0,
786
+ filledAt: response.updateTime ? Number(response.updateTime) : void 0,
787
+ symbol: response.symbol,
788
+ side: String(response.side || "").toLowerCase(),
789
+ type: String(response.type || "").toLowerCase(),
790
+ qty: Number(response.origQty || 0)
791
+ };
792
+ this.emit("order:modified", receipt);
793
+ return receipt;
794
+ }
795
+ async getOpenOrders() {
796
+ const path2 = this.config.futures ? "/fapi/v1/openOrders" : "/api/v3/openOrders";
797
+ const rows = await this._request("GET", path2, { signed: true });
798
+ return rows.map((row) => ({
799
+ orderId: String(row.orderId),
800
+ clientOrderId: row.clientOrderId,
801
+ status: mapOrderStatus2(row.status),
802
+ filledQty: Number(row.executedQty || 0),
803
+ avgFillPrice: Number(row.avgPrice || 0) || void 0,
804
+ filledAt: row.updateTime ? Number(row.updateTime) : void 0,
805
+ symbol: row.symbol,
806
+ side: String(row.side || "").toLowerCase(),
807
+ type: String(row.type || "").toLowerCase(),
808
+ qty: Number(row.origQty || 0),
809
+ rejectReason: row.rejectReason
810
+ }));
811
+ }
812
+ async getOrderStatus(orderId) {
813
+ const path2 = this.config.futures ? "/fapi/v1/order" : "/api/v3/order";
814
+ const row = await this._request("GET", path2, {
815
+ signed: true,
816
+ params: { orderId }
817
+ });
818
+ return {
819
+ orderId: String(row.orderId),
820
+ clientOrderId: row.clientOrderId,
821
+ status: mapOrderStatus2(row.status),
822
+ filledQty: Number(row.executedQty || 0),
823
+ avgFillPrice: Number(row.avgPrice || 0) || void 0,
824
+ filledAt: row.updateTime ? Number(row.updateTime) : void 0,
825
+ symbol: row.symbol,
826
+ side: String(row.side || "").toLowerCase(),
827
+ type: String(row.type || "").toLowerCase(),
828
+ qty: Number(row.origQty || 0),
829
+ rejectReason: row.rejectReason
830
+ };
831
+ }
832
+ async subscribeQuotes(symbol, handler) {
833
+ const list = this.subscriptions.quotes.get(symbol) || [];
834
+ list.push(handler);
835
+ this.subscriptions.quotes.set(symbol, list);
836
+ return {
837
+ unsubscribe: () => {
838
+ const current = this.subscriptions.quotes.get(symbol) || [];
839
+ this.subscriptions.quotes.set(
840
+ symbol,
841
+ current.filter((candidate) => candidate !== handler)
842
+ );
843
+ }
844
+ };
845
+ }
846
+ async subscribeTrades(symbol, handler) {
847
+ const list = this.subscriptions.trades.get(symbol) || [];
848
+ list.push(handler);
849
+ this.subscriptions.trades.set(symbol, list);
850
+ return {
851
+ unsubscribe: () => {
852
+ const current = this.subscriptions.trades.get(symbol) || [];
853
+ this.subscriptions.trades.set(
854
+ symbol,
855
+ current.filter((candidate) => candidate !== handler)
856
+ );
857
+ }
858
+ };
859
+ }
860
+ async subscribeBars(symbol, interval, handler) {
861
+ const key = `${symbol}::${interval}`;
862
+ const list = this.subscriptions.bars.get(key) || [];
863
+ list.push(handler);
864
+ this.subscriptions.bars.set(key, list);
865
+ return {
866
+ unsubscribe: () => {
867
+ const current = this.subscriptions.bars.get(key) || [];
868
+ this.subscriptions.bars.set(
869
+ key,
870
+ current.filter((candidate) => candidate !== handler)
871
+ );
872
+ }
873
+ };
874
+ }
875
+ 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, {
878
+ params: { symbol, interval, limit }
879
+ });
880
+ const bars = rows.map((row) => ({
881
+ time: Number(row[0]),
882
+ open: Number(row[1]),
883
+ high: Number(row[2]),
884
+ low: Number(row[3]),
885
+ close: Number(row[4]),
886
+ volume: Number(row[5] || 0)
887
+ }));
888
+ return normalizeCandles(bars);
889
+ }
890
+ };
891
+ function createBinanceBroker(options) {
892
+ return new BinanceBroker(options);
893
+ }
894
+
895
+ // src/live/broker/coinbase.js
896
+ var import_node_crypto2 = __toESM(require("node:crypto"), 1);
897
+ var import_node_url3 = require("node:url");
898
+ function base64url(input) {
899
+ return Buffer.from(input).toString("base64url");
900
+ }
901
+ function buildJwt({ key, secret, method, host, path: path2 }) {
902
+ const now = Math.floor(Date.now() / 1e3);
903
+ const header = { alg: "HS256", typ: "JWT", kid: key };
904
+ const payload = {
905
+ iss: "cdp",
906
+ sub: key,
907
+ nbf: now - 5,
908
+ exp: now + 120,
909
+ uri: `${method.toUpperCase()} ${host}${path2}`
910
+ };
911
+ const encodedHeader = base64url(JSON.stringify(header));
912
+ const encodedPayload = base64url(JSON.stringify(payload));
913
+ const signingInput = `${encodedHeader}.${encodedPayload}`;
914
+ const signature = import_node_crypto2.default.createHmac("sha256", secret).update(signingInput).digest("base64url");
915
+ return `${signingInput}.${signature}`;
916
+ }
917
+ function mapOrderStatus3(status) {
918
+ const normalized = String(status || "").toUpperCase();
919
+ if (normalized.includes("PARTIALLY")) return "partially_filled";
920
+ if (normalized.includes("FILLED")) return "filled";
921
+ if (normalized.includes("CANCEL")) return "canceled";
922
+ if (normalized.includes("REJECT")) return "rejected";
923
+ if (normalized.includes("EXPIRE")) return "expired";
924
+ return "new";
925
+ }
926
+ function productToSymbol(productId) {
927
+ return String(productId || "").replace("-", "");
928
+ }
929
+ var CoinbaseBroker = class extends BrokerAdapter {
930
+ constructor({ fetchImpl = globalThis.fetch } = {}) {
931
+ super();
932
+ this.fetch = fetchImpl;
933
+ this.connected = false;
934
+ this.config = {};
935
+ this.baseUrl = "https://api.coinbase.com/api/v3/brokerage";
936
+ this.subscriptions = { bars: /* @__PURE__ */ new Map(), trades: /* @__PURE__ */ new Map(), quotes: /* @__PURE__ */ new Map() };
937
+ }
938
+ async connect(config = {}) {
939
+ this.config = { ...config };
940
+ if (config.baseUrl) this.baseUrl = config.baseUrl;
941
+ this.connected = true;
942
+ }
943
+ async disconnect() {
944
+ this.connected = false;
945
+ this.subscriptions.bars.clear();
946
+ this.subscriptions.trades.clear();
947
+ this.subscriptions.quotes.clear();
948
+ }
949
+ isConnected() {
950
+ return this.connected;
951
+ }
952
+ supportsPaperNative() {
953
+ return false;
954
+ }
955
+ async getServerTime() {
956
+ return Date.now();
957
+ }
958
+ _authHeader(method, url) {
959
+ const target = new import_node_url3.URL(url);
960
+ return buildJwt({
961
+ key: this.config.apiKey || "",
962
+ secret: this.config.apiSecret || "",
963
+ method,
964
+ host: target.host,
965
+ path: target.pathname
966
+ });
967
+ }
968
+ async _request(method, path2, { query = {}, body = null } = {}) {
969
+ if (!this.fetch) throw new Error("global fetch is unavailable");
970
+ const url = new import_node_url3.URL(`${this.baseUrl}${path2}`);
971
+ for (const [key, value] of Object.entries(query || {})) {
972
+ if (value === void 0 || value === null) continue;
973
+ url.searchParams.set(key, String(value));
974
+ }
975
+ const response = await this.fetch(url, {
976
+ method,
977
+ headers: {
978
+ "content-type": "application/json",
979
+ Authorization: `Bearer ${this._authHeader(method, url)}`
980
+ },
981
+ body: body ? JSON.stringify(body) : void 0
982
+ });
983
+ const text = await response.text();
984
+ const payload = text ? JSON.parse(text) : {};
985
+ if (!response.ok) {
986
+ const message = payload?.error_response?.message || payload?.message || `coinbase request failed (${response.status})`;
987
+ throw new Error(message);
988
+ }
989
+ return payload;
990
+ }
991
+ async getAccount() {
992
+ const payload = await this._request("GET", "/accounts");
993
+ const accounts = payload.accounts || [];
994
+ const balances = accounts.map((entry) => Number(entry.available_balance?.value || 0));
995
+ const equity = balances.reduce((sum, value) => sum + value, 0);
996
+ return {
997
+ equity,
998
+ buyingPower: equity,
999
+ cash: equity,
1000
+ currency: "USD",
1001
+ marginUsed: 0
1002
+ };
1003
+ }
1004
+ async getPositions() {
1005
+ const payload = await this._request("GET", "/accounts");
1006
+ const accounts = payload.accounts || [];
1007
+ return accounts.map((entry) => {
1008
+ const qty = Number(entry.available_balance?.value || 0);
1009
+ return {
1010
+ symbol: productToSymbol(entry.currency),
1011
+ side: "long",
1012
+ qty,
1013
+ avgEntry: 0,
1014
+ marketValue: qty,
1015
+ unrealizedPnl: 0
1016
+ };
1017
+ }).filter((position) => position.qty > 0);
1018
+ }
1019
+ async submitOrder(order) {
1020
+ const orderType = String(order.type || "market").toLowerCase();
1021
+ const payload = {
1022
+ client_order_id: order.clientOrderId || import_node_crypto2.default.randomUUID(),
1023
+ product_id: order.symbol,
1024
+ side: String(order.side || "buy").toUpperCase(),
1025
+ order_configuration: {}
1026
+ };
1027
+ if (orderType === "market") {
1028
+ payload.order_configuration.market_market_ioc = {
1029
+ base_size: String(order.qty)
1030
+ };
1031
+ } else if (orderType === "limit") {
1032
+ payload.order_configuration.limit_limit_gtc = {
1033
+ base_size: String(order.qty),
1034
+ limit_price: String(order.limitPrice)
1035
+ };
1036
+ } else {
1037
+ payload.order_configuration.stop_limit_stop_limit_gtc = {
1038
+ base_size: String(order.qty),
1039
+ stop_price: String(order.stopPrice),
1040
+ limit_price: String(order.limitPrice ?? order.stopPrice)
1041
+ };
1042
+ }
1043
+ const response = await this._request("POST", "/orders", { body: payload });
1044
+ const result = response.success_response || response.order || {};
1045
+ const receipt = {
1046
+ orderId: String(result.order_id || response.order_id || payload.client_order_id),
1047
+ clientOrderId: payload.client_order_id,
1048
+ status: mapOrderStatus3(result.status || "PENDING"),
1049
+ filledQty: Number(result.filled_size || 0),
1050
+ avgFillPrice: Number(result.average_filled_price || 0) || void 0,
1051
+ filledAt: result.last_fill_time ? Date.parse(result.last_fill_time) : void 0,
1052
+ symbol: order.symbol,
1053
+ side: String(order.side || "buy").toLowerCase(),
1054
+ type: orderType,
1055
+ qty: Number(order.qty || 0),
1056
+ rejectReason: result.reject_reason
1057
+ };
1058
+ this.emit("order:submitted", receipt);
1059
+ return receipt;
1060
+ }
1061
+ async cancelOrder(orderId) {
1062
+ await this._request("POST", "/orders/batch_cancel", { body: { order_ids: [String(orderId)] } });
1063
+ this.emit("order:canceled", { orderId: String(orderId) });
1064
+ }
1065
+ async modifyOrder(orderId, changes = {}) {
1066
+ const response = await this._request("POST", "/orders/edit", {
1067
+ body: {
1068
+ order_id: String(orderId),
1069
+ size: changes.qty ? String(changes.qty) : void 0,
1070
+ limit_price: changes.limitPrice ? String(changes.limitPrice) : void 0,
1071
+ stop_price: changes.stopPrice ? String(changes.stopPrice) : void 0
1072
+ }
1073
+ });
1074
+ const result = response.success_response || {};
1075
+ const receipt = {
1076
+ orderId: String(result.order_id || orderId),
1077
+ clientOrderId: result.client_order_id,
1078
+ status: mapOrderStatus3(result.status || "PENDING"),
1079
+ filledQty: Number(result.filled_size || 0),
1080
+ avgFillPrice: Number(result.average_filled_price || 0) || void 0,
1081
+ filledAt: result.last_fill_time ? Date.parse(result.last_fill_time) : void 0,
1082
+ symbol: result.product_id || "",
1083
+ side: String(result.side || "").toLowerCase(),
1084
+ type: String(result.order_type || "").toLowerCase(),
1085
+ qty: Number(result.base_size || 0),
1086
+ rejectReason: result.reject_reason
1087
+ };
1088
+ this.emit("order:modified", receipt);
1089
+ return receipt;
1090
+ }
1091
+ async getOpenOrders() {
1092
+ const response = await this._request("GET", "/orders/historical/batch", {
1093
+ query: { order_status: "OPEN" }
1094
+ });
1095
+ const orders = response.orders || [];
1096
+ return orders.map((order) => ({
1097
+ orderId: String(order.order_id),
1098
+ clientOrderId: order.client_order_id,
1099
+ status: mapOrderStatus3(order.status),
1100
+ filledQty: Number(order.filled_size || 0),
1101
+ avgFillPrice: Number(order.average_filled_price || 0) || void 0,
1102
+ filledAt: order.last_fill_time ? Date.parse(order.last_fill_time) : void 0,
1103
+ symbol: order.product_id,
1104
+ side: String(order.side || "").toLowerCase(),
1105
+ type: String(order.order_type || "").toLowerCase(),
1106
+ qty: Number(order.base_size || 0),
1107
+ rejectReason: order.reject_reason
1108
+ }));
1109
+ }
1110
+ async getOrderStatus(orderId) {
1111
+ const response = await this._request("GET", `/orders/historical/${orderId}`);
1112
+ const order = response.order || {};
1113
+ return {
1114
+ orderId: String(order.order_id || orderId),
1115
+ clientOrderId: order.client_order_id,
1116
+ status: mapOrderStatus3(order.status),
1117
+ filledQty: Number(order.filled_size || 0),
1118
+ avgFillPrice: Number(order.average_filled_price || 0) || void 0,
1119
+ filledAt: order.last_fill_time ? Date.parse(order.last_fill_time) : void 0,
1120
+ symbol: order.product_id || "",
1121
+ side: String(order.side || "").toLowerCase(),
1122
+ type: String(order.order_type || "").toLowerCase(),
1123
+ qty: Number(order.base_size || 0),
1124
+ rejectReason: order.reject_reason
1125
+ };
1126
+ }
1127
+ async subscribeQuotes(symbol, handler) {
1128
+ const list = this.subscriptions.quotes.get(symbol) || [];
1129
+ list.push(handler);
1130
+ this.subscriptions.quotes.set(symbol, list);
1131
+ return {
1132
+ unsubscribe: () => {
1133
+ const current = this.subscriptions.quotes.get(symbol) || [];
1134
+ this.subscriptions.quotes.set(
1135
+ symbol,
1136
+ current.filter((candidate) => candidate !== handler)
1137
+ );
1138
+ }
1139
+ };
1140
+ }
1141
+ async subscribeTrades(symbol, handler) {
1142
+ const list = this.subscriptions.trades.get(symbol) || [];
1143
+ list.push(handler);
1144
+ this.subscriptions.trades.set(symbol, list);
1145
+ return {
1146
+ unsubscribe: () => {
1147
+ const current = this.subscriptions.trades.get(symbol) || [];
1148
+ this.subscriptions.trades.set(
1149
+ symbol,
1150
+ current.filter((candidate) => candidate !== handler)
1151
+ );
1152
+ }
1153
+ };
1154
+ }
1155
+ async subscribeBars(symbol, interval, handler) {
1156
+ const key = `${symbol}::${interval}`;
1157
+ const list = this.subscriptions.bars.get(key) || [];
1158
+ list.push(handler);
1159
+ this.subscriptions.bars.set(key, list);
1160
+ return {
1161
+ unsubscribe: () => {
1162
+ const current = this.subscriptions.bars.get(key) || [];
1163
+ this.subscriptions.bars.set(
1164
+ key,
1165
+ current.filter((candidate) => candidate !== handler)
1166
+ );
1167
+ }
1168
+ };
1169
+ }
1170
+ async getHistoricalBars(symbol, interval, limit = 200) {
1171
+ const granularity = (() => {
1172
+ const raw = String(interval || "1m").toLowerCase();
1173
+ if (raw.endsWith("m")) return Number(raw.slice(0, -1)) * 60;
1174
+ if (raw.endsWith("h")) return Number(raw.slice(0, -1)) * 3600;
1175
+ if (raw.endsWith("d")) return Number(raw.slice(0, -1)) * 86400;
1176
+ return 60;
1177
+ })();
1178
+ const response = await this._request("GET", `/products/${symbol}/candles`, {
1179
+ query: {
1180
+ granularity,
1181
+ limit
1182
+ }
1183
+ });
1184
+ const rows = response.candles || response || [];
1185
+ const bars = rows.map((row) => ({
1186
+ time: Number(row.start || row.time || row[0]) * 1e3,
1187
+ low: Number(row.low ?? row[1]),
1188
+ high: Number(row.high ?? row[2]),
1189
+ open: Number(row.open ?? row[3]),
1190
+ close: Number(row.close ?? row[4]),
1191
+ volume: Number(row.volume ?? row[5] ?? 0)
1192
+ }));
1193
+ return normalizeCandles(bars);
1194
+ }
1195
+ };
1196
+ function createCoinbaseBroker(options) {
1197
+ return new CoinbaseBroker(options);
1198
+ }
1199
+
1200
+ // src/live/broker/interactiveBrokers.js
1201
+ var InteractiveBrokersBroker = class extends BrokerAdapter {
1202
+ constructor() {
1203
+ super();
1204
+ this.connected = false;
1205
+ this.config = {};
1206
+ this.ibModule = null;
1207
+ this.orderCounter = 1;
1208
+ this.orders = /* @__PURE__ */ new Map();
1209
+ this.positions = /* @__PURE__ */ new Map();
1210
+ }
1211
+ async connect(config = {}) {
1212
+ this.config = { ...config };
1213
+ try {
1214
+ this.ibModule = await import("@stoqey/ib");
1215
+ } catch {
1216
+ throw new Error(
1217
+ 'InteractiveBrokersBroker requires optional peer dependency "@stoqey/ib". Install it to enable IB support.'
1218
+ );
1219
+ }
1220
+ this.connected = true;
1221
+ }
1222
+ async disconnect() {
1223
+ this.connected = false;
1224
+ }
1225
+ isConnected() {
1226
+ return this.connected;
1227
+ }
1228
+ supportsPaperNative() {
1229
+ return true;
1230
+ }
1231
+ async getServerTime() {
1232
+ return Date.now();
1233
+ }
1234
+ async getAccount() {
1235
+ return {
1236
+ equity: 0,
1237
+ buyingPower: 0,
1238
+ cash: 0,
1239
+ currency: "USD",
1240
+ marginUsed: 0
1241
+ };
1242
+ }
1243
+ async getPositions() {
1244
+ return [...this.positions.values()];
1245
+ }
1246
+ async submitOrder(order) {
1247
+ const receipt = {
1248
+ orderId: String(this.orderCounter++),
1249
+ clientOrderId: order.clientOrderId,
1250
+ status: "new",
1251
+ filledQty: 0,
1252
+ symbol: order.symbol,
1253
+ side: order.side,
1254
+ type: order.type,
1255
+ qty: Number(order.qty || 0),
1256
+ avgFillPrice: void 0,
1257
+ filledAt: void 0
1258
+ };
1259
+ this.orders.set(receipt.orderId, receipt);
1260
+ this.emit("order:submitted", receipt);
1261
+ return receipt;
1262
+ }
1263
+ async cancelOrder(orderId) {
1264
+ const order = this.orders.get(String(orderId));
1265
+ if (!order) return;
1266
+ order.status = "canceled";
1267
+ this.emit("order:canceled", { ...order });
1268
+ }
1269
+ async modifyOrder(orderId, changes = {}) {
1270
+ const order = this.orders.get(String(orderId));
1271
+ if (!order) throw new Error(`IB order "${orderId}" not found`);
1272
+ if (changes.qty !== void 0) order.qty = Number(changes.qty || order.qty);
1273
+ if (changes.limitPrice !== void 0) order.limitPrice = Number(changes.limitPrice);
1274
+ if (changes.stopPrice !== void 0) order.stopPrice = Number(changes.stopPrice);
1275
+ this.emit("order:modified", { ...order });
1276
+ return { ...order };
1277
+ }
1278
+ async getOpenOrders() {
1279
+ return [...this.orders.values()].filter((order) => order.status === "new");
1280
+ }
1281
+ async getOrderStatus(orderId) {
1282
+ const order = this.orders.get(String(orderId));
1283
+ if (!order) throw new Error(`IB order "${orderId}" not found`);
1284
+ return { ...order };
1285
+ }
1286
+ async subscribeQuotes(_symbol, _handler) {
1287
+ return { unsubscribe: () => {
1288
+ } };
1289
+ }
1290
+ async subscribeTrades(_symbol, _handler) {
1291
+ return { unsubscribe: () => {
1292
+ } };
1293
+ }
1294
+ async subscribeBars(_symbol, _interval, _handler) {
1295
+ return { unsubscribe: () => {
1296
+ } };
1297
+ }
1298
+ async getHistoricalBars(_symbol, _interval, _limit = 200) {
1299
+ return [];
1300
+ }
1301
+ };
1302
+ function createInteractiveBrokersBroker(options) {
1303
+ return new InteractiveBrokersBroker(options);
1304
+ }
1305
+
1306
+ // src/live/feed/interface.js
1307
+ function notImplemented2(method) {
1308
+ throw new Error(`FeedProvider.${method}() not implemented`);
1309
+ }
1310
+ var FeedProvider = class {
1311
+ async connect() {
1312
+ notImplemented2("connect");
1313
+ }
1314
+ async disconnect() {
1315
+ notImplemented2("disconnect");
1316
+ }
1317
+ subscribeBars(_symbol, _interval, _handler) {
1318
+ notImplemented2("subscribeBars");
1319
+ }
1320
+ subscribeTicks(_symbol, _handler) {
1321
+ notImplemented2("subscribeTicks");
1322
+ }
1323
+ async getHistoricalBars(_symbol, _interval, _count) {
1324
+ notImplemented2("getHistoricalBars");
1325
+ }
1326
+ };
1327
+
1328
+ // src/live/feed/brokerFeed.js
1329
+ var BrokerFeed = class extends FeedProvider {
1330
+ constructor({ broker }) {
1331
+ super();
1332
+ this.broker = broker;
1333
+ }
1334
+ async connect() {
1335
+ return void 0;
1336
+ }
1337
+ async disconnect() {
1338
+ return void 0;
1339
+ }
1340
+ subscribeBars(symbol, interval, handler) {
1341
+ return this.broker.subscribeBars(symbol, interval, handler);
1342
+ }
1343
+ subscribeTicks(symbol, handler) {
1344
+ return this.broker.subscribeTrades(symbol, handler);
1345
+ }
1346
+ async getHistoricalBars(symbol, interval, count) {
1347
+ return this.broker.getHistoricalBars(symbol, interval, count);
1348
+ }
1349
+ };
1350
+ function createBrokerFeed(options) {
1351
+ return new BrokerFeed(options);
1352
+ }
1353
+
1354
+ // src/live/feed/pollingFeed.js
1355
+ function keyFor(symbol, interval) {
1356
+ return `${symbol}::${interval}`;
1357
+ }
1358
+ var PollingFeed = class extends FeedProvider {
1359
+ constructor({ broker, pollIntervalMs = 6e4, defaultBarsPerPoll = 2 } = {}) {
1360
+ super();
1361
+ this.broker = broker;
1362
+ this.pollIntervalMs = Math.max(500, Number(pollIntervalMs) || 6e4);
1363
+ this.defaultBarsPerPoll = Math.max(1, Number(defaultBarsPerPoll) || 2);
1364
+ this.barSubscriptions = /* @__PURE__ */ new Map();
1365
+ this.tickSubscriptions = /* @__PURE__ */ new Map();
1366
+ this.lastEmittedByStream = /* @__PURE__ */ new Map();
1367
+ this.timer = null;
1368
+ this.connected = false;
1369
+ }
1370
+ async connect() {
1371
+ this.connected = true;
1372
+ }
1373
+ async disconnect() {
1374
+ this.connected = false;
1375
+ if (this.timer) {
1376
+ clearInterval(this.timer);
1377
+ this.timer = null;
1378
+ }
1379
+ }
1380
+ subscribeBars(symbol, interval, handler) {
1381
+ const streamKey = keyFor(symbol, interval);
1382
+ const list = this.barSubscriptions.get(streamKey) || [];
1383
+ list.push(handler);
1384
+ this.barSubscriptions.set(streamKey, list);
1385
+ return {
1386
+ unsubscribe: () => {
1387
+ const current = this.barSubscriptions.get(streamKey) || [];
1388
+ this.barSubscriptions.set(
1389
+ streamKey,
1390
+ current.filter((candidate) => candidate !== handler)
1391
+ );
1392
+ }
1393
+ };
1394
+ }
1395
+ subscribeTicks(symbol, handler) {
1396
+ const list = this.tickSubscriptions.get(symbol) || [];
1397
+ list.push(handler);
1398
+ this.tickSubscriptions.set(symbol, list);
1399
+ return {
1400
+ unsubscribe: () => {
1401
+ const current = this.tickSubscriptions.get(symbol) || [];
1402
+ this.tickSubscriptions.set(
1403
+ symbol,
1404
+ current.filter((candidate) => candidate !== handler)
1405
+ );
1406
+ }
1407
+ };
1408
+ }
1409
+ async getHistoricalBars(symbol, interval, count) {
1410
+ return this.broker.getHistoricalBars(symbol, interval, count);
1411
+ }
1412
+ async pollOnce() {
1413
+ const streams = [...this.barSubscriptions.keys()];
1414
+ for (const stream of streams) {
1415
+ const [symbol, interval] = stream.split("::");
1416
+ const bars = await this.broker.getHistoricalBars(symbol, interval, this.defaultBarsPerPoll);
1417
+ const ordered = [...bars].sort((left, right) => left.time - right.time);
1418
+ const lastSeen = this.lastEmittedByStream.get(stream) ?? -Infinity;
1419
+ const next = ordered.filter((bar) => bar.time > lastSeen);
1420
+ if (!next.length) continue;
1421
+ const handlers = this.barSubscriptions.get(stream) || [];
1422
+ for (const bar of next) {
1423
+ for (const handler of handlers) {
1424
+ await handler(bar);
1425
+ }
1426
+ }
1427
+ this.lastEmittedByStream.set(stream, next[next.length - 1].time);
1428
+ }
1429
+ }
1430
+ startPolling() {
1431
+ if (this.timer) return;
1432
+ this.timer = setInterval(() => {
1433
+ this.pollOnce().catch(() => {
1434
+ });
1435
+ }, this.pollIntervalMs);
1436
+ }
1437
+ stopPolling() {
1438
+ if (!this.timer) return;
1439
+ clearInterval(this.timer);
1440
+ this.timer = null;
1441
+ }
1442
+ };
1443
+ function createPollingFeed(options) {
1444
+ return new PollingFeed(options);
1445
+ }
1446
+
1447
+ // src/live/storage/interface.js
1448
+ function notImplemented3(method) {
1449
+ throw new Error(`StorageProvider.${method}() not implemented`);
1450
+ }
1451
+ var StorageProvider = class {
1452
+ async load(_namespace) {
1453
+ notImplemented3("load");
1454
+ }
1455
+ async save(_namespace, _state) {
1456
+ notImplemented3("save");
1457
+ }
1458
+ async appendTrade(_namespace, _trade) {
1459
+ notImplemented3("appendTrade");
1460
+ }
1461
+ async appendEquityPoint(_namespace, _point) {
1462
+ notImplemented3("appendEquityPoint");
1463
+ }
1464
+ async loadTrades(_namespace) {
1465
+ notImplemented3("loadTrades");
1466
+ }
1467
+ async loadEquityCurve(_namespace) {
1468
+ notImplemented3("loadEquityCurve");
1469
+ }
1470
+ async clear(_namespace) {
1471
+ notImplemented3("clear");
1472
+ }
1473
+ };
1474
+
1475
+ // src/live/storage/jsonFileStorage.js
1476
+ var import_node_fs = __toESM(require("node:fs"), 1);
1477
+ var import_promises = __toESM(require("node:fs/promises"), 1);
1478
+ var import_node_path = __toESM(require("node:path"), 1);
1479
+ function sanitizeNamespace(namespace) {
1480
+ return String(namespace || "default").replace(/[^a-zA-Z0-9._-]/g, "_");
1481
+ }
1482
+ async function ensureDir(dirPath) {
1483
+ await import_promises.default.mkdir(dirPath, { recursive: true });
1484
+ }
1485
+ async function readJsonFile(filePath) {
1486
+ try {
1487
+ const raw = await import_promises.default.readFile(filePath, "utf8");
1488
+ return JSON.parse(raw);
1489
+ } catch (error) {
1490
+ if (error && error.code === "ENOENT") return null;
1491
+ throw error;
1492
+ }
1493
+ }
1494
+ async function writeJsonAtomic(filePath, payload) {
1495
+ const tmpPath = `${filePath}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`;
1496
+ await import_promises.default.writeFile(tmpPath, JSON.stringify(payload, null, 2), "utf8");
1497
+ await import_promises.default.rename(tmpPath, filePath);
1498
+ }
1499
+ async function appendJsonLine(filePath, payload) {
1500
+ await ensureDir(import_node_path.default.dirname(filePath));
1501
+ await import_promises.default.appendFile(filePath, `${JSON.stringify(payload)}
1502
+ `, "utf8");
1503
+ }
1504
+ async function readJsonLines(filePath) {
1505
+ try {
1506
+ const raw = await import_promises.default.readFile(filePath, "utf8");
1507
+ return raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
1508
+ } catch (error) {
1509
+ if (error && error.code === "ENOENT") return [];
1510
+ throw error;
1511
+ }
1512
+ }
1513
+ var JsonFileStorage = class extends StorageProvider {
1514
+ constructor({ baseDir = import_node_path.default.resolve(process.cwd(), "output/live-state") } = {}) {
1515
+ super();
1516
+ this.baseDir = baseDir;
1517
+ }
1518
+ namespaceDir(namespace) {
1519
+ return import_node_path.default.join(this.baseDir, sanitizeNamespace(namespace));
1520
+ }
1521
+ statePath(namespace) {
1522
+ return import_node_path.default.join(this.namespaceDir(namespace), "state.json");
1523
+ }
1524
+ tradesPath(namespace) {
1525
+ return import_node_path.default.join(this.namespaceDir(namespace), "trades.jsonl");
1526
+ }
1527
+ equityPath(namespace) {
1528
+ return import_node_path.default.join(this.namespaceDir(namespace), "equity.jsonl");
1529
+ }
1530
+ async load(namespace) {
1531
+ return readJsonFile(this.statePath(namespace));
1532
+ }
1533
+ async save(namespace, state) {
1534
+ const dir = this.namespaceDir(namespace);
1535
+ await ensureDir(dir);
1536
+ await writeJsonAtomic(this.statePath(namespace), state);
1537
+ }
1538
+ async appendTrade(namespace, trade) {
1539
+ await appendJsonLine(this.tradesPath(namespace), trade);
1540
+ }
1541
+ async appendEquityPoint(namespace, point) {
1542
+ await appendJsonLine(this.equityPath(namespace), point);
1543
+ }
1544
+ async loadTrades(namespace) {
1545
+ return readJsonLines(this.tradesPath(namespace));
1546
+ }
1547
+ async loadEquityCurve(namespace) {
1548
+ return readJsonLines(this.equityPath(namespace));
1549
+ }
1550
+ async clear(namespace) {
1551
+ const dir = this.namespaceDir(namespace);
1552
+ if (!import_node_fs.default.existsSync(dir)) return;
1553
+ await import_promises.default.rm(dir, { recursive: true, force: true });
1554
+ }
1555
+ };
1556
+ function createJsonFileStorage(options) {
1557
+ return new JsonFileStorage(options);
1558
+ }
1559
+
1560
+ // src/live/engine/candleAggregator.js
1561
+ var import_node_events3 = require("node:events");
1562
+
1563
+ // src/utils/time.js
1564
+ function usDstBoundsUTC(year) {
1565
+ let marchCursor = new Date(Date.UTC(year, 2, 1, 7, 0, 0));
1566
+ let sundaysSeen = 0;
1567
+ while (marchCursor.getUTCMonth() === 2) {
1568
+ if (marchCursor.getUTCDay() === 0) sundaysSeen += 1;
1569
+ if (sundaysSeen === 2) break;
1570
+ marchCursor = new Date(marchCursor.getTime() + 24 * 60 * 60 * 1e3);
1571
+ }
1572
+ const dstStart = new Date(Date.UTC(year, 2, marchCursor.getUTCDate(), 7, 0, 0));
1573
+ let novemberCursor = new Date(Date.UTC(year, 10, 1, 6, 0, 0));
1574
+ while (novemberCursor.getUTCDay() !== 0) {
1575
+ novemberCursor = new Date(novemberCursor.getTime() + 24 * 60 * 60 * 1e3);
1576
+ }
1577
+ const dstEnd = new Date(Date.UTC(year, 10, novemberCursor.getUTCDate(), 6, 0, 0));
1578
+ return { dstStart, dstEnd };
1579
+ }
1580
+ function isUsEasternDST(timeMs) {
1581
+ const date = new Date(timeMs);
1582
+ const { dstStart, dstEnd } = usDstBoundsUTC(date.getUTCFullYear());
1583
+ return date >= dstStart && date < dstEnd;
1584
+ }
1585
+ function offsetET(timeMs) {
1586
+ return isUsEasternDST(timeMs) ? 4 : 5;
1587
+ }
1588
+ function minutesET(timeMs) {
1589
+ const date = new Date(timeMs);
1590
+ const offset = offsetET(timeMs);
1591
+ return (date.getUTCHours() - offset + 24) % 24 * 60 + date.getUTCMinutes();
1592
+ }
1593
+ function isSession(timeMs, session = "NYSE") {
1594
+ const day = new Date(timeMs).getUTCDay();
1595
+ if (day === 0 || day === 6) {
1596
+ if (session === "FUT") {
1597
+ const minutes2 = minutesET(timeMs);
1598
+ return minutes2 >= 18 * 60 || minutes2 < 17 * 60;
1599
+ }
1600
+ return false;
1601
+ }
1602
+ const minutes = minutesET(timeMs);
1603
+ if (session === "AUTO") return true;
1604
+ if (session === "FUT") {
1605
+ const maintenanceStart = 17 * 60;
1606
+ const maintenanceEnd = 18 * 60;
1607
+ return !(minutes >= maintenanceStart && minutes < maintenanceEnd);
1608
+ }
1609
+ const open = 9 * 60 + 30;
1610
+ const close = 16 * 60;
1611
+ return minutes >= open && minutes <= close;
1612
+ }
1613
+ function parseWindowsCSV(csv) {
1614
+ if (!csv) return null;
1615
+ return csv.split(",").map((token) => token.trim()).filter(Boolean).map((windowText) => {
1616
+ const [start, end] = windowText.split("-").map((value) => value.trim());
1617
+ const [startHour, startMinute] = start.split(":").map(Number);
1618
+ const [endHour, endMinute] = end.split(":").map(Number);
1619
+ return {
1620
+ aMin: startHour * 60 + startMinute,
1621
+ bMin: endHour * 60 + endMinute
1622
+ };
1623
+ });
1624
+ }
1625
+ function inWindowsET(timeMs, windows) {
1626
+ if (!windows?.length) return true;
1627
+ const minutes = minutesET(timeMs);
1628
+ return windows.some((window) => minutes >= window.aMin && minutes <= window.bMin);
1629
+ }
1630
+
1631
+ // src/engine/execution.js
1632
+ function resolveSlippageBps(kind, slippageBps, slippageByKind) {
1633
+ if (Number.isFinite(slippageByKind?.[kind])) {
1634
+ return slippageByKind[kind];
1635
+ }
1636
+ let effectiveSlippageBps = slippageBps;
1637
+ if (kind === "limit") effectiveSlippageBps *= 0.25;
1638
+ if (kind === "stop") effectiveSlippageBps *= 1.25;
1639
+ return effectiveSlippageBps;
1640
+ }
1641
+ function applyFill(price, side, { slippageBps = 0, feeBps = 0, kind = "market", qty = 0, costs = {} } = {}) {
1642
+ const model = costs || {};
1643
+ const modelSlippageBps = Number.isFinite(model.slippageBps) ? model.slippageBps : slippageBps;
1644
+ const modelFeeBps = Number.isFinite(model.commissionBps) ? model.commissionBps : feeBps;
1645
+ const effectiveSlippageBps = resolveSlippageBps(kind, modelSlippageBps, model.slippageByKind);
1646
+ const halfSpreadBps = Number.isFinite(model.spreadBps) ? model.spreadBps / 2 : 0;
1647
+ const slippage = (effectiveSlippageBps + halfSpreadBps) / 1e4 * price;
1648
+ const filledPrice = side === "long" ? price + slippage : price - slippage;
1649
+ const variableFeePerUnit = (modelFeeBps || 0) / 1e4 * Math.abs(filledPrice) + (Number.isFinite(model.commissionPerUnit) ? model.commissionPerUnit : 0);
1650
+ const variableFeeTotal = variableFeePerUnit * Math.max(0, qty);
1651
+ const fixedFeeTotal = Number.isFinite(model.commissionPerOrder) ? model.commissionPerOrder : 0;
1652
+ const grossFeeTotal = variableFeeTotal + fixedFeeTotal;
1653
+ const feeTotal = Math.max(
1654
+ Number.isFinite(model.minCommission) ? model.minCommission : 0,
1655
+ grossFeeTotal
1656
+ );
1657
+ const feePerUnit = qty > 0 ? feeTotal / qty : variableFeePerUnit;
1658
+ return { price: filledPrice, fee: feePerUnit, feeTotal };
1659
+ }
1660
+ function touchedLimit(side, limitPrice, bar, mode = "intrabar") {
1661
+ if (!bar || limitPrice === void 0 || limitPrice === null) return false;
1662
+ if (mode === "close") {
1663
+ return side === "long" ? bar.close <= limitPrice : bar.close >= limitPrice;
1664
+ }
1665
+ return side === "long" ? bar.low <= limitPrice : bar.high >= limitPrice;
1666
+ }
1667
+ function ocoExitCheck({ side, stop, tp, bar, mode = "intrabar", tieBreak = "pessimistic" }) {
1668
+ if (mode === "close") {
1669
+ const close = bar.close;
1670
+ if (side === "long") {
1671
+ if (close <= stop) return { hit: "SL", px: stop };
1672
+ if (close >= tp) return { hit: "TP", px: tp };
1673
+ } else {
1674
+ if (close >= stop) return { hit: "SL", px: stop };
1675
+ if (close <= tp) return { hit: "TP", px: tp };
1676
+ }
1677
+ return { hit: null, px: null };
1678
+ }
1679
+ const hitStop = side === "long" ? bar.low <= stop : bar.high >= stop;
1680
+ const hitTarget = side === "long" ? bar.high >= tp : bar.low <= tp;
1681
+ if (hitStop && hitTarget) {
1682
+ return tieBreak === "optimistic" ? { hit: "TP", px: tp } : { hit: "SL", px: stop };
1683
+ }
1684
+ if (hitStop) return { hit: "SL", px: stop };
1685
+ if (hitTarget) return { hit: "TP", px: tp };
1686
+ return { hit: null, px: null };
1687
+ }
1688
+ function isEODBar(timeMs) {
1689
+ return minutesET(timeMs) >= 16 * 60;
1690
+ }
1691
+ function roundStep(value, step = 1e-3) {
1692
+ return Math.floor(value / step) * step;
1693
+ }
1694
+ function estimateBarMs(candles) {
1695
+ if (candles.length >= 2) {
1696
+ const deltas = [];
1697
+ for (let index = 1; index < Math.min(candles.length, 500); index += 1) {
1698
+ const delta = candles[index].time - candles[index - 1].time;
1699
+ if (Number.isFinite(delta) && delta > 0) deltas.push(delta);
1700
+ }
1701
+ if (deltas.length) {
1702
+ deltas.sort((a, b) => a - b);
1703
+ const middle = Math.floor(deltas.length / 2);
1704
+ const median = deltas.length % 2 ? deltas[middle] : (deltas[middle - 1] + deltas[middle]) / 2;
1705
+ return Math.max(6e4, Math.min(median, 60 * 6e4));
1706
+ }
1707
+ }
1708
+ return 5 * 60 * 1e3;
1709
+ }
1710
+ function dayKeyUTC(timeMs) {
1711
+ const date = new Date(timeMs);
1712
+ return [
1713
+ date.getUTCFullYear(),
1714
+ String(date.getUTCMonth() + 1).padStart(2, "0"),
1715
+ String(date.getUTCDate()).padStart(2, "0")
1716
+ ].join("-");
1717
+ }
1718
+ function dayKeyET(timeMs) {
1719
+ const date = new Date(timeMs);
1720
+ const minutes = minutesET(timeMs);
1721
+ const hoursET = Math.floor(minutes / 60);
1722
+ const minutesETDay = minutes % 60;
1723
+ const anchor = new Date(
1724
+ Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0)
1725
+ );
1726
+ const pseudoEtTime = anchor.getTime() + hoursET * 60 * 60 * 1e3 + minutesETDay * 60 * 1e3;
1727
+ return dayKeyUTC(pseudoEtTime);
1728
+ }
1729
+
1730
+ // src/live/engine/candleAggregator.js
1731
+ function intervalToMs(interval) {
1732
+ const raw = String(interval || "1m").trim().toLowerCase();
1733
+ const match = raw.match(/^(\d+)(m|h|d)$/);
1734
+ if (!match) return 6e4;
1735
+ const amount = Number(match[1]);
1736
+ const unit = match[2];
1737
+ if (unit === "m") return amount * 6e4;
1738
+ if (unit === "h") return amount * 60 * 6e4;
1739
+ return amount * 24 * 60 * 6e4;
1740
+ }
1741
+ function normalizeTick(tick) {
1742
+ const time = Number(tick?.time);
1743
+ const price = Number(tick?.price ?? tick?.last ?? tick?.close ?? tick?.bid ?? tick?.ask);
1744
+ const volume = Number(tick?.size ?? tick?.volume ?? 0);
1745
+ if (!Number.isFinite(time) || !Number.isFinite(price)) return null;
1746
+ return {
1747
+ time,
1748
+ price,
1749
+ volume: Number.isFinite(volume) ? volume : 0
1750
+ };
1751
+ }
1752
+ function bucketStart(time, bucketMs) {
1753
+ return Math.floor(time / bucketMs) * bucketMs;
1754
+ }
1755
+ var CandleAggregator = class extends import_node_events3.EventEmitter {
1756
+ constructor({ mode = "stream", interval = "1m", graceMs = 5e3, session = "AUTO" } = {}) {
1757
+ super();
1758
+ this.mode = mode;
1759
+ this.interval = interval;
1760
+ this.graceMs = Math.max(0, Number(graceMs) || 5e3);
1761
+ this.session = session;
1762
+ this.intervalMs = intervalToMs(interval);
1763
+ this.current = null;
1764
+ this.lastEmittedTime = -Infinity;
1765
+ }
1766
+ onBar(handler) {
1767
+ this.on("bar", handler);
1768
+ return () => this.off("bar", handler);
1769
+ }
1770
+ emitBar(bar) {
1771
+ if (!bar || !Number.isFinite(bar.time)) return;
1772
+ if (bar.time <= this.lastEmittedTime) return;
1773
+ this.lastEmittedTime = bar.time;
1774
+ this.emit("bar", bar);
1775
+ }
1776
+ processBar(bar, { isFinal = true } = {}) {
1777
+ if (!bar || !Number.isFinite(bar.time)) return;
1778
+ if (this.mode === "stream") {
1779
+ if (isFinal) this.emitBar(bar);
1780
+ return;
1781
+ }
1782
+ this.emitBar(bar);
1783
+ }
1784
+ processPolledBars(bars = []) {
1785
+ const ordered = [...bars].sort((left, right) => left.time - right.time);
1786
+ for (const bar of ordered) {
1787
+ this.emitBar(bar);
1788
+ }
1789
+ }
1790
+ processTick(rawTick) {
1791
+ const tick = normalizeTick(rawTick);
1792
+ if (!tick) return;
1793
+ const start = bucketStart(tick.time, this.intervalMs);
1794
+ if (!this.current) {
1795
+ this.current = {
1796
+ time: start,
1797
+ open: tick.price,
1798
+ high: tick.price,
1799
+ low: tick.price,
1800
+ close: tick.price,
1801
+ volume: tick.volume,
1802
+ _lastTickTime: tick.time
1803
+ };
1804
+ return;
1805
+ }
1806
+ if (start === this.current.time) {
1807
+ this.current.high = Math.max(this.current.high, tick.price);
1808
+ this.current.low = Math.min(this.current.low, tick.price);
1809
+ this.current.close = tick.price;
1810
+ this.current.volume += tick.volume;
1811
+ this.current._lastTickTime = tick.time;
1812
+ return;
1813
+ }
1814
+ if (start > this.current.time) {
1815
+ this.emitBar({
1816
+ time: this.current.time,
1817
+ open: this.current.open,
1818
+ high: this.current.high,
1819
+ low: this.current.low,
1820
+ close: this.current.close,
1821
+ volume: this.current.volume
1822
+ });
1823
+ this.current = {
1824
+ time: start,
1825
+ open: tick.price,
1826
+ high: tick.price,
1827
+ low: tick.price,
1828
+ close: tick.price,
1829
+ volume: tick.volume,
1830
+ _lastTickTime: tick.time
1831
+ };
1832
+ }
1833
+ }
1834
+ forceClose(timeMs = Date.now()) {
1835
+ if (!this.current) return;
1836
+ const closeDeadline = this.current.time + this.intervalMs + this.graceMs;
1837
+ const sessionOpen = isSession(this.current.time + this.intervalMs, this.session);
1838
+ if (timeMs >= closeDeadline || !sessionOpen) {
1839
+ this.emitBar({
1840
+ time: this.current.time,
1841
+ open: this.current.open,
1842
+ high: this.current.high,
1843
+ low: this.current.low,
1844
+ close: this.current.close,
1845
+ volume: this.current.volume
1846
+ });
1847
+ this.current = null;
1848
+ }
1849
+ }
1850
+ estimateFromSeries(candles) {
1851
+ const estimated = estimateBarMs(candles);
1852
+ if (Number.isFinite(estimated) && estimated > 0) {
1853
+ this.intervalMs = estimated;
1854
+ }
1855
+ return this.intervalMs;
1856
+ }
1857
+ };
1858
+ function createCandleAggregator(options) {
1859
+ return new CandleAggregator(options);
1860
+ }
1861
+
1862
+ // src/live/engine/riskManager.js
1863
+ function pctToFraction(value, fallback = 0) {
1864
+ if (!Number.isFinite(value)) return fallback;
1865
+ return Math.abs(value) / 100;
1866
+ }
1867
+ var RiskManager = class {
1868
+ constructor(options = {}) {
1869
+ this.options = {
1870
+ maxDailyLossPct: 2,
1871
+ maxDailyLossDollars: null,
1872
+ maxDrawdownPct: 20,
1873
+ maxPositions: 10,
1874
+ maxPositionPct: 50,
1875
+ maxDailyTrades: 0,
1876
+ cooldownAfterLossMs: 0,
1877
+ allowedSessions: "AUTO",
1878
+ allowedWindows: null,
1879
+ ...options
1880
+ };
1881
+ this.allowedWindows = parseWindowsCSV(this.options.allowedWindows);
1882
+ this.startEquity = null;
1883
+ this.currentEquity = null;
1884
+ this.peakEquity = null;
1885
+ this.currentDayKey = null;
1886
+ this.dayPnl = 0;
1887
+ this.dayTrades = 0;
1888
+ this.lastLossAt = null;
1889
+ this.halted = false;
1890
+ this.haltReason = null;
1891
+ }
1892
+ initialize(equity, timeMs = Date.now()) {
1893
+ const value = Number.isFinite(equity) ? equity : 0;
1894
+ this.startEquity = value;
1895
+ this.currentEquity = value;
1896
+ this.peakEquity = value;
1897
+ this.currentDayKey = dayKeyET(timeMs);
1898
+ this.dayPnl = 0;
1899
+ this.dayTrades = 0;
1900
+ this.lastLossAt = null;
1901
+ this.halted = false;
1902
+ this.haltReason = null;
1903
+ }
1904
+ update({ timeMs, equity }) {
1905
+ if (this.startEquity === null) this.initialize(equity, timeMs);
1906
+ const nextDay = dayKeyET(timeMs);
1907
+ if (this.currentDayKey !== nextDay) {
1908
+ this.currentDayKey = nextDay;
1909
+ this.dayPnl = 0;
1910
+ this.dayTrades = 0;
1911
+ this.halted = false;
1912
+ this.haltReason = null;
1913
+ }
1914
+ this.currentEquity = Number.isFinite(equity) ? equity : this.currentEquity;
1915
+ if (this.currentEquity > this.peakEquity) this.peakEquity = this.currentEquity;
1916
+ this._maybeHaltForDrawdown();
1917
+ this._maybeHaltForDailyLoss();
1918
+ }
1919
+ _maybeHaltForDrawdown() {
1920
+ if (this.halted || !Number.isFinite(this.currentEquity) || !(this.peakEquity > 0)) return;
1921
+ const drawdown = (this.peakEquity - this.currentEquity) / this.peakEquity;
1922
+ const maxDrawdown = pctToFraction(this.options.maxDrawdownPct, 0.2);
1923
+ if (maxDrawdown > 0 && drawdown >= maxDrawdown) {
1924
+ this.halt(`max drawdown reached (${(drawdown * 100).toFixed(2)}%)`);
1925
+ }
1926
+ }
1927
+ _maybeHaltForDailyLoss() {
1928
+ if (this.halted) return;
1929
+ const maxLossPct = pctToFraction(this.options.maxDailyLossPct, 0.02);
1930
+ const maxLossDollars = Number.isFinite(this.options.maxDailyLossDollars) ? Math.abs(this.options.maxDailyLossDollars) : null;
1931
+ const lossesExceededPct = maxLossPct > 0 && this.dayPnl <= -Math.abs(this.startEquity * maxLossPct);
1932
+ const lossesExceededAbs = Number.isFinite(maxLossDollars) && this.dayPnl <= -Math.abs(maxLossDollars);
1933
+ if (lossesExceededPct || lossesExceededAbs) {
1934
+ this.halt("daily loss limit reached");
1935
+ }
1936
+ }
1937
+ isSessionAllowed(timeMs) {
1938
+ const sessionName = this.options.allowedSessions || "AUTO";
1939
+ if (!isSession(timeMs, sessionName)) return false;
1940
+ return inWindowsET(timeMs, this.allowedWindows);
1941
+ }
1942
+ canTrade({ timeMs = Date.now() } = {}) {
1943
+ if (this.halted) return { ok: false, reason: this.haltReason || "risk halt active" };
1944
+ if (!this.isSessionAllowed(timeMs))
1945
+ return { ok: false, reason: "outside allowed session/window" };
1946
+ if (Number.isFinite(this.options.cooldownAfterLossMs) && this.options.cooldownAfterLossMs > 0 && Number.isFinite(this.lastLossAt) && timeMs - this.lastLossAt < this.options.cooldownAfterLossMs) {
1947
+ return { ok: false, reason: "cooldown after loss active" };
1948
+ }
1949
+ return { ok: true, reason: null };
1950
+ }
1951
+ canOpenPosition({
1952
+ timeMs = Date.now(),
1953
+ positionCount = 0,
1954
+ positionValue = 0,
1955
+ equity = null
1956
+ } = {}) {
1957
+ const base = this.canTrade({ timeMs });
1958
+ if (!base.ok) return base;
1959
+ if (this.options.maxPositions > 0 && positionCount >= this.options.maxPositions) {
1960
+ return { ok: false, reason: "max positions reached" };
1961
+ }
1962
+ if (this.options.maxDailyTrades > 0 && this.dayTrades >= this.options.maxDailyTrades) {
1963
+ return { ok: false, reason: "max daily trades reached" };
1964
+ }
1965
+ const eq = Number.isFinite(equity) ? equity : this.currentEquity;
1966
+ const maxPositionFraction = pctToFraction(this.options.maxPositionPct, 0.5);
1967
+ if (maxPositionFraction > 0 && Number.isFinite(eq) && eq > 0) {
1968
+ const fraction = Math.abs(positionValue) / eq;
1969
+ if (fraction > maxPositionFraction) {
1970
+ return { ok: false, reason: "max position size exceeded" };
1971
+ }
1972
+ }
1973
+ return { ok: true, reason: null };
1974
+ }
1975
+ recordTrade({ pnl = 0, timeMs = Date.now(), equity = null } = {}) {
1976
+ if (this.currentDayKey !== dayKeyET(timeMs)) {
1977
+ this.currentDayKey = dayKeyET(timeMs);
1978
+ this.dayPnl = 0;
1979
+ this.dayTrades = 0;
1980
+ this.halted = false;
1981
+ this.haltReason = null;
1982
+ }
1983
+ const realized = Number.isFinite(pnl) ? pnl : 0;
1984
+ this.dayPnl += realized;
1985
+ this.dayTrades += 1;
1986
+ if (realized < 0) this.lastLossAt = timeMs;
1987
+ if (Number.isFinite(equity)) this.currentEquity = equity;
1988
+ this._maybeHaltForDailyLoss();
1989
+ this._maybeHaltForDrawdown();
1990
+ }
1991
+ halt(reason = "manual halt") {
1992
+ this.halted = true;
1993
+ this.haltReason = reason;
1994
+ }
1995
+ clearHalt() {
1996
+ this.halted = false;
1997
+ this.haltReason = null;
1998
+ }
1999
+ getState() {
2000
+ return {
2001
+ startEquity: this.startEquity,
2002
+ currentEquity: this.currentEquity,
2003
+ peakEquity: this.peakEquity,
2004
+ dayPnl: this.dayPnl,
2005
+ dayTrades: this.dayTrades,
2006
+ currentDayKey: this.currentDayKey,
2007
+ halted: this.halted,
2008
+ haltReason: this.haltReason,
2009
+ lastLossAt: this.lastLossAt
2010
+ };
2011
+ }
2012
+ };
2013
+ function createRiskManager(options) {
2014
+ return new RiskManager(options);
2015
+ }
2016
+
2017
+ // src/live/engine/stateManager.js
2018
+ function qtyCloseEnough(a, b, tolerancePct = 0.05) {
2019
+ const left = Math.abs(Number(a) || 0);
2020
+ const right = Math.abs(Number(b) || 0);
2021
+ if (left === 0 && right === 0) return true;
2022
+ const baseline = Math.max(left, right, 1e-12);
2023
+ return Math.abs(left - right) / baseline <= tolerancePct;
2024
+ }
2025
+ function sideMatches(openPosition, brokerPosition) {
2026
+ if (!openPosition || !brokerPosition) return false;
2027
+ const openSide = openPosition.side;
2028
+ const brokerSide = brokerPosition.side;
2029
+ return openSide === brokerSide;
2030
+ }
2031
+ var StateManager = class {
2032
+ constructor({ storage }) {
2033
+ this.storage = storage;
2034
+ }
2035
+ async load(namespace) {
2036
+ return this.storage.load(namespace);
2037
+ }
2038
+ async save(namespace, state) {
2039
+ await this.storage.save(namespace, {
2040
+ ...state,
2041
+ savedAt: Date.now()
2042
+ });
2043
+ }
2044
+ async appendTrade(namespace, trade) {
2045
+ await this.storage.appendTrade(namespace, trade);
2046
+ }
2047
+ async appendEquityPoint(namespace, point) {
2048
+ await this.storage.appendEquityPoint(namespace, point);
2049
+ }
2050
+ async loadTrades(namespace) {
2051
+ return this.storage.loadTrades(namespace);
2052
+ }
2053
+ async loadEquityCurve(namespace) {
2054
+ return this.storage.loadEquityCurve(namespace);
2055
+ }
2056
+ async clear(namespace) {
2057
+ await this.storage.clear(namespace);
2058
+ }
2059
+ reconcile({ persistedState, brokerPositions = [], symbol }) {
2060
+ const report = {
2061
+ status: "ok",
2062
+ action: "none",
2063
+ message: "no reconciliation needed",
2064
+ adoptedPosition: null,
2065
+ mismatch: null
2066
+ };
2067
+ const persistedOpen = persistedState?.openPosition || null;
2068
+ const brokerForSymbol = brokerPositions.find((position) => position.symbol === symbol) || null;
2069
+ if (persistedOpen && brokerForSymbol) {
2070
+ const sameSide = sideMatches(persistedOpen, brokerForSymbol);
2071
+ const similarQty = qtyCloseEnough(
2072
+ persistedOpen.size ?? persistedOpen.qty,
2073
+ brokerForSymbol.qty
2074
+ );
2075
+ if (sameSide && similarQty) {
2076
+ report.action = "adopt-broker";
2077
+ report.message = "persisted and broker positions matched";
2078
+ report.adoptedPosition = {
2079
+ ...persistedOpen,
2080
+ size: brokerForSymbol.qty,
2081
+ entryFill: brokerForSymbol.avgEntry ?? persistedOpen.entryFill ?? persistedOpen.entry
2082
+ };
2083
+ return report;
2084
+ }
2085
+ report.status = "error";
2086
+ report.action = "mismatch";
2087
+ report.message = "persisted and broker positions mismatch";
2088
+ report.mismatch = { persisted: persistedOpen, broker: brokerForSymbol };
2089
+ return report;
2090
+ }
2091
+ if (persistedOpen && !brokerForSymbol) {
2092
+ report.status = "warn";
2093
+ report.action = "closed-externally";
2094
+ report.message = "persisted open position missing at broker";
2095
+ return report;
2096
+ }
2097
+ if (!persistedOpen && brokerForSymbol) {
2098
+ report.status = "warn";
2099
+ report.action = "external-position";
2100
+ report.message = "broker has external position not present in persisted state";
2101
+ report.adoptedPosition = null;
2102
+ return report;
2103
+ }
2104
+ return report;
2105
+ }
2106
+ };
2107
+ function createStateManager(options) {
2108
+ return new StateManager(options);
2109
+ }
2110
+
2111
+ // src/live/engine/paperEngine.js
2112
+ function asNumber(value, fallback = null) {
2113
+ const numeric = Number(value);
2114
+ return Number.isFinite(numeric) ? numeric : fallback;
2115
+ }
2116
+ function normalizeOrderSide(side) {
2117
+ const normalized = String(side || "").toLowerCase();
2118
+ if (normalized === "buy") return "buy";
2119
+ if (normalized === "sell") return "sell";
2120
+ throw new Error(`Unsupported paper order side "${side}"`);
2121
+ }
2122
+ function normalizeOrderType(type) {
2123
+ const normalized = String(type || "market").toLowerCase();
2124
+ if (normalized === "market") return "market";
2125
+ if (normalized === "limit") return "limit";
2126
+ if (normalized === "stop") return "stop";
2127
+ if (normalized === "stop_limit") return "stop_limit";
2128
+ throw new Error(`Unsupported paper order type "${type}"`);
2129
+ }
2130
+ function cloneOrder(order) {
2131
+ return {
2132
+ orderId: order.orderId,
2133
+ clientOrderId: order.clientOrderId,
2134
+ status: order.status,
2135
+ filledQty: order.filledQty,
2136
+ avgFillPrice: order.avgFillPrice,
2137
+ filledAt: order.filledAt,
2138
+ symbol: order.symbol,
2139
+ side: order.side,
2140
+ type: order.type,
2141
+ qty: order.qty,
2142
+ limitPrice: order.limitPrice,
2143
+ stopPrice: order.stopPrice,
2144
+ timeInForce: order.timeInForce,
2145
+ rejectReason: order.rejectReason
2146
+ };
2147
+ }
2148
+ function sideToDirection(side) {
2149
+ return side === "buy" ? 1 : -1;
2150
+ }
2151
+ var PaperEngine = class extends BrokerAdapter {
2152
+ constructor({
2153
+ equity = 1e4,
2154
+ currency = "USD",
2155
+ slippageBps = 0,
2156
+ feeBps = 0,
2157
+ costs = null,
2158
+ qtyStep = 1e-3
2159
+ } = {}) {
2160
+ super();
2161
+ this.connected = false;
2162
+ this.config = {};
2163
+ this.currency = currency;
2164
+ this.startingEquity = Math.max(0, Number(equity) || 0);
2165
+ this.cash = this.startingEquity;
2166
+ this.slippageBps = slippageBps;
2167
+ this.feeBps = feeBps;
2168
+ this.costs = costs;
2169
+ this.qtyStep = qtyStep;
2170
+ this.positions = /* @__PURE__ */ new Map();
2171
+ this.openOrders = /* @__PURE__ */ new Map();
2172
+ this.orderHistory = /* @__PURE__ */ new Map();
2173
+ this.lastPrices = /* @__PURE__ */ new Map();
2174
+ this.barSubscribers = /* @__PURE__ */ new Map();
2175
+ this.tradeSubscribers = /* @__PURE__ */ new Map();
2176
+ this.quoteSubscribers = /* @__PURE__ */ new Map();
2177
+ this.historicalBars = /* @__PURE__ */ new Map();
2178
+ this.orderIdCounter = 1;
2179
+ }
2180
+ async connect(config = {}) {
2181
+ this.config = { ...config };
2182
+ this.connected = true;
2183
+ }
2184
+ async disconnect() {
2185
+ this.connected = false;
2186
+ this.barSubscribers.clear();
2187
+ this.tradeSubscribers.clear();
2188
+ this.quoteSubscribers.clear();
2189
+ }
2190
+ isConnected() {
2191
+ return this.connected;
2192
+ }
2193
+ supportsPaperNative() {
2194
+ return true;
2195
+ }
2196
+ async getServerTime() {
2197
+ return Date.now();
2198
+ }
2199
+ _positionMark(position) {
2200
+ const mark = this.lastPrices.get(position.symbol) ?? position.avgEntry;
2201
+ if (position.side === "long") {
2202
+ return {
2203
+ mark,
2204
+ marketValue: mark * position.qty,
2205
+ unrealizedPnl: (mark - position.avgEntry) * position.qty
2206
+ };
2207
+ }
2208
+ return {
2209
+ mark,
2210
+ marketValue: mark * position.qty,
2211
+ unrealizedPnl: (position.avgEntry - mark) * position.qty
2212
+ };
2213
+ }
2214
+ _realizedUnrealizedSummary() {
2215
+ let unrealized = 0;
2216
+ let marketValue = 0;
2217
+ for (const position of this.positions.values()) {
2218
+ const marked = this._positionMark(position);
2219
+ unrealized += marked.unrealizedPnl;
2220
+ marketValue += marked.marketValue;
2221
+ }
2222
+ return { unrealized, marketValue };
2223
+ }
2224
+ async getAccount() {
2225
+ const { unrealized, marketValue } = this._realizedUnrealizedSummary();
2226
+ const equity = this.cash + unrealized;
2227
+ return {
2228
+ equity,
2229
+ buyingPower: Math.max(0, equity),
2230
+ cash: this.cash,
2231
+ currency: this.currency,
2232
+ marginUsed: Math.max(0, marketValue - this.cash)
2233
+ };
2234
+ }
2235
+ async getPositions() {
2236
+ const rows = [];
2237
+ for (const position of this.positions.values()) {
2238
+ const marked = this._positionMark(position);
2239
+ rows.push({
2240
+ symbol: position.symbol,
2241
+ side: position.side,
2242
+ qty: position.qty,
2243
+ avgEntry: position.avgEntry,
2244
+ marketValue: marked.marketValue,
2245
+ unrealizedPnl: marked.unrealizedPnl
2246
+ });
2247
+ }
2248
+ return rows;
2249
+ }
2250
+ _streamKey(symbol, interval = "*") {
2251
+ return `${symbol}::${interval}`;
2252
+ }
2253
+ _subscribe(map, key, handler) {
2254
+ const list = map.get(key) || [];
2255
+ list.push(handler);
2256
+ map.set(key, list);
2257
+ return {
2258
+ unsubscribe: () => {
2259
+ const current = map.get(key) || [];
2260
+ map.set(
2261
+ key,
2262
+ current.filter((candidate) => candidate !== handler)
2263
+ );
2264
+ }
2265
+ };
2266
+ }
2267
+ async subscribeBars(symbol, interval, handler) {
2268
+ return this._subscribe(this.barSubscribers, this._streamKey(symbol, interval), handler);
2269
+ }
2270
+ async subscribeTrades(symbol, handler) {
2271
+ return this._subscribe(this.tradeSubscribers, symbol, handler);
2272
+ }
2273
+ async subscribeQuotes(symbol, handler) {
2274
+ return this._subscribe(this.quoteSubscribers, symbol, handler);
2275
+ }
2276
+ async _emitTo(map, key, payload) {
2277
+ const handlers = map.get(key) || [];
2278
+ for (const handler of handlers) {
2279
+ await Promise.resolve(handler(payload));
2280
+ }
2281
+ }
2282
+ setHistoricalBars(symbol, interval, bars) {
2283
+ const streamKey = this._streamKey(symbol, interval);
2284
+ this.historicalBars.set(streamKey, [...bars]);
2285
+ }
2286
+ async getHistoricalBars(symbol, interval, limit = 200) {
2287
+ const streamKey = this._streamKey(symbol, interval);
2288
+ const all = this.historicalBars.get(streamKey) || [];
2289
+ return all.slice(Math.max(0, all.length - limit));
2290
+ }
2291
+ _nextOrderId() {
2292
+ const id = `paper-${this.orderIdCounter}`;
2293
+ this.orderIdCounter += 1;
2294
+ return id;
2295
+ }
2296
+ _recordOrder(order) {
2297
+ this.orderHistory.set(order.orderId, { ...order });
2298
+ }
2299
+ _fillOrder(order, fillPrice, kind = "market", fillTime = Date.now()) {
2300
+ const side = normalizeOrderSide(order.side);
2301
+ const qty = Math.max(0, asNumber(order.qty, 0));
2302
+ if (!(qty > 0)) {
2303
+ order.status = "rejected";
2304
+ order.rejectReason = "invalid quantity";
2305
+ this._recordOrder(order);
2306
+ this.emit("order:rejected", cloneOrder(order));
2307
+ return cloneOrder(order);
2308
+ }
2309
+ const sideForFill = side === "buy" ? "long" : "short";
2310
+ const filled = applyFill(fillPrice, sideForFill, {
2311
+ slippageBps: this.slippageBps,
2312
+ feeBps: this.feeBps,
2313
+ kind,
2314
+ qty,
2315
+ costs: this.costs
2316
+ });
2317
+ const direction = sideToDirection(side);
2318
+ let remaining = qty;
2319
+ const position = this.positions.get(order.symbol) || null;
2320
+ let realizedPnl = 0;
2321
+ if (!position) {
2322
+ const nextSide = direction > 0 ? "long" : "short";
2323
+ this.positions.set(order.symbol, {
2324
+ symbol: order.symbol,
2325
+ side: nextSide,
2326
+ qty: remaining,
2327
+ avgEntry: filled.price
2328
+ });
2329
+ } else {
2330
+ const signedQty = position.side === "long" ? position.qty : -position.qty;
2331
+ const signedIncoming = direction * remaining;
2332
+ if (signedQty >= 0 && signedIncoming >= 0 || signedQty <= 0 && signedIncoming <= 0) {
2333
+ const totalAbs = Math.abs(signedQty) + Math.abs(signedIncoming);
2334
+ const nextAvg = totalAbs > 0 ? (Math.abs(signedQty) * position.avgEntry + Math.abs(signedIncoming) * filled.price) / totalAbs : filled.price;
2335
+ const nextSide = signedQty + signedIncoming >= 0 ? "long" : "short";
2336
+ this.positions.set(order.symbol, {
2337
+ symbol: order.symbol,
2338
+ side: nextSide,
2339
+ qty: Math.abs(signedQty + signedIncoming),
2340
+ avgEntry: nextAvg
2341
+ });
2342
+ } else {
2343
+ const closeQty = Math.min(Math.abs(signedQty), Math.abs(signedIncoming));
2344
+ if (position.side === "long") {
2345
+ realizedPnl += (filled.price - position.avgEntry) * closeQty;
2346
+ } else {
2347
+ realizedPnl += (position.avgEntry - filled.price) * closeQty;
2348
+ }
2349
+ const remainder = Math.abs(signedIncoming) - closeQty;
2350
+ if (remainder > 0) {
2351
+ const nextSide = direction > 0 ? "long" : "short";
2352
+ this.positions.set(order.symbol, {
2353
+ symbol: order.symbol,
2354
+ side: nextSide,
2355
+ qty: remainder,
2356
+ avgEntry: filled.price
2357
+ });
2358
+ } else if (Math.abs(signedQty) - closeQty > 0) {
2359
+ this.positions.set(order.symbol, {
2360
+ symbol: order.symbol,
2361
+ side: position.side,
2362
+ qty: Math.abs(signedQty) - closeQty,
2363
+ avgEntry: position.avgEntry
2364
+ });
2365
+ } else {
2366
+ this.positions.delete(order.symbol);
2367
+ }
2368
+ }
2369
+ }
2370
+ this.cash -= filled.feeTotal;
2371
+ this.cash += realizedPnl;
2372
+ order.status = "filled";
2373
+ order.filledQty = qty;
2374
+ order.avgFillPrice = filled.price;
2375
+ order.filledAt = fillTime;
2376
+ this._recordOrder(order);
2377
+ this.openOrders.delete(order.orderId);
2378
+ const receipt = cloneOrder(order);
2379
+ this.emit("order:filled", receipt);
2380
+ const account = {
2381
+ cash: this.cash,
2382
+ realizedPnl,
2383
+ feeTotal: filled.feeTotal,
2384
+ equity: this.cash + this._realizedUnrealizedSummary().unrealized
2385
+ };
2386
+ this.emit("equity:update", account);
2387
+ return receipt;
2388
+ }
2389
+ _touchesLimit(order, bar) {
2390
+ const side = order.side === "buy" ? "long" : "short";
2391
+ return touchedLimit(side, order.limitPrice, bar, "intrabar");
2392
+ }
2393
+ _touchesStop(order, bar) {
2394
+ if (order.side === "buy") return bar.high >= order.stopPrice;
2395
+ return bar.low <= order.stopPrice;
2396
+ }
2397
+ async submitOrder(order) {
2398
+ const normalized = {
2399
+ orderId: this._nextOrderId(),
2400
+ clientOrderId: order.clientOrderId,
2401
+ status: "new",
2402
+ filledQty: 0,
2403
+ avgFillPrice: void 0,
2404
+ filledAt: void 0,
2405
+ symbol: String(order.symbol),
2406
+ side: normalizeOrderSide(order.side),
2407
+ type: normalizeOrderType(order.type),
2408
+ qty: roundStep(Math.max(0, asNumber(order.qty, 0)), this.qtyStep),
2409
+ limitPrice: asNumber(order.limitPrice),
2410
+ stopPrice: asNumber(order.stopPrice),
2411
+ timeInForce: order.timeInForce || "day",
2412
+ rejectReason: void 0
2413
+ };
2414
+ if (!(normalized.qty > 0)) {
2415
+ normalized.status = "rejected";
2416
+ normalized.rejectReason = "invalid quantity";
2417
+ this._recordOrder(normalized);
2418
+ this.emit("order:rejected", cloneOrder(normalized));
2419
+ return cloneOrder(normalized);
2420
+ }
2421
+ this._recordOrder(normalized);
2422
+ this.emit("order:submitted", cloneOrder(normalized));
2423
+ if (normalized.type === "market") {
2424
+ const mark = this.lastPrices.get(normalized.symbol);
2425
+ const fillPrice = mark ?? normalized.limitPrice ?? normalized.stopPrice ?? 0;
2426
+ return this._fillOrder(normalized, fillPrice, "market");
2427
+ }
2428
+ this.openOrders.set(normalized.orderId, normalized);
2429
+ return cloneOrder(normalized);
2430
+ }
2431
+ async cancelOrder(orderId) {
2432
+ const order = this.openOrders.get(orderId);
2433
+ if (!order) return;
2434
+ order.status = "canceled";
2435
+ this._recordOrder(order);
2436
+ this.openOrders.delete(orderId);
2437
+ this.emit("order:canceled", cloneOrder(order));
2438
+ }
2439
+ async modifyOrder(orderId, changes = {}) {
2440
+ const order = this.openOrders.get(orderId);
2441
+ if (!order) {
2442
+ throw new Error(`paper order "${orderId}" not found or already closed`);
2443
+ }
2444
+ if (changes.qty !== void 0) {
2445
+ order.qty = roundStep(Math.max(0, asNumber(changes.qty, order.qty)), this.qtyStep);
2446
+ }
2447
+ if (changes.limitPrice !== void 0) {
2448
+ order.limitPrice = asNumber(changes.limitPrice);
2449
+ }
2450
+ if (changes.stopPrice !== void 0) {
2451
+ order.stopPrice = asNumber(changes.stopPrice);
2452
+ }
2453
+ this._recordOrder(order);
2454
+ const receipt = cloneOrder(order);
2455
+ this.emit("order:modified", receipt);
2456
+ return receipt;
2457
+ }
2458
+ async getOpenOrders() {
2459
+ return [...this.openOrders.values()].map((order) => cloneOrder(order));
2460
+ }
2461
+ async getOrderStatus(orderId) {
2462
+ const order = this.openOrders.get(orderId) || this.orderHistory.get(orderId);
2463
+ if (!order) throw new Error(`paper order "${orderId}" not found`);
2464
+ return cloneOrder(order);
2465
+ }
2466
+ async simulateBar(symbol, interval, bar) {
2467
+ const normalizedBar = {
2468
+ time: Number(bar.time),
2469
+ open: Number(bar.open),
2470
+ high: Number(bar.high),
2471
+ low: Number(bar.low),
2472
+ close: Number(bar.close),
2473
+ volume: asNumber(bar.volume, 0)
2474
+ };
2475
+ this.lastPrices.set(symbol, normalizedBar.close);
2476
+ await this._emitTo(this.barSubscribers, this._streamKey(symbol, interval), normalizedBar);
2477
+ await this._emitTo(this.tradeSubscribers, symbol, {
2478
+ time: normalizedBar.time,
2479
+ price: normalizedBar.close,
2480
+ size: normalizedBar.volume ?? 0
2481
+ });
2482
+ const orders = [...this.openOrders.values()].filter((order) => order.symbol === symbol);
2483
+ for (const order of orders) {
2484
+ if (order.type === "limit") {
2485
+ if (this._touchesLimit(order, normalizedBar)) {
2486
+ this._fillOrder(order, order.limitPrice, "limit", normalizedBar.time);
2487
+ }
2488
+ continue;
2489
+ }
2490
+ if (order.type === "stop") {
2491
+ if (this._touchesStop(order, normalizedBar)) {
2492
+ this._fillOrder(order, order.stopPrice, "stop", normalizedBar.time);
2493
+ }
2494
+ continue;
2495
+ }
2496
+ if (order.type === "stop_limit") {
2497
+ order._triggered = Boolean(order._triggered) || this._touchesStop(order, normalizedBar);
2498
+ if (order._triggered && this._touchesLimit(order, normalizedBar)) {
2499
+ this._fillOrder(order, order.limitPrice, "limit", normalizedBar.time);
2500
+ }
2501
+ }
2502
+ }
2503
+ }
2504
+ };
2505
+ function createPaperEngine(options) {
2506
+ return new PaperEngine(options);
2507
+ }
2508
+
2509
+ // src/utils/positionSizing.js
2510
+ function roundStep2(value, step) {
2511
+ return Math.floor(value / step) * step;
2512
+ }
2513
+ var warnedNonPositiveEquity = false;
2514
+ function warnNonPositiveEquity(equity) {
2515
+ if (warnedNonPositiveEquity) return;
2516
+ warnedNonPositiveEquity = true;
2517
+ console.warn(
2518
+ `[tradelab] calculatePositionSize() received non-positive equity (${equity}); returning size 0`
2519
+ );
2520
+ }
2521
+ function calculatePositionSize({
2522
+ equity,
2523
+ entry,
2524
+ stop,
2525
+ riskFraction = 0.01,
2526
+ qtyStep = 1e-3,
2527
+ minQty = 1e-3,
2528
+ maxLeverage = 2
2529
+ }) {
2530
+ if (!Number.isFinite(equity) || equity <= 0) {
2531
+ warnNonPositiveEquity(equity);
2532
+ return 0;
2533
+ }
2534
+ const riskPerUnit = Math.abs(entry - stop);
2535
+ if (!Number.isFinite(riskPerUnit) || riskPerUnit <= 0) return 0;
2536
+ const maxRiskDollars = Math.max(0, equity * riskFraction);
2537
+ let quantity = maxRiskDollars / riskPerUnit;
2538
+ const leverageCapQty = equity * maxLeverage / Math.max(1e-12, Math.abs(entry));
2539
+ quantity = Math.min(quantity, leverageCapQty);
2540
+ quantity = roundStep2(quantity, qtyStep);
2541
+ return quantity >= minQty ? quantity : 0;
2542
+ }
2543
+
2544
+ // src/engine/barSystemRunner.js
2545
+ function asNumber2(value) {
2546
+ const numeric = Number(value);
2547
+ return Number.isFinite(numeric) ? numeric : null;
2548
+ }
2549
+ function formatIsoTime(time) {
2550
+ return Number.isFinite(time) ? new Date(time).toISOString() : "invalid-time";
2551
+ }
2552
+ function callSignalWithContext({ signal, context, index, bar, symbol }) {
2553
+ try {
2554
+ return signal(context);
2555
+ } catch (error) {
2556
+ const cause = error instanceof Error ? error.message : String(error);
2557
+ throw new Error(
2558
+ `signal() threw at index=${index}, time=${formatIsoTime(bar?.time)}, symbol=${symbol}: ${cause}`
2559
+ );
2560
+ }
2561
+ }
2562
+ function snapshotOpenPosition(open, markPrice) {
2563
+ if (!open) return null;
2564
+ const entryPrice = open.entryFill ?? open.entry;
2565
+ const direction = open.side === "long" ? 1 : -1;
2566
+ const unrealizedPnl = (markPrice - entryPrice) * direction * open.size;
2567
+ return {
2568
+ id: open.id,
2569
+ symbol: open.symbol,
2570
+ side: open.side,
2571
+ size: open.size,
2572
+ entry: open.entry,
2573
+ entryFill: open.entryFill,
2574
+ stop: open.stop,
2575
+ takeProfit: open.takeProfit,
2576
+ openTime: open.openTime,
2577
+ markPrice,
2578
+ unrealizedPnl,
2579
+ _initRisk: open._initRisk
2580
+ };
2581
+ }
2582
+ function normalizeSide(value) {
2583
+ if (value === "long" || value === "buy") return "long";
2584
+ if (value === "short" || value === "sell") return "short";
2585
+ return null;
2586
+ }
2587
+ function normalizeSignal(signal, bar, fallbackR) {
2588
+ if (!signal) return null;
2589
+ const side = normalizeSide(signal.side ?? signal.direction ?? signal.action);
2590
+ if (!side) return null;
2591
+ const entry = asNumber2(signal.entry ?? signal.limit ?? signal.price) ?? asNumber2(bar?.close);
2592
+ const stop = asNumber2(signal.stop ?? signal.stopLoss ?? signal.sl);
2593
+ if (entry === null || stop === null) return null;
2594
+ const risk = Math.abs(entry - stop);
2595
+ if (!(risk > 0)) return null;
2596
+ let takeProfit = asNumber2(signal.takeProfit ?? signal.target ?? signal.tp);
2597
+ const rrHint = asNumber2(signal._rr ?? signal.rr);
2598
+ const targetR = rrHint ?? fallbackR;
2599
+ if (takeProfit === null && Number.isFinite(targetR) && targetR > 0) {
2600
+ takeProfit = side === "long" ? entry + risk * targetR : entry - risk * targetR;
2601
+ }
2602
+ if (takeProfit === null) return null;
2603
+ return {
2604
+ ...signal,
2605
+ side,
2606
+ entry,
2607
+ stop,
2608
+ takeProfit,
2609
+ qty: asNumber2(signal.qty ?? signal.size),
2610
+ riskPct: asNumber2(signal.riskPct),
2611
+ riskFraction: asNumber2(signal.riskFraction),
2612
+ _rr: rrHint ?? signal._rr,
2613
+ _initRisk: asNumber2(signal._initRisk) ?? signal._initRisk
2614
+ };
2615
+ }
2616
+
2617
+ // src/live/engine/liveEngine.js
2618
+ function asNumber3(value, fallback = null) {
2619
+ const numeric = Number(value);
2620
+ return Number.isFinite(numeric) ? numeric : fallback;
2621
+ }
2622
+ function oppositeSide(side) {
2623
+ return side === "long" ? "sell" : "buy";
2624
+ }
2625
+ function nowIso() {
2626
+ return (/* @__PURE__ */ new Date()).toISOString();
2627
+ }
2628
+ function matchesPendingOrder(pendingOrder, order) {
2629
+ if (!pendingOrder || !order) return false;
2630
+ if (order.orderId && pendingOrder.orderId && order.orderId === pendingOrder.orderId) return true;
2631
+ if (order.clientOrderId && pendingOrder.clientOrderId && order.clientOrderId === pendingOrder.clientOrderId) {
2632
+ return true;
2633
+ }
2634
+ return false;
2635
+ }
2636
+ function isOrderForSymbol(order, symbol) {
2637
+ return !order?.symbol || order.symbol === symbol;
2638
+ }
2639
+ var LiveEngine = class {
2640
+ constructor(options = {}) {
2641
+ if (typeof options.signal !== "function") {
2642
+ throw new Error(`liveEngine requires a signal function, got ${typeof options.signal}`);
2643
+ }
2644
+ if (!options.broker) {
2645
+ throw new Error("liveEngine requires a broker adapter");
2646
+ }
2647
+ if (!options.symbol) {
2648
+ throw new Error("liveEngine requires symbol");
2649
+ }
2650
+ this.options = {
2651
+ interval: "1m",
2652
+ mode: "streaming",
2653
+ pollIntervalMs: 6e4,
2654
+ warmupBars: 200,
2655
+ equity: 1e4,
2656
+ riskPct: 1,
2657
+ finalTP_R: 3,
2658
+ flattenAtClose: false,
2659
+ qtyStep: 1e-3,
2660
+ minQty: 1e-3,
2661
+ maxLeverage: 2,
2662
+ dailyMaxTrades: 0,
2663
+ entryChase: {
2664
+ enabled: true,
2665
+ afterBars: 2,
2666
+ maxSlipR: 0.2,
2667
+ convertOnExpiry: false
2668
+ },
2669
+ logLevel: "info",
2670
+ ...options
2671
+ };
2672
+ this.symbol = this.options.symbol;
2673
+ this.interval = this.options.interval;
2674
+ this.namespace = this.options.id || `${this.symbol}-${this.interval}`.replace(/[^a-zA-Z0-9._-]/g, "_");
2675
+ this.broker = this.options.broker;
2676
+ this.feed = this.options.feed || (this.options.mode === "polling" ? new PollingFeed({
2677
+ broker: this.broker,
2678
+ pollIntervalMs: this.options.pollIntervalMs
2679
+ }) : new BrokerFeed({ broker: this.broker }));
2680
+ this.eventBus = this.options.eventBus || new EventBus();
2681
+ this.storage = this.options.storage || new JsonFileStorage();
2682
+ this.stateManager = new StateManager({ storage: this.storage });
2683
+ this.riskManager = new RiskManager({
2684
+ maxDailyLossPct: this.options.maxDailyLossPct,
2685
+ maxDailyTrades: this.options.dailyMaxTrades,
2686
+ ...this.options.risk || {}
2687
+ });
2688
+ this.clock = new BrokerClock();
2689
+ this.running = false;
2690
+ this.connected = false;
2691
+ this.subscriptions = [];
2692
+ this.candleBuffer = [];
2693
+ this.lastBarTime = null;
2694
+ this.openPosition = null;
2695
+ this.pendingOrder = null;
2696
+ this.tradeIdCounter = 0;
2697
+ this.trades = [];
2698
+ this.eqSeries = [];
2699
+ this.equity = this.options.equity;
2700
+ this.dayPnl = 0;
2701
+ this.dayTrades = 0;
2702
+ this.startedAt = null;
2703
+ this._boundOrderSubmitted = (payload) => this._forwardBrokerEvent("order:submitted", payload);
2704
+ this._boundOrderFilled = (payload) => this._handleOrderFilled(payload);
2705
+ this._boundOrderCanceled = (payload) => this._handleOrderCanceled(payload);
2706
+ this._boundOrderRejected = (payload) => this._handleOrderRejected(payload);
2707
+ this._boundOrderModified = (payload) => this._forwardBrokerEvent("order:modified", payload);
2708
+ }
2709
+ _emit(event, payload = {}) {
2710
+ this.eventBus.emitEvent(event, payload);
2711
+ }
2712
+ _forwardBrokerEvent(event, payload = {}) {
2713
+ if (!isOrderForSymbol(payload, this.symbol)) return;
2714
+ this._emit(event, { ...payload, symbol: payload.symbol || this.symbol });
2715
+ }
2716
+ _attachBrokerListeners() {
2717
+ this.broker.on("order:submitted", this._boundOrderSubmitted);
2718
+ this.broker.on("order:filled", this._boundOrderFilled);
2719
+ this.broker.on("order:canceled", this._boundOrderCanceled);
2720
+ this.broker.on("order:rejected", this._boundOrderRejected);
2721
+ this.broker.on("order:modified", this._boundOrderModified);
2722
+ }
2723
+ _detachBrokerListeners() {
2724
+ this.broker.off("order:submitted", this._boundOrderSubmitted);
2725
+ this.broker.off("order:filled", this._boundOrderFilled);
2726
+ this.broker.off("order:canceled", this._boundOrderCanceled);
2727
+ this.broker.off("order:rejected", this._boundOrderRejected);
2728
+ this.broker.off("order:modified", this._boundOrderModified);
2729
+ }
2730
+ _appendBar(bar) {
2731
+ this.candleBuffer.push(bar);
2732
+ const maxSize = Math.max(10, Number(this.options.warmupBars || 200) + 100);
2733
+ if (this.candleBuffer.length > maxSize) {
2734
+ this.candleBuffer.splice(0, this.candleBuffer.length - maxSize);
2735
+ }
2736
+ this.lastBarTime = bar.time;
2737
+ }
2738
+ _currentMarkPrice(defaultPrice = null) {
2739
+ return this.candleBuffer.length ? this.candleBuffer[this.candleBuffer.length - 1].close : defaultPrice;
2740
+ }
2741
+ _markedEquity(markPrice = null) {
2742
+ if (!this.openPosition) return this.equity;
2743
+ const mark = Number.isFinite(markPrice) ? markPrice : this._currentMarkPrice(this.openPosition.entryFill);
2744
+ const direction = this.openPosition.side === "long" ? 1 : -1;
2745
+ return this.equity + (mark - this.openPosition.entryFill) * direction * this.openPosition.size;
2746
+ }
2747
+ _signalContext(bar) {
2748
+ const markEquity = this._markedEquity(bar.close);
2749
+ return {
2750
+ candles: this.candleBuffer,
2751
+ index: this.candleBuffer.length - 1,
2752
+ bar,
2753
+ equity: markEquity,
2754
+ openPosition: this.openPosition ? snapshotOpenPosition(this.openPosition, bar.close) : null,
2755
+ pendingOrder: this.pendingOrder
2756
+ };
2757
+ }
2758
+ async _persistState() {
2759
+ await this.stateManager.save(this.namespace, {
2760
+ openPosition: this.openPosition,
2761
+ pendingOrder: this.pendingOrder,
2762
+ equity: this.equity,
2763
+ candleBuffer: this.candleBuffer,
2764
+ strategyState: {},
2765
+ lastBarTime: this.lastBarTime,
2766
+ dayPnl: this.dayPnl,
2767
+ dayTrades: this.dayTrades,
2768
+ tradeIdCounter: this.tradeIdCounter,
2769
+ savedAt: Date.now()
2770
+ });
2771
+ }
2772
+ async _recordEquity(timeMs, markPrice) {
2773
+ const point = {
2774
+ time: timeMs,
2775
+ timestamp: timeMs,
2776
+ equity: this._markedEquity(markPrice)
2777
+ };
2778
+ this.eqSeries.push(point);
2779
+ await this.stateManager.appendEquityPoint(this.namespace, point);
2780
+ this._emit("equity:update", {
2781
+ symbol: this.symbol,
2782
+ equity: point.equity,
2783
+ time: point.time
2784
+ });
2785
+ }
2786
+ async _submitEntry(signalDecision, { hasExplicitEntry }) {
2787
+ const riskFraction = Number.isFinite(signalDecision.riskFraction) ? signalDecision.riskFraction : Number.isFinite(signalDecision.riskPct) ? signalDecision.riskPct / 100 : this.options.riskPct / 100;
2788
+ const requestedSize = Number.isFinite(signalDecision.qty) ? signalDecision.qty : calculatePositionSize({
2789
+ equity: this._markedEquity(signalDecision.entry),
2790
+ entry: signalDecision.entry,
2791
+ stop: signalDecision.stop,
2792
+ riskFraction,
2793
+ qtyStep: this.options.qtyStep,
2794
+ minQty: this.options.minQty,
2795
+ maxLeverage: this.options.maxLeverage
2796
+ });
2797
+ if (!(requestedSize >= this.options.minQty)) return;
2798
+ const positionValue = Math.abs(signalDecision.entry * requestedSize);
2799
+ const canOpen = this.riskManager.canOpenPosition({
2800
+ timeMs: this.lastBarTime || Date.now(),
2801
+ positionCount: this.openPosition ? 1 : 0,
2802
+ positionValue,
2803
+ equity: this._markedEquity(signalDecision.entry)
2804
+ });
2805
+ if (!canOpen.ok) {
2806
+ this._emit("risk:warning", { symbol: this.symbol, reason: canOpen.reason });
2807
+ return;
2808
+ }
2809
+ const side = signalDecision.side === "long" ? "buy" : "sell";
2810
+ const orderType = hasExplicitEntry ? "limit" : "market";
2811
+ const clientOrderId = `${this.namespace}-entry-${Date.now()}`;
2812
+ const expiryBars = signalDecision._entryExpiryBars ?? 5;
2813
+ this.pendingOrder = {
2814
+ side: signalDecision.side,
2815
+ entry: signalDecision.entry,
2816
+ stop: signalDecision.stop,
2817
+ tp: signalDecision.takeProfit,
2818
+ riskFrac: riskFraction,
2819
+ fixedQty: signalDecision.qty ?? requestedSize,
2820
+ expiresAt: this.candleBuffer.length - 1 + Math.max(1, expiryBars),
2821
+ startedAtIndex: this.candleBuffer.length - 1,
2822
+ meta: signalDecision,
2823
+ plannedRiskAbs: Math.abs(
2824
+ signalDecision._initRisk ?? signalDecision.entry - signalDecision.stop
2825
+ ),
2826
+ orderId: null,
2827
+ clientOrderId,
2828
+ type: orderType,
2829
+ _chasedCE: false
2830
+ };
2831
+ const receipt = await this.broker.submitOrder({
2832
+ symbol: this.symbol,
2833
+ side,
2834
+ type: orderType,
2835
+ qty: requestedSize,
2836
+ limitPrice: orderType === "limit" ? signalDecision.entry : void 0,
2837
+ clientOrderId
2838
+ });
2839
+ if (!this.pendingOrder) return;
2840
+ this.pendingOrder.orderId = receipt.orderId || this.pendingOrder.orderId;
2841
+ if (receipt.clientOrderId) this.pendingOrder.clientOrderId = receipt.clientOrderId;
2842
+ await this._persistState();
2843
+ if (receipt.status === "filled") {
2844
+ await this._handleOrderFilled(receipt);
2845
+ }
2846
+ }
2847
+ async _submitExit(reason, priceHint, kind = "market") {
2848
+ if (!this.openPosition) return;
2849
+ this.openPosition._pendingExitReason = reason;
2850
+ this.openPosition._pendingExitPriceHint = priceHint;
2851
+ const receipt = await this.broker.submitOrder({
2852
+ symbol: this.symbol,
2853
+ side: oppositeSide(this.openPosition.side),
2854
+ type: kind,
2855
+ qty: this.openPosition.size,
2856
+ limitPrice: kind === "limit" ? priceHint : void 0,
2857
+ stopPrice: kind === "stop" ? priceHint : void 0,
2858
+ clientOrderId: `${this.namespace}-exit-${Date.now()}`
2859
+ });
2860
+ if (receipt.status === "filled" && this.openPosition && isOrderForSymbol(receipt, this.symbol)) {
2861
+ await this._handleOrderFilled(receipt);
2862
+ }
2863
+ await this._persistState();
2864
+ }
2865
+ async _managePending(_bar) {
2866
+ if (!this.pendingOrder) return;
2867
+ const index = this.candleBuffer.length - 1;
2868
+ if (index > this.pendingOrder.expiresAt) {
2869
+ if (this.pendingOrder.orderId) {
2870
+ await this.broker.cancelOrder(this.pendingOrder.orderId).catch(() => {
2871
+ });
2872
+ }
2873
+ this.pendingOrder = null;
2874
+ await this._persistState();
2875
+ return;
2876
+ }
2877
+ if (this.options.entryChase?.enabled) {
2878
+ const elapsedBars = index - (this.pendingOrder.startedAtIndex ?? index);
2879
+ const midpoint = asNumber3(this.pendingOrder.meta?._imb?.mid);
2880
+ if (midpoint !== null && !this.pendingOrder._chasedCE && elapsedBars >= Math.max(1, this.options.entryChase.afterBars || 2) && this.pendingOrder.orderId) {
2881
+ await this.broker.modifyOrder(this.pendingOrder.orderId, { limitPrice: midpoint }).catch(() => {
2882
+ });
2883
+ this.pendingOrder.entry = midpoint;
2884
+ this.pendingOrder._chasedCE = true;
2885
+ await this._persistState();
2886
+ }
2887
+ }
2888
+ }
2889
+ async _manageOpenPosition(bar) {
2890
+ if (!this.openPosition) return;
2891
+ if (this.options.flattenAtClose && isEODBar(bar.time)) {
2892
+ await this._submitExit("EOD", bar.close);
2893
+ return;
2894
+ }
2895
+ const barsHeld = this.candleBuffer.length - (this.openPosition._openedAtIndex ?? 0);
2896
+ if (Number.isFinite(this.openPosition._maxBarsInTrade) && this.openPosition._maxBarsInTrade > 0 && barsHeld >= this.openPosition._maxBarsInTrade) {
2897
+ await this._submitExit("TIME", bar.close);
2898
+ return;
2899
+ }
2900
+ const { hit, px } = ocoExitCheck({
2901
+ side: this.openPosition.side,
2902
+ stop: this.openPosition.stop,
2903
+ tp: this.openPosition.takeProfit,
2904
+ bar,
2905
+ mode: this.options.oco?.mode || "intrabar",
2906
+ tieBreak: this.options.oco?.tieBreak || "pessimistic"
2907
+ });
2908
+ if (hit) {
2909
+ const kind = hit === "TP" ? "limit" : "stop";
2910
+ await this._submitExit(hit, px, kind);
2911
+ }
2912
+ }
2913
+ async _handleOrderFilled(order) {
2914
+ if (!isOrderForSymbol(order, this.symbol)) return;
2915
+ this._emit("order:filled", { symbol: this.symbol, ...order });
2916
+ const pendingMatches = matchesPendingOrder(this.pendingOrder, order);
2917
+ if (pendingMatches) {
2918
+ const entryFill = asNumber3(order.avgFillPrice, this.pendingOrder.entry);
2919
+ this.openPosition = {
2920
+ id: ++this.tradeIdCounter,
2921
+ symbol: this.symbol,
2922
+ side: this.pendingOrder.side,
2923
+ entry: this.pendingOrder.entry,
2924
+ entryFill,
2925
+ stop: this.pendingOrder.stop,
2926
+ takeProfit: this.pendingOrder.tp,
2927
+ size: Number(order.filledQty || this.pendingOrder.fixedQty || 0),
2928
+ openTime: asNumber3(order.filledAt, this.lastBarTime || Date.now()),
2929
+ _initRisk: Math.abs(
2930
+ this.pendingOrder.meta?._initRisk ?? this.pendingOrder.entry - this.pendingOrder.stop
2931
+ ),
2932
+ _maxBarsInTrade: this.pendingOrder.meta?._maxBarsInTrade,
2933
+ _maxHoldMin: this.pendingOrder.meta?._maxHoldMin,
2934
+ _openedAtIndex: this.candleBuffer.length - 1
2935
+ };
2936
+ this.pendingOrder = null;
2937
+ this.dayTrades += 1;
2938
+ this._emit("position:opened", {
2939
+ symbol: this.symbol,
2940
+ position: snapshotOpenPosition(this.openPosition, entryFill)
2941
+ });
2942
+ await this._persistState();
2943
+ return;
2944
+ }
2945
+ if (this.openPosition && order.side === oppositeSide(this.openPosition.side)) {
2946
+ const closingPosition = this.openPosition;
2947
+ const exitPrice = asNumber3(
2948
+ order.avgFillPrice,
2949
+ closingPosition._pendingExitPriceHint ?? this._currentMarkPrice(closingPosition.entryFill)
2950
+ );
2951
+ const direction = closingPosition.side === "long" ? 1 : -1;
2952
+ const qty = Number(order.filledQty || closingPosition.size || 0);
2953
+ const pnl = (exitPrice - closingPosition.entryFill) * direction * qty;
2954
+ this.equity += pnl;
2955
+ this.dayPnl += pnl;
2956
+ this.openPosition = null;
2957
+ this.riskManager.recordTrade({
2958
+ pnl,
2959
+ timeMs: asNumber3(order.filledAt, Date.now()),
2960
+ equity: this.equity
2961
+ });
2962
+ const trade = {
2963
+ symbol: this.symbol,
2964
+ id: closingPosition.id,
2965
+ side: closingPosition.side,
2966
+ entry: closingPosition.entry,
2967
+ stop: closingPosition.stop,
2968
+ takeProfit: closingPosition.takeProfit,
2969
+ size: qty,
2970
+ openTime: closingPosition.openTime,
2971
+ entryFill: closingPosition.entryFill,
2972
+ _initRisk: closingPosition._initRisk,
2973
+ exit: {
2974
+ price: exitPrice,
2975
+ time: asNumber3(order.filledAt, Date.now()),
2976
+ reason: closingPosition._pendingExitReason || "EXIT",
2977
+ pnl
2978
+ }
2979
+ };
2980
+ this.trades.push(trade);
2981
+ await this.stateManager.appendTrade(this.namespace, trade);
2982
+ this._emit("position:closed", {
2983
+ symbol: this.symbol,
2984
+ trade
2985
+ });
2986
+ await this._persistState();
2987
+ }
2988
+ }
2989
+ async _handleOrderCanceled(order) {
2990
+ if (!isOrderForSymbol(order, this.symbol)) return;
2991
+ this._emit("order:canceled", { symbol: this.symbol, ...order });
2992
+ const pendingMatches = matchesPendingOrder(this.pendingOrder, order);
2993
+ if (pendingMatches) {
2994
+ this.pendingOrder = null;
2995
+ await this._persistState();
2996
+ }
2997
+ }
2998
+ async _handleOrderRejected(order) {
2999
+ if (!isOrderForSymbol(order, this.symbol)) return;
3000
+ this._emit("order:rejected", { symbol: this.symbol, ...order });
3001
+ const pendingMatches = matchesPendingOrder(this.pendingOrder, order);
3002
+ if (pendingMatches) {
3003
+ this.pendingOrder = null;
3004
+ await this._persistState();
3005
+ }
3006
+ }
3007
+ async handleBar(rawBar) {
3008
+ const normalized = normalizeCandles([rawBar]);
3009
+ const bar = normalized[0];
3010
+ if (!bar) return;
3011
+ if (Number.isFinite(this.lastBarTime) && bar.time <= this.lastBarTime) return;
3012
+ if (!this.running) return;
3013
+ this._appendBar(bar);
3014
+ this._emit("bar", { symbol: this.symbol, bar });
3015
+ this.riskManager.update({
3016
+ timeMs: bar.time,
3017
+ equity: this._markedEquity(bar.close)
3018
+ });
3019
+ if (this.openPosition) {
3020
+ await this._manageOpenPosition(bar);
3021
+ }
3022
+ if (this.pendingOrder) {
3023
+ await this._managePending(bar);
3024
+ }
3025
+ const canTrade = this.riskManager.canTrade({ timeMs: bar.time });
3026
+ if (!canTrade.ok && this.pendingOrder) {
3027
+ if (this.pendingOrder.orderId) {
3028
+ await this.broker.cancelOrder(this.pendingOrder.orderId).catch(() => {
3029
+ });
3030
+ }
3031
+ this.pendingOrder = null;
3032
+ await this._persistState();
3033
+ }
3034
+ if (!canTrade.ok) {
3035
+ this._emit("risk:halt", { symbol: this.symbol, reason: canTrade.reason });
3036
+ await this._recordEquity(bar.time, bar.close);
3037
+ return;
3038
+ }
3039
+ if (!this.openPosition && !this.pendingOrder) {
3040
+ const context = this._signalContext(bar);
3041
+ const rawSignal = callSignalWithContext({
3042
+ signal: this.options.signal,
3043
+ context,
3044
+ index: context.index,
3045
+ bar,
3046
+ symbol: this.symbol
3047
+ });
3048
+ if (rawSignal) {
3049
+ this._emit("signal", {
3050
+ symbol: this.symbol,
3051
+ t: nowIso(),
3052
+ signal: rawSignal
3053
+ });
3054
+ }
3055
+ const nextSignal = normalizeSignal(rawSignal, bar, this.options.finalTP_R);
3056
+ if (nextSignal) {
3057
+ const hasExplicitEntry = rawSignal?.entry !== void 0 || rawSignal?.limit !== void 0 || rawSignal?.price !== void 0;
3058
+ await this._submitEntry(nextSignal, { hasExplicitEntry });
3059
+ }
3060
+ }
3061
+ await this._recordEquity(bar.time, bar.close);
3062
+ }
3063
+ async pollOnce() {
3064
+ if (typeof this.feed.pollOnce === "function") {
3065
+ await this.feed.pollOnce();
3066
+ return;
3067
+ }
3068
+ const bars = await this.feed.getHistoricalBars(this.symbol, this.interval, 2);
3069
+ const ordered = [...bars].sort((left, right) => left.time - right.time);
3070
+ for (const bar of ordered) {
3071
+ await this.handleBar(bar);
3072
+ }
3073
+ }
3074
+ async start() {
3075
+ if (this.running) return;
3076
+ if (!(typeof this.broker.isConnected === "function" && this.broker.isConnected())) {
3077
+ await this.broker.connect(this.options.brokerConfig || {});
3078
+ }
3079
+ await this.feed.connect();
3080
+ this._attachBrokerListeners();
3081
+ const clock = await this.clock.syncWithBroker(this.broker);
3082
+ if (clock.warning) {
3083
+ this._emit("risk:warning", {
3084
+ symbol: this.symbol,
3085
+ reason: clock.warning
3086
+ });
3087
+ }
3088
+ if (this.options.useBrokerAccountEquity !== false) {
3089
+ try {
3090
+ const account = await this.broker.getAccount();
3091
+ if (Number.isFinite(account?.equity) && account.equity > 0) {
3092
+ this.equity = account.equity;
3093
+ }
3094
+ } catch {
3095
+ this.equity = this.options.equity;
3096
+ }
3097
+ }
3098
+ const persisted = await this.stateManager.load(this.namespace);
3099
+ if (persisted) {
3100
+ this.openPosition = persisted.openPosition || null;
3101
+ this.pendingOrder = persisted.pendingOrder || null;
3102
+ this.equity = Number.isFinite(persisted.equity) ? persisted.equity : this.equity;
3103
+ this.candleBuffer = Array.isArray(persisted.candleBuffer) ? persisted.candleBuffer : [];
3104
+ this.lastBarTime = Number.isFinite(persisted.lastBarTime) ? persisted.lastBarTime : null;
3105
+ this.dayPnl = Number.isFinite(persisted.dayPnl) ? persisted.dayPnl : 0;
3106
+ this.dayTrades = Number.isFinite(persisted.dayTrades) ? persisted.dayTrades : 0;
3107
+ this.tradeIdCounter = Number.isFinite(persisted.tradeIdCounter) ? persisted.tradeIdCounter : 0;
3108
+ this._emit("stateRestored", { symbol: this.symbol, namespace: this.namespace });
3109
+ }
3110
+ const warmup = await this.feed.getHistoricalBars(
3111
+ this.symbol,
3112
+ this.interval,
3113
+ Math.max(1, this.options.warmupBars)
3114
+ );
3115
+ const normalizedWarmup = normalizeCandles(warmup || []);
3116
+ for (const bar of normalizedWarmup) {
3117
+ if (this.lastBarTime !== null && bar.time <= this.lastBarTime) continue;
3118
+ this._appendBar(bar);
3119
+ }
3120
+ const reconcile = this.stateManager.reconcile({
3121
+ persistedState: persisted,
3122
+ brokerPositions: await this.broker.getPositions().catch(() => []),
3123
+ symbol: this.symbol
3124
+ });
3125
+ if (reconcile.action === "adopt-broker" && reconcile.adoptedPosition) {
3126
+ this.openPosition = {
3127
+ ...this.openPosition,
3128
+ ...reconcile.adoptedPosition
3129
+ };
3130
+ }
3131
+ if (reconcile.action === "mismatch") {
3132
+ this.riskManager.halt("position mismatch on restart");
3133
+ }
3134
+ this._emit("reconciled", { symbol: this.symbol, reconcile });
3135
+ this.riskManager.initialize(this.equity, this.lastBarTime || Date.now());
3136
+ if (this.dayTrades > 0 || this.dayPnl !== 0) {
3137
+ this.riskManager.dayTrades = this.dayTrades;
3138
+ this.riskManager.dayPnl = this.dayPnl;
3139
+ }
3140
+ const subscription = await this.feed.subscribeBars(this.symbol, this.interval, async (bar) => {
3141
+ await this.handleBar(bar);
3142
+ });
3143
+ this.subscriptions.push(subscription);
3144
+ if (this.options.mode === "polling" && typeof this.feed.startPolling === "function") {
3145
+ this.feed.startPolling();
3146
+ }
3147
+ this.startedAt = Date.now();
3148
+ this.connected = true;
3149
+ this.running = true;
3150
+ this._emit("connected", { symbol: this.symbol, namespace: this.namespace });
3151
+ await this._persistState();
3152
+ }
3153
+ async stop({ flattenOnShutdown = false } = {}) {
3154
+ if (!this.connected) return;
3155
+ if (flattenOnShutdown && this.openPosition) {
3156
+ await this._submitExit("SHUTDOWN", this._currentMarkPrice(this.openPosition.entryFill));
3157
+ }
3158
+ if (typeof this.feed.stopPolling === "function") {
3159
+ this.feed.stopPolling();
3160
+ }
3161
+ for (const subscription of this.subscriptions) {
3162
+ if (subscription && typeof subscription.unsubscribe === "function") {
3163
+ subscription.unsubscribe();
3164
+ }
3165
+ }
3166
+ this.subscriptions = [];
3167
+ await this._persistState();
3168
+ await this.feed.disconnect();
3169
+ await this.broker.disconnect();
3170
+ this._detachBrokerListeners();
3171
+ this.running = false;
3172
+ this.connected = false;
3173
+ this._emit("shutdown", { symbol: this.symbol, namespace: this.namespace });
3174
+ }
3175
+ getStatus() {
3176
+ return {
3177
+ id: this.namespace,
3178
+ symbol: this.symbol,
3179
+ interval: this.interval,
3180
+ running: this.running,
3181
+ connected: this.connected,
3182
+ startedAt: this.startedAt,
3183
+ lastBarTime: this.lastBarTime,
3184
+ equity: this._markedEquity(),
3185
+ realizedEquity: this.equity,
3186
+ openPosition: this.openPosition ? snapshotOpenPosition(this.openPosition, this._currentMarkPrice()) : null,
3187
+ pendingOrder: this.pendingOrder,
3188
+ dayPnl: this.dayPnl,
3189
+ dayTrades: this.dayTrades,
3190
+ trades: this.trades.length,
3191
+ risk: this.riskManager.getState()
3192
+ };
3193
+ }
3194
+ };
3195
+ function createLiveEngine(options) {
3196
+ return new LiveEngine(options);
3197
+ }
3198
+
3199
+ // src/live/orchestrator.js
3200
+ function asWeight(value) {
3201
+ return Number.isFinite(value) && value > 0 ? value : 0;
3202
+ }
3203
+ function defaultSystemId(system, index) {
3204
+ return system.id || `${system.symbol}-${system.interval || "1m"}-${index + 1}`;
3205
+ }
3206
+ var LiveOrchestrator = class {
3207
+ constructor(options = {}) {
3208
+ if (!Array.isArray(options.systems) || options.systems.length === 0) {
3209
+ throw new Error("orchestrator requires a non-empty systems array");
3210
+ }
3211
+ if (!options.broker) {
3212
+ throw new Error("orchestrator requires a broker adapter");
3213
+ }
3214
+ this.options = {
3215
+ allocation: "equal",
3216
+ equity: 1e4,
3217
+ maxDailyLossPct: 0,
3218
+ ...options
3219
+ };
3220
+ this.eventBus = this.options.eventBus || new EventBus();
3221
+ this.engines = [];
3222
+ this.running = false;
3223
+ this.dayStartEquity = this.options.equity;
3224
+ this.currentDay = null;
3225
+ }
3226
+ _emit(event, payload = {}) {
3227
+ this.eventBus.emitEvent(event, payload);
3228
+ }
3229
+ _allocationWeights() {
3230
+ const systems = this.options.systems;
3231
+ if (this.options.allocation === "equal") {
3232
+ return systems.map(() => 1);
3233
+ }
3234
+ return systems.map((system) => asWeight(system.weight || 0));
3235
+ }
3236
+ _allocatedEquities(totalEquity) {
3237
+ const weights = this._allocationWeights();
3238
+ const totalWeight = weights.reduce((sum, value) => sum + value, 0) || 1;
3239
+ return weights.map((weight) => totalEquity * weight / totalWeight);
3240
+ }
3241
+ async start() {
3242
+ if (this.running) return;
3243
+ const account = await this.options.broker.getAccount().catch(() => null);
3244
+ const totalEquity = Number.isFinite(account?.equity) ? account.equity : this.options.equity;
3245
+ const perSystemEquity = this._allocatedEquities(totalEquity);
3246
+ this.engines = this.options.systems.map((system, index) => {
3247
+ const engineBus = new EventBus();
3248
+ engineBus.onAny(({ event, payload }) => {
3249
+ this._emit(event, {
3250
+ systemId: defaultSystemId(system, index),
3251
+ ...payload
3252
+ });
3253
+ if (event === "equity:update") this._checkPortfolioLimits();
3254
+ });
3255
+ return new LiveEngine({
3256
+ ...system,
3257
+ id: defaultSystemId(system, index),
3258
+ broker: this.options.broker,
3259
+ feed: this.options.feed,
3260
+ storage: this.options.storage,
3261
+ eventBus: engineBus,
3262
+ brokerConfig: this.options.brokerConfig,
3263
+ equity: perSystemEquity[index],
3264
+ useBrokerAccountEquity: false
3265
+ });
3266
+ });
3267
+ await Promise.all(this.engines.map((engine) => engine.start()));
3268
+ this.running = true;
3269
+ this.dayStartEquity = this.getStatus().aggregateEquity;
3270
+ this.currentDay = dayKeyET(Date.now());
3271
+ }
3272
+ _checkPortfolioLimits() {
3273
+ if (!this.options.maxDailyLossPct || this.options.maxDailyLossPct <= 0) return;
3274
+ const nowDay = dayKeyET(Date.now());
3275
+ if (this.currentDay !== nowDay) {
3276
+ this.currentDay = nowDay;
3277
+ this.dayStartEquity = this.getStatus().aggregateEquity;
3278
+ return;
3279
+ }
3280
+ const equity = this.getStatus().aggregateEquity;
3281
+ const maxLossFraction = Math.abs(this.options.maxDailyLossPct) / 100;
3282
+ if (equity <= this.dayStartEquity * (1 - maxLossFraction)) {
3283
+ for (const engine of this.engines) {
3284
+ engine.riskManager.halt("portfolio daily loss limit reached");
3285
+ }
3286
+ this._emit("risk:halt", {
3287
+ reason: "portfolio daily loss limit reached",
3288
+ aggregateEquity: equity
3289
+ });
3290
+ }
3291
+ }
3292
+ async stop() {
3293
+ await Promise.all(this.engines.map((engine) => engine.stop()));
3294
+ this.running = false;
3295
+ }
3296
+ getStatus() {
3297
+ const systems = this.engines.map((engine) => engine.getStatus());
3298
+ const aggregateEquity = systems.reduce((sum, status) => sum + (status.equity || 0), 0);
3299
+ const openPositions = systems.filter((status) => status.openPosition).length;
3300
+ return {
3301
+ running: this.running,
3302
+ systems,
3303
+ aggregateEquity,
3304
+ openPositions,
3305
+ dayStartEquity: this.dayStartEquity
3306
+ };
3307
+ }
3308
+ };
3309
+ function createLiveOrchestrator(options) {
3310
+ return new LiveOrchestrator(options);
3311
+ }
3312
+ // Annotate the CommonJS export names for ESM import in node:
3313
+ 0 && (module.exports = {
3314
+ AlpacaBroker,
3315
+ BinanceBroker,
3316
+ BrokerAdapter,
3317
+ BrokerClock,
3318
+ BrokerFeed,
3319
+ CandleAggregator,
3320
+ CoinbaseBroker,
3321
+ EventBus,
3322
+ FeedProvider,
3323
+ InteractiveBrokersBroker,
3324
+ JsonFileStorage,
3325
+ LIVE_EVENTS,
3326
+ LiveEngine,
3327
+ LiveLogger,
3328
+ LiveOrchestrator,
3329
+ PaperEngine,
3330
+ PollingFeed,
3331
+ RiskManager,
3332
+ StateManager,
3333
+ StorageProvider,
3334
+ createAlpacaBroker,
3335
+ createBinanceBroker,
3336
+ createBrokerFeed,
3337
+ createCandleAggregator,
3338
+ createClock,
3339
+ createCoinbaseBroker,
3340
+ createEventBus,
3341
+ createInteractiveBrokersBroker,
3342
+ createJsonFileStorage,
3343
+ createLiveEngine,
3344
+ createLiveOrchestrator,
3345
+ createLogger,
3346
+ createPaperEngine,
3347
+ createPollingFeed,
3348
+ createRiskManager,
3349
+ createStateManager
3350
+ });