tradelab 1.2.1 → 1.3.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 +48 -0
- package/README.md +24 -11
- package/bin/tradelab.js +37 -1
- package/dist/cjs/index.cjs +90 -0
- package/dist/cjs/live.cjs +243 -52
- package/docs/README.md +6 -4
- package/docs/api-reference.md +15 -1
- package/docs/data-reporting-cli.md +9 -0
- package/docs/live-trading.md +130 -0
- package/docs/mcp.md +92 -23
- package/docs/research.md +15 -0
- package/examples/agentResearchLoop.js +188 -0
- package/examples/multiSymbolPortfolio.js +122 -0
- package/package.json +1 -1
- package/src/cli/runPreset.js +42 -0
- package/src/index.js +2 -0
- package/src/live/engine/riskManager.js +38 -0
- package/src/live/index.js +1 -0
- package/src/live/notify.js +42 -0
- package/src/live/session.js +170 -56
- package/src/mcp/liveTools.js +28 -24
- package/src/mcp/researchSession.js +24 -0
- package/src/mcp/schemas.js +5 -1
- package/src/mcp/tools.js +28 -3
- package/src/reporting/summarize.js +43 -0
- package/src/research/store.js +67 -0
- package/types/index.d.ts +30 -0
package/dist/cjs/live.cjs
CHANGED
|
@@ -52,6 +52,7 @@ __export(index_exports, {
|
|
|
52
52
|
StateManager: () => StateManager,
|
|
53
53
|
StorageProvider: () => StorageProvider,
|
|
54
54
|
TradingSession: () => TradingSession,
|
|
55
|
+
attachNotifier: () => attachNotifier,
|
|
55
56
|
createAlpacaBroker: () => createAlpacaBroker,
|
|
56
57
|
createBinanceBroker: () => createBinanceBroker,
|
|
57
58
|
createBrokerFeed: () => createBrokerFeed,
|
|
@@ -1881,6 +1882,8 @@ var RiskManager = class {
|
|
|
1881
1882
|
cooldownAfterLossMs: 0,
|
|
1882
1883
|
allowedSessions: "AUTO",
|
|
1883
1884
|
allowedWindows: null,
|
|
1885
|
+
maxGrossExposurePct: 0,
|
|
1886
|
+
maxNetExposurePct: 0,
|
|
1884
1887
|
...options
|
|
1885
1888
|
};
|
|
1886
1889
|
this.allowedWindows = parseWindowsCSV(this.options.allowedWindows);
|
|
@@ -1957,7 +1960,9 @@ var RiskManager = class {
|
|
|
1957
1960
|
timeMs = Date.now(),
|
|
1958
1961
|
positionCount = 0,
|
|
1959
1962
|
positionValue = 0,
|
|
1960
|
-
equity = null
|
|
1963
|
+
equity = null,
|
|
1964
|
+
grossExposure = void 0,
|
|
1965
|
+
netExposure = void 0
|
|
1961
1966
|
} = {}) {
|
|
1962
1967
|
const base = this.canTrade({ timeMs });
|
|
1963
1968
|
if (!base.ok) return base;
|
|
@@ -1975,6 +1980,35 @@ var RiskManager = class {
|
|
|
1975
1980
|
return { ok: false, reason: "max position size exceeded" };
|
|
1976
1981
|
}
|
|
1977
1982
|
}
|
|
1983
|
+
return this._checkExposureCaps({ grossExposure, netExposure, equity: eq });
|
|
1984
|
+
}
|
|
1985
|
+
/**
|
|
1986
|
+
* Check only the portfolio exposure caps (no session/halt/trade-count checks).
|
|
1987
|
+
* Called from placeOrder after the halt check has already run.
|
|
1988
|
+
*/
|
|
1989
|
+
checkExposure({ grossExposure = void 0, netExposure = void 0, equity = null } = {}) {
|
|
1990
|
+
const eq = Number.isFinite(equity) ? equity : this.currentEquity;
|
|
1991
|
+
return this._checkExposureCaps({ grossExposure, netExposure, equity: eq });
|
|
1992
|
+
}
|
|
1993
|
+
/**
|
|
1994
|
+
* Shared gross/net exposure cap logic used by canOpenPosition and checkExposure.
|
|
1995
|
+
* Expects a resolved equity value (NaN/null fallback already applied by caller).
|
|
1996
|
+
* Returns { ok: true, reason: null } when within caps or caps are disabled.
|
|
1997
|
+
*/
|
|
1998
|
+
_checkExposureCaps({ grossExposure = void 0, netExposure = void 0, equity } = {}) {
|
|
1999
|
+
const eq = equity;
|
|
2000
|
+
const grossCap = pctToFraction(this.options.maxGrossExposurePct, 0);
|
|
2001
|
+
if (grossCap > 0 && Number.isFinite(eq) && eq > 0 && Number.isFinite(grossExposure)) {
|
|
2002
|
+
if (Math.abs(grossExposure) / eq > grossCap) {
|
|
2003
|
+
return { ok: false, reason: "max gross exposure exceeded" };
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
const netCap = pctToFraction(this.options.maxNetExposurePct, 0);
|
|
2007
|
+
if (netCap > 0 && Number.isFinite(eq) && eq > 0 && Number.isFinite(netExposure)) {
|
|
2008
|
+
if (Math.abs(netExposure) / eq > netCap) {
|
|
2009
|
+
return { ok: false, reason: "max net exposure exceeded" };
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
1978
2012
|
return { ok: true, reason: null };
|
|
1979
2013
|
}
|
|
1980
2014
|
recordTrade({ pnl = 0, timeMs = Date.now(), equity = null } = {}) {
|
|
@@ -3496,6 +3530,7 @@ var TradingSession = class _TradingSession {
|
|
|
3496
3530
|
constructor({
|
|
3497
3531
|
id,
|
|
3498
3532
|
symbol,
|
|
3533
|
+
symbols,
|
|
3499
3534
|
interval = "1m",
|
|
3500
3535
|
broker,
|
|
3501
3536
|
mode = "paper",
|
|
@@ -3503,6 +3538,8 @@ var TradingSession = class _TradingSession {
|
|
|
3503
3538
|
riskPct = 1,
|
|
3504
3539
|
maxDailyLossPct = 0,
|
|
3505
3540
|
maxPositionPct = 1,
|
|
3541
|
+
maxGrossExposurePct = 0,
|
|
3542
|
+
maxNetExposurePct = 0,
|
|
3506
3543
|
qtyStep = 1e-3,
|
|
3507
3544
|
minQty = 1e-3,
|
|
3508
3545
|
maxLeverage = 2,
|
|
@@ -3515,9 +3552,11 @@ var TradingSession = class _TradingSession {
|
|
|
3515
3552
|
);
|
|
3516
3553
|
}
|
|
3517
3554
|
if (!broker) throw new Error("TradingSession requires a broker (PaperEngine by default)");
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
this.
|
|
3555
|
+
const symbolList = Array.isArray(symbols) && symbols.length ? symbols : symbol ? [symbol] : null;
|
|
3556
|
+
if (!symbolList) throw new Error("TradingSession requires a symbol or symbols");
|
|
3557
|
+
this.symbols = symbolList;
|
|
3558
|
+
this.symbol = symbolList[0];
|
|
3559
|
+
this.id = id || `${this.symbol}-${interval}`;
|
|
3521
3560
|
this.interval = interval;
|
|
3522
3561
|
this.broker = broker;
|
|
3523
3562
|
this.mode = mode;
|
|
@@ -3529,18 +3568,62 @@ var TradingSession = class _TradingSession {
|
|
|
3529
3568
|
this.minQty = minQty;
|
|
3530
3569
|
this.maxLeverage = maxLeverage;
|
|
3531
3570
|
this.eventBus = eventBus || new EventBus();
|
|
3532
|
-
this.riskManager = new RiskManager({ maxDailyLossPct, maxDrawdownPct: 0 });
|
|
3533
|
-
this.lastPrice = null;
|
|
3571
|
+
this.riskManager = new RiskManager({ maxDailyLossPct, maxDrawdownPct: 0, maxGrossExposurePct, maxNetExposurePct });
|
|
3534
3572
|
this.running = false;
|
|
3535
3573
|
this.events = [];
|
|
3536
3574
|
this.brackets = /* @__PURE__ */ new Map();
|
|
3537
|
-
this.
|
|
3575
|
+
this._pendingBrackets = /* @__PURE__ */ new Map();
|
|
3576
|
+
this._entryMeta = /* @__PURE__ */ new Map();
|
|
3577
|
+
this._legMeta = /* @__PURE__ */ new Map();
|
|
3538
3578
|
this._cachedPositions = [];
|
|
3539
3579
|
this._cachedOpenOrders = [];
|
|
3540
|
-
this.
|
|
3541
|
-
this.
|
|
3580
|
+
this._lastPrice = /* @__PURE__ */ new Map();
|
|
3581
|
+
this._candleBuffers = /* @__PURE__ */ new Map();
|
|
3582
|
+
this._strategies = /* @__PURE__ */ new Map();
|
|
3583
|
+
for (const sym of this.symbols) this._candleBuffers.set(sym, []);
|
|
3584
|
+
this._wasHalted = false;
|
|
3585
|
+
this._coidSeq = 0;
|
|
3542
3586
|
this._wireBrokerEvents();
|
|
3543
3587
|
}
|
|
3588
|
+
// Back-compat getters/setters for single-symbol usage and MCP feed_price handler
|
|
3589
|
+
get lastPrice() {
|
|
3590
|
+
return this._lastPrice.get(this.symbol) ?? null;
|
|
3591
|
+
}
|
|
3592
|
+
set lastPrice(v) {
|
|
3593
|
+
this._lastPrice.set(this.symbol, v);
|
|
3594
|
+
}
|
|
3595
|
+
get candleBuffer() {
|
|
3596
|
+
return this._candleBuffers.get(this.symbol) ?? [];
|
|
3597
|
+
}
|
|
3598
|
+
set candleBuffer(v) {
|
|
3599
|
+
this._candleBuffers.set(this.symbol, v);
|
|
3600
|
+
}
|
|
3601
|
+
get _strategy() {
|
|
3602
|
+
return this._strategies.get(this.symbol) ?? null;
|
|
3603
|
+
}
|
|
3604
|
+
set _strategy(fn) {
|
|
3605
|
+
this._strategies.set(this.symbol, fn);
|
|
3606
|
+
}
|
|
3607
|
+
// Back-compat for tests that read/write _pendingBracket directly (primary symbol only)
|
|
3608
|
+
get _pendingBracket() {
|
|
3609
|
+
return this._pendingBrackets.get(this.symbol) ?? null;
|
|
3610
|
+
}
|
|
3611
|
+
set _pendingBracket(v) {
|
|
3612
|
+
if (v == null) this._pendingBrackets.delete(this.symbol);
|
|
3613
|
+
else this._pendingBrackets.set(this.symbol, v);
|
|
3614
|
+
}
|
|
3615
|
+
// Per-symbol accessors
|
|
3616
|
+
lastPriceFor(sym = this.symbol) {
|
|
3617
|
+
return this._lastPrice.get(sym) ?? null;
|
|
3618
|
+
}
|
|
3619
|
+
candleBufferFor(sym = this.symbol) {
|
|
3620
|
+
return this._candleBuffers.get(sym) ?? [];
|
|
3621
|
+
}
|
|
3622
|
+
_resolveSymbol(symbol) {
|
|
3623
|
+
if (symbol) return symbol;
|
|
3624
|
+
if (this.symbols.length === 1) return this.symbol;
|
|
3625
|
+
throw new Error("symbol is required for a multi-symbol session");
|
|
3626
|
+
}
|
|
3544
3627
|
static liveAllowed() {
|
|
3545
3628
|
return process.env.TRADELAB_ALLOW_LIVE === "true";
|
|
3546
3629
|
}
|
|
@@ -3552,7 +3635,7 @@ var TradingSession = class _TradingSession {
|
|
|
3552
3635
|
}
|
|
3553
3636
|
_wireBrokerEvents() {
|
|
3554
3637
|
this.broker.on?.("order:filled", (order) => this._onBrokerFillSync(order));
|
|
3555
|
-
this.broker.on?.("order:submitted", (order) => this._record("order:submitted", order));
|
|
3638
|
+
this.broker.on?.("order:submitted", (order) => this._record("order:submitted", this._withMeta(order)));
|
|
3556
3639
|
this.broker.on?.(
|
|
3557
3640
|
"order:canceled",
|
|
3558
3641
|
(order) => this._onBrokerTerminalOrderSync("order:canceled", order)
|
|
@@ -3565,30 +3648,48 @@ var TradingSession = class _TradingSession {
|
|
|
3565
3648
|
}
|
|
3566
3649
|
_onBrokerTerminalOrderSync(event, order) {
|
|
3567
3650
|
this._record(event, order);
|
|
3568
|
-
|
|
3569
|
-
|
|
3651
|
+
for (const [sym, staged] of this._pendingBrackets) {
|
|
3652
|
+
if (matchesOrderRef(staged, order)) {
|
|
3653
|
+
this._pendingBrackets.delete(sym);
|
|
3654
|
+
break;
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3657
|
+
}
|
|
3658
|
+
_withMeta(order) {
|
|
3659
|
+
const key = order.clientOrderId;
|
|
3660
|
+
if (key && this._entryMeta?.has(key)) {
|
|
3661
|
+
const m = this._entryMeta.get(key);
|
|
3662
|
+
return { ...order, sizing: m.sizing, ...m.rationale ? { rationale: m.rationale } : {} };
|
|
3663
|
+
}
|
|
3664
|
+
if (key && this._legMeta?.has(key)) {
|
|
3665
|
+
return { ...order, ...this._legMeta.get(key) };
|
|
3570
3666
|
}
|
|
3667
|
+
return order;
|
|
3571
3668
|
}
|
|
3572
|
-
// Sync event handler
|
|
3669
|
+
// Sync event handler; fire-and-forget async OCO work via a stored promise
|
|
3573
3670
|
_onBrokerFillSync(order) {
|
|
3574
|
-
this._record("order:filled", order);
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
this.
|
|
3580
|
-
|
|
3581
|
-
|
|
3671
|
+
this._record("order:filled", this._withMeta(order));
|
|
3672
|
+
for (const [sym, staged] of this._pendingBrackets) {
|
|
3673
|
+
if (matchesOrderRef(staged, order)) {
|
|
3674
|
+
this._pendingBrackets.delete(sym);
|
|
3675
|
+
const parentEntryId = staged.parentEntryId ?? order.clientOrderId;
|
|
3676
|
+
this._pendingCancelPromise = Promise.resolve(
|
|
3677
|
+
this._attachBracket({ ...staged, symbol: sym, receipt: order, parentEntryId })
|
|
3678
|
+
);
|
|
3679
|
+
return;
|
|
3680
|
+
}
|
|
3582
3681
|
}
|
|
3583
|
-
const bracket
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3682
|
+
for (const [sym, bracket] of this.brackets) {
|
|
3683
|
+
if (bracket && (order.orderId === bracket.stopId || order.orderId === bracket.targetId)) {
|
|
3684
|
+
const siblingId = order.orderId === bracket.stopId ? bracket.targetId : bracket.stopId;
|
|
3685
|
+
this._pendingCancelPromise = (async () => {
|
|
3686
|
+
if (siblingId) await this.broker.cancelOrder(siblingId).catch(() => {
|
|
3687
|
+
});
|
|
3688
|
+
this.brackets.delete(sym);
|
|
3689
|
+
this._record("position:closed", { symbol: sym, reason: order.orderId === bracket.stopId ? "SL" : "TP" });
|
|
3690
|
+
})();
|
|
3691
|
+
return;
|
|
3692
|
+
}
|
|
3592
3693
|
}
|
|
3593
3694
|
}
|
|
3594
3695
|
async start() {
|
|
@@ -3607,18 +3708,21 @@ var TradingSession = class _TradingSession {
|
|
|
3607
3708
|
this.running = false;
|
|
3608
3709
|
this._record("shutdown", {});
|
|
3609
3710
|
}
|
|
3610
|
-
async pushBar(b) {
|
|
3611
|
-
|
|
3711
|
+
async pushBar(b, symbol) {
|
|
3712
|
+
const sym = this._resolveSymbol(symbol);
|
|
3713
|
+
this._lastPrice.set(sym, b.close);
|
|
3612
3714
|
if (typeof this.broker.simulateBar === "function") {
|
|
3613
|
-
await this.broker.simulateBar(
|
|
3715
|
+
await this.broker.simulateBar(sym, this.interval, b);
|
|
3614
3716
|
}
|
|
3615
3717
|
if (this._pendingCancelPromise) {
|
|
3616
3718
|
await this._pendingCancelPromise;
|
|
3617
3719
|
this._pendingCancelPromise = null;
|
|
3618
3720
|
}
|
|
3619
|
-
this.
|
|
3620
|
-
|
|
3621
|
-
|
|
3721
|
+
const buf = this._candleBuffers.get(sym) ?? [];
|
|
3722
|
+
buf.push(b);
|
|
3723
|
+
if (buf.length > 200) buf.shift();
|
|
3724
|
+
this._candleBuffers.set(sym, buf);
|
|
3725
|
+
this._record("bar", { symbol: sym, close: b.close, time: b.time });
|
|
3622
3726
|
await this._syncEquityAndRisk();
|
|
3623
3727
|
await this.refresh();
|
|
3624
3728
|
}
|
|
@@ -3626,14 +3730,15 @@ var TradingSession = class _TradingSession {
|
|
|
3626
3730
|
const state = this.riskManager.getState?.() || {};
|
|
3627
3731
|
return Boolean(state.halted);
|
|
3628
3732
|
}
|
|
3629
|
-
async placeOrder({ side, type = "market", qty, riskPct, stop, target, rr, limitPrice } = {}) {
|
|
3733
|
+
async placeOrder({ side, type = "market", qty, riskPct, stop, target, rr, limitPrice, rationale, symbol } = {}) {
|
|
3630
3734
|
if (!this.running) throw new Error("session not started");
|
|
3631
3735
|
if (this._riskHalted()) throw new Error("session is risk-halted for the day");
|
|
3632
|
-
const
|
|
3736
|
+
const sym = this._resolveSymbol(symbol);
|
|
3737
|
+
const entryRef = type === "limit" ? limitPrice : this.lastPriceFor(sym);
|
|
3633
3738
|
if (!Number.isFinite(entryRef)) throw new Error("no price available; pushBar() a price first");
|
|
3739
|
+
const fraction = Number.isFinite(riskPct) ? riskPct / 100 : this.riskPct / 100;
|
|
3634
3740
|
let size = qty;
|
|
3635
3741
|
if (!Number.isFinite(size)) {
|
|
3636
|
-
const fraction = Number.isFinite(riskPct) ? riskPct / 100 : this.riskPct / 100;
|
|
3637
3742
|
if (!Number.isFinite(stop)) throw new Error("risk-based sizing requires a stop");
|
|
3638
3743
|
size = calculatePositionSize({
|
|
3639
3744
|
equity: this.equity,
|
|
@@ -3647,9 +3752,38 @@ var TradingSession = class _TradingSession {
|
|
|
3647
3752
|
}
|
|
3648
3753
|
size = roundStep(size, this.qtyStep);
|
|
3649
3754
|
if (!(size >= this.minQty)) throw new Error(`sized below minQty (${size})`);
|
|
3650
|
-
const
|
|
3755
|
+
const targetPx = Number.isFinite(target) ? target : Number.isFinite(rr) && Number.isFinite(stop) ? side === "long" || side === "buy" ? entryRef + rr * Math.abs(entryRef - stop) : entryRef - rr * Math.abs(entryRef - stop) : null;
|
|
3756
|
+
const sizing = {
|
|
3757
|
+
entry: entryRef,
|
|
3758
|
+
stop: Number.isFinite(stop) ? stop : null,
|
|
3759
|
+
target: targetPx,
|
|
3760
|
+
rr: Number.isFinite(rr) ? rr : null,
|
|
3761
|
+
riskFraction: fraction,
|
|
3762
|
+
riskAmount: this.equity * fraction,
|
|
3763
|
+
qty: size,
|
|
3764
|
+
notional: size * entryRef
|
|
3765
|
+
};
|
|
3766
|
+
const positions = this._cachedPositions ?? [];
|
|
3767
|
+
const newNotional = sizing.notional * (side === "long" || side === "buy" ? 1 : -1);
|
|
3768
|
+
let gross = Math.abs(sizing.notional);
|
|
3769
|
+
let net = newNotional;
|
|
3770
|
+
for (const p of positions) {
|
|
3771
|
+
const px = p.avgEntry ?? p.avgPrice ?? p.entryPrice ?? entryRef;
|
|
3772
|
+
const pv = Number.isFinite(p.marketValue) ? p.marketValue : (p.qty ?? 0) * px;
|
|
3773
|
+
const signed = p.side === "long" || p.side === "buy" ? pv : -pv;
|
|
3774
|
+
gross += Math.abs(pv);
|
|
3775
|
+
net += signed;
|
|
3776
|
+
}
|
|
3777
|
+
const gate = this.riskManager.checkExposure({
|
|
3778
|
+
grossExposure: gross,
|
|
3779
|
+
netExposure: net,
|
|
3780
|
+
equity: this.equity
|
|
3781
|
+
});
|
|
3782
|
+
if (!gate.ok) throw new Error(`risk rejected: ${gate.reason}`);
|
|
3783
|
+
const entryClientOrderId = `${this.id}-entry-${Date.now()}-${++this._coidSeq}`;
|
|
3784
|
+
this._entryMeta.set(entryClientOrderId, { sizing, rationale });
|
|
3651
3785
|
const receipt = await this.broker.submitOrder({
|
|
3652
|
-
symbol:
|
|
3786
|
+
symbol: sym,
|
|
3653
3787
|
side: toBrokerSide(side),
|
|
3654
3788
|
type,
|
|
3655
3789
|
qty: size,
|
|
@@ -3657,10 +3791,11 @@ var TradingSession = class _TradingSession {
|
|
|
3657
3791
|
clientOrderId: entryClientOrderId
|
|
3658
3792
|
});
|
|
3659
3793
|
if (Number.isFinite(stop) || Number.isFinite(target) || Number.isFinite(rr)) {
|
|
3794
|
+
const parentEntryId = receipt?.clientOrderId ?? entryClientOrderId;
|
|
3660
3795
|
if (receipt.status === "filled") {
|
|
3661
|
-
await this._attachBracket({ side, size, stop, target, rr, entryRef, receipt });
|
|
3796
|
+
await this._attachBracket({ side, size, stop, target, rr, entryRef, receipt, parentEntryId, symbol: sym });
|
|
3662
3797
|
} else if (receipt.status !== "rejected") {
|
|
3663
|
-
this.
|
|
3798
|
+
this._pendingBrackets.set(sym, {
|
|
3664
3799
|
side,
|
|
3665
3800
|
size,
|
|
3666
3801
|
stop,
|
|
@@ -3668,44 +3803,50 @@ var TradingSession = class _TradingSession {
|
|
|
3668
3803
|
rr,
|
|
3669
3804
|
entryRef,
|
|
3670
3805
|
orderId: receipt.orderId,
|
|
3671
|
-
clientOrderId: receipt.clientOrderId || entryClientOrderId
|
|
3672
|
-
|
|
3806
|
+
clientOrderId: receipt.clientOrderId || entryClientOrderId,
|
|
3807
|
+
parentEntryId
|
|
3808
|
+
});
|
|
3673
3809
|
} else {
|
|
3674
|
-
this.
|
|
3810
|
+
this._pendingBrackets.delete(sym);
|
|
3675
3811
|
}
|
|
3676
3812
|
}
|
|
3677
3813
|
await this.refresh();
|
|
3678
3814
|
return receipt;
|
|
3679
3815
|
}
|
|
3680
|
-
async _attachBracket({ side, size, stop, target, rr, entryRef, receipt }) {
|
|
3816
|
+
async _attachBracket({ side, size, stop, target, rr, entryRef, receipt, parentEntryId, symbol }) {
|
|
3817
|
+
const sym = symbol ?? this.symbol;
|
|
3681
3818
|
const entryFill = receipt?.avgFillPrice ?? entryRef;
|
|
3682
3819
|
const risk = Number.isFinite(stop) ? Math.abs(entryFill - stop) : null;
|
|
3683
3820
|
const targetPrice = Number.isFinite(target) ? target : Number.isFinite(rr) && risk ? side === "long" || side === "buy" ? entryFill + rr * risk : entryFill - rr * risk : null;
|
|
3684
3821
|
const exitSide = oppositeSide2(side);
|
|
3685
3822
|
const bracket = {};
|
|
3686
3823
|
if (Number.isFinite(stop)) {
|
|
3824
|
+
const stopCoid = `${this.id}-stop-${Date.now()}-${++this._coidSeq}`;
|
|
3825
|
+
if (parentEntryId) this._legMeta.set(stopCoid, { parentEntryId, leg: "stop" });
|
|
3687
3826
|
const stopOrder = await this.broker.submitOrder({
|
|
3688
|
-
symbol:
|
|
3827
|
+
symbol: sym,
|
|
3689
3828
|
side: exitSide,
|
|
3690
3829
|
type: "stop",
|
|
3691
3830
|
qty: size,
|
|
3692
3831
|
stopPrice: stop,
|
|
3693
|
-
clientOrderId:
|
|
3832
|
+
clientOrderId: stopCoid
|
|
3694
3833
|
});
|
|
3695
3834
|
bracket.stopId = stopOrder.orderId;
|
|
3696
3835
|
}
|
|
3697
3836
|
if (Number.isFinite(targetPrice)) {
|
|
3837
|
+
const tgtCoid = `${this.id}-target-${Date.now()}-${++this._coidSeq}`;
|
|
3838
|
+
if (parentEntryId) this._legMeta.set(tgtCoid, { parentEntryId, leg: "target" });
|
|
3698
3839
|
const tgtOrder = await this.broker.submitOrder({
|
|
3699
|
-
symbol:
|
|
3840
|
+
symbol: sym,
|
|
3700
3841
|
side: exitSide,
|
|
3701
3842
|
type: "limit",
|
|
3702
3843
|
qty: size,
|
|
3703
3844
|
limitPrice: targetPrice,
|
|
3704
|
-
clientOrderId:
|
|
3845
|
+
clientOrderId: tgtCoid
|
|
3705
3846
|
});
|
|
3706
3847
|
bracket.targetId = tgtOrder.orderId;
|
|
3707
3848
|
}
|
|
3708
|
-
this.brackets.set(
|
|
3849
|
+
this.brackets.set(sym, bracket);
|
|
3709
3850
|
}
|
|
3710
3851
|
async _syncEquityAndRisk() {
|
|
3711
3852
|
const acct = await this.broker.getAccount?.().catch(() => null);
|
|
@@ -3718,6 +3859,11 @@ var TradingSession = class _TradingSession {
|
|
|
3718
3859
|
} else {
|
|
3719
3860
|
this.riskManager.update({ timeMs: Date.now(), equity: this.equity });
|
|
3720
3861
|
}
|
|
3862
|
+
const nowHalted = Boolean(this.riskManager.getState?.().halted);
|
|
3863
|
+
if (nowHalted && !this._wasHalted) {
|
|
3864
|
+
this._record("risk:halt", { reason: this.riskManager.haltReason ?? this.riskManager.getState?.().haltReason ?? "risk halt" });
|
|
3865
|
+
}
|
|
3866
|
+
this._wasHalted = nowHalted;
|
|
3721
3867
|
}
|
|
3722
3868
|
async closePosition(symbol = this.symbol) {
|
|
3723
3869
|
const positions = await this.broker.getPositions();
|
|
@@ -3768,6 +3914,7 @@ var TradingSession = class _TradingSession {
|
|
|
3768
3914
|
return {
|
|
3769
3915
|
id: this.id,
|
|
3770
3916
|
symbol: this.symbol,
|
|
3917
|
+
symbols: this.symbols,
|
|
3771
3918
|
interval: this.interval,
|
|
3772
3919
|
mode: this.mode,
|
|
3773
3920
|
running: this.running,
|
|
@@ -3853,6 +4000,49 @@ var SessionManager = class {
|
|
|
3853
4000
|
function createSessionManager(opts) {
|
|
3854
4001
|
return new SessionManager(opts);
|
|
3855
4002
|
}
|
|
4003
|
+
|
|
4004
|
+
// src/live/notify.js
|
|
4005
|
+
var DEFAULT_EVENTS = ["order:filled", "risk:halt"];
|
|
4006
|
+
function attachNotifier(session, { onEvent, webhookUrl, events = DEFAULT_EVENTS, drawdownPct = 0 } = {}) {
|
|
4007
|
+
const wanted = new Set(events);
|
|
4008
|
+
let peak = null;
|
|
4009
|
+
const deliver = async (event, payload) => {
|
|
4010
|
+
if (typeof onEvent === "function") {
|
|
4011
|
+
try {
|
|
4012
|
+
await onEvent({ event, payload });
|
|
4013
|
+
} catch {
|
|
4014
|
+
}
|
|
4015
|
+
}
|
|
4016
|
+
if (webhookUrl && typeof fetch === "function") {
|
|
4017
|
+
try {
|
|
4018
|
+
await fetch(webhookUrl, {
|
|
4019
|
+
method: "POST",
|
|
4020
|
+
headers: { "content-type": "application/json" },
|
|
4021
|
+
body: JSON.stringify({ event, payload })
|
|
4022
|
+
});
|
|
4023
|
+
} catch {
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
4026
|
+
};
|
|
4027
|
+
const handler = ({ event, payload }) => {
|
|
4028
|
+
if (wanted.has(event)) {
|
|
4029
|
+
deliver(event, payload).catch(() => {
|
|
4030
|
+
});
|
|
4031
|
+
return;
|
|
4032
|
+
}
|
|
4033
|
+
if (drawdownPct > 0 && event === "equity:update") {
|
|
4034
|
+
const eq = payload?.equity;
|
|
4035
|
+
if (Number.isFinite(eq)) {
|
|
4036
|
+
if (peak === null || eq > peak) peak = eq;
|
|
4037
|
+
if (peak > 0 && (peak - eq) / peak * 100 >= drawdownPct) {
|
|
4038
|
+
deliver("drawdown:breach", { equity: eq, peak, drawdownPct: (peak - eq) / peak * 100 }).catch(() => {
|
|
4039
|
+
});
|
|
4040
|
+
}
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
};
|
|
4044
|
+
return session.eventBus.onAny(handler);
|
|
4045
|
+
}
|
|
3856
4046
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3857
4047
|
0 && (module.exports = {
|
|
3858
4048
|
AlpacaBroker,
|
|
@@ -3877,6 +4067,7 @@ function createSessionManager(opts) {
|
|
|
3877
4067
|
StateManager,
|
|
3878
4068
|
StorageProvider,
|
|
3879
4069
|
TradingSession,
|
|
4070
|
+
attachNotifier,
|
|
3880
4071
|
createAlpacaBroker,
|
|
3881
4072
|
createBinanceBroker,
|
|
3882
4073
|
createBrokerFeed,
|
package/docs/README.md
CHANGED
|
@@ -11,8 +11,8 @@ Use this page to choose the right guide. If you are new to tradelab, read the fi
|
|
|
11
11
|
## Reference
|
|
12
12
|
|
|
13
13
|
- [API reference](api-reference.md) - public exports by module.
|
|
14
|
-
- [Research tools](research.md) - Monte Carlo, deflated Sharpe, PBO, and
|
|
15
|
-
- [MCP server](mcp.md) - `tradelab-mcp` setup and tool
|
|
14
|
+
- [Research tools](research.md) - Monte Carlo, deflated Sharpe, PBO, CPCV, and the agent research loop.
|
|
15
|
+
- [MCP server](mcp.md) - `tradelab-mcp` setup and the 25-tool reference for AI agents.
|
|
16
16
|
- [Strategy examples](examples.md) - complete strategy patterns you can adapt.
|
|
17
17
|
|
|
18
18
|
## Common Paths
|
|
@@ -26,17 +26,19 @@ Use this page to choose the right guide. If you are new to tradelab, read the fi
|
|
|
26
26
|
| Test parameter stability | [Backtesting](backtest-engine.md#walk-forward-validation) |
|
|
27
27
|
| Run a local paper session | [Live trading](live-trading.md) |
|
|
28
28
|
| Connect an MCP client | [MCP server](mcp.md) |
|
|
29
|
+
| Let an AI agent research and trade | [MCP server](mcp.md) |
|
|
29
30
|
| Check exact function names | [API reference](api-reference.md) |
|
|
30
31
|
|
|
31
32
|
## Package Scope
|
|
32
33
|
|
|
33
|
-
tradelab is built for strategy research and operational dry-runs:
|
|
34
|
+
tradelab is built for strategy research and operational dry-runs, by humans and AI agents alike:
|
|
34
35
|
|
|
35
36
|
- candle and tick backtests
|
|
36
37
|
- shared-capital portfolio simulation
|
|
37
38
|
- realistic cost assumptions
|
|
38
39
|
- walk-forward validation and overfitting checks
|
|
39
|
-
- paper and live execution through broker adapters
|
|
40
|
+
- single and multi-symbol paper and live execution through broker adapters
|
|
41
|
+
- a 25-tool MCP server so an agent can research, validate, and trade end to end
|
|
40
42
|
- local reports and machine-readable exports
|
|
41
43
|
|
|
42
44
|
It is not an exchange simulator. It does not try to model full market depth, queue priority, latency, or venue-specific microstructure.
|
package/docs/api-reference.md
CHANGED
|
@@ -108,6 +108,7 @@ registerStrategy("my-strategy", {
|
|
|
108
108
|
| `exportTradesCsv(trades, options)` | Write a trade or position CSV ledger |
|
|
109
109
|
| `exportMetricsJSON(options)` | Write machine-readable metrics JSON |
|
|
110
110
|
| `exportBacktestArtifacts(options)` | Write HTML, CSV, and JSON artifacts together |
|
|
111
|
+
| `summarize(metrics, options)` | Render metrics into one plain-English paragraph |
|
|
111
112
|
|
|
112
113
|
### Research
|
|
113
114
|
|
|
@@ -127,6 +128,8 @@ import { research } from "tradelab";
|
|
|
127
128
|
| `research.normalPpf(p)` | Standard normal inverse CDF |
|
|
128
129
|
| `research.moments(values)` | Mean, standard deviation, skew, kurtosis |
|
|
129
130
|
|
|
131
|
+
For the agent research loop, `createResearchStore({ dir })` returns a file-backed `{ open, log, recall, close }` store for tracking strategy hypotheses, their metrics, and overfitting verdicts across runs. The MCP `research_*` tools wrap it.
|
|
132
|
+
|
|
130
133
|
### Indicators And Helpers
|
|
131
134
|
|
|
132
135
|
| Export | Summary |
|
|
@@ -184,6 +187,17 @@ import { LiveEngine, PaperEngine, createDashboardServer } from "tradelab/live";
|
|
|
184
187
|
| `PaperEngine` | In-process broker simulator |
|
|
185
188
|
| `createPaperEngine(options)` | Factory for `PaperEngine` |
|
|
186
189
|
|
|
190
|
+
### Sessions And Notifications
|
|
191
|
+
|
|
192
|
+
| Export | Summary |
|
|
193
|
+
| --------------------------------- | ----------------------------------------------------------- |
|
|
194
|
+
| `TradingSession` | One account, one or many symbols, with risk-sized brackets |
|
|
195
|
+
| `SessionManager` | Create and track sessions; gates live mode |
|
|
196
|
+
| `createSessionManager(options)` | Factory for `SessionManager` |
|
|
197
|
+
| `attachNotifier(session, options)`| Fire a callback or webhook on fills, risk halts, drawdown |
|
|
198
|
+
|
|
199
|
+
`SessionManager.create({ symbols: ["BTC", "ETH"] })` opens a multi-symbol portfolio session; pass a single `symbol` for the original behavior. Per-symbol calls take a `symbol` argument (`pushBar(bar, symbol)`, `placeOrder({ symbol })`). Portfolio-level `RiskManager` options `maxGrossExposurePct` and `maxNetExposurePct` (default 0, off) reject orders that would breach the cap.
|
|
200
|
+
|
|
187
201
|
### Broker Adapters
|
|
188
202
|
|
|
189
203
|
| Export | Summary |
|
|
@@ -287,7 +301,7 @@ import { createServer, startStdioServer } from "tradelab/mcp";
|
|
|
287
301
|
| `createServer()` | Build an MCP server with tradelab tools |
|
|
288
302
|
| `startStdioServer()` | Start the MCP server on stdio |
|
|
289
303
|
|
|
290
|
-
See [mcp.md](mcp.md) for client configuration and tool payload examples.
|
|
304
|
+
The server exposes 25 tools: 8 research, 4 research-loop (`research_open`, `research_log`, `research_recall`, `research_close`), and 13 live-trading (sessions, orders, brackets, kill-switch). See [mcp.md](mcp.md) for client configuration and tool payload examples.
|
|
291
305
|
|
|
292
306
|
## Types
|
|
293
307
|
|
|
@@ -198,6 +198,15 @@ tradelab walk-forward \
|
|
|
198
198
|
|
|
199
199
|
You can pass `--strategy ./strategy.mjs` for local modules that export `signalFactory(params, args)` and either `parameterSets` or `createParameterSets(args)`.
|
|
200
200
|
|
|
201
|
+
### Run a Preset
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
tradelab run ema-cross --source yahoo --symbol SPY --period 1y
|
|
205
|
+
tradelab run rsi-reversion --source csv --csvPath ./btc.csv --params '{"period":14,"oversold":25}'
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`tradelab run <preset>` backtests a named built-in strategy and prints a plain-English summary of the result. Use `--params` to override the preset defaults. Run `tradelab run` with an unknown name to see the available presets.
|
|
209
|
+
|
|
201
210
|
### Live and Paper
|
|
202
211
|
|
|
203
212
|
```bash
|