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.
- package/README.md +89 -41
- package/bin/tradelab.js +276 -30
- package/dist/cjs/data.cjs +134 -104
- package/dist/cjs/index.cjs +378 -177
- package/dist/cjs/live.cjs +3350 -0
- package/docs/README.md +21 -9
- package/docs/api-reference.md +87 -29
- package/docs/backtest-engine.md +37 -53
- package/docs/data-reporting-cli.md +60 -34
- package/docs/examples.md +6 -12
- package/docs/live-trading.md +186 -0
- package/examples/yahooEmaCross.js +1 -6
- package/package.json +18 -3
- package/src/data/csv.js +24 -14
- package/src/data/index.js +1 -5
- package/src/data/yahoo.js +6 -19
- package/src/engine/backtest.js +137 -144
- package/src/engine/backtestTicks.js +89 -37
- package/src/engine/barSystemRunner.js +182 -118
- package/src/engine/execution.js +11 -39
- package/src/engine/portfolio.js +54 -6
- package/src/engine/walkForward.js +37 -14
- package/src/index.js +2 -11
- package/src/live/broker/alpaca.js +254 -0
- package/src/live/broker/binance.js +351 -0
- package/src/live/broker/coinbase.js +339 -0
- package/src/live/broker/interactiveBrokers.js +123 -0
- package/src/live/broker/interface.js +74 -0
- package/src/live/clock.js +56 -0
- package/src/live/engine/candleAggregator.js +154 -0
- package/src/live/engine/liveEngine.js +694 -0
- package/src/live/engine/paperEngine.js +453 -0
- package/src/live/engine/riskManager.js +185 -0
- package/src/live/engine/stateManager.js +112 -0
- package/src/live/events.js +48 -0
- package/src/live/feed/brokerFeed.js +35 -0
- package/src/live/feed/interface.js +28 -0
- package/src/live/feed/pollingFeed.js +105 -0
- package/src/live/index.js +27 -0
- package/src/live/logger.js +82 -0
- package/src/live/orchestrator.js +133 -0
- package/src/live/storage/interface.js +36 -0
- package/src/live/storage/jsonFileStorage.js +112 -0
- package/src/metrics/buildMetrics.js +18 -41
- package/src/reporting/exportBacktestArtifacts.js +1 -4
- package/src/reporting/exportTradesCsv.js +2 -7
- package/src/reporting/renderHtmlReport.js +8 -13
- package/src/utils/indicators.js +1 -2
- package/src/utils/positionSizing.js +16 -2
- package/src/utils/time.js +4 -12
- package/templates/report.html +23 -9
- package/templates/report.js +83 -69
- package/types/index.d.ts +21 -3
- 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
|
+
});
|