tradelab 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/README.md +185 -388
- package/dist/cjs/index.cjs +31 -9
- package/dist/cjs/live.cjs +409 -7
- package/docs/README.md +32 -66
- package/docs/api-reference.md +269 -144
- package/docs/backtest-engine.md +167 -321
- package/docs/data-reporting-cli.md +114 -156
- package/docs/examples.md +6 -6
- package/docs/live-trading.md +254 -134
- package/docs/mcp.md +244 -23
- package/docs/research.md +99 -45
- package/examples/mcpLiveTrading.js +77 -0
- package/package.json +11 -3
- package/src/engine/optimize.js +25 -1
- package/src/engine/portfolio.js +4 -1
- package/src/live/dashboard/server.js +67 -8
- package/src/live/engine/paperEngine.js +5 -0
- package/src/live/index.js +2 -0
- package/src/live/session.js +402 -0
- package/src/mcp/liveTools.js +179 -0
- package/src/mcp/schemas.js +119 -0
- package/src/mcp/server.js +5 -1
- package/src/mcp/tools.js +125 -2
- package/templates/dashboard.html +595 -108
- package/types/index.d.ts +25 -0
- package/types/live.d.ts +99 -0
- package/types/mcp.d.ts +17 -0
- package/docs/superpowers/plans/2026-00-overview.md +0 -101
- package/docs/superpowers/plans/2026-01-metrics-correctness.md +0 -873
- package/docs/superpowers/plans/2026-02-indicator-library.md +0 -677
- package/docs/superpowers/plans/2026-03-overfitting-toolkit.md +0 -882
- package/docs/superpowers/plans/2026-04-async-signals-seeding.md +0 -981
- package/docs/superpowers/plans/2026-05-mcp-server.md +0 -758
- package/docs/superpowers/plans/2026-06-parallel-param-sweep.md +0 -508
- package/docs/superpowers/plans/2026-07-funding-carry-costs.md +0 -535
- package/docs/superpowers/plans/2026-08-live-dashboard.md +0 -547
- package/docs/superpowers/plans/HANDOFF.md +0 -88
package/dist/cjs/index.cjs
CHANGED
|
@@ -3194,6 +3194,7 @@ function forceExitAll(runners, time) {
|
|
|
3194
3194
|
function backtestPortfolio({
|
|
3195
3195
|
systems = [],
|
|
3196
3196
|
equity = 1e4,
|
|
3197
|
+
interval,
|
|
3197
3198
|
allocation = "equal",
|
|
3198
3199
|
collectEqSeries = true,
|
|
3199
3200
|
collectReplay = false,
|
|
@@ -3327,17 +3328,19 @@ function backtestPortfolio({
|
|
|
3327
3328
|
const replay = combineReplay(systemResults, eqSeries, collectReplay);
|
|
3328
3329
|
const allCandles = systems.flatMap((system) => system.candles || []);
|
|
3329
3330
|
const orderedCandles = [...allCandles].sort((left, right) => left.time - right.time);
|
|
3331
|
+
const metricsInterval = interval ?? systems[0]?.interval;
|
|
3330
3332
|
const metrics = buildMetrics({
|
|
3331
3333
|
closed: trades,
|
|
3332
3334
|
equityStart: equity,
|
|
3333
3335
|
equityFinal: eqSeries.length ? eqSeries[eqSeries.length - 1].equity : equity,
|
|
3334
3336
|
candles: orderedCandles,
|
|
3335
3337
|
estBarMs: estimateBarMs(orderedCandles),
|
|
3336
|
-
eqSeries
|
|
3338
|
+
eqSeries,
|
|
3339
|
+
interval: metricsInterval
|
|
3337
3340
|
});
|
|
3338
3341
|
return {
|
|
3339
3342
|
symbol: "PORTFOLIO",
|
|
3340
|
-
interval:
|
|
3343
|
+
interval: metricsInterval,
|
|
3341
3344
|
range: void 0,
|
|
3342
3345
|
trades,
|
|
3343
3346
|
positions,
|
|
@@ -3632,7 +3635,9 @@ function walkForwardOptimize({
|
|
|
3632
3635
|
// src/engine/optimize.js
|
|
3633
3636
|
var import_node_worker_threads = require("node:worker_threads");
|
|
3634
3637
|
var import_node_os = __toESM(require("node:os"), 1);
|
|
3635
|
-
var
|
|
3638
|
+
var import_node_fs = require("node:fs");
|
|
3639
|
+
var import_node_path = __toESM(require("node:path"), 1);
|
|
3640
|
+
var import_node_url = require("node:url");
|
|
3636
3641
|
function defaultConcurrency() {
|
|
3637
3642
|
return Math.max(1, (import_node_os.default.cpus()?.length ?? 2) - 1);
|
|
3638
3643
|
}
|
|
@@ -3640,6 +3645,23 @@ function scoreValue(metrics, scoreBy) {
|
|
|
3640
3645
|
const v = metrics?.[scoreBy];
|
|
3641
3646
|
return Number.isFinite(v) ? v : -Infinity;
|
|
3642
3647
|
}
|
|
3648
|
+
function callerModuleDir() {
|
|
3649
|
+
const stack = new Error().stack || "";
|
|
3650
|
+
const lines = stack.split("\n").slice(1);
|
|
3651
|
+
const match = lines.map((line) => line.match(/(?:\()?(file:\/\/\/[^\s)]+|\/[^\s)]+):\d+:\d+/)).find(Boolean);
|
|
3652
|
+
if (!match) return process.cwd();
|
|
3653
|
+
const filePath = match[1].startsWith("file://") ? (0, import_node_url.fileURLToPath)(match[1]) : match[1];
|
|
3654
|
+
return import_node_path.default.dirname(filePath);
|
|
3655
|
+
}
|
|
3656
|
+
function workerUrl() {
|
|
3657
|
+
const here = callerModuleDir();
|
|
3658
|
+
const candidates = [
|
|
3659
|
+
import_node_path.default.join(here, "optimizeWorker.js"),
|
|
3660
|
+
import_node_path.default.join(here, "..", "..", "src", "engine", "optimizeWorker.js"),
|
|
3661
|
+
import_node_path.default.join(process.cwd(), "src", "engine", "optimizeWorker.js")
|
|
3662
|
+
];
|
|
3663
|
+
return (0, import_node_url.pathToFileURL)(candidates.find((candidate) => (0, import_node_fs.existsSync)(candidate)) || candidates[0]);
|
|
3664
|
+
}
|
|
3643
3665
|
function optimize({
|
|
3644
3666
|
candles,
|
|
3645
3667
|
signalModulePath,
|
|
@@ -3682,7 +3704,7 @@ function optimize({
|
|
|
3682
3704
|
worker.postMessage({ type: "run", index, params: parameterSets[index] });
|
|
3683
3705
|
};
|
|
3684
3706
|
for (let i = 0; i < poolSize; i += 1) {
|
|
3685
|
-
const worker = new import_node_worker_threads.Worker(
|
|
3707
|
+
const worker = new import_node_worker_threads.Worker(workerUrl(), {
|
|
3686
3708
|
workerData: { candles, signalModulePath, interval, backtestOptions }
|
|
3687
3709
|
});
|
|
3688
3710
|
workers.push(worker);
|
|
@@ -3952,22 +3974,22 @@ function monteCarlo({
|
|
|
3952
3974
|
const drawdowns = [];
|
|
3953
3975
|
const pathSamples = Array.from({ length: n + 1 }, () => []);
|
|
3954
3976
|
for (let it = 0; it < iterations; it += 1) {
|
|
3955
|
-
const
|
|
3977
|
+
const path7 = [equityStart];
|
|
3956
3978
|
let equity = equityStart;
|
|
3957
3979
|
let filled = 0;
|
|
3958
3980
|
while (filled < n) {
|
|
3959
3981
|
const start = randInt(rng, n);
|
|
3960
3982
|
for (let k = 0; k < block && filled < n; k += 1) {
|
|
3961
3983
|
equity += tradePnls[(start + k) % n];
|
|
3962
|
-
|
|
3984
|
+
path7.push(equity);
|
|
3963
3985
|
filled += 1;
|
|
3964
3986
|
}
|
|
3965
3987
|
}
|
|
3966
|
-
for (let step = 0; step <
|
|
3967
|
-
pathSamples[step].push(
|
|
3988
|
+
for (let step = 0; step < path7.length; step += 1) {
|
|
3989
|
+
pathSamples[step].push(path7[step]);
|
|
3968
3990
|
}
|
|
3969
3991
|
finals.push(equity);
|
|
3970
|
-
drawdowns.push(maxDrawdownOf(
|
|
3992
|
+
drawdowns.push(maxDrawdownOf(path7));
|
|
3971
3993
|
}
|
|
3972
3994
|
const sortedFinals = [...finals].sort((a, b) => a - b);
|
|
3973
3995
|
const sortedDd = [...drawdowns].sort((a, b) => a - b);
|
package/dist/cjs/live.cjs
CHANGED
|
@@ -48,8 +48,10 @@ __export(index_exports, {
|
|
|
48
48
|
PaperEngine: () => PaperEngine,
|
|
49
49
|
PollingFeed: () => PollingFeed,
|
|
50
50
|
RiskManager: () => RiskManager,
|
|
51
|
+
SessionManager: () => SessionManager,
|
|
51
52
|
StateManager: () => StateManager,
|
|
52
53
|
StorageProvider: () => StorageProvider,
|
|
54
|
+
TradingSession: () => TradingSession,
|
|
53
55
|
createAlpacaBroker: () => createAlpacaBroker,
|
|
54
56
|
createBinanceBroker: () => createBinanceBroker,
|
|
55
57
|
createBrokerFeed: () => createBrokerFeed,
|
|
@@ -66,6 +68,7 @@ __export(index_exports, {
|
|
|
66
68
|
createPaperEngine: () => createPaperEngine,
|
|
67
69
|
createPollingFeed: () => createPollingFeed,
|
|
68
70
|
createRiskManager: () => createRiskManager,
|
|
71
|
+
createSessionManager: () => createSessionManager,
|
|
69
72
|
createStateManager: () => createStateManager
|
|
70
73
|
});
|
|
71
74
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -2483,6 +2486,7 @@ var PaperEngine = class extends BrokerAdapter {
|
|
|
2483
2486
|
});
|
|
2484
2487
|
const orders = [...this.openOrders.values()].filter((order) => order.symbol === symbol);
|
|
2485
2488
|
for (const order of orders) {
|
|
2489
|
+
if (!this.openOrders.has(order.orderId)) continue;
|
|
2486
2490
|
if (order.type === "limit") {
|
|
2487
2491
|
if (this._touchesLimit(order, normalizedBar)) {
|
|
2488
2492
|
this._fillOrder(order, order.limitPrice, "limit", normalizedBar.time);
|
|
@@ -3317,7 +3321,6 @@ var import_node_http = __toESM(require("node:http"), 1);
|
|
|
3317
3321
|
var import_node_fs2 = require("node:fs");
|
|
3318
3322
|
var import_node_path2 = __toESM(require("node:path"), 1);
|
|
3319
3323
|
var import_node_url4 = require("node:url");
|
|
3320
|
-
var import_meta = {};
|
|
3321
3324
|
var FALLBACK_HTML = `<!doctype html>
|
|
3322
3325
|
<html lang="en">
|
|
3323
3326
|
<head>
|
|
@@ -3336,12 +3339,23 @@ var FALLBACK_HTML = `<!doctype html>
|
|
|
3336
3339
|
</script>
|
|
3337
3340
|
</body>
|
|
3338
3341
|
</html>`;
|
|
3342
|
+
function callerModuleDir() {
|
|
3343
|
+
const stack = new Error().stack || "";
|
|
3344
|
+
const lines = stack.split("\n").slice(1);
|
|
3345
|
+
const match = lines.map((line) => line.match(/(?:\()?(file:\/\/\/[^\s)]+|\/[^\s)]+):\d+:\d+/)).find(Boolean);
|
|
3346
|
+
if (!match) return process.cwd();
|
|
3347
|
+
const filePath = match[1].startsWith("file://") ? (0, import_node_url4.fileURLToPath)(match[1]) : match[1];
|
|
3348
|
+
return import_node_path2.default.dirname(filePath);
|
|
3349
|
+
}
|
|
3339
3350
|
function readDashboardHtml() {
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3351
|
+
const here = callerModuleDir();
|
|
3352
|
+
const candidates = [
|
|
3353
|
+
import_node_path2.default.join(here, "..", "..", "..", "templates", "dashboard.html"),
|
|
3354
|
+
import_node_path2.default.join(here, "..", "..", "templates", "dashboard.html"),
|
|
3355
|
+
import_node_path2.default.join(process.cwd(), "templates", "dashboard.html")
|
|
3356
|
+
];
|
|
3357
|
+
const htmlPath = candidates.find((candidate) => (0, import_node_fs2.existsSync)(candidate));
|
|
3358
|
+
if (htmlPath) return (0, import_node_fs2.readFileSync)(htmlPath, "utf8");
|
|
3345
3359
|
try {
|
|
3346
3360
|
return (0, import_node_fs2.readFileSync)(import_node_path2.default.join(process.cwd(), "templates", "dashboard.html"), "utf8");
|
|
3347
3361
|
} catch {
|
|
@@ -3363,7 +3377,7 @@ function createDashboardServer({ source, port = 4317, maxBuffer = 200 }) {
|
|
|
3363
3377
|
`;
|
|
3364
3378
|
for (const res of clients) res.write(frame);
|
|
3365
3379
|
});
|
|
3366
|
-
const server = import_node_http.default.createServer((req, res) => {
|
|
3380
|
+
const server = import_node_http.default.createServer(async (req, res) => {
|
|
3367
3381
|
const url = (req.url || "/").split("?")[0];
|
|
3368
3382
|
if (url === "/") {
|
|
3369
3383
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
@@ -3371,11 +3385,52 @@ function createDashboardServer({ source, port = 4317, maxBuffer = 200 }) {
|
|
|
3371
3385
|
return;
|
|
3372
3386
|
}
|
|
3373
3387
|
if (url === "/state") {
|
|
3388
|
+
if (typeof source.refresh === "function") await source.refresh().catch(() => {
|
|
3389
|
+
});
|
|
3374
3390
|
const status = typeof source.getStatus === "function" ? source.getStatus() : {};
|
|
3375
3391
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3376
3392
|
res.end(JSON.stringify(status));
|
|
3377
3393
|
return;
|
|
3378
3394
|
}
|
|
3395
|
+
if (url === "/command" && req.method === "POST") {
|
|
3396
|
+
const WHITELIST = {
|
|
3397
|
+
flatten: "flatten",
|
|
3398
|
+
stop: "stop",
|
|
3399
|
+
closePosition: "closePosition",
|
|
3400
|
+
cancelOrder: "cancelOrder"
|
|
3401
|
+
};
|
|
3402
|
+
let body = "";
|
|
3403
|
+
req.on("data", (c) => body += c);
|
|
3404
|
+
req.on("end", async () => {
|
|
3405
|
+
let cmd;
|
|
3406
|
+
try {
|
|
3407
|
+
cmd = JSON.parse(body || "{}");
|
|
3408
|
+
} catch {
|
|
3409
|
+
cmd = {};
|
|
3410
|
+
}
|
|
3411
|
+
const method = WHITELIST[cmd.type];
|
|
3412
|
+
if (!method || typeof source[method] !== "function") {
|
|
3413
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
3414
|
+
res.end(JSON.stringify({ ok: false, error: `unsupported command "${cmd.type}"` }));
|
|
3415
|
+
return;
|
|
3416
|
+
}
|
|
3417
|
+
try {
|
|
3418
|
+
const arg = cmd.type === "closePosition" ? cmd.symbol : cmd.type === "cancelOrder" ? cmd.orderId : void 0;
|
|
3419
|
+
await source[method](arg);
|
|
3420
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3421
|
+
res.end(JSON.stringify({ ok: true }));
|
|
3422
|
+
} catch (error) {
|
|
3423
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
3424
|
+
res.end(
|
|
3425
|
+
JSON.stringify({
|
|
3426
|
+
ok: false,
|
|
3427
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3428
|
+
})
|
|
3429
|
+
);
|
|
3430
|
+
}
|
|
3431
|
+
});
|
|
3432
|
+
return;
|
|
3433
|
+
}
|
|
3379
3434
|
if (url === "/events") {
|
|
3380
3435
|
res.writeHead(200, {
|
|
3381
3436
|
"Content-Type": "text/event-stream",
|
|
@@ -3417,6 +3472,350 @@ function createDashboardServer({ source, port = 4317, maxBuffer = 200 }) {
|
|
|
3417
3472
|
server
|
|
3418
3473
|
};
|
|
3419
3474
|
}
|
|
3475
|
+
|
|
3476
|
+
// src/live/session.js
|
|
3477
|
+
function oppositeSide2(side) {
|
|
3478
|
+
return side === "long" || side === "buy" ? "sell" : "buy";
|
|
3479
|
+
}
|
|
3480
|
+
function toBrokerSide(side) {
|
|
3481
|
+
return side === "long" || side === "buy" ? "buy" : "sell";
|
|
3482
|
+
}
|
|
3483
|
+
var TradingSession = class _TradingSession {
|
|
3484
|
+
constructor({
|
|
3485
|
+
id,
|
|
3486
|
+
symbol,
|
|
3487
|
+
interval = "1m",
|
|
3488
|
+
broker,
|
|
3489
|
+
mode = "paper",
|
|
3490
|
+
equity = 1e4,
|
|
3491
|
+
riskPct = 1,
|
|
3492
|
+
maxDailyLossPct = 0,
|
|
3493
|
+
maxPositionPct = 1,
|
|
3494
|
+
qtyStep = 1e-3,
|
|
3495
|
+
minQty = 1e-3,
|
|
3496
|
+
maxLeverage = 2,
|
|
3497
|
+
confirmLive = false,
|
|
3498
|
+
eventBus
|
|
3499
|
+
} = {}) {
|
|
3500
|
+
if (mode === "live" && (!_TradingSession.liveAllowed() || !confirmLive)) {
|
|
3501
|
+
throw new Error(
|
|
3502
|
+
"live trading is gated: set TRADELAB_ALLOW_LIVE=true and pass confirmLive:true with a credentialed broker"
|
|
3503
|
+
);
|
|
3504
|
+
}
|
|
3505
|
+
if (!broker) throw new Error("TradingSession requires a broker (PaperEngine by default)");
|
|
3506
|
+
if (!symbol) throw new Error("TradingSession requires a symbol");
|
|
3507
|
+
this.id = id || `${symbol}-${interval}`;
|
|
3508
|
+
this.symbol = symbol;
|
|
3509
|
+
this.interval = interval;
|
|
3510
|
+
this.broker = broker;
|
|
3511
|
+
this.mode = mode;
|
|
3512
|
+
this.equity = equity;
|
|
3513
|
+
this._startEquity = equity;
|
|
3514
|
+
this.riskPct = riskPct;
|
|
3515
|
+
this.maxPositionPct = maxPositionPct;
|
|
3516
|
+
this.qtyStep = qtyStep;
|
|
3517
|
+
this.minQty = minQty;
|
|
3518
|
+
this.maxLeverage = maxLeverage;
|
|
3519
|
+
this.eventBus = eventBus || new EventBus();
|
|
3520
|
+
this.riskManager = new RiskManager({ maxDailyLossPct, maxDrawdownPct: 0 });
|
|
3521
|
+
this.lastPrice = null;
|
|
3522
|
+
this.running = false;
|
|
3523
|
+
this.events = [];
|
|
3524
|
+
this.brackets = /* @__PURE__ */ new Map();
|
|
3525
|
+
this._pendingBracket = null;
|
|
3526
|
+
this._cachedPositions = [];
|
|
3527
|
+
this._cachedOpenOrders = [];
|
|
3528
|
+
this.candleBuffer = [];
|
|
3529
|
+
this._strategy = null;
|
|
3530
|
+
this._wireBrokerEvents();
|
|
3531
|
+
}
|
|
3532
|
+
static liveAllowed() {
|
|
3533
|
+
return process.env.TRADELAB_ALLOW_LIVE === "true";
|
|
3534
|
+
}
|
|
3535
|
+
_record(event, payload) {
|
|
3536
|
+
const msg = { event, payload, t: Date.now() };
|
|
3537
|
+
this.events.push(msg);
|
|
3538
|
+
if (this.events.length > 500) this.events.shift();
|
|
3539
|
+
this.eventBus.emitEvent(event, { sessionId: this.id, symbol: this.symbol, ...payload });
|
|
3540
|
+
}
|
|
3541
|
+
_wireBrokerEvents() {
|
|
3542
|
+
this.broker.on?.("order:filled", (order) => this._onBrokerFillSync(order));
|
|
3543
|
+
this.broker.on?.("order:submitted", (order) => this._record("order:submitted", order));
|
|
3544
|
+
this.broker.on?.("order:canceled", (order) => this._record("order:canceled", order));
|
|
3545
|
+
this.broker.on?.("equity:update", (acct) => this._record("equity:update", acct));
|
|
3546
|
+
}
|
|
3547
|
+
// Sync event handler — fire-and-forget async OCO work via a stored promise
|
|
3548
|
+
_onBrokerFillSync(order) {
|
|
3549
|
+
this._record("order:filled", order);
|
|
3550
|
+
if (this._pendingBracket && String(order.clientOrderId || "").includes("-entry-")) {
|
|
3551
|
+
const staged = this._pendingBracket;
|
|
3552
|
+
this._pendingBracket = null;
|
|
3553
|
+
this._pendingCancelPromise = Promise.resolve(
|
|
3554
|
+
this._attachBracket({ ...staged, receipt: order })
|
|
3555
|
+
);
|
|
3556
|
+
return;
|
|
3557
|
+
}
|
|
3558
|
+
const bracket = this.brackets.get(this.symbol);
|
|
3559
|
+
if (bracket && (order.orderId === bracket.stopId || order.orderId === bracket.targetId)) {
|
|
3560
|
+
const siblingId = order.orderId === bracket.stopId ? bracket.targetId : bracket.stopId;
|
|
3561
|
+
this._pendingCancelPromise = (async () => {
|
|
3562
|
+
if (siblingId) await this.broker.cancelOrder(siblingId).catch(() => {
|
|
3563
|
+
});
|
|
3564
|
+
this.brackets.delete(this.symbol);
|
|
3565
|
+
this._record("position:closed", { reason: order.orderId === bracket.stopId ? "SL" : "TP" });
|
|
3566
|
+
})();
|
|
3567
|
+
}
|
|
3568
|
+
}
|
|
3569
|
+
async start() {
|
|
3570
|
+
if (!this.broker.isConnected?.()) await this.broker.connect?.({});
|
|
3571
|
+
const acct = await this.broker.getAccount?.().catch(() => null);
|
|
3572
|
+
if (Number.isFinite(acct?.equity)) {
|
|
3573
|
+
this.equity = acct.equity;
|
|
3574
|
+
this._startEquity = acct.equity;
|
|
3575
|
+
}
|
|
3576
|
+
this.riskManager.initialize(this.equity, Date.now());
|
|
3577
|
+
this.running = true;
|
|
3578
|
+
this._record("connected", { mode: this.mode });
|
|
3579
|
+
}
|
|
3580
|
+
async stop({ flatten = false } = {}) {
|
|
3581
|
+
if (flatten) await this.flatten();
|
|
3582
|
+
this.running = false;
|
|
3583
|
+
this._record("shutdown", {});
|
|
3584
|
+
}
|
|
3585
|
+
async pushBar(b) {
|
|
3586
|
+
this.lastPrice = b.close;
|
|
3587
|
+
if (typeof this.broker.simulateBar === "function") {
|
|
3588
|
+
await this.broker.simulateBar(this.symbol, this.interval, b);
|
|
3589
|
+
}
|
|
3590
|
+
if (this._pendingCancelPromise) {
|
|
3591
|
+
await this._pendingCancelPromise;
|
|
3592
|
+
this._pendingCancelPromise = null;
|
|
3593
|
+
}
|
|
3594
|
+
this.candleBuffer.push(b);
|
|
3595
|
+
if (this.candleBuffer.length > 200) this.candleBuffer.shift();
|
|
3596
|
+
this._record("bar", { close: b.close, time: b.time });
|
|
3597
|
+
await this._syncEquityAndRisk();
|
|
3598
|
+
await this.refresh();
|
|
3599
|
+
}
|
|
3600
|
+
_riskHalted() {
|
|
3601
|
+
const state = this.riskManager.getState?.() || {};
|
|
3602
|
+
return Boolean(state.halted);
|
|
3603
|
+
}
|
|
3604
|
+
async placeOrder({ side, type = "market", qty, riskPct, stop, target, rr, limitPrice } = {}) {
|
|
3605
|
+
if (!this.running) throw new Error("session not started");
|
|
3606
|
+
if (this._riskHalted()) throw new Error("session is risk-halted for the day");
|
|
3607
|
+
const entryRef = type === "limit" ? limitPrice : this.lastPrice;
|
|
3608
|
+
if (!Number.isFinite(entryRef)) throw new Error("no price available; pushBar() a price first");
|
|
3609
|
+
let size = qty;
|
|
3610
|
+
if (!Number.isFinite(size)) {
|
|
3611
|
+
const fraction = Number.isFinite(riskPct) ? riskPct / 100 : this.riskPct / 100;
|
|
3612
|
+
if (!Number.isFinite(stop)) throw new Error("risk-based sizing requires a stop");
|
|
3613
|
+
size = calculatePositionSize({
|
|
3614
|
+
equity: this.equity,
|
|
3615
|
+
entry: entryRef,
|
|
3616
|
+
stop,
|
|
3617
|
+
riskFraction: fraction,
|
|
3618
|
+
qtyStep: this.qtyStep,
|
|
3619
|
+
minQty: this.minQty,
|
|
3620
|
+
maxLeverage: this.maxLeverage
|
|
3621
|
+
});
|
|
3622
|
+
}
|
|
3623
|
+
size = roundStep(size, this.qtyStep);
|
|
3624
|
+
if (!(size >= this.minQty)) throw new Error(`sized below minQty (${size})`);
|
|
3625
|
+
const receipt = await this.broker.submitOrder({
|
|
3626
|
+
symbol: this.symbol,
|
|
3627
|
+
side: toBrokerSide(side),
|
|
3628
|
+
type,
|
|
3629
|
+
qty: size,
|
|
3630
|
+
limitPrice: type === "limit" ? limitPrice : void 0,
|
|
3631
|
+
clientOrderId: `${this.id}-entry-${Date.now()}`
|
|
3632
|
+
});
|
|
3633
|
+
if (Number.isFinite(stop) || Number.isFinite(target) || Number.isFinite(rr)) {
|
|
3634
|
+
if (receipt.status === "filled") {
|
|
3635
|
+
await this._attachBracket({ side, size, stop, target, rr, entryRef, receipt });
|
|
3636
|
+
} else {
|
|
3637
|
+
this._pendingBracket = { side, size, stop, target, rr, entryRef };
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
await this.refresh();
|
|
3641
|
+
return receipt;
|
|
3642
|
+
}
|
|
3643
|
+
async _attachBracket({ side, size, stop, target, rr, entryRef, receipt }) {
|
|
3644
|
+
const entryFill = receipt?.avgFillPrice ?? entryRef;
|
|
3645
|
+
const risk = Number.isFinite(stop) ? Math.abs(entryFill - stop) : null;
|
|
3646
|
+
const targetPrice = Number.isFinite(target) ? target : Number.isFinite(rr) && risk ? side === "long" || side === "buy" ? entryFill + rr * risk : entryFill - rr * risk : null;
|
|
3647
|
+
const exitSide = oppositeSide2(side);
|
|
3648
|
+
const bracket = {};
|
|
3649
|
+
if (Number.isFinite(stop)) {
|
|
3650
|
+
const stopOrder = await this.broker.submitOrder({
|
|
3651
|
+
symbol: this.symbol,
|
|
3652
|
+
side: exitSide,
|
|
3653
|
+
type: "stop",
|
|
3654
|
+
qty: size,
|
|
3655
|
+
stopPrice: stop,
|
|
3656
|
+
clientOrderId: `${this.id}-stop-${Date.now()}`
|
|
3657
|
+
});
|
|
3658
|
+
bracket.stopId = stopOrder.orderId;
|
|
3659
|
+
}
|
|
3660
|
+
if (Number.isFinite(targetPrice)) {
|
|
3661
|
+
const tgtOrder = await this.broker.submitOrder({
|
|
3662
|
+
symbol: this.symbol,
|
|
3663
|
+
side: exitSide,
|
|
3664
|
+
type: "limit",
|
|
3665
|
+
qty: size,
|
|
3666
|
+
limitPrice: targetPrice,
|
|
3667
|
+
clientOrderId: `${this.id}-target-${Date.now()}`
|
|
3668
|
+
});
|
|
3669
|
+
bracket.targetId = tgtOrder.orderId;
|
|
3670
|
+
}
|
|
3671
|
+
this.brackets.set(this.symbol, bracket);
|
|
3672
|
+
}
|
|
3673
|
+
async _syncEquityAndRisk() {
|
|
3674
|
+
const acct = await this.broker.getAccount?.().catch(() => null);
|
|
3675
|
+
if (!Number.isFinite(acct?.equity)) return;
|
|
3676
|
+
const prevEquity = this.equity;
|
|
3677
|
+
this.equity = acct.equity;
|
|
3678
|
+
const pnlDelta = this.equity - prevEquity;
|
|
3679
|
+
if (pnlDelta !== 0) {
|
|
3680
|
+
this.riskManager.recordTrade({ pnl: pnlDelta, timeMs: Date.now(), equity: this.equity });
|
|
3681
|
+
} else {
|
|
3682
|
+
this.riskManager.update({ timeMs: Date.now(), equity: this.equity });
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
async closePosition(symbol = this.symbol) {
|
|
3686
|
+
const positions = await this.broker.getPositions();
|
|
3687
|
+
const pos = positions.find((p) => p.symbol === symbol);
|
|
3688
|
+
if (!pos) return null;
|
|
3689
|
+
const bracket = this.brackets.get(symbol);
|
|
3690
|
+
if (bracket) {
|
|
3691
|
+
for (const id of [bracket.stopId, bracket.targetId]) {
|
|
3692
|
+
if (id) await this.broker.cancelOrder(id).catch(() => {
|
|
3693
|
+
});
|
|
3694
|
+
}
|
|
3695
|
+
this.brackets.delete(symbol);
|
|
3696
|
+
}
|
|
3697
|
+
const receipt = await this.broker.submitOrder({
|
|
3698
|
+
symbol,
|
|
3699
|
+
side: oppositeSide2(pos.side),
|
|
3700
|
+
type: "market",
|
|
3701
|
+
qty: pos.qty,
|
|
3702
|
+
clientOrderId: `${this.id}-close-${Date.now()}`
|
|
3703
|
+
});
|
|
3704
|
+
await this._syncEquityAndRisk();
|
|
3705
|
+
await this.refresh();
|
|
3706
|
+
return receipt;
|
|
3707
|
+
}
|
|
3708
|
+
async flatten() {
|
|
3709
|
+
const positions = await this.broker.getPositions();
|
|
3710
|
+
for (const p of positions) await this.closePosition(p.symbol);
|
|
3711
|
+
const open = await this.broker.getOpenOrders?.().catch(() => []) ?? [];
|
|
3712
|
+
for (const o of open) await this.broker.cancelOrder(o.orderId).catch(() => {
|
|
3713
|
+
});
|
|
3714
|
+
await this.refresh();
|
|
3715
|
+
}
|
|
3716
|
+
async cancelOrder(orderId) {
|
|
3717
|
+
await this.broker.cancelOrder(orderId);
|
|
3718
|
+
await this.refresh();
|
|
3719
|
+
}
|
|
3720
|
+
async getAccount() {
|
|
3721
|
+
return this.broker.getAccount();
|
|
3722
|
+
}
|
|
3723
|
+
async getPositions() {
|
|
3724
|
+
return this.broker.getPositions();
|
|
3725
|
+
}
|
|
3726
|
+
recentEvents(limit = 50) {
|
|
3727
|
+
return this.events.slice(-limit);
|
|
3728
|
+
}
|
|
3729
|
+
getStatus() {
|
|
3730
|
+
const risk = this.riskManager.getState?.() || {};
|
|
3731
|
+
return {
|
|
3732
|
+
id: this.id,
|
|
3733
|
+
symbol: this.symbol,
|
|
3734
|
+
interval: this.interval,
|
|
3735
|
+
mode: this.mode,
|
|
3736
|
+
running: this.running,
|
|
3737
|
+
equity: this.equity,
|
|
3738
|
+
dayPnl: risk.dayPnl ?? 0,
|
|
3739
|
+
lastPrice: this.lastPrice,
|
|
3740
|
+
positions: this._cachedPositions ?? [],
|
|
3741
|
+
openOrders: this._cachedOpenOrders ?? [],
|
|
3742
|
+
risk: { halted: Boolean(risk.halted), ...risk }
|
|
3743
|
+
};
|
|
3744
|
+
}
|
|
3745
|
+
/** Refresh sync caches used by getStatus() */
|
|
3746
|
+
async refresh() {
|
|
3747
|
+
if (this._pendingCancelPromise) {
|
|
3748
|
+
await this._pendingCancelPromise;
|
|
3749
|
+
this._pendingCancelPromise = null;
|
|
3750
|
+
}
|
|
3751
|
+
this._cachedPositions = await this.broker.getPositions().catch(() => []);
|
|
3752
|
+
this._cachedOpenOrders = await this.broker.getOpenOrders?.().catch(() => []) ?? [];
|
|
3753
|
+
const acct = await this.broker.getAccount?.().catch(() => null);
|
|
3754
|
+
if (Number.isFinite(acct?.equity)) this.equity = acct.equity;
|
|
3755
|
+
return this.getStatus();
|
|
3756
|
+
}
|
|
3757
|
+
};
|
|
3758
|
+
var SessionManager = class {
|
|
3759
|
+
constructor({ brokerFactory } = {}) {
|
|
3760
|
+
this.sessions = /* @__PURE__ */ new Map();
|
|
3761
|
+
this.brokerFactory = brokerFactory;
|
|
3762
|
+
}
|
|
3763
|
+
async create({
|
|
3764
|
+
id,
|
|
3765
|
+
mode = "paper",
|
|
3766
|
+
symbol,
|
|
3767
|
+
interval = "1m",
|
|
3768
|
+
equity = 1e4,
|
|
3769
|
+
confirmLive = false,
|
|
3770
|
+
broker,
|
|
3771
|
+
...rest
|
|
3772
|
+
} = {}) {
|
|
3773
|
+
if (this.sessions.has(id)) throw new Error(`session "${id}" already exists`);
|
|
3774
|
+
let resolvedBroker = broker;
|
|
3775
|
+
if (mode === "live") {
|
|
3776
|
+
if (!TradingSession.liveAllowed() || !confirmLive) {
|
|
3777
|
+
throw new Error("live mode requires TRADELAB_ALLOW_LIVE=true and confirmLive:true");
|
|
3778
|
+
}
|
|
3779
|
+
if (!resolvedBroker && this.brokerFactory) {
|
|
3780
|
+
resolvedBroker = this.brokerFactory({ symbol, ...rest });
|
|
3781
|
+
}
|
|
3782
|
+
if (!resolvedBroker) throw new Error("live mode requires a credentialed broker");
|
|
3783
|
+
}
|
|
3784
|
+
if (!resolvedBroker) resolvedBroker = new PaperEngine({ equity });
|
|
3785
|
+
const session = new TradingSession({
|
|
3786
|
+
id,
|
|
3787
|
+
symbol,
|
|
3788
|
+
interval,
|
|
3789
|
+
broker: resolvedBroker,
|
|
3790
|
+
mode,
|
|
3791
|
+
equity,
|
|
3792
|
+
confirmLive,
|
|
3793
|
+
...rest
|
|
3794
|
+
});
|
|
3795
|
+
await session.start();
|
|
3796
|
+
this.sessions.set(session.id, session);
|
|
3797
|
+
return session;
|
|
3798
|
+
}
|
|
3799
|
+
get(id) {
|
|
3800
|
+
return this.sessions.get(id) ?? null;
|
|
3801
|
+
}
|
|
3802
|
+
list() {
|
|
3803
|
+
return [...this.sessions.values()];
|
|
3804
|
+
}
|
|
3805
|
+
async remove(id, { flatten = true } = {}) {
|
|
3806
|
+
const s = this.sessions.get(id);
|
|
3807
|
+
if (!s) return;
|
|
3808
|
+
await s.stop({ flatten });
|
|
3809
|
+
this.sessions.delete(id);
|
|
3810
|
+
}
|
|
3811
|
+
async haltAll() {
|
|
3812
|
+
for (const s of this.sessions.values()) await s.stop({ flatten: true });
|
|
3813
|
+
this.sessions.clear();
|
|
3814
|
+
}
|
|
3815
|
+
};
|
|
3816
|
+
function createSessionManager(opts) {
|
|
3817
|
+
return new SessionManager(opts);
|
|
3818
|
+
}
|
|
3420
3819
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3421
3820
|
0 && (module.exports = {
|
|
3422
3821
|
AlpacaBroker,
|
|
@@ -3437,8 +3836,10 @@ function createDashboardServer({ source, port = 4317, maxBuffer = 200 }) {
|
|
|
3437
3836
|
PaperEngine,
|
|
3438
3837
|
PollingFeed,
|
|
3439
3838
|
RiskManager,
|
|
3839
|
+
SessionManager,
|
|
3440
3840
|
StateManager,
|
|
3441
3841
|
StorageProvider,
|
|
3842
|
+
TradingSession,
|
|
3442
3843
|
createAlpacaBroker,
|
|
3443
3844
|
createBinanceBroker,
|
|
3444
3845
|
createBrokerFeed,
|
|
@@ -3455,5 +3856,6 @@ function createDashboardServer({ source, port = 4317, maxBuffer = 200 }) {
|
|
|
3455
3856
|
createPaperEngine,
|
|
3456
3857
|
createPollingFeed,
|
|
3457
3858
|
createRiskManager,
|
|
3859
|
+
createSessionManager,
|
|
3458
3860
|
createStateManager
|
|
3459
3861
|
});
|