tradelab 1.2.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +14 -1
- package/dist/cjs/index.cjs +8 -3
- package/dist/cjs/live.cjs +52 -15
- package/docs/live-trading.md +1 -1
- package/docs/mcp.md +1 -1
- package/package.json +1 -1
- package/src/engine/portfolio.js +2 -1
- package/src/live/engine/paperEngine.js +16 -11
- package/src/live/session.js +41 -4
- package/src/mcp/liveTools.js +31 -8
- package/src/research/monteCarlo.js +6 -2
- package/types/live.d.ts +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.2.1] - 2026-06-27
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- `backtestPortfolio()` now reports aggregate `metrics.finalEquity` correctly when `collectEqSeries: false`.
|
|
13
|
+
- `PaperEngine` now rejects market orders that have no price reference instead of filling them at zero.
|
|
14
|
+
- `TradingSession` now clears staged brackets when an async entry is rejected or canceled, and staged brackets are matched by order id/client order id instead of a loose string check.
|
|
15
|
+
- MCP `attach_strategy` now evaluates built-in strategies with the normal backtest signal context and auto-places returned order intents from `feed_price`.
|
|
16
|
+
- `research.monteCarlo()` now rejects non-positive iteration counts instead of returning empty bands and `NaN` probability fields.
|
|
17
|
+
- Public live types now include `TradingSessionOptions.confirmLive` and optional dashboard `source.refresh()`.
|
|
18
|
+
|
|
8
19
|
## [1.2.0] - 2026-06-26
|
|
9
20
|
|
|
10
21
|
### Added
|
package/README.md
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="https://i.imgur.com/HGvvQbq.png" width="420" alt="tradelab logo" />
|
|
3
|
+
|
|
4
|
+
<p><strong>A Node.js backtesting toolkit for serious trading strategy research.</strong></p>
|
|
5
|
+
|
|
6
|
+
[](https://www.npmjs.com/package/tradelab)
|
|
7
|
+
[](https://github.com/ishsharm0/tradelab)
|
|
8
|
+
[](https://github.com/ishsharm0/tradelab/blob/main/LICENSE)
|
|
9
|
+
[](https://nodejs.org)
|
|
10
|
+
[](https://github.com/ishsharm0/tradelab/blob/main/types/index.d.ts)
|
|
11
|
+
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
---
|
|
2
15
|
|
|
3
16
|
A Node.js toolkit for testing, validating, and operating trading strategies.
|
|
4
17
|
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -3329,10 +3329,11 @@ function backtestPortfolio({
|
|
|
3329
3329
|
const allCandles = systems.flatMap((system) => system.candles || []);
|
|
3330
3330
|
const orderedCandles = [...allCandles].sort((left, right) => left.time - right.time);
|
|
3331
3331
|
const metricsInterval = interval ?? systems[0]?.interval;
|
|
3332
|
+
const finalState = portfolioState(runners, equity);
|
|
3332
3333
|
const metrics = buildMetrics({
|
|
3333
3334
|
closed: trades,
|
|
3334
3335
|
equityStart: equity,
|
|
3335
|
-
equityFinal: eqSeries.length ? eqSeries[eqSeries.length - 1].equity :
|
|
3336
|
+
equityFinal: eqSeries.length ? eqSeries[eqSeries.length - 1].equity : finalState.markedEquity,
|
|
3336
3337
|
candles: orderedCandles,
|
|
3337
3338
|
estBarMs: estimateBarMs(orderedCandles),
|
|
3338
3339
|
eqSeries,
|
|
@@ -3967,13 +3968,17 @@ function monteCarlo({
|
|
|
3967
3968
|
if (!Array.isArray(tradePnls) || tradePnls.length === 0) {
|
|
3968
3969
|
throw new Error("monteCarlo() requires a non-empty tradePnls array");
|
|
3969
3970
|
}
|
|
3971
|
+
const runCount = Math.floor(Number(iterations));
|
|
3972
|
+
if (!Number.isFinite(runCount) || runCount < 1) {
|
|
3973
|
+
throw new Error("monteCarlo() requires positive iterations");
|
|
3974
|
+
}
|
|
3970
3975
|
const rng = makeRng(seed);
|
|
3971
3976
|
const n = tradePnls.length;
|
|
3972
3977
|
const block = Math.max(1, Math.floor(blockSize));
|
|
3973
3978
|
const finals = [];
|
|
3974
3979
|
const drawdowns = [];
|
|
3975
3980
|
const pathSamples = Array.from({ length: n + 1 }, () => []);
|
|
3976
|
-
for (let it = 0; it <
|
|
3981
|
+
for (let it = 0; it < runCount; it += 1) {
|
|
3977
3982
|
const path7 = [equityStart];
|
|
3978
3983
|
let equity = equityStart;
|
|
3979
3984
|
let filled = 0;
|
|
@@ -4005,7 +4010,7 @@ function monteCarlo({
|
|
|
4005
4010
|
p95: percentile2(sorted, 0.95)
|
|
4006
4011
|
});
|
|
4007
4012
|
return {
|
|
4008
|
-
iterations,
|
|
4013
|
+
iterations: runCount,
|
|
4009
4014
|
blockSize: block,
|
|
4010
4015
|
finalEquity: bands(sortedFinals),
|
|
4011
4016
|
maxDrawdown: bands(sortedDd),
|
package/dist/cjs/live.cjs
CHANGED
|
@@ -2301,15 +2301,20 @@ var PaperEngine = class extends BrokerAdapter {
|
|
|
2301
2301
|
_recordOrder(order) {
|
|
2302
2302
|
this.orderHistory.set(order.orderId, { ...order });
|
|
2303
2303
|
}
|
|
2304
|
+
_rejectOrder(order, reason) {
|
|
2305
|
+
order.status = "rejected";
|
|
2306
|
+
order.rejectReason = reason;
|
|
2307
|
+
this._recordOrder(order);
|
|
2308
|
+
this.openOrders.delete(order.orderId);
|
|
2309
|
+
const receipt = cloneOrder(order);
|
|
2310
|
+
this.emit("order:rejected", receipt);
|
|
2311
|
+
return receipt;
|
|
2312
|
+
}
|
|
2304
2313
|
_fillOrder(order, fillPrice, kind = "market", fillTime = Date.now()) {
|
|
2305
2314
|
const side = normalizeOrderSide(order.side);
|
|
2306
2315
|
const qty = Math.max(0, asNumber(order.qty, 0));
|
|
2307
2316
|
if (!(qty > 0)) {
|
|
2308
|
-
order
|
|
2309
|
-
order.rejectReason = "invalid quantity";
|
|
2310
|
-
this._recordOrder(order);
|
|
2311
|
-
this.emit("order:rejected", cloneOrder(order));
|
|
2312
|
-
return cloneOrder(order);
|
|
2317
|
+
return this._rejectOrder(order, "invalid quantity");
|
|
2313
2318
|
}
|
|
2314
2319
|
const sideForFill = side === "buy" ? "long" : "short";
|
|
2315
2320
|
const filled = applyFill(fillPrice, sideForFill, {
|
|
@@ -2417,17 +2422,16 @@ var PaperEngine = class extends BrokerAdapter {
|
|
|
2417
2422
|
rejectReason: void 0
|
|
2418
2423
|
};
|
|
2419
2424
|
if (!(normalized.qty > 0)) {
|
|
2420
|
-
normalized
|
|
2421
|
-
normalized.rejectReason = "invalid quantity";
|
|
2422
|
-
this._recordOrder(normalized);
|
|
2423
|
-
this.emit("order:rejected", cloneOrder(normalized));
|
|
2424
|
-
return cloneOrder(normalized);
|
|
2425
|
+
return this._rejectOrder(normalized, "invalid quantity");
|
|
2425
2426
|
}
|
|
2426
2427
|
this._recordOrder(normalized);
|
|
2427
2428
|
this.emit("order:submitted", cloneOrder(normalized));
|
|
2428
2429
|
if (normalized.type === "market") {
|
|
2429
2430
|
const mark = this.lastPrices.get(normalized.symbol);
|
|
2430
|
-
const fillPrice = mark ?? normalized.limitPrice ?? normalized.stopPrice
|
|
2431
|
+
const fillPrice = mark ?? normalized.limitPrice ?? normalized.stopPrice;
|
|
2432
|
+
if (!Number.isFinite(fillPrice)) {
|
|
2433
|
+
return this._rejectOrder(normalized, "no price available for market order");
|
|
2434
|
+
}
|
|
2431
2435
|
return this._fillOrder(normalized, fillPrice, "market");
|
|
2432
2436
|
}
|
|
2433
2437
|
this.openOrders.set(normalized.orderId, normalized);
|
|
@@ -3480,6 +3484,14 @@ function oppositeSide2(side) {
|
|
|
3480
3484
|
function toBrokerSide(side) {
|
|
3481
3485
|
return side === "long" || side === "buy" ? "buy" : "sell";
|
|
3482
3486
|
}
|
|
3487
|
+
function matchesOrderRef(reference, order) {
|
|
3488
|
+
if (!reference || !order) return false;
|
|
3489
|
+
if (reference.orderId && order.orderId && reference.orderId === order.orderId) return true;
|
|
3490
|
+
if (reference.clientOrderId && order.clientOrderId && reference.clientOrderId === order.clientOrderId) {
|
|
3491
|
+
return true;
|
|
3492
|
+
}
|
|
3493
|
+
return false;
|
|
3494
|
+
}
|
|
3483
3495
|
var TradingSession = class _TradingSession {
|
|
3484
3496
|
constructor({
|
|
3485
3497
|
id,
|
|
@@ -3541,13 +3553,26 @@ var TradingSession = class _TradingSession {
|
|
|
3541
3553
|
_wireBrokerEvents() {
|
|
3542
3554
|
this.broker.on?.("order:filled", (order) => this._onBrokerFillSync(order));
|
|
3543
3555
|
this.broker.on?.("order:submitted", (order) => this._record("order:submitted", order));
|
|
3544
|
-
this.broker.on?.(
|
|
3556
|
+
this.broker.on?.(
|
|
3557
|
+
"order:canceled",
|
|
3558
|
+
(order) => this._onBrokerTerminalOrderSync("order:canceled", order)
|
|
3559
|
+
);
|
|
3560
|
+
this.broker.on?.(
|
|
3561
|
+
"order:rejected",
|
|
3562
|
+
(order) => this._onBrokerTerminalOrderSync("order:rejected", order)
|
|
3563
|
+
);
|
|
3545
3564
|
this.broker.on?.("equity:update", (acct) => this._record("equity:update", acct));
|
|
3546
3565
|
}
|
|
3566
|
+
_onBrokerTerminalOrderSync(event, order) {
|
|
3567
|
+
this._record(event, order);
|
|
3568
|
+
if (matchesOrderRef(this._pendingBracket, order)) {
|
|
3569
|
+
this._pendingBracket = null;
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3547
3572
|
// Sync event handler — fire-and-forget async OCO work via a stored promise
|
|
3548
3573
|
_onBrokerFillSync(order) {
|
|
3549
3574
|
this._record("order:filled", order);
|
|
3550
|
-
if (this._pendingBracket
|
|
3575
|
+
if (matchesOrderRef(this._pendingBracket, order)) {
|
|
3551
3576
|
const staged = this._pendingBracket;
|
|
3552
3577
|
this._pendingBracket = null;
|
|
3553
3578
|
this._pendingCancelPromise = Promise.resolve(
|
|
@@ -3622,19 +3647,31 @@ var TradingSession = class _TradingSession {
|
|
|
3622
3647
|
}
|
|
3623
3648
|
size = roundStep(size, this.qtyStep);
|
|
3624
3649
|
if (!(size >= this.minQty)) throw new Error(`sized below minQty (${size})`);
|
|
3650
|
+
const entryClientOrderId = `${this.id}-entry-${Date.now()}`;
|
|
3625
3651
|
const receipt = await this.broker.submitOrder({
|
|
3626
3652
|
symbol: this.symbol,
|
|
3627
3653
|
side: toBrokerSide(side),
|
|
3628
3654
|
type,
|
|
3629
3655
|
qty: size,
|
|
3630
3656
|
limitPrice: type === "limit" ? limitPrice : void 0,
|
|
3631
|
-
clientOrderId:
|
|
3657
|
+
clientOrderId: entryClientOrderId
|
|
3632
3658
|
});
|
|
3633
3659
|
if (Number.isFinite(stop) || Number.isFinite(target) || Number.isFinite(rr)) {
|
|
3634
3660
|
if (receipt.status === "filled") {
|
|
3635
3661
|
await this._attachBracket({ side, size, stop, target, rr, entryRef, receipt });
|
|
3662
|
+
} else if (receipt.status !== "rejected") {
|
|
3663
|
+
this._pendingBracket = {
|
|
3664
|
+
side,
|
|
3665
|
+
size,
|
|
3666
|
+
stop,
|
|
3667
|
+
target,
|
|
3668
|
+
rr,
|
|
3669
|
+
entryRef,
|
|
3670
|
+
orderId: receipt.orderId,
|
|
3671
|
+
clientOrderId: receipt.clientOrderId || entryClientOrderId
|
|
3672
|
+
};
|
|
3636
3673
|
} else {
|
|
3637
|
-
this._pendingBracket =
|
|
3674
|
+
this._pendingBracket = null;
|
|
3638
3675
|
}
|
|
3639
3676
|
}
|
|
3640
3677
|
await this.refresh();
|
package/docs/live-trading.md
CHANGED
|
@@ -49,7 +49,7 @@ console.log(engine.getStatus());
|
|
|
49
49
|
await engine.stop();
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
`PaperEngine` implements the broker interface in memory. Use it first for CLI runs, dashboard checks, and strategy wiring.
|
|
52
|
+
`PaperEngine` implements the broker interface in memory. Use it first for CLI runs, dashboard checks, and strategy wiring. Market orders need a price reference, so call `pushBar()`, `simulateBar()`, or the MCP `feed_price` tool before submitting one.
|
|
53
53
|
|
|
54
54
|
## Signal Contract
|
|
55
55
|
|
package/docs/mcp.md
CHANGED
|
@@ -129,7 +129,7 @@ A typical autonomous paper-trading loop:
|
|
|
129
129
|
4. Call `session_status` any time for a snapshot of positions, orders, equity, and risk state.
|
|
130
130
|
5. Call `flatten` or `halt_all` to emergency-close everything.
|
|
131
131
|
|
|
132
|
-
If you attach a strategy with `attach_strategy`, `feed_price` will auto-evaluate it each bar and place orders when the session is flat.
|
|
132
|
+
If you attach a strategy with `attach_strategy`, `feed_price` will auto-evaluate it each bar and place orders when the session is flat. Attached strategies receive the same `{ candles, index, bar, equity, openPosition, pendingOrder }` context as `backtest()`, and returned order intents default to a market order unless `type` is set.
|
|
133
133
|
|
|
134
134
|
## Typical Research Flow
|
|
135
135
|
|
package/package.json
CHANGED
package/src/engine/portfolio.js
CHANGED
|
@@ -309,10 +309,11 @@ export function backtestPortfolio({
|
|
|
309
309
|
const allCandles = systems.flatMap((system) => system.candles || []);
|
|
310
310
|
const orderedCandles = [...allCandles].sort((left, right) => left.time - right.time);
|
|
311
311
|
const metricsInterval = interval ?? systems[0]?.interval;
|
|
312
|
+
const finalState = portfolioState(runners, equity);
|
|
312
313
|
const metrics = buildMetrics({
|
|
313
314
|
closed: trades,
|
|
314
315
|
equityStart: equity,
|
|
315
|
-
equityFinal: eqSeries.length ? eqSeries[eqSeries.length - 1].equity :
|
|
316
|
+
equityFinal: eqSeries.length ? eqSeries[eqSeries.length - 1].equity : finalState.markedEquity,
|
|
316
317
|
candles: orderedCandles,
|
|
317
318
|
estBarMs: estimateBarMs(orderedCandles),
|
|
318
319
|
eqSeries,
|
|
@@ -216,15 +216,21 @@ export class PaperEngine extends BrokerAdapter {
|
|
|
216
216
|
this.orderHistory.set(order.orderId, { ...order });
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
_rejectOrder(order, reason) {
|
|
220
|
+
order.status = "rejected";
|
|
221
|
+
order.rejectReason = reason;
|
|
222
|
+
this._recordOrder(order);
|
|
223
|
+
this.openOrders.delete(order.orderId);
|
|
224
|
+
const receipt = cloneOrder(order);
|
|
225
|
+
this.emit("order:rejected", receipt);
|
|
226
|
+
return receipt;
|
|
227
|
+
}
|
|
228
|
+
|
|
219
229
|
_fillOrder(order, fillPrice, kind = "market", fillTime = Date.now()) {
|
|
220
230
|
const side = normalizeOrderSide(order.side);
|
|
221
231
|
const qty = Math.max(0, asNumber(order.qty, 0));
|
|
222
232
|
if (!(qty > 0)) {
|
|
223
|
-
order
|
|
224
|
-
order.rejectReason = "invalid quantity";
|
|
225
|
-
this._recordOrder(order);
|
|
226
|
-
this.emit("order:rejected", cloneOrder(order));
|
|
227
|
-
return cloneOrder(order);
|
|
233
|
+
return this._rejectOrder(order, "invalid quantity");
|
|
228
234
|
}
|
|
229
235
|
|
|
230
236
|
const sideForFill = side === "buy" ? "long" : "short";
|
|
@@ -346,11 +352,7 @@ export class PaperEngine extends BrokerAdapter {
|
|
|
346
352
|
};
|
|
347
353
|
|
|
348
354
|
if (!(normalized.qty > 0)) {
|
|
349
|
-
normalized
|
|
350
|
-
normalized.rejectReason = "invalid quantity";
|
|
351
|
-
this._recordOrder(normalized);
|
|
352
|
-
this.emit("order:rejected", cloneOrder(normalized));
|
|
353
|
-
return cloneOrder(normalized);
|
|
355
|
+
return this._rejectOrder(normalized, "invalid quantity");
|
|
354
356
|
}
|
|
355
357
|
|
|
356
358
|
this._recordOrder(normalized);
|
|
@@ -358,7 +360,10 @@ export class PaperEngine extends BrokerAdapter {
|
|
|
358
360
|
|
|
359
361
|
if (normalized.type === "market") {
|
|
360
362
|
const mark = this.lastPrices.get(normalized.symbol);
|
|
361
|
-
const fillPrice = mark ?? normalized.limitPrice ?? normalized.stopPrice
|
|
363
|
+
const fillPrice = mark ?? normalized.limitPrice ?? normalized.stopPrice;
|
|
364
|
+
if (!Number.isFinite(fillPrice)) {
|
|
365
|
+
return this._rejectOrder(normalized, "no price available for market order");
|
|
366
|
+
}
|
|
362
367
|
return this._fillOrder(normalized, fillPrice, "market");
|
|
363
368
|
}
|
|
364
369
|
|
package/src/live/session.js
CHANGED
|
@@ -12,6 +12,19 @@ function toBrokerSide(side) {
|
|
|
12
12
|
return side === "long" || side === "buy" ? "buy" : "sell";
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
function matchesOrderRef(reference, order) {
|
|
16
|
+
if (!reference || !order) return false;
|
|
17
|
+
if (reference.orderId && order.orderId && reference.orderId === order.orderId) return true;
|
|
18
|
+
if (
|
|
19
|
+
reference.clientOrderId &&
|
|
20
|
+
order.clientOrderId &&
|
|
21
|
+
reference.clientOrderId === order.clientOrderId
|
|
22
|
+
) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
15
28
|
export class TradingSession {
|
|
16
29
|
constructor({
|
|
17
30
|
id,
|
|
@@ -79,16 +92,28 @@ export class TradingSession {
|
|
|
79
92
|
// Forward broker fills/cancels onto the session bus, and run OCO bracket logic.
|
|
80
93
|
this.broker.on?.("order:filled", (order) => this._onBrokerFillSync(order));
|
|
81
94
|
this.broker.on?.("order:submitted", (order) => this._record("order:submitted", order));
|
|
82
|
-
this.broker.on?.("order:canceled", (order) =>
|
|
95
|
+
this.broker.on?.("order:canceled", (order) =>
|
|
96
|
+
this._onBrokerTerminalOrderSync("order:canceled", order)
|
|
97
|
+
);
|
|
98
|
+
this.broker.on?.("order:rejected", (order) =>
|
|
99
|
+
this._onBrokerTerminalOrderSync("order:rejected", order)
|
|
100
|
+
);
|
|
83
101
|
this.broker.on?.("equity:update", (acct) => this._record("equity:update", acct));
|
|
84
102
|
}
|
|
85
103
|
|
|
104
|
+
_onBrokerTerminalOrderSync(event, order) {
|
|
105
|
+
this._record(event, order);
|
|
106
|
+
if (matchesOrderRef(this._pendingBracket, order)) {
|
|
107
|
+
this._pendingBracket = null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
86
111
|
// Sync event handler — fire-and-forget async OCO work via a stored promise
|
|
87
112
|
_onBrokerFillSync(order) {
|
|
88
113
|
this._record("order:filled", order);
|
|
89
114
|
|
|
90
115
|
// Resting entry order (e.g. a limit) just filled — attach its staged bracket.
|
|
91
|
-
if (this._pendingBracket
|
|
116
|
+
if (matchesOrderRef(this._pendingBracket, order)) {
|
|
92
117
|
const staged = this._pendingBracket;
|
|
93
118
|
this._pendingBracket = null;
|
|
94
119
|
// simulateBar may still be iterating orders, so schedule attach without awaiting.
|
|
@@ -175,21 +200,33 @@ export class TradingSession {
|
|
|
175
200
|
size = roundStep(size, this.qtyStep);
|
|
176
201
|
if (!(size >= this.minQty)) throw new Error(`sized below minQty (${size})`);
|
|
177
202
|
|
|
203
|
+
const entryClientOrderId = `${this.id}-entry-${Date.now()}`;
|
|
178
204
|
const receipt = await this.broker.submitOrder({
|
|
179
205
|
symbol: this.symbol,
|
|
180
206
|
side: toBrokerSide(side),
|
|
181
207
|
type,
|
|
182
208
|
qty: size,
|
|
183
209
|
limitPrice: type === "limit" ? limitPrice : undefined,
|
|
184
|
-
clientOrderId:
|
|
210
|
+
clientOrderId: entryClientOrderId,
|
|
185
211
|
});
|
|
186
212
|
|
|
187
213
|
// Stage bracket if needed — market orders fill synchronously in PaperEngine
|
|
188
214
|
if (Number.isFinite(stop) || Number.isFinite(target) || Number.isFinite(rr)) {
|
|
189
215
|
if (receipt.status === "filled") {
|
|
190
216
|
await this._attachBracket({ side, size, stop, target, rr, entryRef, receipt });
|
|
217
|
+
} else if (receipt.status !== "rejected") {
|
|
218
|
+
this._pendingBracket = {
|
|
219
|
+
side,
|
|
220
|
+
size,
|
|
221
|
+
stop,
|
|
222
|
+
target,
|
|
223
|
+
rr,
|
|
224
|
+
entryRef,
|
|
225
|
+
orderId: receipt.orderId,
|
|
226
|
+
clientOrderId: receipt.clientOrderId || entryClientOrderId,
|
|
227
|
+
};
|
|
191
228
|
} else {
|
|
192
|
-
this._pendingBracket =
|
|
229
|
+
this._pendingBracket = null;
|
|
193
230
|
}
|
|
194
231
|
}
|
|
195
232
|
|
package/src/mcp/liveTools.js
CHANGED
|
@@ -10,6 +10,33 @@ function requireSession(sessionId) {
|
|
|
10
10
|
return s;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
function strategyContext(session) {
|
|
14
|
+
const candles = session.candleBuffer;
|
|
15
|
+
const bar = candles[candles.length - 1] ?? null;
|
|
16
|
+
const status = session.getStatus();
|
|
17
|
+
return {
|
|
18
|
+
candles,
|
|
19
|
+
index: candles.length - 1,
|
|
20
|
+
bar,
|
|
21
|
+
equity: status.equity,
|
|
22
|
+
openPosition: status.positions[0] ?? null,
|
|
23
|
+
pendingOrder: null,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function signalToOrder(signal) {
|
|
28
|
+
return {
|
|
29
|
+
side: signal.side ?? signal.direction ?? signal.action,
|
|
30
|
+
type: signal.type ?? "market",
|
|
31
|
+
qty: signal.qty ?? signal.size,
|
|
32
|
+
riskPct: signal.riskPct,
|
|
33
|
+
stop: signal.stop ?? signal.stopLoss ?? signal.sl,
|
|
34
|
+
target: signal.target ?? signal.takeProfit ?? signal.tp,
|
|
35
|
+
rr: signal.rr ?? signal._rr,
|
|
36
|
+
limitPrice: signal.limitPrice ?? signal.limit ?? signal.entry ?? signal.price,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
13
40
|
export { manager as sessionManager };
|
|
14
41
|
|
|
15
42
|
export const liveTools = {
|
|
@@ -71,9 +98,9 @@ export const liveTools = {
|
|
|
71
98
|
// If a strategy is attached and session is flat, evaluate it
|
|
72
99
|
if (session._strategy && session.getStatus().positions.length === 0) {
|
|
73
100
|
try {
|
|
74
|
-
const signal = session._strategy(session
|
|
75
|
-
if (signal && signal.side
|
|
76
|
-
await session.placeOrder(signal).catch(() => {});
|
|
101
|
+
const signal = session._strategy(strategyContext(session));
|
|
102
|
+
if (signal && (signal.side || signal.direction || signal.action)) {
|
|
103
|
+
await session.placeOrder(signalToOrder(signal)).catch(() => {});
|
|
77
104
|
}
|
|
78
105
|
} catch {
|
|
79
106
|
// strategy errors are non-fatal
|
|
@@ -160,11 +187,7 @@ export const liveTools = {
|
|
|
160
187
|
const session = requireSession(sessionId);
|
|
161
188
|
const factory = getStrategy(strategy);
|
|
162
189
|
const signal = factory(params);
|
|
163
|
-
|
|
164
|
-
session._strategy = (candleBuffer) => {
|
|
165
|
-
if (!candleBuffer || candleBuffer.length === 0) return null;
|
|
166
|
-
return signal(candleBuffer[candleBuffer.length - 1], candleBuffer);
|
|
167
|
-
};
|
|
190
|
+
session._strategy = signal;
|
|
168
191
|
return { ok: true, strategy, params };
|
|
169
192
|
},
|
|
170
193
|
},
|
|
@@ -35,6 +35,10 @@ export function monteCarlo({
|
|
|
35
35
|
if (!Array.isArray(tradePnls) || tradePnls.length === 0) {
|
|
36
36
|
throw new Error("monteCarlo() requires a non-empty tradePnls array");
|
|
37
37
|
}
|
|
38
|
+
const runCount = Math.floor(Number(iterations));
|
|
39
|
+
if (!Number.isFinite(runCount) || runCount < 1) {
|
|
40
|
+
throw new Error("monteCarlo() requires positive iterations");
|
|
41
|
+
}
|
|
38
42
|
const rng = makeRng(seed);
|
|
39
43
|
const n = tradePnls.length;
|
|
40
44
|
const block = Math.max(1, Math.floor(blockSize));
|
|
@@ -43,7 +47,7 @@ export function monteCarlo({
|
|
|
43
47
|
const drawdowns = [];
|
|
44
48
|
const pathSamples = Array.from({ length: n + 1 }, () => []);
|
|
45
49
|
|
|
46
|
-
for (let it = 0; it <
|
|
50
|
+
for (let it = 0; it < runCount; it += 1) {
|
|
47
51
|
const path = [equityStart];
|
|
48
52
|
let equity = equityStart;
|
|
49
53
|
let filled = 0;
|
|
@@ -78,7 +82,7 @@ export function monteCarlo({
|
|
|
78
82
|
});
|
|
79
83
|
|
|
80
84
|
return {
|
|
81
|
-
iterations,
|
|
85
|
+
iterations: runCount,
|
|
82
86
|
blockSize: block,
|
|
83
87
|
finalEquity: bands(sortedFinals),
|
|
84
88
|
maxDrawdown: bands(sortedDd),
|
package/types/live.d.ts
CHANGED
|
@@ -341,7 +341,8 @@ export interface DashboardServer {
|
|
|
341
341
|
export function createDashboardServer(options: {
|
|
342
342
|
source: {
|
|
343
343
|
eventBus: EventBus;
|
|
344
|
-
getStatus?: () =>
|
|
344
|
+
getStatus?: () => unknown;
|
|
345
|
+
refresh?: () => Promise<unknown>;
|
|
345
346
|
};
|
|
346
347
|
port?: number;
|
|
347
348
|
maxBuffer?: number;
|
|
@@ -414,6 +415,7 @@ export interface TradingSessionOptions {
|
|
|
414
415
|
minQty?: number;
|
|
415
416
|
maxLeverage?: number;
|
|
416
417
|
eventBus?: EventBus;
|
|
418
|
+
confirmLive?: boolean;
|
|
417
419
|
}
|
|
418
420
|
|
|
419
421
|
export interface SessionPlaceOrderOptions {
|